@comitor/ui 3.0.1 → 3.2.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/dist/components/composite/avatar-group.d.ts +10 -1
- package/dist/components/composite/avatar-group.js +37 -14
- package/dist/components/composite/avatar-group.js.map +1 -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/status-config.d.ts +41 -1
- package/dist/components/composite/status-config.js +6 -0
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/composite/status-pill.d.ts +4 -1
- package/dist/components/composite/status-pill.js +1 -1
- package/dist/components/composite/status-pill.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/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/mask-input-constants.d.ts +34 -2
- package/dist/components/ui/mask-input-constants.js +56 -5
- 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 +5 -1
- 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 +120 -33
- package/dist/components/ui/phone-input.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/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/data-grid/data-grid-cell-variants.js +27 -9
- 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 +23 -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/namespaces/search-filter-bar.js +8 -6
- package/dist/i18n/namespaces/search-filter-bar.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/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/shell/app-shell.d.ts +1 -0
- package/dist/shell/display-axis-provider.d.ts +1 -1
- 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/dist/tokens.d.ts +43 -11
- package/dist/tokens.js +36 -9
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +73 -13
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
|
4
|
-
import { useComitorLabels } from "../../i18n/locale-provider.js";
|
|
4
|
+
import { useComitorLabels, useComitorLocale } from "../../i18n/locale-provider.js";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
6
|
import { Button } from "../ui/button.js";
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select.js";
|
|
8
8
|
import { DEFAULT_PAGE_SIZE_OPTIONS } from "./table-pagination-constants.js";
|
|
9
|
+
function formatNumber(value, locale) {
|
|
10
|
+
try {
|
|
11
|
+
return value.toLocaleString(locale);
|
|
12
|
+
} catch {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
9
16
|
function TablePagination({
|
|
10
17
|
page,
|
|
11
18
|
pageSize,
|
|
@@ -20,6 +27,7 @@ function TablePagination({
|
|
|
20
27
|
className
|
|
21
28
|
}) {
|
|
22
29
|
const labels = useComitorLabels("tablePagination", labelsProp);
|
|
30
|
+
const { locale } = useComitorLocale();
|
|
23
31
|
const unitLabel = unitLabelProp ?? labels.unitLabel;
|
|
24
32
|
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
|
25
33
|
const current = Math.min(Math.max(1, page), pageCount);
|
|
@@ -39,7 +47,7 @@ function TablePagination({
|
|
|
39
47
|
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: labels.pageSizePrefix }),
|
|
40
48
|
/* @__PURE__ */ jsxs(Select, { value: String(pageSize), onValueChange: (value) => onPageSizeChange(Number(value)), children: [
|
|
41
49
|
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-[4.5rem]", "aria-label": labels.pageSizeSelect(unitLabel), children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
42
|
-
/* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: size }, size)) })
|
|
50
|
+
/* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: formatNumber(size, locale) }, size)) })
|
|
43
51
|
] })
|
|
44
52
|
] }),
|
|
45
53
|
/* @__PURE__ */ jsx("span", { role: "status", className: "tabular-nums", children: labels.range(start, end, total, unitLabel) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/table-pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { useComitorLabels } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select.js\";\nimport { DEFAULT_PAGE_SIZE_OPTIONS, type TablePaginationLabels } from \"./table-pagination-constants.js\";\n\nexport interface TablePaginationProps {\n /** Trang hiện tại, đếm từ 1 */\n page: number;\n pageSize: number;\n /** Tổng số bản ghi (sau khi lọc) */\n total: number;\n pageSizeOptions?: readonly number[];\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n /** Đơn vị đếm theo nội dung bảng, vd \"tasks\", \"contacts\" — mặc định `labels.unitLabel` (\"items\") */\n unitLabel?: string;\n /** Ẩn ô chọn số dòng/trang (khi cỡ trang do hệ thống quyết định) */\n hidePageSize?: boolean;\n /** Nhãn vùng phân trang — cần khi một trang có nhiều bảng; mặc định `labels.region` */\n label?: string;\n /**\n * Ghi đè chuỗi hiển thị/a11y — không truyền thì theo namespace `tablePagination` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Xem `TablePaginationLabels`.\n */\n labels?: Partial<TablePaginationLabels>;\n className?: string;\n}\n\n/**\n * Thanh phân trang đi kèm `DataTable`.\n *\n * Tổng số trang tính từ `total`/`pageSize` ngay tại đây thay vì bắt app truyền vào — hai bên\n * tự tính rất dễ lệch nhau một trang ở bản ghi cuối, và người dùng sẽ thấy nút sang trang sau\n * còn bấm được trong khi bảng đã hết dữ liệu.\n */\nexport function TablePagination({\n page,\n pageSize,\n total,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n onPageChange,\n onPageSizeChange,\n unitLabel: unitLabelProp,\n hidePageSize = false,\n label,\n labels: labelsProp,\n className\n}: TablePaginationProps) {\n const labels = useComitorLabels(\"tablePagination\", labelsProp);\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));\n const current = Math.min(Math.max(1, page), pageCount);\n const start = total === 0 ? 0 : (current - 1) * pageSize + 1;\n const end = Math.min(current * pageSize, total);\n const isFirst = current <= 1;\n const isLast = current >= pageCount;\n\n return (\n <nav\n aria-label={label ?? labels.region}\n data-slot=\"table-pagination\"\n className={cn(\"flex flex-wrap items-center justify-between gap-3 py-2 text-sm\", className)}\n >\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n {!hidePageSize && (\n <>\n <span className=\"hidden sm:inline\">{labels.pageSizePrefix}</span>\n <Select value={String(pageSize)} onValueChange={(value) => onPageSizeChange(Number(value))}>\n <SelectTrigger className=\"h-8 w-[4.5rem]\" aria-label={labels.pageSizeSelect(unitLabel)}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((size) => (\n <SelectItem key={size} value={String(size)}>\n {size}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n )}\n <span role=\"status\" className=\"tabular-nums\">\n {labels.range(start, end, total, unitLabel)}\n </span>\n </div>\n\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isFirst}\n onClick={() => onPageChange(1)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.firstPage}</span>\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isFirst} onClick={() => onPageChange(current - 1)}>\n <ChevronLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.previousPage}</span>\n </Button>\n\n <span role=\"status\" className=\"px-2 tabular-nums text-muted-foreground\">\n {labels.pageStatus(current, pageCount)}\n </span>\n\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isLast} onClick={() => onPageChange(current + 1)}>\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.nextPage}</span>\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isLast}\n onClick={() => onPageChange(pageCount)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.lastPage}</span>\n </Button>\n </div>\n </nav>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/table-pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select.js\";\nimport { DEFAULT_PAGE_SIZE_OPTIONS, type TablePaginationLabels } from \"./table-pagination-constants.js\";\n\n/**\n * Số theo `locale` của `ComitorLocaleProvider` — cho số in NGOÀI bảng chữ (các mức cỡ trang: `1,000` /\n * `1.000`). Mã locale hỏng (`\"vi_VN\"`, `\"\"`) làm `Intl` ném `RangeError`; `ComitorLocaleProvider` cố ý\n * nhận mã hỏng mà không ném (xem `getComitorLanguage`), nên ở đây rơi về số thô thay vì làm trắng cả\n * trang danh sách.\n */\nfunction formatNumber(value: number, locale: string): string {\n try {\n return value.toLocaleString(locale);\n } catch {\n return String(value);\n }\n}\n\nexport interface TablePaginationProps {\n /** Trang hiện tại, đếm từ 1 */\n page: number;\n pageSize: number;\n /** Tổng số bản ghi (sau khi lọc) */\n total: number;\n pageSizeOptions?: readonly number[];\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n /** Đơn vị đếm theo nội dung bảng, vd \"tasks\", \"contacts\" — mặc định `labels.unitLabel` (\"items\") */\n unitLabel?: string;\n /** Ẩn ô chọn số dòng/trang (khi cỡ trang do hệ thống quyết định) */\n hidePageSize?: boolean;\n /** Nhãn vùng phân trang — cần khi một trang có nhiều bảng; mặc định `labels.region` */\n label?: string;\n /**\n * Ghi đè chuỗi hiển thị/a11y — không truyền thì theo namespace `tablePagination` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Xem `TablePaginationLabels`.\n */\n labels?: Partial<TablePaginationLabels>;\n className?: string;\n}\n\n/**\n * Thanh phân trang đi kèm `DataTable`.\n *\n * Tổng số trang tính từ `total`/`pageSize` ngay tại đây thay vì bắt app truyền vào — hai bên\n * tự tính rất dễ lệch nhau một trang ở bản ghi cuối, và người dùng sẽ thấy nút sang trang sau\n * còn bấm được trong khi bảng đã hết dữ liệu.\n */\nexport function TablePagination({\n page,\n pageSize,\n total,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n onPageChange,\n onPageSizeChange,\n unitLabel: unitLabelProp,\n hidePageSize = false,\n label,\n labels: labelsProp,\n className\n}: TablePaginationProps) {\n const labels = useComitorLabels(\"tablePagination\", labelsProp);\n const { locale } = useComitorLocale();\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));\n const current = Math.min(Math.max(1, page), pageCount);\n const start = total === 0 ? 0 : (current - 1) * pageSize + 1;\n const end = Math.min(current * pageSize, total);\n const isFirst = current <= 1;\n const isLast = current >= pageCount;\n\n return (\n <nav\n aria-label={label ?? labels.region}\n data-slot=\"table-pagination\"\n className={cn(\"flex flex-wrap items-center justify-between gap-3 py-2 text-sm\", className)}\n >\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n {!hidePageSize && (\n <>\n <span className=\"hidden sm:inline\">{labels.pageSizePrefix}</span>\n <Select value={String(pageSize)} onValueChange={(value) => onPageSizeChange(Number(value))}>\n <SelectTrigger className=\"h-8 w-[4.5rem]\" aria-label={labels.pageSizeSelect(unitLabel)}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {/* Giá trị giữ số thô (máy đọc); chữ hiện ra — kể cả ở `SelectValue` — theo locale. */}\n {pageSizeOptions.map((size) => (\n <SelectItem key={size} value={String(size)}>\n {formatNumber(size, locale)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n )}\n <span role=\"status\" className=\"tabular-nums\">\n {labels.range(start, end, total, unitLabel)}\n </span>\n </div>\n\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isFirst}\n onClick={() => onPageChange(1)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.firstPage}</span>\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isFirst} onClick={() => onPageChange(current - 1)}>\n <ChevronLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.previousPage}</span>\n </Button>\n\n <span role=\"status\" className=\"px-2 tabular-nums text-muted-foreground\">\n {labels.pageStatus(current, pageCount)}\n </span>\n\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isLast} onClick={() => onPageChange(current + 1)}>\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.nextPage}</span>\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isLast}\n onClick={() => onPageChange(pageCount)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.lastPage}</span>\n </Button>\n </div>\n </nav>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,31 +1,12 @@
|
|
|
1
|
+
export { matchesSearch, normalizeVi, removeVietnameseTones } from '../../lib/text-search.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Tiện ích chuỗi dùng chung cho tầng composite (không phụ thuộc React → dùng được cả 2 phía).
|
|
3
5
|
*
|
|
4
6
|
* Vì sao không nhét vào `lib/cn.ts`: những hàm này phục vụ riêng composite (tìm kiếm không dấu,
|
|
5
7
|
* initials avatar). Tách file để `cn()` giữ đúng một việc.
|
|
6
8
|
*/
|
|
7
|
-
|
|
8
|
-
* Bỏ dấu tiếng Việt để so khớp tìm kiếm ("Nguyễn" ⇄ "nguyen").
|
|
9
|
-
*
|
|
10
|
-
* Dùng NFD tách dấu thành ký tự tổ hợp rồi loại dải U+0300–U+036F. `đ/Đ` KHÔNG phải chữ có dấu
|
|
11
|
-
* tổ hợp (là ký tự riêng trong Unicode) nên phải thay tay — thiếu bước này thì gõ "duc" sẽ không
|
|
12
|
-
* ra "Đức".
|
|
13
|
-
*/
|
|
14
|
-
declare function removeVietnameseTones(input: string): string;
|
|
15
|
-
/**
|
|
16
|
-
* Chuẩn hoá chuỗi để so khớp: bỏ dấu + hạ chữ thường + `trim`.
|
|
17
|
-
*
|
|
18
|
-
* Đây là phép chuẩn hoá DUY NHẤT của cả gói (tầng shell cũng gọi lại hàm này, không giữ bản
|
|
19
|
-
* riêng) — mỗi nơi tự chuẩn hoá một kiểu là cùng một từ khoá ra hai kết quả tìm kiếm khác nhau.
|
|
20
|
-
* `removeVietnameseTones` ở trên là bậc thấp hơn, chỉ bỏ dấu, dùng khi cần giữ nguyên hoa/thường
|
|
21
|
-
* (vd sinh slug).
|
|
22
|
-
*/
|
|
23
|
-
declare function normalizeVi(value: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* So khớp không dấu, không phân biệt hoa thường: `needle` có nằm trong `haystack` không.
|
|
26
|
-
* Cả hai vế đi qua `normalizeVi` — cùng một luật với ô tìm kiếm của shell.
|
|
27
|
-
*/
|
|
28
|
-
declare function matchesSearch(haystack: string, needle: string): boolean;
|
|
9
|
+
|
|
29
10
|
/**
|
|
30
11
|
* Initials kiểu Việt Nam: chữ đầu của HỌ + chữ đầu của TÊN ("Nguyễn Đức Thành" → "NT").
|
|
31
12
|
*
|
|
@@ -36,4 +17,4 @@ declare function getVietnameseInitials(name: string): string;
|
|
|
36
17
|
/** Cắt chuỗi kèm dấu "…" — dùng cho tên workspace/task dài trong chip, tooltip. */
|
|
37
18
|
declare function truncate(input: string, maxLength: number): string;
|
|
38
19
|
|
|
39
|
-
export { getVietnameseInitials,
|
|
20
|
+
export { getVietnameseInitials, truncate };
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return input.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D");
|
|
3
|
-
}
|
|
4
|
-
function normalizeVi(value) {
|
|
5
|
-
return removeVietnameseTones(value).toLowerCase().trim();
|
|
6
|
-
}
|
|
7
|
-
function matchesSearch(haystack, needle) {
|
|
8
|
-
const query = normalizeVi(needle);
|
|
9
|
-
if (!query) return true;
|
|
10
|
-
return normalizeVi(haystack).includes(query);
|
|
11
|
-
}
|
|
1
|
+
import { matchesSearch, normalizeVi, removeVietnameseTones } from "../../lib/text-search.js";
|
|
12
2
|
function getVietnameseInitials(name) {
|
|
13
3
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
14
4
|
if (parts.length === 0) return "?";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/text-utils.ts"],"sourcesContent":["/**\n * Tiện ích chuỗi dùng chung cho tầng composite (không phụ thuộc React → dùng được cả 2 phía).\n *\n * Vì sao không nhét vào `lib/cn.ts`: những hàm này phục vụ riêng composite (tìm kiếm không dấu,\n * initials avatar). Tách file để `cn()` giữ đúng một việc.\n */\n\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/text-utils.ts"],"sourcesContent":["/**\n * Tiện ích chuỗi dùng chung cho tầng composite (không phụ thuộc React → dùng được cả 2 phía).\n *\n * Vì sao không nhét vào `lib/cn.ts`: những hàm này phục vụ riêng composite (tìm kiếm không dấu,\n * initials avatar). Tách file để `cn()` giữ đúng một việc.\n */\n\n// ⚠ Từ `3.1.0` ba hàm tìm kiếm không dấu sống ở `lib/text-search.ts` (Tier 1 cũng dùng chúng); file\n// này xuất lại NGUYÊN tên và chữ ký — với app không có gì đổi.\nexport { matchesSearch, normalizeVi, removeVietnameseTones } from \"../../lib/text-search.js\";\n\n/**\n * Initials kiểu Việt Nam: chữ đầu của HỌ + chữ đầu của TÊN (\"Nguyễn Đức Thành\" → \"NT\").\n *\n * Người Việt gọi nhau bằng TÊN (từ cuối), nên lấy 2 từ đầu như phương Tây (\"NĐ\") là sai thói quen.\n * Tên một từ → lấy 1 ký tự. Chuỗi rỗng → \"?\" (tránh ô avatar trống trơn).\n */\nexport function getVietnameseInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n\n const first = parts[0] ?? \"\";\n const last = parts[parts.length - 1] ?? \"\";\n const initials = parts.length === 1 ? first.slice(0, 1) : `${first.slice(0, 1)}${last.slice(0, 1)}`;\n\n return initials.toLocaleUpperCase(\"vi-VN\");\n}\n\n/** Cắt chuỗi kèm dấu \"…\" — dùng cho tên workspace/task dài trong chip, tooltip. */\nexport function truncate(input: string, maxLength: number): string {\n if (input.length <= maxLength) return input;\n return `${input.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;\n}\n"],"mappings":"AASA,SAAS,eAAe,aAAa,6BAA6B;AAQ3D,SAAS,sBAAsB,MAAsB;AAC1D,QAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAO;AACrD,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,QAAQ,MAAM,CAAC,KAAK;AAC1B,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AACxC,QAAM,WAAW,MAAM,WAAW,IAAI,MAAM,MAAM,GAAG,CAAC,IAAI,GAAG,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,CAAC;AAEjG,SAAO,SAAS,kBAAkB,OAAO;AAC3C;AAGO,SAAS,SAAS,OAAe,WAA2B;AACjE,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,GAAG,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAC;AAChE;","names":[]}
|
|
@@ -53,6 +53,14 @@ declare const bannerVariants: (props?: ({
|
|
|
53
53
|
interface BannerProps extends DivProps, VariantProps<typeof bannerVariants> {
|
|
54
54
|
open?: boolean;
|
|
55
55
|
defaultOpen?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Báo `false` khi banner đóng THẬT: `BannerClose`; trong `Banners` thêm hết `duration` hay hàng đợi
|
|
58
|
+
* gỡ nó. Không báo khi render lại, khi unmount, hay khi chính app đặt `open={false}`.
|
|
59
|
+
*
|
|
60
|
+
* Focus: nếu lúc banner rời DOM focus còn ở trong nó, gói trả focus về phần tử giữ focus ngay trước
|
|
61
|
+
* khi vào banner. Muốn đưa focus nơi khác (vd tiêu đề trang) thì làm ngay ở đây — app luôn thắng.
|
|
62
|
+
* Không có phần tử ấy (hoặc nó đã bị gỡ) thì gói không đoán, focus rơi về `<body>`.
|
|
63
|
+
*/
|
|
56
64
|
onOpenChange?: (open: boolean) => void;
|
|
57
65
|
onDismiss?: () => void;
|
|
58
66
|
priority?: number;
|
|
@@ -51,6 +51,28 @@ function useBanner() {
|
|
|
51
51
|
};
|
|
52
52
|
}, [id, variant, dismissible, onClose, storeContext]);
|
|
53
53
|
}
|
|
54
|
+
function insertByPriority(banners, banner) {
|
|
55
|
+
const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY;
|
|
56
|
+
const insertIndex = banners.findIndex((b) => (b.priority ?? DEFAULT_BANNER_PRIORITY) < priority);
|
|
57
|
+
if (insertIndex === -1) {
|
|
58
|
+
banners.push(banner);
|
|
59
|
+
} else {
|
|
60
|
+
banners.splice(insertIndex, 0, banner);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function scheduleAutoClose(timeouts, id, duration, onElapsed) {
|
|
64
|
+
const pendingTimeoutId = timeouts.get(id);
|
|
65
|
+
if (pendingTimeoutId !== void 0) {
|
|
66
|
+
clearTimeout(pendingTimeoutId);
|
|
67
|
+
timeouts.delete(id);
|
|
68
|
+
}
|
|
69
|
+
if (!duration || duration <= 0) return;
|
|
70
|
+
const timeoutId = setTimeout(() => {
|
|
71
|
+
onElapsed();
|
|
72
|
+
timeouts.delete(id);
|
|
73
|
+
}, duration);
|
|
74
|
+
timeouts.set(id, timeoutId);
|
|
75
|
+
}
|
|
54
76
|
function Banners(props) {
|
|
55
77
|
const { children, maxVisible = 1, side = "top", strategy = "fixed", container: containerProp } = props;
|
|
56
78
|
const instanceId = React.useId();
|
|
@@ -78,25 +100,34 @@ function Banners(props) {
|
|
|
78
100
|
idCounterRef.current += 1;
|
|
79
101
|
const id = `${instanceId}-${idCounterRef.current}`;
|
|
80
102
|
const newBanner = { ...banner, id };
|
|
81
|
-
const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY;
|
|
82
103
|
const banners2 = [...stateRef.current.banners];
|
|
83
|
-
|
|
84
|
-
if (insertIndex === -1) {
|
|
85
|
-
banners2.push(newBanner);
|
|
86
|
-
} else {
|
|
87
|
-
banners2.splice(insertIndex, 0, newBanner);
|
|
88
|
-
}
|
|
104
|
+
insertByPriority(banners2, newBanner);
|
|
89
105
|
stateRef.current.banners = banners2;
|
|
90
106
|
store.notify();
|
|
91
|
-
|
|
92
|
-
const timeoutId = setTimeout(() => {
|
|
93
|
-
store.onRemovingChange(id, true);
|
|
94
|
-
timeoutsRef.current.delete(id);
|
|
95
|
-
}, banner.duration);
|
|
96
|
-
timeoutsRef.current.set(id, timeoutId);
|
|
97
|
-
}
|
|
107
|
+
scheduleAutoClose(timeoutsRef.current, id, banner.duration, () => store.onRemovingChange(id, true));
|
|
98
108
|
return id;
|
|
99
109
|
},
|
|
110
|
+
onBannerUpdate: (id, patch) => {
|
|
111
|
+
const banners2 = stateRef.current.banners;
|
|
112
|
+
const index = banners2.findIndex((b) => b.id === id);
|
|
113
|
+
const current = banners2[index];
|
|
114
|
+
if (!current) return;
|
|
115
|
+
const keys = Object.keys(patch);
|
|
116
|
+
if (keys.every((key) => Object.is(current[key], patch[key]))) return;
|
|
117
|
+
const next = { ...current, ...patch };
|
|
118
|
+
const nextBanners = [...banners2];
|
|
119
|
+
if (Object.is(next.priority ?? DEFAULT_BANNER_PRIORITY, current.priority ?? DEFAULT_BANNER_PRIORITY)) {
|
|
120
|
+
nextBanners[index] = next;
|
|
121
|
+
} else {
|
|
122
|
+
nextBanners.splice(index, 1);
|
|
123
|
+
insertByPriority(nextBanners, next);
|
|
124
|
+
}
|
|
125
|
+
stateRef.current.banners = nextBanners;
|
|
126
|
+
if (!Object.is(next.duration, current.duration) && !stateRef.current.removing.has(id)) {
|
|
127
|
+
scheduleAutoClose(timeoutsRef.current, id, next.duration, () => store.onRemovingChange(id, true));
|
|
128
|
+
}
|
|
129
|
+
store.notify();
|
|
130
|
+
},
|
|
100
131
|
onBannerRemove: (id) => {
|
|
101
132
|
const banner = stateRef.current.banners.find((b) => b.id === id);
|
|
102
133
|
if (!banner) return;
|
|
@@ -232,6 +263,49 @@ const bannerVariants = cva(
|
|
|
232
263
|
}
|
|
233
264
|
}
|
|
234
265
|
);
|
|
266
|
+
function useFocusReturn(active, onFocusProp, onBlurProp) {
|
|
267
|
+
const returnFocusRef = React.useRef(null);
|
|
268
|
+
const focusInsideRef = React.useRef(false);
|
|
269
|
+
const onFocus = React.useCallback(
|
|
270
|
+
(event) => {
|
|
271
|
+
onFocusProp?.(event);
|
|
272
|
+
const from = event.relatedTarget;
|
|
273
|
+
if (!focusInsideRef.current && from instanceof HTMLElement && !event.currentTarget.contains(from)) {
|
|
274
|
+
returnFocusRef.current = from;
|
|
275
|
+
}
|
|
276
|
+
focusInsideRef.current = true;
|
|
277
|
+
},
|
|
278
|
+
[onFocusProp]
|
|
279
|
+
);
|
|
280
|
+
const onBlur = React.useCallback(
|
|
281
|
+
(event) => {
|
|
282
|
+
onBlurProp?.(event);
|
|
283
|
+
const to = event.relatedTarget;
|
|
284
|
+
if (!(to instanceof Node) || !event.currentTarget.contains(to)) {
|
|
285
|
+
focusInsideRef.current = false;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
[onBlurProp]
|
|
289
|
+
);
|
|
290
|
+
useIsomorphicLayoutEffect(() => {
|
|
291
|
+
if (!active) return;
|
|
292
|
+
return () => {
|
|
293
|
+
const returnTarget = returnFocusRef.current;
|
|
294
|
+
const hadFocus = focusInsideRef.current;
|
|
295
|
+
returnFocusRef.current = null;
|
|
296
|
+
focusInsideRef.current = false;
|
|
297
|
+
if (!hadFocus || !returnTarget) return;
|
|
298
|
+
queueMicrotask(() => {
|
|
299
|
+
const ownerDocument = returnTarget.ownerDocument;
|
|
300
|
+
const activeElement = ownerDocument.activeElement;
|
|
301
|
+
if (activeElement && activeElement !== ownerDocument.body && activeElement.isConnected) return;
|
|
302
|
+
if (!returnTarget.isConnected) return;
|
|
303
|
+
returnTarget.focus({ preventScroll: true });
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
}, [active]);
|
|
307
|
+
return { onFocus, onBlur };
|
|
308
|
+
}
|
|
235
309
|
function BannerImpl(props) {
|
|
236
310
|
const { banner, side, index } = props;
|
|
237
311
|
const store = useStoreContext("BannerImpl");
|
|
@@ -257,9 +331,15 @@ function BannerImpl(props) {
|
|
|
257
331
|
return () => cancelAnimationFrame(frame);
|
|
258
332
|
}, []);
|
|
259
333
|
useIsomorphicLayoutEffect(() => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
store.onHeightChange(banner.id, height);
|
|
334
|
+
const node = bannerRef.current;
|
|
335
|
+
if (!node || removing) return;
|
|
336
|
+
store.onHeightChange(banner.id, node.getBoundingClientRect().height);
|
|
337
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
338
|
+
const observer = new ResizeObserver(() => {
|
|
339
|
+
store.onHeightChange(banner.id, node.getBoundingClientRect().height);
|
|
340
|
+
});
|
|
341
|
+
observer.observe(node);
|
|
342
|
+
return () => observer.disconnect();
|
|
263
343
|
}, [store, banner.id, removing]);
|
|
264
344
|
React.useEffect(() => {
|
|
265
345
|
if (!removing) return;
|
|
@@ -269,6 +349,7 @@ function BannerImpl(props) {
|
|
|
269
349
|
}, [removing, store, banner.id]);
|
|
270
350
|
const onClose = React.useCallback(() => store.onRemovingChange(banner.id, true), [store, banner.id]);
|
|
271
351
|
const onRemove = React.useCallback(() => store.onBannerRemove(banner.id), [store, banner.id]);
|
|
352
|
+
const { onFocus, onBlur } = useFocusReturn(true);
|
|
272
353
|
const dismissible = banner.dismissible ?? DEFAULT_BANNER_DISMISSIBLE;
|
|
273
354
|
const contextValue = React.useMemo(
|
|
274
355
|
() => ({ id: banner.id, variant: banner.variant, dismissible, onClose }),
|
|
@@ -319,10 +400,36 @@ function BannerImpl(props) {
|
|
|
319
400
|
// Thời lượng qua `--banner-duration` (0 khi giảm chuyển động) — xem chú thích ở `Banners`.
|
|
320
401
|
transition: `transform var(--banner-duration, ${BANNER_ANIMATION_DURATION}ms) cubic-bezier(0.32, 0.72, 0, 1), opacity var(--banner-duration, ${opacityDuration}ms) ease`
|
|
321
402
|
},
|
|
403
|
+
onFocus,
|
|
404
|
+
onBlur,
|
|
322
405
|
children: typeof banner.content === "function" ? banner.content(renderProps) : banner.content
|
|
323
406
|
}
|
|
324
407
|
) });
|
|
325
408
|
}
|
|
409
|
+
function createChildrenSource(initialChildren) {
|
|
410
|
+
let children = initialChildren;
|
|
411
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
412
|
+
return {
|
|
413
|
+
get: () => children,
|
|
414
|
+
set: (nextChildren) => {
|
|
415
|
+
if (Object.is(children, nextChildren)) return;
|
|
416
|
+
children = nextChildren;
|
|
417
|
+
for (const listener of listeners) {
|
|
418
|
+
listener();
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
subscribe: (listener) => {
|
|
422
|
+
listeners.add(listener);
|
|
423
|
+
return () => {
|
|
424
|
+
listeners.delete(listener);
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function DeclaredBannerChildren(props) {
|
|
430
|
+
const { source } = props;
|
|
431
|
+
return React.useSyncExternalStore(source.subscribe, source.get, source.get);
|
|
432
|
+
}
|
|
326
433
|
function Banner(props) {
|
|
327
434
|
const {
|
|
328
435
|
className,
|
|
@@ -336,6 +443,8 @@ function Banner(props) {
|
|
|
336
443
|
dismissible = DEFAULT_BANNER_DISMISSIBLE,
|
|
337
444
|
children,
|
|
338
445
|
asChild,
|
|
446
|
+
onFocus: onFocusProp,
|
|
447
|
+
onBlur: onBlurProp,
|
|
339
448
|
...rootProps
|
|
340
449
|
} = props;
|
|
341
450
|
const store = React.useContext(StoreContext);
|
|
@@ -346,6 +455,9 @@ function Banner(props) {
|
|
|
346
455
|
const bannerIdRef = React.useRef(null);
|
|
347
456
|
const onDismissRef = useAsRef(onDismiss);
|
|
348
457
|
const onOpenChangeRef = useAsRef(onOpenChange);
|
|
458
|
+
const isControlledRef = useAsRef(isControlled);
|
|
459
|
+
const queuedDataRef = useAsRef({ variant: variant ?? void 0, priority, dismissible, duration });
|
|
460
|
+
const childrenSourceRef = useLazyRef(() => createChildrenSource(children));
|
|
349
461
|
if (isControlled) {
|
|
350
462
|
openRef.current = openProp;
|
|
351
463
|
}
|
|
@@ -358,18 +470,23 @@ function Banner(props) {
|
|
|
358
470
|
);
|
|
359
471
|
const getSnapshot = React.useCallback(() => openRef.current, [openRef]);
|
|
360
472
|
const open = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
473
|
+
useIsomorphicLayoutEffect(() => {
|
|
474
|
+
childrenSourceRef.current.set(children);
|
|
475
|
+
});
|
|
476
|
+
useIsomorphicLayoutEffect(() => {
|
|
477
|
+
const id = bannerIdRef.current;
|
|
478
|
+
if (!store || id === null) return;
|
|
479
|
+
store.onBannerUpdate(id, { variant: variant ?? void 0, priority, dismissible, duration });
|
|
480
|
+
}, [store, variant, priority, dismissible, duration]);
|
|
361
481
|
React.useEffect(() => {
|
|
362
|
-
if (!
|
|
482
|
+
if (!store || !open) return;
|
|
363
483
|
let isCleaningUp = false;
|
|
364
484
|
const id = store.onBannerAdd({
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
priority,
|
|
368
|
-
dismissible,
|
|
369
|
-
duration,
|
|
485
|
+
...queuedDataRef.current,
|
|
486
|
+
content: /* @__PURE__ */ jsx(DeclaredBannerChildren, { source: childrenSourceRef.current }),
|
|
370
487
|
onDismiss: () => {
|
|
371
488
|
if (isCleaningUp) return;
|
|
372
|
-
if (!
|
|
489
|
+
if (!isControlledRef.current) {
|
|
373
490
|
openRef.current = false;
|
|
374
491
|
for (const listener of listenersRef.current) {
|
|
375
492
|
listener();
|
|
@@ -382,21 +499,15 @@ function Banner(props) {
|
|
|
382
499
|
bannerIdRef.current = id;
|
|
383
500
|
return () => {
|
|
384
501
|
isCleaningUp = true;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
bannerIdRef.current = null;
|
|
388
|
-
}
|
|
502
|
+
bannerIdRef.current = null;
|
|
503
|
+
store.onBannerRemove(id);
|
|
389
504
|
};
|
|
390
505
|
}, [
|
|
391
|
-
isInsideStore,
|
|
392
506
|
store,
|
|
393
507
|
open,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
priority,
|
|
398
|
-
dismissible,
|
|
399
|
-
duration,
|
|
508
|
+
queuedDataRef,
|
|
509
|
+
childrenSourceRef,
|
|
510
|
+
isControlledRef,
|
|
400
511
|
openRef,
|
|
401
512
|
listenersRef,
|
|
402
513
|
onDismissRef,
|
|
@@ -411,6 +522,7 @@ function Banner(props) {
|
|
|
411
522
|
}
|
|
412
523
|
onOpenChangeRef.current?.(false);
|
|
413
524
|
}, [isControlled, openRef, listenersRef, onOpenChangeRef]);
|
|
525
|
+
const { onFocus, onBlur } = useFocusReturn(open && !isInsideStore, onFocusProp, onBlurProp);
|
|
414
526
|
const contextValue = React.useMemo(
|
|
415
527
|
() => ({
|
|
416
528
|
variant,
|
|
@@ -430,6 +542,8 @@ function Banner(props) {
|
|
|
430
542
|
"data-state": "open",
|
|
431
543
|
className: cn(bannerVariants({ variant, className })),
|
|
432
544
|
...rootProps,
|
|
545
|
+
onFocus,
|
|
546
|
+
onBlur,
|
|
433
547
|
children
|
|
434
548
|
}
|
|
435
549
|
) });
|