@comitor/ui 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +5 -1
  2. package/dist/components/composite/confirm-dialog.js +56 -35
  3. package/dist/components/composite/confirm-dialog.js.map +1 -1
  4. package/dist/components/composite/search-filter-bar-constants.d.ts +3 -0
  5. package/dist/components/composite/search-filter-bar-constants.js.map +1 -1
  6. package/dist/components/composite/table-pagination-constants.d.ts +3 -1
  7. package/dist/components/composite/table-pagination-constants.js.map +1 -1
  8. package/dist/components/composite/table-pagination.js +10 -2
  9. package/dist/components/composite/table-pagination.js.map +1 -1
  10. package/dist/components/composite/text-utils.d.ts +4 -23
  11. package/dist/components/composite/text-utils.js +1 -11
  12. package/dist/components/composite/text-utils.js.map +1 -1
  13. package/dist/components/composite/toast.d.ts +2 -1
  14. package/dist/components/composite/toast.js +3 -9
  15. package/dist/components/composite/toast.js.map +1 -1
  16. package/dist/components/ui/action-bar.js +45 -1
  17. package/dist/components/ui/action-bar.js.map +1 -1
  18. package/dist/components/ui/banner.d.ts +8 -0
  19. package/dist/components/ui/banner.js +149 -35
  20. package/dist/components/ui/banner.js.map +1 -1
  21. package/dist/components/ui/circular-progress.js +10 -2
  22. package/dist/components/ui/circular-progress.js.map +1 -1
  23. package/dist/components/ui/kanban.js +9 -3
  24. package/dist/components/ui/kanban.js.map +1 -1
  25. package/dist/components/ui/key-value.js +70 -76
  26. package/dist/components/ui/key-value.js.map +1 -1
  27. package/dist/components/ui/mask-input-constants.d.ts +34 -2
  28. package/dist/components/ui/mask-input-constants.js +87 -76
  29. package/dist/components/ui/mask-input-constants.js.map +1 -1
  30. package/dist/components/ui/mask-input.d.ts +6 -5
  31. package/dist/components/ui/mask-input.js +23 -21
  32. package/dist/components/ui/mask-input.js.map +1 -1
  33. package/dist/components/ui/mention.d.ts +14 -2
  34. package/dist/components/ui/mention.js +33 -6
  35. package/dist/components/ui/mention.js.map +1 -1
  36. package/dist/components/ui/phone-input.d.ts +18 -8
  37. package/dist/components/ui/phone-input.js +125 -35
  38. package/dist/components/ui/phone-input.js.map +1 -1
  39. package/dist/components/ui/rating.js +4 -2
  40. package/dist/components/ui/rating.js.map +1 -1
  41. package/dist/components/ui/scroll-spy.js +46 -31
  42. package/dist/components/ui/scroll-spy.js.map +1 -1
  43. package/dist/components/ui/slider.d.ts +1 -1
  44. package/dist/components/ui/slider.js +15 -1
  45. package/dist/components/ui/slider.js.map +1 -1
  46. package/dist/components/ui/sonner.js +2 -0
  47. package/dist/components/ui/sonner.js.map +1 -1
  48. package/dist/components/ui/stepper.js +26 -3
  49. package/dist/components/ui/stepper.js.map +1 -1
  50. package/dist/components/ui/time-picker.d.ts +24 -4
  51. package/dist/components/ui/time-picker.js +425 -162
  52. package/dist/components/ui/time-picker.js.map +1 -1
  53. package/dist/components/ui/timeline.js +45 -9
  54. package/dist/components/ui/timeline.js.map +1 -1
  55. package/dist/components/ui/toggle-group.js +12 -1
  56. package/dist/components/ui/toggle-group.js.map +1 -1
  57. package/dist/components/ui/tour.js +3 -2
  58. package/dist/components/ui/tour.js.map +1 -1
  59. package/dist/data-grid/data-grid-cell-variants.js +4 -4
  60. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  61. package/dist/data-grid/data-grid-cell-wrapper.d.ts +1 -1
  62. package/dist/data-grid/data-grid-cell-wrapper.js +6 -0
  63. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  64. package/dist/data-grid/data-grid-row.js +1 -1
  65. package/dist/data-grid/data-grid-row.js.map +1 -1
  66. package/dist/data-grid/data-grid-select-column.js +6 -2
  67. package/dist/data-grid/data-grid-select-column.js.map +1 -1
  68. package/dist/data-grid/data-grid-skeleton.d.ts +5 -0
  69. package/dist/data-grid/data-grid-skeleton.js +1 -4
  70. package/dist/data-grid/data-grid-skeleton.js.map +1 -1
  71. package/dist/data-grid/data-grid.d.ts +1 -1
  72. package/dist/data-grid/data-grid.js +3 -2
  73. package/dist/data-grid/data-grid.js.map +1 -1
  74. package/dist/data-grid/filters.js +3 -2
  75. package/dist/data-grid/filters.js.map +1 -1
  76. package/dist/data-grid/types.d.ts +7 -0
  77. package/dist/data-grid/use-data-grid.d.ts +1 -0
  78. package/dist/data-grid/use-data-grid.js +70 -24
  79. package/dist/data-grid/use-data-grid.js.map +1 -1
  80. package/dist/data-grid/utils.d.ts +7 -0
  81. package/dist/data-grid/utils.js +3 -1
  82. package/dist/data-grid/utils.js.map +1 -1
  83. package/dist/i18n/locale-provider.d.ts +18 -0
  84. package/dist/i18n/locale-provider.js.map +1 -1
  85. package/dist/i18n/namespaces/rating.js +2 -1
  86. package/dist/i18n/namespaces/rating.js.map +1 -1
  87. package/dist/i18n/namespaces/search-filter-bar.js +8 -6
  88. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
  89. package/dist/i18n/namespaces/shell.d.ts +4 -0
  90. package/dist/i18n/namespaces/shell.js +1 -1
  91. package/dist/i18n/namespaces/shell.js.map +1 -1
  92. package/dist/i18n/namespaces/table-pagination.js +6 -4
  93. package/dist/i18n/namespaces/table-pagination.js.map +1 -1
  94. package/dist/i18n/namespaces/toast.d.ts +3 -2
  95. package/dist/i18n/namespaces/toast.js.map +1 -1
  96. package/dist/index.d.ts +2 -1
  97. package/dist/lib/collect-item-elements.d.ts +47 -0
  98. package/dist/lib/collect-item-elements.js +22 -0
  99. package/dist/lib/collect-item-elements.js.map +1 -0
  100. package/dist/lib/text-search.d.ts +32 -0
  101. package/dist/lib/text-search.js +17 -0
  102. package/dist/lib/text-search.js.map +1 -0
  103. package/dist/lib/toast-language.d.ts +8 -0
  104. package/dist/lib/toast-language.js +19 -0
  105. package/dist/lib/toast-language.js.map +1 -0
  106. package/dist/shell/app-shell.d.ts +1 -0
  107. package/dist/shell/index.d.ts +2 -1
  108. package/dist/shell/shell-context.d.ts +1 -0
  109. package/dist/shell/utils.d.ts +2 -1
  110. 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
  /**
@@ -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("[role='dialog']") !== null || element.closest("[role='menu']") !== null || element.closest("[role='listbox']") !== null;
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":"AACA,SAAuC,QAAQ,SAAS,aAAa;AACrE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAgC,2BAA2B;AAGpD,SAAS,WACd,MACA,OACiB;AACjB,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT;AACA,SAAO,OAAO,KAAK;AACrB;AAEO,SAAS,kBAAkB,MAAqC;AACrE,SAAO,CAAC,CAAC,QAAQ,OAAO,SAAS,YAAY,QAAQ,QAAQ,UAAU,QAAQ,UAAU,QAAQ,UAAU;AAC7G;AAEO,SAAS,kBAAkB,OAAe,SAAiE;AAChH,SAAO,QAAQ;AAAA,IACb,CAAC,MACC,EAAE,UAAU,SACZ,EAAE,MAAM,YAAY,MAAM,MAAM,YAAY,KAC5C,EAAE,MAAM,YAAY,MAAM,MAAM,YAAY;AAAA,EAChD,GAAG;AACL;AAEO,SAAS,WAAW,UAAkB,UAAkB;AAC7D,SAAO,GAAG,QAAQ,IAAI,QAAQ;AAChC;AAEO,SAAS,aAAa,SAAyC;AACpE,QAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAM,cAAc,MAAM,CAAC;AAC3B,QAAM,WAAW,MAAM,CAAC;AACxB,MAAI,eAAe,UAAU;AAC3B,UAAM,WAAW,SAAS,aAAa,EAAE;AACzC,QAAI,CAAC,OAAO,MAAM,QAAQ,GAAG;AAC3B,aAAO,EAAE,UAAU,SAAS;AAAA,IAC9B;AAAA,EACF;AACA,SAAO,EAAE,UAAU,GAAG,UAAU,GAAG;AACrC;AAEO,SAAS,kBAAkB,WAAmC;AACnE,QAAM,eAA+C;AAAA,IACnD,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,cAAc;AAAA,EAChB;AAEA,SAAO,aAAa,SAAS;AAC/B;AAEO,SAAS,aAAa,WAAmC;AAC9D,QAAM,eAA+C;AAAA,IACnD,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,cAAc;AAAA,EAChB;AAEA,SAAO,aAAa,SAAS;AAC/B;AAEO,SAAS,0BAAiC,QAO/C;AACA,QAAM,EAAE,QAAQ,YAAY,aAAa,IAAI;AAE7C,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,2BAA2B,aAAa,WAAW,OAAO,iBAAiB,OAAO;AACxF,QAAM,0BAA0B,aAAa,WAAW,OAAO,gBAAgB,OAAO;AAEtF,QAAM,eAAe,YAAY,YAAY;AAC7C,QAAM,sBAAsB,iBAAiB,WAAW,YAAY,iBAAiB,OAAO;AAE5F,QAAM,gBAAgB,CAAC,wBAAwB,gBAAgB,CAAC;AAEhE,QAAM,kBAAkB;AAExB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,sBAA6B,QAIrB;AACtB,QAAM,EAAE,QAAQ,MAAM,OAAO,aAAa,MAAM,IAAI;AAEpD,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,yBAAyB,aAAa,UAAU,OAAO,gBAAgB,MAAM;AACnF,QAAM,2BAA2B,aAAa,WAAW,OAAO,iBAAiB,OAAO;AAExF,QAAM,QAAQ,QAAQ;AAEtB,QAAM,eAAe,aAAa,SAAS,GAAG,OAAO,SAAS,MAAM,CAAC,OAAO;AAC5E,QAAM,gBAAgB,aAAa,UAAU,GAAG,OAAO,SAAS,OAAO,CAAC,OAAO;AAW/E,SAAO;AAAA,IACL,WAAW,aACP,yBACE,QACE,uCACA,wCACF,2BACE,QACE,wCACA,uCACF,SACJ;AAAA,IACJ,MAAM,QAAQ,gBAAgB;AAAA,IAC9B,OAAO,QAAQ,eAAe;AAAA,IAC9B,SAAS,WAAW,OAAO;AAAA,IAC3B,UAAU,WAAW,WAAW;AAAA,IAChC,YAAY;AAAA,IACZ,OAAO,OAAO,QAAQ;AAAA,IACtB,QAAQ,WAAW,IAAI;AAAA,EACzB;AACF;AAEO,SAAS,mBAAmB,WAAkE;AACnG,MAAI,cAAc,UAAU,cAAc,WAAW,cAAc,UAAU,cAAc,OAAO;AAChG,WAAO;AAAA,EACT;AACA,MAAI,cAAc,WAAY,QAAO;AACrC,MAAI,cAAc,YAAa,QAAO;AACtC,SAAO;AACT;AAEO,SAAS,mBAA0B,QAOjC;AACP,QAAM,EAAE,WAAW,YAAY,UAAU,WAAW,gBAAgB,MAAM,IAAI;AAE9E,QAAM,gBAAgB,UAAU,sBAAsB;AACtD,QAAM,WAAW,WAAW,sBAAsB;AAElD,QAAM,oBAAoB,UAAU,aAAa;AACjD,QAAM,gBAAgB,SAAS;AAE/B,QAAM,eAAe,SAAS;AAC9B,QAAM,oBAAoB,cAAc,0BAA0B,KAAK,CAAC;AACxE,QAAM,qBAAqB,cAAc,2BAA2B,KAAK,CAAC;AAE1E,QAAM,kBAAkB,kBAAkB,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,GAAG,CAAC;AACjF,QAAM,mBAAmB,mBAAmB,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,GAAG,CAAC;AAEnF,QAAM,eAAe,gBACjB,cAAc,OAAO,mBAAmB,iBACxC,cAAc,OAAO,kBAAkB;AAC3C,QAAM,gBAAgB,gBAClB,cAAc,QAAQ,kBAAkB,iBACxC,cAAc,QAAQ,mBAAmB;AAE7C,QAAM,iBAAiB,SAAS,QAAQ,gBAAgB,SAAS,SAAS;AAE1E,MAAI,eAAgB;AAEpB,QAAM,gBAAgB,SAAS,OAAO;AACtC,QAAM,iBAAiB,SAAS,QAAQ;AAExC,MAAI,cAAc;AAElB,MAAI,CAAC,WAAW;AACd,QAAI,gBAAgB;AAClB,oBAAc,SAAS,QAAQ;AAAA,IACjC,WAAW,eAAe;AACxB,oBAAc,EAAE,eAAe,SAAS;AAAA,IAC1C;AAAA,EACF,OAAO;AACL,UAAM,oBAAoB,gBACtB,cAAc,WAAW,cAAc,SACvC,cAAc,WAAW,cAAc;AAE3C,QAAI,mBAAmB;AACrB,oBAAc,SAAS,QAAQ;AAAA,IACjC,OAAO;AACL,oBAAc,EAAE,eAAe,SAAS;AAAA,IAC1C;AAAA,EACF;AAEA,YAAU,cAAc;AAC1B;AAEA,SAAS,UAAU,GAAmB;AACpC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAI,EAAE,CAAC,MAAM,IAAM;AACtD,SAAO;AACT;AAEO,SAAS,SAAS,MAAc,qBAAyC;AAC9E,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,IAAK,GAAG;AAChD,UAAMA,QAAmB,CAAC;AAC1B,QAAI,aAAuB,CAAC;AAC5B,QAAI,eAAe;AACnB,QAAI,WAAW;AACf,QAAI,IAAI;AAER,WAAO,IAAI,KAAK,QAAQ;AACtB,YAAM,OAAO,KAAK,CAAC;AACnB,YAAM,WAAW,KAAK,IAAI,CAAC;AAE3B,UAAI,UAAU;AACZ,YAAI,SAAS,OAAO,aAAa,KAAK;AACpC,0BAAgB;AAChB,eAAK;AAAA,QACP,WAAW,SAAS,KAAK;AACvB,qBAAW;AACX;AAAA,QACF,OAAO;AACL,0BAAgB;AAChB;AAAA,QACF;AAAA,MACF,OAAO;AACL,YAAI,SAAS,OAAO,iBAAiB,IAAI;AACvC,qBAAW;AACX;AAAA,QACF,WAAW,SAAS,KAAM;AACxB,qBAAW,KAAK,YAAY;AAC5B,yBAAe;AACf;AAAA,QACF,WAAW,SAAS,MAAM;AACxB,qBAAW,KAAK,YAAY;AAC5B,cAAI,WAAW,SAAS,KAAK,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG;AACjE,YAAAA,MAAK,KAAK,UAAU;AAAA,UACtB;AACA,uBAAa,CAAC;AACd,yBAAe;AACf;AAAA,QACF,WAAW,SAAS,QAAQ,aAAa,MAAM;AAC7C,qBAAW,KAAK,YAAY;AAC5B,cAAI,WAAW,SAAS,KAAK,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG;AACjE,YAAAA,MAAK,KAAK,UAAU;AAAA,UACtB;AACA,uBAAa,CAAC;AACd,yBAAe;AACf,eAAK;AAAA,QACP,OAAO;AACL,0BAAgB;AAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,eAAW,KAAK,YAAY;AAC5B,QAAI,WAAW,SAAS,KAAK,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG;AACjE,MAAAA,MAAK,KAAK,UAAU;AAAA,IACtB;AAEA,WAAOA;AAAA,EACT;AAEA,QAAM,QAAQ,KAAK,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC;AAC9D,MAAI,cAAc;AAClB,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,UAAU,IAAI;AACxB,QAAI,IAAI,YAAa,eAAc;AAAA,EACrC;AACA,QAAM,cAAc,cAAc,IAAI,cAAc,IAAI;AACxD,MAAI,eAAe,EAAG,QAAO,CAAC;AAE9B,QAAM,mBAAmB,cAAc;AACvC,QAAM,OAAmB,CAAC;AAC1B,MAAI,MAAM;AACV,MAAI,cAAc;AAElB,aAAW,QAAQ,OAAO;AACxB,UAAM,KAAK,UAAU,IAAI;AAEzB,QAAI,OAAO,kBAAkB;AAC3B,UAAI,OAAO,gBAAgB,iBAAkB,MAAK,KAAK,IAAI,MAAM,GAAI,CAAC;AACtE,YAAM;AACN,oBAAc;AACd,WAAK,KAAK,KAAK,MAAM,GAAI,CAAC;AAAA,IAC5B,OAAO;AACL,YAAM,MAAM,GAAG,GAAG;AAAA,EAAK,IAAI,KAAK;AAChC,qBAAe;AACf,UAAI,gBAAgB,kBAAkB;AACpC,aAAK,KAAK,IAAI,MAAM,GAAI,CAAC;AACzB,cAAM;AACN,sBAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,gBAAgB,iBAAkB,MAAK,KAAK,IAAI,MAAM,GAAI,CAAC;AAEtE,SAAO,KAAK,SAAS,IAAI,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAI,CAAC;AAC5F;AAgBO,SAAS,eAAe,SAA2B;AACxD,MAAI,EAAE,mBAAmB,SAAU,QAAO;AAE1C,SACE,QAAQ,QAAQ,yBAAyB,MAAM,QAC/C,QAAQ,QAAQ,qBAAqB,MAAM,QAC3C,QAAQ,QAAQ,qCAAqC,MAAM,QAC3D,QAAQ,QAAQ,0BAA0B,MAAM,QAChD,QAAQ,QAAQ,iBAAiB,MAAM;AAAA;AAAA,EAGvC,QAAQ,QAAQ,sBAAsB,MAAM,QAC5C,QAAQ,QAAQ,eAAe,MAAM,QACrC,QAAQ,QAAQ,kBAAkB,MAAM;AAE5C;AASO,SAAS,iBACd,SACA,UAIO;AACP,QAAM,MAAM,YAAY,oBAAoB;AAC5C,UAAQ,SAAS;AAAA,IACf,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,WAAW,MAAM,aAAa;AAAA,IACnE,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,UAAU,MAAM,aAAa;AAAA,IAClE,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,QAAQ,MAAM,SAAS;AAAA,IAC5D,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,KAAK,MAAM,SAAS;AAAA,IACzD,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,UAAU,MAAM,gBAAgB;AAAA,IACrE,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,QAAQ,MAAM,SAAS;AAAA,IAC5D,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,aAAa,MAAM,eAAe;AAAA,IACvE,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,MAAM,MAAM,aAAa;AAAA,IAC9D,KAAK;AACH,aAAO,EAAE,OAAO,IAAI,OAAO,QAAQ,MAAM,MAAM,SAAS;AAAA,IAC1D;AACE,aAAO;AAAA,EACX;AACF;AAEO,SAAS,kBAAkB,SAAmD;AACnF,MAAI,YAAY,kBAAkB,YAAY,OAAQ,QAAO,CAAC;AAC9D,MAAI,YAAY,YAAY,YAAY,UAAU,YAAY,SAAU,QAAO;AAC/E,MAAI,YAAY,WAAY,QAAO;AACnC,SAAO;AACT;AAEO,SAAS,WAAW,WAA2B;AACpD,MAAI,CAAC,aAAa,UAAU,KAAK,MAAM,GAAI,QAAO;AAElD,QAAM,UAAU,UAAU,KAAK;AAG/B,MAAI,qCAAqC,KAAK,OAAO,GAAG;AACtD,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,WAAW,SAAS,KAAK,QAAQ,WAAW,UAAU,GAAG;AACnE,WAAO;AAAA,EACT;AAEA,SAAO,UAAU,OAAO;AAC1B;AAEO,SAAS,eAAe,SAA+B;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,mBAAmB,KAAM,QAAO;AACpC,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,QAAM,CAAC,MAAM,OAAO,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI,MAAM;AACxD,MAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAK,QAAO;AACpC,QAAM,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG,GAAG;AAE1C,MAAI,KAAK,YAAY,MAAM,QAAQ,KAAK,SAAS,MAAM,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK;AAC1F,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,MAAoB;AACrD,QAAM,OAAO,KAAK,YAAY;AAC9B,QAAM,QAAQ,OAAO,KAAK,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACzD,QAAM,MAAM,OAAO,KAAK,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,SAAO,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG;AAChC;AAOA,MAAM,sBAAsB;AAYrB,SAAS,WAAW,MAAiD,QAAgC;AAC1G,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,gBAAgB,OAAO,OAAO,IAAI,KAAK,IAAI;AACzD,MAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC1C,SAAO,OAAO,OAAO,qBAAqB,EAAE,OAAO,CAAC;AACtD;AAGO,SAAS,qBAAqB,SAAkB,QAAgC;AACrF,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,eAAe,OAAO;AACnC,MAAI,CAAC,KAAM,QAAO,OAAO,YAAY,WAAW,UAAU;AAC1D,SAAO,WAAW,MAAM,MAAM;AAChC;AAUO,SAAS,eAAe,MAAc,QAAqC;AAChF,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,YAAY,MAAM,SAAS,qBAAqB,oBAAI,KAAK,GAAG,EAAE,OAAO,CAAC;AAC5E,MAAI,QAAQ,SAAS,KAAK,UAAU,YAAY,KAAK,IAAM,QAAO;AAClE,QAAM,OAAO,IAAI,KAAK,OAAO;AAC7B,SAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,OAAO;AAC/C;AAMO,SAAS,iBAAyB;AACvC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,QAAQ,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACnF;AAOA,eAAsB,mBAAmB,MAA6B;AACpE,MAAI,OAAO,cAAc,eAAe,UAAU,WAAW,WAAW;AACtE,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,IAAI;AACxC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,oBAAoB,IAAI,GAAG;AAC9B,UAAM,IAAI,MAAM,uCAA0B;AAAA,EAC5C;AACF;AAEA,SAAS,oBAAoB,MAAuB;AAClD,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,QAAQ;AACjB,WAAS,aAAa,YAAY,EAAE;AACpC,WAAS,MAAM,WAAW;AAC1B,WAAS,MAAM,MAAM;AACrB,WAAS,MAAM,mBAAmB;AAClC,WAAS,KAAK,YAAY,QAAQ;AAElC,QAAM,oBAAoB,SAAS;AACnC,WAAS,OAAO;AAEhB,MAAI,SAAS;AACb,MAAI;AACF,aAAS,SAAS,YAAY,MAAM;AAAA,EACtC,QAAQ;AACN,aAAS;AAAA,EACX;AAEA,WAAS,KAAK,YAAY,QAAQ;AAClC,MAAI,6BAA6B,aAAa;AAC5C,sBAAkB,MAAM;AAAA,EAC1B;AAEA,SAAO;AACT;AAGA,eAAsB,oBAA4C;AAChE,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,WAAW,SAAU,QAAO;AAC/E,MAAI;AACF,WAAO,MAAM,UAAU,UAAU,SAAS;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eAAe,OAAuB;AACpD,MAAI,SAAS,KAAK,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AAClD,QAAM,IAAI;AACV,QAAM,QAAQ,CAAC,KAAK,MAAM,MAAM,IAAI;AACpC,QAAM,IAAI,KAAK,IAAI,MAAM,SAAS,GAAG,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;AAC9E,SAAO,GAAG,OAAO,YAAY,QAAQ,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AACtE;AAEO,SAAS,YAAY,MAAkE;AAC5F,MAAI,KAAK,WAAW,QAAQ,EAAG,QAAO;AACtC,MAAI,KAAK,WAAW,QAAQ,EAAG,QAAO;AACtC,MAAI,KAAK,WAAW,QAAQ,EAAG,QAAO;AACtC,MAAI,KAAK,SAAS,KAAK,EAAG,QAAO;AACjC,MAAI,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACzD,MAAI,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACvF,MAAI,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACrF,MAAI,KAAK,SAAS,cAAc,KAAK,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACjG,SAAO;AACT;","names":["rows"]}
@@ -86,11 +86,29 @@ interface ComitorLocaleProviderProps {
86
86
  * Ghi đè một phần bảng chữ (theo namespace), hoặc cấp nguyên một bảng cho ngôn ngữ không dựng sẵn.
87
87
  * ⚠ Giữ tham chiếu ỔN ĐỊNH (hằng module hoặc `useMemo`) — object mới mỗi lượt render làm mọi
88
88
  * component đọc chữ render lại.
89
+ * ⚠ Có khoá là HÀM (vd `tablePagination.range`) thì object này không đi qua ranh giới server →
90
+ * client được: khai nó trong một component `"use client"` của app, đừng truyền từ root layout.
89
91
  */
