@orsetra/shared-ui 1.0.33 → 1.0.35
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/ui/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsib
|
|
|
33
33
|
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } from './command'
|
|
34
34
|
export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup } from './context-menu'
|
|
35
35
|
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from './dialog'
|
|
36
|
+
export { ProjectSelectorModal, type Project, type ProjectSelectorModalProps } from './project-selector-modal'
|
|
36
37
|
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription } from './drawer'
|
|
37
38
|
export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from './form'
|
|
38
39
|
export { HoverCard, HoverCardTrigger, HoverCardContent } from './hover-card'
|
|
@@ -48,6 +49,7 @@ export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './resizabl
|
|
|
48
49
|
export { ScrollArea, ScrollBar } from './scroll-area'
|
|
49
50
|
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator } from './select'
|
|
50
51
|
export { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from './sheet'
|
|
52
|
+
export { SidePanel, SidePanelTrigger, SidePanelClose, SidePanelContent, SidePanelHeader, SidePanelTitle, SidePanelDescription, SidePanelBody, SidePanelFooter } from './side-panel'
|
|
51
53
|
export { Skeleton } from './skeleton'
|
|
52
54
|
export { Slider } from './slider'
|
|
53
55
|
export { Toaster } from './toaster'
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "./dialog"
|
|
11
|
+
import { Button } from "./button"
|
|
12
|
+
import { Label } from "./label"
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "./select"
|
|
20
|
+
import { Loader2, FolderKanban } from "lucide-react"
|
|
21
|
+
|
|
22
|
+
export interface Project {
|
|
23
|
+
name: string
|
|
24
|
+
alias?: string
|
|
25
|
+
description?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProjectSelectorModalProps {
|
|
29
|
+
open: boolean
|
|
30
|
+
onOpenChange?: (open: boolean) => void
|
|
31
|
+
getProjects: () => Promise<Project[]>
|
|
32
|
+
storage: {
|
|
33
|
+
getItem: (key: string) => string | null
|
|
34
|
+
setItem: (key: string, value: string) => void
|
|
35
|
+
}
|
|
36
|
+
storageKey?: string
|
|
37
|
+
doInit: () => void | Promise<void>
|
|
38
|
+
title?: string
|
|
39
|
+
description?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ProjectSelectorModal({
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
getProjects,
|
|
46
|
+
storage,
|
|
47
|
+
storageKey = "current-project",
|
|
48
|
+
doInit,
|
|
49
|
+
title = "Select Project",
|
|
50
|
+
description = "Choose a project to continue. This will be used for all operations in this application.",
|
|
51
|
+
}: ProjectSelectorModalProps) {
|
|
52
|
+
const [projects, setProjects] = React.useState<Project[]>([])
|
|
53
|
+
const [loading, setLoading] = React.useState(true)
|
|
54
|
+
const [selectedProject, setSelectedProject] = React.useState<string>("")
|
|
55
|
+
const [submitting, setSubmitting] = React.useState(false)
|
|
56
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
57
|
+
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if (open) {
|
|
60
|
+
loadProjects()
|
|
61
|
+
// Try to load current project from storage
|
|
62
|
+
const currentProject = storage.getItem(storageKey)
|
|
63
|
+
if (currentProject) {
|
|
64
|
+
setSelectedProject(currentProject)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, [open, storage, storageKey])
|
|
68
|
+
|
|
69
|
+
const loadProjects = async () => {
|
|
70
|
+
setLoading(true)
|
|
71
|
+
setError(null)
|
|
72
|
+
try {
|
|
73
|
+
const projectsList = await getProjects()
|
|
74
|
+
setProjects(projectsList)
|
|
75
|
+
|
|
76
|
+
// If no project selected yet and we have projects, select the first one
|
|
77
|
+
if (!selectedProject && projectsList.length > 0) {
|
|
78
|
+
setSelectedProject(projectsList[0].name)
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error("Failed to load projects:", err)
|
|
82
|
+
setError(err instanceof Error ? err.message : "Failed to load projects")
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
|
|
91
|
+
if (!selectedProject) {
|
|
92
|
+
setError("Please select a project")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSubmitting(true)
|
|
97
|
+
setError(null)
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Save to storage
|
|
101
|
+
storage.setItem(storageKey, selectedProject)
|
|
102
|
+
|
|
103
|
+
// Call doInit
|
|
104
|
+
await Promise.resolve(doInit())
|
|
105
|
+
|
|
106
|
+
// Close modal
|
|
107
|
+
onOpenChange?.(false)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error("Failed to initialize:", err)
|
|
110
|
+
setError(err instanceof Error ? err.message : "Failed to initialize")
|
|
111
|
+
setSubmitting(false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
117
|
+
<DialogContent className="rounded-none sm:max-w-md" onInteractOutside={(e) => e.preventDefault()}>
|
|
118
|
+
<DialogHeader>
|
|
119
|
+
<DialogTitle className="flex items-center gap-2">
|
|
120
|
+
<FolderKanban className="h-5 w-5 text-ibm-blue-60" />
|
|
121
|
+
{title}
|
|
122
|
+
</DialogTitle>
|
|
123
|
+
<DialogDescription>{description}</DialogDescription>
|
|
124
|
+
</DialogHeader>
|
|
125
|
+
|
|
126
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
127
|
+
{loading ? (
|
|
128
|
+
<div className="flex items-center justify-center py-8">
|
|
129
|
+
<Loader2 className="h-6 w-6 animate-spin text-ibm-blue-60" />
|
|
130
|
+
<span className="ml-2 text-sm text-ibm-gray-70">Loading projects...</span>
|
|
131
|
+
</div>
|
|
132
|
+
) : error ? (
|
|
133
|
+
<div className="rounded-none border border-red-200 bg-red-50 p-3 text-sm text-red-800">
|
|
134
|
+
{error}
|
|
135
|
+
</div>
|
|
136
|
+
) : projects.length === 0 ? (
|
|
137
|
+
<div className="text-center py-8 text-sm text-ibm-gray-60">
|
|
138
|
+
No projects available
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<Label htmlFor="project">
|
|
143
|
+
Project <span className="text-red-600">*</span>
|
|
144
|
+
</Label>
|
|
145
|
+
<Select value={selectedProject} onValueChange={setSelectedProject}>
|
|
146
|
+
<SelectTrigger id="project" className="rounded-none">
|
|
147
|
+
<SelectValue placeholder="Select a project" />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent className="rounded-none">
|
|
150
|
+
{projects.map((project) => (
|
|
151
|
+
<SelectItem key={project.name} value={project.name} className="rounded-none">
|
|
152
|
+
<div className="flex flex-col">
|
|
153
|
+
<span className="font-medium">{project.alias || project.name}</span>
|
|
154
|
+
{project.description && (
|
|
155
|
+
<span className="text-xs text-ibm-gray-60">{project.description}</span>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</SelectItem>
|
|
159
|
+
))}
|
|
160
|
+
</SelectContent>
|
|
161
|
+
</Select>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
166
|
+
<Button
|
|
167
|
+
type="submit"
|
|
168
|
+
className="rounded-none"
|
|
169
|
+
disabled={submitting || loading || !selectedProject || projects.length === 0}
|
|
170
|
+
>
|
|
171
|
+
{submitting ? (
|
|
172
|
+
<>
|
|
173
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
174
|
+
Initializing...
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
"Continue"
|
|
178
|
+
)}
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</form>
|
|
182
|
+
</DialogContent>
|
|
183
|
+
</Dialog>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
5
|
+
import { X } from "lucide-react"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
const SidePanel = DialogPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const SidePanelTrigger = DialogPrimitive.Trigger
|
|
11
|
+
|
|
12
|
+
const SidePanelClose = DialogPrimitive.Close
|
|
13
|
+
|
|
14
|
+
const SidePanelPortal = DialogPrimitive.Portal
|
|
15
|
+
|
|
16
|
+
const SidePanelOverlay = React.forwardRef<
|
|
17
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
18
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
19
|
+
>(({ className, ...props }, ref) => (
|
|
20
|
+
<DialogPrimitive.Overlay
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn(
|
|
23
|
+
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
))
|
|
29
|
+
SidePanelOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
30
|
+
|
|
31
|
+
interface SidePanelContentProps
|
|
32
|
+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
33
|
+
side?: "left" | "right"
|
|
34
|
+
width?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SidePanelContent = React.forwardRef<
|
|
38
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
39
|
+
SidePanelContentProps
|
|
40
|
+
>(({ side = "right", width = "max-w-lg", className, children, ...props }, ref) => (
|
|
41
|
+
<SidePanelPortal>
|
|
42
|
+
<SidePanelOverlay />
|
|
43
|
+
<DialogPrimitive.Content
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
"fixed z-50 gap-4 bg-white shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
47
|
+
side === "right" &&
|
|
48
|
+
"inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
49
|
+
side === "left" &&
|
|
50
|
+
"inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
|
|
51
|
+
width,
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</DialogPrimitive.Content>
|
|
58
|
+
</SidePanelPortal>
|
|
59
|
+
))
|
|
60
|
+
SidePanelContent.displayName = DialogPrimitive.Content.displayName
|
|
61
|
+
|
|
62
|
+
interface SidePanelHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
63
|
+
onClose?: () => void
|
|
64
|
+
showCloseButton?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SidePanelHeader = React.forwardRef<HTMLDivElement, SidePanelHeaderProps>(
|
|
68
|
+
({ className, children, onClose, showCloseButton = true, ...props }, ref) => (
|
|
69
|
+
<div
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn(
|
|
72
|
+
"flex items-center justify-between border-b border-ibm-gray-20 px-6 py-4",
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
<div className="flex-1">{children}</div>
|
|
78
|
+
{showCloseButton && (
|
|
79
|
+
<DialogPrimitive.Close
|
|
80
|
+
className="rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ibm-blue-60 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-ibm-gray-10"
|
|
81
|
+
onClick={onClose}
|
|
82
|
+
>
|
|
83
|
+
<X className="h-5 w-5 text-ibm-gray-70" />
|
|
84
|
+
<span className="sr-only">Close</span>
|
|
85
|
+
</DialogPrimitive.Close>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
SidePanelHeader.displayName = "SidePanelHeader"
|
|
91
|
+
|
|
92
|
+
const SidePanelTitle = React.forwardRef<
|
|
93
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
94
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
95
|
+
>(({ className, ...props }, ref) => (
|
|
96
|
+
<DialogPrimitive.Title
|
|
97
|
+
ref={ref}
|
|
98
|
+
className={cn("text-lg font-semibold text-ibm-gray-100", className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
))
|
|
102
|
+
SidePanelTitle.displayName = DialogPrimitive.Title.displayName
|
|
103
|
+
|
|
104
|
+
const SidePanelDescription = React.forwardRef<
|
|
105
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
106
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
107
|
+
>(({ className, ...props }, ref) => (
|
|
108
|
+
<DialogPrimitive.Description
|
|
109
|
+
ref={ref}
|
|
110
|
+
className={cn("text-sm text-ibm-gray-60", className)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
))
|
|
114
|
+
SidePanelDescription.displayName = DialogPrimitive.Description.displayName
|
|
115
|
+
|
|
116
|
+
const SidePanelBody = React.forwardRef<
|
|
117
|
+
HTMLDivElement,
|
|
118
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
119
|
+
>(({ className, ...props }, ref) => (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cn("flex-1 overflow-y-auto px-6 py-6", className)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
))
|
|
126
|
+
SidePanelBody.displayName = "SidePanelBody"
|
|
127
|
+
|
|
128
|
+
const SidePanelFooter = React.forwardRef<
|
|
129
|
+
HTMLDivElement,
|
|
130
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
131
|
+
>(({ className, ...props }, ref) => (
|
|
132
|
+
<div
|
|
133
|
+
ref={ref}
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex items-center justify-end gap-3 border-t border-ibm-gray-20 px-6 py-4",
|
|
136
|
+
className
|
|
137
|
+
)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
))
|
|
141
|
+
SidePanelFooter.displayName = "SidePanelFooter"
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
SidePanel,
|
|
145
|
+
SidePanelTrigger,
|
|
146
|
+
SidePanelClose,
|
|
147
|
+
SidePanelContent,
|
|
148
|
+
SidePanelHeader,
|
|
149
|
+
SidePanelTitle,
|
|
150
|
+
SidePanelDescription,
|
|
151
|
+
SidePanelBody,
|
|
152
|
+
SidePanelFooter,
|
|
153
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orsetra/shared-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
4
|
"description": "Shared UI components for Orsetra platform",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -93,4 +93,4 @@
|
|
|
93
93
|
"next": "^16.0.7",
|
|
94
94
|
"typescript": "^5"
|
|
95
95
|
}
|
|
96
|
-
}
|
|
96
|
+
}
|