@comitor/ui 3.3.1 → 3.3.3

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 (60) hide show
  1. package/dist/components/composite/command-palette.d.ts +1 -1
  2. package/dist/components/composite/command-palette.js.map +1 -1
  3. package/dist/components/composite/data-table.d.ts +10 -5
  4. package/dist/components/composite/data-table.js +2 -2
  5. package/dist/components/composite/data-table.js.map +1 -1
  6. package/dist/components/composite/filter-chips.d.ts +1 -1
  7. package/dist/components/composite/filter-chips.js.map +1 -1
  8. package/dist/components/composite/inline-edit.d.ts +2 -2
  9. package/dist/components/composite/inline-edit.js.map +1 -1
  10. package/dist/components/composite/route-tabs.d.ts +8 -6
  11. package/dist/components/composite/route-tabs.js.map +1 -1
  12. package/dist/components/composite/status-config.d.ts +1 -1
  13. package/dist/components/composite/status-config.js.map +1 -1
  14. package/dist/components/ui/progress.d.ts +14 -2
  15. package/dist/components/ui/progress.js +12 -3
  16. package/dist/components/ui/progress.js.map +1 -1
  17. package/dist/components/ui/resizable.d.ts +1 -1
  18. package/dist/components/ui/resizable.js.map +1 -1
  19. package/dist/data-grid/data-grid-context-menu.js +3 -2
  20. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  21. package/dist/data-grid/data-grid-row-height-menu.js +2 -2
  22. package/dist/data-grid/data-grid-row-height-menu.js.map +1 -1
  23. package/dist/data-grid/types.d.ts +7 -2
  24. package/dist/data-grid/use-data-grid.d.ts +14 -4
  25. package/dist/data-grid/use-data-grid.js +5 -3
  26. package/dist/data-grid/use-data-grid.js.map +1 -1
  27. package/dist/data-grid/utils.js +0 -1
  28. package/dist/data-grid/utils.js.map +1 -1
  29. package/dist/i18n/namespaces/combobox.js +2 -2
  30. package/dist/i18n/namespaces/combobox.js.map +1 -1
  31. package/dist/i18n/namespaces/confirm-dialog.js +1 -1
  32. package/dist/i18n/namespaces/confirm-dialog.js.map +1 -1
  33. package/dist/i18n/namespaces/date-picker.js +2 -2
  34. package/dist/i18n/namespaces/date-picker.js.map +1 -1
  35. package/dist/i18n/namespaces/inline-edit.js +1 -1
  36. package/dist/i18n/namespaces/inline-edit.js.map +1 -1
  37. package/dist/i18n/namespaces/search-filter-bar.js +2 -2
  38. package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
  39. package/dist/i18n/namespaces/time-picker.js +1 -1
  40. package/dist/i18n/namespaces/time-picker.js.map +1 -1
  41. package/dist/shell/app-launcher.d.ts +2 -2
  42. package/dist/shell/app-launcher.js.map +1 -1
  43. package/dist/shell/app-shell.d.ts +1 -1
  44. package/dist/shell/app-shell.js.map +1 -1
  45. package/dist/shell/comitor-logo.d.ts +1 -1
  46. package/dist/shell/comitor-logo.js.map +1 -1
  47. package/dist/shell/shell-context.d.ts +3 -2
  48. package/dist/shell/shell-context.js.map +1 -1
  49. package/dist/shell/sidebar-group.d.ts +1 -1
  50. package/dist/shell/sidebar-group.js.map +1 -1
  51. package/dist/shell/types.d.ts +1 -1
  52. package/dist/shell/types.js.map +1 -1
  53. package/dist/shell/utils.d.ts +2 -2
  54. package/dist/shell/utils.js.map +1 -1
  55. package/dist/tokens.d.ts +23 -9
  56. package/dist/tokens.js +17 -9
  57. package/dist/tokens.js.map +1 -1
  58. package/dist/uploader/image-upload-field.d.ts +2 -2
  59. package/dist/uploader/image-upload-field.js.map +1 -1
  60. package/package.json +1 -1
@@ -92,7 +92,6 @@ function getColumnPinningStyle(params) {
92
92
  boxShadow: withBorder ? isLastLeftPinnedColumn ? isRtl ? "4px 0 4px -4px var(--border) inset" : "-4px 0 4px -4px var(--border) inset" : isFirstRightPinnedColumn ? isRtl ? "-4px 0 4px -4px var(--border) inset" : "4px 0 4px -4px var(--border) inset" : void 0 : void 0,
93
93
  left: isRtl ? rightPosition : leftPosition,
94
94
  right: isRtl ? leftPosition : rightPosition,
95
- opacity: isPinned ? 0.97 : 1,
96
95
  position: isPinned ? "sticky" : "relative",
97
96
  background: "var(--card)",
98
97
  width: column.getSize(),
@@ -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 *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
1
+ {"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 * ⚠ Khác bản gốc (3.3.2): KHÔNG có `opacity`. Bản gốc đặt `opacity: 0.97` cho ô ghim — nền đã\n * đục nhưng cả Ô mờ 3%, nên khi cuộn ngang, chữ của cột đang trôi phía dưới lộ thành vệt mờ sau\n * cột ghim. Cùng một kết cục với lỗi `hsl()` ngay trên (cột ghim để lộ nội dung cuộn qua), chỉ\n * nhạt hơn — và cũng không cổng nào thấy, vì `opacity` nằm ở `style` inline. `test:grid-render`\n * nay canh cả chuyện này. Ranh giới vùng ghim ⇄ vùng cuộn đã có viền cuối cột lo.\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 position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
@@ -12,8 +12,8 @@ const comboboxVi = {
12
12
  searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
13
13
  emptyText: "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
14
14
  loadingText: "\u0110ang t\u1EA3i\u2026",
15
- clearLabel: "X\xF3a l\u1EF1a ch\u1ECDn",
16
- clearAllLabel: "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
15
+ clearLabel: "Xo\xE1 l\u1EF1a ch\u1ECDn",
16
+ clearAllLabel: "Xo\xE1 t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
17
17
  selectedCountLabel: (selected, max) => `\u0110\xE3 ch\u1ECDn ${selected}/${max}`
18
18
  };
19
19
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/combobox.ts"],"sourcesContent":["/** Chữ của `Combobox` / `MultiCombobox`. Mọi khoá đều có prop rời cùng tên để đè riêng một chỗ. */\nexport interface ComboboxMessages {\n /** Chữ trên trigger khi chưa chọn gì */\n placeholder: string;\n /** Placeholder của ô tìm trong panel */\n searchPlaceholder: string;\n /** Khi không mục nào khớp */\n emptyText: string;\n /** Chữ THẤY ĐƯỢC trong panel khi `loading` */\n loadingText: string;\n /** Tên (sr-only) của nút xoá ở `Combobox` — bỏ MỘT lựa chọn */\n clearLabel: string;\n /** Tên (sr-only) của nút xoá ở `MultiCombobox` — bỏ SẠCH danh sách đang chọn */\n clearAllLabel: string;\n /** Dòng đếm ở đáy panel khi có `maxSelected`. HÀM vì chèn HAI số. */\n selectedCountLabel: (selected: number, max: number) => string;\n}\n\nexport const comboboxEn: ComboboxMessages = {\n placeholder: \"Select…\",\n searchPlaceholder: \"Search…\",\n emptyText: \"No results found.\",\n loadingText: \"Loading…\",\n clearLabel: \"Clear selection\",\n clearAllLabel: \"Clear all selections\",\n selectedCountLabel: (selected, max) => `${selected}/${max} selected`\n};\n\nexport const comboboxVi: ComboboxMessages = {\n placeholder: \"Chọn…\",\n searchPlaceholder: \"Tìm kiếm…\",\n emptyText: \"Không tìm thấy kết quả.\",\n loadingText: \"Đang tải…\",\n clearLabel: \"Xóa lựa chọn\",\n clearAllLabel: \"Xóa tất cả lựa chọn\",\n selectedCountLabel: (selected, max) => `Đã chọn ${selected}/${max}`\n};\n"],"mappings":"AAkBO,MAAM,aAA+B;AAAA,EAC1C,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,oBAAoB,CAAC,UAAU,QAAQ,GAAG,QAAQ,IAAI,GAAG;AAC3D;AAEO,MAAM,aAA+B;AAAA,EAC1C,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,oBAAoB,CAAC,UAAU,QAAQ,wBAAW,QAAQ,IAAI,GAAG;AACnE;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/combobox.ts"],"sourcesContent":["/** Chữ của `Combobox` / `MultiCombobox`. Mọi khoá đều có prop rời cùng tên để đè riêng một chỗ. */\nexport interface ComboboxMessages {\n /** Chữ trên trigger khi chưa chọn gì */\n placeholder: string;\n /** Placeholder của ô tìm trong panel */\n searchPlaceholder: string;\n /** Khi không mục nào khớp */\n emptyText: string;\n /** Chữ THẤY ĐƯỢC trong panel khi `loading` */\n loadingText: string;\n /** Tên (sr-only) của nút xoá ở `Combobox` — bỏ MỘT lựa chọn */\n clearLabel: string;\n /** Tên (sr-only) của nút xoá ở `MultiCombobox` — bỏ SẠCH danh sách đang chọn */\n clearAllLabel: string;\n /** Dòng đếm ở đáy panel khi có `maxSelected`. HÀM vì chèn HAI số. */\n selectedCountLabel: (selected: number, max: number) => string;\n}\n\nexport const comboboxEn: ComboboxMessages = {\n placeholder: \"Select…\",\n searchPlaceholder: \"Search…\",\n emptyText: \"No results found.\",\n loadingText: \"Loading…\",\n clearLabel: \"Clear selection\",\n clearAllLabel: \"Clear all selections\",\n selectedCountLabel: (selected, max) => `${selected}/${max} selected`\n};\n\nexport const comboboxVi: ComboboxMessages = {\n placeholder: \"Chọn…\",\n searchPlaceholder: \"Tìm kiếm…\",\n emptyText: \"Không tìm thấy kết quả.\",\n loadingText: \"Đang tải…\",\n clearLabel: \"Xoá lựa chọn\",\n clearAllLabel: \"Xoá tất cả lựa chọn\",\n selectedCountLabel: (selected, max) => `Đã chọn ${selected}/${max}`\n};\n"],"mappings":"AAkBO,MAAM,aAA+B;AAAA,EAC1C,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,oBAAoB,CAAC,UAAU,QAAQ,GAAG,QAAQ,IAAI,GAAG;AAC3D;AAEO,MAAM,aAA+B;AAAA,EAC1C,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,oBAAoB,CAAC,UAAU,QAAQ,wBAAW,QAAQ,IAAI,GAAG;AACnE;","names":[]}
@@ -5,7 +5,7 @@ const confirmDialogEn = {
5
5
  };
6
6
  const confirmDialogVi = {
7
7
  confirmLabel: "X\xE1c nh\u1EADn",
8
- cancelLabel: "H\u1EE7y",
8
+ cancelLabel: "Hu\u1EF7",
9
9
  pendingLabel: "\u0110ang x\u1EED l\xFD\u2026"
10
10
  };
