@comitor/ui 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -30
- package/dist/chart/chart-colors.d.ts +56 -0
- package/dist/chart/chart-colors.js +21 -0
- package/dist/chart/chart-colors.js.map +1 -0
- package/dist/chart/chart.d.ts +40 -0
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +21 -43
- package/dist/chart/comitor-chart.js +13 -24
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +2 -1
- package/dist/chart/index.js +1 -0
- package/dist/chart/index.js.map +1 -1
- package/dist/components/composite/data-table-constants.d.ts +40 -0
- package/dist/components/composite/data-table-constants.js +13 -0
- package/dist/components/composite/data-table-constants.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +2 -27
- package/dist/components/composite/data-table.js +1 -10
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker-constants.d.ts +21 -0
- package/dist/components/composite/date-picker-constants.js +39 -0
- package/dist/components/composite/date-picker-constants.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +2 -18
- package/dist/components/composite/date-picker.js +6 -48
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/form-field-constants.d.ts +26 -0
- package/dist/components/composite/form-field-constants.js +13 -0
- package/dist/components/composite/form-field-constants.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +1 -13
- package/dist/components/composite/form-field.js +1 -11
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +52 -0
- package/dist/components/composite/search-filter-bar-constants.js +18 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +3 -37
- package/dist/components/composite/search-filter-bar.js +2 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +56 -0
- package/dist/components/composite/table-pagination-constants.js +18 -0
- package/dist/components/composite/table-pagination-constants.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +2 -43
- package/dist/components/composite/table-pagination.js +4 -15
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/ui/badge.js +3 -3
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/button.js +4 -4
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/checkbox.js +2 -2
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/input-otp.js +1 -1
- package/dist/components/ui/input-otp.js.map +1 -1
- package/dist/components/ui/navigation-menu.js +1 -1
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/radio-group.js +1 -1
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/resizable.js +1 -1
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/contrast-constants.d.ts +30 -0
- package/dist/shell/contrast-constants.js +16 -0
- package/dist/shell/contrast-constants.js.map +1 -0
- package/dist/shell/contrast-provider.d.ts +60 -0
- package/dist/shell/contrast-provider.js +61 -0
- package/dist/shell/contrast-provider.js.map +1 -0
- package/dist/shell/contrast-toggle.d.ts +24 -0
- package/dist/shell/contrast-toggle.js +29 -0
- package/dist/shell/contrast-toggle.js.map +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +3 -0
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/shell-context.js +3 -2
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/tokens.d.ts +170 -70
- package/dist/tokens.js +124 -47
- package/dist/tokens.js.map +1 -1
- package/package.json +3 -2
- package/styles.css +255 -120
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
const DEFAULT_SEARCH_FILTER_BAR_LABELS = {
|
|
3
|
+
searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
|
|
4
|
+
unitLabel: "m\u1EE5c",
|
|
5
|
+
clearSearch: "X\xF3a t\u1EEB kh\xF3a",
|
|
6
|
+
filterCount: (count) => `${count} b\u1ED9 l\u1ECDc`,
|
|
7
|
+
clearFilters: "X\xF3a l\u1ECDc",
|
|
8
|
+
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9
|
+
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
10
|
+
total !== void 0 ? ` / ${total}` : "",
|
|
11
|
+
" ",
|
|
12
|
+
unitLabel
|
|
13
|
+
] })
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_SEARCH_FILTER_BAR_LABELS
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=search-filter-bar-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/search-filter-bar-constants.tsx"],"sourcesContent":["/**\n * Nhãn mặc định của `SearchFilterBar` (`.tsx` vì `resultSummary` trả JSX).\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `search-filter-bar.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\nimport type { ReactNode } from \"react\";\n\n/**\n * Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.\n *\n * `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);\n * các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào\n * chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.\n */\nexport interface SearchFilterBarLabels {\n /** Placeholder + `aria-label` của ô tìm */\n searchPlaceholder: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên\n * phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng\n * bảng (\"việc\", \"liên hệ\") nên vẫn là prop rời và vẫn thắng `labels`.\n */\n unitLabel: string;\n /** `sr-only` của nút xoá từ khoá trong ô tìm */\n clearSearch: string;\n /** Huy hiệu đếm bộ lọc đang bật: \"3 bộ lọc\" */\n filterCount: (count: number) => string;\n /** Nhãn nút xoá toàn bộ bộ lọc */\n clearFilters: string;\n /**\n * Dòng đếm kết quả sau lọc: \"**3** / 57 mục\" (`total` bỏ trống thì chỉ \"**3** mục\").\n *\n * Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không\n * nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.\n * Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.\n */\n resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${count} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` / ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n"],"mappings":"AAwDI,mBACE,KADF;AAPG,MAAM,mCAA0D;AAAA,EACrE,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,KAAK;AAAA,EAChC,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,uBAAY;AAAA,IACzE,UAAU,SAAY,MAAM,KAAK,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KAC9C;AAEJ;","names":[]}
|
|
@@ -1,41 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Ref, ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { SearchFilterBarLabels } from './search-filter-bar-constants.js';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.
|
|
6
|
-
*
|
|
7
|
-
* `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);
|
|
8
|
-
* các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào
|
|
9
|
-
* chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.
|
|
10
|
-
*/
|
|
11
|
-
interface SearchFilterBarLabels {
|
|
12
|
-
/** Placeholder + `aria-label` của ô tìm */
|
|
13
|
-
searchPlaceholder: string;
|
|
14
|
-
/**
|
|
15
|
-
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
16
|
-
*
|
|
17
|
-
* Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên
|
|
18
|
-
* phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng
|
|
19
|
-
* bảng ("việc", "liên hệ") nên vẫn là prop rời và vẫn thắng `labels`.
|
|
20
|
-
*/
|
|
21
|
-
unitLabel: string;
|
|
22
|
-
/** `sr-only` của nút xoá từ khoá trong ô tìm */
|
|
23
|
-
clearSearch: string;
|
|
24
|
-
/** Huy hiệu đếm bộ lọc đang bật: "3 bộ lọc" */
|
|
25
|
-
filterCount: (count: number) => string;
|
|
26
|
-
/** Nhãn nút xoá toàn bộ bộ lọc */
|
|
27
|
-
clearFilters: string;
|
|
28
|
-
/**
|
|
29
|
-
* Dòng đếm kết quả sau lọc: "**3** / 57 mục" (`total` bỏ trống thì chỉ "**3** mục").
|
|
30
|
-
*
|
|
31
|
-
* Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không
|
|
32
|
-
* nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.
|
|
33
|
-
* Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.
|
|
34
|
-
*/
|
|
35
|
-
resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;
|
|
36
|
-
}
|
|
37
|
-
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
38
|
-
declare const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels;
|
|
39
5
|
interface SearchFilterBarProps {
|
|
40
6
|
/** Giá trị ô tìm kiếm (controlled) */
|
|
41
7
|
searchValue: string;
|
|
@@ -73,4 +39,4 @@ interface SearchFilterBarProps {
|
|
|
73
39
|
*/
|
|
74
40
|
declare function SearchFilterBar({ searchValue, onSearchChange, searchPlaceholder, debounceMs, searchInputRef, searchInputProps, children, actions, activeFilterCount, onClearFilters, resultCount, total, unitLabel: unitLabelProp, labels: labelsProp, className }: SearchFilterBarProps): React.JSX.Element;
|
|
75
41
|
|
|
76
|
-
export {
|
|
42
|
+
export { SearchFilterBar, type SearchFilterBarProps };
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
6
|
import { Badge } from "../ui/badge.js";
|
|
7
7
|
import { Button } from "../ui/button.js";
|
|
8
8
|
import { Input } from "../ui/input.js";
|
|
9
|
-
|
|
10
|
-
searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
|
|
11
|
-
unitLabel: "m\u1EE5c",
|
|
12
|
-
clearSearch: "X\xF3a t\u1EEB kh\xF3a",
|
|
13
|
-
filterCount: (count) => `${count} b\u1ED9 l\u1ECDc`,
|
|
14
|
-
clearFilters: "X\xF3a l\u1ECDc",
|
|
15
|
-
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
16
|
-
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
17
|
-
total !== void 0 ? ` / ${total}` : "",
|
|
18
|
-
" ",
|
|
19
|
-
unitLabel
|
|
20
|
-
] })
|
|
21
|
-
};
|
|
9
|
+
import { DEFAULT_SEARCH_FILTER_BAR_LABELS } from "./search-filter-bar-constants.js";
|
|
22
10
|
function SearchFilterBar({
|
|
23
11
|
searchValue,
|
|
24
12
|
onSearchChange,
|
|
@@ -141,7 +129,6 @@ function SearchFilterBar({
|
|
|
141
129
|
] });
|
|
142
130
|
}
|
|
143
131
|
export {
|
|
144
|
-
DEFAULT_SEARCH_FILTER_BAR_LABELS,
|
|
145
132
|
SearchFilterBar
|
|
146
133
|
};
|
|
147
134
|
//# sourceMappingURL=search-filter-bar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/search-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { type ComponentPropsWithoutRef, type ReactNode, type Ref, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Input } from \"../ui/input.js\";\n\n/**\n * Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.\n *\n * `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);\n * các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào\n * chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.\n */\nexport interface SearchFilterBarLabels {\n /** Placeholder + `aria-label` của ô tìm */\n searchPlaceholder: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên\n * phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng\n * bảng (\"việc\", \"liên hệ\") nên vẫn là prop rời và vẫn thắng `labels`.\n */\n unitLabel: string;\n /** `sr-only` của nút xoá từ khoá trong ô tìm */\n clearSearch: string;\n /** Huy hiệu đếm bộ lọc đang bật: \"3 bộ lọc\" */\n filterCount: (count: number) => string;\n /** Nhãn nút xoá toàn bộ bộ lọc */\n clearFilters: string;\n /**\n * Dòng đếm kết quả sau lọc: \"**3** / 57 mục\" (`total` bỏ trống thì chỉ \"**3** mục\").\n *\n * Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không\n * nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.\n * Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.\n */\n resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${count} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` / ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n\nexport interface SearchFilterBarProps {\n /** Giá trị ô tìm kiếm (controlled) */\n searchValue: string;\n onSearchChange: (value: string) => void;\n searchPlaceholder?: string;\n /** Trễ trước khi gọi `onSearchChange` (ms). 0 = gọi ngay. Gõ tìm gọi API thì đặt 250–350. */\n debounceMs?: number;\n searchInputRef?: Ref<HTMLInputElement>;\n /** Props đặt thẳng lên ô nhập (id, name, aria-label, enterKeyHint…) */\n searchInputProps?: ComponentPropsWithoutRef<\"input\">;\n\n /** Slot bộ lọc cạnh ô tìm (Select, Combobox, DatePicker…) */\n children?: ReactNode;\n /** Slot hành động bên phải (nút Tạo mới, menu cột…) */\n actions?: ReactNode;\n\n /** Số bộ lọc đang bật — hiện huy hiệu và mở dòng đếm kết quả */\n activeFilterCount?: number;\n onClearFilters?: () => void;\n /** Số bản ghi sau lọc */\n resultCount?: number;\n /** Tổng số bản ghi khi chưa lọc */\n total?: number;\n /** Đơn vị đếm theo nội dung bảng, vd \"việc\", \"liên hệ\" — mặc định `labels.unitLabel` (\"mục\") */\n unitLabel?: string;\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `SearchFilterBarLabels` */\n labels?: Partial<SearchFilterBarLabels>;\n className?: string;\n}\n\n/**\n * Thanh tìm kiếm + lọc chuẩn cho trang danh sách.\n *\n * Ô tìm giữ giá trị cục bộ và chỉ đẩy lên sau `debounceMs` — nếu bind thẳng vào state của trang,\n * mỗi phím gõ là một lần render cả bảng (và thường là một request).\n *\n * Dòng đếm kết quả có `min-h-7` cố định để việc bật/tắt lọc không làm bảng nhảy lên xuống.\n */\nexport function SearchFilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n debounceMs = 250,\n searchInputRef,\n searchInputProps,\n children,\n actions,\n activeFilterCount = 0,\n onClearFilters,\n resultCount,\n total,\n unitLabel: unitLabelProp,\n labels: labelsProp,\n className\n}: SearchFilterBarProps) {\n const labels = { ...DEFAULT_SEARCH_FILTER_BAR_LABELS, ...labelsProp };\n const placeholder = searchPlaceholder ?? labels.searchPlaceholder;\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const [localValue, setLocalValue] = useState(searchValue);\n const lastEmittedRef = useRef(searchValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const onSearchChangeRef = useRef(onSearchChange);\n\n useEffect(() => {\n onSearchChangeRef.current = onSearchChange;\n });\n\n // Đồng bộ khi giá trị bị đổi từ bên ngoài (nút \"Xóa lọc\", điều hướng lại trang) — nhưng\n // không đè lên những gì người dùng đang gõ dở.\n useEffect(() => {\n if (searchValue !== lastEmittedRef.current) {\n lastEmittedRef.current = searchValue;\n setLocalValue(searchValue);\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [searchValue]);\n\n useEffect(\n () => () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n },\n []\n );\n\n const emit = (value: string) => {\n setLocalValue(value);\n if (timerRef.current) clearTimeout(timerRef.current);\n if (!debounceMs) {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n return;\n }\n timerRef.current = setTimeout(() => {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n }, debounceMs);\n };\n\n const clearSearch = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setLocalValue(\"\");\n lastEmittedRef.current = \"\";\n onSearchChangeRef.current(\"\");\n };\n\n const hasFilters = activeFilterCount > 0;\n const showCount = hasFilters || Boolean(localValue);\n\n return (\n <div data-slot=\"search-filter-bar\" className={cn(\"flex flex-col gap-2\", className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[12rem] flex-1 sm:max-w-sm\">\n <Search\n aria-hidden=\"true\"\n className=\"absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n ref={searchInputRef}\n type=\"search\"\n value={localValue}\n placeholder={placeholder}\n aria-label={placeholder}\n onChange={(event) => emit(event.target.value)}\n className=\"pl-9 pr-8 [&::-webkit-search-cancel-button]:hidden\"\n {...searchInputProps}\n />\n {localValue && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={clearSearch}\n className=\"absolute right-1 top-1/2 size-6 -translate-y-1/2\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{labels.clearSearch}</span>\n </Button>\n )}\n </div>\n\n {children}\n\n {hasFilters && (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SlidersHorizontal aria-hidden=\"true\" />\n {labels.filterCount(activeFilterCount)}\n </Badge>\n )}\n\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n\n {showCount && (\n <div role=\"status\" className=\"flex min-h-7 items-center gap-2 text-sm text-muted-foreground\">\n {resultCount !== undefined && <span>{labels.resultSummary(resultCount, total, unitLabel)}</span>}\n {hasFilters && onClearFilters && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onClearFilters} className=\"h-7 gap-1.5 px-2\">\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n {labels.clearFilters}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/search-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { type ComponentPropsWithoutRef, type ReactNode, type Ref, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Input } from \"../ui/input.js\";\nimport { DEFAULT_SEARCH_FILTER_BAR_LABELS, type SearchFilterBarLabels } from \"./search-filter-bar-constants.js\";\n\nexport interface SearchFilterBarProps {\n /** Giá trị ô tìm kiếm (controlled) */\n searchValue: string;\n onSearchChange: (value: string) => void;\n searchPlaceholder?: string;\n /** Trễ trước khi gọi `onSearchChange` (ms). 0 = gọi ngay. Gõ tìm gọi API thì đặt 250–350. */\n debounceMs?: number;\n searchInputRef?: Ref<HTMLInputElement>;\n /** Props đặt thẳng lên ô nhập (id, name, aria-label, enterKeyHint…) */\n searchInputProps?: ComponentPropsWithoutRef<\"input\">;\n\n /** Slot bộ lọc cạnh ô tìm (Select, Combobox, DatePicker…) */\n children?: ReactNode;\n /** Slot hành động bên phải (nút Tạo mới, menu cột…) */\n actions?: ReactNode;\n\n /** Số bộ lọc đang bật — hiện huy hiệu và mở dòng đếm kết quả */\n activeFilterCount?: number;\n onClearFilters?: () => void;\n /** Số bản ghi sau lọc */\n resultCount?: number;\n /** Tổng số bản ghi khi chưa lọc */\n total?: number;\n /** Đơn vị đếm theo nội dung bảng, vd \"việc\", \"liên hệ\" — mặc định `labels.unitLabel` (\"mục\") */\n unitLabel?: string;\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `SearchFilterBarLabels` */\n labels?: Partial<SearchFilterBarLabels>;\n className?: string;\n}\n\n/**\n * Thanh tìm kiếm + lọc chuẩn cho trang danh sách.\n *\n * Ô tìm giữ giá trị cục bộ và chỉ đẩy lên sau `debounceMs` — nếu bind thẳng vào state của trang,\n * mỗi phím gõ là một lần render cả bảng (và thường là một request).\n *\n * Dòng đếm kết quả có `min-h-7` cố định để việc bật/tắt lọc không làm bảng nhảy lên xuống.\n */\nexport function SearchFilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n debounceMs = 250,\n searchInputRef,\n searchInputProps,\n children,\n actions,\n activeFilterCount = 0,\n onClearFilters,\n resultCount,\n total,\n unitLabel: unitLabelProp,\n labels: labelsProp,\n className\n}: SearchFilterBarProps) {\n const labels = { ...DEFAULT_SEARCH_FILTER_BAR_LABELS, ...labelsProp };\n const placeholder = searchPlaceholder ?? labels.searchPlaceholder;\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const [localValue, setLocalValue] = useState(searchValue);\n const lastEmittedRef = useRef(searchValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const onSearchChangeRef = useRef(onSearchChange);\n\n useEffect(() => {\n onSearchChangeRef.current = onSearchChange;\n });\n\n // Đồng bộ khi giá trị bị đổi từ bên ngoài (nút \"Xóa lọc\", điều hướng lại trang) — nhưng\n // không đè lên những gì người dùng đang gõ dở.\n useEffect(() => {\n if (searchValue !== lastEmittedRef.current) {\n lastEmittedRef.current = searchValue;\n setLocalValue(searchValue);\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [searchValue]);\n\n useEffect(\n () => () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n },\n []\n );\n\n const emit = (value: string) => {\n setLocalValue(value);\n if (timerRef.current) clearTimeout(timerRef.current);\n if (!debounceMs) {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n return;\n }\n timerRef.current = setTimeout(() => {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n }, debounceMs);\n };\n\n const clearSearch = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setLocalValue(\"\");\n lastEmittedRef.current = \"\";\n onSearchChangeRef.current(\"\");\n };\n\n const hasFilters = activeFilterCount > 0;\n const showCount = hasFilters || Boolean(localValue);\n\n return (\n <div data-slot=\"search-filter-bar\" className={cn(\"flex flex-col gap-2\", className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[12rem] flex-1 sm:max-w-sm\">\n <Search\n aria-hidden=\"true\"\n className=\"absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n ref={searchInputRef}\n type=\"search\"\n value={localValue}\n placeholder={placeholder}\n aria-label={placeholder}\n onChange={(event) => emit(event.target.value)}\n className=\"pl-9 pr-8 [&::-webkit-search-cancel-button]:hidden\"\n {...searchInputProps}\n />\n {localValue && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={clearSearch}\n className=\"absolute right-1 top-1/2 size-6 -translate-y-1/2\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{labels.clearSearch}</span>\n </Button>\n )}\n </div>\n\n {children}\n\n {hasFilters && (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SlidersHorizontal aria-hidden=\"true\" />\n {labels.filterCount(activeFilterCount)}\n </Badge>\n )}\n\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n\n {showCount && (\n <div role=\"status\" className=\"flex min-h-7 items-center gap-2 text-sm text-muted-foreground\">\n {resultCount !== undefined && <span>{labels.resultSummary(resultCount, total, unitLabel)}</span>}\n {hasFilters && onClearFilters && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onClearFilters} className=\"h-7 gap-1.5 px-2\">\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n {labels.clearFilters}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cỡ trang và nhãn mặc định của `TablePagination`.
|
|
3
|
+
*
|
|
4
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
5
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
6
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
7
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
8
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `table-pagination.tsx` chỉ được `import`,
|
|
11
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
12
|
+
*/
|
|
13
|
+
/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */
|
|
14
|
+
declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly [20, 50, 100];
|
|
15
|
+
/**
|
|
16
|
+
* Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.
|
|
17
|
+
*
|
|
18
|
+
* Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của
|
|
19
|
+
* cụm nút ("Trang đầu/trước/sau/cuối"), `aria-label` của ô chọn cỡ trang và hai dòng đếm
|
|
20
|
+
* (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`
|
|
21
|
+
* (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn
|
|
22
|
+
* nhất quán tiếng Việt.
|
|
23
|
+
*
|
|
24
|
+
* Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc
|
|
25
|
+
* chắn sai trật tự từ với các ngôn ngữ khác.
|
|
26
|
+
*/
|
|
27
|
+
interface TablePaginationLabels {
|
|
28
|
+
/** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */
|
|
29
|
+
region: string;
|
|
30
|
+
/**
|
|
31
|
+
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
32
|
+
*
|
|
33
|
+
* Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`
|
|
34
|
+
* mà vẫn nhận "mục" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn
|
|
35
|
+
* prop `unitLabel` là chuỗi theo NỘI DUNG ("việc", "liên hệ") — mỗi bảng một khác, nên vẫn
|
|
36
|
+
* là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.
|
|
37
|
+
*/
|
|
38
|
+
unitLabel: string;
|
|
39
|
+
/** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */
|
|
40
|
+
pageSizePrefix: string;
|
|
41
|
+
/** `aria-label` của ô chọn cỡ trang */
|
|
42
|
+
pageSizeSelect: (unitLabel: string) => string;
|
|
43
|
+
/** Dòng đếm bản ghi: "1–20 trên tổng 57 mục" */
|
|
44
|
+
range: (start: number, end: number, total: number, unitLabel: string) => string;
|
|
45
|
+
/** Dòng đếm trang: "Trang 2 / 5" */
|
|
46
|
+
pageStatus: (page: number, pageCount: number) => string;
|
|
47
|
+
/** `sr-only` của 4 nút điều hướng */
|
|
48
|
+
firstPage: string;
|
|
49
|
+
previousPage: string;
|
|
50
|
+
nextPage: string;
|
|
51
|
+
lastPage: string;
|
|
52
|
+
}
|
|
53
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
54
|
+
declare const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels;
|
|
55
|
+
|
|
56
|
+
export { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_TABLE_PAGINATION_LABELS, type TablePaginationLabels };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100];
|
|
2
|
+
const DEFAULT_TABLE_PAGINATION_LABELS = {
|
|
3
|
+
region: "Ph\xE2n trang",
|
|
4
|
+
unitLabel: "m\u1EE5c",
|
|
5
|
+
pageSizePrefix: "Hi\u1EC3n th\u1ECB",
|
|
6
|
+
pageSizeSelect: (unitLabel) => `S\u1ED1 ${unitLabel} m\u1ED7i trang`,
|
|
7
|
+
range: (start, end, total, unitLabel) => `${start}\u2013${end} tr\xEAn t\u1ED5ng ${total} ${unitLabel}`,
|
|
8
|
+
pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
9
|
+
firstPage: "Trang \u0111\u1EA7u",
|
|
10
|
+
previousPage: "Trang tr\u01B0\u1EDBc",
|
|
11
|
+
nextPage: "Trang sau",
|
|
12
|
+
lastPage: "Trang cu\u1ED1i"
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
16
|
+
DEFAULT_TABLE_PAGINATION_LABELS
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=table-pagination-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/table-pagination-constants.ts"],"sourcesContent":["/**\n * Cỡ trang và nhãn mặc định của `TablePagination`.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `table-pagination.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100] as const;\n\n/**\n * Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.\n *\n * Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của\n * cụm nút (\"Trang đầu/trước/sau/cuối\"), `aria-label` của ô chọn cỡ trang và hai dòng đếm\n * (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`\n * (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn\n * nhất quán tiếng Việt.\n *\n * Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc\n * chắn sai trật tự từ với các ngôn ngữ khác.\n */\nexport interface TablePaginationLabels {\n /** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */\n region: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`\n * mà vẫn nhận \"mục\" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn\n * prop `unitLabel` là chuỗi theo NỘI DUNG (\"việc\", \"liên hệ\") — mỗi bảng một khác, nên vẫn\n * là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.\n */\n unitLabel: string;\n /** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */\n pageSizePrefix: string;\n /** `aria-label` của ô chọn cỡ trang */\n pageSizeSelect: (unitLabel: string) => string;\n /** Dòng đếm bản ghi: \"1–20 trên tổng 57 mục\" */\n range: (start: number, end: number, total: number, unitLabel: string) => string;\n /** Dòng đếm trang: \"Trang 2 / 5\" */\n pageStatus: (page: number, pageCount: number) => string;\n /** `sr-only` của 4 nút điều hướng */\n firstPage: string;\n previousPage: string;\n nextPage: string;\n lastPage: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels = {\n region: \"Phân trang\",\n unitLabel: \"mục\",\n pageSizePrefix: \"Hiển thị\",\n pageSizeSelect: (unitLabel) => `Số ${unitLabel} mỗi trang`,\n range: (start, end, total, unitLabel) => `${start}–${end} trên tổng ${total} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,\n firstPage: \"Trang đầu\",\n previousPage: \"Trang trước\",\n nextPage: \"Trang sau\",\n lastPage: \"Trang cuối\"\n};\n"],"mappings":"AAcO,MAAM,4BAA4B,CAAC,IAAI,IAAI,GAAG;AA0C9C,MAAM,kCAAyD;AAAA,EACpE,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB,CAAC,cAAc,WAAM,SAAS;AAAA,EAC9C,OAAO,CAAC,OAAO,KAAK,OAAO,cAAc,GAAG,KAAK,SAAI,GAAG,sBAAc,KAAK,IAAI,SAAS;AAAA,EACxF,YAAY,CAAC,MAAM,cAAc,SAAS,IAAI,MAAM,SAAS;AAAA,EAC7D,WAAW;AAAA,EACX,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AACZ;","names":[]}
|
|
@@ -1,47 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { TablePaginationLabels } from './table-pagination-constants.js';
|
|
2
3
|
|
|
3
|
-
/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */
|
|
4
|
-
declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly [20, 50, 100];
|
|
5
|
-
/**
|
|
6
|
-
* Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.
|
|
7
|
-
*
|
|
8
|
-
* Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của
|
|
9
|
-
* cụm nút ("Trang đầu/trước/sau/cuối"), `aria-label` của ô chọn cỡ trang và hai dòng đếm
|
|
10
|
-
* (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`
|
|
11
|
-
* (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn
|
|
12
|
-
* nhất quán tiếng Việt.
|
|
13
|
-
*
|
|
14
|
-
* Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc
|
|
15
|
-
* chắn sai trật tự từ với các ngôn ngữ khác.
|
|
16
|
-
*/
|
|
17
|
-
interface TablePaginationLabels {
|
|
18
|
-
/** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */
|
|
19
|
-
region: string;
|
|
20
|
-
/**
|
|
21
|
-
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
22
|
-
*
|
|
23
|
-
* Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`
|
|
24
|
-
* mà vẫn nhận "mục" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn
|
|
25
|
-
* prop `unitLabel` là chuỗi theo NỘI DUNG ("việc", "liên hệ") — mỗi bảng một khác, nên vẫn
|
|
26
|
-
* là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.
|
|
27
|
-
*/
|
|
28
|
-
unitLabel: string;
|
|
29
|
-
/** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */
|
|
30
|
-
pageSizePrefix: string;
|
|
31
|
-
/** `aria-label` của ô chọn cỡ trang */
|
|
32
|
-
pageSizeSelect: (unitLabel: string) => string;
|
|
33
|
-
/** Dòng đếm bản ghi: "1–20 trên tổng 57 mục" */
|
|
34
|
-
range: (start: number, end: number, total: number, unitLabel: string) => string;
|
|
35
|
-
/** Dòng đếm trang: "Trang 2 / 5" */
|
|
36
|
-
pageStatus: (page: number, pageCount: number) => string;
|
|
37
|
-
/** `sr-only` của 4 nút điều hướng */
|
|
38
|
-
firstPage: string;
|
|
39
|
-
previousPage: string;
|
|
40
|
-
nextPage: string;
|
|
41
|
-
lastPage: string;
|
|
42
|
-
}
|
|
43
|
-
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
44
|
-
declare const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels;
|
|
45
4
|
interface TablePaginationProps {
|
|
46
5
|
/** Trang hiện tại, đếm từ 1 */
|
|
47
6
|
page: number;
|
|
@@ -70,4 +29,4 @@ interface TablePaginationProps {
|
|
|
70
29
|
*/
|
|
71
30
|
declare function TablePagination({ page, pageSize, total, pageSizeOptions, onPageChange, onPageSizeChange, unitLabel: unitLabelProp, hidePageSize, label, labels: labelsProp, className }: TablePaginationProps): React.JSX.Element;
|
|
72
31
|
|
|
73
|
-
export {
|
|
32
|
+
export { TablePagination, type TablePaginationProps };
|
|
@@ -4,19 +4,10 @@ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-r
|
|
|
4
4
|
import { cn } from "../../lib/cn.js";
|
|
5
5
|
import { Button } from "../ui/button.js";
|
|
6
6
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pageSizePrefix: "Hi\u1EC3n th\u1ECB",
|
|
12
|
-
pageSizeSelect: (unitLabel) => `S\u1ED1 ${unitLabel} m\u1ED7i trang`,
|
|
13
|
-
range: (start, end, total, unitLabel) => `${start}\u2013${end} tr\xEAn t\u1ED5ng ${total} ${unitLabel}`,
|
|
14
|
-
pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
15
|
-
firstPage: "Trang \u0111\u1EA7u",
|
|
16
|
-
previousPage: "Trang tr\u01B0\u1EDBc",
|
|
17
|
-
nextPage: "Trang sau",
|
|
18
|
-
lastPage: "Trang cu\u1ED1i"
|
|
19
|
-
};
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
9
|
+
DEFAULT_TABLE_PAGINATION_LABELS
|
|
10
|
+
} from "./table-pagination-constants.js";
|
|
20
11
|
function TablePagination({
|
|
21
12
|
page,
|
|
22
13
|
pageSize,
|
|
@@ -99,8 +90,6 @@ function TablePagination({
|
|
|
99
90
|
);
|
|
100
91
|
}
|
|
101
92
|
export {
|
|
102
|
-
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
103
|
-
DEFAULT_TABLE_PAGINATION_LABELS,
|
|
104
93
|
TablePagination
|
|
105
94
|
};
|
|
106
95
|
//# sourceMappingURL=table-pagination.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/table-pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select.js\";\
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/table-pagination.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select.js\";\nimport {\n DEFAULT_PAGE_SIZE_OPTIONS,\n DEFAULT_TABLE_PAGINATION_LABELS,\n type TablePaginationLabels\n} from \"./table-pagination-constants.js\";\n\nexport interface TablePaginationProps {\n /** Trang hiện tại, đếm từ 1 */\n page: number;\n pageSize: number;\n /** Tổng số bản ghi (sau khi lọc) */\n total: number;\n pageSizeOptions?: readonly number[];\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n /** Đơn vị đếm theo nội dung bảng, vd \"việc\", \"liên hệ\" — mặc định `labels.unitLabel` (\"mục\") */\n unitLabel?: string;\n /** Ẩn ô chọn số dòng/trang (khi cỡ trang do hệ thống quyết định) */\n hidePageSize?: boolean;\n /** Nhãn vùng phân trang — cần khi một trang có nhiều bảng; mặc định `labels.region` */\n label?: string;\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `TablePaginationLabels` */\n labels?: Partial<TablePaginationLabels>;\n className?: string;\n}\n\n/**\n * Thanh phân trang đi kèm `DataTable`.\n *\n * Tổng số trang tính từ `total`/`pageSize` ngay tại đây thay vì bắt app truyền vào — hai bên\n * tự tính rất dễ lệch nhau một trang ở bản ghi cuối, và người dùng sẽ thấy nút \"Trang sau\"\n * còn bấm được trong khi bảng đã hết dữ liệu.\n */\nexport function TablePagination({\n page,\n pageSize,\n total,\n pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,\n onPageChange,\n onPageSizeChange,\n unitLabel: unitLabelProp,\n hidePageSize = false,\n label,\n labels: labelsProp,\n className\n}: TablePaginationProps) {\n const labels = { ...DEFAULT_TABLE_PAGINATION_LABELS, ...labelsProp };\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));\n const current = Math.min(Math.max(1, page), pageCount);\n const start = total === 0 ? 0 : (current - 1) * pageSize + 1;\n const end = Math.min(current * pageSize, total);\n const isFirst = current <= 1;\n const isLast = current >= pageCount;\n\n return (\n <nav\n aria-label={label ?? labels.region}\n data-slot=\"table-pagination\"\n className={cn(\"flex flex-wrap items-center justify-between gap-3 py-2 text-sm\", className)}\n >\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n {!hidePageSize && (\n <>\n <span className=\"hidden sm:inline\">{labels.pageSizePrefix}</span>\n <Select value={String(pageSize)} onValueChange={(value) => onPageSizeChange(Number(value))}>\n <SelectTrigger className=\"h-8 w-[4.5rem]\" aria-label={labels.pageSizeSelect(unitLabel)}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {pageSizeOptions.map((size) => (\n <SelectItem key={size} value={String(size)}>\n {size}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </>\n )}\n <span role=\"status\" className=\"tabular-nums\">\n {labels.range(start, end, total, unitLabel)}\n </span>\n </div>\n\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isFirst}\n onClick={() => onPageChange(1)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.firstPage}</span>\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isFirst} onClick={() => onPageChange(current - 1)}>\n <ChevronLeft aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.previousPage}</span>\n </Button>\n\n <span role=\"status\" className=\"px-2 tabular-nums text-muted-foreground\">\n {labels.pageStatus(current, pageCount)}\n </span>\n\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isLast} onClick={() => onPageChange(current + 1)}>\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.nextPage}</span>\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={isLast}\n onClick={() => onPageChange(pageCount)}\n className=\"hidden sm:inline-flex\"\n >\n <ChevronsRight aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{labels.lastPage}</span>\n </Button>\n </div>\n </nav>\n );\n}\n"],"mappings":";AAqEU,mBACE,KACA,YAFF;AAnEV,SAAS,aAAa,cAAc,cAAc,qBAAqB;AACvE,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,YAAY,eAAe,mBAAmB;AAC9E;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AA6BA,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAyB;AACvB,QAAM,SAAS,EAAE,GAAG,iCAAiC,GAAG,WAAW;AACnE,QAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC;AACtE,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS;AACrD,QAAM,QAAQ,UAAU,IAAI,KAAK,UAAU,KAAK,WAAW;AAC3D,QAAM,MAAM,KAAK,IAAI,UAAU,UAAU,KAAK;AAC9C,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,WAAW;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,SAAS,OAAO;AAAA,MAC5B,aAAU;AAAA,MACV,WAAW,GAAG,kEAAkE,SAAS;AAAA,MAEzF;AAAA,6BAAC,SAAI,WAAU,iDACZ;AAAA,WAAC,gBACA,iCACE;AAAA,gCAAC,UAAK,WAAU,oBAAoB,iBAAO,gBAAe;AAAA,YAC1D,qBAAC,UAAO,OAAO,OAAO,QAAQ,GAAG,eAAe,CAAC,UAAU,iBAAiB,OAAO,KAAK,CAAC,GACvF;AAAA,kCAAC,iBAAc,WAAU,kBAAiB,cAAY,OAAO,eAAe,SAAS,GACnF,8BAAC,eAAY,GACf;AAAA,cACA,oBAAC,iBACE,0BAAgB,IAAI,CAAC,SACpB,oBAAC,cAAsB,OAAO,OAAO,IAAI,GACtC,kBADc,IAEjB,CACD,GACH;AAAA,eACF;AAAA,aACF;AAAA,UAEF,oBAAC,UAAK,MAAK,UAAS,WAAU,gBAC3B,iBAAO,MAAM,OAAO,KAAK,OAAO,SAAS,GAC5C;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,UAAU;AAAA,cACV,SAAS,MAAM,aAAa,CAAC;AAAA,cAC7B,WAAU;AAAA,cAEV;AAAA,oCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,gBACpD,oBAAC,UAAK,WAAU,WAAW,iBAAO,WAAU;AAAA;AAAA;AAAA,UAC9C;AAAA,UACA,qBAAC,UAAO,SAAQ,WAAU,MAAK,WAAU,UAAU,SAAS,SAAS,MAAM,aAAa,UAAU,CAAC,GACjG;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,UAAS;AAAA,YACnD,oBAAC,UAAK,WAAU,WAAW,iBAAO,cAAa;AAAA,aACjD;AAAA,UAEA,oBAAC,UAAK,MAAK,UAAS,WAAU,2CAC3B,iBAAO,WAAW,SAAS,SAAS,GACvC;AAAA,UAEA,qBAAC,UAAO,SAAQ,WAAU,MAAK,WAAU,UAAU,QAAQ,SAAS,MAAM,aAAa,UAAU,CAAC,GAChG;AAAA,gCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,YACpD,oBAAC,UAAK,WAAU,WAAW,iBAAO,UAAS;AAAA,aAC7C;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,UAAU;AAAA,cACV,SAAS,MAAM,aAAa,SAAS;AAAA,cACrC,WAAU;AAAA,cAEV;AAAA,oCAAC,iBAAc,eAAY,QAAO,WAAU,UAAS;AAAA,gBACrD,oBAAC,UAAK,WAAU,WAAW,iBAAO,UAAS;AAAA;AAAA;AAAA,UAC7C;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -11,9 +11,9 @@ const badgeVariants = cva(
|
|
|
11
11
|
primary: "bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200",
|
|
12
12
|
secondary: "bg-secondary text-secondary-foreground",
|
|
13
13
|
outline: "bg-transparent border border-control-edge text-foreground",
|
|
14
|
-
success: "bg-
|
|
15
|
-
warning: "bg-
|
|
16
|
-
destructive: "bg-
|
|
14
|
+
success: "bg-green/10 dark:bg-green/20 text-green-ink",
|
|
15
|
+
warning: "bg-yellow/10 dark:bg-yellow/20 text-yellow-ink",
|
|
16
|
+
destructive: "bg-red/10 dark:bg-red/15 text-destructive-ink"
|
|
17
17
|
},
|
|
18
18
|
size: {
|
|
19
19
|
sm: "px-1.5 py-0.5 text-[10px]",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. */\n asChild?: boolean;\n}\n\nfunction Badge({ className, variant, size, asChild = false, ...props }: BadgeProps) {\n const Comp = asChild ? Slot : \"span\";\n\n return <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AA2ES;AA3ET,SAAS,YAAY;AACrB,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAuCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAOA,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM,UAAU,OAAO,GAAG,MAAM,GAAe;AAClF,QAAM,OAAO,UAAU,OAAO;AAE9B,SAAO,oBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACxG;","names":[]}
|
|
@@ -9,13 +9,13 @@ const buttonVariants = cva(
|
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
|
-
default: "bg-primary text-primary-foreground hover:bg-
|
|
13
|
-
primary: "bg-primary text-primary-foreground hover:bg-
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control",
|
|
13
|
+
primary: "bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control",
|
|
14
14
|
secondary: "bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge",
|
|
15
15
|
outline: "bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10",
|
|
16
16
|
ghost: "bg-transparent text-foreground hover:bg-muted border border-transparent",
|
|
17
|
-
destructive: "bg-destructive text-destructive-foreground hover:bg-
|
|
18
|
-
link: "bg-transparent text-
|
|
17
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent",
|
|
18
|
+
link: "bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto"
|
|
19
19
|
},
|
|
20
20
|
size: {
|
|
21
21
|
default: "h-9 px-4 text-sm rounded-md gap-2",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port NGUYÊN TRẠNG từ comitor-ds — bản duyệt là nguồn sự thật (xem khối NGUỒN SỰ THẬT ở\n * đầu styles.css). API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của\n * `default` — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ⚠ 0.2.x từng đổi nhóm biến thể này sang token semantic + bậc `-ink` để đạt WCAG. 0.3.0 kéo về\n * đúng comitor-ds. Những khoản dưới ngưỡng còn tồn ở đây, ghi ra để không ai \"sửa\" lại lần nữa:\n *\n * destructive : chữ trắng trên `--red` #D64545 = 4,38:1 (thiếu 0,12 so với AA 4,5:1)\n * link : `--gold-400` #B88A00 trên nền trang = 3,15:1 (AA đòi 4,5:1)\n * outline : viền `--border` ⇄ nền trang = 1,16:1 (1.4.11 đòi 3:1)\n *\n * Vì sao `bg-gold-300` chứ không `bg-primary`: comitor-ds tô cùng một gold ở CẢ hai theme\n * (`dark:data-[state=checked]:bg-gold-300` là lối viết quen của bản duyệt), trong khi `--primary`\n * có bản dark riêng #F5D060. Dùng thang gold thô là cách duy nhất khớp bản duyệt ở dark.\n * ⚠ Đánh đổi: app ghi đè `--primary` cho sản phẩm của mình sẽ KHÔNG đổi được nền nút. Muốn nút\n * theo brand riêng thì ghi đè thang `--gold-*`.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ (BỔ SUNG, comitor-ds chưa có) ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render trigger\n * bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới, còn chính ô\n * thì giữ nguyên viền: người dùng thấy thông báo lỗi mà không thấy ô nào đang lỗi. `Input` đã có\n * nhánh này từ đầu; đặt ở đây để MỌI control dựng trên Button được nhất quán. Token là\n * `--destructive-ink` (= `--destructive` = #D64545), đúng thứ `Input` của comitor-ds dùng\n * (`border-red`).\n */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center font-medium select-none whitespace-nowrap transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 aria-invalid:border-destructive-ink aria-invalid:hover:border-destructive-ink aria-invalid:focus-visible:outline-destructive-ink disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n primary: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge\",\n outline:\n \"bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10\",\n ghost: \"bg-transparent text-foreground hover:bg-muted border border-transparent\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent\",\n link: \"bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto\"\n },\n size: {\n default: \"h-9 px-4 text-sm rounded-md gap-2\",\n md: \"h-9 px-4 text-sm rounded-md gap-2\",\n sm: \"h-8 px-3 text-xs rounded-md gap-1.5\",\n lg: \"h-11 px-6 text-base rounded-md gap-2\",\n icon: \"h-9 w-9 rounded-md\",\n \"icon-sm\": \"h-8 w-8 rounded-md\",\n \"icon-lg\": \"h-11 w-11 rounded-md\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ButtonProps extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <button>. */\n asChild?: boolean;\n /** Hiện spinner và khoá nút. */\n loading?: boolean;\n /** Icon đặt trước nội dung; bị spinner thay thế khi `loading`. */\n leftIcon?: React.ReactNode;\n /** Icon đặt sau nội dung; ẩn khi `loading`. */\n rightIcon?: React.ReactNode;\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leftIcon,\n rightIcon,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n // Slot chỉ nhận đúng MỘT phần tử con → bỏ qua lớp bọc icon/loading khi asChild.\n if (asChild) {\n return (\n <Slot data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n data-slot=\"button\"\n disabled={disabled || loading}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : leftIcon ? (\n <span className=\"shrink-0\">{leftIcon}</span>\n ) : null}\n {children}\n {!loading && rightIcon ? <span className=\"shrink-0\">{rightIcon}</span> : null}\n </button>\n );\n}\n\nexport { Button, buttonVariants };\n"],"mappings":";AA6FM,cAOF,YAPE;AA3FN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AA6BnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WACE;AAAA,QACF,SACE;AAAA,QACF,OAAO;AAAA,QACP,aAAa;AAAA,QACb,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAaA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AAEd,MAAI,SAAS;AACX,WACE,oBAAC,QAAK,aAAU,UAAS,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC,GAAI,GAAG,OACvF,UACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA,kBACC,oBAAC,WAAQ,WAAU,kCAAiC,eAAW,MAAC,IAC9D,WACF,oBAAC,UAAK,WAAU,YAAY,oBAAS,IACnC;AAAA,QACH;AAAA,QACA,CAAC,WAAW,YAAY,oBAAC,UAAK,WAAU,YAAY,qBAAU,IAAU;AAAA;AAAA;AAAA,EAC3E;AAEJ;","names":[]}
|
|
@@ -133,7 +133,7 @@ function CalendarDayButton({ className, day, modifiers, ...props }) {
|
|
|
133
133
|
"data-range-end": modifiers.range_end,
|
|
134
134
|
"data-range-middle": modifiers.range_middle,
|
|
135
135
|
className: cn(
|
|
136
|
-
"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-
|
|
136
|
+
"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-control-check data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-control-check data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-control-check group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80",
|
|
137
137
|
defaultClassNames.day,
|
|
138
138
|
className
|
|
139
139
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/calendar.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { type DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps, buttonVariants } from \"./button.js\";\n\n/**\n * Calendar (react-day-picker v9) — lịch trần, đã map class sang token Comitor.\n * Bản có ô nhập `dd/MM/yyyy` + preset (\"Hôm nay\", \"7 ngày qua\"…) là composite\n * `DatePicker` / `DateRangePicker` ở Tier 2.\n *\n * Ngôn ngữ: truyền `locale={vi}` (date-fns/locale) để đổi tên thứ/tháng.\n *\n * Khác bản comitor-ds: tên tháng rút gọn trong dropdown lấy theo `locale.code`\n * (mặc định `vi-VN`) thay vì `\"default\"` — `\"default\"` phụ thuộc locale của môi\n * trường chạy nên server và trình duyệt có thể ra chuỗi khác nhau → hydration mismatch.\n *\n * ─ Ô NGÀY ĐANG CHỌN: `bg-primary-ink` + `text-background` ─\n * Chữ của ô ngày KHÔNG đổi màu khi được chọn (nghỉ và chọn đều là bậc mực), nên \"đang chọn\"\n * chỉ còn được kể bằng NỀN ô — thông tin thị giác, ngưỡng 3:1. `bg-primary` gold #E8B824 trên\n * nền trắng chỉ **1,86:1** ✗. Đổi sang bậc ink của brand thì đạt cả hai phía, cả hai theme:\n *\n * nền ô ⇄ nền lịch : 6,51:1 (light) · 12,67:1 (dark) ✓\n * chữ `text-background` trên nền ô : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * Chữ phải là `text-background` (trắng ở light) chứ KHÔNG `text-primary-foreground` (mực ở cả\n * hai theme): mực trên `--primary-ink` #7A5800 chỉ 1,86:1 ✗ — cùng lý do với dấu tick\n * `Checkbox` và núm `Switch`.\n *\n * ⚠ CÒN TỒN, cố ý chưa động: `today` và `range_middle` dùng `bg-accent` — 1,09:1 (light) /\n * 1,17:1 (dark) so với nền lịch. Đưa dải giữa lên 3:1 thì cả tuần biến thành một khối đặc,\n * nên đây là quyết định THIẾT KẾ (thêm viền/gạch chân cho \"hôm nay\"?) chứ không sửa được bằng\n * đổi token — để dành cho vòng riêng.\n */\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker> & {\n /** Biến thể Button dùng cho nút điều hướng tháng. */\n buttonVariant?: ButtonProps[\"variant\"];\n};\nexport type CalendarDayButtonProps = React.ComponentProps<typeof DayButton>;\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n locale,\n ...props\n}: CalendarProps) {\n const defaultClassNames = getDefaultClassNames();\n const localeCode = locale?.code ?? \"vi-VN\";\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n locale={locale}\n className={cn(\n \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString(localeCode, { month: \"short\" }),\n ...formatters\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\"flex gap-4 flex-col md:flex-row relative\", defaultClassNames.months),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\", defaultClassNames.nav),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative has-focus:outline-none border border-input shadow-xs has-focus:ring-2 has-focus:ring-ring/50 has-focus:ring-offset-2 has-focus:ring-offset-background rounded-md\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\"absolute bg-popover inset-0 opacity-0\", defaultClassNames.dropdown),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n defaultClassNames.caption_label\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\"select-none w-(--cell-size)\", defaultClassNames.week_number_header),\n week_number: cn(\"text-[0.8rem] select-none text-muted-foreground\", defaultClassNames.week_number),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n defaultClassNames.day\n ),\n range_start: cn(\"rounded-l-md bg-accent\", defaultClassNames.range_start),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\"text-muted-foreground aria-selected:text-muted-foreground\", defaultClassNames.outside),\n disabled: cn(\"text-muted-foreground opacity-50\", defaultClassNames.disabled),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames\n }}\n components={{\n Root: ({ className: rootClassName, rootRef, ...rootProps }) => {\n return <div data-slot=\"calendar\" ref={rootRef} className={cn(rootClassName)} {...rootProps} />;\n },\n Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {\n if (orientation === \"left\") {\n return <ChevronLeftIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n if (orientation === \"right\") {\n return <ChevronRightIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n return <ChevronDownIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, week: _week, ...weekProps }) => {\n return (\n <td {...weekProps}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">{children}</div>\n </td>\n );\n },\n ...components\n }}\n {...props}\n />\n );\n}\n\n/**\n * Ô ngày.\n *\n * ─ `[&>span]:opacity-80` — 80% LÀ MỨC MỜ TỐI ĐA CÒN ĐẠT AA ─\n * `<span>` là nội dung phụ app truyền vào ô ngày (số việc, chấm sự kiện…), cỡ `text-xs` = 12px\n * nên chịu ngưỡng CHỮ 4,5:1. Nút ngày VẪN bấm được và vẫn nhận focus → `opacity-*` chồng lên nó\n * KHÔNG được miễn trừ theo 1.4.3. Ca tệ nhất là ngày ĐÃ CHỌN ở theme sáng (`text-background`\n * trắng trên `bg-primary-ink` #7A5800), đo theo từng mức mờ:\n *\n * opacity-70 → 4,12:1 (light) · 5,90:1 (dark) ✗ ← dưới ngưỡng ở light\n * opacity-75 → 4,46:1 · 6,82:1 ✗\n * opacity-80 → 4,85:1 · 7,97:1 ✓ ← đang dùng\n *\n * Ba con số light ở trên là của nền ô MỚI. Đừng quy chúng cho `0.1.0`: bản đó tô ô chọn bằng\n * `bg-primary` (gold #E8B824) với chữ `text-primary-foreground` (mực), nên `opacity-70` khi ấy\n * đo được 5,26:1 (light) · 5,90:1 (dark) — tức mức mờ 70% VẪN đạt. Khuyết tật thật của `0.1.0`\n * nằm ở chỗ khác: nền ô chọn ⇄ nền lịch chỉ 1,86:1 (light), tức 1.4.11 mới là điều bị vi phạm.\n * Chính việc hạ nền xuống #7A5800 + chữ trắng đã làm ca tệ nhất chặt lại và kéo 70% xuống dưới\n * ngưỡng. (Ở dark hai bản trùng số vì `--primary-ink: var(--primary)`.)\n *\n * Các trạng thái còn lại thoải mái ở 80%: giữa khoảng & hôm nay 9,70:1 (light) · 10,05:1 (dark),\n * ngày thường 10,26:1 · 11,41:1. Hạ dưới 80% là hỏng đúng một ca (chọn + theme sáng) — đúng kiểu\n * lỗi chỉ lộ ở một trạng thái mà ba vòng trước đều sót.\n */\nfunction CalendarDayButton({ className, day, modifiers, ...props }: CalendarDayButtonProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-background data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-background data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-background group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/calendar.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { type DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps, buttonVariants } from \"./button.js\";\n\n/**\n * Calendar (react-day-picker v9) — lịch trần, đã map class sang token Comitor.\n * Bản có ô nhập `dd/MM/yyyy` + preset (\"Hôm nay\", \"7 ngày qua\"…) là composite\n * `DatePicker` / `DateRangePicker` ở Tier 2.\n *\n * Ngôn ngữ: truyền `locale={vi}` (date-fns/locale) để đổi tên thứ/tháng.\n *\n * Khác bản comitor-ds: tên tháng rút gọn trong dropdown lấy theo `locale.code`\n * (mặc định `vi-VN`) thay vì `\"default\"` — `\"default\"` phụ thuộc locale của môi\n * trường chạy nên server và trình duyệt có thể ra chuỗi khác nhau → hydration mismatch.\n *\n * ─ Ô NGÀY ĐANG CHỌN: `bg-primary-ink` + `text-background` ─\n * Chữ của ô ngày KHÔNG đổi màu khi được chọn (nghỉ và chọn đều là bậc mực), nên \"đang chọn\"\n * chỉ còn được kể bằng NỀN ô — thông tin thị giác, ngưỡng 3:1. `bg-primary` gold #E8B824 trên\n * nền trắng chỉ **1,86:1** ✗. Đổi sang bậc ink của brand thì đạt cả hai phía, cả hai theme:\n *\n * nền ô ⇄ nền lịch : 6,51:1 (light) · 12,67:1 (dark) ✓\n * chữ `text-background` trên nền ô : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * Chữ phải là `text-background` (trắng ở light) chứ KHÔNG `text-primary-foreground` (mực ở cả\n * hai theme): mực trên `--primary-ink` #7A5800 chỉ 1,86:1 ✗ — cùng lý do với dấu tick\n * `Checkbox` và núm `Switch`.\n *\n * ⚠ CÒN TỒN, cố ý chưa động: `today` và `range_middle` dùng `bg-accent` — 1,09:1 (light) /\n * 1,17:1 (dark) so với nền lịch. Đưa dải giữa lên 3:1 thì cả tuần biến thành một khối đặc,\n * nên đây là quyết định THIẾT KẾ (thêm viền/gạch chân cho \"hôm nay\"?) chứ không sửa được bằng\n * đổi token — để dành cho vòng riêng.\n */\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker> & {\n /** Biến thể Button dùng cho nút điều hướng tháng. */\n buttonVariant?: ButtonProps[\"variant\"];\n};\nexport type CalendarDayButtonProps = React.ComponentProps<typeof DayButton>;\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n locale,\n ...props\n}: CalendarProps) {\n const defaultClassNames = getDefaultClassNames();\n const localeCode = locale?.code ?? \"vi-VN\";\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n locale={locale}\n className={cn(\n \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString(localeCode, { month: \"short\" }),\n ...formatters\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\"flex gap-4 flex-col md:flex-row relative\", defaultClassNames.months),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\", defaultClassNames.nav),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative has-focus:outline-none border border-input shadow-xs has-focus:ring-2 has-focus:ring-ring/50 has-focus:ring-offset-2 has-focus:ring-offset-background rounded-md\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\"absolute bg-popover inset-0 opacity-0\", defaultClassNames.dropdown),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n defaultClassNames.caption_label\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\"select-none w-(--cell-size)\", defaultClassNames.week_number_header),\n week_number: cn(\"text-[0.8rem] select-none text-muted-foreground\", defaultClassNames.week_number),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n defaultClassNames.day\n ),\n range_start: cn(\"rounded-l-md bg-accent\", defaultClassNames.range_start),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\"text-muted-foreground aria-selected:text-muted-foreground\", defaultClassNames.outside),\n disabled: cn(\"text-muted-foreground opacity-50\", defaultClassNames.disabled),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames\n }}\n components={{\n Root: ({ className: rootClassName, rootRef, ...rootProps }) => {\n return <div data-slot=\"calendar\" ref={rootRef} className={cn(rootClassName)} {...rootProps} />;\n },\n Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {\n if (orientation === \"left\") {\n return <ChevronLeftIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n if (orientation === \"right\") {\n return <ChevronRightIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n return <ChevronDownIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, week: _week, ...weekProps }) => {\n return (\n <td {...weekProps}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">{children}</div>\n </td>\n );\n },\n ...components\n }}\n {...props}\n />\n );\n}\n\n/**\n * Ô ngày.\n *\n * ─ `[&>span]:opacity-80` — 80% LÀ MỨC MỜ TỐI ĐA CÒN ĐẠT AA ─\n * `<span>` là nội dung phụ app truyền vào ô ngày (số việc, chấm sự kiện…), cỡ `text-xs` = 12px\n * nên chịu ngưỡng CHỮ 4,5:1. Nút ngày VẪN bấm được và vẫn nhận focus → `opacity-*` chồng lên nó\n * KHÔNG được miễn trừ theo 1.4.3. Ca tệ nhất là ngày ĐÃ CHỌN ở theme sáng (`text-background`\n * trắng trên `bg-primary-ink` #7A5800), đo theo từng mức mờ:\n *\n * opacity-70 → 4,12:1 (light) · 5,90:1 (dark) ✗ ← dưới ngưỡng ở light\n * opacity-75 → 4,46:1 · 6,82:1 ✗\n * opacity-80 → 4,85:1 · 7,97:1 ✓ ← đang dùng\n *\n * Ba con số light ở trên là của nền ô MỚI. Đừng quy chúng cho `0.1.0`: bản đó tô ô chọn bằng\n * `bg-primary` (gold #E8B824) với chữ `text-primary-foreground` (mực), nên `opacity-70` khi ấy\n * đo được 5,26:1 (light) · 5,90:1 (dark) — tức mức mờ 70% VẪN đạt. Khuyết tật thật của `0.1.0`\n * nằm ở chỗ khác: nền ô chọn ⇄ nền lịch chỉ 1,86:1 (light), tức 1.4.11 mới là điều bị vi phạm.\n * Chính việc hạ nền xuống #7A5800 + chữ trắng đã làm ca tệ nhất chặt lại và kéo 70% xuống dưới\n * ngưỡng. (Ở dark hai bản trùng số vì `--primary-ink: var(--primary)`.)\n *\n * Các trạng thái còn lại thoải mái ở 80%: giữa khoảng & hôm nay 9,70:1 (light) · 10,05:1 (dark),\n * ngày thường 10,26:1 · 11,41:1. Hạ dưới 80% là hỏng đúng một ca (chọn + theme sáng) — đúng kiểu\n * lỗi chỉ lộ ở một trạng thái mà ba vòng trước đều sót.\n */\nfunction CalendarDayButton({ className, day, modifiers, ...props }: CalendarDayButtonProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-control-check data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-control-check data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-control-check group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n"],"mappings":"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","names":[]}
|