@orsetra/shared-ui 1.1.8 → 1.1.10

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.
Files changed (32) hide show
  1. package/components/extends/ArrayItemGroup/index.tsx +56 -0
  2. package/components/extends/CPUNumber/index.tsx +33 -0
  3. package/components/extends/ClassStorageSelect/index.tsx +11 -0
  4. package/components/extends/ClusterSelect/index.tsx +11 -0
  5. package/components/extends/ComponentSelect/index.tsx +112 -0
  6. package/components/extends/DiskNumber/index.tsx +34 -0
  7. package/components/extends/EnvSelect/index.tsx +11 -0
  8. package/components/extends/Group/index.tsx +150 -0
  9. package/components/extends/Ignore/index.tsx +11 -0
  10. package/components/extends/ImageInput/index.less +49 -0
  11. package/components/extends/ImageInput/index.tsx +193 -0
  12. package/components/extends/ImageSecretSelect/index.tsx +168 -0
  13. package/components/extends/KV/index.tsx +23 -0
  14. package/components/extends/MemoryNumber/index.tsx +34 -0
  15. package/components/extends/Numbers/index.tsx +23 -0
  16. package/components/extends/PVCSelect/index.tsx +12 -0
  17. package/components/extends/PolicySelect/index.tsx +53 -0
  18. package/components/extends/SecretKeySelect/index.tsx +95 -0
  19. package/components/extends/SecretSelect/index.tsx +76 -0
  20. package/components/extends/StepSelect/index.tsx +74 -0
  21. package/components/extends/Strings/index.tsx +23 -0
  22. package/components/extends/Switch/index.tsx +23 -0
  23. package/components/extends/index.ts +21 -0
  24. package/components/ui/combobox.tsx +137 -0
  25. package/components/ui/index.ts +3 -0
  26. package/components/ui/kv-input.tsx +117 -0
  27. package/components/ui/multi-select.tsx +151 -0
  28. package/components/ui/numbers-input.tsx +87 -0
  29. package/components/ui/project-selector-modal.tsx +95 -37
  30. package/components/ui/strings-input.tsx +83 -0
  31. package/index.ts +12 -0
  32. package/package.json +2 -2
