@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/mention.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/mention) — 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 * as MentionPrimitive from \"@diceui/mention\";\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * ⚠ Khác thượng nguồn: context RIÊNG của lớp bọc, chỉ mang một id — xem chú thích ở `MentionInput`.\n */\nconst MentionListIdContext = React.createContext<string | undefined>(undefined);\n\n/**\n * Mention — ô nhập có gợi ý `@nhắc tên`: gõ ký tự kích hoạt (mặc định `@`) là mở danh sách, chọn\n * một mục thì chèn `@Tên` vào ô và đánh dấu nó thành một khối (xoá là xoá cả khối).\n * Dùng cho ô soạn tin của Chat và bình luận trong task.\n *\n * Phần lõi (dò ký tự kích hoạt, lọc, điều hướng bàn phím, định vị danh sách theo con trỏ) là gói\n * headless `@diceui/mention`; file này chỉ phủ style theo token của Comitor.\n */\ntype MentionProps = React.ComponentProps<typeof MentionPrimitive.Root>;\n\nfunction Mention({ className, dir: dirProp, ...props }: MentionProps) {\n /*\n * ⚠ Khác thượng nguồn: `@diceui/shared` có `useDirection` RIÊNG, đọc một `DirectionContext` mà\n * gói không xuất ra — nên `DirectionProvider` của Radix (nguồn hướng chữ của mọi component khác\n * trong gói) không tới được nó. Đọc hướng từ Radix rồi truyền xuống bằng prop `dir`.\n */\n const dir = useDirection(dirProp);\n const listId = React.useId();\n\n return (\n <MentionListIdContext.Provider value={listId}>\n <MentionPrimitive.Root\n data-slot=\"mention\"\n dir={dir}\n className={cn(\n /*\n * Khối `@Tên` đã chèn. Gói vẽ nó bằng một lớp phủ (`MentionHighlighter`) định vị TUYỆT\n * ĐỐI nằm ĐÈ LÊN ô nhập: chữ thường của lớp phủ trong suốt để lộ chữ của ô bên dưới,\n * chỉ đoạn `[data-tag]` là có nền.\n *\n * ⚠ Khác thượng nguồn: bản gốc tô `bg-blue-200 text-blue-950` / `dark:bg-blue-800\n * dark:text-blue-50` (bảng màu mặc định của Tailwind). Đổi sang cặp tonal của gói\n * (`info` = teal — bậc \"xanh\" của Comitor), và CHỮ là `text-foreground` chứ không phải\n * `text-info-ink`: nền tint `/15` trong suốt nên chữ của ô nhập vẫn lộ ra dưới khối,\n * chữ khối vẽ đè đúng chỗ đó — hai lớp chữ phải CÙNG MÀU, lệch màu là thành viền răng\n * cưa quanh từng nét. (Bản gốc cũng chọn chữ gần như trùng màu chữ thường ở cả hai theme.)\n * Khối không chỉ nói bằng màu: nó luôn mang ký tự kích hoạt `@` ở đầu.\n * chữ ⇄ khối: 15,69:1 (light, trên nền trắng) · 15,15:1 (dark) · tệ nhất 13,44:1\n * (dark tương phản cao, `--teal` #2DD4BF). Khối ⇄ nền trang 1,21:1 — cố ý nhạt.\n */\n \"**:data-tag:rounded **:data-tag:bg-info/15 **:data-tag:py-px **:data-tag:text-foreground\",\n className\n )}\n {...props}\n />\n </MentionListIdContext.Provider>\n );\n}\n\ntype MentionLabelProps = React.ComponentProps<typeof MentionPrimitive.Label>;\n\nfunction MentionLabel({ className, ...props }: MentionLabelProps) {\n return (\n <MentionPrimitive.Label\n data-slot=\"mention-label\"\n className={cn(\"px-0.5 py-1.5 text-sm font-semibold\", className)}\n {...props}\n />\n );\n}\n\ntype MentionInputProps = React.ComponentProps<typeof MentionPrimitive.Input>;\n\n/**\n * Ô nhập của `Mention`. Muốn nhiều dòng (ô soạn tin) thì `asChild` với một `<textarea />`.\n * Không có `MentionLabel` thì PHẢI truyền `aria-label`.\n */\nfunction MentionInput({ className, ...props }: MentionInputProps) {\n const listId = React.useContext(MentionListIdContext);\n\n return (\n <MentionPrimitive.Input\n data-slot=\"mention-input\"\n /*\n * ⚠ Khác thượng nguồn (a11y): ô nhập là `role=\"combobox\"` và gói đặt `aria-controls` trỏ tới\n * một id sinh trong gốc — nhưng `MentionContent` của gói KHÔNG gắn id đó lên `role=\"listbox\"`,\n * nên quan hệ combobox → listbox trỏ vào hư không. Lớp bọc tự sinh một id, gắn ở CẢ HAI đầu\n * (props của ta được trải SAU props của gói nên thắng).\n */\n aria-controls={listId}\n className={cn(\n /*\n * Khuôn viền/focus của `Input`: `border-input` lúc nghỉ, `--control-edge-strong` khi rê,\n * `--primary-ink` + `ring-2 ring/50` khi focus. Nền theo `Textarea` (ô soạn tin thường là\n * `asChild` + `<textarea>`): trong suốt, ở dark tô `--input-fill` — không `bg-input/30`,\n * xem chú thích đầu textarea.tsx.\n * ⚠ Khác thượng nguồn: `focus-visible:ring-1 ring-ring` → khuôn focus của `Input`; bỏ\n * `shadow-xs` (bóng của gói chỉ đi qua `--elevation-*`, `Input` không có bóng).\n */\n \"flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm text-foreground transition-colors duration-150 ease-out placeholder:text-muted-foreground hover:border-control-edge-strong focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input-fill\",\n className\n )}\n {...props}\n />\n );\n}\n\ntype MentionContentProps = React.ComponentProps<typeof MentionPrimitive.Content>;\n\n/**\n * Danh sách gợi ý (`role=\"listbox\"`), render qua portal.\n * Tên cho trình đọc màn hình mặc định là khoá `listLabel` của namespace `mention`\n * (`ComitorLocaleProvider`) — đổi bằng `aria-label`.\n */\nfunction MentionContent({ className, children, \"aria-label\": ariaLabelProp, ...props }: MentionContentProps) {\n const listId = React.useContext(MentionListIdContext);\n const t = useComitorMessages(\"mention\");\n const ariaLabel = ariaLabelProp ?? t.listLabel;\n\n return (\n <MentionPrimitive.Portal>\n <MentionPrimitive.Content\n data-slot=\"mention-content\"\n // ⚠ Khác thượng nguồn (a11y): id để `aria-controls` của ô nhập trỏ tới được — xem\n // `MentionInput`. Và `role=\"listbox\"` BẮT BUỘC có tên (ARIA 1.2); bản gốc không đặt.\n id={listId}\n aria-label={ariaLabel}\n className={cn(\n /*\n * Mặt nổi cùng bộ với `PopoverContent`/`Command`: `bg-popover text-popover-foreground`,\n * bóng `shadow-overlay` (đổi theo theme qua `--elevation-*`).\n * ⚠ Khác thượng nguồn: `shadow-md` → `shadow-overlay`; thêm `origin-(--dice-transform-origin)`\n * — biến gói headless tính sẵn theo cạnh bám, để hiệu ứng `zoom-in-95` nở ra TỪ phía con\n * trỏ như `PopoverContent` (bản gốc nở từ tâm).\n */\n \"relative z-50 min-w-32 origin-(--dice-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-overlay data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n >\n {children}\n </MentionPrimitive.Content>\n </MentionPrimitive.Portal>\n );\n}\n\ntype MentionItemProps = React.ComponentProps<typeof MentionPrimitive.Item>;\n\nfunction MentionItem({ className, children, ...props }: MentionItemProps) {\n return (\n <MentionPrimitive.Item\n data-slot=\"mention-item\"\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: bộ lớp của `CommandItem` (mục đang sáng `bg-accent`/`text-accent-\n * foreground`, khoảng đệm theo trục mật độ `--menu-item-py`, icon con mặc định\n * `text-muted-foreground` cỡ 4) — chỉ đổi thuộc tính trạng thái sang dạng của gói headless\n * (`data-highlighted` / `data-disabled` là thuộc tính CÓ MẶT, không phải `=\"true\"`).\n */\n \"relative mt-1 flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-(--menu-item-py) text-sm outline-hidden select-none first:mt-0 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children}\n </MentionPrimitive.Item>\n );\n}\n\nexport {\n Mention,\n MentionContent,\n type MentionContentProps,\n MentionInput,\n type MentionInputProps,\n MentionItem,\n type MentionItemProps,\n MentionLabel,\n type MentionLabelProps,\n type MentionProps\n};\n"],"mappings":";AAuCM;AAhCN,YAAY,sBAAsB;AAClC,SAAS,oBAAoB;AAC7B,YAAY,WAAW;AACvB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AAKnB,MAAM,uBAAuB,MAAM,cAAkC,MAAS;AAY9E,SAAS,QAAQ,EAAE,WAAW,KAAK,SAAS,GAAG,MAAM,GAAiB;AAMpE,QAAM,MAAM,aAAa,OAAO;AAChC,QAAM,SAAS,MAAM,MAAM;AAE3B,SACE,oBAAC,qBAAqB,UAArB,EAA8B,OAAO,QACpC;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAgBT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN,GACF;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,uCAAuC,SAAS;AAAA,MAC7D,GAAG;AAAA;AAAA,EACN;AAEJ;AAQA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,QAAM,SAAS,MAAM,WAAW,oBAAoB;AAEpD,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MAOV,iBAAe;AAAA,MACf,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAST;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AASA,SAAS,eAAe,EAAE,WAAW,UAAU,cAAc,eAAe,GAAG,MAAM,GAAwB;AAC3G,QAAM,SAAS,MAAM,WAAW,oBAAoB;AACpD,QAAM,IAAI,mBAAmB,SAAS;AACtC,QAAM,YAAY,iBAAiB,EAAE;AAErC,SACE,oBAAC,iBAAiB,QAAjB,EACC;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MAGV,IAAI;AAAA,MACJ,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH,GACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,UAAU,GAAG,MAAM,GAAqB;AACxE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/mention.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/mention) — 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 * as MentionPrimitive from \"@diceui/mention\";\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n/*\n * Bộ lọc mặc định của `Mention` phải cho ĐÚNG kết quả của `Combobox` / `SearchFilterBar`, nên nó dùng\n * CHÍNH `matchesSearch` — phép chuẩn hoá duy nhất của gói — từ `lib/text-search.ts` (chuyển xuống đó ở\n * `3.1.0` để Tier 1 khỏi nhập ngược lên Tier 2; `composite/text-utils.ts` xuất lại nguyên tên).\n */\nimport { matchesSearch } from \"../../lib/text-search.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\n\n/*\n * ⚠ Khác thượng nguồn: context RIÊNG của lớp bọc (lõi headless không xuất context của nó):\n * · `listId` — xem chú thích ở `MentionInput`;\n * · `itemLabels` (3.1.0) — bảng `value → label` của các `MentionItem` đang mount, cho bộ lọc mặc\n * định — xem `Mention`.\n */\ninterface MentionWrapperContextValue {\n listId: string;\n itemLabels: Map<string, string>;\n}\n\nconst MentionWrapperContext = React.createContext<MentionWrapperContextValue | null>(null);\n\n/**\n * Bộ lọc mặc định: chữ gõ sau ký tự kích hoạt nằm trong NHÃN của mục — không dấu, không phân biệt\n * hoa thường, đúng luật `matchesSearch` của `Combobox` / `SearchFilterBar` (\"@nguyen\" ra \"Nguyễn\",\n * \"@duc\" ra \"Đức\"). Thêm một vế so với nhãn đã BỎ khoảng trắng: lõi đóng danh sách ở dấu cách đầu\n * tiên (xem `Mention`), nên đây là cách duy nhất để đi QUA từ đầu của tên — \"@nguyenvana\" vẫn còn\n * \"Nguyễn Văn An\" mà đã loại \"Nguyễn Văn Bình\".\n */\nfunction matchesMentionLabel(label: string, term: string): boolean {\n return matchesSearch(label, term) || matchesSearch(label.replace(/\\s+/g, \"\"), term);\n}\n\n/**\n * Mention — ô nhập có gợi ý `@nhắc tên`: gõ ký tự kích hoạt (mặc định `@`) là mở danh sách, chọn\n * một mục thì chèn `@Tên` vào ô và đánh dấu nó thành một khối (xoá là xoá cả khối).\n * Dùng cho ô soạn tin của Chat và bình luận trong task.\n *\n * Phần lõi (dò ký tự kích hoạt, lọc, điều hướng bàn phím, định vị danh sách theo con trỏ) là gói\n * headless `@diceui/mention`; file này chỉ phủ style theo token của Comitor.\n *\n * Lọc: không truyền `onFilter` thì so với NHÃN của mục (`label`, thiếu thì `value` — đúng chữ được\n * chèn sau `@`), không dấu, không phân biệt hoa thường. `onFilter` của app vẫn thắng. `exactMatch`\n * không còn tác dụng: lõi bỏ qua nó khi có `onFilter`, mà nay luôn có.\n *\n * Chữ gõ dừng ở dấu cách ĐẦU TIÊN — luật cứng trong `MentionInput` của lõi (chuỗi sau ký tự kích\n * hoạt có dấu cách thì không còn là mention, danh sách đóng), không prop nào mở được. Tên nhiều chữ\n * vì thế gõ LIỀN: \"@nguyenvana\" tới \"Nguyễn Văn An\" (bộ lọc mặc định so cả nhãn đã bỏ khoảng trắng).\n */\ntype MentionProps = React.ComponentProps<typeof MentionPrimitive.Root>;\n\nfunction Mention({ className, dir: dirProp, onFilter: onFilterProp, ...props }: MentionProps) {\n /*\n * ⚠ Khác thượng nguồn: `@diceui/shared` có `useDirection` RIÊNG, đọc một `DirectionContext` mà\n * gói không xuất ra — nên `DirectionProvider` của Radix (nguồn hướng chữ của mọi component khác\n * trong gói) không tới được nó. Đọc hướng từ Radix rồi truyền xuống bằng prop `dir`.\n */\n const dir = useDirection(dirProp);\n const listId = React.useId();\n const itemLabels = useLazyRef(() => new Map<string, string>()).current;\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): bộ lọc MẶC ĐỊNH đi theo nhãn, không dấu. Bộ lọc của lõi so với\n * `value` — thường là id (\"u-anna\"), nên gõ tên chỉ trúng khi id tình cờ chứa chữ đó — và giữ dấu\n * (bỏ khoảng trắng/dấu câu rồi so KHỚP MỜ, hạ chữ thường nhưng chuẩn hoá NFC: \"nguyen\" không tới\n * \"Nguyễn\"). Lõi chỉ đưa `value` cho `onFilter` (gọi từng mục một: `onFilter([value], term)`), nên\n * nhãn lấy từ bảng `itemLabels` mà `MentionItem` ghi vào.\n * Lõi còn một bước CHẠY TRƯỚC `onFilter` mà lớp bọc không chạm được: mục có `value` bằng hoặc bắt\n * đầu bằng chữ gõ (so thô, phân biệt hoa thường) luôn được giữ — \"@u\" giữ mọi id \"u-…\".\n */\n const defaultOnFilter = React.useCallback(\n (values: string[], term: string) =>\n values.filter((value) => matchesMentionLabel(itemLabels.get(value) ?? value, term)),\n [itemLabels]\n );\n\n const wrapperContext = React.useMemo<MentionWrapperContextValue>(\n () => ({ listId, itemLabels }),\n [listId, itemLabels]\n );\n\n return (\n <MentionWrapperContext.Provider value={wrapperContext}>\n <MentionPrimitive.Root\n data-slot=\"mention\"\n dir={dir}\n onFilter={onFilterProp ?? defaultOnFilter}\n className={cn(\n /*\n * Khối `@Tên` đã chèn. Gói vẽ nó bằng một lớp phủ (`MentionHighlighter`) định vị TUYỆT\n * ĐỐI nằm ĐÈ LÊN ô nhập: chữ thường của lớp phủ trong suốt để lộ chữ của ô bên dưới,\n * chỉ đoạn `[data-tag]` là có nền.\n *\n * ⚠ Khác thượng nguồn: bản gốc tô `bg-blue-200 text-blue-950` / `dark:bg-blue-800\n * dark:text-blue-50` (bảng màu mặc định của Tailwind). Đổi sang cặp tonal của gói\n * (`info` = teal — bậc \"xanh\" của Comitor), và CHỮ là `text-foreground` chứ không phải\n * `text-info-ink`: nền tint `/15` trong suốt nên chữ của ô nhập vẫn lộ ra dưới khối,\n * chữ khối vẽ đè đúng chỗ đó — hai lớp chữ phải CÙNG MÀU, lệch màu là thành viền răng\n * cưa quanh từng nét. (Bản gốc cũng chọn chữ gần như trùng màu chữ thường ở cả hai theme.)\n * Khối không chỉ nói bằng màu: nó luôn mang ký tự kích hoạt `@` ở đầu.\n * chữ ⇄ khối: 15,69:1 (light, trên nền trắng) · 15,15:1 (dark) · tệ nhất 13,44:1\n * (dark tương phản cao, `--teal` #2DD4BF). Khối ⇄ nền trang 1,21:1 — cố ý nhạt.\n */\n \"**:data-tag:rounded **:data-tag:bg-info/15 **:data-tag:py-px **:data-tag:text-foreground\",\n className\n )}\n {...props}\n />\n </MentionWrapperContext.Provider>\n );\n}\n\ntype MentionLabelProps = React.ComponentProps<typeof MentionPrimitive.Label>;\n\nfunction MentionLabel({ className, ...props }: MentionLabelProps) {\n return (\n <MentionPrimitive.Label\n data-slot=\"mention-label\"\n className={cn(\"px-0.5 py-1.5 text-sm font-semibold\", className)}\n {...props}\n />\n );\n}\n\ntype MentionInputProps = React.ComponentProps<typeof MentionPrimitive.Input>;\n\n/**\n * Ô nhập của `Mention`. Muốn nhiều dòng (ô soạn tin) thì `asChild` với một `<textarea />`.\n * Không có `MentionLabel` thì PHẢI truyền `aria-label`.\n */\nfunction MentionInput({ className, ...props }: MentionInputProps) {\n const listId = React.useContext(MentionWrapperContext)?.listId;\n\n return (\n <MentionPrimitive.Input\n data-slot=\"mention-input\"\n /*\n * ⚠ Khác thượng nguồn (a11y): ô nhập là `role=\"combobox\"` và gói đặt `aria-controls` trỏ tới\n * một id sinh trong gốc — nhưng `MentionContent` của gói KHÔNG gắn id đó lên `role=\"listbox\"`,\n * nên quan hệ combobox → listbox trỏ vào hư không. Lớp bọc tự sinh một id, gắn ở CẢ HAI đầu\n * (props của ta được trải SAU props của gói nên thắng).\n */\n aria-controls={listId}\n className={cn(\n /*\n * Khuôn viền/focus của `Input`: `border-input` lúc nghỉ, `--control-edge-strong` khi rê,\n * `--primary-ink` + `ring-2 ring/50` khi focus. Nền theo `Textarea` (ô soạn tin thường là\n * `asChild` + `<textarea>`): trong suốt, ở dark tô `--input-fill` — không `bg-input/30`,\n * xem chú thích đầu textarea.tsx.\n * ⚠ Khác thượng nguồn: `focus-visible:ring-1 ring-ring` → khuôn focus của `Input`; bỏ\n * `shadow-xs` (bóng của gói chỉ đi qua `--elevation-*`, `Input` không có bóng).\n */\n \"flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm text-foreground transition-colors duration-150 ease-out placeholder:text-muted-foreground hover:border-control-edge-strong focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input-fill\",\n className\n )}\n {...props}\n />\n );\n}\n\ntype MentionContentProps = React.ComponentProps<typeof MentionPrimitive.Content>;\n\n/**\n * Danh sách gợi ý (`role=\"listbox\"`), render qua portal.\n * Tên cho trình đọc màn hình mặc định là khoá `listLabel` của namespace `mention`\n * (`ComitorLocaleProvider`) — đổi bằng `aria-label`.\n */\nfunction MentionContent({ className, children, \"aria-label\": ariaLabelProp, ...props }: MentionContentProps) {\n const listId = React.useContext(MentionWrapperContext)?.listId;\n const t = useComitorMessages(\"mention\");\n const ariaLabel = ariaLabelProp ?? t.listLabel;\n\n return (\n <MentionPrimitive.Portal>\n <MentionPrimitive.Content\n data-slot=\"mention-content\"\n // ⚠ Khác thượng nguồn (a11y): id để `aria-controls` của ô nhập trỏ tới được — xem\n // `MentionInput`. Và `role=\"listbox\"` BẮT BUỘC có tên (ARIA 1.2); bản gốc không đặt.\n id={listId}\n aria-label={ariaLabel}\n className={cn(\n /*\n * Mặt nổi cùng bộ với `PopoverContent`/`Command`: `bg-popover text-popover-foreground`,\n * bóng `shadow-overlay` (đổi theo theme qua `--elevation-*`).\n * ⚠ Khác thượng nguồn: `shadow-md` → `shadow-overlay`; thêm `origin-(--dice-transform-origin)`\n * — biến gói headless tính sẵn theo cạnh bám, để hiệu ứng `zoom-in-95` nở ra TỪ phía con\n * trỏ như `PopoverContent` (bản gốc nở từ tâm).\n */\n \"relative z-50 min-w-32 origin-(--dice-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-overlay data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n >\n {children}\n </MentionPrimitive.Content>\n </MentionPrimitive.Portal>\n );\n}\n\ntype MentionItemProps = React.ComponentProps<typeof MentionPrimitive.Item>;\n\n/**\n * Một gợi ý. `value` là thứ app nhận lại (id), `label` là chữ chèn sau `@` — và là chữ bộ lọc mặc\n * định của `Mention` so với chữ gõ (thiếu `label` thì dùng `value`).\n */\nfunction MentionItem({ className, children, value, label, ...props }: MentionItemProps) {\n const itemLabels = React.useContext(MentionWrapperContext)?.itemLabels;\n const text = label ?? value;\n\n /*\n * ⚠ Khác thượng nguồn (3.1.0): ghi `value → label` cho bộ lọc mặc định của `Mention` — lõi chỉ\n * đưa `value` cho `onFilter`. Ghi trong layout effect, CÙNG NHỊP lõi tự đăng ký mục (danh sách\n * đóng thì mục bị gỡ, mở ra thì mount lại), nên bảng luôn khớp đúng những mục lõi đang lọc; lõi\n * chỉ lọc trong handler sự kiện của ô nhập (gõ, bấm, focus, đổi vùng chọn), tức sau khi commit đã\n * xong. Gỡ có điều kiện — chỉ khi bảng còn giữ đúng nhãn của mục này — để hai mục trùng `value`\n * không gỡ nhãn của nhau.\n */\n useIsomorphicLayoutEffect(() => {\n if (!itemLabels) return;\n itemLabels.set(value, text);\n return () => {\n if (itemLabels.get(value) === text) itemLabels.delete(value);\n };\n }, [itemLabels, value, text]);\n\n return (\n <MentionPrimitive.Item\n data-slot=\"mention-item\"\n value={value}\n label={label}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: bộ lớp của `CommandItem` (mục đang sáng `bg-accent`/`text-accent-\n * foreground`, khoảng đệm theo trục mật độ `--menu-item-py`, icon con mặc định\n * `text-muted-foreground` cỡ 4) — chỉ đổi thuộc tính trạng thái sang dạng của gói headless\n * (`data-highlighted` / `data-disabled` là thuộc tính CÓ MẶT, không phải `=\"true\"`).\n */\n \"relative mt-1 flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-(--menu-item-py) text-sm outline-hidden select-none first:mt-0 data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children}\n </MentionPrimitive.Item>\n );\n}\n\nexport {\n Mention,\n MentionContent,\n type MentionContentProps,\n MentionInput,\n type MentionInputProps,\n MentionItem,\n type MentionItemProps,\n MentionLabel,\n type MentionLabelProps,\n type MentionProps\n};\n"],"mappings":"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","names":[]}
|
|
@@ -27,19 +27,29 @@ declare function useStore<T>(selector: (state: StoreState) => T, ogStore?: Store
|
|
|
27
27
|
* chỉ gồm chữ số: `"+84912345678"`. Người dùng gõ được theo HAI cách:
|
|
28
28
|
*
|
|
29
29
|
* - **Số nội địa** — chuỗi KHÔNG bắt đầu bằng `+`, và đang có quốc gia được chọn (`defaultCountry`
|
|
30
|
-
* / `country`, hoặc người dùng chọn): bỏ
|
|
31
|
-
* (chỉ gạt ký tự không phải số hay `space ( ) . -`), con trỏ
|
|
32
|
-
* ghép lại theo mã mới, còn chữ trên ô giữ nguyên.
|
|
30
|
+
* / `country`, hoặc người dùng chọn): bỏ tiền tố trung kế của nước đó rồi ghép mã quay số của nước
|
|
31
|
+
* đang chọn. Ô hiện NGUYÊN VĂN chữ đã gõ (chỉ gạt ký tự không phải số hay `space ( ) . -`), con trỏ
|
|
32
|
+
* không nhảy; đổi quốc gia thì giá trị ghép lại theo mã mới, còn chữ trên ô giữ nguyên.
|
|
33
33
|
* Nước là Việt Nam, gõ `0912 345 678` → ô hiện `0912 345 678`, `onValueChange("+84912345678")`.
|
|
34
|
-
* ⚠ Khác thượng nguồn — bản gốc ra `+0912345678`.
|
|
35
|
-
* `
|
|
34
|
+
* ⚠ Khác thượng nguồn — bản gốc ra `+0912345678`. Tiền tố trung kế là `0` ở phần lớn các nước;
|
|
35
|
+
* từ `3.1.0`: Nga/Kazakhstan `8` (`8 912 345 67 89` → `+79123456789`), Hungary `06`, Bắc Mỹ `1`
|
|
36
|
+
* (`1 212 555 0123` → `+12125550123`). Ý, San Marino, Vatican, Bờ Biển Ngà, Bénin giữ số 0 đầu
|
|
37
|
+
* (máy bàn Ý `06 …` → `+39 06 …`).
|
|
38
|
+
* Mở đầu bằng tiền tố gọi quốc tế (`00`; Bắc Mỹ cả `011`; Úc `0011`; Nhật `010`) thì là số quốc
|
|
39
|
+
* tế, như gõ `+` (`3.1.0`): `0044 20 7946 0958` → `+442079460958`, cờ chuyển sang Anh.
|
|
36
40
|
* - **Số quốc tế** — chuỗi bắt đầu bằng `+` (như thượng nguồn): ô hiện tách nhóm
|
|
37
41
|
* (`+84 912 345 678`), quốc gia tự nhận theo mã quay số.
|
|
38
42
|
* Gõ `+84 912 345 678` → `onValueChange("+84912345678")`, cờ chuyển sang Việt Nam.
|
|
43
|
+
* ⚠ Khác thượng nguồn (3.1.0): tiền tố trung kế gõ thừa ngay sau mã bị bỏ — `+84 0912 345 678`
|
|
44
|
+
* cũng ra `+84912345678` (nước giữ số 0 thì giữ: `+39 06 …`).
|
|
39
45
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Mọi cách gõ: tối đa 15 chữ số — trần của E.164 (`3.1.0`). Giá trị nạp từ ngoài
|
|
47
|
+
* (`value`/`defaultValue`) luôn hiện ở dạng quốc tế và không bị sửa. Không có quốc gia nào được
|
|
48
|
+
* chọn — MẶC ĐỊNH từ `3.0.0` — thì mọi chữ số đầu bị coi là mã quốc gia, như thượng nguồn: gõ
|
|
49
|
+
* `0912 345 678` ra `+0912345678`, gõ `84 912 345 678` ra `+84912345678` và cờ tự nhận Việt Nam.
|
|
50
|
+
*
|
|
51
|
+
* ⚠ Gói KHÔNG kiểm số. Mã quốc gia gõ THIẾU `+` sau khi đã chọn nước vẫn là số nội địa (`84 912 345
|
|
52
|
+
* 678` với Việt Nam ra `+8484912345678`), độ dài không được xét. Kiểm ở server (vd libphonenumber).
|
|
43
53
|
*
|
|
44
54
|
* Gốc là `role="group"` chưa có tên: đặt `aria-label` (vd "Phone number") hoặc `aria-labelledby`
|
|
45
55
|
* cho chính `PhoneInput`.
|
|
@@ -7,6 +7,7 @@ import * as React from "react";
|
|
|
7
7
|
import { useComitorLabels, useComitorLocale } from "../../i18n/locale-provider.js";
|
|
8
8
|
import { cn } from "../../lib/cn.js";
|
|
9
9
|
import { useComposedRefs } from "../../lib/compose-refs.js";
|
|
10
|
+
import { removeVietnameseTones } from "../../lib/text-search.js";
|
|
10
11
|
import { useAsRef } from "../../lib/use-as-ref.js";
|
|
11
12
|
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
12
13
|
import { useLazyRef } from "../../lib/use-lazy-ref.js";
|
|
@@ -287,11 +288,12 @@ function getCountries(locale) {
|
|
|
287
288
|
};
|
|
288
289
|
}).sort((a, b) => collator ? collator.compare(a.name, b.name) : a.name.localeCompare(b.name));
|
|
289
290
|
}
|
|
290
|
-
function removeDiacritics(text) {
|
|
291
|
-
return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/đ/g, "d").replace(/Đ/g, "D");
|
|
292
|
-
}
|
|
293
291
|
function filterCountries(value, search, keywords) {
|
|
294
|
-
return defaultFilter(
|
|
292
|
+
return defaultFilter(
|
|
293
|
+
removeVietnameseTones(value),
|
|
294
|
+
removeVietnameseTones(search),
|
|
295
|
+
keywords?.map(removeVietnameseTones)
|
|
296
|
+
);
|
|
295
297
|
}
|
|
296
298
|
const PRIMARY_COUNTRY_BY_DIAL_CODE = {
|
|
297
299
|
"+1": "US",
|
|
@@ -347,14 +349,101 @@ function formatPhoneNumber(value, countries) {
|
|
|
347
349
|
}
|
|
348
350
|
return formatted;
|
|
349
351
|
}
|
|
350
|
-
const
|
|
352
|
+
const E164_MAX_DIGITS = 15;
|
|
353
|
+
const DEFAULT_TRUNK_PREFIX = "0";
|
|
354
|
+
const TRUNK_PREFIX_BY_COUNTRY = {
|
|
355
|
+
IT: "",
|
|
356
|
+
SM: "",
|
|
357
|
+
VA: "",
|
|
358
|
+
CI: "",
|
|
359
|
+
BJ: "",
|
|
360
|
+
RU: "8",
|
|
361
|
+
KZ: "8",
|
|
362
|
+
HU: "06"
|
|
363
|
+
};
|
|
364
|
+
const AMBIGUOUS_TRUNK_NATIONAL_LENGTH = {
|
|
365
|
+
RU: 10,
|
|
366
|
+
KZ: 10
|
|
367
|
+
};
|
|
368
|
+
const NANP_TRUNK_PREFIX = "1";
|
|
369
|
+
const NANP_NATIONAL_LENGTH = 10;
|
|
370
|
+
function isNanp(country) {
|
|
371
|
+
return country.dialCode.startsWith("+1");
|
|
372
|
+
}
|
|
373
|
+
function getTrunkPrefix(country) {
|
|
374
|
+
if (isNanp(country)) return NANP_TRUNK_PREFIX;
|
|
375
|
+
return TRUNK_PREFIX_BY_COUNTRY[country.code] ?? DEFAULT_TRUNK_PREFIX;
|
|
376
|
+
}
|
|
377
|
+
function stripTrunkPrefix(nationalDigits, country) {
|
|
378
|
+
const prefix = getTrunkPrefix(country);
|
|
379
|
+
if (!prefix || !nationalDigits.startsWith(prefix)) return nationalDigits;
|
|
380
|
+
const fixedLength = AMBIGUOUS_TRUNK_NATIONAL_LENGTH[country.code];
|
|
381
|
+
if (fixedLength !== void 0 && nationalDigits.length <= fixedLength) return nationalDigits;
|
|
382
|
+
return nationalDigits.slice(prefix.length);
|
|
383
|
+
}
|
|
384
|
+
const DEFAULT_EXIT_PREFIXES = ["00"];
|
|
385
|
+
const NANP_EXIT_PREFIXES = ["011", "00"];
|
|
386
|
+
const EXIT_PREFIXES_BY_COUNTRY = {
|
|
387
|
+
AU: ["0011"],
|
|
388
|
+
JP: ["010"]
|
|
389
|
+
};
|
|
390
|
+
function stripExitPrefix(digits, country) {
|
|
391
|
+
const prefixes = !country ? DEFAULT_EXIT_PREFIXES : isNanp(country) ? NANP_EXIT_PREFIXES : EXIT_PREFIXES_BY_COUNTRY[country.code] ?? DEFAULT_EXIT_PREFIXES;
|
|
392
|
+
const prefix = prefixes.find((candidate) => digits.startsWith(candidate));
|
|
393
|
+
return prefix === void 0 ? null : digits.slice(prefix.length);
|
|
394
|
+
}
|
|
395
|
+
function findCallingCode(digits) {
|
|
396
|
+
const entry = COUNTRY_DATA.find(([, dialCode]) => digits.startsWith(dialCode));
|
|
397
|
+
return entry ? { code: entry[0].toUpperCase(), dialCode: `+${entry[1]}` } : void 0;
|
|
398
|
+
}
|
|
399
|
+
function normalizeInternationalDigits(digits) {
|
|
400
|
+
let normalized = digits;
|
|
401
|
+
if (digits.startsWith("1")) {
|
|
402
|
+
if (digits.startsWith(NANP_TRUNK_PREFIX, 1)) normalized = `1${digits.slice(2)}`;
|
|
403
|
+
} else {
|
|
404
|
+
const callingCode = findCallingCode(digits);
|
|
405
|
+
if (callingCode) {
|
|
406
|
+
const countryCode = callingCode.dialCode.slice(1);
|
|
407
|
+
normalized = `${countryCode}${stripTrunkPrefix(digits.slice(countryCode.length), callingCode)}`;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return normalized.slice(0, E164_MAX_DIGITS);
|
|
411
|
+
}
|
|
351
412
|
const NATIONAL_INPUT_DISALLOWED = /[^\d\s().-]/g;
|
|
413
|
+
function nationalToE164Digits(nationalInput, country) {
|
|
414
|
+
const national = stripTrunkPrefix(nationalInput.replace(/\D/g, ""), country);
|
|
415
|
+
if (!national) return "";
|
|
416
|
+
const countryCode = isNanp(country) && national.length === NANP_NATIONAL_LENGTH ? "1" : country.dialCode.slice(1);
|
|
417
|
+
return `${countryCode}${national}`;
|
|
418
|
+
}
|
|
352
419
|
function nationalToE164(nationalInput, country) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
420
|
+
const digits = nationalToE164Digits(nationalInput, country);
|
|
421
|
+
return digits ? `+${digits.slice(0, E164_MAX_DIGITS)}` : "";
|
|
422
|
+
}
|
|
423
|
+
function limitNationalInput(nationalInput, country) {
|
|
424
|
+
for (let i = 0; i < nationalInput.length; i++) {
|
|
425
|
+
if (!/\d/.test(nationalInput.charAt(i))) continue;
|
|
426
|
+
if (nationalToE164Digits(nationalInput.slice(0, i + 1), country).length > E164_MAX_DIGITS) {
|
|
427
|
+
return nationalInput.slice(0, i);
|
|
428
|
+
}
|
|
356
429
|
}
|
|
357
|
-
return
|
|
430
|
+
return nationalInput;
|
|
431
|
+
}
|
|
432
|
+
function parseTypedNumber(inputValue, country) {
|
|
433
|
+
const startsWithPlus = inputValue.trimStart().startsWith("+");
|
|
434
|
+
const digits = inputValue.replace(/\D/g, "");
|
|
435
|
+
const afterExitPrefix = stripExitPrefix(digits, country);
|
|
436
|
+
if (!startsWithPlus && country && afterExitPrefix === null) {
|
|
437
|
+
const nationalInput = limitNationalInput(inputValue.replace(NATIONAL_INPUT_DISALLOWED, ""), country);
|
|
438
|
+
return { value: nationalToE164(nationalInput, country), nationalInput, startsWithPlus: false };
|
|
439
|
+
}
|
|
440
|
+
const international = startsWithPlus || afterExitPrefix !== null;
|
|
441
|
+
const e164Digits = normalizeInternationalDigits(afterExitPrefix ?? digits);
|
|
442
|
+
return {
|
|
443
|
+
value: e164Digits ? `+${e164Digits}` : international ? "+" : "",
|
|
444
|
+
nationalInput: null,
|
|
445
|
+
startsWithPlus: international
|
|
446
|
+
};
|
|
358
447
|
}
|
|
359
448
|
function getNationalInput(state, countries) {
|
|
360
449
|
const { nationalInput, country, value } = state;
|
|
@@ -459,10 +548,12 @@ function PhoneInput(props) {
|
|
|
459
548
|
if (nationalInput2 !== null) {
|
|
460
549
|
const selected = propsRef.current.countries.find((c) => c.code === value2);
|
|
461
550
|
if (selected) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
551
|
+
const next = parseTypedNumber(nationalInput2, selected);
|
|
552
|
+
stateRef.current.nationalInput = next.nationalInput;
|
|
553
|
+
stateRef.current.startsWithPlus = next.startsWithPlus;
|
|
554
|
+
if (next.value !== stateRef.current.value) {
|
|
555
|
+
stateRef.current.value = next.value;
|
|
556
|
+
propsRef.current.onValueChange?.(next.value);
|
|
466
557
|
}
|
|
467
558
|
} else {
|
|
468
559
|
stateRef.current.nationalInput = null;
|
|
@@ -577,7 +668,7 @@ function PhoneInput(props) {
|
|
|
577
668
|
}
|
|
578
669
|
function PhoneInputCountrySelect(props) {
|
|
579
670
|
const { disabled: disabledProp, className, children, onOpenChange: onOpenChangeProp, ...popoverProps } = props;
|
|
580
|
-
const { countries, inputRef, disabled, showFlag, labels } = usePhoneInputContext(COUNTRY_SELECT_NAME);
|
|
671
|
+
const { countries, inputRef, disabled, readOnly, showFlag, labels } = usePhoneInputContext(COUNTRY_SELECT_NAME);
|
|
581
672
|
const store = useStoreContext(COUNTRY_SELECT_NAME);
|
|
582
673
|
const country = useStore((state) => state.country);
|
|
583
674
|
const open = useStore((state) => state.open);
|
|
@@ -586,10 +677,11 @@ function PhoneInputCountrySelect(props) {
|
|
|
586
677
|
const countryContext = countries.find((c) => c.code === country);
|
|
587
678
|
const onOpenChange = React.useCallback(
|
|
588
679
|
(open2) => {
|
|
680
|
+
if (open2 && readOnly) return;
|
|
589
681
|
store.setState("open", open2);
|
|
590
682
|
onOpenChangeRef.current?.(open2);
|
|
591
683
|
},
|
|
592
|
-
[store, onOpenChangeRef]
|
|
684
|
+
[store, onOpenChangeRef, readOnly]
|
|
593
685
|
);
|
|
594
686
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange, ...popoverProps, children: [
|
|
595
687
|
/* @__PURE__ */ jsxs(
|
|
@@ -597,6 +689,8 @@ function PhoneInputCountrySelect(props) {
|
|
|
597
689
|
{
|
|
598
690
|
"data-slot": "phone-input-country-select",
|
|
599
691
|
"aria-label": labels.countrySelect(countryContext),
|
|
692
|
+
"aria-disabled": readOnly && !isDisabled ? true : void 0,
|
|
693
|
+
"data-readonly": readOnly ? "" : void 0,
|
|
600
694
|
disabled: isDisabled,
|
|
601
695
|
className: cn(
|
|
602
696
|
/*
|
|
@@ -681,27 +775,23 @@ function PhoneInputField(props) {
|
|
|
681
775
|
onChangeRef.current?.(event);
|
|
682
776
|
if (event.defaultPrevented) return;
|
|
683
777
|
const inputValue = event.target.value;
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
store.setState("value", nationalToE164(nextNationalInput, selectedCountry));
|
|
698
|
-
return;
|
|
778
|
+
const next = parseTypedNumber(
|
|
779
|
+
inputValue,
|
|
780
|
+
countries.find((c) => c.code === store.getState().country)
|
|
781
|
+
);
|
|
782
|
+
if (next.nationalInput !== null && next.nationalInput !== inputValue) {
|
|
783
|
+
const target = event.target;
|
|
784
|
+
const caret = target.selectionStart ?? inputValue.length;
|
|
785
|
+
const nextCaret = Math.min(
|
|
786
|
+
inputValue.slice(0, caret).replace(NATIONAL_INPUT_DISALLOWED, "").length,
|
|
787
|
+
next.nationalInput.length
|
|
788
|
+
);
|
|
789
|
+
target.value = next.nationalInput;
|
|
790
|
+
target.setSelectionRange(nextCaret, nextCaret);
|
|
699
791
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
store.setState("
|
|
703
|
-
store.setState("nationalInput", null);
|
|
704
|
-
store.setState("value", newValue);
|
|
792
|
+
store.setState("startsWithPlus", next.startsWithPlus);
|
|
793
|
+
store.setState("nationalInput", next.nationalInput);
|
|
794
|
+
store.setState("value", next.value);
|
|
705
795
|
},
|
|
706
796
|
[store, countries, onChangeRef, isDisabled, isReadOnly]
|
|
707
797
|
);
|