11
11
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/confirm-dialog.ts"],"sourcesContent":["/** Chữ của `ConfirmDialog`. Mỗi khoá có prop rời cùng tên để đè riêng một chỗ. */\nexport interface ConfirmDialogMessages {\n /** Nút xác nhận */\n confirmLabel: string;\n /** Nút huỷ */\n cancelLabel: string;\n /** Nút xác nhận trong lúc `onConfirm` bất đồng bộ đang chạy */\n pendingLabel: string;\n}\n\nexport const confirmDialogEn: ConfirmDialogMessages = {\n confirmLabel: \"Confirm\",\n cancelLabel: \"Cancel\",\n pendingLabel: \"Working…\"\n};\n\nexport const confirmDialogVi: ConfirmDialogMessages = {\n confirmLabel: \"Xác nhận\",\n cancelLabel: \"Hủy\",\n pendingLabel: \"Đang xử lý…\"\n};\n"],"mappings":"AAUO,MAAM,kBAAyC;AAAA,EACpD,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,MAAM,kBAAyC;AAAA,EACpD,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/confirm-dialog.ts"],"sourcesContent":["/** Chữ của `ConfirmDialog`. Mỗi khoá có prop rời cùng tên để đè riêng một chỗ. */\nexport interface ConfirmDialogMessages {\n /** Nút xác nhận */\n confirmLabel: string;\n /** Nút huỷ */\n cancelLabel: string;\n /** Nút xác nhận trong lúc `onConfirm` bất đồng bộ đang chạy */\n pendingLabel: string;\n}\n\nexport const confirmDialogEn: ConfirmDialogMessages = {\n confirmLabel: \"Confirm\",\n cancelLabel: \"Cancel\",\n pendingLabel: \"Working…\"\n};\n\nexport const confirmDialogVi: ConfirmDialogMessages = {\n confirmLabel: \"Xác nhận\",\n cancelLabel: \"Huỷ\",\n pendingLabel: \"Đang xử lý…\"\n};\n"],"mappings":"AAUO,MAAM,kBAAyC;AAAA,EACpD,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,MAAM,kBAAyC;AAAA,EACpD,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;","names":[]}
@@ -14,8 +14,8 @@ const datePickerEn = {
14
14
  }
15
15
  };
16
16
  const datePickerVi = {
17
- clear: "X\xF3a ng\xE0y",
18
- clearRange: "X\xF3a kho\u1EA3ng ng\xE0y",
17
+ clear: "Xo\xE1 ng\xE0y",
18
+ clearRange: "Xo\xE1 kho\u1EA3ng ng\xE0y",
19
19
  openCalendar: "M\u1EDF l\u1ECBch",
20
20
  rangePlaceholder: "Ch\u1ECDn kho\u1EA3ng ng\xE0y",
21
21
  presets: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/date-picker.ts"],"sourcesContent":["import type { DatePickerLabels } from \"../../components/composite/date-picker-constants.js\";\n\n/**\n * Nhãn của các preset MẶC ĐỊNH (`DEFAULT_DATE_PRESETS` / `DEFAULT_DATE_RANGE_PRESETS`), khoá theo\n * `id` của preset (viết camelCase). `today` dùng chung cho cả hai danh sách.\n *\n * Chỉ áp cho CHÍNH các object preset của gói — preset app tự dựng hiện nguyên `label` của nó, kể\n * cả khi trùng `id` (xem `presetLabel` trong `date-picker.tsx`).\n */\nexport interface DatePresetMessages {\n /** id `today` — cả hai danh sách */\n today: string;\n /** id `yesterday` — `DEFAULT_DATE_PRESETS` */\n yesterday: string;\n /** id `week-ago` — `DEFAULT_DATE_PRESETS` */\n weekAgo: string;\n /** id `last-7` — `DEFAULT_DATE_RANGE_PRESETS` */\n last7Days: string;\n /** id `last-30` — `DEFAULT_DATE_RANGE_PRESETS` */\n last30Days: string;\n /** id `this-month` — `DEFAULT_DATE_RANGE_PRESETS` */\n thisMonth: string;\n /** id `last-month` — `DEFAULT_DATE_RANGE_PRESETS` */\n lastMonth: string;\n}\n\n/**\n * Chữ của `DatePicker` / `DateRangePicker`. Ba khoá của `DatePickerLabels` (`clear`,\n * `clearRange`, `openCalendar`) đè được qua prop `labels`; `rangePlaceholder` qua prop\n * `placeholder`; nhãn preset thì app đè bằng cách truyền preset của riêng mình.\n */\nexport interface DatePickerMessages extends DatePickerLabels {\n /** Chữ trên ô `DateRangePicker` khi chưa chọn khoảng nào (prop `placeholder` thắng) */\n rangePlaceholder: string;\n /** Nhãn preset mặc định — xem {@link DatePresetMessages} */\n presets: DatePresetMessages;\n}\n\nexport const datePickerEn: DatePickerMessages = {\n clear: \"Clear date\",\n clearRange: \"Clear date range\",\n openCalendar: \"Open calendar\",\n rangePlaceholder: \"Pick a date range\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n weekAgo: \"7 days ago\",\n last7Days: \"Last 7 days\",\n last30Days: \"Last 30 days\",\n thisMonth: \"This month\",\n lastMonth: \"Last month\"\n }\n};\n\nexport const datePickerVi: DatePickerMessages = {\n clear: \"Xóa ngày\",\n clearRange: \"Xóa khoảng ngày\",\n openCalendar: \"Mở lịch\",\n rangePlaceholder: \"Chọn khoảng ngày\",\n presets: {\n today: \"Hôm nay\",\n yesterday: \"Hôm qua\",\n weekAgo: \"7 ngày trước\",\n last7Days: \"7 ngày qua\",\n last30Days: \"30 ngày qua\",\n thisMonth: \"Tháng này\",\n lastMonth: \"Tháng trước\"\n }\n};\n"],"mappings":"AAsCO,MAAM,eAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,MAAM,eAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/date-picker.ts"],"sourcesContent":["import type { DatePickerLabels } from \"../../components/composite/date-picker-constants.js\";\n\n/**\n * Nhãn của các preset MẶC ĐỊNH (`DEFAULT_DATE_PRESETS` / `DEFAULT_DATE_RANGE_PRESETS`), khoá theo\n * `id` của preset (viết camelCase). `today` dùng chung cho cả hai danh sách.\n *\n * Chỉ áp cho CHÍNH các object preset của gói — preset app tự dựng hiện nguyên `label` của nó, kể\n * cả khi trùng `id` (xem `presetLabel` trong `date-picker.tsx`).\n */\nexport interface DatePresetMessages {\n /** id `today` — cả hai danh sách */\n today: string;\n /** id `yesterday` — `DEFAULT_DATE_PRESETS` */\n yesterday: string;\n /** id `week-ago` — `DEFAULT_DATE_PRESETS` */\n weekAgo: string;\n /** id `last-7` — `DEFAULT_DATE_RANGE_PRESETS` */\n last7Days: string;\n /** id `last-30` — `DEFAULT_DATE_RANGE_PRESETS` */\n last30Days: string;\n /** id `this-month` — `DEFAULT_DATE_RANGE_PRESETS` */\n thisMonth: string;\n /** id `last-month` — `DEFAULT_DATE_RANGE_PRESETS` */\n lastMonth: string;\n}\n\n/**\n * Chữ của `DatePicker` / `DateRangePicker`. Ba khoá của `DatePickerLabels` (`clear`,\n * `clearRange`, `openCalendar`) đè được qua prop `labels`; `rangePlaceholder` qua prop\n * `placeholder`; nhãn preset thì app đè bằng cách truyền preset của riêng mình.\n */\nexport interface DatePickerMessages extends DatePickerLabels {\n /** Chữ trên ô `DateRangePicker` khi chưa chọn khoảng nào (prop `placeholder` thắng) */\n rangePlaceholder: string;\n /** Nhãn preset mặc định — xem {@link DatePresetMessages} */\n presets: DatePresetMessages;\n}\n\nexport const datePickerEn: DatePickerMessages = {\n clear: \"Clear date\",\n clearRange: \"Clear date range\",\n openCalendar: \"Open calendar\",\n rangePlaceholder: \"Pick a date range\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n weekAgo: \"7 days ago\",\n last7Days: \"Last 7 days\",\n last30Days: \"Last 30 days\",\n thisMonth: \"This month\",\n lastMonth: \"Last month\"\n }\n};\n\nexport const datePickerVi: DatePickerMessages = {\n clear: \"Xoá ngày\",\n clearRange: \"Xoá khoảng ngày\",\n openCalendar: \"Mở lịch\",\n rangePlaceholder: \"Chọn khoảng ngày\",\n presets: {\n today: \"Hôm nay\",\n yesterday: \"Hôm qua\",\n weekAgo: \"7 ngày trước\",\n last7Days: \"7 ngày qua\",\n last30Days: \"30 ngày qua\",\n thisMonth: \"Tháng này\",\n lastMonth: \"Tháng trước\"\n }\n};\n"],"mappings":"AAsCO,MAAM,eAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAEO,MAAM,eAAmC;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;","names":[]}
@@ -8,7 +8,7 @@ const inlineEditVi = {
8
8
  placeholder: "Nh\u1EA5n \u0111\u1EC3 nh\u1EADp\u2026",
9
9
  ariaLabel: "S\u1EEDa t\u1EA1i ch\u1ED7",
10
10
  save: "L\u01B0u",
11
- cancel: "H\u1EE7y"
11
+ cancel: "Hu\u1EF7"
12
12
  };
13
13
  export {
14
14
  inlineEditEn,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/inline-edit.ts"],"sourcesContent":["/** Chữ của `InlineEdit`. */\nexport interface InlineEditMessages {\n /** Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập — prop `placeholder` */\n placeholder: string;\n /** `aria-label` của nút hiển thị lẫn ô nhập — prop `ariaLabel` */\n ariaLabel: string;\n /** Tên (sr-only) của nút ✓ khi `showActions` — không có prop rời */\n save: string;\n /** Tên (sr-only) của nút ✕ khi `showActions` — không có prop rời */\n cancel: string;\n}\n\nexport const inlineEditEn: InlineEditMessages = {\n placeholder: \"Click to edit…\",\n ariaLabel: \"Edit inline\",\n save: \"Save\",\n cancel: \"Cancel\"\n};\n\nexport const inlineEditVi: InlineEditMessages = {\n placeholder: \"Nhấn để nhập…\",\n ariaLabel: \"Sửa tại chỗ\",\n save: \"Lưu\",\n cancel: \"Hủy\"\n};\n"],"mappings":"AAYO,MAAM,eAAmC;AAAA,EAC9C,aAAa;AAAA,EACb,WAAW;AAAA,EACX,MAAM;AAAA,EACN,QAAQ;AACV;AAEO,MAAM,eAAmC;AAAA,EAC9C,aAAa;AAAA,EACb,WAAW;AAAA,EACX,MAAM;AAAA,EACN,QAAQ;AACV;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/inline-edit.ts"],"sourcesContent":["/** Chữ của `InlineEdit`. */\nexport interface InlineEditMessages {\n /** Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập — prop `placeholder` */\n placeholder: string;\n /** `aria-label` của nút hiển thị lẫn ô nhập — prop `ariaLabel` */\n ariaLabel: string;\n /** Tên (sr-only) của nút ✓ khi `showActions` — không có prop rời */\n save: string;\n /** Tên (sr-only) của nút ✕ khi `showActions` — không có prop rời */\n cancel: string;\n}\n\nexport const inlineEditEn: InlineEditMessages = {\n placeholder: \"Click to edit…\",\n ariaLabel: \"Edit inline\",\n save: \"Save\",\n cancel: \"Cancel\"\n};\n\nexport const inlineEditVi: InlineEditMessages = {\n placeholder: \"Nhấn để nhập…\",\n ariaLabel: \"Sửa tại chỗ\",\n save: \"Lưu\",\n cancel: \"Huỷ\"\n};\n"],"mappings":"AAYO,MAAM,eAAmC;AAAA,EAC9C,aAAa;AAAA,EACb,WAAW;AAAA,EACX,MAAM;AAAA,EACN,QAAQ;AACV;AAEO,MAAM,eAAmC;AAAA,EAC9C,aAAa;AAAA,EACb,WAAW;AAAA,EACX,MAAM;AAAA,EACN,QAAQ;AACV;","names":[]}