90
92
  messages?: ComitorMessagesOverride;
91
93
  /**
92
94
  * Locale date-fns thay cho mặc định (`enUS` cho mọi mã không phải tiếng Việt). Cần khi app muốn
93
95
  * định dạng kiểu Anh (`enGB`) hay một ngôn ngữ date-fns có sẵn khác.
96
+ *
97
+ * ⚠ Locale date-fns là object toàn HÀM — root layout của Next là Server Component, và truyền nó
98
+ * thẳng từ đó làm trang lỗi 500 ("Functions cannot be passed directly to Client Components"; đo
99
+ * trên Next 16 ở comitor-ds, 3.0.0). Đặt provider trong một component `"use client"` của app rồi
100
+ * render component đó từ layout:
101
+ *
102
+ * ```tsx
103
+ * "use client";
104
+ * export function ComitorLocale({ locale, children }: { locale: string; children: React.ReactNode }) {
105
+ * return (
106
+ * <ComitorLocaleProvider locale={locale} dateFnsLocale={locale === "en-GB" ? enGB : undefined}>
107
+ * {children}
108
+ * </ComitorLocaleProvider>
109
+ * );
110
+ * }
111
+ * ```
94
112
  */
95
113
  dateFnsLocale?: Locale;
96
114
  children: React.ReactNode;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/locale-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Locale as DateFnsLocale } from \"date-fns\";\nimport { enUS, vi } from \"date-fns/locale\";\nimport * as React from \"react\";\nimport { type ComitorLanguage, DEFAULT_COMITOR_LOCALE, getComitorLanguage } from \"./locale.js\";\nimport { type ComitorMessages, type ComitorMessagesOverride, getComitorMessages } from \"./messages.js\";\n\n/**\n * Ngôn ngữ của CẢ gói, đi xuống cây bằng context — một lần ở root, mọi component đọc theo.\n *\n * ─ THỨ TỰ ƯU TIÊN, cho mọi chuỗi và mọi định dạng ─\n * prop của chính component (`labels`, `placeholder`, `locale`…) → provider gần nhất → `\"en\"`.\n * Không có provider thì gói chạy tiếng Anh — đúng cho app quốc tế, và đúng cho Storybook/test.\n *\n * ─ APP COMITOR: LUÔN truyền ngôn ngữ từ thiết lập của NGƯỜI DÙNG ─\n * Ngôn ngữ của app Comitor sống ở hồ sơ Account (gieo vào cookie, đọc bằng `next-intl`). Bọc một\n * lần ở root layout, TRONG `NextIntlClientProvider`:\n *\n * ```tsx\n * const locale = await getLocale(); // next-intl/server\n * <ComitorLocaleProvider locale={locale}>{children}</ComitorLocaleProvider>\n * ```\n *\n * ⚠ Đừng để mặc định: từ `3.0.0` mặc định là tiếng Anh, nên một app Comitor quên dòng này sẽ hiện\n * nút \"Cancel\" giữa màn hình tiếng Việt — không lỗi, không cảnh báo.\n */\n\ninterface ComitorLocaleContextValue {\n /** Mã BCP 47 app truyền vào — dùng cho mọi phép định dạng `Intl`. */\n locale: string;\n /** Bảng chữ dựng sẵn được chọn từ `locale`. */\n language: ComitorLanguage;\n /** Bảng chữ đầy đủ đang có hiệu lực (đã trộn phần ghi đè của app). */\n messages: ComitorMessages;\n /** Locale của date-fns cho các component định dạng ngày bằng date-fns. */\n dateFnsLocale: DateFnsLocale;\n}\n\nconst DATE_FNS_LOCALES: Record<ComitorLanguage, DateFnsLocale> = { en: enUS, vi };\n\nfunction buildContextValue(\n locale: string,\n override?: ComitorMessagesOverride,\n dateFnsLocale?: DateFnsLocale\n): ComitorLocaleContextValue {\n const language = getComitorLanguage(locale);\n return {\n locale,\n language,\n messages: getComitorMessages(locale, override),\n dateFnsLocale: dateFnsLocale ?? DATE_FNS_LOCALES[language]\n };\n}\n\nconst DEFAULT_CONTEXT_VALUE = buildContextValue(DEFAULT_COMITOR_LOCALE);\n\nconst ComitorLocaleContext = React.createContext<ComitorLocaleContextValue>(DEFAULT_CONTEXT_VALUE);\n\nexport interface ComitorLocaleProviderProps {\n /**\n * Mã BCP 47: `\"vi\"`, `\"en\"`, `\"en-GB\"`… Chọn bảng chữ dựng sẵn (xem `getComitorLanguage`) VÀ\n * quyết định định dạng ngày/giờ/số. App Comitor truyền `getLocale()` / `useLocale()` của next-intl.\n */\n locale?: string;\n /**\n * Ghi đè một phần bảng chữ (theo namespace), hoặc cấp nguyên một bảng cho ngôn ngữ không dựng sẵn.\n * ⚠ Giữ tham chiếu ỔN ĐỊNH (hằng module hoặc `useMemo`) — object mới mỗi lượt render làm mọi\n * component đọc chữ render lại.\n */\n messages?: ComitorMessagesOverride;\n /**\n * Locale date-fns thay cho mặc định (`enUS` cho mọi mã không phải tiếng Việt). Cần khi app muốn\n * định dạng kiểu Anh (`enGB`) hay một ngôn ngữ date-fns có sẵn khác.\n */\n dateFnsLocale?: DateFnsLocale;\n children: React.ReactNode;\n}\n\nexport function ComitorLocaleProvider({\n locale = DEFAULT_COMITOR_LOCALE,\n messages,\n dateFnsLocale,\n children\n}: ComitorLocaleProviderProps) {\n const value = React.useMemo(\n () => buildContextValue(locale, messages, dateFnsLocale),\n [locale, messages, dateFnsLocale]\n );\n return <ComitorLocaleContext.Provider value={value}>{children}</ComitorLocaleContext.Provider>;\n}\n\n/** Ngôn ngữ đang có hiệu lực: `locale` (cho `Intl`), `language`, bảng chữ, locale date-fns. */\nexport function useComitorLocale(): ComitorLocaleContextValue {\n return React.useContext(ComitorLocaleContext);\n}\n\n/** Chữ của MỘT namespace theo provider gần nhất (hoặc tiếng Anh khi không có provider). */\nexport function useComitorMessages<K extends keyof ComitorMessages>(namespace: K): ComitorMessages[K] {\n return React.useContext(ComitorLocaleContext).messages[namespace];\n}\n\n/**\n * Chữ của một namespace, đã trộn `labels` của chính component lên trên — đúng thứ tự ưu tiên\n * prop → provider → `\"en\"`. Dành cho component có prop `labels?: Partial<…>`.\n */\nexport function useComitorLabels<K extends keyof ComitorMessages>(\n namespace: K,\n labels?: Partial<ComitorMessages[K]>\n): ComitorMessages[K] {\n const base = useComitorMessages(namespace);\n return React.useMemo(() => (labels ? { ...base, ...labels } : base), [base, labels]);\n}\n"],"mappings":";AAyFS;AAtFT,SAAS,MAAM,UAAU;AACzB,YAAY,WAAW;AACvB,SAA+B,wBAAwB,0BAA0B;AACjF,SAA6D,0BAA0B;AAiCvF,MAAM,mBAA2D,EAAE,IAAI,MAAM,GAAG;AAEhF,SAAS,kBACP,QACA,UACA,eAC2B;AAC3B,QAAM,WAAW,mBAAmB,MAAM;AAC1C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,mBAAmB,QAAQ,QAAQ;AAAA,IAC7C,eAAe,iBAAiB,iBAAiB,QAAQ;AAAA,EAC3D;AACF;AAEA,MAAM,wBAAwB,kBAAkB,sBAAsB;AAEtE,MAAM,uBAAuB,MAAM,cAAyC,qBAAqB;AAsB1F,SAAS,sBAAsB;AAAA,EACpC,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,QAAQ,MAAM;AAAA,IAClB,MAAM,kBAAkB,QAAQ,UAAU,aAAa;AAAA,IACvD,CAAC,QAAQ,UAAU,aAAa;AAAA,EAClC;AACA,SAAO,oBAAC,qBAAqB,UAArB,EAA8B,OAAe,UAAS;AAChE;AAGO,SAAS,mBAA8C;AAC5D,SAAO,MAAM,WAAW,oBAAoB;AAC9C;AAGO,SAAS,mBAAoD,WAAkC;AACpG,SAAO,MAAM,WAAW,oBAAoB,EAAE,SAAS,SAAS;AAClE;AAMO,SAAS,iBACd,WACA,QACoB;AACpB,QAAM,OAAO,mBAAmB,SAAS;AACzC,SAAO,MAAM,QAAQ,MAAO,SAAS,EAAE,GAAG,MAAM,GAAG,OAAO,IAAI,MAAO,CAAC,MAAM,MAAM,CAAC;AACrF;","names":[]}
