@comitor/ui 3.3.2 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composite/command-palette.d.ts +1 -1
- package/dist/components/composite/command-palette.js.map +1 -1
- package/dist/components/composite/data-table.d.ts +10 -5
- package/dist/components/composite/data-table.js +2 -2
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/filter-chips.d.ts +1 -1
- package/dist/components/composite/filter-chips.js.map +1 -1
- package/dist/components/composite/inline-edit.d.ts +2 -2
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/route-tabs.d.ts +8 -6
- package/dist/components/composite/route-tabs.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +1 -1
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/ui/progress.d.ts +14 -2
- package/dist/components/ui/progress.js +12 -3
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +1 -1
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/data-grid/data-grid-context-menu.js +3 -2
- package/dist/data-grid/data-grid-context-menu.js.map +1 -1
- package/dist/data-grid/types.d.ts +7 -2
- package/dist/data-grid/use-data-grid.d.ts +19 -4
- package/dist/data-grid/use-data-grid.js +7 -5
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/i18n/namespaces/combobox.js +2 -2
- package/dist/i18n/namespaces/combobox.js.map +1 -1
- package/dist/i18n/namespaces/confirm-dialog.js +1 -1
- package/dist/i18n/namespaces/confirm-dialog.js.map +1 -1
- package/dist/i18n/namespaces/date-picker.js +2 -2
- package/dist/i18n/namespaces/date-picker.js.map +1 -1
- package/dist/i18n/namespaces/inline-edit.js +1 -1
- package/dist/i18n/namespaces/inline-edit.js.map +1 -1
- package/dist/i18n/namespaces/search-filter-bar.js +2 -2
- package/dist/i18n/namespaces/search-filter-bar.js.map +1 -1
- package/dist/i18n/namespaces/time-picker.js +1 -1
- package/dist/i18n/namespaces/time-picker.js.map +1 -1
- package/dist/shell/app-launcher.d.ts +2 -2
- package/dist/shell/app-launcher.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/comitor-logo.d.ts +1 -1
- package/dist/shell/comitor-logo.js.map +1 -1
- package/dist/shell/shell-context.d.ts +3 -2
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-group.d.ts +1 -1
- package/dist/shell/sidebar-group.js.map +1 -1
- package/dist/shell/types.d.ts +1 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/utils.d.ts +2 -2
- package/dist/shell/utils.js.map +1 -1
- package/dist/tokens.d.ts +23 -9
- package/dist/tokens.js +17 -9
- package/dist/tokens.js.map +1 -1
- package/dist/uploader/image-upload-field.d.ts +2 -2
- package/dist/uploader/image-upload-field.js.map +1 -1
- package/package.json +1 -1
|
@@ -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: "
|
|
16
|
-
clearAllLabel: "
|
|
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: \"
|
|
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":[]}
|
|
@@ -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: \"
|
|
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: "
|
|
18
|
-
clearRange: "
|
|
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: \"
|
|
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":[]}
|
|
@@ -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: \"
|
|
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: "
|
|
20
|
+
clearSearch: "Xo\xE1 t\u1EEB kho\xE1",
|
|
21
21
|
filterCount: (count) => `${formatVi(count)} b\u1ED9 l\u1ECDc`,
|
|
22
|
-
clearFilters: "
|
|
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: \"
|
|
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":[]}
|
|
@@ -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: \"
|
|
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
|
|
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
|
|
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
|
-
* (
|
|
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 * (
|
|
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
|
|
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
|
|
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
|
-
* (`/
|
|
70
|
-
*
|
|
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.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { layout } from \"../tokens.js\";\nimport type {\n AppDescriptor,\n NavGroup,\n ShellLabels,\n ShellLinkComponent,\n ShellLinkProps,\n ShellNav,\n ShellUser,\n Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (bảng `shell` của `ComitorLocaleProvider`, đã trộn override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn\n * cùng sáng.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /**\n * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của\n * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).\n */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useComitorLabels(\"shell\", labelsProp);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n /*\n * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":";AAuDI;AAxCJ,OAAO,oBAAoB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,eAA+B,aAAa,YAAY,WAAW,SAAS,gBAAgB;AACrG,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,cAAc;AAWvB,SAAS,iBAAuC,mBAAmB,aAAa,sBAAsB;AActG,MAAM,WAAa,eAA+D,WAChF;AAOF,SAAS,iBAAiB,EAAE,MAAM,UAAU,GAAG,MAAM,GAAmB;AACtE,SACE,oBAAC,YAAS,MAAa,GAAG,OACvB,UACH;AAEJ;AAoDA,MAAM,eAAe,cAA6C,MAAS;AAGpE,SAAS,WAA8B;AAC5C,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,qGAAsE;AAAA,EACxF;AACA,SAAO;AACT;AAUA,SAAS,kBAAkB,aAAyC;AAClE,SAAO,mBAAmB,eAAe,SAAS;AACpD;AASA,SAAS,gBAAyB;AAChC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,EAAE,QAAQ,OAAO,IAAI,OAAO;AAClC,SAAO,OAAO,WAAW,eAAe,MAAM,uBAAuB,SAAS,CAAC,KAAK,EAAE;AACxF;AAEA,SAAS,oBAAoB,aAAiD;AAC5E,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,UAAM,MAAM,OAAO,aAAa,QAAQ,kBAAkB,WAAW,CAAC;AACtE,QAAI,QAAQ,IAAK,QAAO;AACxB,QAAI,QAAQ,IAAK,QAAO;AACxB,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AA0FO,SAAS,cAAc;AAAA,EAC5B,aAAa,CAAC;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO,CAAC;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,0BAA0B;AAAA,EAC1B,0BAA0B;AAAA,EAC1B,sBAAsB;AAAA,EACtB,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR;AACF,GAAuB;AAErB,QAAM,eAAe,YAAY;AACjC,QAAM,WAAW,gBAAgB,gBAAgB;AAKjD,QAAM,eAAe,QAAQ,MAAM,eAAe,gBAAgB,GAAG,CAAC,gBAAgB,CAAC;AAEvF,QAAM,mBAAmB;AAAA,IACvB,MAAM,WAAW,KAAK,CAAC,cAAc,UAAU,OAAO,kBAAkB,KAAK,WAAW,CAAC,KAAK;AAAA,IAC9F,CAAC,YAAY,kBAAkB;AAAA,EACjC;AAEA,QAAM,aAAa,QAAQ,MAAM;AAC/B,QAAI,aAAc,QAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,YAAY,KAAK;AACxE,WAAO,kBAAkB,MAAM,QAAQ;AAAA,EACzC,GAAG,CAAC,MAAM,cAAc,QAAQ,CAAC;AAEjC,QAAM,YAAY,QAAQ,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC;AACvD,QAAM,SAAS,iBAAiB,SAAS,UAAU;AAEnD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,uBAAuB;AAChF,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,KAAK;AAC1D,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAAS,KAAK;AAClE,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAmB5D,YAAU,MAAM;AACd,UAAM,SAAS,sBAAsB,oBAAoB,kBAAkB,EAAE,IAAI;AACjF,QAAI,WAAW,MAAM;AACnB,0BAAoB,MAAM;AAC1B;AAAA,IACF;AACA,QAAI,2BAA2B,cAAc,EAAG,qBAAoB,IAAI;AAAA,EAC1E,GAAG,CAAC,qBAAqB,kBAAkB,IAAI,uBAAuB,CAAC;AAEvE,QAAM,mBAAmB;AAAA,IACvB,CAAC,cAAuB;AACtB,UAAI,CAAC,uBAAuB,OAAO,WAAW,YAAa;AAC3D,UAAI;AACF,eAAO,aAAa,QAAQ,kBAAkB,kBAAkB,EAAE,GAAG,YAAY,MAAM,GAAG;AAAA,MAC5F,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,qBAAqB,kBAAkB,EAAE;AAAA,EAC5C;AAEA,QAAM,wBAAwB;AAAA,IAC5B,CAAC,cAAuB;AACtB,0BAAoB,SAAS;AAC7B,uBAAiB,SAAS;AAAA,IAC5B;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,gBAAgB,YAAY,MAAM;AACtC,wBAAoB,CAAC,SAAS;AAC5B,YAAM,OAAO,CAAC;AACd,uBAAiB,IAAI;AACrB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,gBAAgB,CAAC;AAGrB,YAAU,MAAM;AACd,QAAI,CAAC,gBAAiB;AAEtB,UAAM,eAAe,gBAAgB,IAAI;AAEzC,UAAM,gBAAgB,CAAC,UAAyB;AAC9C,YAAM,SAAS,MAAM;AAErB,YAAM,WACJ,kBAAkB,oBAClB,kBAAkB,uBAClB,kBAAkB,qBACjB,kBAAkB,eAAe,OAAO;AAE3C,UAAI,MAAM,QAAQ,UAAU;AAC1B,8BAAsB,KAAK;AAC3B,0BAAkB,KAAK;AACvB,2BAAmB,KAAK;AACxB;AAAA,MACF;AAEA,UAAI,SAAU;AAEd,YAAM,MAAM,MAAM,WAAW,MAAM;AACnC,YAAM,MAAM,MAAM,IAAI,YAAY;AAGlC,UAAI,OAAO,MAAM,YAAY,QAAQ,KAAK;AACxC,cAAM,eAAe;AACrB,2BAAmB,CAAC,SAAS,CAAC,IAAI;AAClC;AAAA,MACF;AAGA,UAAI,OAAO,CAAC,MAAM,YAAY,QAAQ,KAAK;AACzC,cAAM,eAAe;AACrB,8BAAsB,CAAC,SAAS,CAAC,IAAI;AACrC;AAAA,MACF;AAGA,UAAI,OAAO,CAAC,MAAM,YAAY,QAAQ,KAAK;AACzC,cAAM,eAAe;AACrB,sBAAc;AACd;AAAA,MACF;AAIA,UAAI,MAAM,UAAU,CAAC,MAAM,WAAW,CAAC,MAAM,SAAS;AACpD,cAAM,QAAQ,iBAAiB,KAAK,MAAM,IAAI;AAC9C,YAAI,QAAQ,CAAC,GAAG;AACd,gBAAM,QAAQ,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI;AAC9C,gBAAM,MAAM,aAAa,KAAK;AAC9B,cAAI,KAAK;AACP,kBAAM,eAAe;AACrB,0BAAc,GAAG;AAAA,UACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,aAAa;AAChD,WAAO,MAAM,OAAO,oBAAoB,WAAW,aAAa;AAAA,EAClE,GAAG,CAAC,iBAAiB,MAAM,aAAa,aAAa,CAAC;AAItD,YAAU,MAAM;AACd,sBAAkB,KAAK;AACvB,uBAAmB,KAAK;AACxB,0BAAsB,KAAK;AAAA,EAC7B,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,qBAAqB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SACE,oBAAC,aAAa,UAAb,EAAsB,OACrB,8BAAC,mBAAgB,eAAe,KAAM,UAAS,GACjD;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * ĐUÔI `.js` LÀ CHỦ ĐÍCH, ĐỪNG BỎ.\n *\n * Gói `next` KHÔNG khai `exports` map, nên `next/link` chỉ là đường dẫn file. Bundler\n * (webpack/Turbopack) tự thêm đuôi rồi trỏ đúng `node_modules/next/link.js`; Node ESM thuần thì\n * KHÔNG — nó ném `ERR_MODULE_NOT_FOUND: Cannot find module .../next/link` kèm gợi ý\n * \"Did you mean next/link.js?\". Hậu quả: `node -e \"await import('@comitor/ui/shell')\"` chết ngay\n * lúc nạp, tức là mọi smoke-test entry của CI đều mù với riêng entry shell.\n *\n * Ghi thẳng `next/link.js` / `next/navigation.js` là CÙNG MỘT file mà bundler vẫn resolve tới\n * (đã đối chiếu: `next` không có `resolve.alias` cho hai specifier này trong\n * `dist/build/create-compiler-aliases.js`), nhưng Node cũng nạp được.\n */\nimport NextLinkModule from \"next/link.js\";\nimport { usePathname } from \"next/navigation.js\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { layout } from \"../tokens.js\";\nimport type {\n AppDescriptor,\n NavGroup,\n ShellLabels,\n ShellLinkComponent,\n ShellLinkProps,\n ShellNav,\n ShellUser,\n Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Chuẩn hoá interop của `next/link.js`.\n *\n * Hai môi trường trả về hai hình dạng khác nhau cho cùng một file CJS:\n * - bundler → đã interop sẵn, biến này CHÍNH LÀ component `Link`\n * - Node ESM → biến này là namespace `{ default: Link, useLinkStatus }`\n * `.default ?? chính nó` đúng ở cả hai (component `Link` không có thuộc tính `default`).\n */\nconst NextLink = ((NextLinkModule as unknown as { default?: ShellLinkComponent }).default ??\n NextLinkModule) as ShellLinkComponent;\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (bảng `shell` của `ComitorLocaleProvider`, đã trộn override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\n/**\n * Đang ở dải TABLET? — từ `breakpoints.mobile` (768px, mốc thanh bên thôi off-canvas) tới ngay\n * dưới `breakpoints.tablet` (1024px).\n *\n * Hai mốc lấy từ `layout.breakpoints` của `@comitor/ui/tokens` chứ không gõ số ở đây: đổi mốc mà\n * quên chỗ này thì thanh bên tự sập ở một dải mà CSS không còn coi là tablet nữa.\n */\nfunction isTabletWidth(): boolean {\n if (typeof window === \"undefined\") return false;\n const { mobile, tablet } = layout.breakpoints;\n return window.matchMedia(`(min-width: ${mobile}px) and (max-width: ${tablet - 1}px)`).matches;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/settings?tab=general` ⇄ `/settings?tab=notifications`). Bỏ trống thì CHỈ mục KHÔNG mang\n * query mới sáng (`/settings` sáng, `/settings?tab=…` thì không) — luật của `isNavItemActive`\n * từ `0.3.3`; trước đó mọi mục cùng đường dẫn cùng sáng, và đó là lỗi.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /**\n * Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh\n * bên thu gọn sẵn. Mặc định `true`.\n *\n * Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của\n * màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.\n */\n collapseSidebarOnTablet?: boolean;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /**\n * Override từng chuỗi hiển thị. Không truyền thì theo namespace `shell` của\n * `ComitorLocaleProvider` gần nhất (không có provider → tiếng Anh).\n */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n collapseSidebarOnTablet = true,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useComitorLabels(\"shell\", labelsProp);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n /*\n * Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n *\n * Nếu CHƯA có lựa chọn nào của người dùng thì ở dải TABLET thanh bên mặc định thu gọn: từ 768px\n * thanh bên đã chiếm chỗ thật trong bố cục (dưới mức đó nó là sheet off-canvas), mà 768–1023px\n * thì 256px của nó ăn tới một phần ba bề ngang — vùng nội dung còn ~512px cho một bảng nhiều cột.\n *\n * ⚠ KHÔNG ghi lựa chọn này vào localStorage. Đây là GIÁ TRỊ MẶC ĐỊNH, không phải quyết định của\n * người dùng: ghi xuống là lần sau mở ở màn hình rộng thanh bên vẫn thu gọn vì \"đã có lựa chọn\n * lưu\", mà người dùng thì chưa bấm gì cả. Chỉ khi họ tự gạt thì `applySidebarCollapsed` mới lưu.\n *\n * ⚠ Chỉ chạy MỘT LẦN lúc mở, không theo dõi resize. \"Mặc định khi mới mở\" khác với \"ép theo bề\n * ngang\": kéo cửa sổ hẹp lại giữa phiên mà thanh bên tự sập là giật thứ người dùng đang cầm.\n *\n * Đọc bề ngang trong effect nên không đụng tới hydration — server không có `window`, và lần\n * render đầu ở client vẫn dùng đúng `defaultSidebarCollapsed` mà server đã render.\n */\n useEffect(() => {\n const stored = persistSidebarState ? readStoredCollapsed(currentWorkspace?.id) : null;\n if (stored !== null) {\n setSidebarCollapsed(stored);\n return;\n }\n if (collapseSidebarOnTablet && isTabletWidth()) setSidebarCollapsed(true);\n }, [persistSidebarState, currentWorkspace?.id, collapseSidebarOnTablet]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -13,7 +13,7 @@ interface SidebarGroupProps {
|
|
|
13
13
|
className?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Nhóm mục sidebar
|
|
16
|
+
* Nhóm mục sidebar: nhãn nhỏ in hoa, bấm nhãn để thu gọn cả nhóm.
|
|
17
17
|
* Nhóm không có `label` (vd nhóm đầu tiên) render thẳng danh sách, không có phần đầu.
|
|
18
18
|
*/
|
|
19
19
|
declare function SidebarGroup({ group, collapsed, siblings, onNavigate, className }: SidebarGroupProps): React.JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/sidebar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SidebarMenuItem } from \"./sidebar-menu-item.js\";\nimport type { NavGroup, NavItem } from \"./types.js\";\n\n/**\n * Màu nhãn nhóm — `--muted-foreground`, KHÔNG phải `text-sidebar-foreground/45`.\n *\n * Nhãn nhóm là CHỮ (11px in hoa) nên chịu ngưỡng AA **4,5:1**, mà `/45` chỉ được **3,02:1**\n * ở light (`#909193` trên `--sidebar` #FAFAF9) và **4,26:1** ở dark — hỏng ở CẢ HAI theme,\n * light thì hỏng nặng. `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này:\n * **5,01:1** (light) · **5,35:1** (dark) trên `--sidebar` ✓.\n *\n * Hover thì lên thẳng `--sidebar-foreground` (18,11:1 light · 16,59:1 dark) thay cho `/80`.\n */\nconst GROUP_LABEL_CLASS = \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\";\n\nexport interface SidebarGroupProps {\n group: NavGroup;\n /** Sidebar đang thu gọn — ẩn nhãn nhóm, chỉ còn dải icon. */\n collapsed?: boolean;\n /** Toàn bộ mục của app hiện tại — dùng cho active matching chính xác. */\n siblings?: NavItem[];\n /** Chuyển tiếp xuống từng mục — xem `SidebarMenuItemProps.onNavigate`. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Nhóm mục sidebar
|
|
1
|
+
{"version":3,"sources":["../../src/shell/sidebar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SidebarMenuItem } from \"./sidebar-menu-item.js\";\nimport type { NavGroup, NavItem } from \"./types.js\";\n\n/**\n * Màu nhãn nhóm — `--muted-foreground`, KHÔNG phải `text-sidebar-foreground/45`.\n *\n * Nhãn nhóm là CHỮ (11px in hoa) nên chịu ngưỡng AA **4,5:1**, mà `/45` chỉ được **3,02:1**\n * ở light (`#909193` trên `--sidebar` #FAFAF9) và **4,26:1** ở dark — hỏng ở CẢ HAI theme,\n * light thì hỏng nặng. `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này:\n * **5,01:1** (light) · **5,35:1** (dark) trên `--sidebar` ✓.\n *\n * Hover thì lên thẳng `--sidebar-foreground` (18,11:1 light · 16,59:1 dark) thay cho `/80`.\n */\nconst GROUP_LABEL_CLASS = \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\";\n\nexport interface SidebarGroupProps {\n group: NavGroup;\n /** Sidebar đang thu gọn — ẩn nhãn nhóm, chỉ còn dải icon. */\n collapsed?: boolean;\n /** Toàn bộ mục của app hiện tại — dùng cho active matching chính xác. */\n siblings?: NavItem[];\n /** Chuyển tiếp xuống từng mục — xem `SidebarMenuItemProps.onNavigate`. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Nhóm mục sidebar: nhãn nhỏ in hoa, bấm nhãn để thu gọn cả nhóm.\n * Nhóm không có `label` (vd nhóm đầu tiên) render thẳng danh sách, không có phần đầu.\n */\nexport function SidebarGroup({ group, collapsed = false, siblings = [], onNavigate, className }: SidebarGroupProps) {\n const collapsible = group.collapsible ?? Boolean(group.label);\n const [open, setOpen] = useState(!group.defaultCollapsed);\n\n const items = (\n <div className=\"flex flex-col gap-0.5\">\n {group.items.map((item) => (\n <SidebarMenuItem key={item.id} item={item} collapsed={collapsed} siblings={siblings} onNavigate={onNavigate} />\n ))}\n </div>\n );\n\n // Thu gọn: nhãn không còn chỗ, chỉ giữ khoảng cách giữa các nhóm.\n if (collapsed) {\n return (\n <div className={cn(\"flex flex-col items-center gap-0.5\", className)} role=\"group\" aria-label={group.label}>\n {group.items.map((item) => (\n <SidebarMenuItem key={item.id} item={item} collapsed siblings={siblings} onNavigate={onNavigate} />\n ))}\n </div>\n );\n }\n\n if (!group.label || !collapsible) {\n return (\n <div className={cn(\"flex flex-col\", className)} role=\"group\" aria-label={group.label}>\n {group.label ? <p className={cn(\"px-2 pb-1\", GROUP_LABEL_CLASS)}>{group.label}</p> : null}\n {items}\n </div>\n );\n }\n\n return (\n <Collapsible open={open} onOpenChange={setOpen} className={cn(\"flex flex-col\", className)}>\n <CollapsibleTrigger asChild>\n <button\n type=\"button\"\n aria-expanded={open}\n className={cn(\n \"group flex w-full items-center gap-1 rounded-md px-2 py-1\",\n GROUP_LABEL_CLASS,\n \"transition-colors duration-150 ease-out hover:text-sidebar-foreground\"\n )}\n >\n <ChevronDown className={cn(\"size-3 shrink-0 transition-transform duration-150\", !open && \"-rotate-90\")} />\n <span className=\"truncate\">{group.label}</span>\n </button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n <div className=\"pt-0.5\">{items}</div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n"],"mappings":";AA2CQ,cAkBF,YAlBE;AAzCR,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,aAAa,oBAAoB,0BAA0B;AACpE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAahC,MAAM,oBAAoB;AAiBnB,SAAS,aAAa,EAAE,OAAO,YAAY,OAAO,WAAW,CAAC,GAAG,YAAY,UAAU,GAAsB;AAClH,QAAM,cAAc,MAAM,eAAe,QAAQ,MAAM,KAAK;AAC5D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC,MAAM,gBAAgB;AAExD,QAAM,QACJ,oBAAC,SAAI,WAAU,yBACZ,gBAAM,MAAM,IAAI,CAAC,SAChB,oBAAC,mBAA8B,MAAY,WAAsB,UAAoB,cAA/D,KAAK,EAAkF,CAC9G,GACH;AAIF,MAAI,WAAW;AACb,WACE,oBAAC,SAAI,WAAW,GAAG,sCAAsC,SAAS,GAAG,MAAK,SAAQ,cAAY,MAAM,OACjG,gBAAM,MAAM,IAAI,CAAC,SAChB,oBAAC,mBAA8B,MAAY,WAAS,MAAC,UAAoB,cAAnD,KAAK,EAAsE,CAClG,GACH;AAAA,EAEJ;AAEA,MAAI,CAAC,MAAM,SAAS,CAAC,aAAa;AAChC,WACE,qBAAC,SAAI,WAAW,GAAG,iBAAiB,SAAS,GAAG,MAAK,SAAQ,cAAY,MAAM,OAC5E;AAAA,YAAM,QAAQ,oBAAC,OAAE,WAAW,GAAG,aAAa,iBAAiB,GAAI,gBAAM,OAAM,IAAO;AAAA,MACpF;AAAA,OACH;AAAA,EAEJ;AAEA,SACE,qBAAC,eAAY,MAAY,cAAc,SAAS,WAAW,GAAG,iBAAiB,SAAS,GACtF;AAAA,wBAAC,sBAAmB,SAAO,MACzB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,eAAY,WAAW,GAAG,qDAAqD,CAAC,QAAQ,YAAY,GAAG;AAAA,UACxG,oBAAC,UAAK,WAAU,YAAY,gBAAM,OAAM;AAAA;AAAA;AAAA,IAC1C,GACF;AAAA,IACA,oBAAC,sBAAmB,WAAU,yGAC5B,8BAAC,SAAI,WAAU,UAAU,iBAAM,GACjC;AAAA,KACF;AAEJ;","names":[]}
|
package/dist/shell/types.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ interface NavItem {
|
|
|
105
105
|
external?: boolean;
|
|
106
106
|
disabled?: boolean;
|
|
107
107
|
}
|
|
108
|
-
/** Nhóm mục menu (
|
|
108
|
+
/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */
|
|
109
109
|
interface NavGroup {
|
|
110
110
|
id: string;
|
|
111
111
|
/** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */
|