@comitor/ui 0.2.0 → 0.2.2
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 +221 -14
- 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 +75 -3
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +33 -42
- package/dist/chart/comitor-chart.js +13 -24
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +3 -2
- package/dist/chart/index.js +1 -0
- package/dist/chart/index.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +9 -2
- package/dist/components/composite/combobox.js +7 -0
- package/dist/components/composite/combobox.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/button.js +1 -1
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/card.d.ts +35 -2
- package/dist/components/ui/card.js +17 -3
- package/dist/components/ui/card.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -0
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/shell-context.d.ts +28 -1
- package/dist/shell/shell-context.js +8 -3
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +3 -3
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/dist/shell/utils.d.ts +27 -3
- package/dist/shell/utils.js +52 -9
- package/dist/shell/utils.js.map +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nhãn mặc định của `SearchFilterBar` (`.tsx` vì `resultSummary` trả JSX).
|
|
5
|
+
*
|
|
6
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
7
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
8
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
9
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
10
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `search-filter-bar.tsx` chỉ được `import`,
|
|
13
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.
|
|
18
|
+
*
|
|
19
|
+
* `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);
|
|
20
|
+
* 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
|
|
21
|
+
* 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.
|
|
22
|
+
*/
|
|
23
|
+
interface SearchFilterBarLabels {
|
|
24
|
+
/** Placeholder + `aria-label` của ô tìm */
|
|
25
|
+
searchPlaceholder: string;
|
|
26
|
+
/**
|
|
27
|
+
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
28
|
+
*
|
|
29
|
+
* Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên
|
|
30
|
+
* phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng
|
|
31
|
+
* bảng ("việc", "liên hệ") nên vẫn là prop rời và vẫn thắng `labels`.
|
|
32
|
+
*/
|
|
33
|
+
unitLabel: string;
|
|
34
|
+
/** `sr-only` của nút xoá từ khoá trong ô tìm */
|
|
35
|
+
clearSearch: string;
|
|
36
|
+
/** Huy hiệu đếm bộ lọc đang bật: "3 bộ lọc" */
|
|
37
|
+
filterCount: (count: number) => string;
|
|
38
|
+
/** Nhãn nút xoá toàn bộ bộ lọc */
|
|
39
|
+
clearFilters: string;
|
|
40
|
+
/**
|
|
41
|
+
* Dòng đếm kết quả sau lọc: "**3** / 57 mục" (`total` bỏ trống thì chỉ "**3** mục").
|
|
42
|
+
*
|
|
43
|
+
* Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không
|
|
44
|
+
* 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 đó.
|
|
45
|
+
* Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.
|
|
46
|
+
*/
|
|
47
|
+
resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;
|
|
48
|
+
}
|
|
49
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
50
|
+
declare const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels;
|
|
51
|
+
|
|
52
|
+
export { DEFAULT_SEARCH_FILTER_BAR_LABELS, type SearchFilterBarLabels };
|
|
@@ -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":"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","names":[]}
|
|
@@ -5,7 +5,7 @@ import { cva } from "class-variance-authority";
|
|
|
5
5
|
import { Loader2 } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/cn.js";
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"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 disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
"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",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -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 để giao diện khớp bản v0.\n * API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của `default`\n * — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ─ VÌ SAO NỀN NÚT LÀ TOKEN SEMANTIC, KHÔNG PHẢI THANG BRAND THÔ ─\n *\n * 1. `default`/`primary` trước dùng `bg-gold-300 text-ink` — cùng giá trị ở cả hai theme,\n * nên bảng màu dark của chính package (`.dark { --primary: #F5D060 }`) không bao giờ được\n * áp dụng, và app nào set lại `--primary` cho sản phẩm của mình cũng không đổi được nút.\n * Nay dùng `bg-primary text-primary-foreground`: light 10,19:1 (không đổi), dark 12,67:1\n * (trước 10,19:1) — đều vượt AA.\n *\n * 2. `destructive` trước dùng `bg-red text-white`. `--red` là màu phụ được NÂNG SÁNG ở dark\n * (#F87171) để làm CHỮ trên nền tối, dùng làm NỀN nút thì chữ trắng chỉ còn 2,77:1 ✗.\n * `--destructive` cố ý ghim một giá trị ở cả hai theme đúng cho việc này. Bản #D64545 mới\n * đạt 4,38:1 — thiếu 0,12 so với AA 4,5:1 — nên token đã được đẩy đậm thêm một bậc trong\n * `styles.css`: **#C93B3B → 5,03:1** ở cả hai theme. Đó là sửa ở TẦNG TOKEN, đúng chỗ; đừng\n * \"chữa\" lại trong component bằng cách chôn hex hay đổi sang `bg-red`.\n *\n * ─ VÌ SAO HOVER TRỘN `--ink` THAY VÌ `/90` ─\n *\n * `bg-primary/90` là \"nhạt dần về phía nền\" nên ở light mode nút SÁNG lên khi rê chuột (ngược\n * với bản v0 gold-300 → gold-400) và với destructive thì tương phản chữ trắng tụt còn 3,81:1 ✗.\n * Trộn 15% `--ink` thì tối đi ở CẢ hai theme, giữ đúng ngữ nghĩa \"nhấn xuống\" và tương phản chỉ\n * tăng: destructive 6,29:1 ✓, primary 7,58:1 (light) / 9,33:1 (dark) ✓.\n *\n * ─ VIỀN CỦA `outline` VÀ `secondary`: `--control-edge`, KHÔNG PHẢI `--border` ─\n *\n * `--border` là bậc TRANG TRÍ (vạch chia, khung Card lúc nghỉ) và cố ý nhạt — 1,16:1 ở light,\n * 1,17:1 ở dark. Nút `outline` để nền trong suốt nên viền là thứ DUY NHẤT khoanh vùng nó, và\n * nút `secondary` thì viền tách khối khỏi nền trang: cả hai là \"thông tin cần để nhận ra thành\n * phần\" theo WCAG 1.4.11 → ngưỡng 3:1. Số sau khi đổi sang `--control-edge`:\n *\n * outline viền ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * secondary viền ⇄ nền nút `--secondary`: 3,13:1 (light) · 4,21:1 (dark) ✓\n *\n * Hover của `outline` cũng phải ĐẬM HƠN lúc nghỉ: `hover:border-gold-300` cũ chỉ **1,86:1** ở\n * light, tức rê chuột vào là nút mất viền. `--primary-ink` giữ đúng sắc gold mà đạt 6,51:1 với\n * nền trang và 5,96:1 với chính nền hover `gold-50` (light); 12,67:1 / 11,91:1 (dark).\n *\n * ─ `link` ─\n * `text-gold-400` (#B88A00) trên nền trắng chỉ 3,15:1 ✗ — chữ link buộc phải đạt 4,5:1.\n * Đổi sang bậc tối/sáng theo theme: gold-500 6,51:1 (light) và gold-200 12,67:1 (dark).\n * Vẫn là thang gold vì --primary ở light (#E8B824) trên nền trắng chỉ 1,86:1, không dùng\n * làm chữ được; xem docs/quyet-dinh-tich-hop.md.\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 disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control\",\n primary:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] 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:\n \"bg-destructive text-destructive-foreground hover:bg-[color:color-mix(in_srgb,var(--destructive),var(--ink)_15%)] border border-transparent\",\n link: \"bg-transparent text-gold-500 hover:text-gold-600 dark:text-gold-200 dark:hover:text-gold-100 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":";
|
|
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 để giao diện khớp bản v0.\n * API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của `default`\n * — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ─ VÌ SAO NỀN NÚT LÀ TOKEN SEMANTIC, KHÔNG PHẢI THANG BRAND THÔ ─\n *\n * 1. `default`/`primary` trước dùng `bg-gold-300 text-ink` — cùng giá trị ở cả hai theme,\n * nên bảng màu dark của chính package (`.dark { --primary: #F5D060 }`) không bao giờ được\n * áp dụng, và app nào set lại `--primary` cho sản phẩm của mình cũng không đổi được nút.\n * Nay dùng `bg-primary text-primary-foreground`: light 10,19:1 (không đổi), dark 12,67:1\n * (trước 10,19:1) — đều vượt AA.\n *\n * 2. `destructive` trước dùng `bg-red text-white`. `--red` là màu phụ được NÂNG SÁNG ở dark\n * (#F87171) để làm CHỮ trên nền tối, dùng làm NỀN nút thì chữ trắng chỉ còn 2,77:1 ✗.\n * `--destructive` cố ý ghim một giá trị ở cả hai theme đúng cho việc này. Bản #D64545 mới\n * đạt 4,38:1 — thiếu 0,12 so với AA 4,5:1 — nên token đã được đẩy đậm thêm một bậc trong\n * `styles.css`: **#C93B3B → 5,03:1** ở cả hai theme. Đó là sửa ở TẦNG TOKEN, đúng chỗ; đừng\n * \"chữa\" lại trong component bằng cách chôn hex hay đổi sang `bg-red`.\n *\n * ─ VÌ SAO HOVER TRỘN `--ink` THAY VÌ `/90` ─\n *\n * `bg-primary/90` là \"nhạt dần về phía nền\" nên ở light mode nút SÁNG lên khi rê chuột (ngược\n * với bản v0 gold-300 → gold-400) và với destructive thì tương phản chữ trắng tụt còn 3,81:1 ✗.\n * Trộn 15% `--ink` thì tối đi ở CẢ hai theme, giữ đúng ngữ nghĩa \"nhấn xuống\" và tương phản chỉ\n * tăng: destructive 6,29:1 ✓, primary 7,58:1 (light) / 9,33:1 (dark) ✓.\n *\n * ─ VIỀN CỦA `outline` VÀ `secondary`: `--control-edge`, KHÔNG PHẢI `--border` ─\n *\n * `--border` là bậc TRANG TRÍ (vạch chia, khung Card lúc nghỉ) và cố ý nhạt — 1,16:1 ở light,\n * 1,17:1 ở dark. Nút `outline` để nền trong suốt nên viền là thứ DUY NHẤT khoanh vùng nó, và\n * nút `secondary` thì viền tách khối khỏi nền trang: cả hai là \"thông tin cần để nhận ra thành\n * phần\" theo WCAG 1.4.11 → ngưỡng 3:1. Số sau khi đổi sang `--control-edge`:\n *\n * outline viền ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * secondary viền ⇄ nền nút `--secondary`: 3,13:1 (light) · 4,21:1 (dark) ✓\n *\n * Hover của `outline` cũng phải ĐẬM HƠN lúc nghỉ: `hover:border-gold-300` cũ chỉ **1,86:1** ở\n * light, tức rê chuột vào là nút mất viền. `--primary-ink` giữ đúng sắc gold mà đạt 6,51:1 với\n * nền trang và 5,96:1 với chính nền hover `gold-50` (light); 12,67:1 / 11,91:1 (dark).\n *\n * ─ `link` ─\n * `text-gold-400` (#B88A00) trên nền trắng chỉ 3,15:1 ✗ — chữ link buộc phải đạt 4,5:1.\n * Đổi sang bậc tối/sáng theo theme: gold-500 6,51:1 (light) và gold-200 12,67:1 (dark).\n * Vẫn là thang gold vì --primary ở light (#E8B824) trên nền trắng chỉ 1,86:1, không dùng\n * làm chữ được; xem docs/quyet-dinh-tich-hop.md.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render\n * trigger bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới,\n * còn chính ô thì giữ nguyên viền `--control-edge`: người dùng thấy thông báo lỗi mà không\n * thấy ô nào đang lỗi. `Input` đã có nhánh này từ đầu (`border-destructive-ink`); đặt ở đây\n * để MỌI control dựng trên Button được nhất quán, thay vì mỗi composite tự chép lại.\n *\n * Token là bậc `-ink`, đúng như `Input`: `--destructive-ink` #9A3537 (light) / #F87171 (dark)\n * đạt 7,15:1 (light) / 6,84:1 (dark) trên `--background`, và ca TỆ NHẤT trong 5 nền trang là\n * 6,68:1 (light, trên `--muted`) / 5,82:1 (dark, trên `--muted`) — đều thừa ngưỡng 3:1 của\n * viền control (§3 hợp đồng màu). KHÔNG dùng `--destructive` (nền nút xoá, 3,76:1 ở dark —\n * sát ngưỡng) và càng không dùng `--red` (token TÔ NỀN).\n *\n * Nhánh này ghi sau các biến thể trong chuỗi lớp? Không cần: `aria-invalid:` sinh selector\n * `[aria-invalid=\"true\"]` nên đặc tả cao hơn `border-transparent`/`border-control-edge` trần,\n * và `tailwind-merge` giữ cả hai vì tập biến thể khác nhau.\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:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control\",\n primary:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] 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:\n \"bg-destructive text-destructive-foreground hover:bg-[color:color-mix(in_srgb,var(--destructive),var(--ink)_15%)] border border-transparent\",\n link: \"bg-transparent text-gold-500 hover:text-gold-600 dark:text-gold-200 dark:hover:text-gold-100 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":";AAsIM,cAOF,YAPE;AApIN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AAmEnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,SACE;AAAA,QACF,WACE;AAAA,QACF,SACE;AAAA,QACF,OAAO;AAAA,QACP,aACE;AAAA,QACF,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":[]}
|
|
@@ -8,14 +8,47 @@ declare const cardVariants: (props?: ({
|
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
interface CardProps extends React.ComponentProps<"div">, VariantProps<typeof cardVariants> {
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
interface CardHeaderProps extends React.ComponentProps<"div"> {
|
|
12
|
+
/**
|
|
13
|
+
* Nút/menu đặt ở GÓC PHẢI của header, ngang hàng với tiêu đề.
|
|
14
|
+
*
|
|
15
|
+
* Có nó thì header tự chuyển sang hàng ngang; bỏ trống thì bố cục dọc mặc định giữ
|
|
16
|
+
* nguyên (xem chú thích ở `CardHeader`).
|
|
17
|
+
*/
|
|
18
|
+
actions?: React.ReactNode;
|
|
19
|
+
/** className riêng cho cụm `actions` (vd đổi `gap`, cho phép xuống dòng). */
|
|
20
|
+
actionsClassName?: string;
|
|
21
|
+
}
|
|
12
22
|
type CardTitleProps = React.ComponentProps<"h3">;
|
|
13
23
|
type CardDescriptionProps = React.ComponentProps<"p">;
|
|
14
24
|
type CardActionProps = React.ComponentProps<"div">;
|
|
15
25
|
type CardContentProps = React.ComponentProps<"div">;
|
|
16
26
|
type CardFooterProps = React.ComponentProps<"div">;
|
|
17
27
|
declare function CardBase({ className, variant, padding, ...props }: CardProps): React.JSX.Element;
|
|
18
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Đầu thẻ. Mặc định XẾP DỌC (`flex-col space-y-1.5`) — tiêu đề trên, mô tả dưới.
|
|
30
|
+
*
|
|
31
|
+
* ─ VÌ SAO CÓ `actions` ─
|
|
32
|
+
* Thẻ có nút ở góc phải là mẫu lặp lại ở mọi app, mà bố cục dọc mặc định thì chặn nó: mỗi
|
|
33
|
+
* chỗ dùng đều phải ghi đè đúng một chuỗi bốn lớp `"flex-row items-start justify-between
|
|
34
|
+
* gap-3 space-y-0"` — chép tay, dễ sót `space-y-0`, và không có gì bảo đảm hai app ghi đè
|
|
35
|
+
* giống nhau. `actions` gói đúng chuỗi đó lại thành một prop.
|
|
36
|
+
*
|
|
37
|
+
* Mặc định KHÔNG đổi: không truyền `actions` thì lớp và cây DOM y hệt `0.2.0` (thêm một
|
|
38
|
+
* `<div>` bọc chỉ xảy ra ở nhánh có actions), nên bản vá này không phá bố cục nào đang chạy.
|
|
39
|
+
*
|
|
40
|
+
* Quan hệ với `CardAction`: `CardAction` là ô `ml-auto` để tự lắp khi bạn ĐÃ tự đặt header
|
|
41
|
+
* thành hàng ngang. `actions` làm cả hai việc — đổi bố cục *và* đặt chỗ — nên là đường đi
|
|
42
|
+
* mặc định; `CardAction` giữ nguyên cho ai đã dùng.
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <CardHeader actions={<Button size="sm" variant="ghost">Sửa</Button>}>
|
|
46
|
+
* <CardTitle>Thông tin đơn vị</CardTitle>
|
|
47
|
+
* <CardDescription>Cập nhật lần cuối hôm qua</CardDescription>
|
|
48
|
+
* </CardHeader>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function CardHeader({ className, actions, actionsClassName, children, ...props }: CardHeaderProps): React.JSX.Element;
|
|
19
52
|
declare function CardTitle({ className, ...props }: CardTitleProps): React.JSX.Element;
|
|
20
53
|
declare function CardDescription({ className, ...props }: CardDescriptionProps): React.JSX.Element;
|
|
21
54
|
declare function CardAction({ className, ...props }: CardActionProps): React.JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
4
|
const cardVariants = cva("rounded-xl", {
|
|
@@ -24,8 +24,22 @@ const cardVariants = cva("rounded-xl", {
|
|
|
24
24
|
function CardBase({ className, variant, padding, ...props }) {
|
|
25
25
|
return /* @__PURE__ */ jsx("div", { "data-slot": "card", className: cn(cardVariants({ variant, padding }), className), ...props });
|
|
26
26
|
}
|
|
27
|
-
function CardHeader({ className, ...props }) {
|
|
28
|
-
|
|
27
|
+
function CardHeader({ className, actions, actionsClassName, children, ...props }) {
|
|
28
|
+
if (actions === void 0 || actions === null || actions === false) {
|
|
29
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "card-header", className: cn("flex flex-col space-y-1.5", className), ...props, children });
|
|
30
|
+
}
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
"data-slot": "card-header",
|
|
35
|
+
className: cn("flex flex-row items-start justify-between gap-3 space-y-0", className),
|
|
36
|
+
...props,
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "card-header-text", className: "flex min-w-0 flex-col space-y-1.5", children }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "card-header-actions", className: cn("flex shrink-0 items-center gap-2", actionsClassName), children: actions })
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
);
|
|
29
43
|
}
|
|
30
44
|
function CardTitle({ className, ...props }) {
|
|
31
45
|
return /* @__PURE__ */ jsx(
|