@comitor/ui 2.0.0 → 2.1.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.
- package/LICENSE +35 -0
- package/README.md +14 -6
- package/dist/components/ui/action-bar-constants.d.ts +31 -0
- package/dist/components/ui/action-bar-constants.js +9 -0
- package/dist/components/ui/action-bar-constants.js.map +1 -0
- package/dist/components/ui/action-bar.d.ts +67 -0
- package/dist/components/ui/action-bar.js +522 -0
- package/dist/components/ui/action-bar.js.map +1 -0
- package/dist/components/ui/badge-overflow.d.ts +30 -0
- package/dist/components/ui/badge-overflow.js +179 -0
- package/dist/components/ui/badge-overflow.js.map +1 -0
- package/dist/components/ui/banner.d.ts +79 -0
- package/dist/components/ui/banner.js +518 -0
- package/dist/components/ui/banner.js.map +1 -0
- package/dist/components/ui/circular-progress.d.ts +50 -0
- package/dist/components/ui/circular-progress.js +266 -0
- package/dist/components/ui/circular-progress.js.map +1 -0
- package/dist/components/ui/cropper-constants.d.ts +34 -0
- package/dist/components/ui/cropper-constants.js +10 -0
- package/dist/components/ui/cropper-constants.js.map +1 -0
- package/dist/components/ui/cropper.d.ts +106 -0
- package/dist/components/ui/cropper.js +1269 -0
- package/dist/components/ui/cropper.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.js +2 -2
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/kanban-constants.d.ts +61 -0
- package/dist/components/ui/kanban-constants.js +21 -0
- package/dist/components/ui/kanban-constants.js.map +1 -0
- package/dist/components/ui/kanban.d.ts +72 -0
- package/dist/components/ui/kanban.js +787 -0
- package/dist/components/ui/kanban.js.map +1 -0
- package/dist/components/ui/key-value-constants.d.ts +33 -0
- package/dist/components/ui/key-value-constants.js +11 -0
- package/dist/components/ui/key-value-constants.js.map +1 -0
- package/dist/components/ui/key-value.d.ts +96 -0
- package/dist/components/ui/key-value.js +641 -0
- package/dist/components/ui/key-value.js.map +1 -0
- package/dist/components/ui/mask-input-constants.d.ts +99 -0
- package/dist/components/ui/mask-input-constants.js +601 -0
- package/dist/components/ui/mask-input-constants.js.map +1 -0
- package/dist/components/ui/mask-input.d.ts +36 -0
- package/dist/components/ui/mask-input.js +558 -0
- package/dist/components/ui/mask-input.js.map +1 -0
- package/dist/components/ui/mention.d.ts +31 -0
- package/dist/components/ui/mention.js +128 -0
- package/dist/components/ui/mention.js.map +1 -0
- package/dist/components/ui/phone-input-constants.d.ts +54 -0
- package/dist/components/ui/phone-input-constants.js +9 -0
- package/dist/components/ui/phone-input-constants.js.map +1 -0
- package/dist/components/ui/phone-input.d.ts +85 -0
- package/dist/components/ui/phone-input.js +743 -0
- package/dist/components/ui/phone-input.js.map +1 -0
- package/dist/components/ui/rating.d.ts +56 -0
- package/dist/components/ui/rating.js +646 -0
- package/dist/components/ui/rating.js.map +1 -0
- package/dist/components/ui/responsive-dialog.d.ts +35 -0
- package/dist/components/ui/responsive-dialog.js +193 -0
- package/dist/components/ui/responsive-dialog.js.map +1 -0
- package/dist/components/ui/scroll-spy.d.ts +42 -0
- package/dist/components/ui/scroll-spy.js +334 -0
- package/dist/components/ui/scroll-spy.js.map +1 -0
- package/dist/components/ui/stepper-constants.d.ts +29 -0
- package/dist/components/ui/stepper-constants.js +9 -0
- package/dist/components/ui/stepper-constants.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +80 -0
- package/dist/components/ui/stepper.js +902 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/tags-input.d.ts +41 -0
- package/dist/components/ui/tags-input.js +127 -0
- package/dist/components/ui/tags-input.js.map +1 -0
- package/dist/components/ui/time-picker-constants.d.ts +48 -0
- package/dist/components/ui/time-picker-constants.js +14 -0
- package/dist/components/ui/time-picker-constants.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +115 -0
- package/dist/components/ui/time-picker.js +1620 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/timeline.d.ts +32 -0
- package/dist/components/ui/timeline.js +559 -0
- package/dist/components/ui/timeline.js.map +1 -0
- package/dist/components/ui/tour-constants.d.ts +39 -0
- package/dist/components/ui/tour-constants.js +12 -0
- package/dist/components/ui/tour-constants.js.map +1 -0
- package/dist/components/ui/tour.d.ts +124 -0
- package/dist/components/ui/tour.js +1207 -0
- package/dist/components/ui/tour.js.map +1 -0
- package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +1 -1
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js +1 -1
- package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
- package/dist/data-grid/data-grid-row.js +1 -1
- package/dist/data-grid/data-grid-row.js.map +1 -1
- package/dist/data-grid/data-grid-search.js +1 -1
- package/dist/data-grid/data-grid-search.js.map +1 -1
- package/dist/data-grid/data-grid.js +1 -1
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/sortable.js +1 -1
- package/dist/data-grid/sortable.js.map +1 -1
- package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
- package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
- package/dist/data-grid/use-data-grid.js +3 -3
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/index.d.ts +31 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compose-refs.js.map +1 -0
- package/dist/lib/use-as-ref.js.map +1 -0
- package/dist/lib/use-is-mobile.d.ts +10 -0
- package/dist/lib/use-is-mobile.js +21 -0
- package/dist/lib/use-is-mobile.js.map +1 -0
- package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
- package/dist/lib/use-lazy-ref.js.map +1 -0
- package/dist/lib/visually-hidden-input.d.ts +12 -0
- package/dist/lib/visually-hidden-input.js +105 -0
- package/dist/lib/visually-hidden-input.js.map +1 -0
- package/package.json +5 -1
- package/styles.css +19 -0
- package/dist/data-grid/compose-refs.js.map +0 -1
- package/dist/data-grid/use-as-ref.js.map +0 -1
- package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
- package/dist/data-grid/use-lazy-ref.js.map +0 -1
- /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
- /package/dist/{data-grid → lib}/compose-refs.js +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
- /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { Plus, X } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
8
|
+
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
9
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
10
|
+
import { useLazyRef } from "../../lib/use-lazy-ref.js";
|
|
11
|
+
import { VisuallyHiddenInput } from "../../lib/visually-hidden-input.js";
|
|
12
|
+
import { Button } from "./button.js";
|
|
13
|
+
import { Input } from "./input.js";
|
|
14
|
+
import { DEFAULT_KEY_VALUE_LABELS } from "./key-value-constants.js";
|
|
15
|
+
import { Textarea } from "./textarea.js";
|
|
16
|
+
const ROOT_NAME = "KeyValue";
|
|
17
|
+
const LIST_NAME = "KeyValueList";
|
|
18
|
+
const ITEM_NAME = "KeyValueItem";
|
|
19
|
+
const KEY_INPUT_NAME = "KeyValueKeyInput";
|
|
20
|
+
const VALUE_INPUT_NAME = "KeyValueValueInput";
|
|
21
|
+
const REMOVE_NAME = "KeyValueRemove";
|
|
22
|
+
const ADD_NAME = "KeyValueAdd";
|
|
23
|
+
const ERROR_NAME = "KeyValueError";
|
|
24
|
+
function getErrorId(rootId, itemId, field) {
|
|
25
|
+
return `${rootId}-${itemId}-${field}-error`;
|
|
26
|
+
}
|
|
27
|
+
let fallbackIdCounter = 0;
|
|
28
|
+
function createItemId() {
|
|
29
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
30
|
+
return crypto.randomUUID();
|
|
31
|
+
}
|
|
32
|
+
fallbackIdCounter += 1;
|
|
33
|
+
return `kv-${Date.now().toString(36)}-${fallbackIdCounter}`;
|
|
34
|
+
}
|
|
35
|
+
function removeQuotes(string, shouldStrip) {
|
|
36
|
+
if (!shouldStrip) return string;
|
|
37
|
+
const trimmed = string.trim();
|
|
38
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
39
|
+
return trimmed.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
function useStore(selector, ogStore) {
|
|
44
|
+
const contextStore = React.useContext(StoreContext);
|
|
45
|
+
const store = ogStore ?? contextStore;
|
|
46
|
+
if (!store) {
|
|
47
|
+
throw new Error(`\`useKeyValueStore\` ph\u1EA3i \u0111\u01B0\u1EE3c g\u1ECDi b\xEAn trong \`${ROOT_NAME}\``);
|
|
48
|
+
}
|
|
49
|
+
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);
|
|
50
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
51
|
+
}
|
|
52
|
+
const StoreContext = React.createContext(null);
|
|
53
|
+
function useStoreContext(consumerName) {
|
|
54
|
+
const context = React.useContext(StoreContext);
|
|
55
|
+
if (!context) {
|
|
56
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
57
|
+
}
|
|
58
|
+
return context;
|
|
59
|
+
}
|
|
60
|
+
const KeyValueContext = React.createContext(null);
|
|
61
|
+
function useKeyValueContext(consumerName) {
|
|
62
|
+
const context = React.useContext(KeyValueContext);
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${ROOT_NAME}\``);
|
|
65
|
+
}
|
|
66
|
+
return context;
|
|
67
|
+
}
|
|
68
|
+
function KeyValue(props) {
|
|
69
|
+
const {
|
|
70
|
+
value: valueProp,
|
|
71
|
+
defaultValue,
|
|
72
|
+
onValueChange,
|
|
73
|
+
onPaste,
|
|
74
|
+
onAdd,
|
|
75
|
+
onRemove,
|
|
76
|
+
onKeyValidate,
|
|
77
|
+
onValueValidate,
|
|
78
|
+
maxItems,
|
|
79
|
+
minItems = 0,
|
|
80
|
+
labels: labelsProp,
|
|
81
|
+
keyPlaceholder: keyPlaceholderProp,
|
|
82
|
+
valuePlaceholder: valuePlaceholderProp,
|
|
83
|
+
allowDuplicateKeys = false,
|
|
84
|
+
asChild,
|
|
85
|
+
enablePaste = true,
|
|
86
|
+
trim = true,
|
|
87
|
+
stripQuotes = true,
|
|
88
|
+
disabled = false,
|
|
89
|
+
readOnly = false,
|
|
90
|
+
required = false,
|
|
91
|
+
className,
|
|
92
|
+
id,
|
|
93
|
+
name,
|
|
94
|
+
ref,
|
|
95
|
+
...rootProps
|
|
96
|
+
} = props;
|
|
97
|
+
const instanceId = React.useId();
|
|
98
|
+
const rootId = id ?? instanceId;
|
|
99
|
+
const labels = React.useMemo(() => ({ ...DEFAULT_KEY_VALUE_LABELS, ...labelsProp }), [labelsProp]);
|
|
100
|
+
const keyPlaceholder = keyPlaceholderProp ?? labels.keyPlaceholder;
|
|
101
|
+
const valuePlaceholder = valuePlaceholderProp ?? labels.valuePlaceholder;
|
|
102
|
+
const [formTrigger, setFormTrigger] = React.useState(null);
|
|
103
|
+
const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));
|
|
104
|
+
const isFormControl = formTrigger ? !!formTrigger.closest("form") : true;
|
|
105
|
+
const listenersRef = useLazyRef(() => /* @__PURE__ */ new Set());
|
|
106
|
+
const stateRef = useLazyRef(() => ({
|
|
107
|
+
value: valueProp ?? defaultValue ?? [{ id: createItemId(), key: "", value: "" }],
|
|
108
|
+
focusedId: null,
|
|
109
|
+
errors: {}
|
|
110
|
+
}));
|
|
111
|
+
const propsRef = useAsRef({ onValueChange });
|
|
112
|
+
const store = React.useMemo(() => {
|
|
113
|
+
return {
|
|
114
|
+
subscribe: (cb) => {
|
|
115
|
+
listenersRef.current.add(cb);
|
|
116
|
+
return () => listenersRef.current.delete(cb);
|
|
117
|
+
},
|
|
118
|
+
getState: () => stateRef.current,
|
|
119
|
+
setState: (key, val) => {
|
|
120
|
+
if (Object.is(stateRef.current[key], val)) return;
|
|
121
|
+
if (key === "value" && Array.isArray(val)) {
|
|
122
|
+
stateRef.current.value = val;
|
|
123
|
+
propsRef.current.onValueChange?.(val);
|
|
124
|
+
} else {
|
|
125
|
+
stateRef.current[key] = val;
|
|
126
|
+
}
|
|
127
|
+
store.notify();
|
|
128
|
+
},
|
|
129
|
+
notify: () => {
|
|
130
|
+
for (const cb of listenersRef.current) {
|
|
131
|
+
cb();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}, [listenersRef, stateRef, propsRef]);
|
|
136
|
+
const value = useStore((state) => state.value, store);
|
|
137
|
+
const errors = useStore((state) => state.errors, store);
|
|
138
|
+
const isInvalid = Object.keys(errors).length > 0;
|
|
139
|
+
useIsomorphicLayoutEffect(() => {
|
|
140
|
+
if (valueProp !== void 0) {
|
|
141
|
+
store.setState("value", valueProp);
|
|
142
|
+
}
|
|
143
|
+
}, [valueProp]);
|
|
144
|
+
const contextValue = React.useMemo(
|
|
145
|
+
() => ({
|
|
146
|
+
onPaste,
|
|
147
|
+
onAdd,
|
|
148
|
+
onRemove,
|
|
149
|
+
onKeyValidate,
|
|
150
|
+
onValueValidate,
|
|
151
|
+
rootId,
|
|
152
|
+
maxItems,
|
|
153
|
+
minItems,
|
|
154
|
+
keyPlaceholder,
|
|
155
|
+
valuePlaceholder,
|
|
156
|
+
labels,
|
|
157
|
+
allowDuplicateKeys,
|
|
158
|
+
enablePaste,
|
|
159
|
+
trim,
|
|
160
|
+
stripQuotes,
|
|
161
|
+
disabled,
|
|
162
|
+
readOnly,
|
|
163
|
+
required
|
|
164
|
+
}),
|
|
165
|
+
[
|
|
166
|
+
onPaste,
|
|
167
|
+
onAdd,
|
|
168
|
+
onRemove,
|
|
169
|
+
onKeyValidate,
|
|
170
|
+
onValueValidate,
|
|
171
|
+
rootId,
|
|
172
|
+
disabled,
|
|
173
|
+
readOnly,
|
|
174
|
+
required,
|
|
175
|
+
maxItems,
|
|
176
|
+
minItems,
|
|
177
|
+
keyPlaceholder,
|
|
178
|
+
valuePlaceholder,
|
|
179
|
+
labels,
|
|
180
|
+
allowDuplicateKeys,
|
|
181
|
+
enablePaste,
|
|
182
|
+
trim,
|
|
183
|
+
stripQuotes
|
|
184
|
+
]
|
|
185
|
+
);
|
|
186
|
+
const RootPrimitive = asChild ? Slot : "div";
|
|
187
|
+
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store, children: /* @__PURE__ */ jsxs(KeyValueContext.Provider, { value: contextValue, children: [
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
189
|
+
RootPrimitive,
|
|
190
|
+
{
|
|
191
|
+
id,
|
|
192
|
+
"data-slot": "key-value",
|
|
193
|
+
"data-disabled": disabled ? "" : void 0,
|
|
194
|
+
"data-invalid": isInvalid ? "" : void 0,
|
|
195
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
196
|
+
...rootProps,
|
|
197
|
+
ref: composedRef,
|
|
198
|
+
className: cn("flex flex-col gap-2", className)
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
isFormControl && /* @__PURE__ */ jsx(
|
|
202
|
+
VisuallyHiddenInput,
|
|
203
|
+
{
|
|
204
|
+
type: "hidden",
|
|
205
|
+
control: formTrigger,
|
|
206
|
+
name,
|
|
207
|
+
value,
|
|
208
|
+
disabled,
|
|
209
|
+
readOnly,
|
|
210
|
+
required
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
] }) });
|
|
214
|
+
}
|
|
215
|
+
function KeyValueList(props) {
|
|
216
|
+
const { orientation = "vertical", asChild, className, ...listProps } = props;
|
|
217
|
+
const value = useStore((state) => state.value);
|
|
218
|
+
const ListPrimitive = asChild ? Slot : "div";
|
|
219
|
+
return /* @__PURE__ */ jsx(
|
|
220
|
+
ListPrimitive,
|
|
221
|
+
{
|
|
222
|
+
role: "list",
|
|
223
|
+
"data-slot": "key-value-list",
|
|
224
|
+
"data-orientation": orientation,
|
|
225
|
+
...listProps,
|
|
226
|
+
className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row gap-2", className),
|
|
227
|
+
children: value.map((item) => {
|
|
228
|
+
const children = React.Children.toArray(props.children);
|
|
229
|
+
return /* @__PURE__ */ jsx(KeyValueItemContext.Provider, { value: item, children }, item.id);
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const KeyValueItemContext = React.createContext(null);
|
|
235
|
+
function useKeyValueItemContext(consumerName) {
|
|
236
|
+
const context = React.useContext(KeyValueItemContext);
|
|
237
|
+
if (!context) {
|
|
238
|
+
throw new Error(`\`${consumerName}\` ph\u1EA3i n\u1EB1m b\xEAn trong \`${LIST_NAME}\``);
|
|
239
|
+
}
|
|
240
|
+
return context;
|
|
241
|
+
}
|
|
242
|
+
function KeyValueItem(props) {
|
|
243
|
+
const { asChild, className, ...itemProps } = props;
|
|
244
|
+
const itemData = useKeyValueItemContext(ITEM_NAME);
|
|
245
|
+
const focusedId = useStore((state) => state.focusedId);
|
|
246
|
+
const ItemPrimitive = asChild ? Slot : "div";
|
|
247
|
+
return /* @__PURE__ */ jsx(
|
|
248
|
+
ItemPrimitive,
|
|
249
|
+
{
|
|
250
|
+
role: "listitem",
|
|
251
|
+
"data-slot": "key-value-item",
|
|
252
|
+
"data-highlighted": focusedId === itemData.id ? "" : void 0,
|
|
253
|
+
...itemProps,
|
|
254
|
+
className: cn("flex items-start gap-2", className)
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function KeyValueKeyInput(props) {
|
|
259
|
+
const {
|
|
260
|
+
onChange: onChangeProp,
|
|
261
|
+
onPaste: onPasteProp,
|
|
262
|
+
asChild,
|
|
263
|
+
disabled,
|
|
264
|
+
readOnly,
|
|
265
|
+
required,
|
|
266
|
+
ref,
|
|
267
|
+
...keyInputProps
|
|
268
|
+
} = props;
|
|
269
|
+
const context = useKeyValueContext(KEY_INPUT_NAME);
|
|
270
|
+
const itemData = useKeyValueItemContext(KEY_INPUT_NAME);
|
|
271
|
+
const store = useStoreContext(KEY_INPUT_NAME);
|
|
272
|
+
const errors = useStore((state) => state.errors);
|
|
273
|
+
const focusedId = useStore((state) => state.focusedId);
|
|
274
|
+
const inputRef = React.useRef(null);
|
|
275
|
+
const composedRef = useComposedRefs(ref, inputRef);
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
if (focusedId === itemData.id) {
|
|
278
|
+
inputRef.current?.focus();
|
|
279
|
+
}
|
|
280
|
+
}, [focusedId, itemData.id]);
|
|
281
|
+
const propsRef = useAsRef({
|
|
282
|
+
onChange: onChangeProp,
|
|
283
|
+
onPaste: onPasteProp
|
|
284
|
+
});
|
|
285
|
+
const isDisabled = disabled || context.disabled;
|
|
286
|
+
const isReadOnly = readOnly || context.readOnly;
|
|
287
|
+
const isRequired = required || context.required;
|
|
288
|
+
const isInvalid = errors[itemData.id]?.key !== void 0;
|
|
289
|
+
const onChange = React.useCallback(
|
|
290
|
+
(event) => {
|
|
291
|
+
const state = store.getState();
|
|
292
|
+
const newValue = state.value.map((item) => {
|
|
293
|
+
if (item.id !== itemData.id) return item;
|
|
294
|
+
const updated = { ...item, key: event.target.value };
|
|
295
|
+
if (context.trim) updated.key = updated.key.trim();
|
|
296
|
+
return updated;
|
|
297
|
+
});
|
|
298
|
+
store.setState("value", newValue);
|
|
299
|
+
const updatedItemData = newValue.find((item) => item.id === itemData.id);
|
|
300
|
+
if (updatedItemData) {
|
|
301
|
+
const errors2 = {};
|
|
302
|
+
if (context.onKeyValidate) {
|
|
303
|
+
const keyError = context.onKeyValidate(updatedItemData.key, newValue);
|
|
304
|
+
if (keyError) errors2.key = keyError;
|
|
305
|
+
}
|
|
306
|
+
if (!context.allowDuplicateKeys) {
|
|
307
|
+
const duplicateKey = newValue.find(
|
|
308
|
+
(item) => item.id !== updatedItemData.id && item.key === updatedItemData.key && updatedItemData.key !== ""
|
|
309
|
+
);
|
|
310
|
+
if (duplicateKey) {
|
|
311
|
+
errors2.key = context.labels.duplicateKey;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (context.onValueValidate) {
|
|
315
|
+
const valueError = context.onValueValidate(updatedItemData.value, updatedItemData.key, newValue);
|
|
316
|
+
if (valueError) errors2.value = valueError;
|
|
317
|
+
}
|
|
318
|
+
const newErrorsState = { ...state.errors };
|
|
319
|
+
if (Object.keys(errors2).length > 0) {
|
|
320
|
+
newErrorsState[itemData.id] = errors2;
|
|
321
|
+
} else {
|
|
322
|
+
delete newErrorsState[itemData.id];
|
|
323
|
+
}
|
|
324
|
+
store.setState("errors", newErrorsState);
|
|
325
|
+
}
|
|
326
|
+
propsRef.current.onChange?.(event);
|
|
327
|
+
},
|
|
328
|
+
[store, itemData.id, context, propsRef]
|
|
329
|
+
);
|
|
330
|
+
const onPaste = React.useCallback(
|
|
331
|
+
(event) => {
|
|
332
|
+
if (!context.enablePaste) return;
|
|
333
|
+
propsRef.current.onPaste?.(event);
|
|
334
|
+
if (event.defaultPrevented) return;
|
|
335
|
+
const content = event.clipboardData.getData("text");
|
|
336
|
+
const lines = content.split(/\r?\n/).filter((line) => line.trim());
|
|
337
|
+
if (lines.length > 1) {
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
const parsed = [];
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
let key = "";
|
|
342
|
+
let value = "";
|
|
343
|
+
if (line.includes("=")) {
|
|
344
|
+
const parts = line.split("=");
|
|
345
|
+
key = parts[0]?.trim() ?? "";
|
|
346
|
+
value = removeQuotes(parts.slice(1).join("=").trim(), context.stripQuotes);
|
|
347
|
+
} else if (line.includes(":")) {
|
|
348
|
+
const parts = line.split(":");
|
|
349
|
+
key = parts[0]?.trim() ?? "";
|
|
350
|
+
value = removeQuotes(parts.slice(1).join(":").trim(), context.stripQuotes);
|
|
351
|
+
} else if (/\s{2,}|\t/.test(line)) {
|
|
352
|
+
const parts = line.split(/\s{2,}|\t/);
|
|
353
|
+
key = parts[0]?.trim() ?? "";
|
|
354
|
+
value = removeQuotes(parts.slice(1).join(" ").trim(), context.stripQuotes);
|
|
355
|
+
}
|
|
356
|
+
if (key) {
|
|
357
|
+
parsed.push({ id: createItemId(), key, value });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (parsed.length > 0) {
|
|
361
|
+
const state = store.getState();
|
|
362
|
+
const currentIndex = state.value.findIndex((item) => item.id === itemData.id);
|
|
363
|
+
let newValue;
|
|
364
|
+
if (itemData.key === "" && itemData.value === "") {
|
|
365
|
+
newValue = [...state.value.slice(0, currentIndex), ...parsed, ...state.value.slice(currentIndex + 1)];
|
|
366
|
+
} else {
|
|
367
|
+
newValue = [...state.value.slice(0, currentIndex + 1), ...parsed, ...state.value.slice(currentIndex + 1)];
|
|
368
|
+
}
|
|
369
|
+
if (context.maxItems !== void 0) {
|
|
370
|
+
newValue = newValue.slice(0, context.maxItems);
|
|
371
|
+
}
|
|
372
|
+
store.setState("value", newValue);
|
|
373
|
+
if (context.onPaste) {
|
|
374
|
+
context.onPaste(event.nativeEvent, parsed);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
[context, store, itemData, propsRef]
|
|
380
|
+
);
|
|
381
|
+
const KeyInputPrimitive = asChild ? Slot : Input;
|
|
382
|
+
return /* @__PURE__ */ jsx(
|
|
383
|
+
KeyInputPrimitive,
|
|
384
|
+
{
|
|
385
|
+
"aria-invalid": isInvalid,
|
|
386
|
+
"aria-describedby": isInvalid ? getErrorId(context.rootId, itemData.id, "key") : void 0,
|
|
387
|
+
"data-slot": "key-value-key-input",
|
|
388
|
+
autoCapitalize: "off",
|
|
389
|
+
autoComplete: "off",
|
|
390
|
+
autoCorrect: "off",
|
|
391
|
+
spellCheck: "false",
|
|
392
|
+
disabled: isDisabled,
|
|
393
|
+
readOnly: isReadOnly,
|
|
394
|
+
required: isRequired,
|
|
395
|
+
placeholder: context.keyPlaceholder,
|
|
396
|
+
...keyInputProps,
|
|
397
|
+
ref: composedRef,
|
|
398
|
+
value: itemData.key,
|
|
399
|
+
onChange,
|
|
400
|
+
onPaste
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
function KeyValueValueInput(props) {
|
|
405
|
+
const {
|
|
406
|
+
onChange: onChangeProp,
|
|
407
|
+
asChild,
|
|
408
|
+
disabled,
|
|
409
|
+
readOnly,
|
|
410
|
+
required,
|
|
411
|
+
className,
|
|
412
|
+
maxRows,
|
|
413
|
+
style,
|
|
414
|
+
...valueInputProps
|
|
415
|
+
} = props;
|
|
416
|
+
const context = useKeyValueContext(VALUE_INPUT_NAME);
|
|
417
|
+
const itemData = useKeyValueItemContext(VALUE_INPUT_NAME);
|
|
418
|
+
const store = useStoreContext(VALUE_INPUT_NAME);
|
|
419
|
+
const errors = useStore((state) => state.errors);
|
|
420
|
+
const propsRef = useAsRef({
|
|
421
|
+
onChange: onChangeProp
|
|
422
|
+
});
|
|
423
|
+
const isDisabled = disabled || context.disabled;
|
|
424
|
+
const isReadOnly = readOnly || context.readOnly;
|
|
425
|
+
const isRequired = required || context.required;
|
|
426
|
+
const isInvalid = errors[itemData.id]?.value !== void 0;
|
|
427
|
+
const maxHeight = maxRows ? `calc(${maxRows} * 1.5em + 1rem)` : void 0;
|
|
428
|
+
const onChange = React.useCallback(
|
|
429
|
+
(event) => {
|
|
430
|
+
propsRef.current.onChange?.(event);
|
|
431
|
+
const state = store.getState();
|
|
432
|
+
const newValue = state.value.map((item) => {
|
|
433
|
+
if (item.id !== itemData.id) return item;
|
|
434
|
+
const updated = { ...item, value: event.target.value };
|
|
435
|
+
if (context.trim) updated.value = updated.value.trim();
|
|
436
|
+
return updated;
|
|
437
|
+
});
|
|
438
|
+
store.setState("value", newValue);
|
|
439
|
+
const updatedItemData = newValue.find((item) => item.id === itemData.id);
|
|
440
|
+
if (updatedItemData) {
|
|
441
|
+
const errors2 = {};
|
|
442
|
+
if (context.onKeyValidate) {
|
|
443
|
+
const keyError = context.onKeyValidate(updatedItemData.key, newValue);
|
|
444
|
+
if (keyError) errors2.key = keyError;
|
|
445
|
+
}
|
|
446
|
+
if (!context.allowDuplicateKeys) {
|
|
447
|
+
const duplicateKey = newValue.find(
|
|
448
|
+
(item) => item.id !== updatedItemData.id && item.key === updatedItemData.key && updatedItemData.key !== ""
|
|
449
|
+
);
|
|
450
|
+
if (duplicateKey) {
|
|
451
|
+
errors2.key = context.labels.duplicateKey;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (context.onValueValidate) {
|
|
455
|
+
const valueError = context.onValueValidate(updatedItemData.value, updatedItemData.key, newValue);
|
|
456
|
+
if (valueError) errors2.value = valueError;
|
|
457
|
+
}
|
|
458
|
+
const newErrorsState = { ...state.errors };
|
|
459
|
+
if (Object.keys(errors2).length > 0) {
|
|
460
|
+
newErrorsState[itemData.id] = errors2;
|
|
461
|
+
} else {
|
|
462
|
+
delete newErrorsState[itemData.id];
|
|
463
|
+
}
|
|
464
|
+
store.setState("errors", newErrorsState);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
[store, itemData.id, context, propsRef]
|
|
468
|
+
);
|
|
469
|
+
const ValueInputPrimitive = asChild ? Slot : Textarea;
|
|
470
|
+
return /* @__PURE__ */ jsx(
|
|
471
|
+
ValueInputPrimitive,
|
|
472
|
+
{
|
|
473
|
+
"aria-invalid": isInvalid,
|
|
474
|
+
"aria-describedby": isInvalid ? getErrorId(context.rootId, itemData.id, "value") : void 0,
|
|
475
|
+
"data-slot": "key-value-value-input",
|
|
476
|
+
autoCapitalize: "off",
|
|
477
|
+
autoComplete: "off",
|
|
478
|
+
autoCorrect: "off",
|
|
479
|
+
spellCheck: "false",
|
|
480
|
+
disabled: isDisabled,
|
|
481
|
+
readOnly: isReadOnly,
|
|
482
|
+
required: isRequired,
|
|
483
|
+
placeholder: context.valuePlaceholder,
|
|
484
|
+
...valueInputProps,
|
|
485
|
+
className: cn(
|
|
486
|
+
/*
|
|
487
|
+
* ⚠ Khác thượng nguồn: ô giá trị phải trông như ô khoá (`Input`) đứng ngay cạnh khi còn MỘT
|
|
488
|
+
* dòng. `Textarea` của gói theo comitor-ds: `text-base md:text-sm` + `py-2` (cao 38px ở
|
|
489
|
+
* desktop, 42px và chữ 16px ở mobile), nền `bg-transparent dark:bg-input-fill`, có
|
|
490
|
+
* `shadow-xs`; còn `Input` là `h-9 text-sm bg-background`, không bóng. Ghi đè ở ĐÂY chứ
|
|
491
|
+
* không sửa hai primitive: `text-sm` ở mọi cỡ màn, `min-h-9` + `py-1.5` (dòng chữ 20px cách
|
|
492
|
+
* mép trên 7px — đúng chỗ `Input` căn giữa dòng chữ trong 36px), `bg-background` ở cả hai
|
|
493
|
+
* theme, bỏ bóng. Nhiều dòng thì `field-sizing-content` vẫn cho ô cao dần như cũ.
|
|
494
|
+
*/
|
|
495
|
+
"field-sizing-content min-h-9 resize-none py-1.5 text-sm md:text-sm bg-background dark:bg-background shadow-none",
|
|
496
|
+
maxRows && "overflow-y-auto",
|
|
497
|
+
className
|
|
498
|
+
),
|
|
499
|
+
style: {
|
|
500
|
+
...style,
|
|
501
|
+
...maxHeight && { maxHeight }
|
|
502
|
+
},
|
|
503
|
+
value: itemData.value,
|
|
504
|
+
onChange
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
function KeyValueRemove(props) {
|
|
509
|
+
const { onClick: onClickProp, children, className, ...removeProps } = props;
|
|
510
|
+
const context = useKeyValueContext(REMOVE_NAME);
|
|
511
|
+
const itemData = useKeyValueItemContext(REMOVE_NAME);
|
|
512
|
+
const store = useStoreContext(REMOVE_NAME);
|
|
513
|
+
const propsRef = useAsRef({
|
|
514
|
+
onClick: onClickProp
|
|
515
|
+
});
|
|
516
|
+
const value = useStore((state) => state.value);
|
|
517
|
+
const isDisabled = context.disabled || value.length <= context.minItems;
|
|
518
|
+
const itemIndex = value.findIndex((item) => item.id === itemData.id);
|
|
519
|
+
const onClick = React.useCallback(
|
|
520
|
+
(event) => {
|
|
521
|
+
propsRef.current.onClick?.(event);
|
|
522
|
+
const state = store.getState();
|
|
523
|
+
if (state.value.length <= context.minItems) return;
|
|
524
|
+
const itemToRemove = state.value.find((item) => item.id === itemData.id);
|
|
525
|
+
if (!itemToRemove) return;
|
|
526
|
+
const button = event.currentTarget;
|
|
527
|
+
const hadFocus = button.ownerDocument.activeElement === button;
|
|
528
|
+
const root = button.closest('[data-slot="key-value"]');
|
|
529
|
+
const removedIndex = state.value.indexOf(itemToRemove);
|
|
530
|
+
const newValue = state.value.filter((item) => item.id !== itemData.id);
|
|
531
|
+
const newErrors = { ...state.errors };
|
|
532
|
+
delete newErrors[itemData.id];
|
|
533
|
+
store.setState("value", newValue);
|
|
534
|
+
store.setState("errors", newErrors);
|
|
535
|
+
context.onRemove?.(itemToRemove);
|
|
536
|
+
if (hadFocus && root) {
|
|
537
|
+
requestAnimationFrame(() => {
|
|
538
|
+
const removeButtons = root.querySelectorAll('[data-slot="key-value-remove"]:not(:disabled)');
|
|
539
|
+
const nextTarget = removeButtons[Math.min(removedIndex, removeButtons.length - 1)] ?? root.querySelector('[data-slot="key-value-add"]:not(:disabled)');
|
|
540
|
+
nextTarget?.focus();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
[store, context, itemData.id, propsRef]
|
|
545
|
+
);
|
|
546
|
+
return /* @__PURE__ */ jsx(
|
|
547
|
+
Button,
|
|
548
|
+
{
|
|
549
|
+
type: "button",
|
|
550
|
+
"data-slot": "key-value-remove",
|
|
551
|
+
variant: "outline",
|
|
552
|
+
size: "icon",
|
|
553
|
+
"aria-label": context.labels.remove(itemData.key, itemIndex + 1),
|
|
554
|
+
disabled: isDisabled,
|
|
555
|
+
...removeProps,
|
|
556
|
+
className: cn("shrink-0", className),
|
|
557
|
+
onClick,
|
|
558
|
+
children: children ?? /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function KeyValueAdd(props) {
|
|
563
|
+
const { onClick: onClickProp, children, ...addProps } = props;
|
|
564
|
+
const context = useKeyValueContext(ADD_NAME);
|
|
565
|
+
const store = useStoreContext(ADD_NAME);
|
|
566
|
+
const propsRef = useAsRef({
|
|
567
|
+
onClick: onClickProp
|
|
568
|
+
});
|
|
569
|
+
const value = useStore((state) => state.value);
|
|
570
|
+
const isDisabled = context.disabled || context.maxItems !== void 0 && value.length >= context.maxItems;
|
|
571
|
+
const onClick = React.useCallback(
|
|
572
|
+
(event) => {
|
|
573
|
+
propsRef.current.onClick?.(event);
|
|
574
|
+
const state = store.getState();
|
|
575
|
+
if (context.maxItems !== void 0 && state.value.length >= context.maxItems) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const newItem = {
|
|
579
|
+
id: createItemId(),
|
|
580
|
+
key: "",
|
|
581
|
+
value: ""
|
|
582
|
+
};
|
|
583
|
+
const newValue = [...state.value, newItem];
|
|
584
|
+
store.setState("value", newValue);
|
|
585
|
+
store.setState("focusedId", newItem.id);
|
|
586
|
+
context.onAdd?.(newItem);
|
|
587
|
+
},
|
|
588
|
+
[store, context, propsRef]
|
|
589
|
+
);
|
|
590
|
+
return /* @__PURE__ */ jsx(
|
|
591
|
+
Button,
|
|
592
|
+
{
|
|
593
|
+
type: "button",
|
|
594
|
+
"data-slot": "key-value-add",
|
|
595
|
+
variant: "outline",
|
|
596
|
+
disabled: isDisabled,
|
|
597
|
+
...addProps,
|
|
598
|
+
onClick,
|
|
599
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
600
|
+
/* @__PURE__ */ jsx(Plus, { "aria-hidden": "true", className: "size-4" }),
|
|
601
|
+
context.labels.add
|
|
602
|
+
] })
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
function KeyValueError(props) {
|
|
607
|
+
const { field, asChild, className, ...errorProps } = props;
|
|
608
|
+
const context = useKeyValueContext(ERROR_NAME);
|
|
609
|
+
const itemData = useKeyValueItemContext(ERROR_NAME);
|
|
610
|
+
const errors = useStore((state) => state.errors);
|
|
611
|
+
const error = errors[itemData.id]?.[field];
|
|
612
|
+
if (!error) return null;
|
|
613
|
+
const ErrorPrimitive = asChild ? Slot : "span";
|
|
614
|
+
return /* @__PURE__ */ jsx(
|
|
615
|
+
ErrorPrimitive,
|
|
616
|
+
{
|
|
617
|
+
id: getErrorId(context.rootId, itemData.id, field),
|
|
618
|
+
role: "alert",
|
|
619
|
+
...errorProps,
|
|
620
|
+
className: cn(
|
|
621
|
+
// ⚠ Khác thượng nguồn: `text-destructive` (bậc TÔ NỀN) → `text-destructive-ink`, bậc dành
|
|
622
|
+
// cho chữ trên nền trang — cùng token với chữ lỗi của `Input`.
|
|
623
|
+
"text-sm font-medium text-destructive-ink",
|
|
624
|
+
className
|
|
625
|
+
),
|
|
626
|
+
children: error
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
export {
|
|
631
|
+
KeyValue,
|
|
632
|
+
KeyValueAdd,
|
|
633
|
+
KeyValueError,
|
|
634
|
+
KeyValueItem,
|
|
635
|
+
KeyValueKeyInput,
|
|
636
|
+
KeyValueList,
|
|
637
|
+
KeyValueRemove,
|
|
638
|
+
KeyValueValueInput,
|
|
639
|
+
useStore as useKeyValueStore
|
|
640
|
+
};
|
|
641
|
+
//# sourceMappingURL=key-value.js.map
|