@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.
@@ -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.33",
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
+ }