@orsetra/shared-ui 1.3.13 → 1.3.15
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/kv-input.tsx +70 -58
- package/components/ui/side-panel.tsx +12 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect } from "react"
|
|
3
|
+
import React, { useState, useEffect, useRef } from "react"
|
|
4
4
|
import { Input } from "./input"
|
|
5
5
|
import { Button } from "./button"
|
|
6
6
|
import { X, Plus } from "lucide-react"
|
|
@@ -18,6 +18,8 @@ interface KVInputProps {
|
|
|
18
18
|
valuePlaceholder?: string
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const BTN = "rounded-none h-9 w-9 p-0 flex-shrink-0"
|
|
22
|
+
|
|
21
23
|
export function KVInput({
|
|
22
24
|
value = {},
|
|
23
25
|
onChange,
|
|
@@ -25,93 +27,103 @@ export function KVInput({
|
|
|
25
27
|
keyPlaceholder = "Key",
|
|
26
28
|
valuePlaceholder = "Value",
|
|
27
29
|
}: KVInputProps) {
|
|
28
|
-
const [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
})
|
|
30
|
+
const [committed, setCommitted] = useState<KVPair[]>(() =>
|
|
31
|
+
Object.entries(value).map(([k, v]) => ({ key: k, value: v }))
|
|
32
|
+
)
|
|
33
|
+
const [draft, setDraft] = useState<KVPair>({ key: "", value: "" })
|
|
34
|
+
const skipSync = useRef(false)
|
|
32
35
|
|
|
36
|
+
// Sync from parent only when value changes externally (not from our own emit)
|
|
33
37
|
useEffect(() => {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
setPairs(newPairs)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
38
|
+
if (skipSync.current) { skipSync.current = false; return }
|
|
39
|
+
setCommitted(Object.entries(value).map(([k, v]) => ({ key: k, value: v })))
|
|
40
|
+
setDraft({ key: "", value: "" })
|
|
40
41
|
}, [value])
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
const newPairs = [...pairs]
|
|
44
|
-
newPairs[index][field] = newValue
|
|
45
|
-
setPairs(newPairs)
|
|
46
|
-
|
|
43
|
+
const emit = (comm: KVPair[]) => {
|
|
47
44
|
const result: Record<string, string> = {}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
result[pair.key] = pair.value
|
|
51
|
-
}
|
|
52
|
-
})
|
|
45
|
+
comm.forEach(p => { if (p.key) result[p.key] = p.value })
|
|
46
|
+
skipSync.current = true
|
|
53
47
|
onChange?.(result)
|
|
54
48
|
}
|
|
55
49
|
|
|
56
|
-
const
|
|
57
|
-
|
|
50
|
+
const handleCommit = () => {
|
|
51
|
+
if (!draft.key) return
|
|
52
|
+
const next = [...committed, { ...draft }]
|
|
53
|
+
setCommitted(next)
|
|
54
|
+
setDraft({ key: "", value: "" })
|
|
55
|
+
emit(next)
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
const handleRemove = (index: number) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
})
|
|
71
|
-
onChange?.(result)
|
|
59
|
+
const next = committed.filter((_, i) => i !== index)
|
|
60
|
+
setCommitted(next)
|
|
61
|
+
emit(next)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleCommittedChange = (index: number, field: "key" | "value", val: string) => {
|
|
65
|
+
const next = committed.map((p, i) => i === index ? { ...p, [field]: val } : p)
|
|
66
|
+
setCommitted(next)
|
|
67
|
+
emit(next)
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
return (
|
|
75
71
|
<div className="space-y-2">
|
|
76
|
-
{
|
|
72
|
+
{committed.map((pair, index) => (
|
|
77
73
|
<div key={index} className="flex gap-2">
|
|
78
74
|
<Input
|
|
79
75
|
value={pair.key}
|
|
80
|
-
onChange={(e) =>
|
|
76
|
+
onChange={(e) => handleCommittedChange(index, "key", e.target.value)}
|
|
81
77
|
disabled={disabled}
|
|
82
78
|
placeholder={keyPlaceholder}
|
|
83
79
|
className="rounded-none flex-1"
|
|
84
80
|
/>
|
|
85
81
|
<Input
|
|
86
82
|
value={pair.value}
|
|
87
|
-
onChange={(e) =>
|
|
83
|
+
onChange={(e) => handleCommittedChange(index, "value", e.target.value)}
|
|
88
84
|
disabled={disabled}
|
|
89
85
|
placeholder={valuePlaceholder}
|
|
90
86
|
className="rounded-none flex-1"
|
|
91
87
|
/>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</Button>
|
|
102
|
-
)}
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
variant="secondary"
|
|
91
|
+
onClick={() => handleRemove(index)}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
className={BTN}
|
|
94
|
+
>
|
|
95
|
+
<X className="h-4 w-4" />
|
|
96
|
+
</Button>
|
|
103
97
|
</div>
|
|
104
98
|
))}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
|
|
100
|
+
{/* Draft row — + enabled only when key is filled */}
|
|
101
|
+
<div className="flex gap-2">
|
|
102
|
+
<Input
|
|
103
|
+
value={draft.key}
|
|
104
|
+
onChange={(e) => setDraft(d => ({ ...d, key: e.target.value }))}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
placeholder={keyPlaceholder}
|
|
107
|
+
required
|
|
108
|
+
className="rounded-none flex-1"
|
|
109
|
+
/>
|
|
110
|
+
<Input
|
|
111
|
+
value={draft.value}
|
|
112
|
+
onChange={(e) => setDraft(d => ({ ...d, value: e.target.value }))}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
placeholder={valuePlaceholder}
|
|
115
|
+
className="rounded-none flex-1"
|
|
116
|
+
/>
|
|
117
|
+
<Button
|
|
118
|
+
type="button"
|
|
119
|
+
variant="secondary"
|
|
120
|
+
onClick={handleCommit}
|
|
121
|
+
disabled={disabled || !draft.key}
|
|
122
|
+
className={BTN}
|
|
123
|
+
>
|
|
124
|
+
<Plus className="h-4 w-4" />
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
115
127
|
</div>
|
|
116
128
|
)
|
|
117
129
|
}
|
|
@@ -20,7 +20,7 @@ const SidePanelOverlay = React.forwardRef<
|
|
|
20
20
|
<DialogPrimitive.Overlay
|
|
21
21
|
ref={ref}
|
|
22
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",
|
|
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 data-[state=open]:duration-300 data-[state=closed]:duration-200",
|
|
24
24
|
className
|
|
25
25
|
)}
|
|
26
26
|
{...props}
|
|
@@ -31,8 +31,9 @@ SidePanelOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
|
31
31
|
interface SidePanelProps {
|
|
32
32
|
open: boolean
|
|
33
33
|
onOpenChange: (open: boolean) => void
|
|
34
|
-
title:
|
|
35
|
-
description?:
|
|
34
|
+
title: React.ReactNode
|
|
35
|
+
description?: React.ReactNode
|
|
36
|
+
headerExtra?: React.ReactNode
|
|
36
37
|
children: React.ReactNode
|
|
37
38
|
actions: React.ReactNode
|
|
38
39
|
side?: "left" | "right"
|
|
@@ -45,6 +46,7 @@ const SidePanel = ({
|
|
|
45
46
|
onOpenChange,
|
|
46
47
|
title,
|
|
47
48
|
description,
|
|
49
|
+
headerExtra,
|
|
48
50
|
children,
|
|
49
51
|
actions,
|
|
50
52
|
side = "right",
|
|
@@ -57,7 +59,7 @@ const SidePanel = ({
|
|
|
57
59
|
<SidePanelOverlay />
|
|
58
60
|
<DialogPrimitive.Content
|
|
59
61
|
className={cn(
|
|
60
|
-
"fixed z-50 bg-white shadow-lg transition
|
|
62
|
+
"fixed z-50 bg-white shadow-lg transition-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-out data-[state=closed]:ease-in data-[state=open]:duration-300 data-[state=closed]:duration-200 flex flex-col",
|
|
61
63
|
side === "right" &&
|
|
62
64
|
"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",
|
|
63
65
|
side === "left" &&
|
|
@@ -77,6 +79,11 @@ const SidePanel = ({
|
|
|
77
79
|
</DialogPrimitive.Description>
|
|
78
80
|
)}
|
|
79
81
|
</div>
|
|
82
|
+
{headerExtra && (
|
|
83
|
+
<div className="flex items-center mr-3">
|
|
84
|
+
{headerExtra}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
80
87
|
<DialogPrimitive.Close
|
|
81
88
|
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"
|
|
82
89
|
onClick={onClose}
|
|
@@ -117,7 +124,7 @@ const SidePanelContent = React.forwardRef<
|
|
|
117
124
|
<DialogPrimitive.Content
|
|
118
125
|
ref={ref}
|
|
119
126
|
className={cn(
|
|
120
|
-
"fixed z-50 bg-white shadow-lg transition
|
|
127
|
+
"fixed z-50 bg-white shadow-lg transition-transform data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-out data-[state=closed]:ease-in data-[state=open]:duration-300 data-[state=closed]:duration-200 flex flex-col",
|
|
121
128
|
side === "right" &&
|
|
122
129
|
"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",
|
|
123
130
|
side === "left" &&
|