1
+ {"version":3,"sources":["../../src/i18n/locale-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Locale as DateFnsLocale } from \"date-fns\";\nimport { enUS, vi } from \"date-fns/locale\";\nimport * as React from \"react\";\nimport { type ComitorLanguage, DEFAULT_COMITOR_LOCALE, getComitorLanguage } from \"./locale.js\";\nimport { type ComitorMessages, type ComitorMessagesOverride, getComitorMessages } from \"./messages.js\";\n\n/**\n * Ngôn ngữ của CẢ gói, đi xuống cây bằng context — một lần ở root, mọi component đọc theo.\n *\n * ─ THỨ TỰ ƯU TIÊN, cho mọi chuỗi và mọi định dạng ─\n * prop của chính component (`labels`, `placeholder`, `locale`…) → provider gần nhất → `\"en\"`.\n * Không có provider thì gói chạy tiếng Anh — đúng cho app quốc tế, và đúng cho Storybook/test.\n *\n * ─ APP COMITOR: LUÔN truyền ngôn ngữ từ thiết lập của NGƯỜI DÙNG ─\n * Ngôn ngữ của app Comitor sống ở hồ sơ Account (gieo vào cookie, đọc bằng `next-intl`). Bọc một\n * lần ở root layout, TRONG `NextIntlClientProvider`:\n *\n * ```tsx\n * const locale = await getLocale(); // next-intl/server\n * <ComitorLocaleProvider locale={locale}>{children}</ComitorLocaleProvider>\n * ```\n *\n * ⚠ Đừng để mặc định: từ `3.0.0` mặc định là tiếng Anh, nên một app Comitor quên dòng này sẽ hiện\n * nút \"Cancel\" giữa màn hình tiếng Việt — không lỗi, không cảnh báo.\n */\n\ninterface ComitorLocaleContextValue {\n /** Mã BCP 47 app truyền vào — dùng cho mọi phép định dạng `Intl`. */\n locale: string;\n /** Bảng chữ dựng sẵn được chọn từ `locale`. */\n language: ComitorLanguage;\n /** Bảng chữ đầy đủ đang có hiệu lực (đã trộn phần ghi đè của app). */\n messages: ComitorMessages;\n /** Locale của date-fns cho các component định dạng ngày bằng date-fns. */\n dateFnsLocale: DateFnsLocale;\n}\n\nconst DATE_FNS_LOCALES: Record<ComitorLanguage, DateFnsLocale> = { en: enUS, vi };\n\nfunction buildContextValue(\n locale: string,\n override?: ComitorMessagesOverride,\n dateFnsLocale?: DateFnsLocale\n): ComitorLocaleContextValue {\n const language = getComitorLanguage(locale);\n return {\n locale,\n language,\n messages: getComitorMessages(locale, override),\n dateFnsLocale: dateFnsLocale ?? DATE_FNS_LOCALES[language]\n };\n}\n\nconst DEFAULT_CONTEXT_VALUE = buildContextValue(DEFAULT_COMITOR_LOCALE);\n\nconst ComitorLocaleContext = React.createContext<ComitorLocaleContextValue>(DEFAULT_CONTEXT_VALUE);\n\nexport interface ComitorLocaleProviderProps {\n /**\n * Mã BCP 47: `\"vi\"`, `\"en\"`, `\"en-GB\"`… Chọn bảng chữ dựng sẵn (xem `getComitorLanguage`) VÀ\n * quyết định định dạng ngày/giờ/số. App Comitor truyền `getLocale()` / `useLocale()` của next-intl.\n */\n locale?: string;\n /**\n * Ghi đè một phần bảng chữ (theo namespace), hoặc cấp nguyên một bảng cho ngôn ngữ không dựng sẵn.\n * ⚠ Giữ tham chiếu ỔN ĐỊNH (hằng module hoặc `useMemo`) — object mới mỗi lượt render làm mọi\n * component đọc chữ render lại.\n * ⚠ Có khoá là HÀM (vd `tablePagination.range`) thì object này không đi qua ranh giới server →\n * client được: khai nó trong một component `\"use client\"` của app, đừng truyền từ root layout.\n */\n messages?: ComitorMessagesOverride;\n /**\n * Locale date-fns thay cho mặc định (`enUS` cho mọi mã không phải tiếng Việt). Cần khi app muốn\n * định dạng kiểu Anh (`enGB`) hay một ngôn ngữ date-fns có sẵn khác.\n *\n * ⚠ Locale date-fns là object toàn HÀM — root layout của Next là Server Component, và truyền nó\n * thẳng từ đó làm trang lỗi 500 (\"Functions cannot be passed directly to Client Components\"; đo\n * trên Next 16 ở comitor-ds, 3.0.0). Đặt provider trong một component `\"use client\"` của app rồi\n * render component đó từ layout:\n *\n * ```tsx\n * \"use client\";\n * export function ComitorLocale({ locale, children }: { locale: string; children: React.ReactNode }) {\n * return (\n * <ComitorLocaleProvider locale={locale} dateFnsLocale={locale === \"en-GB\" ? enGB : undefined}>\n * {children}\n * </ComitorLocaleProvider>\n * );\n * }\n * ```\n */\n dateFnsLocale?: DateFnsLocale;\n children: React.ReactNode;\n}\n\nexport function ComitorLocaleProvider({\n locale = DEFAULT_COMITOR_LOCALE,\n messages,\n dateFnsLocale,\n children\n}: ComitorLocaleProviderProps) {\n const value = React.useMemo(\n () => buildContextValue(locale, messages, dateFnsLocale),\n [locale, messages, dateFnsLocale]\n );\n return <ComitorLocaleContext.Provider value={value}>{children}</ComitorLocaleContext.Provider>;\n}\n\n/** Ngôn ngữ đang có hiệu lực: `locale` (cho `Intl`), `language`, bảng chữ, locale date-fns. */\nexport function useComitorLocale(): ComitorLocaleContextValue {\n return React.useContext(ComitorLocaleContext);\n}\n\n/** Chữ của MỘT namespace theo provider gần nhất (hoặc tiếng Anh khi không có provider). */\nexport function useComitorMessages<K extends keyof ComitorMessages>(namespace: K): ComitorMessages[K] {\n return React.useContext(ComitorLocaleContext).messages[namespace];\n}\n\n/**\n * Chữ của một namespace, đã trộn `labels` của chính component lên trên — đúng thứ tự ưu tiên\n * prop → provider → `\"en\"`. Dành cho component có prop `labels?: Partial<…>`.\n */\nexport function useComitorLabels<K extends keyof ComitorMessages>(\n namespace: K,\n labels?: Partial<ComitorMessages[K]>\n): ComitorMessages[K] {\n const base = useComitorMessages(namespace);\n return React.useMemo(() => (labels ? { ...base, ...labels } : base), [base, labels]);\n}\n"],"mappings":";AA2GS;AAxGT,SAAS,MAAM,UAAU;AACzB,YAAY,WAAW;AACvB,SAA+B,wBAAwB,0BAA0B;AACjF,SAA6D,0BAA0B;AAiCvF,MAAM,mBAA2D,EAAE,IAAI,MAAM,GAAG;AAEhF,SAAS,kBACP,QACA,UACA,eAC2B;AAC3B,QAAM,WAAW,mBAAmB,MAAM;AAC1C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,mBAAmB,QAAQ,QAAQ;AAAA,IAC7C,eAAe,iBAAiB,iBAAiB,QAAQ;AAAA,EAC3D;AACF;AAEA,MAAM,wBAAwB,kBAAkB,sBAAsB;AAEtE,MAAM,uBAAuB,MAAM,cAAyC,qBAAqB;AAwC1F,SAAS,sBAAsB;AAAA,EACpC,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,QAAQ,MAAM;AAAA,IAClB,MAAM,kBAAkB,QAAQ,UAAU,aAAa;AAAA,IACvD,CAAC,QAAQ,UAAU,aAAa;AAAA,EAClC;AACA,SAAO,oBAAC,qBAAqB,UAArB,EAA8B,OAAe,UAAS;AAChE;AAGO,SAAS,mBAA8C;AAC5D,SAAO,MAAM,WAAW,oBAAoB;AAC9C;AAGO,SAAS,mBAAoD,WAAkC;AACpG,SAAO,MAAM,WAAW,oBAAoB,EAAE,SAAS,SAAS;AAClE;AAMO,SAAS,iBACd,WACA,QACoB;AACpB,QAAM,OAAO,mBAAmB,SAAS;AACzC,SAAO,MAAM,QAAQ,MAAO,SAAS,EAAE,GAAG,MAAM,GAAG,OAAO,IAAI,MAAO,CAAC,MAAM,MAAM,CAAC;AACrF;","names":[]}
@@ -2,7 +2,8 @@ const ratingEn = {
2
2
  itemLabel: (value) => value === 1 ? "1 star" : `${value} stars`
3
3
  };
