@comitor/ui 3.0.1 → 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/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/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 +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 +3 -2
- 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/package.json +1 -1
|
@@ -43,6 +43,13 @@ declare function parseTsv(text: string, fallbackColumnCount: number): string[][]
|
|
|
43
43
|
*
|
|
44
44
|
* Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper
|
|
45
45
|
* (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select "item-aligned".
|
|
46
|
+
*
|
|
47
|
+
* ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot="action-bar"`). Thanh thao tác hàng loạt là bạn
|
|
48
|
+
* đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role="toolbar"` nằm NGOÀI lưới,
|
|
49
|
+
* nên trước đây một cú bấm vào nó bị tính là "bấm ra ngoài": `mousedown` xoá lựa chọn dòng, thanh
|
|
50
|
+
* đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai
|
|
51
|
+
* dòng, bấm "Mark as done" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi
|
|
52
|
+
* vào thanh cũng bị lưới kéo ngược về ô.
|
|
46
53
|
*/
|
|
47
54
|
declare function getIsInPopover(element: unknown): boolean;
|
|
48
55
|
/**
|
package/dist/data-grid/utils.js
CHANGED
|
@@ -238,7 +238,9 @@ ${line}` : line;
|
|
|
238
238
|
}
|
|
239
239
|
function getIsInPopover(element) {
|
|
240
240
|
if (!(element instanceof Element)) return false;
|
|
241
|
-
return element.closest("[data-grid-cell-editor]") !== null || element.closest("[data-grid-popover]") !== null || element.closest("[data-radix-popper-content-wrapper]") !== null || element.closest("[
|
|
241
|
+
return element.closest("[data-grid-cell-editor]") !== null || element.closest("[data-grid-popover]") !== null || element.closest("[data-radix-popper-content-wrapper]") !== null || element.closest("[data-slot='action-bar']") !== null || element.closest("[role='dialog']") !== null || // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role="alertdialog"` — thiếu dòng này thì
|
|
242
|
+
// bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.
|
|
243
|
+
element.closest("[role='alertdialog']") !== null || element.closest("[role='menu']") !== null || element.closest("[role='listbox']") !== null;
|
|
242
244
|
}
|
|
243
245
|
function getColumnVariant(variant, messages) {
|
|
244
246
|
const msg = messages ?? getDataGridMessages();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
const formatEn = (value) => value.toLocaleString("en");
|
|
3
|
+
const formatVi = (value) => value.toLocaleString("vi-VN");
|
|
2
4
|
const searchFilterBarEn = {
|
|
3
5
|
searchPlaceholder: "Search\u2026",
|
|
4
6
|
unitLabel: "items",
|
|
5
7
|
clearSearch: "Clear search",
|
|
6
|
-
filterCount: (count) => `${count} ${count === 1 ? "filter" : "filters"}`,
|
|
8
|
+
filterCount: (count) => `${formatEn(count)} ${count === 1 ? "filter" : "filters"}`,
|
|
7
9
|
clearFilters: "Clear filters",
|
|
8
10
|
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9
|
-
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
10
|
-
total !== void 0 ? ` of ${total}` : "",
|
|
11
|
+
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: formatEn(resultCount) }),
|
|
12
|
+
total !== void 0 ? ` of ${formatEn(total)}` : "",
|
|
11
13
|
" ",
|
|
12
14
|
unitLabel
|
|
13
15
|
] })
|
|
@@ -16,11 +18,11 @@ const searchFilterBarVi = {
|
|
|
16
18
|
searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
|
|
17
19
|
unitLabel: "m\u1EE5c",
|
|
18
20
|
clearSearch: "X\xF3a t\u1EEB kh\xF3a",
|
|
19
|
-
filterCount: (count) => `${count} b\u1ED9 l\u1ECDc`,
|
|
21
|
+
filterCount: (count) => `${formatVi(count)} b\u1ED9 l\u1ECDc`,
|
|
20
22
|
clearFilters: "X\xF3a l\u1ECDc",
|
|
21
23
|
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
22
|
-
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
23
|
-
total !== void 0 ? ` / ${total}` : "",
|
|
24
|
+
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: formatVi(resultCount) }),
|
|
25
|
+
total !== void 0 ? ` / ${formatVi(total)}` : "",
|
|
24
26
|
" ",
|
|
25
27
|
unitLabel
|
|
26
28
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/i18n/namespaces/search-filter-bar.tsx"],"sourcesContent":["/**\n * Chữ của `SearchFilterBar`. Hình dạng + chú thích từng khoá: `SearchFilterBarLabels`.\n *\n * `.tsx` (namespace DUY NHẤT không phải `.ts`): `resultSummary` trả JSX — con số cần thẻ riêng\n * (`tabular-nums`, `text-foreground`), ép về chuỗi là mất phần định dạng đó. File vẫn THUẦN\n * (không directive): JSX chỉ dựng phần tử, không hook, Server Component đọc được.\n */\nimport type { SearchFilterBarLabels } from \"../../components/composite/search-filter-bar-constants.js\";\n\nexport const searchFilterBarEn: SearchFilterBarLabels = {\n searchPlaceholder: \"Search…\",\n unitLabel: \"items\",\n clearSearch: \"Clear search\",\n filterCount: (count) => `${count} ${count === 1 ? \"filter\" : \"filters\"}`,\n clearFilters: \"Clear filters\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` of ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n\nexport const searchFilterBarVi: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${count} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` / ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/i18n/namespaces/search-filter-bar.tsx"],"sourcesContent":["/**\n * Chữ của `SearchFilterBar`. Hình dạng + chú thích từng khoá: `SearchFilterBarLabels`.\n *\n * `.tsx` (namespace DUY NHẤT không phải `.ts`): `resultSummary` trả JSX — con số cần thẻ riêng\n * (`tabular-nums`, `text-foreground`), ép về chuỗi là mất phần định dạng đó. File vẫn THUẦN\n * (không directive): JSX chỉ dựng phần tử, không hook, Server Component đọc được.\n */\nimport type { SearchFilterBarLabels } from \"../../components/composite/search-filter-bar-constants.js\";\n\n/*\n * Số (đếm bộ lọc, đếm kết quả) định dạng NGAY TRONG bảng, theo quy ước của chính ngôn ngữ bảng — cùng\n * khuôn `rating.ts`: bảng Anh `12,345`, bảng Việt `12.345` (trước `3.1.0` in số thô `12345`). Component\n * truyền số THÔ vào hàm, nên bảng tự viết của app giữ toàn quyền định dạng và chữ ký hàm không đổi.\n * Số ít/số nhiều vẫn quyết theo số thô, không theo chuỗi đã định dạng.\n */\nconst formatEn = (value: number) => value.toLocaleString(\"en\");\nconst formatVi = (value: number) => value.toLocaleString(\"vi-VN\");\n\nexport const searchFilterBarEn: SearchFilterBarLabels = {\n searchPlaceholder: \"Search…\",\n unitLabel: \"items\",\n clearSearch: \"Clear search\",\n filterCount: (count) => `${formatEn(count)} ${count === 1 ? \"filter\" : \"filters\"}`,\n clearFilters: \"Clear filters\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{formatEn(resultCount)}</strong>\n {total !== undefined ? ` of ${formatEn(total)}` : \"\"} {unitLabel}\n </>\n )\n};\n\nexport const searchFilterBarVi: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${formatVi(count)} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{formatVi(resultCount)}</strong>\n {total !== undefined ? ` / ${formatVi(total)}` : \"\"} {unitLabel}\n </>\n )\n};\n"],"mappings":"AAyBI,mBACE,KADF;AAVJ,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,IAAI;AAC7D,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,OAAO;AAEzD,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,SAAS,KAAK,CAAC,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAChF,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,mBAAS,WAAW,GAAE;AAAA,IACnF,UAAU,SAAY,OAAO,SAAS,KAAK,CAAC,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KACzD;AAEJ;AAEO,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC1C,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,mBAAS,WAAW,GAAE;AAAA,IACnF,UAAU,SAAY,MAAM,SAAS,KAAK,CAAC,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KACxD;AAEJ;","names":[]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
const formatEn = (value) => value.toLocaleString("en");
|
|
2
|
+
const formatVi = (value) => value.toLocaleString("vi-VN");
|
|
1
3
|
const tablePaginationEn = {
|
|
2
4
|
region: "Pagination",
|
|
3
5
|
unitLabel: "items",
|
|
4
6
|
pageSizePrefix: "Show",
|
|
5
7
|
pageSizeSelect: (unitLabel) => `${unitLabel} per page`,
|
|
6
|
-
range: (start, end, total, unitLabel) => `${start}\u2013${end} of ${total} ${unitLabel}`,
|
|
7
|
-
pageStatus: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
8
|
+
range: (start, end, total, unitLabel) => `${formatEn(start)}\u2013${formatEn(end)} of ${formatEn(total)} ${unitLabel}`,
|
|
9
|
+
pageStatus: (page, pageCount) => `Page ${formatEn(page)} of ${formatEn(pageCount)}`,
|
|
8
10
|
firstPage: "First page",
|
|
9
11
|
previousPage: "Previous page",
|
|
10
12
|
nextPage: "Next page",
|
|
@@ -15,8 +17,8 @@ const tablePaginationVi = {
|
|
|
15
17
|
unitLabel: "m\u1EE5c",
|
|
16
18
|
pageSizePrefix: "Hi\u1EC3n th\u1ECB",
|
|
17
19
|
pageSizeSelect: (unitLabel) => `S\u1ED1 ${unitLabel} m\u1ED7i trang`,
|
|
18
|
-
range: (start, end, total, unitLabel) => `${start}\u2013${end} tr\xEAn t\u1ED5ng ${total} ${unitLabel}`,
|
|
19
|
-
pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
20
|
+
range: (start, end, total, unitLabel) => `${formatVi(start)}\u2013${formatVi(end)} tr\xEAn t\u1ED5ng ${formatVi(total)} ${unitLabel}`,
|
|
21
|
+
pageStatus: (page, pageCount) => `Trang ${formatVi(page)} / ${formatVi(pageCount)}`,
|
|
20
22
|
firstPage: "Trang \u0111\u1EA7u",
|
|
21
23
|
previousPage: "Trang tr\u01B0\u1EDBc",
|
|
22
24
|
nextPage: "Trang sau",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/i18n/namespaces/table-pagination.ts"],"sourcesContent":["import type { TablePaginationLabels } from \"../../components/composite/table-pagination-constants.js\";\n\n/** Chữ của `TablePagination`. Hình dạng + chú thích từng khoá: `TablePaginationLabels`. */\nexport const tablePaginationEn: TablePaginationLabels = {\n region: \"Pagination\",\n unitLabel: \"items\",\n pageSizePrefix: \"Show\",\n pageSizeSelect: (unitLabel) => `${unitLabel} per page`,\n range: (start, end, total, unitLabel) => `${start}–${end} of ${total} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Page ${page} of ${pageCount}`,\n firstPage: \"First page\",\n previousPage: \"Previous page\",\n nextPage: \"Next page\",\n lastPage: \"Last page\"\n};\n\nexport const tablePaginationVi: TablePaginationLabels = {\n region: \"Phân trang\",\n unitLabel: \"mục\",\n pageSizePrefix: \"Hiển thị\",\n pageSizeSelect: (unitLabel) => `Số ${unitLabel} mỗi trang`,\n range: (start, end, total, unitLabel)
|
|
1
|
+
{"version":3,"sources":["../../../src/i18n/namespaces/table-pagination.ts"],"sourcesContent":["import type { TablePaginationLabels } from \"../../components/composite/table-pagination-constants.js\";\n\n/*\n * Số (đếm bản ghi, số trang) định dạng NGAY TRONG bảng, theo quy ước của chính ngôn ngữ bảng — cùng\n * khuôn `rating.ts`: bảng Anh `12,345`, bảng Việt `12.345` (trước `3.1.0` in số thô `12345`). Component\n * truyền số THÔ vào hàm, nên bảng tự viết của app (ngôn ngữ thứ ba) giữ toàn quyền định dạng và chữ ký\n * hàm không đổi.\n */\nconst formatEn = (value: number) => value.toLocaleString(\"en\");\nconst formatVi = (value: number) => value.toLocaleString(\"vi-VN\");\n\n/** Chữ của `TablePagination`. Hình dạng + chú thích từng khoá: `TablePaginationLabels`. */\nexport const tablePaginationEn: TablePaginationLabels = {\n region: \"Pagination\",\n unitLabel: \"items\",\n pageSizePrefix: \"Show\",\n pageSizeSelect: (unitLabel) => `${unitLabel} per page`,\n range: (start, end, total, unitLabel) => `${formatEn(start)}–${formatEn(end)} of ${formatEn(total)} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Page ${formatEn(page)} of ${formatEn(pageCount)}`,\n firstPage: \"First page\",\n previousPage: \"Previous page\",\n nextPage: \"Next page\",\n lastPage: \"Last page\"\n};\n\nexport const tablePaginationVi: TablePaginationLabels = {\n region: \"Phân trang\",\n unitLabel: \"mục\",\n pageSizePrefix: \"Hiển thị\",\n pageSizeSelect: (unitLabel) => `Số ${unitLabel} mỗi trang`,\n range: (start, end, total, unitLabel) =>\n `${formatVi(start)}–${formatVi(end)} trên tổng ${formatVi(total)} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Trang ${formatVi(page)} / ${formatVi(pageCount)}`,\n firstPage: \"Trang đầu\",\n previousPage: \"Trang trước\",\n nextPage: \"Trang sau\",\n lastPage: \"Trang cuối\"\n};\n"],"mappings":"AAQA,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,IAAI;AAC7D,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,OAAO;AAGzD,MAAM,oBAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB,CAAC,cAAc,GAAG,SAAS;AAAA,EAC3C,OAAO,CAAC,OAAO,KAAK,OAAO,cAAc,GAAG,SAAS,KAAK,CAAC,SAAI,SAAS,GAAG,CAAC,OAAO,SAAS,KAAK,CAAC,IAAI,SAAS;AAAA,EAC/G,YAAY,CAAC,MAAM,cAAc,QAAQ,SAAS,IAAI,CAAC,OAAO,SAAS,SAAS,CAAC;AAAA,EACjF,WAAW;AAAA,EACX,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AACZ;AAEO,MAAM,oBAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB,CAAC,cAAc,WAAM,SAAS;AAAA,EAC9C,OAAO,CAAC,OAAO,KAAK,OAAO,cACzB,GAAG,SAAS,KAAK,CAAC,SAAI,SAAS,GAAG,CAAC,sBAAc,SAAS,KAAK,CAAC,IAAI,SAAS;AAAA,EAC/E,YAAY,CAAC,MAAM,cAAc,SAAS,SAAS,IAAI,CAAC,MAAM,SAAS,SAAS,CAAC;AAAA,EACjF,WAAW;AAAA,EACX,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AACZ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export { STATUS_TONES, StatusConfig, StatusTone, StatusToneName, getStatusClasse
|
|
|
28
28
|
export { StatusPill, StatusPillProps, StatusPillVariant } from './components/composite/status-pill.js';
|
|
29
29
|
export { TablePagination, TablePaginationProps } from './components/composite/table-pagination.js';
|
|
30
30
|
export { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_TABLE_PAGINATION_LABELS, TablePaginationLabels } from './components/composite/table-pagination-constants.js';
|
|
31
|
-
export { getVietnameseInitials,
|
|
31
|
+
export { getVietnameseInitials, truncate } from './components/composite/text-utils.js';
|
|
32
32
|
export { ToastOptions, ToastVariant, Toaster, ToasterProps, dismissToast, toast, toastPromise } from './components/composite/toast.js';
|
|
33
33
|
export { Accordion, AccordionContent, AccordionContentProps, AccordionItem, AccordionItemProps, AccordionProps, AccordionTrigger, AccordionTriggerProps } from './components/ui/accordion.js';
|
|
34
34
|
export { ActionBar, ActionBarClose, ActionBarCloseProps, ActionBarGroup, ActionBarItem, ActionBarItemProps, ActionBarProps, ActionBarSelection, ActionBarSelectionProps, ActionBarSeparator, ActionBarSeparatorProps } from './components/ui/action-bar.js';
|
|
@@ -114,6 +114,7 @@ export { AVATAR_TONE_NAMES, AvatarToneColors, AvatarToneName, getAvatarToneIndex
|
|
|
114
114
|
export { cn } from './lib/cn.js';
|
|
115
115
|
export { ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, highContrast, layout, semantic, status, typography } from './tokens.js';
|
|
116
116
|
export { DateRange } from 'react-day-picker';
|
|
117
|
+
export { matchesSearch, normalizeVi, removeVietnameseTones } from './lib/text-search.js';
|
|
117
118
|
import 'react';
|
|
118
119
|
import 'date-fns';
|
|
119
120
|
import 'class-variance-authority/types';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Đọc THỨ TỰ các mục của một component ghép (`Stepper`, `Timeline`) từ chính cây phần tử mà gốc
|
|
5
|
+
* nhận qua `children` — lúc RENDER, không DOM, không effect.
|
|
6
|
+
*
|
|
7
|
+
* Vì sao cần: mục (`StepperItem`, `TimelineItem`) tự đăng ký vị trí trong layout effect, mà effect
|
|
8
|
+
* không chạy ở server và chưa chạy ở lượt render đầu (hydrate, hay mount thuần client). Nên HTML
|
|
9
|
+
* server và khung hình đầu từng không biết vị trí nào. Cây phần tử thì có ngay ở lượt đầu và là hàm
|
|
10
|
+
* THUẦN của props — server và trình duyệt đọc ra cùng một kết quả, nên nó dùng được làm snapshot
|
|
11
|
+
* server của `useSyncExternalStore` mà không lệch hydrate. Sau mount, thứ tự ĐĂNG KÝ vẫn là nguồn sự
|
|
12
|
+
* thật; hàm này chỉ lấp khoảng trước đó.
|
|
13
|
+
*
|
|
14
|
+
* Luật đi cây:
|
|
15
|
+
* - Mảng (`.map`), `Fragment`, thẻ HTML — trong suốt: đi tiếp vào `children`. `false`/`null`/chữ/số
|
|
16
|
+
* bỏ qua, nên `{cond && <Item />}` và `{a ? <Item /> : null}` đọc được.
|
|
17
|
+
* - Phần tử có kiểu `item` — ghi nhận, không đi vào trong (mục không lồng mục).
|
|
18
|
+
* - Phần tử có kiểu `list` (vd `StepperList`) — từ đây trở vào là "trong danh sách". Không truyền
|
|
19
|
+
* `list` thì chính `children` của gốc là danh sách (`Timeline`).
|
|
20
|
+
* - Mọi thứ khác — component của app, `Suspense`, context provider, portal, iterable không phải mảng
|
|
21
|
+
* (đi qua là tiêu thụ nó: generator tới tay React đã cạn), node lazy của RSC:
|
|
22
|
+
* · TRONG danh sách → trả `null` ("không biết"). Nó có thể render mục mà cây không cho thấy, và
|
|
23
|
+
* một danh sách thiếu mục đánh số SAI chứ không chỉ thiếu. Nơi gọi quay về cách cũ.
|
|
24
|
+
* · ngoài danh sách → bỏ qua, không nhìn vào trong.
|
|
25
|
+
*
|
|
26
|
+
* Luật hẹp như vậy là CỐ Ý, để giữ một bất biến: mục nào đọc được ở đây cũng mount CÙNG lượt commit
|
|
27
|
+
* với gốc (thẻ HTML và fragment không hoãn, không giấu con; `Suspense` thì có — lúc hydrate, nội
|
|
28
|
+
* dung của nó luôn hydrate ở một lượt SAU), và đăng ký theo đúng thứ tự này. Nhờ đó nơi gọi đổi từ
|
|
29
|
+
* "thứ tự đọc từ cây" sang "thứ tự đăng ký" ngay ở layout effect đầu tiên của gốc mà màn hình không
|
|
30
|
+
* đổi gì. Nới luật (đi vào `children` của một component lạ) là mở đường cho hai lỗi mới: số nhảy
|
|
31
|
+
* ngay sau mount, và nút Tiếp theo bật → tắt → bật khi mục mount muộn hơn gốc.
|
|
32
|
+
*
|
|
33
|
+
* Không thấy được — nơi gọi rơi về cách cũ, như nhau ở server lẫn trình duyệt nên không lệch hydrate:
|
|
34
|
+
* - mục nằm trong component của app (`steps.map((s) => <MyStep … />)`), hay danh sách nằm trong một
|
|
35
|
+
* component bố cục (`<Card><StepperList>…`);
|
|
36
|
+
* - cây viết THẲNG trong Server Component: RSC mã hoá kiểu của mọi component client thành tham chiếu
|
|
37
|
+
* lazy, nên ở phía client không phần tử nào có kiểu `StepperItem`.
|
|
38
|
+
*
|
|
39
|
+
* So khớp theo ĐỊNH DANH kiểu (`element.type === item`). Không đọc DOM, không mở tham chiếu lazy,
|
|
40
|
+
* không chạm nội bộ nào của React.
|
|
41
|
+
*/
|
|
42
|
+
declare function collectItemElements<P>(children: React.ReactNode, { item, list }: {
|
|
43
|
+
item: React.JSXElementConstructor<P>;
|
|
44
|
+
list?: React.JSXElementConstructor<never>;
|
|
45
|
+
}): React.ReactElement<P>[] | null;
|
|
46
|
+
|
|
47
|
+
export { collectItemElements };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
function collectItemElements(children, { item, list }) {
|
|
3
|
+
const items = [];
|
|
4
|
+
function visit(node, inList) {
|
|
5
|
+
if (node === null || node === void 0 || typeof node !== "object") return true;
|
|
6
|
+
if (Array.isArray(node)) return node.every((child) => visit(child, inList));
|
|
7
|
+
if (!React.isValidElement(node)) return !inList;
|
|
8
|
+
const { type } = node;
|
|
9
|
+
if (type === item) {
|
|
10
|
+
items.push(node);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (type === list) return visit(node.props.children, true);
|
|
14
|
+
if (typeof type === "string" || type === React.Fragment) return visit(node.props.children, inList);
|
|
15
|
+
return !inList;
|
|
16
|
+
}
|
|
17
|
+
return visit(children, list === void 0) ? items : null;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
collectItemElements
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=collect-item-elements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/collect-item-elements.ts"],"sourcesContent":["import * as React from \"react\";\n\n/**\n * Đọc THỨ TỰ các mục của một component ghép (`Stepper`, `Timeline`) từ chính cây phần tử mà gốc\n * nhận qua `children` — lúc RENDER, không DOM, không effect.\n *\n * Vì sao cần: mục (`StepperItem`, `TimelineItem`) tự đăng ký vị trí trong layout effect, mà effect\n * không chạy ở server và chưa chạy ở lượt render đầu (hydrate, hay mount thuần client). Nên HTML\n * server và khung hình đầu từng không biết vị trí nào. Cây phần tử thì có ngay ở lượt đầu và là hàm\n * THUẦN của props — server và trình duyệt đọc ra cùng một kết quả, nên nó dùng được làm snapshot\n * server của `useSyncExternalStore` mà không lệch hydrate. Sau mount, thứ tự ĐĂNG KÝ vẫn là nguồn sự\n * thật; hàm này chỉ lấp khoảng trước đó.\n *\n * Luật đi cây:\n * - Mảng (`.map`), `Fragment`, thẻ HTML — trong suốt: đi tiếp vào `children`. `false`/`null`/chữ/số\n * bỏ qua, nên `{cond && <Item />}` và `{a ? <Item /> : null}` đọc được.\n * - Phần tử có kiểu `item` — ghi nhận, không đi vào trong (mục không lồng mục).\n * - Phần tử có kiểu `list` (vd `StepperList`) — từ đây trở vào là \"trong danh sách\". Không truyền\n * `list` thì chính `children` của gốc là danh sách (`Timeline`).\n * - Mọi thứ khác — component của app, `Suspense`, context provider, portal, iterable không phải mảng\n * (đi qua là tiêu thụ nó: generator tới tay React đã cạn), node lazy của RSC:\n * · TRONG danh sách → trả `null` (\"không biết\"). Nó có thể render mục mà cây không cho thấy, và\n * một danh sách thiếu mục đánh số SAI chứ không chỉ thiếu. Nơi gọi quay về cách cũ.\n * · ngoài danh sách → bỏ qua, không nhìn vào trong.\n *\n * Luật hẹp như vậy là CỐ Ý, để giữ một bất biến: mục nào đọc được ở đây cũng mount CÙNG lượt commit\n * với gốc (thẻ HTML và fragment không hoãn, không giấu con; `Suspense` thì có — lúc hydrate, nội\n * dung của nó luôn hydrate ở một lượt SAU), và đăng ký theo đúng thứ tự này. Nhờ đó nơi gọi đổi từ\n * \"thứ tự đọc từ cây\" sang \"thứ tự đăng ký\" ngay ở layout effect đầu tiên của gốc mà màn hình không\n * đổi gì. Nới luật (đi vào `children` của một component lạ) là mở đường cho hai lỗi mới: số nhảy\n * ngay sau mount, và nút Tiếp theo bật → tắt → bật khi mục mount muộn hơn gốc.\n *\n * Không thấy được — nơi gọi rơi về cách cũ, như nhau ở server lẫn trình duyệt nên không lệch hydrate:\n * - mục nằm trong component của app (`steps.map((s) => <MyStep … />)`), hay danh sách nằm trong một\n * component bố cục (`<Card><StepperList>…`);\n * - cây viết THẲNG trong Server Component: RSC mã hoá kiểu của mọi component client thành tham chiếu\n * lazy, nên ở phía client không phần tử nào có kiểu `StepperItem`.\n *\n * So khớp theo ĐỊNH DANH kiểu (`element.type === item`). Không đọc DOM, không mở tham chiếu lazy,\n * không chạm nội bộ nào của React.\n */\nfunction collectItemElements<P>(\n children: React.ReactNode,\n { item, list }: { item: React.JSXElementConstructor<P>; list?: React.JSXElementConstructor<never> }\n): React.ReactElement<P>[] | null {\n const items: React.ReactElement<P>[] = [];\n\n // `false` = gặp thứ không nhìn xuyên được ở TRONG danh sách → cả kết quả là \"không biết\".\n function visit(node: React.ReactNode, inList: boolean): boolean {\n if (node === null || node === undefined || typeof node !== \"object\") return true;\n if (Array.isArray(node)) return node.every((child) => visit(child, inList));\n if (!React.isValidElement<{ children?: React.ReactNode }>(node)) return !inList;\n\n const { type } = node;\n if (type === item) {\n items.push(node as React.ReactElement<P>);\n return true;\n }\n if (type === list) return visit(node.props.children, true);\n if (typeof type === \"string\" || type === React.Fragment) return visit(node.props.children, inList);\n return !inList;\n }\n\n return visit(children, list === undefined) ? items : null;\n}\n\nexport { collectItemElements };\n"],"mappings":"AAAA,YAAY,WAAW;AAyCvB,SAAS,oBACP,UACA,EAAE,MAAM,KAAK,GACmB;AAChC,QAAM,QAAiC,CAAC;AAGxC,WAAS,MAAM,MAAuB,QAA0B;AAC9D,QAAI,SAAS,QAAQ,SAAS,UAAa,OAAO,SAAS,SAAU,QAAO;AAC5E,QAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,MAAM,CAAC,UAAU,MAAM,OAAO,MAAM,CAAC;AAC1E,QAAI,CAAC,MAAM,eAA+C,IAAI,EAAG,QAAO,CAAC;AAEzE,UAAM,EAAE,KAAK,IAAI;AACjB,QAAI,SAAS,MAAM;AACjB,YAAM,KAAK,IAA6B;AACxC,aAAO;AAAA,IACT;AACA,QAAI,SAAS,KAAM,QAAO,MAAM,KAAK,MAAM,UAAU,IAAI;AACzD,QAAI,OAAO,SAAS,YAAY,SAAS,MAAM,SAAU,QAAO,MAAM,KAAK,MAAM,UAAU,MAAM;AACjG,WAAO,CAAC;AAAA,EACV;AAEA,SAAO,MAAM,UAAU,SAAS,MAAS,IAAI,QAAQ;AACvD;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tìm kiếm KHÔNG DẤU dùng chung cho mọi tầng — thuần, không import gì, không phụ thuộc React.
|
|
3
|
+
*
|
|
4
|
+
* Chuyển từ `components/composite/text-utils.ts` sang đây ở `3.1.0` (tên và chữ ký giữ nguyên, Tier 2
|
|
5
|
+
* vẫn xuất lại qua đúng file đó): từ bản này primitive Tier 1 cũng cần CHÍNH luật này — `Mention` lọc
|
|
6
|
+
* tên người theo `matchesSearch` — mà Tier 1 không được nhập ngược lên Tier 2. Một bản chép riêng là
|
|
7
|
+
* hai luật sẽ lệch nhau (CLAUDE.md, "Kiến trúc 3 tầng").
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Bỏ dấu tiếng Việt để so khớp tìm kiếm ("Nguyễn" ⇄ "nguyen").
|
|
11
|
+
*
|
|
12
|
+
* 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
|
|
13
|
+
* 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
|
|
14
|
+
* ra "Đức".
|
|
15
|
+
*/
|
|
16
|
+
declare function removeVietnameseTones(input: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Chuẩn hoá chuỗi để so khớp: bỏ dấu + hạ chữ thường + `trim`.
|
|
19
|
+
*
|
|
20
|
+
* Đâ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
|
|
21
|
+
* 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.
|
|
22
|
+
* `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
|
|
23
|
+
* (vd sinh slug).
|
|
24
|
+
*/
|
|
25
|
+
declare function normalizeVi(value: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* So khớp không dấu, không phân biệt hoa thường: `needle` có nằm trong `haystack` không.
|
|
28
|
+
* Cả hai vế đi qua `normalizeVi` — cùng một luật với ô tìm kiếm của shell.
|
|
29
|
+
*/
|
|
30
|
+
declare function matchesSearch(haystack: string, needle: string): boolean;
|
|
31
|
+
|
|
32
|
+
export { matchesSearch, normalizeVi, removeVietnameseTones };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function removeVietnameseTones(input) {
|
|
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
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
matchesSearch,
|
|
14
|
+
normalizeVi,
|
|
15
|
+
removeVietnameseTones
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=text-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/text-search.ts"],"sourcesContent":["/**\n * Tìm kiếm KHÔNG DẤU dùng chung cho mọi tầng — thuần, không import gì, không phụ thuộc React.\n *\n * Chuyển từ `components/composite/text-utils.ts` sang đây ở `3.1.0` (tên và chữ ký giữ nguyên, Tier 2\n * vẫn xuất lại qua đúng file đó): từ bản này primitive Tier 1 cũng cần CHÍNH luật này — `Mention` lọc\n * tên người theo `matchesSearch` — mà Tier 1 không được nhập ngược lên Tier 2. Một bản chép riêng là\n * hai luật sẽ lệch nhau (CLAUDE.md, \"Kiến trúc 3 tầng\").\n */\n\n/**\n * Bỏ dấu tiếng Việt để so khớp tìm kiếm (\"Nguyễn\" ⇄ \"nguyen\").\n *\n * 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\n * 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\n * ra \"Đức\".\n */\nexport function removeVietnameseTones(input: string): string {\n return input\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/đ/g, \"d\")\n .replace(/Đ/g, \"D\");\n}\n\n/**\n * Chuẩn hoá chuỗi để so khớp: bỏ dấu + hạ chữ thường + `trim`.\n *\n * Đâ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\n * 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.\n * `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\n * (vd sinh slug).\n */\nexport function normalizeVi(value: string): string {\n return removeVietnameseTones(value).toLowerCase().trim();\n}\n\n/**\n * So khớp không dấu, không phân biệt hoa thường: `needle` có nằm trong `haystack` không.\n * Cả hai vế đi qua `normalizeVi` — cùng một luật với ô tìm kiếm của shell.\n */\nexport function matchesSearch(haystack: string, needle: string): boolean {\n const query = normalizeVi(needle);\n if (!query) return true;\n return normalizeVi(haystack).includes(query);\n}\n"],"mappings":"AAgBO,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MACJ,UAAU,KAAK,EACf,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,MAAM,GAAG,EACjB,QAAQ,MAAM,GAAG;AACtB;AAUO,SAAS,YAAY,OAAuB;AACjD,SAAO,sBAAsB,KAAK,EAAE,YAAY,EAAE,KAAK;AACzD;AAMO,SAAS,cAAc,UAAkB,QAAyB;AACvE,QAAM,QAAQ,YAAY,MAAM;AAChC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,YAAY,QAAQ,EAAE,SAAS,KAAK;AAC7C;","names":[]}
|
|
@@ -9,6 +9,7 @@ import './types.js';
|
|
|
9
9
|
import 'lucide-react';
|
|
10
10
|
import './utils.js';
|
|
11
11
|
import '../components/composite/text-utils.js';
|
|
12
|
+
import '../lib/text-search.js';
|
|
12
13
|
|
|
13
14
|
type AppShellLayout = "sidebar-first" | "header-first";
|
|
14
15
|
interface AppShellProps extends ShellProviderProps {
|
|
@@ -37,7 +37,7 @@ declare function useDensity(): {
|
|
|
37
37
|
};
|
|
38
38
|
declare const FontSizeProvider: ({ children, defaultValue, storageKey, nonce }: {
|
|
39
39
|
children: ReactNode;
|
|
40
|
-
defaultValue?: "
|
|
40
|
+
defaultValue?: "sm" | "md" | "lg" | undefined;
|
|
41
41
|
storageKey?: string;
|
|
42
42
|
/**
|
|
43
43
|
* Nonce của CSP, cho script chống nháy mà provider này TỰ render.
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export { AppDescriptor, DEFAULT_SHELL_LABELS, NavBadgeVariant, NavGroup, NavItem
|
|
|
23
23
|
export { UserAvatar, UserAvatarProps, UserMenu, UserMenuProps } from './user-menu.js';
|
|
24
24
|
export { NavSearchParams, appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups, toSearchParams } from './utils.js';
|
|
25
25
|
export { WorkspaceSwitcher, WorkspaceSwitcherProps } from './workspace-switcher.js';
|
|
26
|
-
export { getVietnameseInitials
|
|
26
|
+
export { getVietnameseInitials } from '../components/composite/text-utils.js';
|
|
27
|
+
export { normalizeVi } from '../lib/text-search.js';
|
|
27
28
|
import 'react';
|
|
28
29
|
import 'sonner';
|
|
29
30
|
import 'next-themes';
|
|
@@ -4,6 +4,7 @@ import { Workspace, AppDescriptor, ShellNav, ShellLinkComponent, ShellUser, Shel
|
|
|
4
4
|
import { NavSearchParams } from './utils.js';
|
|
5
5
|
import 'lucide-react';
|
|
6
6
|
import '../components/composite/text-utils.js';
|
|
7
|
+
import '../lib/text-search.js';
|
|
7
8
|
|
|
8
9
|
interface ShellContextValue {
|
|
9
10
|
workspaces: Workspace[];
|
package/dist/shell/utils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CSSProperties } from 'react';
|
|
2
2
|
import { AppDescriptor, NavGroup, NavItem, ShellNav } from './types.js';
|
|
3
|
-
export { getVietnameseInitials
|
|
3
|
+
export { getVietnameseInitials } from '../components/composite/text-utils.js';
|
|
4
|
+
export { normalizeVi } from '../lib/text-search.js';
|
|
4
5
|
import 'lucide-react';
|
|
5
6
|
|
|
6
7
|
/**
|
package/package.json
CHANGED