@orsetra/shared-ui 1.3.14 → 1.3.16

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.
@@ -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,102 @@ export function KVInput({
25
27
  keyPlaceholder = "Key",
26
28
  valuePlaceholder = "Value",
27
29
  }: 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
- })
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 (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
- }
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 handleChange = (index: number, field: "key" | "value", newValue: string) => {
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
- newPairs.forEach(pair => {
49
- if (pair.key) {
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 handleAdd = () => {
57
- setPairs([...pairs, { key: "", value: "" }])
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 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)
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
- {pairs.map((pair, index) => (
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) => handleChange(index, "key", e.target.value)}
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) => handleChange(index, "value", e.target.value)}
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
- {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
- )}
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
- <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>
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
+ className="rounded-none flex-1"
108
+ />
109
+ <Input
110
+ value={draft.value}
111
+ onChange={(e) => setDraft(d => ({ ...d, value: e.target.value }))}
112
+ disabled={disabled}
113
+ placeholder={valuePlaceholder}
114
+ className="rounded-none flex-1"
115
+ />
116
+ <Button
117
+ type="button"
118
+ variant="secondary"
119
+ onClick={handleCommit}
120
+ disabled={disabled || !draft.key}
121
+ className={BTN}
122
+ >
123
+ <Plus className="h-4 w-4" />
124
+ </Button>
125
+ </div>
115
126
  </div>
116
127
  )
117
128
  }
@@ -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: string
35
- description?: string
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",
@@ -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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.3.14",
3
+ "version": "1.3.16",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",