4
4
  const ratingVi = {
5
- itemLabel: (value) => `${value} sao`
5
+ // Từ 3.0.1 mục đang chọn của giá trị lẻ mang tên theo giá trị thật (4,5) — viết dấu phẩy thập phân.
6
+ itemLabel: (value) => `${value.toLocaleString("vi-VN")} sao`
6
7
  };
7
8
  export {
8
9
  ratingEn,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/rating.ts"],"sourcesContent":["/** Chữ của `Rating` / `RatingItem`. */\nexport interface RatingMessages {\n /**\n * Tên của TỪNG ngôi sao cho trình đọc màn hình — prop `itemLabel` của `Rating`.\n * HÀM vì có số chèn vào giữa câu (và tiếng Anh còn phải chia số ít/số nhiều).\n */\n itemLabel: (value: number, max: number) => string;\n}\n\nexport const ratingEn: RatingMessages = {\n itemLabel: (value) => (value === 1 ? \"1 star\" : `${value} stars`)\n};\n\nexport const ratingVi: RatingMessages = {\n itemLabel: (value) => `${value} sao`\n};\n"],"mappings":"AASO,MAAM,WAA2B;AAAA,EACtC,WAAW,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAC1D;AAEO,MAAM,WAA2B;AAAA,EACtC,WAAW,CAAC,UAAU,GAAG,KAAK;AAChC;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/rating.ts"],"sourcesContent":["/** Chữ của `Rating` / `RatingItem`. */\nexport interface RatingMessages {\n /**\n * Tên của TỪNG ngôi sao cho trình đọc màn hình — prop `itemLabel` của `Rating`.\n * HÀM vì có số chèn vào giữa câu (và tiếng Anh còn phải chia số ít/số nhiều).\n */\n itemLabel: (value: number, max: number) => string;\n}\n\nexport const ratingEn: RatingMessages = {\n itemLabel: (value) => (value === 1 ? \"1 star\" : `${value} stars`)\n};\n\nexport const ratingVi: RatingMessages = {\n // Từ 3.0.1 mục đang chọn của giá trị lẻ mang tên theo giá trị thật (4,5) — viết dấu phẩy thập phân.\n itemLabel: (value) => `${value.toLocaleString(\"vi-VN\")} sao`\n};\n"],"mappings":"AASO,MAAM,WAA2B;AAAA,EACtC,WAAW,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAC1D;AAEO,MAAM,WAA2B;AAAA;AAAA,EAEtC,WAAW,CAAC,UAAU,GAAG,MAAM,eAAe,OAAO,CAAC;AACxD;","names":[]}
