@comitor/ui 0.5.1 → 0.6.1
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 +3 -2
- package/dist/components/composite/data-table.d.ts +7 -0
- package/dist/components/composite/data-table.js +5 -6
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/form-field.js +5 -2
- package/dist/components/composite/form-field.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/context-menu.js +4 -4
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/dropdown-menu.js +4 -4
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/field.js +6 -12
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/input-group.js +21 -3
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/item.js +1 -1
- package/dist/components/ui/item.js.map +1 -1
- package/dist/components/ui/menubar.js +4 -4
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/table.js +2 -2
- package/dist/components/ui/table.js.map +1 -1
- package/dist/shell/comitor-lockup.js +11 -6
- package/dist/shell/comitor-lockup.js.map +1 -1
- package/dist/shell/comitor-logo.d.ts +4 -3
- package/dist/shell/comitor-logo.js +8 -1
- package/dist/shell/comitor-logo.js.map +1 -1
- package/dist/shell/display-axis-constants.d.ts +35 -0
- package/dist/shell/display-axis-constants.js +27 -0
- package/dist/shell/display-axis-constants.js.map +1 -0
- package/dist/shell/display-axis-controls.d.ts +51 -0
- package/dist/shell/display-axis-controls.js +70 -0
- package/dist/shell/display-axis-controls.js.map +1 -0
- package/dist/shell/display-axis-provider.d.ts +40 -0
- package/dist/shell/display-axis-provider.js +101 -0
- package/dist/shell/display-axis-provider.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.d.ts +9 -1
- package/dist/shell/shell-context.js +14 -4
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +1 -1
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +82 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { ComitorLogo } from "./comitor-logo.js";
|
|
4
4
|
const SIZE_MAP = {
|
|
@@ -63,17 +63,22 @@ function ComitorLockup({
|
|
|
63
63
|
role: "img",
|
|
64
64
|
"aria-label": `${wordmark} \u2014 ${slogan}`,
|
|
65
65
|
children: [
|
|
66
|
-
|
|
67
|
-
/* @__PURE__ */ jsx(ComitorLogo, { variant: "light", className: cn("shrink-0", s.icon, "dark:hidden") }),
|
|
68
|
-
/* @__PURE__ */ jsx(ComitorLogo, { variant: "dark", className: cn("shrink-0", s.icon, "hidden dark:block") })
|
|
69
|
-
] }) : /* @__PURE__ */ jsx(ComitorLogo, { variant: dark ? "dark" : "light", className: cn("shrink-0", s.icon) }),
|
|
66
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant, className: cn("shrink-0", s.icon) }),
|
|
70
67
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
71
68
|
/* @__PURE__ */ jsx(
|
|
72
69
|
"span",
|
|
73
70
|
{
|
|
74
71
|
className: cn(
|
|
75
|
-
"font-extrabold tracking-tight
|
|
72
|
+
"font-extrabold tracking-tight",
|
|
76
73
|
s.wordmark,
|
|
74
|
+
// `leading-none` PHẢI đứng SAU `s.wordmark`, không được gộp vào chuỗi đầu.
|
|
75
|
+
// `cn()` chạy qua tailwind-merge, mà ở đó một utility CỠ CHỮ (`text-base`, `text-2xl`…)
|
|
76
|
+
// xung đột với nhóm `leading` và XOÁ mọi `leading-*` đứng TRƯỚC nó — Tailwind coi
|
|
77
|
+
// `text-<size>` là cặp cỡ-chữ + chiều-cao-dòng. Viết `cn("… leading-none", s.wordmark)`
|
|
78
|
+
// thì `leading-none` bị cắt sạch, wordmark rơi về line-height mặc định (24px ở cỡ `sm`
|
|
79
|
+
// thay vì 16px): khối chữ cao thêm 8px, vạch vàng và slogan tụt xuống, lockup không
|
|
80
|
+
// còn khớp tỉ lệ brand book. Không có lỗi biên dịch nào — chỉ đo mới thấy.
|
|
81
|
+
"leading-none",
|
|
77
82
|
auto ? "text-ink dark:text-white" : dark ? "text-white" : "text-ink"
|
|
78
83
|
),
|
|
79
84
|
children: wordmark
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/comitor-lockup.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { ComitorLogo } from \"./comitor-logo.js\";\n\n/**\n * Lockup ngang chính thức của Comitor — icon bên trái, wordmark + gạch vàng + slogan bên phải.\n * Giữ đúng tỉ lệ hình học của brand book để mọi bề mặt sản phẩm dùng chung một bố cục.\n *\n * <ComitorLockup variant=\"auto\" size=\"xs\" slogan=\"Tasks\" />\n *\n * • `variant` — light (nền trắng/kem), dark (nền mực/ấm), auto (tự đổi theo theme).\n * • `size` — xs | sm | md | lg: đổi cỡ icon và cỡ chữ cùng lúc.\n * • `wordmark` / `slogan` — đổi chữ khi cần cặp ngôn ngữ khác\n * (vd \"COMITOR TASKS\", \"THIỆN MINH / CÙNG NHAU TIẾN XA\").\n *\n * Không có \"use client\": thuần trình bày, RSC render được.\n */\nexport type ComitorLockupVariant = \"light\" | \"dark\" | \"auto\";\nexport type ComitorLockupSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface ComitorLockupProps {\n className?: string;\n variant?: ComitorLockupVariant;\n size?: ComitorLockupSize;\n wordmark?: string;\n slogan?: string;\n /** Class Tailwind GHI ĐÈ style slogan của preset cỡ chữ. Dùng khi cần căn bề rộng\n * slogan tuỳ biến cho khớp wordmark — vd header truyền cỡ nhỉnh hơn + tracking chặt\n * hơn để \"DESIGN SYSTEM\" rộng đúng bằng chữ \"COMITOR\". */\n sloganClassName?: string;\n style?: CSSProperties;\n}\n\n// Các cỡ căn theo tỉ lệ SVG trong brand book:\n// • chiều cao icon ≈ 3.3–4 lần chiều cao chữ hoa của wordmark (icon là phần chủ đạo)\n// • cỡ chữ + giãn chữ của slogan chọn sao cho slogan luôn HẸP HƠN HOẶC BẰNG wordmark\n// (không bao giờ tràn quá vạch vàng).\n// Giãn chữ ở đây nhỏ hơn mức 0.25–0.36em của brand book — mức đó chỉ vừa ở cỡ hero.\nconst SIZE_MAP: Record<\n ComitorLockupSize,\n { icon: string; gap: string; wordmark: string; slogan: string; ruleMargin: string }\n> = {\n xs: {\n icon: \"w-9 h-9\", // 36px — vừa header cao 56px\n gap: \"gap-2.5\",\n wordmark: \"text-sm\", // 14px\n slogan: \"text-[6px] tracking-[0.12em]\", // 6px\n ruleMargin: \"my-0.5\"\n },\n sm: {\n icon: \"w-12 h-12\", // 48px\n gap: \"gap-3\",\n wordmark: \"text-base\", // 16px\n slogan: \"text-[7px] tracking-[0.12em]\", // ~7px\n ruleMargin: \"my-1\"\n },\n md: {\n icon: \"w-20 h-20\", // 80px\n gap: \"gap-4\",\n wordmark: \"text-2xl\", // 24px\n slogan: \"text-[10px] tracking-[0.12em]\", // 10px\n ruleMargin: \"my-1.5\"\n },\n lg: {\n icon: \"w-32 h-32\", // 128px\n gap: \"gap-5\",\n wordmark: \"text-4xl\", // 36px\n slogan: \"text-[14px] tracking-[0.14em]\", // 14px\n ruleMargin: \"my-2\"\n }\n};\n\nexport function ComitorLockup({\n className,\n variant = \"light\",\n size = \"md\",\n wordmark = \"COMITOR\",\n slogan = \"Together Further\",\n sloganClassName,\n style\n}: ComitorLockupProps) {\n const s = SIZE_MAP[size];\n const dark = variant === \"dark\";\n const auto = variant === \"auto\";\n\n return (\n <div\n className={cn(\"flex items-center\", s.gap, className)}\n style={style}\n role=\"img\"\n aria-label={`${wordmark} — ${slogan}`}\n >\n {
|
|
1
|
+
{"version":3,"sources":["../../src/shell/comitor-lockup.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { ComitorLogo } from \"./comitor-logo.js\";\n\n/**\n * Lockup ngang chính thức của Comitor — icon bên trái, wordmark + gạch vàng + slogan bên phải.\n * Giữ đúng tỉ lệ hình học của brand book để mọi bề mặt sản phẩm dùng chung một bố cục.\n *\n * <ComitorLockup variant=\"auto\" size=\"xs\" slogan=\"Tasks\" />\n *\n * • `variant` — light (nền trắng/kem), dark (nền mực/ấm), auto (tự đổi theo theme).\n * • `size` — xs | sm | md | lg: đổi cỡ icon và cỡ chữ cùng lúc.\n * • `wordmark` / `slogan` — đổi chữ khi cần cặp ngôn ngữ khác\n * (vd \"COMITOR TASKS\", \"THIỆN MINH / CÙNG NHAU TIẾN XA\").\n *\n * Không có \"use client\": thuần trình bày, RSC render được.\n */\nexport type ComitorLockupVariant = \"light\" | \"dark\" | \"auto\";\nexport type ComitorLockupSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nexport interface ComitorLockupProps {\n className?: string;\n variant?: ComitorLockupVariant;\n size?: ComitorLockupSize;\n wordmark?: string;\n slogan?: string;\n /** Class Tailwind GHI ĐÈ style slogan của preset cỡ chữ. Dùng khi cần căn bề rộng\n * slogan tuỳ biến cho khớp wordmark — vd header truyền cỡ nhỉnh hơn + tracking chặt\n * hơn để \"DESIGN SYSTEM\" rộng đúng bằng chữ \"COMITOR\". */\n sloganClassName?: string;\n style?: CSSProperties;\n}\n\n// Các cỡ căn theo tỉ lệ SVG trong brand book:\n// • chiều cao icon ≈ 3.3–4 lần chiều cao chữ hoa của wordmark (icon là phần chủ đạo)\n// • cỡ chữ + giãn chữ của slogan chọn sao cho slogan luôn HẸP HƠN HOẶC BẰNG wordmark\n// (không bao giờ tràn quá vạch vàng).\n// Giãn chữ ở đây nhỏ hơn mức 0.25–0.36em của brand book — mức đó chỉ vừa ở cỡ hero.\nconst SIZE_MAP: Record<\n ComitorLockupSize,\n { icon: string; gap: string; wordmark: string; slogan: string; ruleMargin: string }\n> = {\n xs: {\n icon: \"w-9 h-9\", // 36px — vừa header cao 56px\n gap: \"gap-2.5\",\n wordmark: \"text-sm\", // 14px\n slogan: \"text-[6px] tracking-[0.12em]\", // 6px\n ruleMargin: \"my-0.5\"\n },\n sm: {\n icon: \"w-12 h-12\", // 48px\n gap: \"gap-3\",\n wordmark: \"text-base\", // 16px\n slogan: \"text-[7px] tracking-[0.12em]\", // ~7px\n ruleMargin: \"my-1\"\n },\n md: {\n icon: \"w-20 h-20\", // 80px\n gap: \"gap-4\",\n wordmark: \"text-2xl\", // 24px\n slogan: \"text-[10px] tracking-[0.12em]\", // 10px\n ruleMargin: \"my-1.5\"\n },\n lg: {\n icon: \"w-32 h-32\", // 128px\n gap: \"gap-5\",\n wordmark: \"text-4xl\", // 36px\n slogan: \"text-[14px] tracking-[0.14em]\", // 14px\n ruleMargin: \"my-2\"\n }\n};\n\nexport function ComitorLockup({\n className,\n variant = \"light\",\n size = \"md\",\n wordmark = \"COMITOR\",\n slogan = \"Together Further\",\n sloganClassName,\n style\n}: ComitorLockupProps) {\n const s = SIZE_MAP[size];\n const dark = variant === \"dark\";\n const auto = variant === \"auto\";\n\n return (\n <div\n className={cn(\"flex items-center\", s.gap, className)}\n style={style}\n role=\"img\"\n aria-label={`${wordmark} — ${slogan}`}\n >\n {/* Ba biến thể đi thẳng xuống `ComitorLogo` — từ 0.6.1 chính nó biết `auto`, nên không còn\n bản sao của cặp `dark:hidden` / `hidden dark:block` ở đây. */}\n <ComitorLogo variant={variant} className={cn(\"shrink-0\", s.icon)} />\n <div className=\"flex flex-col\">\n <span\n className={cn(\n \"font-extrabold tracking-tight\",\n s.wordmark,\n // `leading-none` PHẢI đứng SAU `s.wordmark`, không được gộp vào chuỗi đầu.\n // `cn()` chạy qua tailwind-merge, mà ở đó một utility CỠ CHỮ (`text-base`, `text-2xl`…)\n // xung đột với nhóm `leading` và XOÁ mọi `leading-*` đứng TRƯỚC nó — Tailwind coi\n // `text-<size>` là cặp cỡ-chữ + chiều-cao-dòng. Viết `cn(\"… leading-none\", s.wordmark)`\n // thì `leading-none` bị cắt sạch, wordmark rơi về line-height mặc định (24px ở cỡ `sm`\n // thay vì 16px): khối chữ cao thêm 8px, vạch vàng và slogan tụt xuống, lockup không\n // còn khớp tỉ lệ brand book. Không có lỗi biên dịch nào — chỉ đo mới thấy.\n \"leading-none\",\n auto ? \"text-ink dark:text-white\" : dark ? \"text-white\" : \"text-ink\"\n )}\n >\n {wordmark}\n </span>\n <span\n className={cn(\n \"h-px opacity-55\",\n s.ruleMargin,\n auto ? \"bg-gold-400 dark:bg-gold-200\" : dark ? \"bg-gold-200\" : \"bg-gold-400\"\n )}\n aria-hidden=\"true\"\n />\n <span\n className={cn(\n \"font-semibold uppercase\",\n sloganClassName ?? s.slogan,\n auto ? \"text-gold-500 dark:text-gold-200\" : dark ? \"text-gold-200\" : \"text-gold-500\"\n )}\n >\n {slogan}\n </span>\n </div>\n </div>\n );\n}\n"],"mappings":"AA8FM,cACA,YADA;AA7FN,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAoC5B,MAAM,WAGF;AAAA,EACF,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,IAAI,SAAS,IAAI;AACvB,QAAM,OAAO,YAAY;AACzB,QAAM,OAAO,YAAY;AAEzB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,qBAAqB,EAAE,KAAK,SAAS;AAAA,MACnD;AAAA,MACA,MAAK;AAAA,MACL,cAAY,GAAG,QAAQ,WAAM,MAAM;AAAA,MAInC;AAAA,4BAAC,eAAY,SAAkB,WAAW,GAAG,YAAY,EAAE,IAAI,GAAG;AAAA,QAClE,qBAAC,SAAI,WAAU,iBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAQF;AAAA,gBACA,OAAO,6BAA6B,OAAO,eAAe;AAAA,cAC5D;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,EAAE;AAAA,gBACF,OAAO,iCAAiC,OAAO,gBAAgB;AAAA,cACjE;AAAA,cACA,eAAY;AAAA;AAAA,UACd;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,mBAAmB,EAAE;AAAA,gBACrB,OAAO,qCAAqC,OAAO,kBAAkB;AAAA,cACvE;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -4,9 +4,10 @@ import { CSSProperties } from 'react';
|
|
|
4
4
|
/**
|
|
5
5
|
* Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Ba biến thể:
|
|
8
8
|
* • `variant="light"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)
|
|
9
9
|
* • `variant="dark"` — mặt trên trắng; dùng trên nền tối / nền ấm
|
|
10
|
+
* • `variant="auto"` — tự đổi theo theme, giống `ComitorLockup variant="auto"`
|
|
10
11
|
*
|
|
11
12
|
* ⚠ **Ngoại lệ duy nhất của quy tắc "không hardcode màu"**: mã hex dưới đây là vector
|
|
12
13
|
* lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,
|
|
@@ -14,10 +15,10 @@ import { CSSProperties } from 'react';
|
|
|
14
15
|
*
|
|
15
16
|
* File này KHÔNG có "use client": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.
|
|
16
17
|
*/
|
|
17
|
-
type ComitorLogoVariant = "light" | "dark";
|
|
18
|
+
type ComitorLogoVariant = "light" | "dark" | "auto";
|
|
18
19
|
interface ComitorLogoProps {
|
|
19
20
|
className?: string;
|
|
20
|
-
/** Nền sáng dùng "light", nền tối/ấm dùng "dark". */
|
|
21
|
+
/** Nền sáng dùng "light", nền tối/ấm dùng "dark", theo theme dùng "auto". */
|
|
21
22
|
variant?: ComitorLogoVariant;
|
|
22
23
|
/** Nội dung <title> của SVG — cũng là aria-label. */
|
|
23
24
|
title?: string;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
2
3
|
function ComitorLogo({ className, variant = "light", title = "Comitor", style }) {
|
|
4
|
+
if (variant === "auto") {
|
|
5
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant: "light", className: cn(className, "dark:hidden"), title, style }),
|
|
7
|
+
/* @__PURE__ */ jsx(ComitorLogo, { variant: "dark", className: cn(className, "hidden dark:block"), title, style })
|
|
8
|
+
] });
|
|
9
|
+
}
|
|
3
10
|
const dark = variant === "dark";
|
|
4
11
|
const c = dark ? {
|
|
5
12
|
accent: "#8A6800",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../src/shell/comitor-logo.tsx"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Dấu hiệu nhận diện chính thức của Comitor (khối lập phương vàng + 3 hình người đồng hành).\n *\n * Ba biến thể:\n * • `variant=\"light\"` — mặt trên vàng nhạt; dùng trên nền sáng (mặc định)\n * • `variant=\"dark\"` — mặt trên trắng; dùng trên nền tối / nền ấm\n * • `variant=\"auto\"` — tự đổi theo theme, giống `ComitorLockup variant=\"auto\"`\n *\n * ⚠ **Ngoại lệ duy nhất của quy tắc \"không hardcode màu\"**: mã hex dưới đây là vector\n * lấy nguyên từ file nguồn Figma của brand book. KHÔNG tokenize, KHÔNG đổi màu,\n * KHÔNG đơn giản hoá path — logo phải giống hệt nhau ở mọi sản phẩm.\n *\n * File này KHÔNG có \"use client\": logo là SVG tĩnh, để RSC render thẳng, không tốn JS phía client.\n */\nexport type ComitorLogoVariant = \"light\" | \"dark\" | \"auto\";\n\nexport interface ComitorLogoProps {\n className?: string;\n /** Nền sáng dùng \"light\", nền tối/ấm dùng \"dark\", theo theme dùng \"auto\". */\n variant?: ComitorLogoVariant;\n /** Nội dung <title> của SVG — cũng là aria-label. */\n title?: string;\n style?: CSSProperties;\n}\n\nexport function ComitorLogo({ className, variant = \"light\", title = \"Comitor\", style }: ComitorLogoProps) {\n /*\n * `auto` PHẢI vẽ hai bản rồi ẩn/hiện bằng `dark:`, không thể là một SVG đổi màu: mã hex nằm\n * thẳng trong `fill` của từng path (ngoại lệ có chủ ý của quy tắc \"không hardcode màu\" — vector\n * chép nguyên từ brand book), mà `dark:` không với tới thuộc tính `fill`. Đây đúng cách\n * `ComitorLockup variant=\"auto\"` đang làm với chính component này; đưa vào đây để app không phải\n * chép lại cặp `dark:hidden` / `hidden dark:block` ở mỗi chỗ dùng logo trần.\n */\n if (variant === \"auto\") {\n return (\n <>\n <ComitorLogo variant=\"light\" className={cn(className, \"dark:hidden\")} title={title} style={style} />\n <ComitorLogo variant=\"dark\" className={cn(className, \"hidden dark:block\")} title={title} style={style} />\n </>\n );\n }\n\n const dark = variant === \"dark\";\n const c = dark\n ? {\n accent: \"#8A6800\",\n bright: \"#F0CC50\",\n rich: \"#D4A820\",\n topFace: \"#FFFFFF\",\n sphereHi: \"#F0CC50\",\n sphereLo: \"#D4A820\"\n }\n : {\n accent: \"#7A5800\",\n bright: \"#E8B824\",\n rich: \"#B88A00\",\n topFace: \"#F5D060\",\n sphereHi: \"#E8B824\",\n sphereLo: \"#B88A00\"\n };\n\n return (\n <svg\n viewBox=\"0 0 256 256\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n style={style}\n role=\"img\"\n aria-label={title}\n >\n <title>{title}</title>\n {/* Bottom interior diamond */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 142.059L78.5642 170.543L128 209.32L177.344 170.543L128 142.059Z\"\n fill={c.accent}\n />\n {/* Left-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M56.6951 86.8339L66.4352 149.502L91.1528 135.259L115.779 121.016V92.5315V64.0459L56.6951 86.8339Z\"\n fill={c.bright}\n />\n {/* Right-front face */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M189.473 149.502L196.733 88.3046L140.13 64.0459V92.5315V121.016L164.755 135.259L189.473 149.502Z\"\n fill={c.rich}\n />\n {/* Top face / outer sphere highlight */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 0C182.857 0 229.629 34.5499 247.822 82.975C223.747 95.6553 162.917 112.195 128 128C104.569 118.811 28.3934 89.4991 9.74011 78.9317C28.9448 32.6203 74.7053 0 128 0ZM170.544 81.5968L128 106.131L85.3639 81.5968L115.779 64.046L101.445 55.7761V55.6842L80.4941 43.6468L58.9921 31.15C61.2894 29.588 63.6785 28.0259 66.1594 26.4638L88.6719 39.5118C100.709 34.1824 113.941 31.2419 128 31.2419C145.183 31.2419 161.356 35.7445 175.415 43.6468L140.13 64.046L170.544 81.5968Z\"\n fill={c.topFace}\n />\n {/* Right sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M238.817 63.9541C249.752 82.7915 256 104.66 256 128C256 198.662 198.662 256 128 256L120.557 199.03L128 128C164.939 106.682 201.878 85.3641 238.817 63.9541ZM146.929 188.095L177.344 170.544V211.251C202.981 196.089 220.991 169.35 224.207 138.2L246.811 125.152C246.72 122.303 246.536 119.455 246.26 116.698L207.207 139.211L189.473 149.502V114.401L146.929 138.935V188.095Z\"\n fill={c.sphereHi}\n />\n {/* Left sphere half */}\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M128 256C57.2464 256 0 198.662 0 128C0 104.66 6.15656 82.7915 17.0912 63.9541C54.0302 85.3641 90.9691 106.682 128 128V256ZM108.979 188.095V138.935L66.4352 114.401V149.502L31.1501 129.103C31.5176 160.804 47.1387 188.83 70.9376 206.289V232.293C73.4184 233.672 75.9917 234.958 78.5644 236.152V211.251V170.544L108.979 188.095Z\"\n fill={c.sphereLo}\n />\n </svg>\n );\n}\n"],"mappings":"AAsCM,mBACE,KADF;AArCN,SAAS,UAAU;AA2BZ,SAAS,YAAY,EAAE,WAAW,UAAU,SAAS,QAAQ,WAAW,MAAM,GAAqB;AAQxG,MAAI,YAAY,QAAQ;AACtB,WACE,iCACE;AAAA,0BAAC,eAAY,SAAQ,SAAQ,WAAW,GAAG,WAAW,aAAa,GAAG,OAAc,OAAc;AAAA,MAClG,oBAAC,eAAY,SAAQ,QAAO,WAAW,GAAG,WAAW,mBAAmB,GAAG,OAAc,OAAc;AAAA,OACzG;AAAA,EAEJ;AAEA,QAAM,OAAO,YAAY;AACzB,QAAM,IAAI,OACN;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,IACA;AAAA,IACE,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MAEZ;AAAA,4BAAC,WAAO,iBAAM;AAAA,QAEd;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,UAAS;AAAA,YACT,UAAS;AAAA,YACT,GAAE;AAAA,YACF,MAAM,EAAE;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hằng của HAI trục hiển thị thêm: MẬT ĐỘ và CỠ CHỮ.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ File này KHÔNG có `"use client"`, và đó là điều kiện bắt buộc chứ không phải tuỳ chọn.
|
|
5
|
+
* Root layout là Server Component nhưng vẫn cần đọc tên thuộc tính / khoá lưu (vd để render sẵn
|
|
6
|
+
* giá trị lên `<html>`). Mọi export của một module `"use client"` khi đi tới Server Component sẽ
|
|
7
|
+
* biến thành client-reference proxy và đọc ra `undefined` — KHÔNG ném lỗi, không cảnh báo. Đúng
|
|
8
|
+
* cái bẫy mà `contrast-constants.ts` được tách ra để tránh (CHANGELOG 0.2.2 §2), và cổng
|
|
9
|
+
* `verify:directives` canh đúng chuyện này.
|
|
10
|
+
*
|
|
11
|
+
* Vì vậy: hằng ở đây, provider ở file khác, và `shell/index.ts` re-export THẲNG từ file này
|
|
12
|
+
* chứ không đi vòng qua provider.
|
|
13
|
+
*/
|
|
14
|
+
/** `comfortable` là mặc định và KHÔNG ghi thuộc tính ra DOM — xem ghi chú ở cuối file. */
|
|
15
|
+
declare const DENSITY_MODES: readonly ["comfortable", "compact"];
|
|
16
|
+
type DensityMode = (typeof DENSITY_MODES)[number];
|
|
17
|
+
declare const DENSITY_ATTRIBUTE = "data-density";
|
|
18
|
+
declare const DENSITY_STORAGE_KEY = "comitor-density";
|
|
19
|
+
declare const DEFAULT_DENSITY: DensityMode;
|
|
20
|
+
declare function isDensityMode(value: unknown): value is DensityMode;
|
|
21
|
+
/**
|
|
22
|
+
* `md` là mặc định và KHÔNG ghi thuộc tính ra DOM.
|
|
23
|
+
*
|
|
24
|
+
* Giá trị là `sm`/`md`/`lg` chứ không phải `small`/`medium`/`large`: chúng đi thẳng vào
|
|
25
|
+
* `[data-font-size="…"]` trong CSS và vào localStorage, tức là chuỗi MÁY — theo luật tên trường,
|
|
26
|
+
* không theo luật nhãn. Nhãn tiếng Việt do app truyền vào control.
|
|
27
|
+
*/
|
|
28
|
+
declare const FONT_SIZE_MODES: readonly ["sm", "md", "lg"];
|
|
29
|
+
type FontSizeMode = (typeof FONT_SIZE_MODES)[number];
|
|
30
|
+
declare const FONT_SIZE_ATTRIBUTE = "data-font-size";
|
|
31
|
+
declare const FONT_SIZE_STORAGE_KEY = "comitor-font-size";
|
|
32
|
+
declare const DEFAULT_FONT_SIZE: FontSizeMode;
|
|
33
|
+
declare function isFontSizeMode(value: unknown): value is FontSizeMode;
|
|
34
|
+
|
|
35
|
+
export { DEFAULT_DENSITY, DEFAULT_FONT_SIZE, DENSITY_ATTRIBUTE, DENSITY_MODES, DENSITY_STORAGE_KEY, type DensityMode, FONT_SIZE_ATTRIBUTE, FONT_SIZE_MODES, FONT_SIZE_STORAGE_KEY, type FontSizeMode, isDensityMode, isFontSizeMode };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const DENSITY_MODES = ["comfortable", "compact"];
|
|
2
|
+
const DENSITY_ATTRIBUTE = "data-density";
|
|
3
|
+
const DENSITY_STORAGE_KEY = "comitor-density";
|
|
4
|
+
const DEFAULT_DENSITY = "comfortable";
|
|
5
|
+
function isDensityMode(value) {
|
|
6
|
+
return typeof value === "string" && DENSITY_MODES.includes(value);
|
|
7
|
+
}
|
|
8
|
+
const FONT_SIZE_MODES = ["sm", "md", "lg"];
|
|
9
|
+
const FONT_SIZE_ATTRIBUTE = "data-font-size";
|
|
10
|
+
const FONT_SIZE_STORAGE_KEY = "comitor-font-size";
|
|
11
|
+
const DEFAULT_FONT_SIZE = "md";
|
|
12
|
+
function isFontSizeMode(value) {
|
|
13
|
+
return typeof value === "string" && FONT_SIZE_MODES.includes(value);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_DENSITY,
|
|
17
|
+
DEFAULT_FONT_SIZE,
|
|
18
|
+
DENSITY_ATTRIBUTE,
|
|
19
|
+
DENSITY_MODES,
|
|
20
|
+
DENSITY_STORAGE_KEY,
|
|
21
|
+
FONT_SIZE_ATTRIBUTE,
|
|
22
|
+
FONT_SIZE_MODES,
|
|
23
|
+
FONT_SIZE_STORAGE_KEY,
|
|
24
|
+
isDensityMode,
|
|
25
|
+
isFontSizeMode
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=display-axis-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/display-axis-constants.ts"],"sourcesContent":["/**\n * Hằng của HAI trục hiển thị thêm: MẬT ĐỘ và CỠ CHỮ.\n *\n * ⚠ File này KHÔNG có `\"use client\"`, và đó là điều kiện bắt buộc chứ không phải tuỳ chọn.\n * Root layout là Server Component nhưng vẫn cần đọc tên thuộc tính / khoá lưu (vd để render sẵn\n * giá trị lên `<html>`). Mọi export của một module `\"use client\"` khi đi tới Server Component sẽ\n * biến thành client-reference proxy và đọc ra `undefined` — KHÔNG ném lỗi, không cảnh báo. Đúng\n * cái bẫy mà `contrast-constants.ts` được tách ra để tránh (CHANGELOG 0.2.2 §2), và cổng\n * `verify:directives` canh đúng chuyện này.\n *\n * Vì vậy: hằng ở đây, provider ở file khác, và `shell/index.ts` re-export THẲNG từ file này\n * chứ không đi vòng qua provider.\n */\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 3 — MẬT ĐỘ (bố cục gọn)\n ──────────────────────────────────────────────────────────────────────────── */\n\n/** `comfortable` là mặc định và KHÔNG ghi thuộc tính ra DOM — xem ghi chú ở cuối file. */\nexport const DENSITY_MODES = [\"comfortable\", \"compact\"] as const;\nexport type DensityMode = (typeof DENSITY_MODES)[number];\n\nexport const DENSITY_ATTRIBUTE = \"data-density\";\nexport const DENSITY_STORAGE_KEY = \"comitor-density\";\nexport const DEFAULT_DENSITY: DensityMode = \"comfortable\";\n\nexport function isDensityMode(value: unknown): value is DensityMode {\n return typeof value === \"string\" && (DENSITY_MODES as readonly string[]).includes(value);\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 4 — CỠ CHỮ\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * `md` là mặc định và KHÔNG ghi thuộc tính ra DOM.\n *\n * Giá trị là `sm`/`md`/`lg` chứ không phải `small`/`medium`/`large`: chúng đi thẳng vào\n * `[data-font-size=\"…\"]` trong CSS và vào localStorage, tức là chuỗi MÁY — theo luật tên trường,\n * không theo luật nhãn. Nhãn tiếng Việt do app truyền vào control.\n */\nexport const FONT_SIZE_MODES = [\"sm\", \"md\", \"lg\"] as const;\nexport type FontSizeMode = (typeof FONT_SIZE_MODES)[number];\n\nexport const FONT_SIZE_ATTRIBUTE = \"data-font-size\";\nexport const FONT_SIZE_STORAGE_KEY = \"comitor-font-size\";\nexport const DEFAULT_FONT_SIZE: FontSizeMode = \"md\";\n\nexport function isFontSizeMode(value: unknown): value is FontSizeMode {\n return typeof value === \"string\" && (FONT_SIZE_MODES as readonly string[]).includes(value);\n}\n\n/**\n * ─ VÌ SAO GIÁ TRỊ MẶC ĐỊNH KHÔNG GHI THUỘC TÍNH ─\n *\n * Cả hai trục đều GỠ thuộc tính khi ở mức mặc định thay vì ghi `data-density=\"comfortable\"`.\n * Cùng lối với `ContrastProvider`, và ở trục cỡ chữ thì nó còn là chuyện TRỢ NĂNG:\n *\n * Mức `md` không được phép là `font-size: 100%` viết ra DOM, càng không được là `16px`. Người\n * dùng có thể đã tăng cỡ chữ mặc định của trình duyệt; khai một cỡ tuyệt đối ở `<html>` là XOÁ\n * lựa chọn đó — tức tự tay vi phạm WCAG 1.4.4 ngay bên trong một tính năng trợ năng. Không ghi gì\n * ở mức mặc định thì cỡ gốc của trình duyệt được giữ nguyên, còn `sm`/`lg` là PHẦN TRĂM của chính\n * cỡ đó (87,5% / 112,5%) nên vẫn tôn trọng nó.\n *\n * ⚠ Không cổng nào bắt được lỗi này: `check-contrast.mjs` chỉ có ngưỡng 4,5 và 3, không có logic\n * cỡ chữ nào cả.\n */\n"],"mappings":"AAmBO,MAAM,gBAAgB,CAAC,eAAe,SAAS;AAG/C,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAC5B,MAAM,kBAA+B;AAErC,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;AAaO,MAAM,kBAAkB,CAAC,MAAM,MAAM,IAAI;AAGzC,MAAM,sBAAsB;AAC5B,MAAM,wBAAwB;AAC9B,MAAM,oBAAkC;AAExC,SAAS,eAAe,OAAuC;AACpE,SAAO,OAAO,UAAU,YAAa,gBAAsC,SAAS,KAAK;AAC3F;","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { FontSizeMode } from './display-axis-constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.
|
|
6
|
+
*
|
|
7
|
+
* ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─
|
|
8
|
+
* Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp
|
|
9
|
+
* HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho
|
|
10
|
+
* tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn
|
|
11
|
+
* thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi
|
|
12
|
+
* `mounted` là cách `ContrastToggle` đã dùng.
|
|
13
|
+
*
|
|
14
|
+
* (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước
|
|
15
|
+
* lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)
|
|
16
|
+
*/
|
|
17
|
+
interface DensityToggleProps {
|
|
18
|
+
label?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Nút gạt "bố cục gọn".
|
|
24
|
+
*
|
|
25
|
+
* ⚠ Đây KHÔNG phải một trường của biểu mẫu cài đặt. Nó có hiệu lực NGAY và tự lưu, giống
|
|
26
|
+
* `ThemeToggle`/`ContrastToggle` — đừng đưa nó vào luồng nháp/"Lưu thay đổi". Trộn hai kiểu thì
|
|
27
|
+
* thanh "Có thay đổi chưa lưu" sáng lên cho một thay đổi đã có hiệu lực, và nút "Hoàn tác" trong
|
|
28
|
+
* toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.
|
|
29
|
+
*/
|
|
30
|
+
declare function DensityToggle({ label, description, className }: DensityToggleProps): React.JSX.Element;
|
|
31
|
+
interface FontSizeControlProps {
|
|
32
|
+
/** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). */
|
|
35
|
+
optionLabels?: Record<FontSizeMode, string>;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Chọn cỡ chữ — ba mức, một hàng nút.
|
|
40
|
+
*
|
|
41
|
+
* `role="radiogroup"` + `aria-checked` chứ không phải ba nút thường: đây là chọn MỘT trong ba, và
|
|
42
|
+
* trình đọc màn hình cần nghe "2 trên 3" chứ không nghe ba nút rời rạc. Dùng `<button>` thật (bàn
|
|
43
|
+
* phím tới được bằng Tab) thay vì roving focus của Radix ToggleGroup — nhóm chỉ có ba mục nên
|
|
44
|
+
* roving focus không đáng cái giá thêm một phụ thuộc.
|
|
45
|
+
*
|
|
46
|
+
* Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm "Lớn" sẽ
|
|
47
|
+
* làm cả hàng nút nhảy chỗ ngay dưới ngón tay.
|
|
48
|
+
*/
|
|
49
|
+
declare function FontSizeControl({ label, optionLabels, className }: FontSizeControlProps): React.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { DensityToggle, type DensityToggleProps, FontSizeControl, type FontSizeControlProps };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Switch } from "../components/ui/switch.js";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { FONT_SIZE_MODES } from "./display-axis-constants.js";
|
|
6
|
+
import { useDensity, useFontSize } from "./display-axis-provider.js";
|
|
7
|
+
function DensityToggle({
|
|
8
|
+
label = "B\u1ED1 c\u1EE5c g\u1ECDn",
|
|
9
|
+
description = "Gi\u1EA3m kho\u1EA3ng c\xE1ch d\u1ECDc gi\u1EEFa c\xE1c d\xF2ng trong b\u1EA3ng, danh s\xE1ch v\xE0 menu.",
|
|
10
|
+
className
|
|
11
|
+
}) {
|
|
12
|
+
const { density, setDensity, mounted } = useDensity();
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
Switch,
|
|
15
|
+
{
|
|
16
|
+
className,
|
|
17
|
+
checked: density === "compact",
|
|
18
|
+
onCheckedChange: (checked) => setDensity(checked ? "compact" : "comfortable"),
|
|
19
|
+
disabled: !mounted,
|
|
20
|
+
label,
|
|
21
|
+
description
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_FONT_SIZE_LABELS = {
|
|
26
|
+
sm: "Nh\u1ECF",
|
|
27
|
+
md: "V\u1EEBa",
|
|
28
|
+
lg: "L\u1EDBn"
|
|
29
|
+
};
|
|
30
|
+
function FontSizeControl({
|
|
31
|
+
label = "C\u1EE1 ch\u1EEF",
|
|
32
|
+
optionLabels = DEFAULT_FONT_SIZE_LABELS,
|
|
33
|
+
className
|
|
34
|
+
}) {
|
|
35
|
+
const { fontSize, setFontSize, mounted } = useFontSize();
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
role: "radiogroup",
|
|
40
|
+
"aria-label": label,
|
|
41
|
+
className: cn("inline-flex items-center gap-1 rounded-md border border-input p-1 text-sm", className),
|
|
42
|
+
children: FONT_SIZE_MODES.map((mode) => {
|
|
43
|
+
const selected = fontSize === mode;
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
"button",
|
|
46
|
+
{
|
|
47
|
+
type: "button",
|
|
48
|
+
role: "radio",
|
|
49
|
+
"aria-checked": selected,
|
|
50
|
+
disabled: !mounted,
|
|
51
|
+
onClick: () => setFontSize(mode),
|
|
52
|
+
className: cn(
|
|
53
|
+
"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out",
|
|
54
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
|
|
55
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
56
|
+
selected ? "bg-control-on text-control-check font-medium" : "text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
57
|
+
),
|
|
58
|
+
children: optionLabels[mode]
|
|
59
|
+
},
|
|
60
|
+
mode
|
|
61
|
+
);
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
DensityToggle,
|
|
68
|
+
FontSizeControl
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=display-axis-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/display-axis-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport { Switch } from \"../components/ui/switch.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { FONT_SIZE_MODES, type FontSizeMode } from \"./display-axis-constants.js\";\nimport { useDensity, useFontSize } from \"./display-axis-provider.js\";\n\n/**\n * Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.\n *\n * ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─\n * Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp\n * HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho\n * tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn\n * thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi\n * `mounted` là cách `ContrastToggle` đã dùng.\n *\n * (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước\n * lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)\n */\n\nexport interface DensityToggleProps {\n label?: string;\n description?: string;\n className?: string;\n}\n\n/**\n * Nút gạt \"bố cục gọn\".\n *\n * ⚠ Đây KHÔNG phải một trường của biểu mẫu cài đặt. Nó có hiệu lực NGAY và tự lưu, giống\n * `ThemeToggle`/`ContrastToggle` — đừng đưa nó vào luồng nháp/\"Lưu thay đổi\". Trộn hai kiểu thì\n * thanh \"Có thay đổi chưa lưu\" sáng lên cho một thay đổi đã có hiệu lực, và nút \"Hoàn tác\" trong\n * toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.\n */\nexport function DensityToggle({\n label = \"Bố cục gọn\",\n description = \"Giảm khoảng cách dọc giữa các dòng trong bảng, danh sách và menu.\",\n className\n}: DensityToggleProps) {\n const { density, setDensity, mounted } = useDensity();\n\n return (\n <Switch\n className={className}\n checked={density === \"compact\"}\n onCheckedChange={(checked) => setDensity(checked ? \"compact\" : \"comfortable\")}\n disabled={!mounted}\n label={label}\n description={description}\n />\n );\n}\n\nexport interface FontSizeControlProps {\n /** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình. */\n label?: string;\n /** Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). */\n optionLabels?: Record<FontSizeMode, string>;\n className?: string;\n}\n\nconst DEFAULT_FONT_SIZE_LABELS: Record<FontSizeMode, string> = {\n sm: \"Nhỏ\",\n md: \"Vừa\",\n lg: \"Lớn\"\n};\n\n/**\n * Chọn cỡ chữ — ba mức, một hàng nút.\n *\n * `role=\"radiogroup\"` + `aria-checked` chứ không phải ba nút thường: đây là chọn MỘT trong ba, và\n * trình đọc màn hình cần nghe \"2 trên 3\" chứ không nghe ba nút rời rạc. Dùng `<button>` thật (bàn\n * phím tới được bằng Tab) thay vì roving focus của Radix ToggleGroup — nhóm chỉ có ba mục nên\n * roving focus không đáng cái giá thêm một phụ thuộc.\n *\n * Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm \"Lớn\" sẽ\n * làm cả hàng nút nhảy chỗ ngay dưới ngón tay.\n */\nexport function FontSizeControl({\n label = \"Cỡ chữ\",\n optionLabels = DEFAULT_FONT_SIZE_LABELS,\n className\n}: FontSizeControlProps) {\n const { fontSize, setFontSize, mounted } = useFontSize();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={label}\n className={cn(\"inline-flex items-center gap-1 rounded-md border border-input p-1 text-sm\", className)}\n >\n {FONT_SIZE_MODES.map((mode) => {\n const selected = fontSize === mode;\n return (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n disabled={!mounted}\n onClick={() => setFontSize(mode)}\n className={cn(\n \"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n selected\n ? \"bg-control-on text-control-check font-medium\"\n : \"text-muted-foreground hover:bg-accent hover:text-foreground\"\n )}\n >\n {optionLabels[mode]}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AA2CI;AAzCJ,SAAS,cAAc;AACvB,SAAS,UAAU;AACnB,SAAS,uBAA0C;AACnD,SAAS,YAAY,mBAAmB;AA8BjC,SAAS,cAAc;AAAA,EAC5B,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AACF,GAAuB;AACrB,QAAM,EAAE,SAAS,YAAY,QAAQ,IAAI,WAAW;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY;AAAA,MACrB,iBAAiB,CAAC,YAAY,WAAW,UAAU,YAAY,aAAa;AAAA,MAC5E,UAAU,CAAC;AAAA,MACX;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AAUA,MAAM,2BAAyD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAaO,SAAS,gBAAgB;AAAA,EAC9B,QAAQ;AAAA,EACR,eAAe;AAAA,EACf;AACF,GAAyB;AACvB,QAAM,EAAE,UAAU,aAAa,QAAQ,IAAI,YAAY;AAEvD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,6EAA6E,SAAS;AAAA,MAEnG,0BAAgB,IAAI,CAAC,SAAS;AAC7B,cAAM,WAAW,aAAa;AAC9B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,YAAY,IAAI;AAAA,YAC/B,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,WACI,iDACA;AAAA,YACN;AAAA,YAEC,uBAAa,IAAI;AAAA;AAAA,UAfb;AAAA,QAgBP;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { DensityMode, FontSizeMode } from './display-axis-constants.js';
|
|
4
|
+
|
|
5
|
+
interface DisplayAxisValue<T extends string> {
|
|
6
|
+
value: T;
|
|
7
|
+
setValue: (next: T) => void;
|
|
8
|
+
/** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration ở control. */
|
|
9
|
+
mounted: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare const DensityProvider: ({ children, defaultValue, storageKey }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
defaultValue?: "compact" | "comfortable" | undefined;
|
|
14
|
+
storageKey?: string;
|
|
15
|
+
}) => React.JSX.Element;
|
|
16
|
+
declare const DensityScript: ({ storageKey }: {
|
|
17
|
+
storageKey?: string;
|
|
18
|
+
}) => React.JSX.Element;
|
|
19
|
+
/** Đọc/đổi mật độ bố cục. */
|
|
20
|
+
declare function useDensity(): {
|
|
21
|
+
density: DensityMode;
|
|
22
|
+
setDensity: (m: DensityMode) => void;
|
|
23
|
+
mounted: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare const FontSizeProvider: ({ children, defaultValue, storageKey }: {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
defaultValue?: "sm" | "md" | "lg" | undefined;
|
|
28
|
+
storageKey?: string;
|
|
29
|
+
}) => React.JSX.Element;
|
|
30
|
+
declare const FontSizeScript: ({ storageKey }: {
|
|
31
|
+
storageKey?: string;
|
|
32
|
+
}) => React.JSX.Element;
|
|
33
|
+
/** Đọc/đổi cỡ chữ. */
|
|
34
|
+
declare function useFontSize(): {
|
|
35
|
+
fontSize: FontSizeMode;
|
|
36
|
+
setFontSize: (m: FontSizeMode) => void;
|
|
37
|
+
mounted: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { DensityProvider, DensityScript, type DisplayAxisValue, FontSizeProvider, FontSizeScript, useDensity, useFontSize };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_DENSITY,
|
|
6
|
+
DEFAULT_FONT_SIZE,
|
|
7
|
+
DENSITY_ATTRIBUTE,
|
|
8
|
+
DENSITY_STORAGE_KEY,
|
|
9
|
+
FONT_SIZE_ATTRIBUTE,
|
|
10
|
+
FONT_SIZE_STORAGE_KEY,
|
|
11
|
+
isDensityMode,
|
|
12
|
+
isFontSizeMode
|
|
13
|
+
} from "./display-axis-constants.js";
|
|
14
|
+
function createDisplayAxis(config) {
|
|
15
|
+
const Context = createContext(null);
|
|
16
|
+
function AxisScript({ storageKey = config.storageKey }) {
|
|
17
|
+
const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v&&v!==${JSON.stringify(config.defaultValue)})document.documentElement.setAttribute(${JSON.stringify(config.attribute)},v)}catch(e){}})();`;
|
|
18
|
+
return /* @__PURE__ */ jsx("script", { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: script } });
|
|
19
|
+
}
|
|
20
|
+
function Provider({
|
|
21
|
+
children,
|
|
22
|
+
defaultValue = config.defaultValue,
|
|
23
|
+
storageKey = config.storageKey
|
|
24
|
+
}) {
|
|
25
|
+
const [value, setValueState] = useState(defaultValue);
|
|
26
|
+
const [mounted, setMounted] = useState(false);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let stored = null;
|
|
29
|
+
try {
|
|
30
|
+
stored = window.localStorage.getItem(storageKey);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
if (config.isValue(stored)) setValueState(stored);
|
|
34
|
+
setMounted(true);
|
|
35
|
+
}, [storageKey]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!mounted) return;
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
if (value === config.defaultValue) root.removeAttribute(config.attribute);
|
|
40
|
+
else root.setAttribute(config.attribute, value);
|
|
41
|
+
}, [value, mounted]);
|
|
42
|
+
const setValue = useCallback(
|
|
43
|
+
(next) => {
|
|
44
|
+
setValueState(next);
|
|
45
|
+
try {
|
|
46
|
+
window.localStorage.setItem(storageKey, next);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[storageKey]
|
|
51
|
+
);
|
|
52
|
+
const ctx = useMemo(() => ({ value, setValue, mounted }), [value, setValue, mounted]);
|
|
53
|
+
return /* @__PURE__ */ jsxs(Context.Provider, { value: ctx, children: [
|
|
54
|
+
/* @__PURE__ */ jsx(AxisScript, { storageKey }),
|
|
55
|
+
children
|
|
56
|
+
] });
|
|
57
|
+
}
|
|
58
|
+
function useAxis() {
|
|
59
|
+
const ctx = useContext(Context);
|
|
60
|
+
if (!ctx) throw new Error(`${config.hookName}() ph\u1EA3i n\u1EB1m trong <${config.providerName}>. B\u1ECDc n\xF3 \u1EDF root layout.`);
|
|
61
|
+
return ctx;
|
|
62
|
+
}
|
|
63
|
+
return { Provider, useAxis, AxisScript };
|
|
64
|
+
}
|
|
65
|
+
const densityAxis = createDisplayAxis({
|
|
66
|
+
attribute: DENSITY_ATTRIBUTE,
|
|
67
|
+
storageKey: DENSITY_STORAGE_KEY,
|
|
68
|
+
defaultValue: DEFAULT_DENSITY,
|
|
69
|
+
isValue: isDensityMode,
|
|
70
|
+
hookName: "useDensity",
|
|
71
|
+
providerName: "DensityProvider"
|
|
72
|
+
});
|
|
73
|
+
const DensityProvider = densityAxis.Provider;
|
|
74
|
+
const DensityScript = densityAxis.AxisScript;
|
|
75
|
+
function useDensity() {
|
|
76
|
+
const { value, setValue, mounted } = densityAxis.useAxis();
|
|
77
|
+
return { density: value, setDensity: setValue, mounted };
|
|
78
|
+
}
|
|
79
|
+
const fontSizeAxis = createDisplayAxis({
|
|
80
|
+
attribute: FONT_SIZE_ATTRIBUTE,
|
|
81
|
+
storageKey: FONT_SIZE_STORAGE_KEY,
|
|
82
|
+
defaultValue: DEFAULT_FONT_SIZE,
|
|
83
|
+
isValue: isFontSizeMode,
|
|
84
|
+
hookName: "useFontSize",
|
|
85
|
+
providerName: "FontSizeProvider"
|
|
86
|
+
});
|
|
87
|
+
const FontSizeProvider = fontSizeAxis.Provider;
|
|
88
|
+
const FontSizeScript = fontSizeAxis.AxisScript;
|
|
89
|
+
function useFontSize() {
|
|
90
|
+
const { value, setValue, mounted } = fontSizeAxis.useAxis();
|
|
91
|
+
return { fontSize: value, setFontSize: setValue, mounted };
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
DensityProvider,
|
|
95
|
+
DensityScript,
|
|
96
|
+
FontSizeProvider,
|
|
97
|
+
FontSizeScript,
|
|
98
|
+
useDensity,
|
|
99
|
+
useFontSize
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=display-axis-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/display-axis-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport {\n DEFAULT_DENSITY,\n DEFAULT_FONT_SIZE,\n DENSITY_ATTRIBUTE,\n DENSITY_STORAGE_KEY,\n type DensityMode,\n FONT_SIZE_ATTRIBUTE,\n FONT_SIZE_STORAGE_KEY,\n type FontSizeMode,\n isDensityMode,\n isFontSizeMode\n} from \"./display-axis-constants.js\";\n\n/**\n * Xưởng dựng TRỤC HIỂN THỊ — dùng chung cho mật độ và cỡ chữ.\n *\n * `ContrastProvider` viết tay ~60 dòng cho trục thứ hai. Trục thứ ba và thứ tư sẽ là hai bản chép\n * gần như y hệt, nên phần chung tách ra đây: cùng cơ chế đọc localStorage sau khi mount, cùng cờ\n * `mounted` chống lệch hydration, cùng script chống nháy trong `<body>`, cùng quy ước GỠ thuộc\n * tính ở giá trị mặc định.\n *\n * ⚠ `ContrastProvider` CỐ Ý chưa chuyển sang xưởng này. Nó là API đã phát hành và có hành vi\n * riêng đã được ghi chép; chuyển nó là một thay đổi có rủi ro mà lượt này không cần tới. Nếu sau\n * này chuyển thì chuyển một mình nó, có bản so đầu ra trước/sau.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` ─\n * Nó giữ MỘT context singleton cho cả cây. Lồng thêm provider với `attribute`/`storageKey` khác\n * thì provider trong cùng thắng và `useTheme()` của app đọc nhầm trục — không lỗi biên dịch, chỉ\n * là nút đổi sáng/tối lặng lẽ ngừng ăn. `contrast-provider.tsx` đã ghi lại đúng cạm bẫy này.\n */\ninterface AxisConfig<T extends string> {\n attribute: string;\n storageKey: string;\n defaultValue: T;\n isValue: (v: unknown) => v is T;\n hookName: string;\n providerName: string;\n}\n\nexport interface DisplayAxisValue<T extends string> {\n value: T;\n setValue: (next: T) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration ở control. */\n mounted: boolean;\n}\n\nfunction createDisplayAxis<T extends string>(config: AxisConfig<T>) {\n const Context = createContext<DisplayAxisValue<T> | null>(null);\n\n /**\n * Script chống nháy. Phải chạy TRƯỚC lần sơn đầu tiên, nếu không người đã chọn `lg` sẽ thấy một\n * khung hình cỡ mặc định rồi mới nhảy. Effect của React chạy SAU khi sơn nên không dùng được.\n *\n * ⚠ Nằm trong `<body>`, KHÔNG phải `<head>`: React 19 kêu \"Encountered a script tag while\n * rendering React component\" ở mọi lần render client nếu đặt vào head. Trong body thì tương\n * đương về chống nháy vì parser chạy script ngay khi gặp, mà nó đứng trước toàn bộ markup.\n */\n function AxisScript({ storageKey = config.storageKey }: { storageKey?: string }) {\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v&&v!==${JSON.stringify(config.defaultValue)})document.documentElement.setAttribute(${JSON.stringify(config.attribute)},v)}catch(e){}})();`;\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n return <script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: script }} />;\n }\n\n function Provider({\n children,\n defaultValue = config.defaultValue,\n storageKey = config.storageKey\n }: {\n children: ReactNode;\n defaultValue?: T;\n storageKey?: string;\n }) {\n const [value, setValueState] = useState<T>(defaultValue);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được AxisScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (config.isValue(stored)) setValueState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n // Mặc định thì GỠ thuộc tính, không ghi giá trị — xem ghi chú cuối display-axis-constants.ts.\n if (value === config.defaultValue) root.removeAttribute(config.attribute);\n else root.setAttribute(config.attribute, value);\n }, [value, mounted]);\n\n const setValue = useCallback(\n (next: T) => {\n setValueState(next);\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const ctx = useMemo<DisplayAxisValue<T>>(() => ({ value, setValue, mounted }), [value, setValue, mounted]);\n\n return (\n <Context.Provider value={ctx}>\n <AxisScript storageKey={storageKey} />\n {children}\n </Context.Provider>\n );\n }\n\n function useAxis(): DisplayAxisValue<T> {\n const ctx = useContext(Context);\n // Ném lỗi thay vì trả mặc định: im lặng thì control trông như hỏng mà không chỉ ra nguyên nhân.\n if (!ctx) throw new Error(`${config.hookName}() phải nằm trong <${config.providerName}>. Bọc nó ở root layout.`);\n return ctx;\n }\n\n return { Provider, useAxis, AxisScript };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 3 — MẬT ĐỘ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst densityAxis = createDisplayAxis<DensityMode>({\n attribute: DENSITY_ATTRIBUTE,\n storageKey: DENSITY_STORAGE_KEY,\n defaultValue: DEFAULT_DENSITY,\n isValue: isDensityMode,\n hookName: \"useDensity\",\n providerName: \"DensityProvider\"\n});\n\nexport const DensityProvider = densityAxis.Provider;\nexport const DensityScript = densityAxis.AxisScript;\n\n/** Đọc/đổi mật độ bố cục. */\nexport function useDensity(): { density: DensityMode; setDensity: (m: DensityMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = densityAxis.useAxis();\n return { density: value, setDensity: setValue, mounted };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 4 — CỠ CHỮ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst fontSizeAxis = createDisplayAxis<FontSizeMode>({\n attribute: FONT_SIZE_ATTRIBUTE,\n storageKey: FONT_SIZE_STORAGE_KEY,\n defaultValue: DEFAULT_FONT_SIZE,\n isValue: isFontSizeMode,\n hookName: \"useFontSize\",\n providerName: \"FontSizeProvider\"\n});\n\nexport const FontSizeProvider = fontSizeAxis.Provider;\nexport const FontSizeScript = fontSizeAxis.AxisScript;\n\n/** Đọc/đổi cỡ chữ. */\nexport function useFontSize(): { fontSize: FontSizeMode; setFontSize: (m: FontSizeMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = fontSizeAxis.useAxis();\n return { fontSize: value, setFontSize: setValue, mounted };\n}\n"],"mappings":";AA+DW,cAmDL,YAnDK;AA7DX,SAAS,eAA+B,aAAa,YAAY,WAAW,SAAS,gBAAgB;AACrG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AAmCP,SAAS,kBAAoC,QAAuB;AAClE,QAAM,UAAU,cAA0C,IAAI;AAU9D,WAAS,WAAW,EAAE,aAAa,OAAO,WAAW,GAA4B;AAC/E,UAAM,SAAS,8CAA8C,KAAK,UAAU,UAAU,CAAC,eAAe,KAAK,UAAU,OAAO,YAAY,CAAC,0CAA0C,KAAK,UAAU,OAAO,SAAS,CAAC;AAEnN,WAAO,oBAAC,YAAO,0BAAwB,MAAC,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AAAA,EACvF;AAEA,WAAS,SAAS;AAAA,IAChB;AAAA,IACA,eAAe,OAAO;AAAA,IACtB,aAAa,OAAO;AAAA,EACtB,GAIG;AACD,UAAM,CAAC,OAAO,aAAa,IAAI,SAAY,YAAY;AACvD,UAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAI5C,cAAU,MAAM;AACd,UAAI,SAAwB;AAC5B,UAAI;AACF,iBAAS,OAAO,aAAa,QAAQ,UAAU;AAAA,MACjD,QAAQ;AAAA,MAER;AACA,UAAI,OAAO,QAAQ,MAAM,EAAG,eAAc,MAAM;AAChD,iBAAW,IAAI;AAAA,IACjB,GAAG,CAAC,UAAU,CAAC;AAEf,cAAU,MAAM;AACd,UAAI,CAAC,QAAS;AACd,YAAM,OAAO,SAAS;AAEtB,UAAI,UAAU,OAAO,aAAc,MAAK,gBAAgB,OAAO,SAAS;AAAA,UACnE,MAAK,aAAa,OAAO,WAAW,KAAK;AAAA,IAChD,GAAG,CAAC,OAAO,OAAO,CAAC;AAEnB,UAAM,WAAW;AAAA,MACf,CAAC,SAAY;AACX,sBAAc,IAAI;AAClB,YAAI;AACF,iBAAO,aAAa,QAAQ,YAAY,IAAI;AAAA,QAC9C,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,MACA,CAAC,UAAU;AAAA,IACb;AAEA,UAAM,MAAM,QAA6B,OAAO,EAAE,OAAO,UAAU,QAAQ,IAAI,CAAC,OAAO,UAAU,OAAO,CAAC;AAEzG,WACE,qBAAC,QAAQ,UAAR,EAAiB,OAAO,KACvB;AAAA,0BAAC,cAAW,YAAwB;AAAA,MACnC;AAAA,OACH;AAAA,EAEJ;AAEA,WAAS,UAA+B;AACtC,UAAM,MAAM,WAAW,OAAO;AAE9B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,GAAG,OAAO,QAAQ,gCAAsB,OAAO,YAAY,uCAA0B;AAC/G,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,UAAU,SAAS,WAAW;AACzC;AAMA,MAAM,cAAc,kBAA+B;AAAA,EACjD,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,SAAS;AAAA,EACT,UAAU;AAAA,EACV,cAAc;AAChB,CAAC;AAEM,MAAM,kBAAkB,YAAY;AACpC,MAAM,gBAAgB,YAAY;AAGlC,SAAS,aAA+F;AAC7G,QAAM,EAAE,OAAO,UAAU,QAAQ,IAAI,YAAY,QAAQ;AACzD,SAAO,EAAE,SAAS,OAAO,YAAY,UAAU,QAAQ;AACzD;AAMA,MAAM,eAAe,kBAAgC;AAAA,EACnD,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,SAAS;AAAA,EACT,UAAU;AAAA,EACV,cAAc;AAChB,CAAC;AAEM,MAAM,mBAAmB,aAAa;AACtC,MAAM,iBAAiB,aAAa;AAGpC,SAAS,cAAoG;AAClH,QAAM,EAAE,OAAO,UAAU,QAAQ,IAAI,aAAa,QAAQ;AAC1D,SAAO,EAAE,UAAU,OAAO,aAAa,UAAU,QAAQ;AAC3D;","names":[]}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export { ComitorLogo, ComitorLogoProps, ComitorLogoVariant } from './comitor-log
|
|
|
7
7
|
export { CONTRAST_ATTRIBUTE, CONTRAST_MODES, CONTRAST_STORAGE_KEY, ContrastMode, ContrastToggleLabels, DEFAULT_CONTRAST_TOGGLE_LABELS, isContrastMode } from './contrast-constants.js';
|
|
8
8
|
export { ContrastContextValue, ContrastProvider, ContrastProviderProps, ContrastScript, useContrast } from './contrast-provider.js';
|
|
9
9
|
export { ContrastToggle, ContrastToggleProps } from './contrast-toggle.js';
|
|
10
|
+
export { DEFAULT_DENSITY, DEFAULT_FONT_SIZE, DENSITY_ATTRIBUTE, DENSITY_MODES, DENSITY_STORAGE_KEY, DensityMode, FONT_SIZE_ATTRIBUTE, FONT_SIZE_MODES, FONT_SIZE_STORAGE_KEY, FontSizeMode, isDensityMode, isFontSizeMode } from './display-axis-constants.js';
|
|
11
|
+
export { DensityToggle, DensityToggleProps, FontSizeControl, FontSizeControlProps } from './display-axis-controls.js';
|
|
12
|
+
export { DensityProvider, DensityScript, DisplayAxisValue, FontSizeProvider, FontSizeScript, useDensity, useFontSize } from './display-axis-provider.js';
|
|
10
13
|
export { MobileMenu, MobileMenuProps } from './mobile-menu.js';
|
|
11
14
|
export { NotificationsButton, NotificationsButtonProps } from './notifications-button.js';
|
|
12
15
|
export { ShellContextValue, ShellProvider, ShellProviderProps, useShell } from './shell-context.js';
|
package/dist/shell/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export * from "./comitor-logo.js";
|
|
|
7
7
|
export * from "./contrast-constants.js";
|
|
8
8
|
export * from "./contrast-provider.js";
|
|
9
9
|
export * from "./contrast-toggle.js";
|
|
10
|
+
export * from "./display-axis-constants.js";
|
|
11
|
+
export * from "./display-axis-controls.js";
|
|
12
|
+
export * from "./display-axis-provider.js";
|
|
10
13
|
export * from "./mobile-menu.js";
|
|
11
14
|
export * from "./notifications-button.js";
|
|
12
15
|
export * from "./shell-context.js";
|
package/dist/shell/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/shell\" — Tier 3: khung ứng dụng (AppShell, Sidebar, AppHeader,\n * WorkspaceSwitcher, AppLauncher, UserMenu, ThemeProvider…).\n *\n * Tách khỏi entry chính vì tầng này kéo theo `next/navigation` và `next-themes` — hai\n * peer OPTIONAL. App không chạy trên Next thì đừng import entry này (hoặc tự cấp\n * `ShellLinkComponent` riêng qua `ShellProvider`).\n *\n * Toàn bộ file ở đây là client component: có state, có routing, có phím tắt.\n */\n\n/**\n * Toaster bám theo next-themes — về tier thì nó là primitive (`components/ui/sonner.tsx`),\n * nhưng đặt cửa ra ở entry shell vì nó gọi `useTheme()`, mà `next-themes` chỉ là peer\n * OPTIONAL: để ở entry chính là ép mọi app phải cài next-themes.\n * Dùng chung với `ThemeProvider` ở trên. Nếu app KHÔNG dùng next-themes thì lấy composite\n * `Toaster` ở \"@comitor/ui\" và tự truyền prop `theme`.\n */\nexport { SonnerToaster, type SonnerToasterProps } from \"../components/ui/sonner.js\";\nexport * from \"./app-header.js\";\nexport * from \"./app-launcher.js\";\nexport * from \"./app-shell.js\";\nexport * from \"./comitor-lockup.js\";\nexport * from \"./comitor-logo.js\";\n/**\n * Hằng của trục tương phản xuất THẲNG từ module thuần — root layout là Server Component và nó\n * đọc CONTRAST_STORAGE_KEY. Đi vòng qua contrast-provider.tsx (\"use client\") là hằng thành proxy\n * và ra `undefined` mà không có lỗi nào.\n */\nexport * from \"./contrast-constants.js\";\nexport * from \"./contrast-provider.js\";\nexport * from \"./contrast-toggle.js\";\nexport * from \"./mobile-menu.js\";\nexport * from \"./notifications-button.js\";\nexport * from \"./shell-context.js\";\nexport * from \"./shell-link.js\";\nexport * from \"./sidebar.js\";\nexport * from \"./sidebar-group.js\";\nexport * from \"./sidebar-menu-item.js\";\nexport * from \"./theme-provider.js\";\nexport * from \"./theme-toggle.js\";\nexport * from \"./types.js\";\nexport * from \"./user-menu.js\";\nexport * from \"./utils.js\";\nexport * from \"./workspace-switcher.js\";\n"],"mappings":"AAkBA,SAAS,qBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAMd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/shell\" — Tier 3: khung ứng dụng (AppShell, Sidebar, AppHeader,\n * WorkspaceSwitcher, AppLauncher, UserMenu, ThemeProvider…).\n *\n * Tách khỏi entry chính vì tầng này kéo theo `next/navigation` và `next-themes` — hai\n * peer OPTIONAL. App không chạy trên Next thì đừng import entry này (hoặc tự cấp\n * `ShellLinkComponent` riêng qua `ShellProvider`).\n *\n * Toàn bộ file ở đây là client component: có state, có routing, có phím tắt.\n */\n\n/**\n * Toaster bám theo next-themes — về tier thì nó là primitive (`components/ui/sonner.tsx`),\n * nhưng đặt cửa ra ở entry shell vì nó gọi `useTheme()`, mà `next-themes` chỉ là peer\n * OPTIONAL: để ở entry chính là ép mọi app phải cài next-themes.\n * Dùng chung với `ThemeProvider` ở trên. Nếu app KHÔNG dùng next-themes thì lấy composite\n * `Toaster` ở \"@comitor/ui\" và tự truyền prop `theme`.\n */\nexport { SonnerToaster, type SonnerToasterProps } from \"../components/ui/sonner.js\";\nexport * from \"./app-header.js\";\nexport * from \"./app-launcher.js\";\nexport * from \"./app-shell.js\";\nexport * from \"./comitor-lockup.js\";\nexport * from \"./comitor-logo.js\";\n/**\n * Hằng của trục tương phản xuất THẲNG từ module thuần — root layout là Server Component và nó\n * đọc CONTRAST_STORAGE_KEY. Đi vòng qua contrast-provider.tsx (\"use client\") là hằng thành proxy\n * và ra `undefined` mà không có lỗi nào.\n */\nexport * from \"./contrast-constants.js\";\nexport * from \"./contrast-provider.js\";\nexport * from \"./contrast-toggle.js\";\n/* Cùng lý do như trên — hằng của trục MẬT ĐỘ và CỠ CHỮ xuất thẳng từ module thuần, TRƯỚC provider. */\nexport * from \"./display-axis-constants.js\";\nexport * from \"./display-axis-controls.js\";\nexport * from \"./display-axis-provider.js\";\nexport * from \"./mobile-menu.js\";\nexport * from \"./notifications-button.js\";\nexport * from \"./shell-context.js\";\nexport * from \"./shell-link.js\";\nexport * from \"./sidebar.js\";\nexport * from \"./sidebar-group.js\";\nexport * from \"./sidebar-menu-item.js\";\nexport * from \"./theme-provider.js\";\nexport * from \"./theme-toggle.js\";\nexport * from \"./types.js\";\nexport * from \"./user-menu.js\";\nexport * from \"./utils.js\";\nexport * from \"./workspace-switcher.js\";\n"],"mappings":"AAkBA,SAAS,qBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAMd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -82,6 +82,14 @@ interface ShellProviderProps {
|
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
84
84
|
searchParams?: NavSearchParams | null;
|
|
85
|
+
/**
|
|
86
|
+
* Ở dải TABLET (768–1023px), khi người dùng CHƯA từng gạt thanh bên, mở app lần đầu thì thanh
|
|
87
|
+
* bên thu gọn sẵn. Mặc định `true`.
|
|
88
|
+
*
|
|
89
|
+
* Đặt `false` khi app muốn thanh bên luôn mở rộng ở tablet — vd điều hướng là thứ chính của
|
|
90
|
+
* màn hình đó. Lựa chọn ĐÃ LƯU của người dùng luôn thắng cờ này.
|
|
91
|
+
*/
|
|
92
|
+
collapseSidebarOnTablet?: boolean;
|
|
85
93
|
/** Component link thay `next/link` (app không-Next). */
|
|
86
94
|
LinkComponent?: ShellLinkComponent;
|
|
87
95
|
/** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */
|
|
@@ -107,6 +115,6 @@ interface ShellProviderProps {
|
|
|
107
115
|
* (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback
|
|
108
116
|
* `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.
|
|
109
117
|
*/
|
|
110
|
-
declare function ShellProvider({ workspaces, currentWorkspaceId, onWorkspaceChange, onCreateWorkspace, onWorkspaceSettings, onInviteMembers, apps, currentAppId, onAppSelect, onUpsell, nav, pathname: pathnameProp, searchParams: searchParamsProp, LinkComponent, onNavigate, user, onLogout, defaultSidebarCollapsed, persistSidebarState, enableShortcuts, labels: labelsProp, children }: ShellProviderProps): React.JSX.Element;
|
|
118
|
+
declare function ShellProvider({ workspaces, currentWorkspaceId, onWorkspaceChange, onCreateWorkspace, onWorkspaceSettings, onInviteMembers, apps, currentAppId, onAppSelect, onUpsell, nav, pathname: pathnameProp, searchParams: searchParamsProp, LinkComponent, onNavigate, user, onLogout, defaultSidebarCollapsed, collapseSidebarOnTablet, persistSidebarState, enableShortcuts, labels: labelsProp, children }: ShellProviderProps): React.JSX.Element;
|
|
111
119
|
|
|
112
120
|
export { type ShellContextValue, ShellProvider, type ShellProviderProps, useShell };
|