@@ -0,0 +1,137 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, ChevronDown } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+ import { Button } from "./button"
7
+ import {
8
+ Command,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ } from "./command"
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from "./popover"
19
+
20
+ export interface ComboboxOption {
21
+ label: string
22
+ value: string
23
+ }
24
+
25
+ interface ComboboxProps {
26
+ placeholder?: string
27
+ value?: string
28
+ defaultValue?: string
29
+ onChange?: (value: string) => void
30
+ dataSource?: ComboboxOption[]
31
+ disabled?: boolean
32
+ id?: string
33
+ locale?: any
34
+ enableInput?: boolean
35
+ }
36
+
37
+ export function Combobox({
38
+ placeholder = "Select...",
39
+ value = "",
40
+ defaultValue = "",
41
+ onChange,
42
+ dataSource = [],
43
+ disabled = false,
44
+ id,
45
+ enableInput = false,
46
+ }: ComboboxProps) {
47
+ const [open, setOpen] = React.useState(false)
48
+ const [selected, setSelected] = React.useState<string>(value || defaultValue)
49
+ const [inputValue, setInputValue] = React.useState<string>(value || defaultValue)
50
+
51
+ React.useEffect(() => {
52
+ if (value !== undefined) {
53
+ setSelected(value)
54
+ setInputValue(value)
55
+ }
56
+ }, [value])
57
+
58
+ const handleSelect = (currentValue: string) => {
59
+ const newValue = currentValue === selected ? "" : currentValue
60
+ setSelected(newValue)
61
+ setInputValue(newValue)
62
+ onChange?.(newValue)
63
+ setOpen(false)
64
+ }
65
+
66
+ const handleInputChange = (newValue: string) => {
67
+ setInputValue(newValue)
68
+ if (enableInput) {
69
+ setSelected(newValue)
70
+ onChange?.(newValue)
71
+ }
72
+ }
73
+
74
+ const getLabel = (value: string) => {
75
+ return dataSource.find((option) => option.value === value)?.label || value
76
+ }
77
+
78
+ const displayValue = selected ? getLabel(selected) : placeholder
79
+
80
+ return (
81
+ <Popover open={open} onOpenChange={setOpen}>
82
+ <PopoverTrigger asChild>
83
+ <Button
84
+ id={id}
85
+ variant="secondary"
86
+ role="combobox"
87
+ aria-expanded={open}
88
+ disabled={disabled}
89
+ className={cn(
90
+ "w-full justify-between h-10",
91
+ disabled && "opacity-50 cursor-not-allowed",
92
+ !selected && "text-gray-500"
93
+ )}
94
+ style={{ borderRadius: 0 }}
95
+ >
96
+ <span className="truncate">{displayValue}</span>
97
+ <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
98
+ </Button>
99
+ </PopoverTrigger>
100
+ <PopoverContent className="w-full p-0" align="start" style={{ borderRadius: 0 }}>
101
+ <Command>
102
+ <CommandInput
103
+ placeholder={placeholder}
104
+ value={inputValue}
105
+ onValueChange={handleInputChange}
106
+ />
107
+ <CommandEmpty>
108
+ {enableInput && inputValue ? (
109
+ <div className="px-2 py-1.5 text-sm">
110
+ Press Enter to use "{inputValue}"
111
+ </div>
112
+ ) : (
113
+ "No results found."
114
+ )}
115
+ </CommandEmpty>
116
+ <CommandGroup className="max-h-64 overflow-auto">
117
+ {dataSource.map((option) => (
118
+ <CommandItem
119
+ key={option.value}
120
+ value={option.value}
121
+ onSelect={() => handleSelect(option.value)}
122
+ >
123
+ <Check
124
+ className={cn(
125
+ "mr-2 h-4 w-4",
126
+ selected === option.value ? "opacity-100" : "opacity-0"
127
+ )}
128
+ />
129
+ {option.label}
130
+ </CommandItem>
131
+ ))}
132
+ </CommandGroup>
133
+ </Command>
134
+ </PopoverContent>
135
+ </Popover>
136
+ )
137
+ }
@@ -61,3 +61,6 @@ export { Textarea } from './textarea'
61
61
  export { Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from './toast'
62
62
  export { ToggleGroup, ToggleGroupItem } from './toggle-group'
63
63
  export { Toggle } from './toggle'
64
+ export { StringsInput } from './strings-input'
65
+ export { KVInput } from './kv-input'
66
+ export { NumbersInput } from './numbers-input'
@@ -0,0 +1,117 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Button } from "./button"
6
+ import { X, Plus } from "lucide-react"
7
+
8
+ interface KVPair {
9
+ key: string
10
+ value: string
11
+ }
12
+
13
+ interface KVInputProps {
14
+ value?: Record<string, string>
15
+ onChange?: (value: Record<string, string>) => void
16
+ disabled?: boolean
17
+ keyPlaceholder?: string
18
+ valuePlaceholder?: string
19
+ }
20
+
21
+ export function KVInput({
22
+ value = {},
23
+ onChange,
24
+ disabled = false,
25
+ keyPlaceholder = "Key",
26
+ valuePlaceholder = "Value",
27
+ }: KVInputProps) {
28
+ const [pairs, setPairs] = useState<KVPair[]>(() => {
29
+ const entries = Object.entries(value)
30
+ return entries.length > 0 ? entries.map(([key, value]) => ({ key, value })) : [{ key: "", value: "" }]
31
+ })
32
+
33
+ useEffect(() => {
34
+ if (value && Object.keys(value).length > 0) {
35
+ const newPairs = Object.entries(value).map(([key, val]) => ({ key, value: val }))
36
+ if (JSON.stringify(newPairs) !== JSON.stringify(pairs)) {
37
+ setPairs(newPairs)
38
+ }
39
+ }
40
+ }, [value])
41
+
42
+ const handleChange = (index: number, field: "key" | "value", newValue: string) => {
43
+ const newPairs = [...pairs]
44
+ newPairs[index][field] = newValue
45
+ setPairs(newPairs)
46
+
47
+ const result: Record<string, string> = {}
48
+ newPairs.forEach(pair => {
49
+ if (pair.key) {
50
+ result[pair.key] = pair.value
51
+ }
52
+ })
53
+ onChange?.(result)
54
+ }
55
+
56
+ const handleAdd = () => {
57
+ setPairs([...pairs, { key: "", value: "" }])
58
+ }
59
+
60
+ const handleRemove = (index: number) => {
61
+ const newPairs = pairs.filter((_, i) => i !== index)
62
+ const finalPairs = newPairs.length > 0 ? newPairs : [{ key: "", value: "" }]
63
+ setPairs(finalPairs)
64
+
65
+ const result: Record<string, string> = {}
66
+ finalPairs.forEach(pair => {
67
+ if (pair.key) {
68
+ result[pair.key] = pair.value
69
+ }
70
+ })
71
+ onChange?.(result)
72
+ }
73
+
74
+ return (
75
+ <div className="space-y-2">
76
+ {pairs.map((pair, index) => (
77
+ <div key={index} className="flex gap-2">
78
+ <Input
79
+ value={pair.key}
80
+ onChange={(e) => handleChange(index, "key", e.target.value)}
81
+ disabled={disabled}
82
+ placeholder={keyPlaceholder}
83
+ className="rounded-none flex-1"
84
+ />
85
+ <Input
86
+ value={pair.value}
87
+ onChange={(e) => handleChange(index, "value", e.target.value)}
88
+ disabled={disabled}
89
+ placeholder={valuePlaceholder}
90
+ className="rounded-none flex-1"
91
+ />
92
+ {pairs.length > 1 && (
93
+ <Button
94
+ type="button"
95
+ variant="secondary"
96
+ onClick={() => handleRemove(index)}
97
+ disabled={disabled}
98
+ className="rounded-none h-9 w-9 p-0"
99
+ >
100
+ <X className="h-4 w-4" />
101
+ </Button>
102
+ )}
103
+ </div>
104
+ ))}
105
+ <Button
106
+ type="button"
107
+ variant="secondary"
108
+ onClick={handleAdd}
109
+ disabled={disabled}
110
+ className="rounded-none"
111
+ >
112
+ <Plus className="h-4 w-4 mr-2" />
113
+ Add Item
114
+ </Button>
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,151 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, X, ChevronDown } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+ import { Badge } from "./badge"
7
+ import { Button } from "./button"
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ } from "./command"
15
+ import {
16
+ Popover,
17
+ PopoverContent,
18
+ PopoverTrigger,
19
+ } from "./popover"
20
+
21
+ export interface MultiSelectOption {
22
+ label: string
23
+ value: string
24
+ }
25
+
26
+ interface MultiSelectProps {
27
+ placeholder?: string
28
+ value?: string[]
29
+ defaultValue?: string[]
30
+ onChange?: (value: string[]) => void
31
+ dataSource?: MultiSelectOption[]
32
+ disabled?: boolean
33
+ id?: string
34
+ locale?: any
35
+ mode?: string
36
+ }
37
+
38
+ export function MultiSelect({
39
+ placeholder = "Select items...",
40
+ value = [],
41
+ defaultValue = [],
42
+ onChange,
43
+ dataSource = [],
44
+ disabled = false,
45
+ id,
46
+ }: MultiSelectProps) {
47
+ const [open, setOpen] = React.useState(false)
48
+ const [selected, setSelected] = React.useState<string[]>(value || defaultValue)
49
+
50
+ React.useEffect(() => {
51
+ if (value !== undefined) {
52
+ setSelected(value)
53
+ }
54
+ }, [value])
55
+
56
+ const handleSelect = (currentValue: string) => {
57
+ const newSelected = selected.includes(currentValue)
58
+ ? selected.filter((item) => item !== currentValue)
59
+ : [...selected, currentValue]
60
+
61
+ setSelected(newSelected)
62
+ onChange?.(newSelected)
63
+ }
64
+
65
+ const handleRemove = (valueToRemove: string, e: React.MouseEvent) => {
66
+ e.stopPropagation()
67
+ const newSelected = selected.filter((item) => item !== valueToRemove)
68
+ setSelected(newSelected)
69
+ onChange?.(newSelected)
70
+ }
71
+
72
+ const getLabel = (value: string) => {
73
+ return dataSource.find((option) => option.value === value)?.label || value
74
+ }
75
+
76
+ return (
77
+ <Popover open={open} onOpenChange={setOpen}>
78
+ <PopoverTrigger asChild>
79
+ <Button
80
+ id={id}
81
+ variant="secondary"
82
+ role="combobox"
83
+ aria-expanded={open}
84
+ disabled={disabled}
85
+ className={cn(
86
+ "w-full justify-between h-auto min-h-10",
87
+ disabled && "opacity-50 cursor-not-allowed"
88
+ )}
89
+ style={{ borderRadius: 0 }}
90
+ >
91
+ <div className="flex flex-wrap gap-1 flex-1">
92
+ {selected.length > 0 ? (
93
+ selected.map((item) => (
94
+ <Badge
95
+ key={item}
96
+ variant="secondary"
97
+ className="mr-1"
98
+ >
99
+ {getLabel(item)}
100
+ {!disabled && (
101
+ <button
102
+ className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
103
+ onKeyDown={(e) => {
104
+ if (e.key === "Enter") {
105
+ handleRemove(item, e as any)
106
+ }
107
+ }}
108
+ onMouseDown={(e) => {
109
+ e.preventDefault()
110
+ e.stopPropagation()
111
+ }}
112
+ onClick={(e) => handleRemove(item, e)}
113
+ >
114
+ <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
115
+ </button>
116
+ )}
117
+ </Badge>
118
+ ))
119
+ ) : (
120
+ <span className="text-gray-500">{placeholder}</span>
121
+ )}
122
+ </div>
123
+ <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
124
+ </Button>
125
+ </PopoverTrigger>
126
+ <PopoverContent className="w-full p-0" align="start" style={{ borderRadius: 0 }}>
127
+ <Command>
128
+ <CommandInput placeholder="Search..." />
129
+ <CommandEmpty>No results found.</CommandEmpty>
130
+ <CommandGroup className="max-h-64 overflow-auto">
131
+ {dataSource.map((option) => (
132
+ <CommandItem
133
+ key={option.value}
134
+ value={option.value}
135
+ onSelect={() => handleSelect(option.value)}
136
+ >
137
+ <Check
138
+ className={cn(
139
+ "mr-2 h-4 w-4",
140
+ selected.includes(option.value) ? "opacity-100" : "opacity-0"
141
+ )}
142
+ />
143
+ {option.label}
144
+ </CommandItem>
145
+ ))}
146
+ </CommandGroup>
147
+ </Command>
148
+ </PopoverContent>
149
+ </Popover>
150
+ )
151
+ }
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Button } from "./button"
6
+ import { X, Plus } from "lucide-react"
7
+
8
+ interface NumbersInputProps {
9
+ value?: number[]
10
+ onChange?: (value: number[]) => void
11
+ disabled?: boolean
12
+ placeholder?: string
13
+ }
14
+
15
+ export function NumbersInput({
16
+ value = [],
17
+ onChange,
18
+ disabled = false,
19
+ placeholder = "Enter number",
20
+ }: NumbersInputProps) {
21
+ const [items, setItems] = useState<number[]>(value.length > 0 ? value : [0])
22
+
23
+ useEffect(() => {
24
+ if (value && value.length > 0 && JSON.stringify(value) !== JSON.stringify(items)) {
25
+ setItems(value)
26
+ }
27
+ }, [value])
28
+
29
+ const handleChange = (index: number, newValue: string) => {
30
+ const numValue = newValue === "" ? 0 : Number(newValue)
31
+ const newItems = [...items]
32
+ newItems[index] = numValue
33
+ setItems(newItems)
34
+ onChange?.(newItems)
35
+ }
36
+
37
+ const handleAdd = () => {
38
+ const newItems = [...items, 0]
39
+ setItems(newItems)
40
+ onChange?.(newItems)
41
+ }
42
+
43
+ const handleRemove = (index: number) => {
44
+ const newItems = items.filter((_, i) => i !== index)
45
+ const finalItems = newItems.length > 0 ? newItems : [0]
46
+ setItems(finalItems)
47
+ onChange?.(finalItems)
48
+ }
49
+
50
+ return (
51
+ <div className="space-y-2">
52
+ {items.map((item, index) => (
53
+ <div key={index} className="flex gap-2">
54
+ <Input
55
+ type="number"
56
+ value={item}
57
+ onChange={(e) => handleChange(index, e.target.value)}
58
+ disabled={disabled}
59
+ placeholder={placeholder}
60
+ className="rounded-none flex-1"
61
+ />
62
+ {items.length > 1 && (
63
+ <Button
64
+ type="button"
65
+ variant="secondary"
66
+ onClick={() => handleRemove(index)}
67
+ disabled={disabled}
68
+ className="rounded-none h-9 w-9 p-0"
69
+ >
70
+ <X className="h-4 w-4" />
71
+ </Button>
72
+ )}
73
+ </div>
74
+ ))}
75
+ <Button
76
+ type="button"
77
+ variant="secondary"
78
+ onClick={handleAdd}
79
+ disabled={disabled}
80
+ className="rounded-none"
81
+ >
82
+ <Plus className="h-4 w-4 mr-2" />
83
+ Add Item
84
+ </Button>
85
+ </div>
86
+ )
87
+ }
@@ -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>