@@ -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":"AAgBI,mBACE,KADF;AAPG,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EACtE,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,uBAAY;AAAA,IACzE,UAAU,SAAY,OAAO,KAAK,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KAC/C;AAEJ;AAEO,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,KAAK;AAAA,EAChC,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,uBAAY;AAAA,IACzE,UAAU,SAAY,MAAM,KAAK,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KAC9C;AAEJ;","names":[]}
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":[]}
@@ -10,6 +10,10 @@ import 'react';
10
10
  *
11
11
  * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):
12
12
  * "Collapse" ⊂ "Collapse sidebar", "Thu gọn" ⊂ "Thu gọn thanh bên".
13
+ * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho
14
+ * trình đọc màn hình): "Ứng dụng" ⊂ "Mở danh sách ứng dụng". Bảng tiếng Anh của `3.0.0` hỏng đúng
15
+ * chỗ này — "Apps" không nằm trong "Open app launcher", nên người điều khiển bằng giọng nói đọc
16
+ * chữ họ THẤY ("click Apps") mà không khớp nút nào. Sửa ở `3.0.1`.
13
17
  */
14
18
  declare const shellEn: ShellLabels;
15
19
  declare const shellVi: ShellLabels;
@@ -17,7 +17,7 @@ const shellEn = {
17
17
  inviteMembers: "Invite members",
18
18
  members: (count) => `${count} ${count === 1 ? "member" : "members"}`,
19
19
  apps: "Apps",
20
- appLauncherLabel: "Open app launcher",
20
+ appLauncherLabel: "Open apps list",
21
21
  myApps: "Your apps",
22
22
  discoverApps: "Discover more",
23
23
  locked: "Locked",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open app launcher\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\"\n};\n"],"mappings":"AAWO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho\n * trình đọc màn hình): \"Ứng dụng\" ⊂ \"Mở danh sách ứng dụng\". Bảng tiếng Anh của `3.0.0` hỏng đúng\n * chỗ này — \"Apps\" không nằm trong \"Open app launcher\", nên người điều khiển bằng giọng nói đọc\n * chữ họ THẤY (\"click Apps\") mà không khớp nút nào. Sửa ở `3.0.1`.\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open apps list\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\"\n};\n"],"mappings":"AAeO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AACf;","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) => `${start}–${end} trên tổng ${total} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,\n firstPage: \"Trang đầu\",\n previousPage: \"Trang trước\",\n nextPage: \"Trang sau\",\n lastPage: \"Trang cuối\"\n};\n"],"mappings":"AAGO,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,KAAK,SAAI,GAAG,OAAO,KAAK,IAAI,SAAS;AAAA,EACjF,YAAY,CAAC,MAAM,cAAc,QAAQ,IAAI,OAAO,SAAS;AAAA,EAC7D,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,cAAc,GAAG,KAAK,SAAI,GAAG,sBAAc,KAAK,IAAI,SAAS;AAAA,EACxF,YAAY,CAAC,MAAM,cAAc,SAAS,IAAI,MAAM,SAAS;AAAA,EAC7D,WAAW;AAAA,EACX,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AACZ;","names":[]}
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":[]}
@@ -2,8 +2,9 @@
2
2
  * Chữ của `toastPromise`.
