@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.
Files changed (131) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +14 -6
  3. package/dist/components/ui/action-bar-constants.d.ts +31 -0
  4. package/dist/components/ui/action-bar-constants.js +9 -0
  5. package/dist/components/ui/action-bar-constants.js.map +1 -0
  6. package/dist/components/ui/action-bar.d.ts +67 -0
  7. package/dist/components/ui/action-bar.js +522 -0
  8. package/dist/components/ui/action-bar.js.map +1 -0
  9. package/dist/components/ui/badge-overflow.d.ts +30 -0
  10. package/dist/components/ui/badge-overflow.js +179 -0
  11. package/dist/components/ui/badge-overflow.js.map +1 -0
  12. package/dist/components/ui/banner.d.ts +79 -0
  13. package/dist/components/ui/banner.js +518 -0
  14. package/dist/components/ui/banner.js.map +1 -0
  15. package/dist/components/ui/circular-progress.d.ts +50 -0
  16. package/dist/components/ui/circular-progress.js +266 -0
  17. package/dist/components/ui/circular-progress.js.map +1 -0
  18. package/dist/components/ui/cropper-constants.d.ts +34 -0
  19. package/dist/components/ui/cropper-constants.js +10 -0
  20. package/dist/components/ui/cropper-constants.js.map +1 -0
  21. package/dist/components/ui/cropper.d.ts +106 -0
  22. package/dist/components/ui/cropper.js +1269 -0
  23. package/dist/components/ui/cropper.js.map +1 -0
  24. package/dist/components/ui/drawer.d.ts +1 -1
  25. package/dist/components/ui/drawer.js +2 -2
  26. package/dist/components/ui/drawer.js.map +1 -1
  27. package/dist/components/ui/kanban-constants.d.ts +61 -0
  28. package/dist/components/ui/kanban-constants.js +21 -0
  29. package/dist/components/ui/kanban-constants.js.map +1 -0
  30. package/dist/components/ui/kanban.d.ts +72 -0
  31. package/dist/components/ui/kanban.js +787 -0
  32. package/dist/components/ui/kanban.js.map +1 -0
  33. package/dist/components/ui/key-value-constants.d.ts +33 -0
  34. package/dist/components/ui/key-value-constants.js +11 -0
  35. package/dist/components/ui/key-value-constants.js.map +1 -0
  36. package/dist/components/ui/key-value.d.ts +96 -0
  37. package/dist/components/ui/key-value.js +641 -0
  38. package/dist/components/ui/key-value.js.map +1 -0
  39. package/dist/components/ui/mask-input-constants.d.ts +99 -0
  40. package/dist/components/ui/mask-input-constants.js +601 -0
  41. package/dist/components/ui/mask-input-constants.js.map +1 -0
  42. package/dist/components/ui/mask-input.d.ts +36 -0
  43. package/dist/components/ui/mask-input.js +558 -0
  44. package/dist/components/ui/mask-input.js.map +1 -0
  45. package/dist/components/ui/mention.d.ts +31 -0
  46. package/dist/components/ui/mention.js +128 -0
  47. package/dist/components/ui/mention.js.map +1 -0
  48. package/dist/components/ui/phone-input-constants.d.ts +54 -0
  49. package/dist/components/ui/phone-input-constants.js +9 -0
  50. package/dist/components/ui/phone-input-constants.js.map +1 -0
  51. package/dist/components/ui/phone-input.d.ts +85 -0
  52. package/dist/components/ui/phone-input.js +743 -0
  53. package/dist/components/ui/phone-input.js.map +1 -0
  54. package/dist/components/ui/rating.d.ts +56 -0
  55. package/dist/components/ui/rating.js +646 -0
  56. package/dist/components/ui/rating.js.map +1 -0
  57. package/dist/components/ui/responsive-dialog.d.ts +35 -0
  58. package/dist/components/ui/responsive-dialog.js +193 -0
  59. package/dist/components/ui/responsive-dialog.js.map +1 -0
  60. package/dist/components/ui/scroll-spy.d.ts +42 -0
  61. package/dist/components/ui/scroll-spy.js +334 -0
  62. package/dist/components/ui/scroll-spy.js.map +1 -0
  63. package/dist/components/ui/stepper-constants.d.ts +29 -0
  64. package/dist/components/ui/stepper-constants.js +9 -0
  65. package/dist/components/ui/stepper-constants.js.map +1 -0
  66. package/dist/components/ui/stepper.d.ts +80 -0
  67. package/dist/components/ui/stepper.js +902 -0
  68. package/dist/components/ui/stepper.js.map +1 -0
  69. package/dist/components/ui/tags-input.d.ts +41 -0
  70. package/dist/components/ui/tags-input.js +127 -0
  71. package/dist/components/ui/tags-input.js.map +1 -0
  72. package/dist/components/ui/time-picker-constants.d.ts +48 -0
  73. package/dist/components/ui/time-picker-constants.js +14 -0
  74. package/dist/components/ui/time-picker-constants.js.map +1 -0
  75. package/dist/components/ui/time-picker.d.ts +115 -0
  76. package/dist/components/ui/time-picker.js +1620 -0
  77. package/dist/components/ui/time-picker.js.map +1 -0
  78. package/dist/components/ui/timeline.d.ts +32 -0
  79. package/dist/components/ui/timeline.js +559 -0
  80. package/dist/components/ui/timeline.js.map +1 -0
  81. package/dist/components/ui/tour-constants.d.ts +39 -0
  82. package/dist/components/ui/tour-constants.js +12 -0
  83. package/dist/components/ui/tour-constants.js.map +1 -0
  84. package/dist/components/ui/tour.d.ts +124 -0
  85. package/dist/components/ui/tour.js +1207 -0
  86. package/dist/components/ui/tour.js.map +1 -0
  87. package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
  88. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  89. package/dist/data-grid/data-grid-context-menu.js +1 -1
  90. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  91. package/dist/data-grid/data-grid-paste-dialog.js +1 -1
  92. package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
  93. package/dist/data-grid/data-grid-row.js +1 -1
  94. package/dist/data-grid/data-grid-row.js.map +1 -1
  95. package/dist/data-grid/data-grid-search.js +1 -1
  96. package/dist/data-grid/data-grid-search.js.map +1 -1
  97. package/dist/data-grid/data-grid.js +1 -1
  98. package/dist/data-grid/data-grid.js.map +1 -1
  99. package/dist/data-grid/sortable.js +1 -1
  100. package/dist/data-grid/sortable.js.map +1 -1
  101. package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
  102. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  103. package/dist/data-grid/use-data-grid.js +3 -3
  104. package/dist/data-grid/use-data-grid.js.map +1 -1
  105. package/dist/index.d.ts +31 -0
  106. package/dist/index.js +42 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/lib/compose-refs.js.map +1 -0
  109. package/dist/lib/use-as-ref.js.map +1 -0
  110. package/dist/lib/use-is-mobile.d.ts +10 -0
  111. package/dist/lib/use-is-mobile.js +21 -0
  112. package/dist/lib/use-is-mobile.js.map +1 -0
  113. package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
  114. package/dist/lib/use-lazy-ref.js.map +1 -0
  115. package/dist/lib/visually-hidden-input.d.ts +12 -0
  116. package/dist/lib/visually-hidden-input.js +105 -0
  117. package/dist/lib/visually-hidden-input.js.map +1 -0
  118. package/package.json +5 -1
  119. package/styles.css +19 -0
  120. package/dist/data-grid/compose-refs.js.map +0 -1
  121. package/dist/data-grid/use-as-ref.js.map +0 -1
  122. package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
  123. package/dist/data-grid/use-lazy-ref.js.map +0 -1
  124. /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
  125. /package/dist/{data-grid → lib}/compose-refs.js +0 -0
  126. /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
  127. /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
  128. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
  129. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
  130. /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
  131. /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