@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n /**\n * Lớp tô CHẤM của `StatusPill variant=\"dot\"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền\n * TĨNH (`\"bg-severe\"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách\n * cũ: lấy `currentColor` của bậc ink trong `textColor`.\n *\n * Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn\n * — `--warning-ink` #7A5800 là \"ô-liu\" chứ không phải \"gấp\". Chấm là hình, không phải chữ, nên\n * nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.\n *\n * ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả\n * --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`\n * 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên\n * --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG\n * thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo\n * ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.\n */\n dotColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng `outline` (như `isRowActive` của `DataTable` đang làm) hoặc nói trạng\n * thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong\n * docs/hop-dong-mau.md:\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới\n * neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─\n * Ưu tiên cao, SLA sắp trễ — nấc mà GitHub Primer gọi là \"severe\". Có vì `warning` tô chấm bằng\n * `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra \"gấp\".\n * Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm\n * là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:\n *\n * soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)\n * mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)\n * tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)\n * cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)\n *\n * `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là\n * cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của\n * `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary` ĐI THEO `--primary`, KHÔNG PHẢI THANG `gold-*` ─\n * Trước đây tone này chôn cứng `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100`.\n * Tương phản thì ĐẠT (7,77:1 L · 11,85:1 D soft; 9,58:1 L · 15,33:1 D outline — bảng cũ ghi\n * 11,90:1 cho ô dark, đó là số của `--primary-ink` trên `--card`, chép nhầm sang), nhưng SAI VAI\n * TRÒ: thang `gold-*` không có bản dark nên phải viết hai lớp `dark:`, và app đổi `--primary` cho\n * sản phẩm của mình thì chip \"primary\" vẫn cứ vàng. `bg-primary/20` + `text-primary-ink` đi theo\n * token ở cả hai theme bằng MỘT lớp — hợp đồng §5. Alpha `/20` (không phải `/15` như các tone\n * khác) để giữ đúng độ nổi của `bg-gold-100` cũ: nền chip ⇄ nền trang 1,13:1 thay vì 1,10:1,\n * cùng mức với `warning` — vàng luôn cần thêm một nấc mới thấy được.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-muted-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: { bgColor: \"bg-primary/20\", textColor: \"text-primary-ink\", borderColor: \"border-primary-ink\" },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n severe: {\n bgColor: \"bg-severe/15\",\n textColor: \"text-severe-ink\",\n borderColor: \"border-severe-ink\",\n dotColor: \"bg-severe\"\n },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA0IO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n /**\n * Lớp tô CHẤM của `StatusPill variant=\"dot\"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền\n * TĨNH (`\"bg-severe\"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách\n * cũ: lấy `currentColor` của bậc ink trong `textColor`.\n *\n * Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn\n * — `--warning-ink` #7A5800 là \"ô-liu\" chứ không phải \"gấp\". Chấm là hình, không phải chữ, nên\n * nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.\n *\n * ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả\n * --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`\n * 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên\n * --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG\n * thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo\n * ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.\n */\n dotColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng `outline` (như `isRowActive` của `DataTable` đang làm) hoặc nói trạng\n * thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong\n * docs/hop-dong-mau.md:\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới\n * neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─\n * Ưu tiên cao, SLA sắp trễ — tên \"severe\" là tên quen dùng cho nấc này. Có vì `warning` tô chấm bằng\n * `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra \"gấp\".\n * Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm\n * là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:\n *\n * soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)\n * mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)\n * tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)\n * cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)\n *\n * `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là\n * cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của\n * `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary` ĐI THEO `--primary`, KHÔNG PHẢI THANG `gold-*` ─\n * Trước đây tone này chôn cứng `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100`.\n * Tương phản thì ĐẠT (7,77:1 L · 11,85:1 D soft; 9,58:1 L · 15,33:1 D outline — bảng cũ ghi\n * 11,90:1 cho ô dark, đó là số của `--primary-ink` trên `--card`, chép nhầm sang), nhưng SAI VAI\n * TRÒ: thang `gold-*` không có bản dark nên phải viết hai lớp `dark:`, và app đổi `--primary` cho\n * sản phẩm của mình thì chip \"primary\" vẫn cứ vàng. `bg-primary/20` + `text-primary-ink` đi theo\n * token ở cả hai theme bằng MỘT lớp — hợp đồng §5. Alpha `/20` (không phải `/15` như các tone\n * khác) để giữ đúng độ nổi của `bg-gold-100` cũ: nền chip ⇄ nền trang 1,13:1 thay vì 1,10:1,\n * cùng mức với `warning` — vàng luôn cần thêm một nấc mới thấy được.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-muted-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: { bgColor: \"bg-primary/20\", textColor: \"text-primary-ink\", borderColor: \"border-primary-ink\" },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n severe: {\n bgColor: \"bg-severe/15\",\n textColor: \"text-severe-ink\",\n borderColor: \"border-severe-ink\",\n dotColor: \"bg-severe\"\n },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA0IO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
@@ -4,10 +4,22 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Progress (Radix) — thanh tiến trình xác định (`value` 0
|
|
7
|
+
* Progress (Radix) — thanh tiến trình xác định (`value` 0…`max`, `max` mặc định 100).
|
|
8
8
|
* Bỏ trống `value` để về trạng thái indeterminate của Radix (thanh đứng yên ở 0);
|
|
9
9
|
* animation cho indeterminate là việc của app.
|
|
10
10
|
*
|
|
11
|
+
* ─ THANH VẼ THEO `value / max`, VÀ THANH VỚI `aria-*` NÓI CÙNG MỘT CON SỐ ─
|
|
12
|
+
* Radix đọc `max` để dựng `aria-valuemax` và `aria-valuetext` (`round(value / max · 100)%`). Trước
|
|
13
|
+
* `3.3.3` phần đã chạy lại vẽ bằng `100 - value` — bỏ qua `max` —, nên `max={30} value={24}` vẽ 24%
|
|
14
|
+
* trong khi trình đọc màn hình đọc 80%: hai người nhìn cùng một control nhận hai con số.
|
|
15
|
+
*
|
|
16
|
+
* Nay cả hai đi từ MỘT giá trị: `value` được KẸP vào `[0, max]` rồi mới giao cho Radix (`max` không
|
|
17
|
+
* phải số dương → 100, đúng luật Radix). Kẹp chứ không coi là indeterminate, vì ca vượt ngưỡng hay
|
|
18
|
+
* gặp nhất là CÓ Ý: hạn mức dùng 120% phải là thanh ĐẦY, không phải thanh rỗng. Trước `3.3.3` ca đó
|
|
19
|
+
* tình cờ đã đầy (`translateX(--20%)` là CSS vô hiệu) nhưng Radix coi `value` sai → không có
|
|
20
|
+
* `aria-valuetext`, kèm lỗi console; nay thanh vẫn đầy VÀ trình đọc màn hình đọc "100%". Nói rõ là
|
|
21
|
+
* đã vượt (bao nhiêu, làm gì tiếp) vẫn là việc của chữ cạnh thanh và `tone`, không của thanh.
|
|
22
|
+
*
|
|
11
23
|
* ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─
|
|
12
24
|
* Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần
|
|
13
25
|
* rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với
|
|
@@ -87,6 +99,6 @@ declare const progressBarVariants: (props?: ({
|
|
|
87
99
|
type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>["tone"]>;
|
|
88
100
|
interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root>, VariantProps<typeof progressBarVariants> {
|
|
89
101
|
}
|
|
90
|
-
declare function Progress({ className, value, tone, ...props }: ProgressProps): React.JSX.Element;
|
|
102
|
+
declare function Progress({ className, value, max, tone, ...props }: ProgressProps): React.JSX.Element;
|
|
91
103
|
|
|
92
104
|
export { Progress, type ProgressProps, type ProgressTone, progressBarVariants, progressTrackVariants };
|
|
@@ -26,21 +26,30 @@ const progressBarVariants = cva("h-full w-full flex-1 transition-transform durat
|
|
|
26
26
|
},
|
|
27
27
|
defaultVariants: { tone: "default" }
|
|
28
28
|
});
|
|
29
|
-
|
|
29
|
+
const DEFAULT_MAX = 100;
|
|
30
|
+
function clampValue(value, max) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
32
|
+
return Math.min(Math.max(value, 0), max);
|
|
33
|
+
}
|
|
34
|
+
function Progress({ className, value, max, tone = "default", ...props }) {
|
|
35
|
+
const validMax = typeof max === "number" && max > 0 ? max : DEFAULT_MAX;
|
|
36
|
+
const clamped = clampValue(value, validMax);
|
|
37
|
+
const percent = typeof clamped === "number" && Number.isFinite(clamped) ? clamped / validMax * 100 : 0;
|
|
30
38
|
return /* @__PURE__ */ jsx(
|
|
31
39
|
ProgressPrimitive.Root,
|
|
32
40
|
{
|
|
33
41
|
"data-slot": "progress",
|
|
34
42
|
"data-tone": tone,
|
|
35
43
|
className: cn(progressTrackVariants({ tone }), className),
|
|
36
|
-
value,
|
|
44
|
+
value: clamped,
|
|
45
|
+
max,
|
|
37
46
|
...props,
|
|
38
47
|
children: /* @__PURE__ */ jsx(
|
|
39
48
|
ProgressPrimitive.Indicator,
|
|
40
49
|
{
|
|
41
50
|
"data-slot": "progress-indicator",
|
|
42
51
|
className: progressBarVariants({ tone }),
|
|
43
|
-
style: { transform: `translateX(-${100 -
|
|
52
|
+
style: { transform: `translateX(-${100 - percent}%)` }
|
|
44
53
|
}
|
|
45
54
|
)
|
|
46
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Progress (Radix) — thanh tiến trình xác định (`value` 0…100).\n * Bỏ trống `value` để về trạng thái indeterminate của Radix (thanh đứng yên ở 0);\n * animation cho indeterminate là việc của app.\n *\n * ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─\n * Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần\n * rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với\n * rãnh `bg-primary/20` — thanh 8px vàng nhạt trên nền trắng gần như không thấy. Bậc ink của\n * brand đạt cả hai phía ở cả hai theme:\n *\n * đã chạy ⇄ nền trang : 6,51:1 (light) · 12,67:1 (dark) ✓\n * đã chạy ⇄ rãnh `/20` : 4,85:1 (light) · 7,97:1 (dark) ✓\n * (tệ nhất khi rãnh nằm trên --muted: 4,54:1 · 6,52:1 ✓)\n *\n * ─ RÃNH: TINT `/20` GIỮ NGUYÊN, NHƯNG PHẢI CÓ VIỀN `--control-edge` ─\n * Bản trước chỉ có tint: rãnh ⇄ nền trang mới **1,34:1** (light) và **1,59–1,66:1** (dark), tức\n * KHÔNG nhìn thấy mốc 100% — ở `value` nhỏ không đọc được TỈ LỆ, ở `value=0` cả control biến mất.\n * Cách sửa hiển nhiên (đổi rãnh sang `bg-control-edge` như Slider/Switch) lại hỏng chỗ khác:\n * `--primary-ink` ⇄ `--control-edge` chỉ **1,70:1** (light) / **2,57:1** (dark), mà Progress\n * KHÔNG có núm nên ranh giới đã-chạy/chưa-chạy là thứ DUY NHẤT mang giá trị — mất nó là mất\n * thông tin. Với nền trang trắng không tồn tại cặp màu nào vừa ≥3:1 với nhau vừa ≥3:1 với nền\n * (chứng minh trong chú thích của `Slider`), nên hai ràng buộc phải tách ra hai lớp:\n *\n * khoanh vùng control → `border-control-edge` ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * ⇄ --muted : 3,58:1 · 4,21:1 ✓\n * đọc tỉ lệ → đã chạy ⇄ rãnh `/20` : 4,85:1 · 7,97:1 ✓\n *\n * Đây đúng vai trò hợp đồng đặt cho `--control-edge` (styles.css §RANH GIỚI): \"khi nền control\n * trong suốt/nhạt thì VIỀN là thứ duy nhất khoanh vùng\".\n *\n * ─ TÔNG: BỐN GIÁ TRỊ, VÀ `warning` KHÔNG ĐƯỢC PHÉP CHỈ LÀ MỘT MÀU KHÁC ─\n * `tone` đổi CẢ phần đã chạy lẫn rãnh sang cùng một bậc ink, nên tỉ lệ đọc-được giữ nguyên cấu\n * trúc của bản mặc định. Đo trên ba bề mặt (`--background`, `--card`, `--muted`), tệ nhất mỗi cột.\n *\n * ⚠ Cột \"light\"/\"dark\" dưới đây là **bảng TƯƠNG PHẢN CAO** — đúng bảng mà cổng màu đo, và lý do\n * ghi ở đầu `scripts/a11y/tailwind.mjs`. Ở bảng mặc định (bản duyệt comitor-ds) `--primary-ink`\n * chính là `--primary` gold #E8B824, nên tông `default` chỉ được 1,73:1 với nền và 1,55:1 với\n * rãnh: đó là con số của BẢN DUYỆT, không phải lỗi của component — đổi nó là đổi bảng màu.\n *\n * tông đã chạy ⇄ nền trang đã chạy ⇄ rãnh cùng tông /20\n * default 6,09:1 (light) 10,80 (dark) 4,54:1 (light) 6,52:1 (dark) ✓\n * success 6,09:1 9,25 4,55:1 5,88:1 ✓\n * warning 6,09:1 10,52 4,54:1 6,47:1 ✓\n * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓\n *\n * ⚠ NHƯNG `warning` KHÔNG phân biệt được với `default` BẰNG MÀU, và đó là số đo chứ không phải\n * cảm tính. Đo `--warning-ink` ⇄ `--primary-ink` trên cả BỐN bảng màu của gói:\n *\n * light (mặc định) #7A5800 ⇄ #E8B824 3,51:1 ✓\n * light (tương phản cao) #7A5800 ⇄ #7A5800 1,00:1 ✗ CÙNG MỘT HEX\n * dark (mặc định) #F2C94C ⇄ #F5D060 1,06:1 ✗\n * dark (tương phản cao) #FACC15 ⇄ #F5D060 1,03:1 ✗\n *\n * Ba trên bốn bảng màu cho ra hai thanh trông y hệt nhau. Đây KHÔNG sửa được ở tầng token: brand\n * của Comitor là gold, warning là yellow — cùng một sắc. Muốn tách hai bậc ink ra 3:1 thì một\n * trong hai phải rời khỏi vùng đọc-được: với nền trắng, chữ đạt 4,5:1 đòi độ chói ≤ 0,183, mà\n * cách `--gold-500` (0,111) đúng 3:1 thì phải ≤ 0,004 (gần như đen) hoặc ≥ 0,433 (quá sáng để\n * làm chữ). Hai ràng buộc không giao nhau — không có hex nào thoả cả hai.\n *\n * Nên tông `warning` nói bằng thứ KHÔNG PHẢI MÀU: `hazard-stripes` (khai ở styles.css) khoét\n * những rãnh chéo xuyên phần đã chạy để nền rãnh lộ ra. Ranh giới vạch ⇄ rãnh chính là cặp\n * \"đã chạy ⇄ rãnh\" đã đo ở bảng trên (4,54:1 light · 6,47:1 dark ✓), nên vạch KHÔNG thêm màu nào\n * và không cần đo thêm. Đây đúng là WCAG 1.4.1: tín hiệu không được chỉ nằm ở màu — bản vạch còn\n * đọc ra ở chế độ xám, chỗ mà mọi khác biệt sắc độ đều biến mất.\n *\n * ⚠ Vì vậy: KHÔNG được \"đơn giản hoá\" `warning` xuống còn mỗi `bg-warning-ink`. Bỏ vạch đi là\n * API đổi tông mà không đổi được gì ở ba trên bốn bảng màu — chỗ gọi tưởng đã cảnh báo, người\n * dùng không thấy gì. Cùng lý do đó, ĐỪNG thêm tông thứ năm nào nữa mà chưa đo lại bảng trên.\n *\n * Vạch chỉ là cách nói \"sắp chạm ngưỡng\" trên chính cái thanh; nó KHÔNG thay được một câu chữ.\n * Chỗ nào cần nói rõ ngưỡng là bao nhiêu và làm gì tiếp thì vẫn phải có `Alert`/`StatusPill` hoặc\n * một dòng `text-warning-ink` cạnh thanh.\n */\n\n/*\n * HAI `cva`, không phải hai bảng `Record`.\n *\n * Không phải chuyện phong cách: cổng màu (`scripts/check-contrast.mjs`) chỉ bóc chuỗi lớp từ ba\n * chỗ — thuộc tính `className`, `cva(base, { variants })`, và đối số TRỰC TIẾP của `cn()`. Class\n * nằm trong một `Record` rồi đi vào `cn()` qua biến là VÔ HÌNH với cổng: nó không báo lỗi, nó\n * lặng lẽ không đo — và miễn trừ của rãnh sẽ thành \"ruỗng\", đúng thứ cổng coi là nơi giấu lỗi.\n * Dạng `cva` cho cổng thấy cả base lẫn từng lá biến thể.\n */\nconst progressTrackVariants = cva(\"border-control-edge relative h-2 w-full overflow-hidden rounded-full border\", {\n variants: {\n tone: {\n default: \"bg-primary-ink/20\",\n success: \"bg-success-ink/20\",\n warning: \"bg-warning-ink/20\",\n destructive: \"bg-destructive-ink/20\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\nconst progressBarVariants = cva(\"h-full w-full flex-1 transition-transform duration-150 ease-out\", {\n variants: {\n tone: {\n default: \"bg-primary-ink\",\n success: \"bg-success-ink\",\n // `hazard-stripes` là PHẦN BẮT BUỘC của tông này, không phải trang trí — xem chú thích đầu file.\n warning: \"bg-warning-ink hazard-stripes\",\n destructive: \"bg-destructive-ink\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\n/** Tông của thanh — xem chú thích ở trên về việc vì sao `warning` phải kèm vạch chéo. */\nexport type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>[\"tone\"]>;\n\nexport interface ProgressProps\n extends React.ComponentProps<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressBarVariants> {}\n\nfunction Progress({ className, value, tone = \"default\", ...props }: ProgressProps) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={value}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AAoIM;AAlIN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAsFnB,MAAM,wBAAwB,IAAI,+EAA+E;AAAA,EAC/G,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAED,MAAM,sBAAsB,IAAI,mEAAmE;AAAA,EACjG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA;AAAA,MAET,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AASD,SAAS,SAAS,EAAE,WAAW,OAAO,OAAO,WAAW,GAAG,MAAM,GAAkB;AACjF,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAW,oBAAoB,EAAE,KAAK,CAAC;AAAA,UACvC,OAAO,EAAE,WAAW,eAAe,OAAO,SAAS,EAAE,KAAK;AAAA;AAAA,MAC5D;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Progress (Radix) — thanh tiến trình xác định (`value` 0…`max`, `max` mặc định 100).\n * Bỏ trống `value` để về trạng thái indeterminate của Radix (thanh đứng yên ở 0);\n * animation cho indeterminate là việc của app.\n *\n * ─ THANH VẼ THEO `value / max`, VÀ THANH VỚI `aria-*` NÓI CÙNG MỘT CON SỐ ─\n * Radix đọc `max` để dựng `aria-valuemax` và `aria-valuetext` (`round(value / max · 100)%`). Trước\n * `3.3.3` phần đã chạy lại vẽ bằng `100 - value` — bỏ qua `max` —, nên `max={30} value={24}` vẽ 24%\n * trong khi trình đọc màn hình đọc 80%: hai người nhìn cùng một control nhận hai con số.\n *\n * Nay cả hai đi từ MỘT giá trị: `value` được KẸP vào `[0, max]` rồi mới giao cho Radix (`max` không\n * phải số dương → 100, đúng luật Radix). Kẹp chứ không coi là indeterminate, vì ca vượt ngưỡng hay\n * gặp nhất là CÓ Ý: hạn mức dùng 120% phải là thanh ĐẦY, không phải thanh rỗng. Trước `3.3.3` ca đó\n * tình cờ đã đầy (`translateX(--20%)` là CSS vô hiệu) nhưng Radix coi `value` sai → không có\n * `aria-valuetext`, kèm lỗi console; nay thanh vẫn đầy VÀ trình đọc màn hình đọc \"100%\". Nói rõ là\n * đã vượt (bao nhiêu, làm gì tiếp) vẫn là việc của chữ cạnh thanh và `tone`, không của thanh.\n *\n * ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─\n * Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần\n * rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với\n * rãnh `bg-primary/20` — thanh 8px vàng nhạt trên nền trắng gần như không thấy. Bậc ink của\n * brand đạt cả hai phía ở cả hai theme:\n *\n * đã chạy ⇄ nền trang : 6,51:1 (light) · 12,67:1 (dark) ✓\n * đã chạy ⇄ rãnh `/20` : 4,85:1 (light) · 7,97:1 (dark) ✓\n * (tệ nhất khi rãnh nằm trên --muted: 4,54:1 · 6,52:1 ✓)\n *\n * ─ RÃNH: TINT `/20` GIỮ NGUYÊN, NHƯNG PHẢI CÓ VIỀN `--control-edge` ─\n * Bản trước chỉ có tint: rãnh ⇄ nền trang mới **1,34:1** (light) và **1,59–1,66:1** (dark), tức\n * KHÔNG nhìn thấy mốc 100% — ở `value` nhỏ không đọc được TỈ LỆ, ở `value=0` cả control biến mất.\n * Cách sửa hiển nhiên (đổi rãnh sang `bg-control-edge` như Slider/Switch) lại hỏng chỗ khác:\n * `--primary-ink` ⇄ `--control-edge` chỉ **1,70:1** (light) / **2,57:1** (dark), mà Progress\n * KHÔNG có núm nên ranh giới đã-chạy/chưa-chạy là thứ DUY NHẤT mang giá trị — mất nó là mất\n * thông tin. Với nền trang trắng không tồn tại cặp màu nào vừa ≥3:1 với nhau vừa ≥3:1 với nền\n * (chứng minh trong chú thích của `Slider`), nên hai ràng buộc phải tách ra hai lớp:\n *\n * khoanh vùng control → `border-control-edge` ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * ⇄ --muted : 3,58:1 · 4,21:1 ✓\n * đọc tỉ lệ → đã chạy ⇄ rãnh `/20` : 4,85:1 · 7,97:1 ✓\n *\n * Đây đúng vai trò hợp đồng đặt cho `--control-edge` (styles.css §RANH GIỚI): \"khi nền control\n * trong suốt/nhạt thì VIỀN là thứ duy nhất khoanh vùng\".\n *\n * ─ TÔNG: BỐN GIÁ TRỊ, VÀ `warning` KHÔNG ĐƯỢC PHÉP CHỈ LÀ MỘT MÀU KHÁC ─\n * `tone` đổi CẢ phần đã chạy lẫn rãnh sang cùng một bậc ink, nên tỉ lệ đọc-được giữ nguyên cấu\n * trúc của bản mặc định. Đo trên ba bề mặt (`--background`, `--card`, `--muted`), tệ nhất mỗi cột.\n *\n * ⚠ Cột \"light\"/\"dark\" dưới đây là **bảng TƯƠNG PHẢN CAO** — đúng bảng mà cổng màu đo, và lý do\n * ghi ở đầu `scripts/a11y/tailwind.mjs`. Ở bảng mặc định (bản duyệt comitor-ds) `--primary-ink`\n * chính là `--primary` gold #E8B824, nên tông `default` chỉ được 1,73:1 với nền và 1,55:1 với\n * rãnh: đó là con số của BẢN DUYỆT, không phải lỗi của component — đổi nó là đổi bảng màu.\n *\n * tông đã chạy ⇄ nền trang đã chạy ⇄ rãnh cùng tông /20\n * default 6,09:1 (light) 10,80 (dark) 4,54:1 (light) 6,52:1 (dark) ✓\n * success 6,09:1 9,25 4,55:1 5,88:1 ✓\n * warning 6,09:1 10,52 4,54:1 6,47:1 ✓\n * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓\n *\n * ⚠ NHƯNG `warning` KHÔNG phân biệt được với `default` BẰNG MÀU, và đó là số đo chứ không phải\n * cảm tính. Đo `--warning-ink` ⇄ `--primary-ink` trên cả BỐN bảng màu của gói:\n *\n * light (mặc định) #7A5800 ⇄ #E8B824 3,51:1 ✓\n * light (tương phản cao) #7A5800 ⇄ #7A5800 1,00:1 ✗ CÙNG MỘT HEX\n * dark (mặc định) #F2C94C ⇄ #F5D060 1,06:1 ✗\n * dark (tương phản cao) #FACC15 ⇄ #F5D060 1,03:1 ✗\n *\n * Ba trên bốn bảng màu cho ra hai thanh trông y hệt nhau. Đây KHÔNG sửa được ở tầng token: brand\n * của Comitor là gold, warning là yellow — cùng một sắc. Muốn tách hai bậc ink ra 3:1 thì một\n * trong hai phải rời khỏi vùng đọc-được: với nền trắng, chữ đạt 4,5:1 đòi độ chói ≤ 0,183, mà\n * cách `--gold-500` (0,111) đúng 3:1 thì phải ≤ 0,004 (gần như đen) hoặc ≥ 0,433 (quá sáng để\n * làm chữ). Hai ràng buộc không giao nhau — không có hex nào thoả cả hai.\n *\n * Nên tông `warning` nói bằng thứ KHÔNG PHẢI MÀU: `hazard-stripes` (khai ở styles.css) khoét\n * những rãnh chéo xuyên phần đã chạy để nền rãnh lộ ra. Ranh giới vạch ⇄ rãnh chính là cặp\n * \"đã chạy ⇄ rãnh\" đã đo ở bảng trên (4,54:1 light · 6,47:1 dark ✓), nên vạch KHÔNG thêm màu nào\n * và không cần đo thêm. Đây đúng là WCAG 1.4.1: tín hiệu không được chỉ nằm ở màu — bản vạch còn\n * đọc ra ở chế độ xám, chỗ mà mọi khác biệt sắc độ đều biến mất.\n *\n * ⚠ Vì vậy: KHÔNG được \"đơn giản hoá\" `warning` xuống còn mỗi `bg-warning-ink`. Bỏ vạch đi là\n * API đổi tông mà không đổi được gì ở ba trên bốn bảng màu — chỗ gọi tưởng đã cảnh báo, người\n * dùng không thấy gì. Cùng lý do đó, ĐỪNG thêm tông thứ năm nào nữa mà chưa đo lại bảng trên.\n *\n * Vạch chỉ là cách nói \"sắp chạm ngưỡng\" trên chính cái thanh; nó KHÔNG thay được một câu chữ.\n * Chỗ nào cần nói rõ ngưỡng là bao nhiêu và làm gì tiếp thì vẫn phải có `Alert`/`StatusPill` hoặc\n * một dòng `text-warning-ink` cạnh thanh.\n */\n\n/*\n * HAI `cva`, không phải hai bảng `Record`.\n *\n * Không phải chuyện phong cách: cổng màu (`scripts/check-contrast.mjs`) chỉ bóc chuỗi lớp từ ba\n * chỗ — thuộc tính `className`, `cva(base, { variants })`, và đối số TRỰC TIẾP của `cn()`. Class\n * nằm trong một `Record` rồi đi vào `cn()` qua biến là VÔ HÌNH với cổng: nó không báo lỗi, nó\n * lặng lẽ không đo — và miễn trừ của rãnh sẽ thành \"ruỗng\", đúng thứ cổng coi là nơi giấu lỗi.\n * Dạng `cva` cho cổng thấy cả base lẫn từng lá biến thể.\n */\nconst progressTrackVariants = cva(\"border-control-edge relative h-2 w-full overflow-hidden rounded-full border\", {\n variants: {\n tone: {\n default: \"bg-primary-ink/20\",\n success: \"bg-success-ink/20\",\n warning: \"bg-warning-ink/20\",\n destructive: \"bg-destructive-ink/20\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\nconst progressBarVariants = cva(\"h-full w-full flex-1 transition-transform duration-150 ease-out\", {\n variants: {\n tone: {\n default: \"bg-primary-ink\",\n success: \"bg-success-ink\",\n // `hazard-stripes` là PHẦN BẮT BUỘC của tông này, không phải trang trí — xem chú thích đầu file.\n warning: \"bg-warning-ink hazard-stripes\",\n destructive: \"bg-destructive-ink\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\n/** Tông của thanh — xem chú thích ở trên về việc vì sao `warning` phải kèm vạch chéo. */\nexport type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>[\"tone\"]>;\n\nexport interface ProgressProps\n extends React.ComponentProps<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressBarVariants> {}\n\n/** Mặc định của Radix — `max` không hợp lệ thì Radix cũng rơi về đúng số này cho `aria-*`. */\nconst DEFAULT_MAX = 100;\n\n/**\n * Kẹp `value` vào `[0, max]` — giá trị DUY NHẤT mà cả thanh lẫn Radix (`aria-*`) đọc (xem chú thích\n * đầu file). Không phải số hữu hạn (`null`, `undefined`, `NaN`) thì giữ nguyên: Radix coi là\n * indeterminate và thanh đứng ở 0.\n */\nfunction clampValue(value: number | null | undefined, max: number): number | null | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return value;\n return Math.min(Math.max(value, 0), max);\n}\n\nfunction Progress({ className, value, max, tone = \"default\", ...props }: ProgressProps) {\n const validMax = typeof max === \"number\" && max > 0 ? max : DEFAULT_MAX;\n const clamped = clampValue(value, validMax);\n const percent = typeof clamped === \"number\" && Number.isFinite(clamped) ? (clamped / validMax) * 100 : 0;\n\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={clamped}\n max={max}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - percent}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AAkKM;AAhKN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAkGnB,MAAM,wBAAwB,IAAI,+EAA+E;AAAA,EAC/G,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAED,MAAM,sBAAsB,IAAI,mEAAmE;AAAA,EACjG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA;AAAA,MAET,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAUD,MAAM,cAAc;AAOpB,SAAS,WAAW,OAAkC,KAAwC;AAC5F,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,GAAG;AACzC;AAEA,SAAS,SAAS,EAAE,WAAW,OAAO,KAAK,OAAO,WAAW,GAAG,MAAM,GAAkB;AACtF,QAAM,WAAW,OAAO,QAAQ,YAAY,MAAM,IAAI,MAAM;AAC5D,QAAM,UAAU,WAAW,OAAO,QAAQ;AAC1C,QAAM,UAAU,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,IAAK,UAAU,WAAY,MAAM;AAEvG,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD,OAAO;AAAA,MACP;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAW,oBAAoB,EAAE,KAAK,CAAC;AAAA,UACvC,OAAO,EAAE,WAAW,eAAe,MAAM,OAAO,KAAK;AAAA;AAAA,MACvD;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -3,7 +3,7 @@ import * as ResizablePrimitive from 'react-resizable-panels';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resizable (react-resizable-panels) — bố cục nhiều panel kéo giãn được.
|
|
6
|
-
* Dùng cho layout 3 cột
|
|
6
|
+
* Dùng cho layout 3 cột (danh sách ▸ chi tiết ▸ panel phụ).
|
|
7
7
|
* Muốn nhớ kích thước: đặt `autoSaveId` trên `ResizablePanelGroup`.
|
|
8
8
|
*
|
|
9
9
|
* ─ MÀU: `--control-edge`, KHÔNG PHẢI `--border` ─
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Resizable (react-resizable-panels) — bố cục nhiều panel kéo giãn được.\n * Dùng cho layout 3 cột
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Resizable (react-resizable-panels) — bố cục nhiều panel kéo giãn được.\n * Dùng cho layout 3 cột (danh sách ▸ chi tiết ▸ panel phụ).\n * Muốn nhớ kích thước: đặt `autoSaveId` trên `ResizablePanelGroup`.\n *\n * ─ MÀU: `--control-edge`, KHÔNG PHẢI `--border` ─\n * Thanh chia này là CONTROL THẬT chứ không phải đường kẻ trang trí: react-resizable-panels\n * render nó với `role=\"separator\"` + `tabIndex=0`, kéo được bằng chuột lẫn bàn phím. Nên nó\n * chịu ngưỡng 3:1 của WCAG 1.4.11, và theo hợp đồng ở styles.css §RANH GIỚI thì ranh giới\n * control phải đi qua `--control-edge` — `--border` là bậc TRANG TRÍ, cố ý nhạt:\n *\n * `bg-border` ⇄ --background 1,16:1 (light) · 1,17:1 (dark) ✗\n * ⇄ --card 1,16:1 · 1,10:1 ✗ · ⇄ --muted 1,08:1 · 1,00:1 ✗\n * `bg-control-edge` ⇄ --background 3,83:1 · 4,94:1 ✓ · ⇄ --card 3,83:1 · 4,64:1 ✓\n * ⇄ --muted 3,58:1 · 4,21:1 ✓\n *\n * Núm `withHandle` cũng vậy: tô đặc `bg-control-edge` (tự khoanh vùng, không cần `border`),\n * vạch grip bên trong lấy `--control-knob` (= nền trang) — 3,83:1 / 4,94:1 trên núm ✓.\n */\n\nexport type ResizablePanelGroupProps = React.ComponentProps<typeof ResizablePrimitive.PanelGroup>;\nexport type ResizablePanelProps = React.ComponentProps<typeof ResizablePrimitive.Panel>;\nexport type ResizableHandleProps = React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n /** Hiện tay nắm có gạch dọc ở giữa thanh chia. */\n withHandle?: boolean;\n};\n\nfunction ResizablePanelGroup({ className, ...props }: ResizablePanelGroupProps) {\n return (\n <ResizablePrimitive.PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePanelProps) {\n return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({ withHandle, className, ...props }: ResizableHandleProps) {\n return (\n <ResizablePrimitive.PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"bg-control-edge relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-control-edge z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n <GripVerticalIcon className=\"size-2.5\" />\n </div>\n )}\n </ResizablePrimitive.PanelResizeHandle>\n );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"],"mappings":";AAoCI;AAlCJ,SAAS,wBAAwB;AAEjC,YAAY,wBAAwB;AACpC,SAAS,UAAU;AA6BnB,SAAS,oBAAoB,EAAE,WAAW,GAAG,MAAM,GAA6B;AAC9E,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,qEAAqE,SAAS;AAAA,MAC3F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe,EAAE,GAAG,MAAM,GAAwB;AACzD,SAAO,oBAAC,mBAAmB,OAAnB,EAAyB,aAAU,mBAAmB,GAAG,OAAO;AAC1E;AAEA,SAAS,gBAAgB,EAAE,YAAY,WAAW,GAAG,MAAM,GAAyB;AAClF,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH,wBACC,oBAAC,SAAI,WAAU,mFACb,8BAAC,oBAAiB,WAAU,YAAW,GACzC;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -127,7 +127,8 @@ function ContextMenuImpl({
|
|
|
127
127
|
}
|
|
128
128
|
const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);
|
|
129
129
|
const rowCount = rowIndicesArray.length;
|
|
130
|
-
await onRowsDelete2?.(rowIndicesArray);
|
|
130
|
+
const deleted = await onRowsDelete2?.(rowIndicesArray);
|
|
131
|
+
if (deleted === false) return;
|
|
131
132
|
toastSuccess(msg.toast.rowsDeleted(rowCount));
|
|
132
133
|
}, [propsRef, msg]);
|
|
133
134
|
return /* @__PURE__ */ jsxs(DropdownMenu, { open: contextMenu.open, onOpenChange: onContextMenuOpenChange, children: [
|
|
@@ -147,7 +148,7 @@ function ContextMenuImpl({
|
|
|
147
148
|
] }),
|
|
148
149
|
onRowsDelete && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
149
150
|
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
150
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onSelect: onDelete, children: [
|
|
151
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onSelect: onDelete, disabled: tableMeta?.readOnly, children: [
|
|
151
152
|
/* @__PURE__ */ jsx(Trash2Icon, {}),
|
|
152
153
|
msg.contextMenu.deleteRows
|
|
153
154
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-grid/data-grid-context-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ColumnDef, TableMeta } from \"@tanstack/react-table\";\nimport { CopyIcon, EraserIcon, ScissorsIcon, Trash2Icon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport { useDataGridMessages } from \"./data-grid-locale.js\";\nimport { toastSuccess } from \"./toast.js\";\nimport type { CellUpdate, ContextMenuState } from \"./types.js\";\nimport { getEmptyCellValue, parseCellKey } from \"./utils.js\";\n\ninterface DataGridContextMenuProps<TData> {\n tableMeta: TableMeta<TData>;\n columns: Array<ColumnDef<TData>>;\n contextMenu: ContextMenuState;\n}\n\nexport function DataGridContextMenu<TData>({ tableMeta, columns, contextMenu }: DataGridContextMenuProps<TData>) {\n const onContextMenuOpenChange = tableMeta?.onContextMenuOpenChange;\n const selectionState = tableMeta?.selectionState;\n const dataGridRef = tableMeta?.dataGridRef;\n const onDataUpdate = tableMeta?.onDataUpdate;\n const onRowsDelete = tableMeta?.onRowsDelete;\n const onCellsCopy = tableMeta?.onCellsCopy;\n const onCellsCut = tableMeta?.onCellsCut;\n\n if (!contextMenu.open) return null;\n\n return (\n <ContextMenu\n tableMeta={tableMeta}\n columns={columns}\n dataGridRef={dataGridRef}\n contextMenu={contextMenu}\n onContextMenuOpenChange={onContextMenuOpenChange}\n selectionState={selectionState}\n onDataUpdate={onDataUpdate}\n onRowsDelete={onRowsDelete}\n onCellsCopy={onCellsCopy}\n onCellsCut={onCellsCut}\n />\n );\n}\n\ninterface ContextMenuProps<TData>\n extends Pick<\n TableMeta<TData>,\n | \"dataGridRef\"\n | \"onContextMenuOpenChange\"\n | \"selectionState\"\n | \"onDataUpdate\"\n | \"onRowsDelete\"\n | \"onCellsCopy\"\n | \"onCellsCut\"\n | \"readOnly\"\n >,\n Required<Pick<TableMeta<TData>, \"contextMenu\">> {\n tableMeta: TableMeta<TData>;\n columns: Array<ColumnDef<TData>>;\n}\n\nconst ContextMenu = React.memo(ContextMenuImpl, (prev, next) => {\n if (prev.contextMenu.open !== next.contextMenu.open) return false;\n if (!next.contextMenu.open) return true;\n if (prev.contextMenu.x !== next.contextMenu.x) return false;\n if (prev.contextMenu.y !== next.contextMenu.y) return false;\n\n const prevSize = prev.selectionState?.selectedCells?.size ?? 0;\n const nextSize = next.selectionState?.selectedCells?.size ?? 0;\n if (prevSize !== nextSize) return false;\n\n return true;\n}) as typeof ContextMenuImpl;\n\nfunction ContextMenuImpl<TData>({\n tableMeta,\n columns,\n dataGridRef,\n contextMenu,\n onContextMenuOpenChange,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut\n}: ContextMenuProps<TData>) {\n const msg = useDataGridMessages();\n\n const propsRef = useAsRef({\n dataGridRef,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n columns\n });\n\n const triggerStyle = React.useMemo<React.CSSProperties>(\n () => ({\n position: \"fixed\",\n left: `${contextMenu.x}px`,\n top: `${contextMenu.y}px`,\n width: \"1px\",\n height: \"1px\",\n padding: 0,\n margin: 0,\n border: \"none\",\n background: \"transparent\",\n pointerEvents: \"none\",\n opacity: 0\n }),\n [contextMenu.x, contextMenu.y]\n );\n\n const onCloseAutoFocus: NonNullable<React.ComponentProps<typeof DropdownMenuContent>[\"onCloseAutoFocus\"]> =\n React.useCallback(\n (event) => {\n event.preventDefault();\n propsRef.current.dataGridRef?.current?.focus();\n },\n [propsRef]\n );\n\n const onCopy = React.useCallback(() => {\n propsRef.current.onCellsCopy?.();\n }, [propsRef]);\n\n const onCut = React.useCallback(() => {\n propsRef.current.onCellsCut?.();\n }, [propsRef]);\n\n const onClear = React.useCallback(() => {\n const { selectionState, columns, onDataUpdate } = propsRef.current;\n\n if (!selectionState?.selectedCells || selectionState.selectedCells.size === 0) return;\n\n const updates: Array<CellUpdate> = [];\n\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n // Get column from columns array\n const column = columns.find((col) => {\n if (col.id) return col.id === columnId;\n if (\"accessorKey\" in col) return col.accessorKey === columnId;\n return false;\n });\n const cellVariant = column?.meta?.cell?.variant;\n\n const emptyValue = getEmptyCellValue(cellVariant);\n\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate?.(updates);\n\n toastSuccess(msg.toast.cellsCleared(updates.length));\n }, [propsRef, msg]);\n\n const onDelete = React.useCallback(async () => {\n const { selectionState, onRowsDelete } = propsRef.current;\n\n if (!selectionState?.selectedCells || selectionState.selectedCells.size === 0) return;\n\n const rowIndices = new Set<number>();\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n\n const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);\n const rowCount = rowIndicesArray.length;\n\n await onRowsDelete?.(rowIndicesArray);\n\n toastSuccess(msg.toast.rowsDeleted(rowCount));\n }, [propsRef, msg]);\n\n return (\n <DropdownMenu open={contextMenu.open} onOpenChange={onContextMenuOpenChange}>\n <DropdownMenuTrigger style={triggerStyle} />\n <DropdownMenuContent data-grid-popover=\"\" align=\"start\" className=\"w-48\" onCloseAutoFocus={onCloseAutoFocus}>\n <DropdownMenuItem onSelect={onCopy}>\n <CopyIcon />\n {msg.contextMenu.copy}\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={onCut} disabled={tableMeta?.readOnly}>\n <ScissorsIcon />\n {msg.contextMenu.cut}\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={onClear} disabled={tableMeta?.readOnly}>\n <EraserIcon />\n {msg.contextMenu.clear}\n </DropdownMenuItem>\n {onRowsDelete && (\n <>\n <DropdownMenuSeparator />\n {/* `variant=\"destructive\"` của gói đã đi qua `--destructive-ink` (bậc CHỮ) cho cả\n chữ, icon và nền focus — đừng tự viết lại bằng `text-destructive` (bậc TÔ NỀN). */}\n <DropdownMenuItem variant=\"destructive\" onSelect={onDelete}>\n <Trash2Icon />\n {msg.contextMenu.deleteRows}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":["selectionState","columns","onDataUpdate","onRowsDelete"]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-grid/data-grid-context-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ColumnDef, TableMeta } from \"@tanstack/react-table\";\nimport { CopyIcon, EraserIcon, ScissorsIcon, Trash2Icon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport { useDataGridMessages } from \"./data-grid-locale.js\";\nimport { toastSuccess } from \"./toast.js\";\nimport type { CellUpdate, ContextMenuState } from \"./types.js\";\nimport { getEmptyCellValue, parseCellKey } from \"./utils.js\";\n\ninterface DataGridContextMenuProps<TData> {\n tableMeta: TableMeta<TData>;\n columns: Array<ColumnDef<TData>>;\n contextMenu: ContextMenuState;\n}\n\nexport function DataGridContextMenu<TData>({ tableMeta, columns, contextMenu }: DataGridContextMenuProps<TData>) {\n const onContextMenuOpenChange = tableMeta?.onContextMenuOpenChange;\n const selectionState = tableMeta?.selectionState;\n const dataGridRef = tableMeta?.dataGridRef;\n const onDataUpdate = tableMeta?.onDataUpdate;\n const onRowsDelete = tableMeta?.onRowsDelete;\n const onCellsCopy = tableMeta?.onCellsCopy;\n const onCellsCut = tableMeta?.onCellsCut;\n\n if (!contextMenu.open) return null;\n\n return (\n <ContextMenu\n tableMeta={tableMeta}\n columns={columns}\n dataGridRef={dataGridRef}\n contextMenu={contextMenu}\n onContextMenuOpenChange={onContextMenuOpenChange}\n selectionState={selectionState}\n onDataUpdate={onDataUpdate}\n onRowsDelete={onRowsDelete}\n onCellsCopy={onCellsCopy}\n onCellsCut={onCellsCut}\n />\n );\n}\n\ninterface ContextMenuProps<TData>\n extends Pick<\n TableMeta<TData>,\n | \"dataGridRef\"\n | \"onContextMenuOpenChange\"\n | \"selectionState\"\n | \"onDataUpdate\"\n | \"onRowsDelete\"\n | \"onCellsCopy\"\n | \"onCellsCut\"\n | \"readOnly\"\n >,\n Required<Pick<TableMeta<TData>, \"contextMenu\">> {\n tableMeta: TableMeta<TData>;\n columns: Array<ColumnDef<TData>>;\n}\n\nconst ContextMenu = React.memo(ContextMenuImpl, (prev, next) => {\n if (prev.contextMenu.open !== next.contextMenu.open) return false;\n if (!next.contextMenu.open) return true;\n if (prev.contextMenu.x !== next.contextMenu.x) return false;\n if (prev.contextMenu.y !== next.contextMenu.y) return false;\n\n const prevSize = prev.selectionState?.selectedCells?.size ?? 0;\n const nextSize = next.selectionState?.selectedCells?.size ?? 0;\n if (prevSize !== nextSize) return false;\n\n return true;\n}) as typeof ContextMenuImpl;\n\nfunction ContextMenuImpl<TData>({\n tableMeta,\n columns,\n dataGridRef,\n contextMenu,\n onContextMenuOpenChange,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut\n}: ContextMenuProps<TData>) {\n const msg = useDataGridMessages();\n\n const propsRef = useAsRef({\n dataGridRef,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n columns\n });\n\n const triggerStyle = React.useMemo<React.CSSProperties>(\n () => ({\n position: \"fixed\",\n left: `${contextMenu.x}px`,\n top: `${contextMenu.y}px`,\n width: \"1px\",\n height: \"1px\",\n padding: 0,\n margin: 0,\n border: \"none\",\n background: \"transparent\",\n pointerEvents: \"none\",\n opacity: 0\n }),\n [contextMenu.x, contextMenu.y]\n );\n\n const onCloseAutoFocus: NonNullable<React.ComponentProps<typeof DropdownMenuContent>[\"onCloseAutoFocus\"]> =\n React.useCallback(\n (event) => {\n event.preventDefault();\n propsRef.current.dataGridRef?.current?.focus();\n },\n [propsRef]\n );\n\n const onCopy = React.useCallback(() => {\n propsRef.current.onCellsCopy?.();\n }, [propsRef]);\n\n const onCut = React.useCallback(() => {\n propsRef.current.onCellsCut?.();\n }, [propsRef]);\n\n const onClear = React.useCallback(() => {\n const { selectionState, columns, onDataUpdate } = propsRef.current;\n\n if (!selectionState?.selectedCells || selectionState.selectedCells.size === 0) return;\n\n const updates: Array<CellUpdate> = [];\n\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n // Get column from columns array\n const column = columns.find((col) => {\n if (col.id) return col.id === columnId;\n if (\"accessorKey\" in col) return col.accessorKey === columnId;\n return false;\n });\n const cellVariant = column?.meta?.cell?.variant;\n\n const emptyValue = getEmptyCellValue(cellVariant);\n\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate?.(updates);\n\n toastSuccess(msg.toast.cellsCleared(updates.length));\n }, [propsRef, msg]);\n\n const onDelete = React.useCallback(async () => {\n const { selectionState, onRowsDelete } = propsRef.current;\n\n if (!selectionState?.selectedCells || selectionState.selectedCells.size === 0) return;\n\n const rowIndices = new Set<number>();\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n\n const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);\n const rowCount = rowIndicesArray.length;\n\n // `false` = không xoá gì: app huỷ ở hộp xác nhận của nó, hoặc lưới không có gì để xoá. Báo\n // \"Đã xoá N dòng\" ở đây là nói dối đúng lúc người dùng vừa bấm \"Giữ lại\".\n const deleted = await onRowsDelete?.(rowIndicesArray);\n if (deleted === false) return;\n\n toastSuccess(msg.toast.rowsDeleted(rowCount));\n }, [propsRef, msg]);\n\n return (\n <DropdownMenu open={contextMenu.open} onOpenChange={onContextMenuOpenChange}>\n <DropdownMenuTrigger style={triggerStyle} />\n <DropdownMenuContent data-grid-popover=\"\" align=\"start\" className=\"w-48\" onCloseAutoFocus={onCloseAutoFocus}>\n <DropdownMenuItem onSelect={onCopy}>\n <CopyIcon />\n {msg.contextMenu.copy}\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={onCut} disabled={tableMeta?.readOnly}>\n <ScissorsIcon />\n {msg.contextMenu.cut}\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={onClear} disabled={tableMeta?.readOnly}>\n <EraserIcon />\n {msg.contextMenu.clear}\n </DropdownMenuItem>\n {onRowsDelete && (\n <>\n <DropdownMenuSeparator />\n {/* `variant=\"destructive\"` của gói đã đi qua `--destructive-ink` (bậc CHỮ) cho cả\n chữ, icon và nền focus — đừng tự viết lại bằng `text-destructive` (bậc TÔ NỀN). */}\n {/* Tắt ở lưới chỉ đọc như \"Cắt\"/\"Xoá nội dung\" — trước `3.3.3` mục này bấm được, không\n xoá gì (hook trả về sớm) mà vẫn báo \"Đã xoá N dòng\". */}\n <DropdownMenuItem variant=\"destructive\" onSelect={onDelete} disabled={tableMeta?.readOnly}>\n <Trash2Icon />\n {msg.contextMenu.deleteRows}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":["selectionState","columns","onDataUpdate","onRowsDelete"]}
|
|
@@ -49,7 +49,11 @@ type CellOpts = {
|
|
|
49
49
|
} | {
|
|
50
50
|
variant: "checkbox";
|
|
51
51
|
} | {
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Giá trị lưu dạng chuỗi "yyyy-MM-dd"; hiển thị dạng ngắn theo NGÔN NGỮ của lưới (định dạng
|
|
54
|
+
* `P` của date-fns — `vi` ra `dd/MM/yyyy`, `en` ra `MM/dd/yyyy`). Trước `3.0.0` luôn là
|
|
55
|
+
* `dd/MM/yyyy` bất kể ngôn ngữ — xem `formatDate` trong `utils.ts`.
|
|
56
|
+
*/
|
|
53
57
|
variant: "date";
|
|
54
58
|
} | {
|
|
55
59
|
variant: "url";
|
|
@@ -99,7 +103,8 @@ declare module "@tanstack/react-table" {
|
|
|
99
103
|
onRowHeightChange?: (value: RowHeightValue) => void;
|
|
100
104
|
onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;
|
|
101
105
|
onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;
|
|
102
|
-
|
|
106
|
+
/** `false` (hoặc resolve `false`) = không có dòng nào bị xoá — xem `onRowsDelete` của `useDataGrid`. */
|
|
107
|
+
onRowsDelete?: (rowIndices: number[]) => void | boolean | Promise<void> | Promise<boolean | undefined>;
|
|
103
108
|
onColumnClick?: (columnId: string) => void;
|
|
104
109
|
onCellClick?: (rowIndex: number, columnId: string, event?: React.MouseEvent) => void;
|
|
105
110
|
onCellDoubleClick?: (rowIndex: number, columnId: string) => void;
|
|
@@ -27,8 +27,23 @@ interface UseDataGridProps<TData> extends Omit<TableOptions<TData>, "pageCount"
|
|
|
27
27
|
onRowAdd?: (event?: React.MouseEvent<HTMLDivElement>) => Partial<CellPosition> | Promise<Partial<CellPosition> | null> | null;
|
|
28
28
|
/** Dán nhiều dòng hơn số dòng hiện có → thêm `count` dòng trong MỘT lần gọi API */
|
|
29
29
|
onRowsAdd?: (count: number) => void | Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Xoá dòng (menu chuột phải hoặc Ctrl+Backspace) — nhận cả bản ghi lẫn chỉ số dòng.
|
|
32
|
+
*
|
|
33
|
+
* Lưới CHỜ promise rồi mới xoá lựa chọn, bỏ trạng thái sửa, đưa focus về dòng kế và (ở menu
|
|
34
|
+
* chuột phải) báo "Đã xoá N dòng". Trả (hoặc resolve) **`false`** khi KHÔNG xoá — người dùng bấm
|
|
35
|
+
* Huỷ ở hộp xác nhận app tự dựng: lưới giữ nguyên lựa chọn và không báo gì. Trả `void` như trước
|
|
36
|
+
* `3.3.3` thì lưới coi là đã xoá.
|
|
37
|
+
*
|
|
38
|
+
* ⚠ Trước `3.3.3` không có đường nào báo "đã huỷ": app hỏi lại trong callback rồi resolve khi hộp
|
|
39
|
+
* đóng — kể cả khi người dùng bấm "Giữ lại" — thì lưới vẫn báo "Đã xoá 1 dòng" và bỏ tick mọi dòng.
|
|
40
|
+
*
|
|
41
|
+
* `rowIndices` là vị trí trong thứ tự ĐANG HIỆN (sau sắp xếp và lọc), không phải chỉ số trong `data`:
|
|
42
|
+
* xoá theo `rows` hoặc id của chúng, đừng cắt mảng dữ liệu bằng `rowIndices`.
|
|
43
|
+
* ⚠ Trước `3.3.4`, `Ctrl+Backspace` khi có dòng đang tick trên lưới đang sắp hoặc lọc đưa NHẦM dòng vào
|
|
44
|
+
* đây (lấy vị trí trong `data` rồi tra theo thứ tự đang hiện). Menu chuột phải không dính.
|
|
45
|
+
*/
|
|
46
|
+
onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | boolean | Promise<void> | Promise<boolean | undefined>;
|
|
32
47
|
/** Chạy TRƯỚC khi áp dụng dữ liệu dán — dùng để gọi API bulk update nếu cần */
|
|
33
48
|
onPaste?: (updates: Array<CellUpdate>) => void | Promise<void>;
|
|
34
49
|
/**
|
|
@@ -107,7 +122,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
|
|
|
107
122
|
pasteDialog: PasteDialogState;
|
|
108
123
|
onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
|
|
109
124
|
adjustLayout: boolean;
|
|
110
|
-
locale: {
|
|
125
|
+
locale: "en" | "vi" | {
|
|
111
126
|
grid: {
|
|
112
127
|
label: string;
|
|
113
128
|
addRow: string;
|
|
@@ -382,7 +397,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
|
|
|
382
397
|
shortcuts: string;
|
|
383
398
|
};
|
|
384
399
|
};
|
|
385
|
-
} |
|
|
400
|
+
} | undefined;
|
|
386
401
|
};
|
|
387
402
|
|
|
388
403
|
export { type UseDataGridProps, useDataGrid };
|
|
@@ -881,10 +881,10 @@ function useDataGrid({
|
|
|
881
881
|
);
|
|
882
882
|
const onRowsDelete = React.useCallback(
|
|
883
883
|
async (rowIndices) => {
|
|
884
|
-
if (propsRef.current.readOnly || !propsRef.current.onRowsDelete || rowIndices.length === 0) return;
|
|
884
|
+
if (propsRef.current.readOnly || !propsRef.current.onRowsDelete || rowIndices.length === 0) return false;
|
|
885
885
|
const currentTable = tableRef.current;
|
|
886
886
|
const rows = currentTable?.getRowModel().rows;
|
|
887
|
-
if (!rows || rows.length === 0) return;
|
|
887
|
+
if (!rows || rows.length === 0) return false;
|
|
888
888
|
const currentState = store.getState();
|
|
889
889
|
const currentFocusedColumn = currentState.focusedCell?.columnId ?? navigableColumnIds[0];
|
|
890
890
|
const minDeletedRowIndex = Math.min(...rowIndices);
|
|
@@ -895,7 +895,8 @@ function useDataGrid({
|
|
|
895
895
|
rowsToDelete.push(row.original);
|
|
896
896
|
}
|
|
897
897
|
}
|
|
898
|
-
await propsRef.current.onRowsDelete(rowsToDelete, rowIndices);
|
|
898
|
+
const result = await propsRef.current.onRowsDelete(rowsToDelete, rowIndices);
|
|
899
|
+
if (result === false) return false;
|
|
899
900
|
store.batch(() => {
|
|
900
901
|
store.setState("selectionState", {
|
|
901
902
|
selectedCells: /* @__PURE__ */ new Set(),
|
|
@@ -914,6 +915,7 @@ function useDataGrid({
|
|
|
914
915
|
focusCell(targetRowIndex, currentFocusedColumn);
|
|
915
916
|
}
|
|
916
917
|
});
|
|
918
|
+
return true;
|
|
917
919
|
},
|
|
918
920
|
[propsRef, store, navigableColumnIds, focusCell]
|
|
919
921
|
);
|
|
@@ -1920,9 +1922,9 @@ function useDataGrid({
|
|
|
1920
1922
|
if (selectedRowIds.length > 0) {
|
|
1921
1923
|
const currentTable = tableRef.current;
|
|
1922
1924
|
const rows = currentTable?.getRowModel().rows ?? [];
|
|
1923
|
-
for (const row of rows) {
|
|
1925
|
+
for (const [visualIndex, row] of rows.entries()) {
|
|
1924
1926
|
if (currentState.rowSelection[row.id]) {
|
|
1925
|
-
rowIndices.add(
|
|
1927
|
+
rowIndices.add(visualIndex);
|
|
1926
1928
|
}
|
|
1927
1929
|
}
|
|
1928
1930
|
} else if (currentState.selectionState.selectedCells.size > 0) {
|