3
3
  *
4
4
  * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được
5
- * `ComitorLocaleProvider`. `<Toaster />` (luôn nằm trong cây, dưới provider) ghi bảng này theo
6
- * ngôn ngữ hiện hành cho nó dùng; xem `activeToastMessages` trong `composite/toast.tsx`.
5
+ * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider — `Toaster` của entry chính
6
+ * hay `SonnerToaster` của `/shell`) ghi bảng này theo ngôn ngữ hiện hành cho nó dùng; xem
7
+ * `lib/toast-language.ts`.
7
8
  */
8
9
  interface ToastMessages {
9
10
  /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/toast.ts"],"sourcesContent":["/**\n * Chữ của `toastPromise`.\n *\n * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được\n * `ComitorLocaleProvider`. `<Toaster />` (luôn nằm trong cây, dưới provider) ghi bảng này theo\n * ngôn ngữ hiện hành cho nó dùng; xem `activeToastMessages` trong `composite/toast.tsx`.\n */\nexport interface ToastMessages {\n /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */\n loading: string;\n}\n\nexport const toastEn: ToastMessages = { loading: \"Working…\" };\n\nexport const toastVi: ToastMessages = { loading: \"Đang xử lý…\" };\n"],"mappings":"AAYO,MAAM,UAAyB,EAAE,SAAS,gBAAW;AAErD,MAAM,UAAyB,EAAE,SAAS,gCAAc;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/toast.ts"],"sourcesContent":["/**\n * Chữ của `toastPromise`.\n *\n * `toastPromise` là hàm THƯỜNG gọi từ event handler, ngoài lượt render — nó không tự đọc được\n * `ComitorLocaleProvider`. Toaster (luôn nằm trong cây, dưới provider — `Toaster` của entry chính\n * hay `SonnerToaster` của `/shell`) ghi bảng này theo ngôn ngữ hiện hành cho nó dùng; xem\n * `lib/toast-language.ts`.\n */\nexport interface ToastMessages {\n /** Chữ của toast lúc Promise còn chạy — `messages.loading` của `toastPromise` */\n loading: string;\n}\n\nexport const toastEn: ToastMessages = { loading: \"Working…\" };\n\nexport const toastVi: ToastMessages = { loading: \"Đang xử lý…\" };\n"],"mappings":"AAaO,MAAM,UAAyB,EAAE,SAAS,gBAAW;AAErD,MAAM,UAAyB,EAAE,SAAS,gCAAc;","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, matchesSearch, normalizeVi, removeVietnameseTones, truncate } from './components/composite/text-utils.js';
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