@orsetra/shared-ui 1.1.7 → 1.1.9

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.
@@ -3,6 +3,7 @@
3
3
  import { ReactNode, useState } from "react"
4
4
  import { X, ChevronLeft } from "lucide-react"
5
5
  import { Button } from "../ui/button"
6
+ import { cn } from "../../lib/utils"
6
7
 
7
8
  interface PageWithSidePanelProps {
8
9
  children: ReactNode
@@ -15,6 +16,9 @@ interface PageWithSidePanelProps {
15
16
  showBorder?: boolean
16
17
  onClose?: () => void
17
18
  onOpen?: () => void
19
+ contentHeaderClassName?: string
20
+ sidePanelClassName?: string
21
+ sidePanelHeaderClassName?: string
18
22
  }
19
23
 
20
24
  const PANEL_WIDTHS = {
@@ -34,6 +38,9 @@ export function PageWithSidePanel({
34
38
  showBorder = true,
35
39
  onClose,
36
40
  onOpen,
41
+ contentHeaderClassName,
42
+ sidePanelClassName,
43
+ sidePanelHeaderClassName,
37
44
  }: PageWithSidePanelProps) {
38
45
  const [isOpen, setIsOpen] = useState(defaultOpen)
39
46
  const panelWidthClass = PANEL_WIDTHS[sidePanelWidth]
@@ -50,13 +57,15 @@ export function PageWithSidePanel({
50
57
 
51
58
  return (
52
59
  <div className="flex items-start">
53
-
54
60
  {/* Main content column — grows to fill space left by the panel */}
55
61
  <div className="flex-1 min-w-0 flex flex-col">
56
62
 
57
63
  {/* Content header — sticks at the top of the scroll container */}
58
64
  {contentHeader && (
59
- <div className="flex sticky top-0 z-10 bg-white h-14 flex-shrink-0 items-center justify-between px-4 border-b border-ibm-gray-20">
65
+ <div className={cn(
66
+ "flex sticky top-0 z-10 bg-white h-14 flex-shrink-0 items-center justify-between px-4 border-b border-ibm-gray-20",
67
+ contentHeaderClassName
68
+ )}>
60
69
  {contentHeader}
61
70
  </div>
62
71
  )}
@@ -73,16 +82,22 @@ export function PageWithSidePanel({
73
82
  {sidePanel && (
74
83
  <>
75
84
  <div
76
- className={`hidden lg:flex flex-col flex-shrink-0 sticky top-0 self-start h-[calc(100vh-3.5rem)] overflow-hidden transition-[width] duration-300 ease-in-out ${
77
- isOpen && showBorder ? "border-l border-ibm-gray-40" : ""
78
- } ${isOpen ? panelWidthClass : "w-0"}`}
85
+ className={cn(
86
+ "hidden lg:flex flex-col flex-shrink-0 sticky top-0 self-start h-[calc(100vh-3.5rem)] overflow-hidden transition-[width] duration-300 ease-in-out",
87
+ isOpen && showBorder && "border-l border-ibm-gray-40",
88
+ isOpen ? panelWidthClass : "w-0",
89
+ sidePanelClassName
90
+ )}
79
91
  >
80
92
  {/* Inner wrapper at fixed width to prevent content reflow during animation */}
81
93
  <div className={`flex flex-col h-full bg-white ${panelWidthClass}`}>
82
94
 
83
95
  {/* Panel header */}
84
96
  {(sidePanelHeader || closable) && (
85
- <div className="flex items-center justify-between h-14 flex-shrink-0 px-4 border-b border-ibm-gray-20">
97
+ <div className={cn(
98
+ "flex items-center justify-between h-14 flex-shrink-0 px-4 border-b border-ibm-gray-20",
99
+ sidePanelHeaderClassName
100
+ )}>
86
101
  <div className="flex-1 min-w-0">
87
102
  {sidePanelHeader}
88
103
  </div>
@@ -10,6 +10,7 @@ import {
10
10
  } from "./dialog"
11
11
  import { Button } from "./button"
12
12
  import { Label } from "./label"
13
+ import { Input } from "./input"
13
14
  import {
14
15
  Select,
15
16
  SelectContent,
@@ -29,11 +30,12 @@ export interface ProjectSelectorModalProps {
29
30
  open: boolean
30
31
  onOpenChange?: (open: boolean) => void
31
32
  getProjects: () => Promise<Project[]>
33
+ createProject: (name: string) => Promise<Project>
32
34
  storage: {
33
35
  getItem: (key: string) => string | null
34
36
  setItem: (key: string, value: string) => void
35
37
  }
36
- storageKey?: string
38
+ storageKey: string
37
39
  doInit: () => void | Promise<void>
38
40
  title?: string
39
41
  description?: string
@@ -43,15 +45,17 @@ export function ProjectSelectorModal({
43
45
  open,
44
46
  onOpenChange,
45
47
  getProjects,
48
+ createProject,
46
49
  storage,
47
- storageKey = "current-project",
50
+ storageKey,
48
51
  doInit,
49
- title = "Select Project",
50
- description = "Choose a project to continue. This will be used for all operations in this application.",
52
+ title = "Select Business Unit",
53
+ description = "Choose a business unit to continue. This will be used for all operations in this application.",
51
54
  }: ProjectSelectorModalProps) {
52
55
  const [projects, setProjects] = React.useState<Project[]>([])
53
56
  const [loading, setLoading] = React.useState(true)
54
57
  const [selectedProject, setSelectedProject] = React.useState<string>("")
58
+ const [newProjectName, setNewProjectName] = React.useState<string>("")
55
59
  const [submitting, setSubmitting] = React.useState(false)
56
60
  const [error, setError] = React.useState<string | null>(null)
57
61
 
@@ -88,6 +92,36 @@ export function ProjectSelectorModal({
88
92
  const handleSubmit = async (e: React.FormEvent) => {
89
93
  e.preventDefault()
90
94
 
95
+ // If no projects exist, create a new one
96
+ if (projects.length === 0) {
97
+ if (!newProjectName.trim()) {
98
+ setError("Please enter a business unit name")
99
+ return
100
+ }
101
+
102
+ setSubmitting(true)
103
+ setError(null)
104
+
105
+ try {
106
+ // Create the new project
107
+ const newProject = await createProject(newProjectName.trim())
108
+
109
+ // Save to storage
110
+ storage.setItem(storageKey, newProject.name)
111
+
112
+ // Call doInit
113
+ await Promise.resolve(doInit())
114
+
115
+ // Close modal
116
+ onOpenChange?.(false)
117
+ } catch (err) {
118
+ console.error("Failed to create project:", err)
119
+ setError(err instanceof Error ? err.message : "Failed to create business unit")
120
+ setSubmitting(false)
121
+ }
122
+ return
123
+ }
124
+
91
125
  if (!selectedProject) {
92
126
  setError("Please select a project")
93
127
  return
@@ -127,54 +161,78 @@ export function ProjectSelectorModal({
127
161
  {loading ? (
128
162
  <div className="flex items-center justify-center py-8">
129
163
  <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}
164
+ <span className="ml-2 text-ibm-gray-70">Loading business units...</span>
135
165
  </div>
136
166
  ) : projects.length === 0 ? (
137
- <div className="text-center py-8 text-sm text-ibm-gray-60">
138
- No projects available
139
- </div>
167
+ <>
168
+ {error && (
169
+ <div className="rounded-none border border-red-200 bg-red-50 p-3 text-sm text-red-800">
170
+ {error}
171
+ </div>
172
+ )}
173
+ <div className="space-y-2">
174
+ <Label htmlFor="new-project">
175
+ Business Unit Name <span className="text-red-600">*</span>
176
+ </Label>
177
+ <Input
178
+ id="new-project"
179
+ type="text"
180
+ placeholder="Enter business unit name"
181
+ value={newProjectName}
182
+ onChange={(e) => setNewProjectName(e.target.value)}
183
+ className="rounded-none"
184
+ autoFocus
185
+ />
186
+ <p className="text-xs text-ibm-gray-60">
187
+ No business units found. Create your first one to continue.
188
+ </p>
189
+ </div>
190
+ </>
140
191
  ) : (
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>
192
+ <>
193
+ {error && (
194
+ <div className="rounded-none border border-red-200 bg-red-50 p-3 text-sm text-red-800">
195
+ {error}
196
+ </div>
197
+ )}
198
+ <div className="space-y-2">
199
+ <Label htmlFor="project">
200
+ Business Unit <span className="text-red-600">*</span>
201
+ </Label>
202
+ <Select value={selectedProject} onValueChange={setSelectedProject}>
203
+ <SelectTrigger id="project" className="rounded-none">
204
+ <SelectValue placeholder="Select a business unit" />
205
+ </SelectTrigger>
206
+ <SelectContent className="rounded-none">
207
+ {projects.map((project) => (
208
+ <SelectItem key={project.name} value={project.name} className="rounded-none">
209
+ <div className="flex flex-col">
210
+ <span className="font-medium">{project.alias || project.name}</span>
211
+ {project.description && (
212
+ <span className="text-xs text-ibm-gray-60">{project.description}</span>
213
+ )}
214
+ </div>
215
+ </SelectItem>
216
+ ))}
217
+ </SelectContent>
218
+ </Select>
219
+ </div>
220
+ </>
163
221
  )}
164
222
 
165
223
  <div className="flex items-center justify-end gap-3 pt-2">
166
224
  <Button
167
225
  type="submit"
168
226
  className="rounded-none"
169
- disabled={submitting || loading || !selectedProject || projects.length === 0}
227
+ disabled={submitting || loading || (projects.length === 0 ? !newProjectName.trim() : !selectedProject)}
170
228
  >
171
229
  {submitting ? (
172
230
  <>
173
231
  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
174
- Initializing...
232
+ {projects.length === 0 ? "Creating..." : "Initializing..."}
175
233
  </>
176
234
  ) : (
177
- "Continue"
235
+ projects.length === 0 ? "Create & Continue" : "Continue"
178
236
  )}
179
237
  </Button>
180
238
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",