@@ -17,9 +17,9 @@ const searchFilterBarEn = {
17
17
  const searchFilterBarVi = {
18
18
  searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
19
19
  unitLabel: "m\u1EE5c",
20
- clearSearch: "X\xF3a t\u1EEB kh\xF3a",
20
+ clearSearch: "Xo\xE1 t\u1EEB kho\xE1",
21
21
  filterCount: (count) => `${formatVi(count)} b\u1ED9 l\u1ECDc`,
22
- clearFilters: "X\xF3a l\u1ECDc",
22
+ clearFilters: "Xo\xE1 l\u1ECDc",
23
23
  resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
24
24
  /* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: formatVi(resultCount) }),
25
25
  total !== void 0 ? ` / ${formatVi(total)}` : "",
@@ -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\n/*\n * Số (đếm bộ lọc, đếm kết quả) định dạng NGAY TRONG bảng, theo quy ước của chính ngôn ngữ bảng — cùng\n * khuôn `rating.ts`: bảng Anh `12,345`, bảng Việt `12.345` (trước `3.1.0` in số thô `12345`). Component\n * truyền số THÔ vào hàm, nên bảng tự viết của app giữ toàn quyền định dạng và chữ ký hàm không đổi.\n * Số ít/số nhiều vẫn quyết theo số thô, không theo chuỗi đã định dạng.\n */\nconst formatEn = (value: number) => value.toLocaleString(\"en\");\nconst formatVi = (value: number) => value.toLocaleString(\"vi-VN\");\n\nexport const searchFilterBarEn: SearchFilterBarLabels = {\n searchPlaceholder: \"Search…\",\n unitLabel: \"items\",\n clearSearch: \"Clear search\",\n filterCount: (count) => `${formatEn(count)} ${count === 1 ? \"filter\" : \"filters\"}`,\n clearFilters: \"Clear filters\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{formatEn(resultCount)}</strong>\n {total !== undefined ? ` of ${formatEn(total)}` : \"\"} {unitLabel}\n </>\n )\n};\n\nexport const searchFilterBarVi: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${formatVi(count)} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{formatVi(resultCount)}</strong>\n {total !== undefined ? ` / ${formatVi(total)}` : \"\"} {unitLabel}\n </>\n )\n};\n"],"mappings":"AAyBI,mBACE,KADF;AAVJ,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,IAAI;AAC7D,MAAM,WAAW,CAAC,UAAkB,MAAM,eAAe,OAAO;AAEzD,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,SAAS,KAAK,CAAC,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAChF,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,mBAAS,WAAW,GAAE;AAAA,IACnF,UAAU,SAAY,OAAO,SAAS,KAAK,CAAC,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KACzD;AAEJ;AAEO,MAAM,oBAA2C;AAAA,EACtD,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,SAAS,KAAK,CAAC;AAAA,EAC1C,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,mBAAS,WAAW,GAAE;AAAA,IACnF,UAAU,SAAY,MAAM,SAAS,KAAK,CAAC,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KACxD;AAEJ;","names":[]}
1
+ {"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: \"Xoá từ khoá\",\n filterCount: (count) => `${formatVi(count)} bộ lọc`,\n clearFilters: \"Xoá 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":[]}
@@ -16,7 +16,7 @@ const timePickerVi = {
16
16
  am: "SA",
17
17
  pm: "CH",
18
18
  openPicker: "M\u1EDF b\u1EA3ng ch\u1ECDn gi\u1EDD",
19
- clear: "X\xF3a"
19
+ clear: "Xo\xE1"
20
20
  };
21
21
  export {
22
22
  timePickerEn,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/time-picker.ts"],"sourcesContent":["import type { TimePickerLabels } from \"../../components/ui/time-picker-constants.js\";\n\n/**\n * Chữ của `TimePicker`. Hình dạng + chú thích từng khoá: `TimePickerLabels`. Bảng chỉ lo CHỮ —\n * đồng hồ 12 hay 24 giờ theo `locale` (xem prop `locale` của `TimePicker`).\n */\nexport const timePickerEn: TimePickerLabels = {\n hour: \"Hour\",\n minute: \"Minute\",\n second: \"Second\",\n period: \"AM/PM\",\n am: \"AM\",\n pm: \"PM\",\n openPicker: \"Open time picker\",\n clear: \"Clear\"\n};\n\n/**\n * `SA`/`CH` là đúng cách viết tắt buổi của tiếng Việt theo CLDR — cũng là thứ\n * `Intl.DateTimeFormat(\"vi\", { hour12: true })` in ra — không phải bản dịch tự đặt. Chỉ HIỆN khi\n * app ép đồng hồ 12 giờ bằng prop `locale`: `vi` mặc định chạy 24 giờ, ô và cột buổi tự ẩn.\n */\nexport const timePickerVi: TimePickerLabels = {\n hour: \"Giờ\",\n minute: \"Phút\",\n second: \"Giây\",\n period: \"Buổi\",\n am: \"SA\",\n pm: \"CH\",\n openPicker: \"Mở bảng chọn giờ\",\n clear: \"Xóa\"\n};\n"],"mappings":"AAMO,MAAM,eAAiC;AAAA,EAC5C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,OAAO;AACT;AAOO,MAAM,eAAiC;AAAA,EAC5C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,OAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/time-picker.ts"],"sourcesContent":["import type { TimePickerLabels } from \"../../components/ui/time-picker-constants.js\";\n\n/**\n * Chữ của `TimePicker`. Hình dạng + chú thích từng khoá: `TimePickerLabels`. Bảng chỉ lo CHỮ —\n * đồng hồ 12 hay 24 giờ theo `locale` (xem prop `locale` của `TimePicker`).\n */\nexport const timePickerEn: TimePickerLabels = {\n hour: \"Hour\",\n minute: \"Minute\",\n second: \"Second\",\n period: \"AM/PM\",\n am: \"AM\",\n pm: \"PM\",\n openPicker: \"Open time picker\",\n clear: \"Clear\"\n};\n\n/**\n * `SA`/`CH` là đúng cách viết tắt buổi của tiếng Việt theo CLDR — cũng là thứ\n * `Intl.DateTimeFormat(\"vi\", { hour12: true })` in ra — không phải bản dịch tự đặt. Chỉ HIỆN khi\n * app ép đồng hồ 12 giờ bằng prop `locale`: `vi` mặc định chạy 24 giờ, ô và cột buổi tự ẩn.\n */\nexport const timePickerVi: TimePickerLabels = {\n hour: \"Giờ\",\n minute: \"Phút\",\n second: \"Giây\",\n period: \"Buổi\",\n am: \"SA\",\n pm: \"CH\",\n openPicker: \"Mở bảng chọn giờ\",\n clear: \"Xoá\"\n};\n"],"mappings":"AAMO,MAAM,eAAiC;AAAA,EAC5C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,OAAO;AACT;AAOO,MAAM,eAAiC;AAAA,EAC5C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,OAAO;AACT;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  interface AppLauncherPanelProps {
4
- /** "grid" = lưới 3 cột kiểu Lark/Google Workspace; "list" = danh sách dọc (menu mobile). */
4
+ /** "grid" = lưới 3 cột; "list" = danh sách dọc (menu mobile). */
5
5
  variant?: "grid" | "list";
6
6
  className?: string;
7
7
  /** Gọi sau khi người dùng chọn một app — dùng để đóng popover/menu mobile. */
