@orsetra/shared-ui 1.3.17 → 1.4.0

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.
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
18
18
  >(({ className, ...props }, ref) => (
19
19
  <AlertDialogPrimitive.Overlay
20
20
  className={cn(
21
- "fixed inset-0 z-50 bg-ibm-gray-100/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
21
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
22
  className
23
23
  )}
24
24
  {...props}
@@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
36
36
  <AlertDialogPrimitive.Content
37
37
  ref={ref}
38
38
  className={cn(
39
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
39
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
40
40
  className
41
41
  )}
42
42
  {...props}
@@ -103,7 +103,7 @@ const AlertDialogAction = React.forwardRef<
103
103
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104
104
  >(({ className, ...props }, ref) => (
105
105
  <AlertDialogPrimitive.Action asChild>
106
- <Button ref={ref} className={className} {...props} />
106
+ <Button ref={ref} className={cn("rounded-none", className)} {...props} />
107
107
  </AlertDialogPrimitive.Action>
108
108
  ))
109
109
  AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
@@ -113,7 +113,7 @@ const AlertDialogCancel = React.forwardRef<
113
113
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
114
  >(({ className, ...props }, ref) => (
115
115
  <AlertDialogPrimitive.Cancel asChild>
116
- <Button ref={ref} variant="secondary" className={cn("mt-2 sm:mt-0", className)} {...props} />
116
+ <Button ref={ref} variant="secondary" className={cn("rounded-none mt-2 sm:mt-0", className)} {...props} />
117
117
  </AlertDialogPrimitive.Cancel>
118
118
  ))
119
119
  AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
@@ -0,0 +1,42 @@
1
+ "use client"
2
+
3
+ import React from "react"
4
+
5
+ interface CodeEditorProps {
6
+ id?: string
7
+ value?: string
8
+ onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
9
+ onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void
10
+ placeholder?: string
11
+ disabled?: boolean
12
+ required?: boolean
13
+ className?: string
14
+ rows?: number
15
+ }
16
+
17
+ export function CodeEditor({
18
+ id,
19
+ value,
20
+ onChange,
21
+ onBlur,
22
+ placeholder,
23
+ disabled,
24
+ required,
25
+ className = "",
26
+ rows = 14,
27
+ }: CodeEditorProps) {
28
+ return (
29
+ <textarea
30
+ id={id}
31
+ value={value ?? ""}
32
+ onChange={onChange}
33
+ onBlur={onBlur}
34
+ placeholder={placeholder}
35
+ disabled={disabled}
36
+ required={required}
37
+ rows={rows}
38
+ spellCheck={false}
39
+ className={`w-full font-mono text-xs text-ibm-gray-80 bg-ibm-gray-10 border border-ibm-gray-30 px-3 py-2 resize-y focus:outline-none focus:ring-1 focus:ring-ibm-blue-60 placeholder:text-ibm-gray-40 disabled:opacity-50 ${className}`}
40
+ />
41
+ )
42
+ }
@@ -125,6 +125,7 @@ export function ConfirmationDialog({
125
125
  }
126
126
  disabled={loading}
127
127
  autoComplete="off"
128
+ className="rounded-none"
128
129
  />
129
130
  </div>
130
131
  )}
@@ -0,0 +1,95 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./dialog"
5
+ import { Loader2, Globe } from "lucide-react"
6
+
7
+ interface Environment {
8
+ name: string
9
+ alias?: string
10
+ }
11
+
12
+ export interface EnvironmentPickerDialogProps {
13
+ open: boolean
14
+ onOpenChange: (open: boolean) => void
15
+ projectName: string
16
+ fetchEnvironments: (projectName: string) => Promise<{ envs: Environment[] }>
17
+ onSelect: (envName: string) => void
18
+ excludeEnvs?: string[]
19
+ title?: string
20
+ description?: string
21
+ }
22
+
23
+ export function EnvironmentPickerDialog({
24
+ open,
25
+ onOpenChange,
26
+ projectName,
27
+ fetchEnvironments,
28
+ onSelect,
29
+ excludeEnvs = [],
30
+ title = "Select an environment",
31
+ description,
32
+ }: EnvironmentPickerDialogProps) {
33
+ const [environments, setEnvironments] = useState<Environment[]>([])
34
+ const [loading, setLoading] = useState(true)
35
+
36
+ useEffect(() => {
37
+ if (open) loadEnvironments()
38
+ }, [open])
39
+
40
+ const loadEnvironments = async () => {
41
+ setLoading(true)
42
+ try {
43
+ const response = await fetchEnvironments(projectName)
44
+ const all = response.envs || []
45
+ setEnvironments(excludeEnvs.length ? all.filter((e) => !excludeEnvs.includes(e.name)) : all)
46
+ } catch {
47
+ // silent
48
+ } finally {
49
+ setLoading(false)
50
+ }
51
+ }
52
+
53
+ return (
54
+ <Dialog open={open} onOpenChange={onOpenChange}>
55
+ <DialogContent className="rounded-none max-w-lg">
56
+ <DialogHeader>
57
+ <DialogTitle>{title}</DialogTitle>
58
+ {description && <DialogDescription>{description}</DialogDescription>}
59
+ </DialogHeader>
60
+
61
+ <div className="mt-4">
62
+ {loading ? (
63
+ <div className="flex items-center justify-center py-8">
64
+ <Loader2 className="h-5 w-5 animate-spin text-ibm-blue-60" />
65
+ <span className="ml-2 text-sm text-ibm-gray-70">Loading environments...</span>
66
+ </div>
67
+ ) : environments.length === 0 ? (
68
+ <div className="text-center py-8 text-sm text-ibm-gray-60">
69
+ No environments found
70
+ </div>
71
+ ) : (
72
+ <div className="border border-ibm-gray-20 max-h-60 overflow-y-auto">
73
+ {environments.map((env, index) => (
74
+ <button
75
+ key={env.name}
76
+ type="button"
77
+ className={`w-full text-left px-4 py-3 hover:bg-ibm-gray-10 transition-colors flex items-center gap-3 ${index !== environments.length - 1 ? "border-b border-ibm-gray-20" : ""}`}
78
+ onClick={() => {
79
+ onSelect(env.name)
80
+ onOpenChange(false)
81
+ }}
82
+ >
83
+ <Globe className="h-4 w-4 text-ibm-gray-70 flex-shrink-0" />
84
+ <span className="text-sm font-medium text-ibm-gray-100">
85
+ {env.alias || env.name}
86
+ </span>
87
+ </button>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+ </DialogContent>
93
+ </Dialog>
94
+ )
95
+ }
@@ -65,3 +65,12 @@ export { Toggle } from './toggle'
65
65
  export { StringsInput } from './strings-input'
66
66
  export { KVInput } from './kv-input'
67
67
  export { NumbersInput } from './numbers-input'
68
+ export { KVDynamicInput } from './kv-dynamic-input'
69
+ export { SelectInput } from './select-input'
70
+ export { SecretContextProvider, useSecretContext, type SecretEntry } from './secret-context'
71
+ export { SecretInput } from './secret-input'
72
+ export { SecretsInput } from './secrets-input'
73
+ export { StructsInput } from './structs-input'
74
+ export { CodeEditor } from './code-editor'
75
+ export { K8sObjectsCode } from './k8s-objects-code'
76
+ export { EnvironmentPickerDialog, type EnvironmentPickerDialogProps } from './environment-picker-dialog'
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import * as yaml from "js-yaml"
5
+
6
+ interface K8sObjectsCodeProps {
7
+ id?: string
8
+ value?: any[] | string
9
+ onChange?: (val: any[]) => void
10
+ disabled?: boolean
11
+ required?: boolean
12
+ className?: string
13
+ }
14
+
15
+ function toYamlStr(val: any[] | string | undefined): string {
16
+ if (!val) return ""
17
+ if (typeof val === "string") return val
18
+ try {
19
+ return yaml.dump(val, { lineWidth: -1, noRefs: true })
20
+ } catch {
21
+ return JSON.stringify(val, null, 2)
22
+ }
23
+ }
24
+
25
+ export function K8sObjectsCode({ id, value, onChange, disabled, required, className = "" }: K8sObjectsCodeProps) {
26
+ const [text, setText] = useState(() => toYamlStr(value))
27
+ const [parseError, setParseError] = useState<string | null>(null)
28
+
29
+ useEffect(() => {
30
+ setText(toYamlStr(value))
31
+ }, [value])
32
+
33
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
34
+ const raw = e.target.value
35
+ setText(raw)
36
+ if (!raw.trim()) {
37
+ setParseError(null)
38
+ onChange?.([])
39
+ return
40
+ }
41
+ try {
42
+ const docs = yaml.loadAll(raw) as any[]
43
+ const arr = docs.flat().filter(Boolean)
44
+ setParseError(null)
45
+ onChange?.(arr)
46
+ } catch (err: any) {
47
+ setParseError(err.message?.split("\n")[0] ?? "Invalid YAML")
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div className="space-y-1">
53
+ <textarea
54
+ id={id}
55
+ value={text}
56
+ onChange={handleChange}
57
+ disabled={disabled}
58
+ required={required}
59
+ rows={14}
60
+ spellCheck={false}
61
+ placeholder={"- from:\n uri: timer:tick\n steps:\n - to: log:hello"}
62
+ className={`w-full font-mono text-xs text-ibm-gray-80 bg-ibm-gray-10 border px-3 py-2 resize-y focus:outline-none focus:ring-1 focus:ring-ibm-blue-60 disabled:opacity-50 ${parseError ? "border-red-500" : "border-ibm-gray-30"} ${className}`}
63
+ />
64
+ {parseError && (
65
+ <p className="text-xs text-red-500 font-mono break-all">{parseError}</p>
66
+ )}
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,116 @@
1
+ "use client"
2
+
3
+ import { useState, useRef, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Plus, X } from "lucide-react"
6
+
7
+ interface KVDynamicInputProps {
8
+ value?: Record<string, string>
9
+ onChange?: (value: Record<string, string>) => void
10
+ disabled?: boolean
11
+ }
12
+
13
+ export function KVDynamicInput({ value = {}, onChange, disabled = false }: KVDynamicInputProps) {
14
+ const [pairs, setPairs] = useState<Record<string, string>>(value)
15
+ const [showAdd, setShowAdd] = useState(false)
16
+ const [draftKey, setDraftKey] = useState("")
17
+ const [draftValue, setDraftValue] = useState("")
18
+ const keyRef = useRef<HTMLInputElement>(null)
19
+ const skipSync = useRef(false)
20
+
21
+ useEffect(() => {
22
+ if (skipSync.current) { skipSync.current = false; return }
23
+ setPairs(value ?? {})
24
+ }, [value])
25
+
26
+ useEffect(() => {
27
+ if (showAdd) keyRef.current?.focus()
28
+ }, [showAdd])
29
+
30
+ const emit = (next: Record<string, string>) => {
31
+ skipSync.current = true
32
+ onChange?.(next)
33
+ }
34
+
35
+ const handleAdd = () => {
36
+ const key = draftKey.trim()
37
+ if (!key) return
38
+ const next = { ...pairs, [key]: draftValue.trim() }
39
+ setPairs(next)
40
+ emit(next)
41
+ setDraftKey("")
42
+ setDraftValue("")
43
+ setShowAdd(false)
44
+ }
45
+
46
+ const handleRemove = (key: string) => {
47
+ const next = { ...pairs }
48
+ delete next[key]
49
+ setPairs(next)
50
+ emit(next)
51
+ }
52
+
53
+ const handleKeyDown = (e: React.KeyboardEvent) => {
54
+ if (e.key === "Enter") { e.preventDefault(); handleAdd() }
55
+ if (e.key === "Escape") { setShowAdd(false); setDraftKey(""); setDraftValue("") }
56
+ }
57
+
58
+ return (
59
+ <div className="space-y-2">
60
+ {/* Chips row */}
61
+ <div className="flex flex-wrap items-center gap-2">
62
+ {Object.entries(pairs).map(([key, val]) => (
63
+ <span key={key} className="inline-flex items-center gap-1 px-2 py-1 bg-ibm-gray-10 border border-ibm-gray-20 text-xs font-mono">
64
+ <span className="text-ibm-gray-100">{key}</span>
65
+ {val && <><span className="text-ibm-gray-40">=</span><span className="text-ibm-blue-70">{val}</span></>}
66
+ {!disabled && (
67
+ <button type="button" onClick={() => handleRemove(key)} className="ml-0.5 text-ibm-gray-50 hover:text-red-600 transition-colors">
68
+ <X className="h-3 w-3" />
69
+ </button>
70
+ )}
71
+ </span>
72
+ ))}
73
+
74
+ {/* Add trigger */}
75
+ {!showAdd && !disabled && (
76
+ <button
77
+ type="button"
78
+ onClick={() => setShowAdd(true)}
79
+ className="inline-flex items-center justify-center h-6 w-6 border border-dashed border-ibm-gray-30 text-ibm-gray-50 hover:border-ibm-blue-60 hover:text-ibm-blue-60 transition-colors"
80
+ >
81
+ <Plus className="h-3.5 w-3.5" />
82
+ </button>
83
+ )}
84
+ </div>
85
+
86
+ {/* Inline add row */}
87
+ {showAdd && (
88
+ <div className="flex items-center gap-2">
89
+ <Input
90
+ ref={keyRef}
91
+ placeholder="Key"
92
+ value={draftKey}
93
+ onChange={(e) => setDraftKey(e.target.value)}
94
+ onKeyDown={handleKeyDown}
95
+ className="rounded-none flex-1 h-10"
96
+ />
97
+ <Input
98
+ placeholder="Value"
99
+ value={draftValue}
100
+ onChange={(e) => setDraftValue(e.target.value)}
101
+ onKeyDown={handleKeyDown}
102
+ className="rounded-none flex-1 h-10"
103
+ />
104
+ <button
105
+ type="button"
106
+ onClick={handleAdd}
107
+ disabled={!draftKey.trim()}
108
+ className="h-10 w-9 flex items-center justify-center border border-ibm-gray-30 bg-white text-ibm-gray-60 hover:bg-ibm-blue-10 hover:border-ibm-blue-60 hover:text-ibm-blue-60 disabled:opacity-40 disabled:pointer-events-none transition-colors flex-shrink-0"
109
+ >
110
+ <Plus className="h-4 w-4" />
111
+ </button>
112
+ </div>
113
+ )}
114
+ </div>
115
+ )
116
+ }
@@ -18,7 +18,7 @@ interface KVInputProps {
18
18
  valuePlaceholder?: string
19
19
  }
20
20
 
21
- const BTN = "rounded-none h-9 w-9 p-0 flex-shrink-0"
21
+ const BTN = "rounded-none h-10 w-10 p-0 flex-shrink-0"
22
22
 
23
23
  export function KVInput({
24
24
  value = {},
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext } from "react"
4
+ import type { ReactNode } from "react"
5
+
6
+ export interface SecretEntry {
7
+ name: string
8
+ description?: string
9
+ }
10
+
11
+ interface SecretContextValue {
12
+ templateId?: string
13
+ fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
14
+ }
15
+
16
+ const SecretContext = createContext<SecretContextValue | null>(null)
17
+
18
+ export function SecretContextProvider({
19
+ children,
20
+ templateId,
21
+ fetchSecrets,
22
+ }: {
23
+ children: ReactNode
24
+ templateId?: string
25
+ fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
26
+ }) {
27
+ return (
28
+ <SecretContext.Provider value={{ templateId, fetchSecrets }}>
29
+ {children}
30
+ </SecretContext.Provider>
31
+ )
32
+ }
33
+
34
+ export function useSecretContext(): SecretContextValue | null {
35
+ return useContext(SecretContext)
36
+ }
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { Loader2 } from "lucide-react"
5
+ import { SelectInput } from "./select-input"
6
+ import { useSecretContext } from "./secret-context"
7
+ import type { SecretEntry } from "./secret-context"
8
+
9
+ interface SecretInputProps {
10
+ id?: string
11
+ type?: string
12
+ value?: string
13
+ onChange?: (value: string) => void
14
+ disabled?: boolean
15
+ className?: string
16
+ placeholder?: string
17
+ }
18
+
19
+ export function SecretInput({
20
+ id,
21
+ type,
22
+ value,
23
+ onChange,
24
+ disabled,
25
+ className,
26
+ placeholder = "Select a secret",
27
+ }: SecretInputProps) {
28
+ const ctx = useSecretContext()
29
+ const [secrets, setSecrets] = useState<SecretEntry[]>([])
30
+ const [loading, setLoading] = useState(false)
31
+
32
+ useEffect(() => {
33
+ if (!ctx) return
34
+ setLoading(true)
35
+ ctx.fetchSecrets(type === "configs" ? ctx.templateId : type)
36
+ .then(setSecrets)
37
+ .catch(() => setSecrets([]))
38
+ .finally(() => setLoading(false))
39
+ }, [ctx?.templateId])
40
+
41
+ if (loading) {
42
+ return (
43
+ <div className="flex items-center gap-2 h-10 px-3 border border-ibm-gray-30 bg-white text-sm text-ibm-gray-60">
44
+ <Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
45
+ Loading secrets…
46
+ </div>
47
+ )
48
+ }
49
+
50
+ if (!ctx) {
51
+ return (
52
+ <div className="flex items-center h-10 px-3 border border-ibm-gray-30 bg-ibm-gray-10 text-sm text-ibm-gray-50">
53
+ No secret context provided
54
+ </div>
55
+ )
56
+ }
57
+
58
+ return (
59
+ <SelectInput
60
+ id={id}
61
+ value={value}
62
+ onChange={onChange}
63
+ disabled={disabled}
64
+ className={className}
65
+ placeholder={placeholder}
66
+ options={secrets.map((s) => ({ label: s.description || s.name, value: s.name }))}
67
+ />
68
+ )
69
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { Button } from "./button"
5
+ import { X, Plus } from "lucide-react"
6
+ import { SecretInput } from "./secret-input"
7
+
8
+ interface SecretsInputProps {
9
+ id?: string
10
+ value?: string[]
11
+ onChange?: (value: string[]) => void
12
+ disabled?: boolean
13
+ [key: string]: any
14
+ }
15
+
16
+ export function SecretsInput({ id, value = [], onChange, disabled = false, ...rest }: SecretsInputProps) {
17
+ const [items, setItems] = useState<string[]>(value.filter(Boolean))
18
+ const [draft, setDraft] = useState("")
19
+
20
+ useEffect(() => {
21
+ const filtered = value.filter(Boolean)
22
+ if (JSON.stringify(filtered) !== JSON.stringify(items)) {
23
+ setItems(filtered)
24
+ }
25
+ }, [value])
26
+
27
+ const handleAdd = () => {
28
+ if (!draft || items.includes(draft)) return
29
+ const next = [...items, draft]
30
+ setItems(next)
31
+ onChange?.(next)
32
+ setDraft("")
33
+ }
34
+
35
+ const handleRemove = (index: number) => {
36
+ const next = items.filter((_, i) => i !== index)
37
+ setItems(next)
38
+ onChange?.(next)
39
+ }
40
+
41
+ return (
42
+ <div className="space-y-2">
43
+ {items.map((item, index) => (
44
+ <div key={index} className="flex items-center gap-2">
45
+ <span className="flex-1 text-sm px-3 h-10 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate font-mono">
46
+ {item}
47
+ </span>
48
+ <Button
49
+ type="button"
50
+ variant="secondary"
51
+ onClick={() => handleRemove(index)}
52
+ disabled={disabled}
53
+ className="rounded-none h-10 w-10 p-0 shrink-0"
54
+ >
55
+ <X className="h-3.5 w-3.5" />
56
+ </Button>
57
+ </div>
58
+ ))}
59
+
60
+ <div className="flex gap-2">
61
+ <div className="flex-1">
62
+ <SecretInput value={draft} onChange={setDraft} disabled={disabled} type={id} />
63
+ </div>
64
+ <Button
65
+ type="button"
66
+ variant="secondary"
67
+ onClick={handleAdd}
68
+ disabled={disabled || !draft}
69
+ className="rounded-none h-10 w-10 p-0 shrink-0"
70
+ >
71
+ <Plus className="h-3.5 w-3.5" />
72
+ </Button>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
4
+
5
+ interface SelectOption {
6
+ label: string
7
+ value: any
8
+ }
9
+
10
+ interface SelectInputProps {
11
+ id?: string
12
+ value?: string
13
+ onChange?: (value: string) => void
14
+ options?: SelectOption[]
15
+ disabled?: boolean
16
+ required?: boolean
17
+ className?: string
18
+ placeholder?: string
19
+ }
20
+
21
+ export function SelectInput({
22
+ id,
23
+ value,
24
+ onChange,
25
+ options = [],
26
+ disabled = false,
27
+ className,
28
+ placeholder = "Select an option",
29
+ }: SelectInputProps) {
30
+ return (
31
+ <Select value={value || undefined} onValueChange={onChange} disabled={disabled}>
32
+ <SelectTrigger id={id} className={className}>
33
+ <SelectValue placeholder={placeholder} />
34
+ </SelectTrigger>
35
+ <SelectContent>
36
+ {options.map((opt) => (
37
+ <SelectItem key={String(opt.value)} value={String(opt.value)}>
38
+ {opt.label}
39
+ </SelectItem>
40
+ ))}
41
+ </SelectContent>
42
+ </Select>
43
+ )
44
+ }
@@ -54,7 +54,7 @@ export function StringsInput({
54
54
  <div className="space-y-2">
55
55
  {items.map((item, index) => (
56
56
  <div key={index} className="flex items-center gap-2">
57
- <span className="flex-1 text-sm px-3 h-7 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
57
+ <span className="flex-1 text-sm px-3 h-10 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
58
58
  {item}
59
59
  </span>
60
60
  <Button
@@ -62,7 +62,7 @@ export function StringsInput({
62
62
  variant="secondary"
63
63
  onClick={() => handleRemove(index)}
64
64
  disabled={disabled}
65
- className="rounded-none h-7 w-7 p-0 shrink-0"
65
+ className="rounded-none h-10 w-10 p-0 shrink-0"
66
66
  >
67
67
  <X className="h-3.5 w-3.5" />
68
68
  </Button>
@@ -75,14 +75,14 @@ export function StringsInput({
75
75
  onKeyDown={handleKeyDown}
76
76
  disabled={disabled}
77
77
  placeholder={placeholder}
78
- className="rounded-none flex-1 h-7 text-sm"
78
+ className="rounded-none flex-1 h-10 text-sm"
79
79
  />
80
80
  <Button
81
81
  type="button"
82
82
  variant="secondary"
83
83
  onClick={handleAdd}
84
84
  disabled={disabled || !inputValue.trim()}
85
- className="rounded-none h-7 w-7 p-0 shrink-0"
85
+ className="rounded-none h-10 w-10 p-0 shrink-0"
86
86
  >
87
87
  <Plus className="h-3.5 w-3.5" />
88
88
  </Button>
@@ -0,0 +1,331 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Button } from "./button"
5
+ import { Label } from "./label"
6
+ import { Input } from "./input"
7
+ import { Textarea } from "./textarea"
8
+ import { Switch } from "./switch"
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
10
+ import { StringsInput } from "./strings-input"
11
+ import { KVInput } from "./kv-input"
12
+ import { NumbersInput } from "./numbers-input"
13
+ import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react"
14
+
15
+ interface UIParamValidate {
16
+ required?: boolean
17
+ defaultValue?: any
18
+ options?: Array<{ label: string; value: any }>
19
+ immutable?: boolean
20
+ }
21
+
22
+ interface UIParam {
23
+ jsonKey: string
24
+ label?: string
25
+ uiType: string
26
+ validate?: UIParamValidate
27
+ subParameters?: UIParam[]
28
+ }
29
+
30
+ interface StructsInputProps {
31
+ id?: string
32
+ value?: any[]
33
+ onChange?: (value: any[]) => void
34
+ disabled?: boolean
35
+ subParameters?: UIParam[]
36
+ label?: string
37
+ }
38
+
39
+ interface StructItemProps {
40
+ index: number
41
+ value: any
42
+ onChange: (value: any) => void
43
+ onRemove: () => void
44
+ subParameters: UIParam[]
45
+ disabled?: boolean
46
+ isExpanded: boolean
47
+ onToggleExpand: () => void
48
+ }
49
+
50
+ function StructItem({ index, value, onChange, onRemove, subParameters, disabled, isExpanded, onToggleExpand }: StructItemProps) {
51
+ const handleFieldChange = (key: string, fieldValue: any) => {
52
+ onChange({ ...value, [key]: fieldValue })
53
+ }
54
+
55
+ const renderField = (param: UIParam) => {
56
+ const fieldValue = value?.[param.jsonKey] ?? param.validate?.defaultValue
57
+ const required = param.validate?.required || false
58
+ const fieldDisabled = disabled || (param.validate?.immutable) || false
59
+ const label = param.label || param.jsonKey
60
+
61
+ switch (param.uiType) {
62
+ case "Input":
63
+ case "ImageInput":
64
+ return (
65
+ <div key={param.jsonKey} className="space-y-1">
66
+ <Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
67
+ {label}
68
+ {required && <span className="text-red-600 ml-1">*</span>}
69
+ </Label>
70
+ <Input
71
+ id={`${param.jsonKey}-${index}`}
72
+ value={fieldValue ?? ""}
73
+ onChange={(e) => handleFieldChange(param.jsonKey, e.target.value)}
74
+ disabled={fieldDisabled}
75
+ required={required}
76
+ className="rounded-none h-8 text-sm"
77
+ />
78
+ </div>
79
+ )
80
+
81
+ case "Textarea":
82
+ return (
83
+ <div key={param.jsonKey} className="space-y-1">
84
+ <Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
85
+ {label}
86
+ {required && <span className="text-red-600 ml-1">*</span>}
87
+ </Label>
88
+ <Textarea
89
+ id={`${param.jsonKey}-${index}`}
90
+ value={fieldValue ?? ""}
91
+ onChange={(e) => handleFieldChange(param.jsonKey, e.target.value)}
92
+ disabled={fieldDisabled}
93
+ required={required}
94
+ className="rounded-none min-h-[60px] text-sm"
95
+ />
96
+ </div>
97
+ )
98
+
99
+ case "Number":
100
+ return (
101
+ <div key={param.jsonKey} className="space-y-1">
102
+ <Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
103
+ {label}
104
+ {required && <span className="text-red-600 ml-1">*</span>}
105
+ </Label>
106
+ <Input
107
+ id={`${param.jsonKey}-${index}`}
108
+ type="number"
109
+ value={fieldValue ?? ""}
110
+ onChange={(e) => {
111
+ const val = e.target.value === "" ? undefined : Number(e.target.value)
112
+ handleFieldChange(param.jsonKey, val)
113
+ }}
114
+ disabled={fieldDisabled}
115
+ required={required}
116
+ className="rounded-none h-8 text-sm"
117
+ />
118
+ </div>
119
+ )
120
+
121
+ case "Switch":
122
+ return (
123
+ <div key={param.jsonKey} className="flex items-center space-x-2 py-1">
124
+ <Switch
125
+ id={`${param.jsonKey}-${index}`}
126
+ checked={fieldValue || false}
127
+ onCheckedChange={(checked) => handleFieldChange(param.jsonKey, checked)}
128
+ disabled={fieldDisabled}
129
+ />
130
+ <Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs cursor-pointer">
131
+ {label}
132
+ {required && <span className="text-red-600 ml-1">*</span>}
133
+ </Label>
134
+ </div>
135
+ )
136
+
137
+ case "Select":
138
+ return (
139
+ <div key={param.jsonKey} className="space-y-1">
140
+ <Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
141
+ {label}
142
+ {required && <span className="text-red-600 ml-1">*</span>}
143
+ </Label>
144
+ <Select
145
+ value={fieldValue || ""}
146
+ onValueChange={(val) => handleFieldChange(param.jsonKey, val)}
147
+ disabled={fieldDisabled}
148
+ >
149
+ <SelectTrigger className="rounded-none h-8 text-sm">
150
+ <SelectValue placeholder={`Select ${label}`} />
151
+ </SelectTrigger>
152
+ <SelectContent>
153
+ {param.validate?.options?.map((opt) => (
154
+ <SelectItem key={opt.value} value={String(opt.value)}>
155
+ {opt.label}
156
+ </SelectItem>
157
+ ))}
158
+ </SelectContent>
159
+ </Select>
160
+ </div>
161
+ )
162
+
163
+ case "KV":
164
+ return (
165
+ <div key={param.jsonKey} className="space-y-1">
166
+ <Label className="text-xs">
167
+ {label}
168
+ {required && <span className="text-red-600 ml-1">*</span>}
169
+ </Label>
170
+ <KVInput
171
+ value={fieldValue || {}}
172
+ onChange={(val) => handleFieldChange(param.jsonKey, val)}
173
+ disabled={fieldDisabled}
174
+ />
175
+ </div>
176
+ )
177
+
178
+ case "Strings":
179
+ return (
180
+ <div key={param.jsonKey} className="space-y-1">
181
+ <Label className="text-xs">
182
+ {label}
183
+ {required && <span className="text-red-600 ml-1">*</span>}
184
+ </Label>
185
+ <StringsInput
186
+ value={fieldValue || []}
187
+ onChange={(val) => handleFieldChange(param.jsonKey, val)}
188
+ disabled={fieldDisabled}
189
+ />
190
+ </div>
191
+ )
192
+
193
+ case "Numbers":
194
+ return (
195
+ <div key={param.jsonKey} className="space-y-1">
196
+ <Label className="text-xs">
197
+ {label}
198
+ {required && <span className="text-red-600 ml-1">*</span>}
199
+ </Label>
200
+ <NumbersInput
201
+ value={fieldValue || []}
202
+ onChange={(val) => handleFieldChange(param.jsonKey, val)}
203
+ disabled={fieldDisabled}
204
+ />
205
+ </div>
206
+ )
207
+
208
+ default:
209
+ return null
210
+ }
211
+ }
212
+
213
+ const displayName = value?.name || value?.containerName || `Item ${index + 1}`
214
+
215
+ return (
216
+ <div className="border border-ibm-gray-20 bg-white">
217
+ <div className="flex items-center justify-between p-3 bg-ibm-gray-10 border-b border-ibm-gray-20">
218
+ <button
219
+ type="button"
220
+ onClick={onToggleExpand}
221
+ className="flex items-center gap-2 flex-1 text-left hover:text-ibm-blue-60 transition-colors"
222
+ >
223
+ {isExpanded ? (
224
+ <ChevronDown className="h-4 w-4 text-ibm-gray-70" />
225
+ ) : (
226
+ <ChevronRight className="h-4 w-4 text-ibm-gray-70" />
227
+ )}
228
+ <span className="text-sm font-medium text-ibm-gray-100">{displayName}</span>
229
+ </button>
230
+ <button
231
+ type="button"
232
+ onClick={onRemove}
233
+ disabled={disabled}
234
+ className="p-1.5 text-ibm-gray-50 hover:text-ibm-red-60 hover:bg-ibm-red-10 transition-colors disabled:opacity-40"
235
+ title="Remove"
236
+ >
237
+ <Trash2 className="h-4 w-4" />
238
+ </button>
239
+ </div>
240
+
241
+ {isExpanded && (
242
+ <div className="p-4 space-y-3">
243
+ {subParameters.length === 0 ? (
244
+ <div className="text-sm text-ibm-gray-60 italic">
245
+ No fields defined for this structure
246
+ </div>
247
+ ) : (
248
+ subParameters.map((param) => renderField(param))
249
+ )}
250
+ </div>
251
+ )}
252
+ </div>
253
+ )
254
+ }
255
+
256
+ export function StructsInput({ id, value = [], onChange, disabled = false, subParameters = [], label }: StructsInputProps) {
257
+ const [items, setItems] = useState<any[]>(Array.isArray(value) ? value : [])
258
+ const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set([0]))
259
+
260
+ useEffect(() => {
261
+ setItems(Array.isArray(value) ? value : [])
262
+ }, [value])
263
+
264
+ const handleAdd = () => {
265
+ const newItem: any = {}
266
+ subParameters.forEach((param) => {
267
+ if (param.validate?.defaultValue !== undefined) {
268
+ newItem[param.jsonKey] = param.validate.defaultValue
269
+ }
270
+ })
271
+ const newItems = [...items, newItem]
272
+ setItems(newItems)
273
+ onChange?.(newItems)
274
+ setExpandedItems(new Set([...expandedItems, items.length]))
275
+ }
276
+
277
+ const handleRemove = (index: number) => {
278
+ const newItems = items.filter((_, i) => i !== index)
279
+ setItems(newItems)
280
+ onChange?.(newItems)
281
+ const newExpanded = new Set<number>()
282
+ expandedItems.forEach((i) => {
283
+ if (i < index) newExpanded.add(i)
284
+ else if (i > index) newExpanded.add(i - 1)
285
+ })
286
+ setExpandedItems(newExpanded)
287
+ }
288
+
289
+ const handleChange = (index: number, itemValue: any) => {
290
+ const newItems = [...items]
291
+ newItems[index] = itemValue
292
+ setItems(newItems)
293
+ onChange?.(newItems)
294
+ }
295
+
296
+ const toggleExpand = (index: number) => {
297
+ const newExpanded = new Set(expandedItems)
298
+ if (newExpanded.has(index)) newExpanded.delete(index)
299
+ else newExpanded.add(index)
300
+ setExpandedItems(newExpanded)
301
+ }
302
+
303
+ return (
304
+ <div className="space-y-3">
305
+ {items.map((item, index) => (
306
+ <StructItem
307
+ key={index}
308
+ index={index}
309
+ value={item}
310
+ onChange={(val) => handleChange(index, val)}
311
+ onRemove={() => handleRemove(index)}
312
+ subParameters={subParameters}
313
+ disabled={disabled}
314
+ isExpanded={expandedItems.has(index)}
315
+ onToggleExpand={() => toggleExpand(index)}
316
+ />
317
+ ))}
318
+
319
+ <Button
320
+ type="button"
321
+ variant="secondary"
322
+ onClick={handleAdd}
323
+ disabled={disabled}
324
+ className="w-full rounded-none"
325
+ leftIcon={<Plus className="h-4 w-4" />}
326
+ >
327
+ Add {label || "Item"}
328
+ </Button>
329
+ </div>
330
+ )
331
+ }
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
19
19
  ref={ref}
20
20
  sideOffset={sideOffset}
21
21
  className={cn(
22
- "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
22
+ "z-50 overflow-hidden rounded-md border border-ibm-gray-20 bg-white px-3 py-1.5 text-sm text-ibm-gray-100 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
23
  className
24
24
  )}
25
25
  {...props}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.3.17",
3
+ "version": "1.4.0",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",