@comitor/ui 3.0.0 → 3.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/README.md +5 -1
- package/dist/components/composite/confirm-dialog.js +56 -35
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +3 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +3 -1
- package/dist/components/composite/table-pagination-constants.js.map +1 -1
- package/dist/components/composite/table-pagination.js +10 -2
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/composite/text-utils.d.ts +4 -23
- package/dist/components/composite/text-utils.js +1 -11
- package/dist/components/composite/text-utils.js.map +1 -1
- package/dist/components/composite/toast.d.ts +2 -1
- package/dist/components/composite/toast.js +3 -9
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/action-bar.js +45 -1
- package/dist/components/ui/action-bar.js.map +1 -1
- package/dist/components/ui/banner.d.ts +8 -0
- package/dist/components/ui/banner.js +149 -35
- package/dist/components/ui/banner.js.map +1 -1
- package/dist/components/ui/circular-progress.js +10 -2
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/kanban.js +9 -3
- package/dist/components/ui/kanban.js.map +1 -1
- package/dist/components/ui/key-value.js +70 -76
- package/dist/components/ui/key-value.js.map +1 -1
- package/dist/components/ui/mask-input-constants.d.ts +34 -2
- package/dist/components/ui/mask-input-constants.js +87 -76
- package/dist/components/ui/mask-input-constants.js.map +1 -1
- package/dist/components/ui/mask-input.d.ts +6 -5
- package/dist/components/ui/mask-input.js +23 -21
- package/dist/components/ui/mask-input.js.map +1 -1
- package/dist/components/ui/mention.d.ts +14 -2
- package/dist/components/ui/mention.js +33 -6
- package/dist/components/ui/mention.js.map +1 -1
- package/dist/components/ui/phone-input.d.ts +18 -8
- package/dist/components/ui/phone-input.js +125 -35
- package/dist/components/ui/phone-input.js.map +1 -1
- package/dist/components/ui/rating.js +4 -2
- package/dist/components/ui/rating.js.map +1 -1
- package/dist/components/ui/scroll-spy.js +46 -31
- package/dist/components/ui/scroll-spy.js.map +1 -1
- package/dist/components/ui/slider.d.ts +1 -1
- package/dist/components/ui/slider.js +15 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.js +2 -0
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/stepper.js +26 -3
- package/dist/components/ui/stepper.js.map +1 -1
- package/dist/components/ui/time-picker.d.ts +24 -4
- package/dist/components/ui/time-picker.js +425 -162
- package/dist/components/ui/time-picker.js.map +1 -1
- package/dist/components/ui/timeline.js +45 -9
- package/dist/components/ui/timeline.js.map +1 -1
- package/dist/components/ui/toggle-group.js +12 -1
- package/dist/components/ui/toggle-group.js.map +1 -1
- package/dist/components/ui/tour.js +3 -2
- package/dist/components/ui/tour.js.map +1 -1
- package/dist/data-grid/data-grid-cell-variants.js +4 -4
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.d.ts +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js +6 -0
- package/dist/data-grid/data-grid-cell-wrapper.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-select-column.js +6 -2
- package/dist/data-grid/data-grid-select-column.js.map +1 -1
- package/dist/data-grid/data-grid-skeleton.d.ts +5 -0
- package/dist/data-grid/data-grid-skeleton.js +1 -4
- package/dist/data-grid/data-grid-skeleton.js.map +1 -1
- package/dist/data-grid/data-grid.d.ts +1 -1
- package/dist/data-grid/data-grid.js +3 -2
- package/dist/data-grid/data-grid.js.map +1 -1
- package/dist/data-grid/filters.js +3 -2
- package/dist/data-grid/filters.js.map +1 -1
- package/dist/data-grid/types.d.ts +7 -0
- package/dist/data-grid/use-data-grid.d.ts +1 -0
- package/dist/data-grid/use-data-grid.js +70 -24
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/data-grid/utils.d.ts +7 -0
- package/dist/data-grid/utils.js +3 -1
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/i18n/locale-provider.d.ts +18 -0
- package/dist/i18n/locale-provider.js.map +1 -1
- package/dist/i18n/namespaces/rating.js +2 -1
- package/dist/i18n/namespaces/rating.js.map +1 -1
- package/dist/i18n/namespaces/search-filter-bar.js +8 -6
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
- package/dist/i18n/namespaces/shell.d.ts +4 -0
- package/dist/i18n/namespaces/shell.js +1 -1
- package/dist/i18n/namespaces/shell.js.map +1 -1
- package/dist/i18n/namespaces/table-pagination.js +6 -4
- package/dist/i18n/namespaces/table-pagination.js.map +1 -1
- package/dist/i18n/namespaces/toast.d.ts +3 -2
- package/dist/i18n/namespaces/toast.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/lib/collect-item-elements.d.ts +47 -0
- package/dist/lib/collect-item-elements.js +22 -0
- package/dist/lib/collect-item-elements.js.map +1 -0
- package/dist/lib/text-search.d.ts +32 -0
- package/dist/lib/text-search.js +17 -0
- package/dist/lib/text-search.js.map +1 -0
- package/dist/lib/toast-language.d.ts +8 -0
- package/dist/lib/toast-language.js +19 -0
- package/dist/lib/toast-language.js.map +1 -0
- package/dist/shell/app-shell.d.ts +1 -0
- package/dist/shell/index.d.ts +2 -1
- package/dist/shell/shell-context.d.ts +1 -0
- package/dist/shell/utils.d.ts +2 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/key-value.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/key-value) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Plus, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useComitorLabels } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\nimport { VisuallyHiddenInput } from \"../../lib/visually-hidden-input.js\";\nimport { Button } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport type { KeyValueLabels } from \"./key-value-constants.js\";\nimport { Textarea } from \"./textarea.js\";\n\nconst ROOT_NAME = \"KeyValue\";\nconst LIST_NAME = \"KeyValueList\";\nconst ITEM_NAME = \"KeyValueItem\";\nconst KEY_INPUT_NAME = \"KeyValueKeyInput\";\nconst VALUE_INPUT_NAME = \"KeyValueValueInput\";\nconst REMOVE_NAME = \"KeyValueRemove\";\nconst ADD_NAME = \"KeyValueAdd\";\nconst ERROR_NAME = \"KeyValueError\";\n\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Field = \"key\" | \"value\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof KeyValue>;\ntype KeyInputElement = React.ComponentRef<typeof KeyValueKeyInput>;\ntype RemoveElement = React.ComponentRef<typeof KeyValueRemove>;\ntype AddElement = React.ComponentRef<typeof KeyValueAdd>;\n\nfunction getErrorId(rootId: string, itemId: string, field: Field) {\n return `${rootId}-${itemId}-${field}-error`;\n}\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc gọi thẳng `crypto.randomUUID()`. Hàm đó chỉ tồn tại trong NGỮ CẢNH\n * AN TOÀN (HTTPS hoặc localhost) — mở trang quản trị qua `http://` bằng IP nội bộ là `TypeError`\n * ngay lúc render dòng đầu tiên. Id ở đây chỉ làm key của React và nối vào id của thông báo lỗi,\n * không cần ngẫu nhiên mật mã, nên có đường lui.\n */\nlet fallbackIdCounter = 0;\n\nfunction createItemId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n fallbackIdCounter += 1;\n return `kv-${Date.now().toString(36)}-${fallbackIdCounter}`;\n}\n\nfunction removeQuotes(string: string, shouldStrip: boolean): string {\n if (!shouldStrip) return string;\n\n const trimmed = string.trim();\n if ((trimmed.startsWith('\"') && trimmed.endsWith('\"')) || (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => KeyValueState;\n setState: <K extends keyof KeyValueState>(key: K, value: KeyValueState[K]) => void;\n notify: () => void;\n}\n\nfunction useStore<T>(selector: (state: KeyValueState) => T, ogStore?: Store | null): T {\n const contextStore = React.useContext(StoreContext);\n\n const store = ogStore ?? contextStore;\n\n if (!store) {\n throw new Error(`\\`useKeyValueStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ItemData {\n id: string;\n key: string;\n value: string;\n}\n\ninterface KeyValueState {\n value: ItemData[];\n focusedId: string | null;\n errors: Record<string, { key?: string; value?: string }>;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface KeyValueContextValue {\n onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n onAdd?: (value: ItemData) => void;\n onRemove?: (value: ItemData) => void;\n onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n onValueValidate?: (value: string, key: string, items: ItemData[]) => string | undefined;\n rootId: string;\n maxItems?: number;\n minItems: number;\n keyPlaceholder: string;\n valuePlaceholder: string;\n labels: KeyValueLabels;\n allowDuplicateKeys: boolean;\n enablePaste: boolean;\n trim: boolean;\n stripQuotes: boolean;\n disabled: boolean;\n readOnly: boolean;\n required: boolean;\n}\n\nconst KeyValueContext = React.createContext<KeyValueContextValue | null>(null);\n\nfunction useKeyValueContext(consumerName: string) {\n const context = React.useContext(KeyValueContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface KeyValueProps extends Omit<DivProps, \"onPaste\" | \"defaultValue\"> {\n id?: string;\n defaultValue?: ItemData[];\n value?: ItemData[];\n onValueChange?: (value: ItemData[]) => void;\n maxItems?: number;\n minItems?: number;\n /** Placeholder của ô khoá — thắng `labels.keyPlaceholder` (mặc định \"Key\"). */\n keyPlaceholder?: string;\n /** Placeholder của ô giá trị — thắng `labels.valuePlaceholder` (mặc định \"Value\"). */\n valuePlaceholder?: string;\n /**\n * Chuỗi hiển thị và chuỗi cho trình đọc màn hình — truyền MỘT PHẦN là đủ, phần còn lại theo\n * namespace `keyValue` của `ComitorLocaleProvider` (mặc định tiếng Anh). ⚠ Thêm so với thượng\n * nguồn (bản gốc chôn cứng \"Add\" / \"Duplicate key\" và để nút xoá không có tên).\n */\n labels?: Partial<KeyValueLabels>;\n name?: string;\n allowDuplicateKeys?: boolean;\n enablePaste?: boolean;\n trim?: boolean;\n stripQuotes?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n required?: boolean;\n onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n onAdd?: (value: ItemData) => void;\n onRemove?: (value: ItemData) => void;\n onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n onValueValidate?: (value: string, key: string, items: ItemData[]) => string | undefined;\n}\n\n/**\n * KeyValue — danh sách cặp khoá/giá trị sửa được: trường tuỳ biến của bản ghi CRM, header /\n * metadata của webhook ở trang quản trị. Dán nhiều dòng `KEY=value` (hoặc `KEY: value`, hoặc\n * cách bằng tab) vào ô khoá là tách thành nhiều cặp.\n */\nfunction KeyValue(props: KeyValueProps) {\n const {\n value: valueProp,\n defaultValue,\n onValueChange,\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n maxItems,\n minItems = 0,\n labels: labelsProp,\n keyPlaceholder: keyPlaceholderProp,\n valuePlaceholder: valuePlaceholderProp,\n allowDuplicateKeys = false,\n asChild,\n enablePaste = true,\n trim = true,\n stripQuotes = true,\n disabled = false,\n readOnly = false,\n required = false,\n className,\n id,\n name,\n ref,\n ...rootProps\n } = props;\n\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n\n const labels = useComitorLabels(\"keyValue\", labelsProp);\n const keyPlaceholder = keyPlaceholderProp ?? labels.keyPlaceholder;\n const valuePlaceholder = valuePlaceholderProp ?? labels.valuePlaceholder;\n\n const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(null);\n const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef<KeyValueState>(() => ({\n value: valueProp ?? defaultValue ?? [{ id: createItemId(), key: \"\", value: \"\" }],\n focusedId: null,\n errors: {}\n }));\n const propsRef = useAsRef({ onValueChange });\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, val) => {\n if (Object.is(stateRef.current[key], val)) return;\n\n if (key === \"value\" && Array.isArray(val)) {\n stateRef.current.value = val as ItemData[];\n propsRef.current.onValueChange?.(val as ItemData[]);\n } else {\n stateRef.current[key] = val;\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n }\n };\n }, [listenersRef, stateRef, propsRef]);\n\n const value = useStore((state) => state.value, store);\n const errors = useStore((state) => state.errors, store);\n const isInvalid = Object.keys(errors).length > 0;\n\n useIsomorphicLayoutEffect(() => {\n if (valueProp !== undefined) {\n store.setState(\"value\", valueProp);\n }\n }, [valueProp]);\n\n const contextValue = React.useMemo<KeyValueContextValue>(\n () => ({\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n rootId,\n maxItems,\n minItems,\n keyPlaceholder,\n valuePlaceholder,\n labels,\n allowDuplicateKeys,\n enablePaste,\n trim,\n stripQuotes,\n disabled,\n readOnly,\n required\n }),\n [\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n rootId,\n disabled,\n readOnly,\n required,\n maxItems,\n minItems,\n keyPlaceholder,\n valuePlaceholder,\n labels,\n allowDuplicateKeys,\n enablePaste,\n trim,\n stripQuotes\n ]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <KeyValueContext.Provider value={contextValue}>\n <RootPrimitive\n id={id}\n data-slot=\"key-value\"\n data-disabled={disabled ? \"\" : undefined}\n data-invalid={isInvalid ? \"\" : undefined}\n data-readonly={readOnly ? \"\" : undefined}\n {...rootProps}\n ref={composedRef}\n className={cn(\"flex flex-col gap-2\", className)}\n />\n {isFormControl && (\n <VisuallyHiddenInput\n type=\"hidden\"\n control={formTrigger}\n name={name}\n value={value}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n />\n )}\n </KeyValueContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface KeyValueListProps extends DivProps {\n orientation?: Orientation;\n}\n\nfunction KeyValueList(props: KeyValueListProps) {\n const { orientation = \"vertical\", asChild, className, ...listProps } = props;\n\n const value = useStore((state) => state.value);\n\n const ListPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ListPrimitive\n role=\"list\"\n /*\n * ⚠ Khác thượng nguồn (a11y): bỏ `aria-orientation`. Thuộc tính đó KHÔNG được phép trên\n * `role=\"list\"` (ARIA 1.2 chỉ cho nó ở listbox, menu, tablist, toolbar, radiogroup, slider…)\n * — axe báo `aria-allowed-attr`. Hướng vẫn còn ở `data-orientation` cho CSS.\n */\n data-slot=\"key-value-list\"\n data-orientation={orientation}\n {...listProps}\n className={cn(\"flex\", orientation === \"vertical\" ? \"flex-col gap-2\" : \"flex-row gap-2\", className)}\n >\n {value.map((item) => {\n const children = React.Children.toArray(props.children);\n\n return (\n <KeyValueItemContext.Provider key={item.id} value={item}>\n {children}\n </KeyValueItemContext.Provider>\n );\n })}\n </ListPrimitive>\n );\n}\n\nconst KeyValueItemContext = React.createContext<ItemData | null>(null);\n\nfunction useKeyValueItemContext(consumerName: string) {\n const context = React.useContext(KeyValueItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${LIST_NAME}\\``);\n }\n return context;\n}\n\ninterface KeyValueItemProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\nfunction KeyValueItem(props: KeyValueItemProps) {\n const { asChild, className, ...itemProps } = props;\n const itemData = useKeyValueItemContext(ITEM_NAME);\n\n const focusedId = useStore((state) => state.focusedId);\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ItemPrimitive\n role=\"listitem\"\n data-slot=\"key-value-item\"\n data-highlighted={focusedId === itemData.id ? \"\" : undefined}\n {...itemProps}\n className={cn(\"flex items-start gap-2\", className)}\n />\n );\n}\n\ninterface KeyValueKeyInputProps extends React.ComponentProps<\"input\"> {\n asChild?: boolean;\n}\n\nfunction KeyValueKeyInput(props: KeyValueKeyInputProps) {\n const {\n onChange: onChangeProp,\n onPaste: onPasteProp,\n asChild,\n disabled,\n readOnly,\n required,\n ref,\n ...keyInputProps\n } = props;\n\n const context = useKeyValueContext(KEY_INPUT_NAME);\n const itemData = useKeyValueItemContext(KEY_INPUT_NAME);\n const store = useStoreContext(KEY_INPUT_NAME);\n const errors = useStore((state) => state.errors);\n const focusedId = useStore((state) => state.focusedId);\n\n const inputRef = React.useRef<KeyInputElement>(null);\n const composedRef = useComposedRefs(ref, inputRef);\n\n /*\n * ⚠ Khác thượng nguồn (a11y): `KeyValueAdd` ghi id dòng mới vào `focusedId` nhưng bản gốc không\n * bao giờ dùng nó để đưa focus — bấm \"Thêm\" xong focus vẫn đứng ở nút, người dùng bàn phím phải\n * tự Tab ngược lên dòng vừa thêm. Ở đây ô KHOÁ của dòng mới tự nhận focus. Chỉ chạy khi\n * `focusedId` ĐỔI sang dòng này (mà chỉ `KeyValueAdd` ghi nó), nên dòng có sẵn lúc mount và dòng\n * sinh ra do dán không bị cướp focus.\n */\n React.useEffect(() => {\n if (focusedId === itemData.id) {\n inputRef.current?.focus();\n }\n }, [focusedId, itemData.id]);\n\n const propsRef = useAsRef({\n onChange: onChangeProp,\n onPaste: onPasteProp\n });\n\n const isDisabled = disabled || context.disabled;\n const isReadOnly = readOnly || context.readOnly;\n const isRequired = required || context.required;\n const isInvalid = errors[itemData.id]?.key !== undefined;\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent<KeyInputElement>) => {\n const state = store.getState();\n const newValue = state.value.map((item) => {\n if (item.id !== itemData.id) return item;\n const updated = { ...item, key: event.target.value };\n if (context.trim) updated.key = updated.key.trim();\n return updated;\n });\n\n store.setState(\"value\", newValue);\n\n const updatedItemData = newValue.find((item) => item.id === itemData.id);\n if (updatedItemData) {\n const errors: { key?: string; value?: string } = {};\n\n if (context.onKeyValidate) {\n const keyError = context.onKeyValidate(updatedItemData.key, newValue);\n if (keyError) errors.key = keyError;\n }\n\n if (!context.allowDuplicateKeys) {\n const duplicateKey = newValue.find(\n (item) => item.id !== updatedItemData.id && item.key === updatedItemData.key && updatedItemData.key !== \"\"\n );\n if (duplicateKey) {\n errors.key = context.labels.duplicateKey;\n }\n }\n\n if (context.onValueValidate) {\n const valueError = context.onValueValidate(updatedItemData.value, updatedItemData.key, newValue);\n if (valueError) errors.value = valueError;\n }\n\n const newErrorsState = { ...state.errors };\n if (Object.keys(errors).length > 0) {\n newErrorsState[itemData.id] = errors;\n } else {\n delete newErrorsState[itemData.id];\n }\n store.setState(\"errors\", newErrorsState);\n }\n\n propsRef.current.onChange?.(event);\n },\n [store, itemData.id, context, propsRef]\n );\n\n const onPaste = React.useCallback(\n (event: React.ClipboardEvent<KeyInputElement>) => {\n if (!context.enablePaste) return;\n\n propsRef.current.onPaste?.(event);\n if (event.defaultPrevented) return;\n\n const content = event.clipboardData.getData(\"text\");\n const lines = content.split(/\\r?\\n/).filter((line) => line.trim());\n\n if (lines.length > 1) {\n event.preventDefault();\n\n const parsed: ItemData[] = [];\n\n for (const line of lines) {\n let key = \"\";\n let value = \"\";\n\n if (line.includes(\"=\")) {\n const parts = line.split(\"=\");\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\"=\").trim(), context.stripQuotes);\n } else if (line.includes(\":\")) {\n const parts = line.split(\":\");\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\":\").trim(), context.stripQuotes);\n } else if (/\\s{2,}|\\t/.test(line)) {\n const parts = line.split(/\\s{2,}|\\t/);\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\" \").trim(), context.stripQuotes);\n }\n\n if (key) {\n parsed.push({ id: createItemId(), key, value });\n }\n }\n\n if (parsed.length > 0) {\n const state = store.getState();\n const currentIndex = state.value.findIndex((item) => item.id === itemData.id);\n\n let newValue: ItemData[];\n if (itemData.key === \"\" && itemData.value === \"\") {\n newValue = [...state.value.slice(0, currentIndex), ...parsed, ...state.value.slice(currentIndex + 1)];\n } else {\n newValue = [...state.value.slice(0, currentIndex + 1), ...parsed, ...state.value.slice(currentIndex + 1)];\n }\n\n if (context.maxItems !== undefined) {\n newValue = newValue.slice(0, context.maxItems);\n }\n\n store.setState(\"value\", newValue);\n\n if (context.onPaste) {\n context.onPaste(event.nativeEvent as unknown as ClipboardEvent, parsed);\n }\n }\n }\n },\n [context, store, itemData, propsRef]\n );\n\n const KeyInputPrimitive = asChild ? Slot : Input;\n\n return (\n <KeyInputPrimitive\n aria-invalid={isInvalid}\n aria-describedby={isInvalid ? getErrorId(context.rootId, itemData.id, \"key\") : undefined}\n data-slot=\"key-value-key-input\"\n autoCapitalize=\"off\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n disabled={isDisabled}\n readOnly={isReadOnly}\n required={isRequired}\n placeholder={context.keyPlaceholder}\n {...keyInputProps}\n ref={composedRef}\n value={itemData.key}\n onChange={onChange}\n onPaste={onPaste}\n />\n );\n}\n\ninterface KeyValueValueInputProps extends Omit<React.ComponentProps<\"textarea\">, \"rows\"> {\n maxRows?: number;\n asChild?: boolean;\n}\n\nfunction KeyValueValueInput(props: KeyValueValueInputProps) {\n const {\n onChange: onChangeProp,\n asChild,\n disabled,\n readOnly,\n required,\n className,\n maxRows,\n style,\n ...valueInputProps\n } = props;\n\n const context = useKeyValueContext(VALUE_INPUT_NAME);\n const itemData = useKeyValueItemContext(VALUE_INPUT_NAME);\n const store = useStoreContext(VALUE_INPUT_NAME);\n const errors = useStore((state) => state.errors);\n\n const propsRef = useAsRef({\n onChange: onChangeProp\n });\n\n const isDisabled = disabled || context.disabled;\n const isReadOnly = readOnly || context.readOnly;\n const isRequired = required || context.required;\n const isInvalid = errors[itemData.id]?.value !== undefined;\n const maxHeight = maxRows ? `calc(${maxRows} * 1.5em + 1rem)` : undefined;\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n propsRef.current.onChange?.(event);\n\n const state = store.getState();\n const newValue = state.value.map((item) => {\n if (item.id !== itemData.id) return item;\n const updated = { ...item, value: event.target.value };\n if (context.trim) updated.value = updated.value.trim();\n return updated;\n });\n\n store.setState(\"value\", newValue);\n\n const updatedItemData = newValue.find((item) => item.id === itemData.id);\n if (updatedItemData) {\n const errors: { key?: string; value?: string } = {};\n\n if (context.onKeyValidate) {\n const keyError = context.onKeyValidate(updatedItemData.key, newValue);\n if (keyError) errors.key = keyError;\n }\n\n if (!context.allowDuplicateKeys) {\n const duplicateKey = newValue.find(\n (item) => item.id !== updatedItemData.id && item.key === updatedItemData.key && updatedItemData.key !== \"\"\n );\n if (duplicateKey) {\n errors.key = context.labels.duplicateKey;\n }\n }\n\n if (context.onValueValidate) {\n const valueError = context.onValueValidate(updatedItemData.value, updatedItemData.key, newValue);\n if (valueError) errors.value = valueError;\n }\n\n const newErrorsState = { ...state.errors };\n if (Object.keys(errors).length > 0) {\n newErrorsState[itemData.id] = errors;\n } else {\n delete newErrorsState[itemData.id];\n }\n store.setState(\"errors\", newErrorsState);\n }\n },\n [store, itemData.id, context, propsRef]\n );\n\n const ValueInputPrimitive = asChild ? Slot : Textarea;\n\n return (\n <ValueInputPrimitive\n aria-invalid={isInvalid}\n aria-describedby={isInvalid ? getErrorId(context.rootId, itemData.id, \"value\") : undefined}\n data-slot=\"key-value-value-input\"\n autoCapitalize=\"off\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n disabled={isDisabled}\n readOnly={isReadOnly}\n required={isRequired}\n placeholder={context.valuePlaceholder}\n {...valueInputProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: ô giá trị phải trông như ô khoá (`Input`) đứng ngay cạnh khi còn MỘT\n * dòng. `Textarea` của gói theo comitor-ds: `text-base md:text-sm` + `py-2` (cao 38px ở\n * desktop, 42px và chữ 16px ở mobile), nền `bg-transparent dark:bg-input-fill`, có\n * `shadow-xs`; còn `Input` là `h-9 text-sm bg-background`, không bóng. Ghi đè ở ĐÂY chứ\n * 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\n * mép trên 7px — đúng chỗ `Input` căn giữa dòng chữ trong 36px), `bg-background` ở cả hai\n * theme, bỏ bóng. Nhiều dòng thì `field-sizing-content` vẫn cho ô cao dần như cũ.\n */\n \"field-sizing-content min-h-9 resize-none py-1.5 text-sm md:text-sm bg-background dark:bg-background shadow-none\",\n maxRows && \"overflow-y-auto\",\n className\n )}\n style={{\n ...style,\n ...(maxHeight && { maxHeight })\n }}\n value={itemData.value}\n onChange={onChange}\n />\n );\n}\n\ninterface KeyValueRemoveProps extends React.ComponentProps<typeof Button> {}\n\nfunction KeyValueRemove(props: KeyValueRemoveProps) {\n const { onClick: onClickProp, children, className, ...removeProps } = props;\n\n const context = useKeyValueContext(REMOVE_NAME);\n const itemData = useKeyValueItemContext(REMOVE_NAME);\n const store = useStoreContext(REMOVE_NAME);\n\n const propsRef = useAsRef({\n onClick: onClickProp\n });\n const value = useStore((state) => state.value);\n const isDisabled = context.disabled || value.length <= context.minItems;\n const itemIndex = value.findIndex((item) => item.id === itemData.id);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<RemoveElement>) => {\n propsRef.current.onClick?.(event);\n\n const state = store.getState();\n if (state.value.length <= context.minItems) return;\n\n const itemToRemove = state.value.find((item) => item.id === itemData.id);\n if (!itemToRemove) return;\n\n /*\n * ⚠ Khác thượng nguồn (a11y): nút đang giữ focus biến mất cùng dòng của nó, và focus rơi về\n * `<body>` — người dùng bàn phím phải Tab lại từ đầu trang (WCAG 2.4.3). Nếu nút có focus lúc\n * bấm thì sau khi xoá, đưa focus sang nút xoá của dòng kế (hoặc dòng trước nếu vừa xoá dòng\n * cuối), không còn nút nào dùng được thì sang nút thêm.\n */\n const button = event.currentTarget;\n const hadFocus = button.ownerDocument.activeElement === button;\n const root = button.closest<HTMLElement>('[data-slot=\"key-value\"]');\n const removedIndex = state.value.indexOf(itemToRemove);\n\n const newValue = state.value.filter((item) => item.id !== itemData.id);\n const newErrors = { ...state.errors };\n delete newErrors[itemData.id];\n\n store.setState(\"value\", newValue);\n store.setState(\"errors\", newErrors);\n\n context.onRemove?.(itemToRemove);\n\n if (hadFocus && root) {\n requestAnimationFrame(() => {\n const removeButtons = root.querySelectorAll<HTMLElement>('[data-slot=\"key-value-remove\"]:not(:disabled)');\n const nextTarget =\n removeButtons[Math.min(removedIndex, removeButtons.length - 1)] ??\n root.querySelector<HTMLElement>('[data-slot=\"key-value-add\"]:not(:disabled)');\n nextTarget?.focus();\n });\n }\n },\n [store, context, itemData.id, propsRef]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"key-value-remove\"\n variant=\"outline\"\n size=\"icon\"\n /*\n * ⚠ Khác thượng nguồn (a11y): nút chỉ có icon ✕ và bản gốc không đặt tên nào — trình đọc màn\n * hình đọc \"nút\" trơn, lặp lại ở mọi dòng. Tên mặc định nói RÕ dòng nào: \"Remove API_KEY\",\n * hoặc \"Remove row 3\" khi khoá còn trống. Đổi qua `labels.remove` của `KeyValue`, hoặc `aria-label`.\n */\n aria-label={context.labels.remove(itemData.key, itemIndex + 1)}\n disabled={isDisabled}\n {...removeProps}\n // ⚠ Khác thượng nguồn: `shrink-0` — nút là mục flex cạnh hai ô `w-full`; thiếu nó thì flex\n // bóp nút còn ~18×36px. `size=\"icon\"` của `Button` đã là ô vuông 36×36 (`h-9 w-9`).\n className={cn(\"shrink-0\", className)}\n onClick={onClick}\n >\n {/* `size-4`: `Button` của gói không tự đặt cỡ cho svg con như shadcn — thiếu là icon 24px. */}\n {children ?? <X aria-hidden=\"true\" className=\"size-4\" />}\n </Button>\n );\n}\n\ntype KeyValueAddProps = React.ComponentProps<typeof Button>;\n\nfunction KeyValueAdd(props: KeyValueAddProps) {\n const { onClick: onClickProp, children, ...addProps } = props;\n\n const context = useKeyValueContext(ADD_NAME);\n const store = useStoreContext(ADD_NAME);\n\n const propsRef = useAsRef({\n onClick: onClickProp\n });\n const value = useStore((state) => state.value);\n const isDisabled = context.disabled || (context.maxItems !== undefined && value.length >= context.maxItems);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<AddElement>) => {\n propsRef.current.onClick?.(event);\n\n const state = store.getState();\n if (context.maxItems !== undefined && state.value.length >= context.maxItems) {\n return;\n }\n\n const newItem: ItemData = {\n id: createItemId(),\n key: \"\",\n value: \"\"\n };\n\n const newValue = [...state.value, newItem];\n store.setState(\"value\", newValue);\n store.setState(\"focusedId\", newItem.id);\n\n context.onAdd?.(newItem);\n },\n [store, context, propsRef]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"key-value-add\"\n variant=\"outline\"\n disabled={isDisabled}\n {...addProps}\n onClick={onClick}\n >\n {children ?? (\n <>\n <Plus aria-hidden=\"true\" className=\"size-4\" />\n {context.labels.add}\n </>\n )}\n </Button>\n );\n}\n\ninterface KeyValueErrorProps extends DivProps {\n field: Field;\n}\n\nfunction KeyValueError(props: KeyValueErrorProps) {\n const { field, asChild, className, ...errorProps } = props;\n\n const context = useKeyValueContext(ERROR_NAME);\n const itemData = useKeyValueItemContext(ERROR_NAME);\n\n const errors = useStore((state) => state.errors);\n const error = errors[itemData.id]?.[field];\n\n if (!error) return null;\n\n const ErrorPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ErrorPrimitive\n id={getErrorId(context.rootId, itemData.id, field)}\n role=\"alert\"\n {...errorProps}\n className={cn(\n // ⚠ Khác thượng nguồn: `text-destructive` (bậc TÔ NỀN) → `text-destructive-ink`, bậc dành\n // cho chữ trên nền trang — cùng token với chữ lỗi của `Input`.\n \"text-sm font-medium text-destructive-ink\",\n className\n )}\n >\n {error}\n </ErrorPrimitive>\n );\n}\n\nexport {\n type ItemData as KeyValueItemData,\n KeyValue,\n KeyValueAdd,\n type KeyValueAddProps,\n KeyValueError,\n type KeyValueErrorProps,\n KeyValueItem,\n type KeyValueItemProps,\n KeyValueKeyInput,\n type KeyValueKeyInputProps,\n KeyValueList,\n type KeyValueListProps,\n type KeyValueProps,\n KeyValueRemove,\n type KeyValueRemoveProps,\n KeyValueValueInput,\n type KeyValueValueInputProps,\n useStore as useKeyValueStore\n};\n"],"mappings":"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","names":["errors"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/key-value.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/key-value) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Plus, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useComitorLabels } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useAsRef } from \"../../lib/use-as-ref.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\nimport { VisuallyHiddenInput } from \"../../lib/visually-hidden-input.js\";\nimport { Button } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport type { KeyValueLabels } from \"./key-value-constants.js\";\nimport { Textarea } from \"./textarea.js\";\n\nconst ROOT_NAME = \"KeyValue\";\nconst LIST_NAME = \"KeyValueList\";\nconst ITEM_NAME = \"KeyValueItem\";\nconst KEY_INPUT_NAME = \"KeyValueKeyInput\";\nconst VALUE_INPUT_NAME = \"KeyValueValueInput\";\nconst REMOVE_NAME = \"KeyValueRemove\";\nconst ADD_NAME = \"KeyValueAdd\";\nconst ERROR_NAME = \"KeyValueError\";\n\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Field = \"key\" | \"value\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof KeyValue>;\ntype KeyInputElement = React.ComponentRef<typeof KeyValueKeyInput>;\ntype RemoveElement = React.ComponentRef<typeof KeyValueRemove>;\ntype AddElement = React.ComponentRef<typeof KeyValueAdd>;\n\nfunction getErrorId(rootId: string, itemId: string, field: Field) {\n return `${rootId}-${itemId}-${field}-error`;\n}\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc gọi thẳng `crypto.randomUUID()`. Hàm đó chỉ tồn tại trong NGỮ CẢNH\n * AN TOÀN (HTTPS hoặc localhost) — mở trang quản trị qua `http://` bằng IP nội bộ là `TypeError`\n * ngay lúc render dòng đầu tiên. Id ở đây chỉ làm key của React và nối vào id của thông báo lỗi,\n * không cần ngẫu nhiên mật mã, nên có đường lui.\n */\nlet fallbackIdCounter = 0;\n\nfunction createItemId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n fallbackIdCounter += 1;\n return `kv-${Date.now().toString(36)}-${fallbackIdCounter}`;\n}\n\nfunction removeQuotes(string: string, shouldStrip: boolean): string {\n if (!shouldStrip) return string;\n\n const trimmed = string.trim();\n if ((trimmed.startsWith('\"') && trimmed.endsWith('\"')) || (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => KeyValueState;\n setState: <K extends keyof KeyValueState>(key: K, value: KeyValueState[K]) => void;\n notify: () => void;\n}\n\nfunction useStore<T>(selector: (state: KeyValueState) => T, ogStore?: Store | null): T {\n const contextStore = React.useContext(StoreContext);\n\n const store = ogStore ?? contextStore;\n\n if (!store) {\n throw new Error(`\\`useKeyValueStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ItemData {\n id: string;\n key: string;\n value: string;\n}\n\ninterface KeyValueState {\n value: ItemData[];\n focusedId: string | null;\n errors: Record<string, { key?: string; value?: string }>;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface KeyValueContextValue {\n onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n onAdd?: (value: ItemData) => void;\n onRemove?: (value: ItemData) => void;\n onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n onValueValidate?: (value: string, key: string, items: ItemData[]) => string | undefined;\n rootId: string;\n maxItems?: number;\n minItems: number;\n keyPlaceholder: string;\n valuePlaceholder: string;\n labels: KeyValueLabels;\n allowDuplicateKeys: boolean;\n enablePaste: boolean;\n trim: boolean;\n stripQuotes: boolean;\n disabled: boolean;\n readOnly: boolean;\n required: boolean;\n}\n\nconst KeyValueContext = React.createContext<KeyValueContextValue | null>(null);\n\nfunction useKeyValueContext(consumerName: string) {\n const context = React.useContext(KeyValueContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\n/**\n * Ghi MỘT ô (khoá hoặc giá trị) của một dòng vào store rồi kiểm lỗi lại dòng đó. Dùng chung cho\n * hai ô nhập — trước `3.0.1` khối này chép nguyên hai lần, mỗi ô một bản.\n */\nfunction commitItemField(store: Store, context: KeyValueContextValue, itemId: string, field: Field, text: string) {\n const state = store.getState();\n const newValue = state.value.map((item) => (item.id === itemId ? { ...item, [field]: text } : item));\n\n store.setState(\"value\", newValue);\n\n const updatedItemData = newValue.find((item) => item.id === itemId);\n if (!updatedItemData) return;\n\n const errors: { key?: string; value?: string } = {};\n\n if (context.onKeyValidate) {\n const keyError = context.onKeyValidate(updatedItemData.key, newValue);\n if (keyError) errors.key = keyError;\n }\n\n if (!context.allowDuplicateKeys) {\n const duplicateKey = newValue.find(\n (item) => item.id !== updatedItemData.id && item.key === updatedItemData.key && updatedItemData.key !== \"\"\n );\n if (duplicateKey) {\n errors.key = context.labels.duplicateKey;\n }\n }\n\n if (context.onValueValidate) {\n const valueError = context.onValueValidate(updatedItemData.value, updatedItemData.key, newValue);\n if (valueError) errors.value = valueError;\n }\n\n const newErrorsState = { ...state.errors };\n if (Object.keys(errors).length > 0) {\n newErrorsState[itemId] = errors;\n } else {\n delete newErrorsState[itemId];\n }\n store.setState(\"errors\", newErrorsState);\n}\n\n/*\n * ⚠ Khác thượng nguồn (3.0.1): với `trim`, Ô NHẬP hiện đúng chữ đang gõ, còn STORE nhận bản đã cắt.\n * Bản gốc cắt ngay trong `onChange` rồi đưa chính chuỗi đã cắt về lại ô, nên dấu cách vừa gõ ở cuối\n * biến mất trước khi chữ kế tiếp tới — với `trim` mặc định `true`, không gõ được \"Contract term\" hay\n * \"Bearer token\" theo cách bình thường (ra \"Contractterm\").\n * Mọi thứ app nhận (`onValueChange`, ô ẩn trong form, kiểm lỗi, trùng khoá) vẫn LUÔN là bản đã cắt,\n * đúng như bản gốc — kể cả khi Enter gửi form ngay sau một dấu cách. Chỉ phần hiển thị giữ khoảng\n * trắng, cho tới khi rời ô.\n */\nfunction useTrimDraft(committed: string) {\n const [draft, setDraft] = React.useState<string | null>(null);\n // Bản nháp chỉ được hiện khi nó còn khớp store: store đổi từ nơi khác (app đặt `value`, dán nhiều\n // dòng) thì hiện giá trị mới.\n const shown = draft !== null && draft.trim() === committed ? draft : committed;\n return [shown, setDraft] as const;\n}\n\ninterface KeyValueProps extends Omit<DivProps, \"onPaste\" | \"defaultValue\"> {\n id?: string;\n defaultValue?: ItemData[];\n value?: ItemData[];\n onValueChange?: (value: ItemData[]) => void;\n maxItems?: number;\n minItems?: number;\n /** Placeholder của ô khoá — thắng `labels.keyPlaceholder` (mặc định \"Key\"). */\n keyPlaceholder?: string;\n /** Placeholder của ô giá trị — thắng `labels.valuePlaceholder` (mặc định \"Value\"). */\n valuePlaceholder?: string;\n /**\n * Chuỗi hiển thị và chuỗi cho trình đọc màn hình — truyền MỘT PHẦN là đủ, phần còn lại theo\n * namespace `keyValue` của `ComitorLocaleProvider` (mặc định tiếng Anh). ⚠ Thêm so với thượng\n * nguồn (bản gốc chôn cứng \"Add\" / \"Duplicate key\" và để nút xoá không có tên).\n */\n labels?: Partial<KeyValueLabels>;\n name?: string;\n allowDuplicateKeys?: boolean;\n enablePaste?: boolean;\n trim?: boolean;\n stripQuotes?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n required?: boolean;\n onPaste?: (event: ClipboardEvent, items: ItemData[]) => void;\n onAdd?: (value: ItemData) => void;\n onRemove?: (value: ItemData) => void;\n onKeyValidate?: (key: string, value: ItemData[]) => string | undefined;\n onValueValidate?: (value: string, key: string, items: ItemData[]) => string | undefined;\n}\n\n/**\n * KeyValue — danh sách cặp khoá/giá trị sửa được: trường tuỳ biến của bản ghi CRM, header /\n * metadata của webhook ở trang quản trị. Dán nhiều dòng `KEY=value` (hoặc `KEY: value`, hoặc\n * cách bằng tab) vào ô khoá là tách thành nhiều cặp.\n */\nfunction KeyValue(props: KeyValueProps) {\n const {\n value: valueProp,\n defaultValue,\n onValueChange,\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n maxItems,\n minItems = 0,\n labels: labelsProp,\n keyPlaceholder: keyPlaceholderProp,\n valuePlaceholder: valuePlaceholderProp,\n allowDuplicateKeys = false,\n asChild,\n enablePaste = true,\n trim = true,\n stripQuotes = true,\n disabled = false,\n readOnly = false,\n required = false,\n className,\n id,\n name,\n ref,\n ...rootProps\n } = props;\n\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n\n const labels = useComitorLabels(\"keyValue\", labelsProp);\n const keyPlaceholder = keyPlaceholderProp ?? labels.keyPlaceholder;\n const valuePlaceholder = valuePlaceholderProp ?? labels.valuePlaceholder;\n\n const [formTrigger, setFormTrigger] = React.useState<RootElement | null>(null);\n const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node));\n const isFormControl = formTrigger ? !!formTrigger.closest(\"form\") : true;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef<KeyValueState>(() => ({\n value: valueProp ?? defaultValue ?? [{ id: createItemId(), key: \"\", value: \"\" }],\n focusedId: null,\n errors: {}\n }));\n const propsRef = useAsRef({ onValueChange });\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, val) => {\n if (Object.is(stateRef.current[key], val)) return;\n\n if (key === \"value\" && Array.isArray(val)) {\n stateRef.current.value = val as ItemData[];\n propsRef.current.onValueChange?.(val as ItemData[]);\n } else {\n stateRef.current[key] = val;\n }\n\n store.notify();\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n }\n };\n }, [listenersRef, stateRef, propsRef]);\n\n const value = useStore((state) => state.value, store);\n const errors = useStore((state) => state.errors, store);\n const isInvalid = Object.keys(errors).length > 0;\n\n useIsomorphicLayoutEffect(() => {\n if (valueProp !== undefined) {\n store.setState(\"value\", valueProp);\n }\n }, [valueProp]);\n\n const contextValue = React.useMemo<KeyValueContextValue>(\n () => ({\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n rootId,\n maxItems,\n minItems,\n keyPlaceholder,\n valuePlaceholder,\n labels,\n allowDuplicateKeys,\n enablePaste,\n trim,\n stripQuotes,\n disabled,\n readOnly,\n required\n }),\n [\n onPaste,\n onAdd,\n onRemove,\n onKeyValidate,\n onValueValidate,\n rootId,\n disabled,\n readOnly,\n required,\n maxItems,\n minItems,\n keyPlaceholder,\n valuePlaceholder,\n labels,\n allowDuplicateKeys,\n enablePaste,\n trim,\n stripQuotes\n ]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <KeyValueContext.Provider value={contextValue}>\n <RootPrimitive\n id={id}\n data-slot=\"key-value\"\n data-disabled={disabled ? \"\" : undefined}\n data-invalid={isInvalid ? \"\" : undefined}\n data-readonly={readOnly ? \"\" : undefined}\n {...rootProps}\n ref={composedRef}\n className={cn(\"flex flex-col gap-2\", className)}\n />\n {isFormControl && (\n <VisuallyHiddenInput\n type=\"hidden\"\n control={formTrigger}\n name={name}\n value={value}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n />\n )}\n </KeyValueContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface KeyValueListProps extends DivProps {\n orientation?: Orientation;\n}\n\nfunction KeyValueList(props: KeyValueListProps) {\n const { orientation = \"vertical\", asChild, className, ...listProps } = props;\n\n const value = useStore((state) => state.value);\n\n const ListPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ListPrimitive\n role=\"list\"\n /*\n * ⚠ Khác thượng nguồn (a11y): bỏ `aria-orientation`. Thuộc tính đó KHÔNG được phép trên\n * `role=\"list\"` (ARIA 1.2 chỉ cho nó ở listbox, menu, tablist, toolbar, radiogroup, slider…)\n * — axe báo `aria-allowed-attr`. Hướng vẫn còn ở `data-orientation` cho CSS.\n */\n data-slot=\"key-value-list\"\n data-orientation={orientation}\n {...listProps}\n className={cn(\"flex\", orientation === \"vertical\" ? \"flex-col gap-2\" : \"flex-row gap-2\", className)}\n >\n {value.map((item) => {\n const children = React.Children.toArray(props.children);\n\n return (\n <KeyValueItemContext.Provider key={item.id} value={item}>\n {children}\n </KeyValueItemContext.Provider>\n );\n })}\n </ListPrimitive>\n );\n}\n\nconst KeyValueItemContext = React.createContext<ItemData | null>(null);\n\nfunction useKeyValueItemContext(consumerName: string) {\n const context = React.useContext(KeyValueItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${LIST_NAME}\\``);\n }\n return context;\n}\n\ninterface KeyValueItemProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\nfunction KeyValueItem(props: KeyValueItemProps) {\n const { asChild, className, ...itemProps } = props;\n const itemData = useKeyValueItemContext(ITEM_NAME);\n\n const focusedId = useStore((state) => state.focusedId);\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ItemPrimitive\n role=\"listitem\"\n data-slot=\"key-value-item\"\n data-highlighted={focusedId === itemData.id ? \"\" : undefined}\n {...itemProps}\n className={cn(\"flex items-start gap-2\", className)}\n />\n );\n}\n\ninterface KeyValueKeyInputProps extends React.ComponentProps<\"input\"> {\n asChild?: boolean;\n}\n\nfunction KeyValueKeyInput(props: KeyValueKeyInputProps) {\n const {\n onChange: onChangeProp,\n onBlur: onBlurProp,\n onPaste: onPasteProp,\n asChild,\n disabled,\n readOnly,\n required,\n ref,\n ...keyInputProps\n } = props;\n\n const context = useKeyValueContext(KEY_INPUT_NAME);\n const itemData = useKeyValueItemContext(KEY_INPUT_NAME);\n const store = useStoreContext(KEY_INPUT_NAME);\n const errors = useStore((state) => state.errors);\n const focusedId = useStore((state) => state.focusedId);\n\n const inputRef = React.useRef<KeyInputElement>(null);\n const composedRef = useComposedRefs(ref, inputRef);\n\n /*\n * ⚠ Khác thượng nguồn (a11y): `KeyValueAdd` ghi id dòng mới vào `focusedId` nhưng bản gốc không\n * bao giờ dùng nó để đưa focus — bấm \"Thêm\" xong focus vẫn đứng ở nút, người dùng bàn phím phải\n * tự Tab ngược lên dòng vừa thêm. Ở đây ô KHOÁ của dòng mới tự nhận focus. Chỉ chạy khi\n * `focusedId` ĐỔI sang dòng này (mà chỉ `KeyValueAdd` ghi nó), nên dòng có sẵn lúc mount và dòng\n * sinh ra do dán không bị cướp focus.\n */\n React.useEffect(() => {\n if (focusedId === itemData.id) {\n inputRef.current?.focus();\n }\n }, [focusedId, itemData.id]);\n\n const propsRef = useAsRef({\n onChange: onChangeProp,\n onBlur: onBlurProp,\n onPaste: onPasteProp\n });\n\n const isDisabled = disabled || context.disabled;\n const isReadOnly = readOnly || context.readOnly;\n const isRequired = required || context.required;\n const isInvalid = errors[itemData.id]?.key !== undefined;\n const [shownKey, setKeyDraft] = useTrimDraft(itemData.key);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent<KeyInputElement>) => {\n const text = event.target.value;\n if (context.trim) setKeyDraft(text);\n commitItemField(store, context, itemData.id, \"key\", context.trim ? text.trim() : text);\n propsRef.current.onChange?.(event);\n },\n [store, itemData.id, context, propsRef, setKeyDraft]\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<KeyInputElement>) => {\n propsRef.current.onBlur?.(event);\n setKeyDraft(null);\n },\n [propsRef, setKeyDraft]\n );\n\n const onPaste = React.useCallback(\n (event: React.ClipboardEvent<KeyInputElement>) => {\n if (!context.enablePaste) return;\n\n propsRef.current.onPaste?.(event);\n if (event.defaultPrevented) return;\n\n const content = event.clipboardData.getData(\"text\");\n const lines = content.split(/\\r?\\n/).filter((line) => line.trim());\n\n if (lines.length > 1) {\n event.preventDefault();\n\n const parsed: ItemData[] = [];\n\n for (const line of lines) {\n let key = \"\";\n let value = \"\";\n\n if (line.includes(\"=\")) {\n const parts = line.split(\"=\");\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\"=\").trim(), context.stripQuotes);\n } else if (line.includes(\":\")) {\n const parts = line.split(\":\");\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\":\").trim(), context.stripQuotes);\n } else if (/\\s{2,}|\\t/.test(line)) {\n const parts = line.split(/\\s{2,}|\\t/);\n key = parts[0]?.trim() ?? \"\";\n value = removeQuotes(parts.slice(1).join(\" \").trim(), context.stripQuotes);\n }\n\n if (key) {\n parsed.push({ id: createItemId(), key, value });\n }\n }\n\n if (parsed.length > 0) {\n const state = store.getState();\n const currentIndex = state.value.findIndex((item) => item.id === itemData.id);\n\n let newValue: ItemData[];\n if (itemData.key === \"\" && itemData.value === \"\") {\n newValue = [...state.value.slice(0, currentIndex), ...parsed, ...state.value.slice(currentIndex + 1)];\n } else {\n newValue = [...state.value.slice(0, currentIndex + 1), ...parsed, ...state.value.slice(currentIndex + 1)];\n }\n\n if (context.maxItems !== undefined) {\n newValue = newValue.slice(0, context.maxItems);\n }\n\n store.setState(\"value\", newValue);\n\n if (context.onPaste) {\n context.onPaste(event.nativeEvent as unknown as ClipboardEvent, parsed);\n }\n }\n }\n },\n [context, store, itemData, propsRef]\n );\n\n const KeyInputPrimitive = asChild ? Slot : Input;\n\n return (\n <KeyInputPrimitive\n aria-invalid={isInvalid}\n aria-describedby={isInvalid ? getErrorId(context.rootId, itemData.id, \"key\") : undefined}\n data-slot=\"key-value-key-input\"\n autoCapitalize=\"off\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n disabled={isDisabled}\n readOnly={isReadOnly}\n required={isRequired}\n placeholder={context.keyPlaceholder}\n {...keyInputProps}\n ref={composedRef}\n value={shownKey}\n onChange={onChange}\n onBlur={onBlur}\n onPaste={onPaste}\n />\n );\n}\n\ninterface KeyValueValueInputProps extends Omit<React.ComponentProps<\"textarea\">, \"rows\"> {\n maxRows?: number;\n asChild?: boolean;\n}\n\nfunction KeyValueValueInput(props: KeyValueValueInputProps) {\n const {\n onChange: onChangeProp,\n onBlur: onBlurProp,\n asChild,\n disabled,\n readOnly,\n required,\n className,\n maxRows,\n style,\n ...valueInputProps\n } = props;\n\n const context = useKeyValueContext(VALUE_INPUT_NAME);\n const itemData = useKeyValueItemContext(VALUE_INPUT_NAME);\n const store = useStoreContext(VALUE_INPUT_NAME);\n const errors = useStore((state) => state.errors);\n\n const propsRef = useAsRef({\n onChange: onChangeProp,\n onBlur: onBlurProp\n });\n\n const isDisabled = disabled || context.disabled;\n const isReadOnly = readOnly || context.readOnly;\n const isRequired = required || context.required;\n const isInvalid = errors[itemData.id]?.value !== undefined;\n const maxHeight = maxRows ? `calc(${maxRows} * 1.5em + 1rem)` : undefined;\n const [shownValue, setValueDraft] = useTrimDraft(itemData.value);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n propsRef.current.onChange?.(event);\n const text = event.target.value;\n if (context.trim) setValueDraft(text);\n commitItemField(store, context, itemData.id, \"value\", context.trim ? text.trim() : text);\n },\n [store, itemData.id, context, propsRef, setValueDraft]\n );\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent<HTMLTextAreaElement>) => {\n propsRef.current.onBlur?.(event);\n setValueDraft(null);\n },\n [propsRef, setValueDraft]\n );\n\n const ValueInputPrimitive = asChild ? Slot : Textarea;\n\n return (\n <ValueInputPrimitive\n aria-invalid={isInvalid}\n aria-describedby={isInvalid ? getErrorId(context.rootId, itemData.id, \"value\") : undefined}\n data-slot=\"key-value-value-input\"\n autoCapitalize=\"off\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n disabled={isDisabled}\n readOnly={isReadOnly}\n required={isRequired}\n placeholder={context.valuePlaceholder}\n {...valueInputProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: ô giá trị phải trông như ô khoá (`Input`) đứng ngay cạnh khi còn MỘT\n * dòng. `Textarea` của gói theo comitor-ds: `text-base md:text-sm` + `py-2` (cao 38px ở\n * desktop, 42px và chữ 16px ở mobile), nền `bg-transparent dark:bg-input-fill`, có\n * `shadow-xs`; còn `Input` là `h-9 text-sm bg-background`, không bóng. Ghi đè ở ĐÂY chứ\n * 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\n * mép trên 7px — đúng chỗ `Input` căn giữa dòng chữ trong 36px), `bg-background` ở cả hai\n * theme, bỏ bóng. Nhiều dòng thì `field-sizing-content` vẫn cho ô cao dần như cũ.\n */\n \"field-sizing-content min-h-9 resize-none py-1.5 text-sm md:text-sm bg-background dark:bg-background shadow-none\",\n maxRows && \"overflow-y-auto\",\n className\n )}\n style={{\n ...style,\n ...(maxHeight && { maxHeight })\n }}\n value={shownValue}\n onChange={onChange}\n onBlur={onBlur}\n />\n );\n}\n\ninterface KeyValueRemoveProps extends React.ComponentProps<typeof Button> {}\n\nfunction KeyValueRemove(props: KeyValueRemoveProps) {\n const { onClick: onClickProp, children, className, ...removeProps } = props;\n\n const context = useKeyValueContext(REMOVE_NAME);\n const itemData = useKeyValueItemContext(REMOVE_NAME);\n const store = useStoreContext(REMOVE_NAME);\n\n const propsRef = useAsRef({\n onClick: onClickProp\n });\n const value = useStore((state) => state.value);\n const isDisabled = context.disabled || value.length <= context.minItems;\n const itemIndex = value.findIndex((item) => item.id === itemData.id);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<RemoveElement>) => {\n propsRef.current.onClick?.(event);\n\n const state = store.getState();\n if (state.value.length <= context.minItems) return;\n\n const itemToRemove = state.value.find((item) => item.id === itemData.id);\n if (!itemToRemove) return;\n\n /*\n * ⚠ Khác thượng nguồn (a11y): nút đang giữ focus biến mất cùng dòng của nó, và focus rơi về\n * `<body>` — người dùng bàn phím phải Tab lại từ đầu trang (WCAG 2.4.3). Nếu nút có focus lúc\n * bấm thì sau khi xoá, đưa focus sang nút xoá của dòng kế (hoặc dòng trước nếu vừa xoá dòng\n * cuối), không còn nút nào dùng được thì sang nút thêm.\n */\n const button = event.currentTarget;\n const hadFocus = button.ownerDocument.activeElement === button;\n const root = button.closest<HTMLElement>('[data-slot=\"key-value\"]');\n const removedIndex = state.value.indexOf(itemToRemove);\n\n const newValue = state.value.filter((item) => item.id !== itemData.id);\n const newErrors = { ...state.errors };\n delete newErrors[itemData.id];\n\n store.setState(\"value\", newValue);\n store.setState(\"errors\", newErrors);\n\n context.onRemove?.(itemToRemove);\n\n if (hadFocus && root) {\n requestAnimationFrame(() => {\n const removeButtons = root.querySelectorAll<HTMLElement>('[data-slot=\"key-value-remove\"]:not(:disabled)');\n const nextTarget =\n removeButtons[Math.min(removedIndex, removeButtons.length - 1)] ??\n root.querySelector<HTMLElement>('[data-slot=\"key-value-add\"]:not(:disabled)');\n nextTarget?.focus();\n });\n }\n },\n [store, context, itemData.id, propsRef]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"key-value-remove\"\n variant=\"outline\"\n size=\"icon\"\n /*\n * ⚠ Khác thượng nguồn (a11y): nút chỉ có icon ✕ và bản gốc không đặt tên nào — trình đọc màn\n * hình đọc \"nút\" trơn, lặp lại ở mọi dòng. Tên mặc định nói RÕ dòng nào: \"Remove API_KEY\",\n * hoặc \"Remove row 3\" khi khoá còn trống. Đổi qua `labels.remove` của `KeyValue`, hoặc `aria-label`.\n */\n aria-label={context.labels.remove(itemData.key, itemIndex + 1)}\n disabled={isDisabled}\n {...removeProps}\n // ⚠ Khác thượng nguồn: `shrink-0` — nút là mục flex cạnh hai ô `w-full`; thiếu nó thì flex\n // bóp nút còn ~18×36px. `size=\"icon\"` của `Button` đã là ô vuông 36×36 (`h-9 w-9`).\n className={cn(\"shrink-0\", className)}\n onClick={onClick}\n >\n {/* `size-4`: `Button` của gói không tự đặt cỡ cho svg con như shadcn — thiếu là icon 24px. */}\n {children ?? <X aria-hidden=\"true\" className=\"size-4\" />}\n </Button>\n );\n}\n\ntype KeyValueAddProps = React.ComponentProps<typeof Button>;\n\nfunction KeyValueAdd(props: KeyValueAddProps) {\n const { onClick: onClickProp, children, ...addProps } = props;\n\n const context = useKeyValueContext(ADD_NAME);\n const store = useStoreContext(ADD_NAME);\n\n const propsRef = useAsRef({\n onClick: onClickProp\n });\n const value = useStore((state) => state.value);\n const isDisabled = context.disabled || (context.maxItems !== undefined && value.length >= context.maxItems);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent<AddElement>) => {\n propsRef.current.onClick?.(event);\n\n const state = store.getState();\n if (context.maxItems !== undefined && state.value.length >= context.maxItems) {\n return;\n }\n\n const newItem: ItemData = {\n id: createItemId(),\n key: \"\",\n value: \"\"\n };\n\n const newValue = [...state.value, newItem];\n store.setState(\"value\", newValue);\n store.setState(\"focusedId\", newItem.id);\n\n context.onAdd?.(newItem);\n },\n [store, context, propsRef]\n );\n\n return (\n <Button\n type=\"button\"\n data-slot=\"key-value-add\"\n variant=\"outline\"\n disabled={isDisabled}\n {...addProps}\n onClick={onClick}\n >\n {children ?? (\n <>\n <Plus aria-hidden=\"true\" className=\"size-4\" />\n {context.labels.add}\n </>\n )}\n </Button>\n );\n}\n\ninterface KeyValueErrorProps extends DivProps {\n field: Field;\n}\n\nfunction KeyValueError(props: KeyValueErrorProps) {\n const { field, asChild, className, ...errorProps } = props;\n\n const context = useKeyValueContext(ERROR_NAME);\n const itemData = useKeyValueItemContext(ERROR_NAME);\n\n const errors = useStore((state) => state.errors);\n const error = errors[itemData.id]?.[field];\n\n if (!error) return null;\n\n const ErrorPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ErrorPrimitive\n id={getErrorId(context.rootId, itemData.id, field)}\n role=\"alert\"\n {...errorProps}\n className={cn(\n // ⚠ Khác thượng nguồn: `text-destructive` (bậc TÔ NỀN) → `text-destructive-ink`, bậc dành\n // cho chữ trên nền trang — cùng token với chữ lỗi của `Input`.\n \"text-sm font-medium text-destructive-ink\",\n className\n )}\n >\n {error}\n </ErrorPrimitive>\n );\n}\n\nexport {\n type ItemData as KeyValueItemData,\n KeyValue,\n KeyValueAdd,\n type KeyValueAddProps,\n KeyValueError,\n type KeyValueErrorProps,\n KeyValueItem,\n type KeyValueItemProps,\n KeyValueKeyInput,\n type KeyValueKeyInputProps,\n KeyValueList,\n type KeyValueListProps,\n type KeyValueProps,\n KeyValueRemove,\n type KeyValueRemoveProps,\n KeyValueValueInput,\n type KeyValueValueInputProps,\n useStore as useKeyValueStore\n};\n"],"mappings":"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","names":[]}
|
|
@@ -10,6 +10,9 @@ declare const REGEX_CACHE: {
|
|
|
10
10
|
readonly hashPattern: RegExp;
|
|
11
11
|
readonly currencyAtEnd: RegExp;
|
|
12
12
|
readonly percentageChars: RegExp;
|
|
13
|
+
readonly percentageCommaChars: RegExp;
|
|
14
|
+
readonly decimalMark: RegExp;
|
|
15
|
+
readonly decimalMarks: RegExp;
|
|
13
16
|
readonly phone: RegExp;
|
|
14
17
|
readonly ssn: RegExp;
|
|
15
18
|
readonly zipCode: RegExp;
|
|
@@ -61,7 +64,21 @@ declare function applyCurrencyMask(opts: {
|
|
|
61
64
|
currency?: string;
|
|
62
65
|
locale?: string;
|
|
63
66
|
}): string;
|
|
64
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Giá trị thô dạng chuẩn (`"12.5"`) → chuỗi hiển thị (`"12.5%"`).
|
|
69
|
+
*
|
|
70
|
+
* ⚠ Khác thượng nguồn (3.1.0): nhận thêm `locale` (tuỳ chọn) và vẽ dấu thập phân của nó — `vi` →
|
|
71
|
+
* `12,5%`. Không truyền thì `.` như thượng nguồn. Chỉ đổi DẤU: không nhóm nghìn, và `%` luôn dính
|
|
72
|
+
* ngay sau số (`Intl` của de-DE viết `12,5 %`) — để con trỏ và nút xoá chỉ phải biết một dáng chuỗi.
|
|
73
|
+
*/
|
|
74
|
+
declare function applyPercentageMask(value: string, opts?: {
|
|
75
|
+
locale?: string;
|
|
76
|
+
}): string;
|
|
77
|
+
/** Ô phần trăm đang đọc và vẽ dấu `,` — `mask-input.tsx` dùng để chọn cách đặt con trỏ. */
|
|
78
|
+
declare function isDecimalCommaPercentage(opts: {
|
|
79
|
+
mask: MaskPatternKey | MaskPattern | undefined;
|
|
80
|
+
locale?: string;
|
|
81
|
+
}): boolean;
|
|
65
82
|
declare function getUnmaskedValue(opts: {
|
|
66
83
|
value: string;
|
|
67
84
|
currency?: string;
|
|
@@ -86,6 +103,21 @@ declare function getCurrencyCaretPosition(opts: {
|
|
|
86
103
|
oldValue?: string;
|
|
87
104
|
previousUnmasked?: string;
|
|
88
105
|
}): number;
|
|
106
|
+
/**
|
|
107
|
+
* Con trỏ sau khi ô phần trăm ở chế độ dấu phẩy (`isDecimalCommaPercentage`) vẽ lại chuỗi.
|
|
108
|
+
*
|
|
109
|
+
* ⚠ Khác thượng nguồn (3.1.0) — hàm MỚI. `getCurrencyCaretPosition` chỉ đếm CHỮ SỐ trước con trỏ,
|
|
110
|
+
* mà ở đây mặt nạ còn đổi chính dấu thập phân (phím `.` của bàn phím số hiện thành `,`): đếm chữ số
|
|
111
|
+
* thì con trỏ rơi về TRƯỚC dấu vừa gõ, và chữ số kế tiếp chen vào phần nguyên — `12` `.` `5` ra
|
|
112
|
+
* `125,%`. Đây đúng là cái bẫy làm cách chữa tạm "`transform` đổi `,` thành `.`" không dùng được.
|
|
113
|
+
* Ở đây đếm cả dấu thập phân ĐƯỢC GIỮ (dấu đầu tiên — luật của `transform`), nên con trỏ đứng sau nó.
|
|
114
|
+
* Chế độ dấu chấm (en…) vẫn đi `getCurrencyCaretPosition` như cũ.
|
|
115
|
+
*/
|
|
116
|
+
declare function getPercentageCaretPosition(opts: {
|
|
117
|
+
newValue: string;
|
|
118
|
+
oldValue: string;
|
|
119
|
+
oldCursorPosition: number;
|
|
120
|
+
}): number;
|
|
89
121
|
declare function getPatternCaretPosition(opts: {
|
|
90
122
|
newValue: string;
|
|
91
123
|
maskPattern: MaskPattern;
|
|
@@ -95,4 +127,4 @@ declare function getPatternCaretPosition(opts: {
|
|
|
95
127
|
previousUnmasked?: string;
|
|
96
128
|
}): number;
|
|
97
129
|
|
|
98
|
-
export { CURRENCY_PERCENTAGE_SYMBOLS, DEFAULT_CURRENCY, MASK_PATTERNS, type MaskPattern, type MaskPatternKey, NUMERIC_MASK_PATTERNS, REGEX_CACHE, type TransformOptions, applyCurrencyMask, applyMask, applyPercentageMask, fromUnmaskedIndex, getCurrencyCaretPosition, getPatternCaretPosition, getUnmaskedValue, isCurrencyAtEnd, isCurrencyMask, toUnmaskedIndex };
|
|
130
|
+
export { CURRENCY_PERCENTAGE_SYMBOLS, DEFAULT_CURRENCY, MASK_PATTERNS, type MaskPattern, type MaskPatternKey, NUMERIC_MASK_PATTERNS, REGEX_CACHE, type TransformOptions, applyCurrencyMask, applyMask, applyPercentageMask, fromUnmaskedIndex, getCurrencyCaretPosition, getPatternCaretPosition, getPercentageCaretPosition, getUnmaskedValue, isCurrencyAtEnd, isCurrencyMask, isDecimalCommaPercentage, toUnmaskedIndex };
|
|
@@ -8,6 +8,7 @@ const formattersCache = /* @__PURE__ */ new Map();
|
|
|
8
8
|
const currencyAtEndCache = /* @__PURE__ */ new Map();
|
|
9
9
|
const currencySymbolsCache = /* @__PURE__ */ new Map();
|
|
10
10
|
const fractionDigitsCache = /* @__PURE__ */ new Map();
|
|
11
|
+
const decimalSeparatorCache = /* @__PURE__ */ new Map();
|
|
11
12
|
const daysInMonthCache = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
12
13
|
const REGEX_CACHE = {
|
|
13
14
|
digitsOnly: /^\d+$/,
|
|
@@ -18,6 +19,9 @@ const REGEX_CACHE = {
|
|
|
18
19
|
hashPattern: /#/g,
|
|
19
20
|
currencyAtEnd: /\d\s*[^\d\s]+$/,
|
|
20
21
|
percentageChars: /[^\d.]/g,
|
|
22
|
+
percentageCommaChars: /[^\d.,]/g,
|
|
23
|
+
decimalMark: /[.,]/,
|
|
24
|
+
decimalMarks: /[.,]/g,
|
|
21
25
|
phone: /^\d{10}$/,
|
|
22
26
|
ssn: /^\d{9}$/,
|
|
23
27
|
zipCode: /^\d{5}$/,
|
|
@@ -111,6 +115,39 @@ function getCurrencyFractionDigits(opts) {
|
|
|
111
115
|
fractionDigitsCache.set(key, digits);
|
|
112
116
|
return digits;
|
|
113
117
|
}
|
|
118
|
+
function findCurrencyDecimalIndex(cleaned, decimal, group) {
|
|
119
|
+
const lastDot = cleaned.lastIndexOf(".");
|
|
120
|
+
const lastComma = cleaned.lastIndexOf(",");
|
|
121
|
+
if (lastDot !== -1 && lastComma !== -1) return Math.max(lastDot, lastComma);
|
|
122
|
+
const separator = lastDot !== -1 ? "." : lastComma !== -1 ? "," : null;
|
|
123
|
+
if (!separator) return -1;
|
|
124
|
+
const last = cleaned.lastIndexOf(separator);
|
|
125
|
+
if (separator === decimal) return last;
|
|
126
|
+
if (separator === group) {
|
|
127
|
+
return separator === "." && decimal === "," && last === cleaned.length - 1 ? last : -1;
|
|
128
|
+
}
|
|
129
|
+
const occurrences = cleaned.split(separator).length - 1;
|
|
130
|
+
const digitsAfter = cleaned.length - last - 1;
|
|
131
|
+
return occurrences === 1 && digitsAfter !== 3 ? last : -1;
|
|
132
|
+
}
|
|
133
|
+
function isCanonicalAmount(value, fractionDigits) {
|
|
134
|
+
const dot = value.indexOf(".");
|
|
135
|
+
const integerPart = dot === -1 ? value : value.slice(0, dot);
|
|
136
|
+
const fractionPart = dot === -1 ? "" : value.slice(dot + 1);
|
|
137
|
+
return /^\d*$/.test(integerPart) && /^\d*$/.test(fractionPart) && fractionPart.length <= fractionDigits;
|
|
138
|
+
}
|
|
139
|
+
function usesDecimalComma(locale) {
|
|
140
|
+
let decimal = decimalSeparatorCache.get(locale);
|
|
141
|
+
if (decimal === void 0) {
|
|
142
|
+
decimal = ".";
|
|
143
|
+
try {
|
|
144
|
+
decimal = new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value ?? ".";
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
decimalSeparatorCache.set(locale, decimal);
|
|
148
|
+
}
|
|
149
|
+
return decimal === ",";
|
|
150
|
+
}
|
|
114
151
|
function isCurrencyAtEnd(opts) {
|
|
115
152
|
const { locale, currency } = opts;
|
|
116
153
|
const key = `${locale}|${currency}`;
|
|
@@ -251,81 +288,20 @@ const MASK_PATTERNS = {
|
|
|
251
288
|
currency: {
|
|
252
289
|
pattern: "$###,###.##",
|
|
253
290
|
transform: (value, { currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE } = {}) => {
|
|
254
|
-
|
|
291
|
+
const fractionDigits = getCurrencyFractionDigits({ currency, locale });
|
|
292
|
+
if (fractionDigits === 0) {
|
|
255
293
|
return value.replace(REGEX_CACHE.nonDigits, "");
|
|
256
294
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const formatter = getCachedFormatter(locale, {
|
|
260
|
-
currency,
|
|
261
|
-
minimumFractionDigits: 0,
|
|
262
|
-
maximumFractionDigits: 2
|
|
263
|
-
});
|
|
264
|
-
const parts = formatter.formatToParts(1234.5);
|
|
265
|
-
const decimalPart = parts.find((part) => part.type === "decimal");
|
|
266
|
-
if (decimalPart) localeDecimalSeparator = decimalPart.value;
|
|
267
|
-
} catch {
|
|
268
|
-
}
|
|
295
|
+
if (isCanonicalAmount(value, fractionDigits)) return value;
|
|
296
|
+
const { decimal, group } = getCachedCurrencySymbols({ currency, locale });
|
|
269
297
|
const cleaned = value.replace(REGEX_CACHE.nonCurrencyChars, "");
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let decimalIndex = -1;
|
|
274
|
-
if (localeDecimalSeparator === ",") {
|
|
275
|
-
const lastCommaIndex = cleaned.lastIndexOf(",");
|
|
276
|
-
if (lastCommaIndex !== -1) {
|
|
277
|
-
const afterComma = cleaned.substring(lastCommaIndex + 1);
|
|
278
|
-
if (afterComma.length <= 2 && /^\d*$/.test(afterComma)) {
|
|
279
|
-
hasDecimalSeparator = true;
|
|
280
|
-
decimalIndex = lastCommaIndex;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (!hasDecimalSeparator && dotIndex !== -1) {
|
|
284
|
-
const afterDot = cleaned.substring(dotIndex + 1);
|
|
285
|
-
if (afterDot.length <= 2 && /^\d*$/.test(afterDot)) {
|
|
286
|
-
hasDecimalSeparator = true;
|
|
287
|
-
decimalIndex = dotIndex;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (!hasDecimalSeparator && cleaned.length >= 4) {
|
|
291
|
-
const match = cleaned.match(/^(\d+)\.(\d{3})(\d{1,2})$/);
|
|
292
|
-
if (match) {
|
|
293
|
-
const [, beforeDot, thousandsPart, decimalPart] = match;
|
|
294
|
-
const integerPart = (beforeDot ?? "") + (thousandsPart ?? "");
|
|
295
|
-
const result = `${integerPart}.${decimalPart}`;
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
const lastDotIndex = cleaned.lastIndexOf(".");
|
|
301
|
-
if (lastDotIndex !== -1) {
|
|
302
|
-
const afterDot = cleaned.substring(lastDotIndex + 1);
|
|
303
|
-
if (afterDot.length <= 2 && /^\d*$/.test(afterDot)) {
|
|
304
|
-
hasDecimalSeparator = true;
|
|
305
|
-
decimalIndex = lastDotIndex;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (!hasDecimalSeparator && commaIndex !== -1) {
|
|
309
|
-
const afterComma = cleaned.substring(commaIndex + 1);
|
|
310
|
-
const looksLikeThousands = commaIndex <= 3 && afterComma.length >= 3;
|
|
311
|
-
if (!looksLikeThousands && afterComma.length <= 2 && /^\d*$/.test(afterComma)) {
|
|
312
|
-
hasDecimalSeparator = true;
|
|
313
|
-
decimalIndex = commaIndex;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
298
|
+
const decimalIndex = findCurrencyDecimalIndex(cleaned, decimal, group);
|
|
299
|
+
if (decimalIndex === -1) {
|
|
300
|
+
return cleaned.replace(/[.,]/g, "");
|
|
316
301
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (afterDecimal === "") {
|
|
321
|
-
const result2 = `${beforeDecimal}.`;
|
|
322
|
-
return result2;
|
|
323
|
-
}
|
|
324
|
-
const result = `${beforeDecimal}.${afterDecimal.substring(0, 2)}`;
|
|
325
|
-
return result;
|
|
326
|
-
}
|
|
327
|
-
const digitsOnly = cleaned.replace(/[.,]/g, "");
|
|
328
|
-
return digitsOnly;
|
|
302
|
+
const integerPart = cleaned.substring(0, decimalIndex).replace(/[.,]/g, "");
|
|
303
|
+
const fractionPart = cleaned.substring(decimalIndex + 1).replace(/[.,]/g, "").substring(0, fractionDigits);
|
|
304
|
+
return `${integerPart}.${fractionPart}`;
|
|
329
305
|
},
|
|
330
306
|
validate: (value) => {
|
|
331
307
|
if (!REGEX_CACHE.currencyValidation.test(value)) return false;
|
|
@@ -334,11 +310,23 @@ const MASK_PATTERNS = {
|
|
|
334
310
|
}
|
|
335
311
|
},
|
|
336
312
|
/**
|
|
337
|
-
* Phần trăm
|
|
313
|
+
* Phần trăm `##.##%`, tối đa 2 chữ số lẻ. Giá trị thô (vế thứ hai của `onValueChange`, và thứ
|
|
314
|
+
* `validate` nhận) LUÔN dùng `.`: `"12.5"`.
|
|
315
|
+
*
|
|
316
|
+
* Dấu thập phân theo `locale` (`3.1.0`): ở locale dùng `,` (vi, de, fr…) ô đọc CẢ `,` lẫn `.` là
|
|
317
|
+
* dấu thập phân — `.` vì bàn phím số của điện thoại vẽ dấu theo ngôn ngữ MÁY, không theo app — và
|
|
318
|
+
* hiện `12,5%`. Locale dùng `.` (en…): như thượng nguồn, `,` bị bỏ qua (`12,5` → `125%`).
|
|
338
319
|
*/
|
|
339
320
|
percentage: {
|
|
340
321
|
pattern: "##.##%",
|
|
341
|
-
transform: (value) => {
|
|
322
|
+
transform: (value, { locale = DEFAULT_LOCALE } = {}) => {
|
|
323
|
+
if (usesDecimalComma(locale)) {
|
|
324
|
+
const cleaned2 = value.replace(REGEX_CACHE.percentageCommaChars, "");
|
|
325
|
+
const markIndex = cleaned2.search(REGEX_CACHE.decimalMark);
|
|
326
|
+
if (markIndex === -1) return cleaned2;
|
|
327
|
+
const fractionPart = cleaned2.slice(markIndex + 1).replace(REGEX_CACHE.decimalMarks, "").slice(0, 2);
|
|
328
|
+
return `${cleaned2.slice(0, markIndex)}.${fractionPart}`;
|
|
329
|
+
}
|
|
342
330
|
const cleaned = value.replace(REGEX_CACHE.percentageChars, "");
|
|
343
331
|
const parts = cleaned.split(".");
|
|
344
332
|
if (parts.length > 2) {
|
|
@@ -415,7 +403,7 @@ function applyMask(opts) {
|
|
|
415
403
|
});
|
|
416
404
|
}
|
|
417
405
|
if (pattern.includes("%")) {
|
|
418
|
-
return applyPercentageMask(cleanValue);
|
|
406
|
+
return applyPercentageMask(cleanValue, { locale: isLocalizedPercentage(mask) ? locale : void 0 });
|
|
419
407
|
}
|
|
420
408
|
if (mask === "ipv4") {
|
|
421
409
|
return cleanValue;
|
|
@@ -482,15 +470,22 @@ function applyCurrencyMask(opts) {
|
|
|
482
470
|
return result;
|
|
483
471
|
}
|
|
484
472
|
}
|
|
485
|
-
function applyPercentageMask(value) {
|
|
473
|
+
function applyPercentageMask(value, opts = {}) {
|
|
486
474
|
if (!value) return "";
|
|
475
|
+
const decimalSeparator = usesDecimalComma(opts.locale ?? DEFAULT_LOCALE) ? "," : ".";
|
|
487
476
|
const parts = value.split(".");
|
|
488
477
|
let result = parts[0] ?? "0";
|
|
489
478
|
if (value.includes(".")) {
|
|
490
|
-
result +=
|
|
479
|
+
result += `${decimalSeparator}${(parts[1] ?? "").substring(0, 2)}`;
|
|
491
480
|
}
|
|
492
481
|
return `${result}%`;
|
|
493
482
|
}
|
|
483
|
+
function isLocalizedPercentage(mask) {
|
|
484
|
+
return mask === "percentage" || typeof mask === "object" && mask.transform === MASK_PATTERNS.percentage.transform;
|
|
485
|
+
}
|
|
486
|
+
function isDecimalCommaPercentage(opts) {
|
|
487
|
+
return isLocalizedPercentage(opts.mask) && usesDecimalComma(opts.locale ?? DEFAULT_LOCALE);
|
|
488
|
+
}
|
|
494
489
|
function getUnmaskedValue(opts) {
|
|
495
490
|
const { value, transform, currency, locale } = opts;
|
|
496
491
|
return transform ? transform(value, { currency, locale }) : value.replace(REGEX_CACHE.nonDigits, "");
|
|
@@ -547,6 +542,20 @@ function getCurrencyCaretPosition(opts) {
|
|
|
547
542
|
}
|
|
548
543
|
return newValue.length;
|
|
549
544
|
}
|
|
545
|
+
function getPercentageCaretPosition(opts) {
|
|
546
|
+
const { newValue, oldValue, oldCursorPosition } = opts;
|
|
547
|
+
const markIndex = oldValue.search(REGEX_CACHE.decimalMark);
|
|
548
|
+
const target = oldValue.slice(0, oldCursorPosition).replace(REGEX_CACHE.nonDigits, "").length + (markIndex !== -1 && markIndex < oldCursorPosition ? 1 : 0);
|
|
549
|
+
if (target === 0) return 0;
|
|
550
|
+
let seen = 0;
|
|
551
|
+
for (let i = 0; i < newValue.length; i++) {
|
|
552
|
+
if (/[\d.,]/.test(newValue.charAt(i))) {
|
|
553
|
+
seen++;
|
|
554
|
+
if (seen === target) return i + 1;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return newValue.length;
|
|
558
|
+
}
|
|
550
559
|
function getPatternCaretPosition(opts) {
|
|
551
560
|
const { newValue, maskPattern, currentUnmasked, oldCursorPosition, oldValue, previousUnmasked } = opts;
|
|
552
561
|
let position = 0;
|
|
@@ -592,9 +601,11 @@ export {
|
|
|
592
601
|
fromUnmaskedIndex,
|
|
593
602
|
getCurrencyCaretPosition,
|
|
594
603
|
getPatternCaretPosition,
|
|
604
|
+
getPercentageCaretPosition,
|
|
595
605
|
getUnmaskedValue,
|
|
596
606
|
isCurrencyAtEnd,
|
|
597
607
|
isCurrencyMask,
|
|
608
|
+
isDecimalCommaPercentage,
|
|
598
609
|
toUnmaskedIndex
|
|
599
610
|
};
|
|
600
611
|
//# sourceMappingURL=mask-input-constants.js.map
|