@@ -24,7 +24,7 @@ interface AppLauncherProps extends AppLauncherPanelProps {
24
24
  /**
25
25
  * **Trục 2 — chuyển sản phẩm trong cùng workspace.**
26
26
  *
27
- * Nút lưới 3×3 ở header (kiểu Lark / Google Workspace) mở popover lưới ô app.
27
+ * Nút lưới 3×3 ở header mở popover lưới ô app.
28
28
  * Trạng thái mở nằm ở `ShellProvider` để phím tắt `Cmd/Ctrl + Shift + K` bật được.
29
29
  * App chưa có trong gói hiện ô khoá và gọi `onUpsell(appId)` thay vì điều hướng.
30
30
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/app-launcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { LayoutGrid, Lock } from \"lucide-react\";\nimport { type KeyboardEvent, useRef } from \"react\";\nimport { IconAvatar } from \"../components/composite/icon-avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { AppDescriptor } from \"./types.js\";\nimport { getEntitledApps, getLockedApps } from \"./utils.js\";\n\n/** Đánh dấu ô app để điều hướng bàn phím tìm được, kể cả ô render bằng LinkComponent của app. */\nconst TILE_ATTR = \"data-app-tile\";\n\nexport interface AppLauncherPanelProps {\n /** \"grid\" = lưới 3 cột kiểu Lark/Google Workspace; \"list\" = danh sách dọc (menu mobile). */\n variant?: \"grid\" | \"list\";\n className?: string;\n /** Gọi sau khi người dùng chọn một app — dùng để đóng popover/menu mobile. */\n onSelected?: () => void;\n}\n\n/**\n * Nội dung của AppLauncher, tách riêng để `MobileMenu` nhúng thẳng (không popover).\n *\n * Điều hướng bàn phím: ←/→/↑/↓ chạy giữa các ô (xuyên cả hai nhóm như một lưới liền),\n * Home/End nhảy đầu-cuối; Enter/Space để chính thẻ button/link xử lý.\n * Vị trí ô đọc từ DOM chứ không giữ mảng ref: ô \"đã mở khoá\" có thể là `LinkComponent`\n * do app cung cấp — không chắc component đó nhận `ref`.\n */\nexport function AppLauncherPanel({ variant = \"grid\", className, onSelected }: AppLauncherPanelProps) {\n const { apps, currentApp, onAppSelect, onUpsell, labels } = useShell();\n\n const entitled = getEntitledApps(apps);\n const locked = getLockedApps(apps);\n const total = entitled.length + locked.length;\n\n const rootRef = useRef<HTMLDivElement>(null);\n const columns = variant === \"grid\" ? 3 : 1;\n\n const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n const root = rootRef.current;\n if (!root) return;\n const tiles = Array.from(root.querySelectorAll<HTMLElement>(`[${TILE_ATTR}]`));\n const index = tiles.indexOf(event.currentTarget);\n if (index < 0) return;\n\n const moves: Record<string, number | undefined> = {\n ArrowRight: index + 1,\n ArrowLeft: index - 1,\n ArrowDown: index + columns,\n ArrowUp: index - columns,\n Home: 0,\n End: tiles.length - 1\n };\n const next = moves[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n tiles[Math.max(0, Math.min(tiles.length - 1, next))]?.focus();\n };\n\n const renderTile = (app: AppDescriptor) => {\n const isLocked = app.entitled === false;\n const isCurrent = app.id === currentApp?.id;\n const Icon = app.icon;\n\n // Ô KHOÁ không tô `opacity-60` nữa. Nó KHÔNG phải control vô hiệu — bấm được và gọi\n // `onUpsell(appId)` — nên không hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3/1.4.11.\n // Với 60% độ mờ thì mô tả app tụt còn 2,41:1 (light) / 2,74:1 (dark) ✗ và icon trong ô\n // `bg-muted` còn 2,31:1 / 2,60:1 ✗, chỉ phục hồi khi hover — tức người không rê chuột thì\n // không đọc được. Bỏ độ mờ: mô tả `text-muted-foreground` 5,23:1 (light) / 5,35:1 (dark) ✓,\n // icon trên `bg-muted` 4,89:1 / 4,86:1 ✓. Tín hiệu \"chưa mở khoá\" không mất gì: ô icon vẫn\n // xám (thay vì tô màu app), vẫn có huy hiệu ổ khoá, vẫn có `title`/`aria-label` nói rõ.\n const tileClass = cn(\n \"group relative flex rounded-md text-left transition-colors duration-150 ease-out\",\n variant === \"grid\" ? \"flex-col items-start gap-1.5 p-2.5\" : \"items-center gap-2.5 p-2\",\n isLocked ? \"hover:bg-muted\" : \"hover:bg-accent\",\n isCurrent && \"bg-accent\"\n );\n\n const body = (\n <>\n {/*\n * ── Ô icon: `IconAvatar`, màu TỰ SUY theo `id` ────────────────────────────────────\n * Từ 1.10.0 ô này thôi tô bằng `--app-accent`. Lý do không phải thẩm mỹ: bảng accent cũ\n * bắt mỗi app phải được ai đó gán màu tay, nên sản phẩm thứ sáu hoặc trùng màu với sản\n * phẩm khác, hoặc phải chờ một lần sửa mã. `IconAvatar` suy màu từ `id` bằng bảng tone\n * dùng chung với `LetterAvatar`, nên thêm app là nó tự có màu — và cùng màu ấy ở email,\n * canvas, PDF vì `getAvatarToneColors` đọc cùng bảng.\n *\n * ⚠ `seed={app.id}`, KHÔNG phải `app.name`: tên đổi theo ngôn ngữ giao diện. Đo được\n * 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên.\n *\n * ⚠ Nhánh KHOÁ vẫn tô xám bằng `className`, không bằng một tone: \"chưa mở khoá\" là\n * trạng thái, mà bảng tone là màu ĐỊNH DANH — mượn một tone làm tín hiệu trạng thái là\n * đúng cái nhầm vai mà `lib/avatar-tone.ts` cấm. `cn()` cho `bg-muted` thắng tone nhờ\n * tailwind-merge; cả hai lớp đều ĐẶC nên icon không lộ vệt nối.\n */}\n <span className=\"relative flex shrink-0\">\n <IconAvatar\n name={app.name}\n seed={app.id}\n icon={Icon}\n tone={app.tone}\n size={variant === \"grid\" ? \"md\" : \"sm\"}\n className={cn(isLocked && \"bg-muted text-muted-foreground\")}\n />\n {isLocked ? (\n <span className=\"absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-popover\">\n <Lock className=\"size-2.5\" />\n </span>\n ) : null}\n </span>\n\n {/*\n * `w-full` ở ô lưới là thứ THẬT SỰ bó được chữ — không phải `min-w-0`.\n *\n * Ô lưới là `flex-col items-start` (xem `tileClass`), nên theo trục ngang cột này lấy\n * `fit-content`: nó RỘNG RA THEO TÊN thay vì bằng ô. Mọi phép cắt bên trong vì thế không\n * bao giờ có gì để bám vào. Đo ở app thật, ô 109px: tên một từ dài cho cột 272px và tràn\n * 174px, `scrollWidth === clientWidth` — `line-clamp` được đặt nhưng chưa từng chạy.\n * Thêm `min-w-0` vào hàng bên dưới KHÔNG đổi được số nào (đo: y hệt bản hỏng); `w-full`\n * đưa cột về đúng 89px và từ đó phép cắt mới có hiệu lực.\n *\n * Biến thể `list` không dính: ô đó là hàng ngang nên `flex-1` đã ghim bề rộng sẵn.\n */}\n <span className={cn(\"flex min-w-0 flex-col\", variant === \"grid\" ? \"w-full\" : \"flex-1\")}>\n {/*\n * `items-start` chứ không `items-center` khi tên có thể xuống hai dòng: căn giữa đẩy\n * badge xuống giữa khe hai dòng (đo được 10,6px), xa hẳn dòng chữ nó đang chú thích.\n * Tên một dòng thì hai cách lệch nhau 0,8px — không nhìn thấy.\n */}\n <span className={cn(\"flex min-w-0 gap-1.5\", variant === \"grid\" ? \"items-start\" : \"items-center\")}>\n <span\n className={cn(\n \"text-sm font-medium text-foreground\",\n /*\n * Lưới: tên ứng dụng dài hơn ô là chuyện thường (\"Tài khoản Comitor\" ở ô 109px),\n * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên\n * ô cao thêm một dòng không làm hàng bị so le.\n *\n * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`\n * chỉ đặt được dấu \"…\" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có\n * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị\n * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,\n * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong\n * từ thì nó thành hai dòng và dấu \"…\" quay lại.\n *\n * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều\n * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`\n * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.\n */\n variant === \"grid\" ? \"line-clamp-2 break-words\" : \"truncate\"\n )}\n >\n {app.name}\n </span>\n {app.badge ? (\n /*\n * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,\n * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng\n * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).\n *\n * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell\n * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để\n * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào\n * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.\n */\n <Badge variant=\"destructive\" size=\"sm\" className=\"shrink-0\">\n {app.badge}\n </Badge>\n ) : null}\n </span>\n {app.description ? (\n <span\n className={cn(\"text-[11px] leading-snug text-muted-foreground\", variant === \"grid\" && \"line-clamp-2\")}\n >\n {app.description}\n </span>\n ) : null}\n </span>\n </>\n );\n\n // Chưa mở khoá → không điều hướng, chỉ báo cho app biết để mời nâng gói.\n if (isLocked) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n title={labels.lockedHint}\n aria-label={`${app.name} — ${labels.locked}`}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onUpsell?.(app.id);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n // Có `onAppSelect` → app tự lo điều hướng (đổi subdomain, nạp trước dữ liệu…);\n // không có → dùng link thật để giữ chuột-giữa / mở tab mới / trình đọc màn hình.\n if (onAppSelect) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onAppSelect(app);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n return (\n <ShellNavLink\n key={app.id}\n href={app.href}\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => onSelected?.()}\n >\n {body}\n </ShellNavLink>\n );\n };\n\n if (total === 0) return null;\n\n /*\n * `auto-rows-fr` vá thứ duy nhất mà việc cho tên xuống hai dòng làm xấu đi: TRONG một hàng thì\n * các ô đã bằng nhau sẵn (`align-items: stretch` là mặc định của Grid), nhưng GIỮA các hàng thì\n * không — hàng có một tên hai dòng cao 116px trong khi hàng kia 97px. `fr` cho mọi hàng chung\n * một chiều cao.\n */\n const groupClass = variant === \"grid\" ? \"grid auto-rows-fr grid-cols-3 gap-1\" : \"flex flex-col gap-0.5\";\n\n return (\n <div ref={rootRef} className={cn(\"flex flex-col gap-3\", className)}>\n {entitled.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.myApps}\n </p>\n <div className={groupClass}>{entitled.map(renderTile)}</div>\n </section>\n ) : null}\n\n {locked.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.discoverApps}\n </p>\n <div className={groupClass}>{locked.map(renderTile)}</div>\n </section>\n ) : null}\n </div>\n );\n}\n\nexport interface AppLauncherProps extends AppLauncherPanelProps {\n /** Bỏ nút bấm + Popover, render thẳng panel (MobileMenu dùng cách này). */\n inline?: boolean;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 2 — chuyển sản phẩm trong cùng workspace.**\n *\n * Nút lưới 3×3 ở header (kiểu Lark / Google Workspace) mở popover lưới ô app.\n * Trạng thái mở nằm ở `ShellProvider` để phím tắt `Cmd/Ctrl + Shift + K` bật được.\n * App chưa có trong gói hiện ô khoá và gọi `onUpsell(appId)` thay vì điều hướng.\n */\nexport function AppLauncher({\n className,\n variant = \"grid\",\n inline = false,\n align = \"end\",\n onSelected\n}: AppLauncherProps) {\n const { apps, appLauncherOpen, setAppLauncherOpen, labels } = useShell();\n\n if (apps.length === 0) return null;\n\n if (inline) {\n return <AppLauncherPanel variant={variant} className={className} onSelected={onSelected} />;\n }\n\n return (\n <Popover open={appLauncherOpen} onOpenChange={setAppLauncherOpen}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.appLauncherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n appLauncherOpen && \"bg-muted text-foreground\",\n className\n )}\n >\n <LayoutGrid className=\"size-4\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{labels.apps}</TooltipContent>\n </Tooltip>\n\n <PopoverContent align={align} sideOffset={8} className=\"w-[22rem] p-2\">\n <AppLauncherPanel\n variant={variant}\n onSelected={() => {\n setAppLauncherOpen(false);\n onSelected?.();\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/app-launcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { LayoutGrid, Lock } from \"lucide-react\";\nimport { type KeyboardEvent, useRef } from \"react\";\nimport { IconAvatar } from \"../components/composite/icon-avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { AppDescriptor } from \"./types.js\";\nimport { getEntitledApps, getLockedApps } from \"./utils.js\";\n\n/** Đánh dấu ô app để điều hướng bàn phím tìm được, kể cả ô render bằng LinkComponent của app. */\nconst TILE_ATTR = \"data-app-tile\";\n\nexport interface AppLauncherPanelProps {\n /** \"grid\" = lưới 3 cột; \"list\" = danh sách dọc (menu mobile). */\n variant?: \"grid\" | \"list\";\n className?: string;\n /** Gọi sau khi người dùng chọn một app — dùng để đóng popover/menu mobile. */\n onSelected?: () => void;\n}\n\n/**\n * Nội dung của AppLauncher, tách riêng để `MobileMenu` nhúng thẳng (không popover).\n *\n * Điều hướng bàn phím: ←/→/↑/↓ chạy giữa các ô (xuyên cả hai nhóm như một lưới liền),\n * Home/End nhảy đầu-cuối; Enter/Space để chính thẻ button/link xử lý.\n * Vị trí ô đọc từ DOM chứ không giữ mảng ref: ô \"đã mở khoá\" có thể là `LinkComponent`\n * do app cung cấp — không chắc component đó nhận `ref`.\n */\nexport function AppLauncherPanel({ variant = \"grid\", className, onSelected }: AppLauncherPanelProps) {\n const { apps, currentApp, onAppSelect, onUpsell, labels } = useShell();\n\n const entitled = getEntitledApps(apps);\n const locked = getLockedApps(apps);\n const total = entitled.length + locked.length;\n\n const rootRef = useRef<HTMLDivElement>(null);\n const columns = variant === \"grid\" ? 3 : 1;\n\n const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n const root = rootRef.current;\n if (!root) return;\n const tiles = Array.from(root.querySelectorAll<HTMLElement>(`[${TILE_ATTR}]`));\n const index = tiles.indexOf(event.currentTarget);\n if (index < 0) return;\n\n const moves: Record<string, number | undefined> = {\n ArrowRight: index + 1,\n ArrowLeft: index - 1,\n ArrowDown: index + columns,\n ArrowUp: index - columns,\n Home: 0,\n End: tiles.length - 1\n };\n const next = moves[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n tiles[Math.max(0, Math.min(tiles.length - 1, next))]?.focus();\n };\n\n const renderTile = (app: AppDescriptor) => {\n const isLocked = app.entitled === false;\n const isCurrent = app.id === currentApp?.id;\n const Icon = app.icon;\n\n // Ô KHOÁ không tô `opacity-60` nữa. Nó KHÔNG phải control vô hiệu — bấm được và gọi\n // `onUpsell(appId)` — nên không hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3/1.4.11.\n // Với 60% độ mờ thì mô tả app tụt còn 2,41:1 (light) / 2,74:1 (dark) ✗ và icon trong ô\n // `bg-muted` còn 2,31:1 / 2,60:1 ✗, chỉ phục hồi khi hover — tức người không rê chuột thì\n // không đọc được. Bỏ độ mờ: mô tả `text-muted-foreground` 5,23:1 (light) / 5,35:1 (dark) ✓,\n // icon trên `bg-muted` 4,89:1 / 4,86:1 ✓. Tín hiệu \"chưa mở khoá\" không mất gì: ô icon vẫn\n // xám (thay vì tô màu app), vẫn có huy hiệu ổ khoá, vẫn có `title`/`aria-label` nói rõ.\n const tileClass = cn(\n \"group relative flex rounded-md text-left transition-colors duration-150 ease-out\",\n variant === \"grid\" ? \"flex-col items-start gap-1.5 p-2.5\" : \"items-center gap-2.5 p-2\",\n isLocked ? \"hover:bg-muted\" : \"hover:bg-accent\",\n isCurrent && \"bg-accent\"\n );\n\n const body = (\n <>\n {/*\n * ── Ô icon: `IconAvatar`, màu TỰ SUY theo `id` ────────────────────────────────────\n * Từ 1.10.0 ô này thôi tô bằng `--app-accent`. Lý do không phải thẩm mỹ: bảng accent cũ\n * bắt mỗi app phải được ai đó gán màu tay, nên sản phẩm thứ sáu hoặc trùng màu với sản\n * phẩm khác, hoặc phải chờ một lần sửa mã. `IconAvatar` suy màu từ `id` bằng bảng tone\n * dùng chung với `LetterAvatar`, nên thêm app là nó tự có màu — và cùng màu ấy ở email,\n * canvas, PDF vì `getAvatarToneColors` đọc cùng bảng.\n *\n * ⚠ `seed={app.id}`, KHÔNG phải `app.name`: tên đổi theo ngôn ngữ giao diện. Đo được\n * 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên.\n *\n * ⚠ Nhánh KHOÁ vẫn tô xám bằng `className`, không bằng một tone: \"chưa mở khoá\" là\n * trạng thái, mà bảng tone là màu ĐỊNH DANH — mượn một tone làm tín hiệu trạng thái là\n * đúng cái nhầm vai mà `lib/avatar-tone.ts` cấm. `cn()` cho `bg-muted` thắng tone nhờ\n * tailwind-merge; cả hai lớp đều ĐẶC nên icon không lộ vệt nối.\n */}\n <span className=\"relative flex shrink-0\">\n <IconAvatar\n name={app.name}\n seed={app.id}\n icon={Icon}\n tone={app.tone}\n size={variant === \"grid\" ? \"md\" : \"sm\"}\n className={cn(isLocked && \"bg-muted text-muted-foreground\")}\n />\n {isLocked ? (\n <span className=\"absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-popover\">\n <Lock className=\"size-2.5\" />\n </span>\n ) : null}\n </span>\n\n {/*\n * `w-full` ở ô lưới là thứ THẬT SỰ bó được chữ — không phải `min-w-0`.\n *\n * Ô lưới là `flex-col items-start` (xem `tileClass`), nên theo trục ngang cột này lấy\n * `fit-content`: nó RỘNG RA THEO TÊN thay vì bằng ô. Mọi phép cắt bên trong vì thế không\n * bao giờ có gì để bám vào. Đo ở app thật, ô 109px: tên một từ dài cho cột 272px và tràn\n * 174px, `scrollWidth === clientWidth` — `line-clamp` được đặt nhưng chưa từng chạy.\n * Thêm `min-w-0` vào hàng bên dưới KHÔNG đổi được số nào (đo: y hệt bản hỏng); `w-full`\n * đưa cột về đúng 89px và từ đó phép cắt mới có hiệu lực.\n *\n * Biến thể `list` không dính: ô đó là hàng ngang nên `flex-1` đã ghim bề rộng sẵn.\n */}\n <span className={cn(\"flex min-w-0 flex-col\", variant === \"grid\" ? \"w-full\" : \"flex-1\")}>\n {/*\n * `items-start` chứ không `items-center` khi tên có thể xuống hai dòng: căn giữa đẩy\n * badge xuống giữa khe hai dòng (đo được 10,6px), xa hẳn dòng chữ nó đang chú thích.\n * Tên một dòng thì hai cách lệch nhau 0,8px — không nhìn thấy.\n */}\n <span className={cn(\"flex min-w-0 gap-1.5\", variant === \"grid\" ? \"items-start\" : \"items-center\")}>\n <span\n className={cn(\n \"text-sm font-medium text-foreground\",\n /*\n * Lưới: tên ứng dụng dài hơn ô là chuyện thường (\"Tài khoản Comitor\" ở ô 109px),\n * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên\n * ô cao thêm một dòng không làm hàng bị so le.\n *\n * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`\n * chỉ đặt được dấu \"…\" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có\n * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị\n * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,\n * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong\n * từ thì nó thành hai dòng và dấu \"…\" quay lại.\n *\n * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều\n * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`\n * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.\n */\n variant === \"grid\" ? \"line-clamp-2 break-words\" : \"truncate\"\n )}\n >\n {app.name}\n </span>\n {app.badge ? (\n /*\n * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,\n * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng\n * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).\n *\n * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell\n * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để\n * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào\n * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.\n */\n <Badge variant=\"destructive\" size=\"sm\" className=\"shrink-0\">\n {app.badge}\n </Badge>\n ) : null}\n </span>\n {app.description ? (\n <span\n className={cn(\"text-[11px] leading-snug text-muted-foreground\", variant === \"grid\" && \"line-clamp-2\")}\n >\n {app.description}\n </span>\n ) : null}\n </span>\n </>\n );\n\n // Chưa mở khoá → không điều hướng, chỉ báo cho app biết để mời nâng gói.\n if (isLocked) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n title={labels.lockedHint}\n aria-label={`${app.name} — ${labels.locked}`}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onUpsell?.(app.id);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n // Có `onAppSelect` → app tự lo điều hướng (đổi subdomain, nạp trước dữ liệu…);\n // không có → dùng link thật để giữ chuột-giữa / mở tab mới / trình đọc màn hình.\n if (onAppSelect) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onAppSelect(app);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n return (\n <ShellNavLink\n key={app.id}\n href={app.href}\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => onSelected?.()}\n >\n {body}\n </ShellNavLink>\n );\n };\n\n if (total === 0) return null;\n\n /*\n * `auto-rows-fr` vá thứ duy nhất mà việc cho tên xuống hai dòng làm xấu đi: TRONG một hàng thì\n * các ô đã bằng nhau sẵn (`align-items: stretch` là mặc định của Grid), nhưng GIỮA các hàng thì\n * không — hàng có một tên hai dòng cao 116px trong khi hàng kia 97px. `fr` cho mọi hàng chung\n * một chiều cao.\n */\n const groupClass = variant === \"grid\" ? \"grid auto-rows-fr grid-cols-3 gap-1\" : \"flex flex-col gap-0.5\";\n\n return (\n <div ref={rootRef} className={cn(\"flex flex-col gap-3\", className)}>\n {entitled.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.myApps}\n </p>\n <div className={groupClass}>{entitled.map(renderTile)}</div>\n </section>\n ) : null}\n\n {locked.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.discoverApps}\n </p>\n <div className={groupClass}>{locked.map(renderTile)}</div>\n </section>\n ) : null}\n </div>\n );\n}\n\nexport interface AppLauncherProps extends AppLauncherPanelProps {\n /** Bỏ nút bấm + Popover, render thẳng panel (MobileMenu dùng cách này). */\n inline?: boolean;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 2 — chuyển sản phẩm trong cùng workspace.**\n *\n * Nút lưới 3×3 ở header mở popover lưới ô app.\n * Trạng thái mở nằm ở `ShellProvider` để phím tắt `Cmd/Ctrl + Shift + K` bật được.\n * App chưa có trong gói hiện ô khoá và gọi `onUpsell(appId)` thay vì điều hướng.\n */\nexport function AppLauncher({\n className,\n variant = \"grid\",\n inline = false,\n align = \"end\",\n onSelected\n}: AppLauncherProps) {\n const { apps, appLauncherOpen, setAppLauncherOpen, labels } = useShell();\n\n if (apps.length === 0) return null;\n\n if (inline) {\n return <AppLauncherPanel variant={variant} className={className} onSelected={onSelected} />;\n }\n\n return (\n <Popover open={appLauncherOpen} onOpenChange={setAppLauncherOpen}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.appLauncherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n appLauncherOpen && \"bg-muted text-foreground\",\n className\n )}\n >\n <LayoutGrid className=\"size-4\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{labels.apps}</TooltipContent>\n </Tooltip>\n\n <PopoverContent align={align} sideOffset={8} className=\"w-[22rem] p-2\">\n <AppLauncherPanel\n variant={variant}\n onSelected={() => {\n setAppLauncherOpen(false);\n onSelected?.();\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
@@ -15,7 +15,7 @@ type AppShellLayout = "sidebar-first" | "header-first";
15
15
  interface AppShellProps extends ShellProviderProps {
16
16
  /**
17
17
  * "sidebar-first" (mặc định) — sidebar cao hết màn hình, header nằm bên phải nó
18
- * (kiểu Linear/Lark: WorkspaceSwitcher ở đỉnh sidebar).
18
+ * (WorkspaceSwitcher ở đỉnh sidebar).
19
19
  * "header-first" — header full width trên cùng, sidebar nằm dưới
20
20
  * (WorkspaceSwitcher chuyển lên header).
21
21
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/app-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { ComitorLocaleProvider } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppHeader, type AppHeaderProps } from \"./app-header.js\";\nimport { MobileMenu, type MobileMenuProps } from \"./mobile-menu.js\";\nimport { ShellProvider, type ShellProviderProps, useShell } from \"./shell-context.js\";\nimport { Sidebar, type SidebarProps } from \"./sidebar.js\";\nimport { appAccentStyle } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport type AppShellLayout = \"sidebar-first\" | \"header-first\";\n\nexport interface AppShellProps extends ShellProviderProps {\n /**\n * \"sidebar-first\" (mặc định) — sidebar cao hết màn hình, header nằm bên phải nó\n * (kiểu Linear/Lark: WorkspaceSwitcher ở đỉnh sidebar).\n * \"header-first\" — header full width trên cùng, sidebar nằm dưới\n * (WorkspaceSwitcher chuyển lên header).\n */\n layout?: AppShellLayout;\n /** Ẩn sidebar hoàn toàn (app một khu, chỉ có header). */\n showSidebar?: boolean;\n headerProps?: AppHeaderProps;\n sidebarProps?: SidebarProps;\n mobileMenuProps?: MobileMenuProps;\n /**\n * Slot Command Palette — app tự render (dữ liệu tìm kiếm là của app),\n * đọc/ghi trạng thái qua `useShell().commandPaletteOpen`.\n */\n commandPaletteSlot?: ReactNode;\n /**\n * Ngôn ngữ của cả cây shell (mã BCP 47: `\"vi\"`, `\"en\"`, `\"en-GB\"`…) — TIỆN ÍCH, không phải chỗ\n * khuyên dùng.\n *\n * Chỗ đúng là `<ComitorLocaleProvider locale={…}>` ở ROOT layout: nó phủ cả những trang KHÔNG\n * nằm trong shell (đăng nhập, trang lỗi, trang công khai) và mọi component của gói đọc theo nó.\n * Prop này chỉ dành cho app không có root layout riêng để đặt provider.\n *\n * Có truyền → `AppShell` bọc cây của nó trong `ComitorLocaleProvider locale={locale}`. Bỏ trống →\n * không bọc gì, mọi thứ đọc provider gần nhất bên ngoài (không có thì tiếng Anh).\n *\n * ⚠ Provider dựng ở đây là provider MỚI: nó KHÔNG kế thừa `messages` (ghi đè chữ) hay\n * `dateFnsLocale` của provider bên ngoài. Cần những thứ đó thì đặt provider ở root layout.\n * ⚠ Đổi qua lại giữa có và không truyền prop này đổi hình dạng cây — cả shell dựng lại từ đầu\n * (mất trạng thái). Giữ nó luôn có hoặc luôn vắng.\n */\n locale?: string;\n className?: string;\n contentClassName?: string;\n}\n\n/**\n * Khung ứng dụng lắp sẵn: `ShellProvider` + `Sidebar` + `AppHeader` + `<main>` + `MobileMenu`.\n *\n * ⚠ Bọc app bằng `<ThemeProvider>` ở **root layout**, NGOÀI `<AppShell>` — next-themes\n * cần nằm trên cùng để tránh nháy theme lúc hydrate. `<ComitorLocaleProvider>` cũng nên ở đó\n * (ngôn ngữ của cả gói, kể cả trang ngoài shell); prop `locale` chỉ là lối tắt.\n *\n * Có `\"use client\"`: mọi callback (`onLogout`, `onWorkspaceChange`…) là hàm nên nơi\n * render `<AppShell>` phải là client component.\n */\nexport function AppShell({\n layout = \"sidebar-first\",\n showSidebar = true,\n headerProps,\n sidebarProps,\n mobileMenuProps,\n commandPaletteSlot,\n locale,\n className,\n contentClassName,\n children,\n ...providerProps\n}: AppShellProps) {\n const shell = (\n <ShellProvider {...providerProps}>\n <AppShellFrame\n layout={layout}\n showSidebar={showSidebar}\n headerProps={headerProps}\n sidebarProps={sidebarProps}\n mobileMenuProps={mobileMenuProps}\n commandPaletteSlot={commandPaletteSlot}\n className={className}\n contentClassName={contentClassName}\n >\n {children}\n </AppShellFrame>\n </ShellProvider>\n );\n\n // Provider ở NGOÀI `ShellProvider`: chính `ShellProvider` đọc namespace `shell` để dựng `labels`.\n return locale === undefined ? shell : <ComitorLocaleProvider locale={locale}>{shell}</ComitorLocaleProvider>;\n}\n\ntype AppShellFrameProps = Pick<\n AppShellProps,\n | \"layout\"\n | \"showSidebar\"\n | \"headerProps\"\n | \"sidebarProps\"\n | \"mobileMenuProps\"\n | \"commandPaletteSlot\"\n | \"className\"\n | \"contentClassName\"\n> & { children: ReactNode };\n\n/**\n * Phần khung thật sự — tách khỏi `AppShell` vì cần `useShell()` (phải ở TRONG provider)\n * để gán `--app-accent` của app đang mở cho cả cây.\n */\nfunction AppShellFrame({\n layout = \"sidebar-first\",\n showSidebar = true,\n headerProps,\n sidebarProps,\n mobileMenuProps,\n commandPaletteSlot,\n className,\n contentClassName,\n children\n}: AppShellFrameProps) {\n const { currentApp } = useShell();\n const headerFirst = layout === \"header-first\";\n\n // Ở header-first, bộ chuyển workspace chuyển lên header — nếu để cả hai chỗ\n // thì người dùng thấy hai nút giống hệt nhau.\n // Spread TRƯỚC rồi mới đặt giá trị mặc định, nếu không `{...headerProps}` sẽ ghi đè\n // bằng `undefined` và giá trị mặc định tính ở trên thành công cốc.\n const header = (\n <AppHeader\n {...headerProps}\n showSidebarToggle={headerProps?.showSidebarToggle ?? headerFirst}\n logo={headerProps?.logo ?? (headerFirst ? <WorkspaceSwitcher className=\"w-56 shrink-0\" /> : undefined)}\n />\n );\n\n const sidebar = showSidebar ? (\n <Sidebar\n {...sidebarProps}\n showWorkspaceSwitcher={sidebarProps?.showWorkspaceSwitcher ?? !headerFirst}\n showCollapseButton={sidebarProps?.showCollapseButton ?? !headerFirst}\n />\n ) : null;\n\n /*\n * ─ VÌ SAO <main> PHẢI CÓ `relative` ─\n *\n * `overflow-y-auto` KHÔNG cắt phần tử `position: absolute` nếu chính nó không được định vị:\n * containing block của những phần tử đó nhảy lên tới `body`/ICB, nên chúng nằm ngoài tầm cắt\n * của `main` và **kéo dài vùng cuộn của cả TÀI LIỆU**. Khung shell là `h-dvh overflow-hidden`\n * nên phần kéo dài đó không có gì để vẽ: người dùng cuộn xuống và thấy một khoảng TRẮNG lớn\n * dưới đáy trang.\n *\n * Không phải lỗi lý thuyết — hai nguồn sinh ra nó có mặt trong chính gói này:\n * • `sr-only` của Tailwind là `position: absolute` không có `top/left`, nên nó đứng ở vị trí\n * tĩnh của mình. Trong `TablePagination`, bốn nhãn ẩn (\"Trang đầu/trước/sau/cuối\") nằm\n * trong `<Button>` — mà `Button` không `relative`.\n * • Radix để `<input>`/`<select>` gốc ẩn bằng `position: absolute` (RadioGroup, Select).\n *\n * Đo trên trang `/cong-viec` trước khi vá: `documentElement.scrollHeight` = 1531 trong khi\n * `clientHeight` = 900 → 631px trắng. Trang `/bieu-mau`: 1792 / 900 → 892px. Trang ngắn hoặc\n * trang có các phần tử đó nằm TRÊN nếp gấp thì không lộ ra — đó là lý do lỗi trông như chỉ dính\n * vài trang.\n *\n * `relative` đưa containing block về đúng `main`, và `overflow-y-auto` cắt được như mong đợi.\n * ⚠ Đừng đổi sang `contain: paint`: nó cũng chữa được nhưng kéo theo containment về sơn/bố cục\n * mà không ai cần ở đây.\n */\n const main = (\n <main className={cn(\"relative flex-1 overflow-y-auto bg-background\", contentClassName)}>{children}</main>\n );\n\n return (\n <div\n style={appAccentStyle(currentApp)}\n className={cn(\n \"flex h-dvh w-full overflow-hidden bg-background\",\n headerFirst ? \"flex-col\" : \"flex-row\",\n className\n )}\n >\n {headerFirst ? (\n <>\n {header}\n <div className=\"flex min-h-0 flex-1\">\n {sidebar}\n {main}\n </div>\n </>\n ) : (\n <>\n {sidebar}\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {header}\n {main}\n </div>\n </>\n )}\n\n <MobileMenu {...mobileMenuProps} />\n {commandPaletteSlot}\n </div>\n );\n}\n"],"mappings":";AA8EM,SA2GE,UA3GF,KA6GI,YA7GJ;AA3EN,SAAS,6BAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,iBAAsC;AAC/C,SAAS,kBAAwC;AACjD,SAAS,eAAwC,gBAAgB;AACjE,SAAS,eAAkC;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAqD3B,SAAS,SAAS;AAAA,EACvB,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,QACJ,oBAAC,iBAAe,GAAG,eACjB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA;AAAA,EACH,GACF;AAIF,SAAO,WAAW,SAAY,QAAQ,oBAAC,yBAAsB,QAAiB,iBAAM;AACtF;AAkBA,SAAS,cAAc;AAAA,EACrB,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,WAAW,IAAI,SAAS;AAChC,QAAM,cAAc,WAAW;AAM/B,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,mBAAmB,aAAa,qBAAqB;AAAA,MACrD,MAAM,aAAa,SAAS,cAAc,oBAAC,qBAAkB,WAAU,iBAAgB,IAAK;AAAA;AAAA,EAC9F;AAGF,QAAM,UAAU,cACd;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,uBAAuB,cAAc,yBAAyB,CAAC;AAAA,MAC/D,oBAAoB,cAAc,sBAAsB,CAAC;AAAA;AAAA,EAC3D,IACE;AA0BJ,QAAM,OACJ,oBAAC,UAAK,WAAW,GAAG,iDAAiD,gBAAgB,GAAI,UAAS;AAGpG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA,cAAc,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,MAEC;AAAA,sBACC,iCACG;AAAA;AAAA,UACD,qBAAC,SAAI,WAAU,uBACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,WACF,IAEA,iCACG;AAAA;AAAA,UACD,qBAAC,SAAI,WAAU,gCACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,WACF;AAAA,QAGF,oBAAC,cAAY,GAAG,iBAAiB;AAAA,QAChC;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/app-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { ComitorLocaleProvider } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppHeader, type AppHeaderProps } from \"./app-header.js\";\nimport { MobileMenu, type MobileMenuProps } from \"./mobile-menu.js\";\nimport { ShellProvider, type ShellProviderProps, useShell } from \"./shell-context.js\";\nimport { Sidebar, type SidebarProps } from \"./sidebar.js\";\nimport { appAccentStyle } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport type AppShellLayout = \"sidebar-first\" | \"header-first\";\n\nexport interface AppShellProps extends ShellProviderProps {\n /**\n * \"sidebar-first\" (mặc định) — sidebar cao hết màn hình, header nằm bên phải nó\n * (WorkspaceSwitcher ở đỉnh sidebar).\n * \"header-first\" — header full width trên cùng, sidebar nằm dưới\n * (WorkspaceSwitcher chuyển lên header).\n */\n layout?: AppShellLayout;\n /** Ẩn sidebar hoàn toàn (app một khu, chỉ có header). */\n showSidebar?: boolean;\n headerProps?: AppHeaderProps;\n sidebarProps?: SidebarProps;\n mobileMenuProps?: MobileMenuProps;\n /**\n * Slot Command Palette — app tự render (dữ liệu tìm kiếm là của app),\n * đọc/ghi trạng thái qua `useShell().commandPaletteOpen`.\n */\n commandPaletteSlot?: ReactNode;\n /**\n * Ngôn ngữ của cả cây shell (mã BCP 47: `\"vi\"`, `\"en\"`, `\"en-GB\"`…) — TIỆN ÍCH, không phải chỗ\n * khuyên dùng.\n *\n * Chỗ đúng là `<ComitorLocaleProvider locale={…}>` ở ROOT layout: nó phủ cả những trang KHÔNG\n * nằm trong shell (đăng nhập, trang lỗi, trang công khai) và mọi component của gói đọc theo nó.\n * Prop này chỉ dành cho app không có root layout riêng để đặt provider.\n *\n * Có truyền → `AppShell` bọc cây của nó trong `ComitorLocaleProvider locale={locale}`. Bỏ trống →\n * không bọc gì, mọi thứ đọc provider gần nhất bên ngoài (không có thì tiếng Anh).\n *\n * ⚠ Provider dựng ở đây là provider MỚI: nó KHÔNG kế thừa `messages` (ghi đè chữ) hay\n * `dateFnsLocale` của provider bên ngoài. Cần những thứ đó thì đặt provider ở root layout.\n * ⚠ Đổi qua lại giữa có và không truyền prop này đổi hình dạng cây — cả shell dựng lại từ đầu\n * (mất trạng thái). Giữ nó luôn có hoặc luôn vắng.\n */\n locale?: string;\n className?: string;\n contentClassName?: string;\n}\n\n/**\n * Khung ứng dụng lắp sẵn: `ShellProvider` + `Sidebar` + `AppHeader` + `<main>` + `MobileMenu`.\n *\n * ⚠ Bọc app bằng `<ThemeProvider>` ở **root layout**, NGOÀI `<AppShell>` — next-themes\n * cần nằm trên cùng để tránh nháy theme lúc hydrate. `<ComitorLocaleProvider>` cũng nên ở đó\n * (ngôn ngữ của cả gói, kể cả trang ngoài shell); prop `locale` chỉ là lối tắt.\n *\n * Có `\"use client\"`: mọi callback (`onLogout`, `onWorkspaceChange`…) là hàm nên nơi\n * render `<AppShell>` phải là client component.\n */\nexport function AppShell({\n layout = \"sidebar-first\",\n showSidebar = true,\n headerProps,\n sidebarProps,\n mobileMenuProps,\n commandPaletteSlot,\n locale,\n className,\n contentClassName,\n children,\n ...providerProps\n}: AppShellProps) {\n const shell = (\n <ShellProvider {...providerProps}>\n <AppShellFrame\n layout={layout}\n showSidebar={showSidebar}\n headerProps={headerProps}\n sidebarProps={sidebarProps}\n mobileMenuProps={mobileMenuProps}\n commandPaletteSlot={commandPaletteSlot}\n className={className}\n contentClassName={contentClassName}\n >\n {children}\n </AppShellFrame>\n </ShellProvider>\n );\n\n // Provider ở NGOÀI `ShellProvider`: chính `ShellProvider` đọc namespace `shell` để dựng `labels`.\n return locale === undefined ? shell : <ComitorLocaleProvider locale={locale}>{shell}</ComitorLocaleProvider>;\n}\n\ntype AppShellFrameProps = Pick<\n AppShellProps,\n | \"layout\"\n | \"showSidebar\"\n | \"headerProps\"\n | \"sidebarProps\"\n | \"mobileMenuProps\"\n | \"commandPaletteSlot\"\n | \"className\"\n | \"contentClassName\"\n> & { children: ReactNode };\n\n/**\n * Phần khung thật sự — tách khỏi `AppShell` vì cần `useShell()` (phải ở TRONG provider)\n * để gán `--app-accent` của app đang mở cho cả cây.\n */\nfunction AppShellFrame({\n layout = \"sidebar-first\",\n showSidebar = true,\n headerProps,\n sidebarProps,\n mobileMenuProps,\n commandPaletteSlot,\n className,\n contentClassName,\n children\n}: AppShellFrameProps) {\n const { currentApp } = useShell();\n const headerFirst = layout === \"header-first\";\n\n // Ở header-first, bộ chuyển workspace chuyển lên header — nếu để cả hai chỗ\n // thì người dùng thấy hai nút giống hệt nhau.\n // Spread TRƯỚC rồi mới đặt giá trị mặc định, nếu không `{...headerProps}` sẽ ghi đè\n // bằng `undefined` và giá trị mặc định tính ở trên thành công cốc.\n const header = (\n <AppHeader\n {...headerProps}\n showSidebarToggle={headerProps?.showSidebarToggle ?? headerFirst}\n logo={headerProps?.logo ?? (headerFirst ? <WorkspaceSwitcher className=\"w-56 shrink-0\" /> : undefined)}\n />\n );\n\n const sidebar = showSidebar ? (\n <Sidebar\n {...sidebarProps}\n showWorkspaceSwitcher={sidebarProps?.showWorkspaceSwitcher ?? !headerFirst}\n showCollapseButton={sidebarProps?.showCollapseButton ?? !headerFirst}\n />\n ) : null;\n\n /*\n * ─ VÌ SAO <main> PHẢI CÓ `relative` ─\n *\n * `overflow-y-auto` KHÔNG cắt phần tử `position: absolute` nếu chính nó không được định vị:\n * containing block của những phần tử đó nhảy lên tới `body`/ICB, nên chúng nằm ngoài tầm cắt\n * của `main` và **kéo dài vùng cuộn của cả TÀI LIỆU**. Khung shell là `h-dvh overflow-hidden`\n * nên phần kéo dài đó không có gì để vẽ: người dùng cuộn xuống và thấy một khoảng TRẮNG lớn\n * dưới đáy trang.\n *\n * Không phải lỗi lý thuyết — hai nguồn sinh ra nó có mặt trong chính gói này:\n * • `sr-only` của Tailwind là `position: absolute` không có `top/left`, nên nó đứng ở vị trí\n * tĩnh của mình. Trong `TablePagination`, bốn nhãn ẩn (\"Trang đầu/trước/sau/cuối\") nằm\n * trong `<Button>` — mà `Button` không `relative`.\n * • Radix để `<input>`/`<select>` gốc ẩn bằng `position: absolute` (RadioGroup, Select).\n *\n * Đo trên trang `/cong-viec` trước khi vá: `documentElement.scrollHeight` = 1531 trong khi\n * `clientHeight` = 900 → 631px trắng. Trang `/bieu-mau`: 1792 / 900 → 892px. Trang ngắn hoặc\n * trang có các phần tử đó nằm TRÊN nếp gấp thì không lộ ra — đó là lý do lỗi trông như chỉ dính\n * vài trang.\n *\n * `relative` đưa containing block về đúng `main`, và `overflow-y-auto` cắt được như mong đợi.\n * ⚠ Đừng đổi sang `contain: paint`: nó cũng chữa được nhưng kéo theo containment về sơn/bố cục\n * mà không ai cần ở đây.\n */\n const main = (\n <main className={cn(\"relative flex-1 overflow-y-auto bg-background\", contentClassName)}>{children}</main>\n );\n\n return (\n <div\n style={appAccentStyle(currentApp)}\n className={cn(\n \"flex h-dvh w-full overflow-hidden bg-background\",\n headerFirst ? \"flex-col\" : \"flex-row\",\n className\n )}\n >\n {headerFirst ? (\n <>\n {header}\n <div className=\"flex min-h-0 flex-1\">\n {sidebar}\n {main}\n </div>\n </>\n ) : (\n <>\n {sidebar}\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {header}\n {main}\n </div>\n </>\n )}\n\n <MobileMenu {...mobileMenuProps} />\n {commandPaletteSlot}\n </div>\n );\n}\n"],"mappings":";AA8EM,SA2GE,UA3GF,KA6GI,YA7GJ;AA3EN,SAAS,6BAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,iBAAsC;AAC/C,SAAS,kBAAwC;AACjD,SAAS,eAAwC,gBAAgB;AACjE,SAAS,eAAkC;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAqD3B,SAAS,SAAS;AAAA,EACvB,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,QACJ,oBAAC,iBAAe,GAAG,eACjB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA;AAAA,EACH,GACF;AAIF,SAAO,WAAW,SAAY,QAAQ,oBAAC,yBAAsB,QAAiB,iBAAM;AACtF;AAkBA,SAAS,cAAc;AAAA,EACrB,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,WAAW,IAAI,SAAS;AAChC,QAAM,cAAc,WAAW;AAM/B,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,mBAAmB,aAAa,qBAAqB;AAAA,MACrD,MAAM,aAAa,SAAS,cAAc,oBAAC,qBAAkB,WAAU,iBAAgB,IAAK;AAAA;AAAA,EAC9F;AAGF,QAAM,UAAU,cACd;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,uBAAuB,cAAc,yBAAyB,CAAC;AAAA,MAC/D,oBAAoB,cAAc,sBAAsB,CAAC;AAAA;AAAA,EAC3D,IACE;AA0BJ,QAAM,OACJ,oBAAC,UAAK,WAAW,GAAG,iDAAiD,gBAAgB,GAAI,UAAS;AAGpG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA,cAAc,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,MAEC;AAAA,sBACC,iCACG;AAAA;AAAA,UACD,qBAAC,SAAI,WAAU,uBACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,WACF,IAEA,iCACG;AAAA;AAAA,UACD,qBAAC,SAAI,WAAU,gCACZ;AAAA;AAAA,YACA;AAAA,aACH;AAAA,WACF;AAAA,QAGF,oBAAC,cAAY,GAAG,iBAAiB;AAAA,QAChC;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -10,7 +10,7 @@ import { CSSProperties } from 'react';
10
10
  * • `variant="auto"` — tự đổi theo theme, giống `ComitorLockup variant="auto"`
11
11
  *
12
12
  * ⚠ **Ngoại lệ duy nhất của quy tắc "không hardcode màu"**: mã hex dưới đây là vector
13
- * lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,
13
+ * lấy nguyên từ file nguồn của brand book. KHÔNG tokenize, KHÔNG đổi màu,
14
14
  * KHÔNG đơn giản hoá path — logo phải giống hệt nhau ở mọi sản phẩm.
15
15
  *
16
16
  * File này KHÔNG có "use client": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n * Ba biến thể:\n * • `variant=\"light\"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)\n * • `variant=\"dark\"` — mặt trên trắng; dùng trên nền tối / nền ấm\n * • `variant=\"auto\"` — tự đổi theo theme, giống `ComitorLockup variant=\"auto\"`\n *\n * ⚠ **Ngoại lệ duy nhất của quy tắc \"không hardcode màu\"**: mã hex dưới đây là vector\n * lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,\n * KHÔNG đơn giản hoá path — logo phải giống hệt nhau ở mọi sản phẩm.\n *\n * File này KHÔNG có \"use client\": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.\n */\nexport type ComitorLogoVariant = \"light\" | \"dark\" | \"auto\";\n\nexport interface ComitorLogoProps {\n className?: string;\n /** Nền sáng dùng \"light\", nền tối/ấm dùng \"dark\", theo theme dùng \"auto\". */\n variant?: ComitorLogoVariant;\n /** Nội dung <title> của SVG — cũng là aria-label. */\n title?: string;\n style?: CSSProperties;\n}\n\nexport function ComitorLogo({ className, variant = \"light\", title = \"Comitor\", style }: ComitorLogoProps) {\n /*\n * `auto` PHẢI vẽ hai bản rồi ẩn/hiện bằng `dark:`, không thể là một SVG đổi màu: mã hex nằm\n * thẳng trong `fill` của từng path (ngoại lệ có chủ ý của quy tắc \"không hardcode màu\" — vector\n * chép nguyên từ brand book), mà `dark:` không với tới thuộc tính `fill`. Đây đúng cách\n * `ComitorLockup variant=\"auto\"` đang làm với chính component này; đưa vào đây để app không phải\n * chép lại cặp `dark:hidden` / `hidden dark:block` ở mỗi chỗ dùng logo trần.\n */\n if (variant === \"auto\") {\n return (\n <>\n <ComitorLogo variant=\"light\" className={cn(className, \"dark:hidden\")} title={title} style={style} />\n <ComitorLogo variant=\"dark\" className={cn(className, \"hidden dark:block\")} title={title} style={style} />\n </>\n );\n }\n\n const dark = variant === \"dark\";\n const c = dark\n ? {\n accent: \"#8A6800\",\n bright: \"#F0CC50\",\n rich: \"#D4A820\",\n topFace: \"#FFFFFF\",\n sphereHi: \"#F0CC50\",\n sphereLo: \"#D4A820\"\n }\n : {\n accent: \"#7A5800\",\n bright: \"#E8B824\",\n rich: \"#B88A00\",\n topFace: \"#F5D060\",\n sphereHi: \"#E8B824\",\n sphereLo: \"#B88A00\"\n };\n\n return (\n <svg\n viewBox=\"0 0 256 256\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n style={style}\n role=\"img\"\n aria-label={title}\n >\n <title>{title}</title>\n {/* Bottom interior diamond */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 142.059L78.5642 170.543L128 209.32L177.344 170.543L128 142.059Z\"\n fill={c.accent}\n />\n {/* Left-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M56.6951 86.8339L66.4352 149.502L91.1528 135.259L115.779 121.016V92.5315V64.0459L56.6951 86.8339Z\"\n fill={c.bright}\n />\n {/* Right-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M189.473 149.502L196.733 88.3046L140.13 64.0459V92.5315V121.016L164.755 135.259L189.473 149.502Z\"\n fill={c.rich}\n />\n {/* Top face / outer sphere highlight */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 0C182.857 0 229.629 34.5499 247.822 82.975C223.747 95.6553 162.917 112.195 128 128C104.569 118.811 28.3934 89.4991 9.74011 78.9317C28.9448 32.6203 74.7053 0 128 0ZM170.544 81.5968L128 106.131L85.3639 81.5968L115.779 64.046L101.445 55.7761V55.6842L80.4941 43.6468L58.9921 31.15C61.2894 29.588 63.6785 28.0259 66.1594 26.4638L88.6719 39.5118C100.709 34.1824 113.941 31.2419 128 31.2419C145.183 31.2419 161.356 35.7445 175.415 43.6468L140.13 64.046L170.544 81.5968Z\"\n fill={c.topFace}\n />\n {/* Right sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M238.817 63.9541C249.752 82.7915 256 104.66 256 128C256 198.662 198.662 256 128 256L120.557 199.03L128 128C164.939 106.682 201.878 85.3641 238.817 63.9541ZM146.929 188.095L177.344 170.544V211.251C202.981 196.089 220.991 169.35 224.207 138.2L246.811 125.152C246.72 122.303 246.536 119.455 246.26 116.698L207.207 139.211L189.473 149.502V114.401L146.929 138.935V188.095Z\"\n fill={c.sphereHi}\n />\n {/* Left sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 256C57.2464 256 0 198.662 0 128C0 104.66 6.15656 82.7915 17.0912 63.9541C54.0302 85.3641 90.9691 106.682 128 128V256ZM108.979 188.095V138.935L66.4352 114.401V149.502L31.1501 129.103C31.5176 160.804 47.1387 188.83 70.9376 206.289V232.293C73.4184 233.672 75.9917 234.958 78.5644 236.152V211.251V170.544L108.979 188.095Z\"\n fill={c.sphereLo}\n />\n </svg>\n );\n}\n"],"mappings":"AAsCM,mBACE,KADF;AArCN,SAAS,UAAU;AA2BZ,SAAS,YAAY,EAAE,WAAW,UAAU,SAAS,QAAQ,WAAW,MAAM,GAAqB;AAQxG,MAAI,YAAY,QAAQ;AACtB,WACE,iCACE;AAAA,0BAAC,eAAY,SAAQ,SAAQ,WAAW,GAAG,WAAW,aAAa,GAAG,OAAc,OAAc;AAAA,MAClG,oBAAC,eAAY,SAAQ,QAAO,WAAW,GAAG,WAAW,mBAAmB,GAAG,OAAc,OAAc;AAAA,OACzG;AAAA,EAEJ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,IAAI,OACN;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,IACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MAEZ;AAAA,4BAAC,WAAO,iBAAM;AAAA,QAEd;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n * Ba biến thể:\n * • `variant=\"light\"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)\n * • `variant=\"dark\"` — mặt trên trắng; dùng trên nền tối / nền ấm\n * • `variant=\"auto\"` — tự đổi theo theme, giống `ComitorLockup variant=\"auto\"`\n *\n * ⚠ **Ngoại lệ duy nhất của quy tắc \"không hardcode màu\"**: mã hex dưới đây là vector\n * lấy nguyên từ file nguồn của brand book. KHÔNG tokenize, KHÔNG đổi màu,\n * KHÔNG đơn giản hoá path — logo phải giống hệt nhau ở mọi sản phẩm.\n *\n * File này KHÔNG có \"use client\": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.\n */\nexport type ComitorLogoVariant = \"light\" | \"dark\" | \"auto\";\n\nexport interface ComitorLogoProps {\n className?: string;\n /** Nền sáng dùng \"light\", nền tối/ấm dùng \"dark\", theo theme dùng \"auto\". */\n variant?: ComitorLogoVariant;\n /** Nội dung <title> của SVG — cũng là aria-label. */\n title?: string;\n style?: CSSProperties;\n}\n\nexport function ComitorLogo({ className, variant = \"light\", title = \"Comitor\", style }: ComitorLogoProps) {\n /*\n * `auto` PHẢI vẽ hai bản rồi ẩn/hiện bằng `dark:`, không thể là một SVG đổi màu: mã hex nằm\n * thẳng trong `fill` của từng path (ngoại lệ có chủ ý của quy tắc \"không hardcode màu\" — vector\n * chép nguyên từ brand book), mà `dark:` không với tới thuộc tính `fill`. Đây đúng cách\n * `ComitorLockup variant=\"auto\"` đang làm với chính component này; đưa vào đây để app không phải\n * chép lại cặp `dark:hidden` / `hidden dark:block` ở mỗi chỗ dùng logo trần.\n */\n if (variant === \"auto\") {\n return (\n <>\n <ComitorLogo variant=\"light\" className={cn(className, \"dark:hidden\")} title={title} style={style} />\n <ComitorLogo variant=\"dark\" className={cn(className, \"hidden dark:block\")} title={title} style={style} />\n </>\n );\n }\n\n const dark = variant === \"dark\";\n const c = dark\n ? {\n accent: \"#8A6800\",\n bright: \"#F0CC50\",\n rich: \"#D4A820\",\n topFace: \"#FFFFFF\",\n sphereHi: \"#F0CC50\",\n sphereLo: \"#D4A820\"\n }\n : {\n accent: \"#7A5800\",\n bright: \"#E8B824\",\n rich: \"#B88A00\",\n topFace: \"#F5D060\",\n sphereHi: \"#E8B824\",\n sphereLo: \"#B88A00\"\n };\n\n return (\n <svg\n viewBox=\"0 0 256 256\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n style={style}\n role=\"img\"\n aria-label={title}\n >\n <title>{title}</title>\n {/* Bottom interior diamond */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 142.059L78.5642 170.543L128 209.32L177.344 170.543L128 142.059Z\"\n fill={c.accent}\n />\n {/* Left-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M56.6951 86.8339L66.4352 149.502L91.1528 135.259L115.779 121.016V92.5315V64.0459L56.6951 86.8339Z\"\n fill={c.bright}\n />\n {/* Right-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M189.473 149.502L196.733 88.3046L140.13 64.0459V92.5315V121.016L164.755 135.259L189.473 149.502Z\"\n fill={c.rich}\n />\n {/* Top face / outer sphere highlight */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 0C182.857 0 229.629 34.5499 247.822 82.975C223.747 95.6553 162.917 112.195 128 128C104.569 118.811 28.3934 89.4991 9.74011 78.9317C28.9448 32.6203 74.7053 0 128 0ZM170.544 81.5968L128 106.131L85.3639 81.5968L115.779 64.046L101.445 55.7761V55.6842L80.4941 43.6468L58.9921 31.15C61.2894 29.588 63.6785 28.0259 66.1594 26.4638L88.6719 39.5118C100.709 34.1824 113.941 31.2419 128 31.2419C145.183 31.2419 161.356 35.7445 175.415 43.6468L140.13 64.046L170.544 81.5968Z\"\n fill={c.topFace}\n />\n {/* Right sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M238.817 63.9541C249.752 82.7915 256 104.66 256 128C256 198.662 198.662 256 128 256L120.557 199.03L128 128C164.939 106.682 201.878 85.3641 238.817 63.9541ZM146.929 188.095L177.344 170.544V211.251C202.981 196.089 220.991 169.35 224.207 138.2L246.811 125.152C246.72 122.303 246.536 119.455 246.26 116.698L207.207 139.211L189.473 149.502V114.401L146.929 138.935V188.095Z\"\n fill={c.sphereHi}\n />\n {/* Left sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 256C57.2464 256 0 198.662 0 128C0 104.66 6.15656 82.7915 17.0912 63.9541C54.0302 85.3641 90.9691 106.682 128 128V256ZM108.979 188.095V138.935L66.4352 114.401V149.502L31.1501 129.103C31.5176 160.804 47.1387 188.83 70.9376 206.289V232.293C73.4184 233.672 75.9917 234.958 78.5644 236.152V211.251V170.544L108.979 188.095Z\"\n fill={c.sphereLo}\n />\n </svg>\n );\n}\n"],"mappings":"AAsCM,mBACE,KADF;AArCN,SAAS,UAAU;AA2BZ,SAAS,YAAY,EAAE,WAAW,UAAU,SAAS,QAAQ,WAAW,MAAM,GAAqB;AAQxG,MAAI,YAAY,QAAQ;AACtB,WACE,iCACE;AAAA,0BAAC,eAAY,SAAQ,SAAQ,WAAW,GAAG,WAAW,aAAa,GAAG,OAAc,OAAc;AAAA,MAClG,oBAAC,eAAY,SAAQ,QAAO,WAAW,GAAG,WAAW,mBAAmB,GAAG,OAAc,OAAc;AAAA,OACzG;AAAA,EAEJ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,IAAI,OACN;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,IACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MAEZ;AAAA,4BAAC,WAAO,iBAAM;AAAA,QAEd;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -66,8 +66,9 @@ interface ShellProviderProps {
66
66
  pathname?: string;
67
67
  /**
68
68
  * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query
69
- * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn
70
- * cùng sáng.
69
+ * (`/settings?tab=general` ⇄ `/settings?tab=notifications`). Bỏ trống thì CHỈ mục KHÔNG mang
70
+ * query mới sáng (`/settings` sáng, `/settings?tab=…` thì không) — luật của `isNavItemActive`
71
+ * từ `0.3.3`; trước đó mọi mục cùng đường dẫn cùng sáng, và đó là lỗi.
71
72
  *
72
73
  * Nhận `useSearchParams()` của Next, chuỗi `"?tab=x"`, hoặc object `searchParams` của
73
74
  * Server Component.