@comitor/ui 0.2.0 → 0.2.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.
@@ -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":";AAoHM,cAOF,YAPE;AAlHN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AAiDnB,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":[]}
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
- type CardHeaderProps = React.ComponentProps<"div">;
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
- declare function CardHeader({ className, ...props }: CardHeaderProps): React.JSX.Element;
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
- return /* @__PURE__ */ jsx("div", { "data-slot": "card-header", className: cn("flex flex-col space-y-1.5", className), ...props });
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(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Cấu trúc + style port nguyên trạng từ comitor-ds. Tên export giữ tương thích shadcn\n * (Card / CardHeader / CardTitle / CardDescription / CardContent / CardFooter), thêm\n * CardAction (bổ sung, không có trong shadcn gốc).\n */\nconst cardVariants = cva(\"rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-card border border-border shadow-surface\",\n elevated: \"bg-card border border-border shadow-card\",\n outline: \"bg-transparent border border-border\",\n ghost: \"bg-transparent border-none\"\n },\n padding: {\n none: \"\",\n sm: \"p-4\",\n md: \"p-6\",\n lg: \"p-8\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n padding: \"md\"\n }\n});\n\nexport interface CardProps extends React.ComponentProps<\"div\">, VariantProps<typeof cardVariants> {}\nexport type CardHeaderProps = React.ComponentProps<\"div\">;\nexport type CardTitleProps = React.ComponentProps<\"h3\">;\nexport type CardDescriptionProps = React.ComponentProps<\"p\">;\nexport type CardActionProps = React.ComponentProps<\"div\">;\nexport type CardContentProps = React.ComponentProps<\"div\">;\nexport type CardFooterProps = React.ComponentProps<\"div\">;\n\nfunction CardBase({ className, variant, padding, ...props }: CardProps) {\n return <div data-slot=\"card\" className={cn(cardVariants({ variant, padding }), className)} {...props} />;\n}\n\nfunction CardHeader({ className, ...props }: CardHeaderProps) {\n return <div data-slot=\"card-header\" className={cn(\"flex flex-col space-y-1.5\", className)} {...props} />;\n}\n\nfunction CardTitle({ className, ...props }: CardTitleProps) {\n return (\n <h3\n data-slot=\"card-title\"\n className={cn(\"text-lg font-semibold text-foreground leading-none tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: CardDescriptionProps) {\n return <p data-slot=\"card-description\" className={cn(\"text-sm text-muted-foreground\", className)} {...props} />;\n}\n\nfunction CardAction({ className, ...props }: CardActionProps) {\n return <div data-slot=\"card-action\" className={cn(\"ml-auto\", className)} {...props} />;\n}\n\nfunction CardContent({ className, ...props }: CardContentProps) {\n return <div data-slot=\"card-content\" className={cn(\"pt-4\", className)} {...props} />;\n}\n\nfunction CardFooter({ className, ...props }: CardFooterProps) {\n return <div data-slot=\"card-footer\" className={cn(\"flex items-center pt-4\", className)} {...props} />;\n}\n\n/*\n * `Card` mở cả hai lối dùng: API tên rời (Card + CardHeader/CardTitle/…) và API gộp\n * (Card.Header / Card.Title / Card.Description / Card.Content / Card.Footer / Card.Action).\n */\nconst Card = Object.assign(CardBase, {\n Header: CardHeader,\n Title: CardTitle,\n Description: CardDescription,\n Content: CardContent,\n Footer: CardFooter,\n Action: CardAction\n});\n\nexport { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };\n"],"mappings":"AAuCS;AAvCT,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAOnB,MAAM,eAAe,IAAI,cAAc;AAAA,EACrC,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF,CAAC;AAUD,SAAS,SAAS,EAAE,WAAW,SAAS,SAAS,GAAG,MAAM,GAAc;AACtE,SAAO,oBAAC,SAAI,aAAU,QAAO,WAAW,GAAG,aAAa,EAAE,SAAS,QAAQ,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACxG;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,6BAA6B,SAAS,GAAI,GAAG,OAAO;AACxG;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AAC1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,qEAAqE,SAAS;AAAA,MAC3F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SAAO,oBAAC,OAAE,aAAU,oBAAmB,WAAW,GAAG,iCAAiC,SAAS,GAAI,GAAG,OAAO;AAC/G;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,WAAW,SAAS,GAAI,GAAG,OAAO;AACtF;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,QAAQ,SAAS,GAAI,GAAG,OAAO;AACpF;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AACrG;AAMA,MAAM,OAAO,OAAO,OAAO,UAAU;AAAA,EACnC,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,aAAa;AAAA,EACb,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AACV,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Cấu trúc + style port nguyên trạng từ comitor-ds. Tên export giữ tương thích shadcn\n * (Card / CardHeader / CardTitle / CardDescription / CardContent / CardFooter), thêm\n * CardAction (bổ sung, không có trong shadcn gốc).\n */\nconst cardVariants = cva(\"rounded-xl\", {\n variants: {\n variant: {\n default: \"bg-card border border-border shadow-surface\",\n elevated: \"bg-card border border-border shadow-card\",\n outline: \"bg-transparent border border-border\",\n ghost: \"bg-transparent border-none\"\n },\n padding: {\n none: \"\",\n sm: \"p-4\",\n md: \"p-6\",\n lg: \"p-8\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n padding: \"md\"\n }\n});\n\nexport interface CardProps extends React.ComponentProps<\"div\">, VariantProps<typeof cardVariants> {}\nexport interface CardHeaderProps extends React.ComponentProps<\"div\"> {\n /**\n * Nút/menu đặt ở GÓC PHẢI của header, ngang hàng với tiêu đề.\n *\n * Có nó thì header tự chuyển sang hàng ngang; bỏ trống thì bố cục dọc mặc định giữ\n * nguyên (xem chú thích ở `CardHeader`).\n */\n actions?: React.ReactNode;\n /** className riêng cho cụm `actions` (vd đổi `gap`, cho phép xuống dòng). */\n actionsClassName?: string;\n}\nexport type CardTitleProps = React.ComponentProps<\"h3\">;\nexport type CardDescriptionProps = React.ComponentProps<\"p\">;\nexport type CardActionProps = React.ComponentProps<\"div\">;\nexport type CardContentProps = React.ComponentProps<\"div\">;\nexport type CardFooterProps = React.ComponentProps<\"div\">;\n\nfunction CardBase({ className, variant, padding, ...props }: CardProps) {\n return <div data-slot=\"card\" className={cn(cardVariants({ variant, padding }), className)} {...props} />;\n}\n\n/**\n * Đầu thẻ. Mặc định XẾP DỌC (`flex-col space-y-1.5`) — tiêu đề trên, mô tả dưới.\n *\n * ─ VÌ SAO CÓ `actions` ─\n * 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\n * chỗ dùng đều phải ghi đè đúng một chuỗi bốn lớp `\"flex-row items-start justify-between\n * 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 đè\n * giống nhau. `actions` gói đúng chuỗi đó lại thành một prop.\n *\n * 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\n * `<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.\n *\n * Quan hệ với `CardAction`: `CardAction` là ô `ml-auto` để tự lắp khi bạn ĐÃ tự đặt header\n * thành hàng ngang. `actions` làm cả hai việc — đổi bố cục *và* đặt chỗ — nên là đường đi\n * mặc định; `CardAction` giữ nguyên cho ai đã dùng.\n *\n * ```tsx\n * <CardHeader actions={<Button size=\"sm\" variant=\"ghost\">Sửa</Button>}>\n * <CardTitle>Thông tin đơn vị</CardTitle>\n * <CardDescription>Cập nhật lần cuối hôm qua</CardDescription>\n * </CardHeader>\n * ```\n */\nfunction CardHeader({ className, actions, actionsClassName, children, ...props }: CardHeaderProps) {\n if (actions === undefined || actions === null || actions === false) {\n return (\n <div data-slot=\"card-header\" className={cn(\"flex flex-col space-y-1.5\", className)} {...props}>\n {children}\n </div>\n );\n }\n\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\"flex flex-row items-start justify-between gap-3 space-y-0\", className)}\n {...props}\n >\n {/* `min-w-0` để tiêu đề dài `truncate` được thay vì đẩy cụm nút ra khỏi thẻ. */}\n <div data-slot=\"card-header-text\" className=\"flex min-w-0 flex-col space-y-1.5\">\n {children}\n </div>\n <div data-slot=\"card-header-actions\" className={cn(\"flex shrink-0 items-center gap-2\", actionsClassName)}>\n {actions}\n </div>\n </div>\n );\n}\n\nfunction CardTitle({ className, ...props }: CardTitleProps) {\n return (\n <h3\n data-slot=\"card-title\"\n className={cn(\"text-lg font-semibold text-foreground leading-none tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: CardDescriptionProps) {\n return <p data-slot=\"card-description\" className={cn(\"text-sm text-muted-foreground\", className)} {...props} />;\n}\n\nfunction CardAction({ className, ...props }: CardActionProps) {\n return <div data-slot=\"card-action\" className={cn(\"ml-auto\", className)} {...props} />;\n}\n\nfunction CardContent({ className, ...props }: CardContentProps) {\n return <div data-slot=\"card-content\" className={cn(\"pt-4\", className)} {...props} />;\n}\n\nfunction CardFooter({ className, ...props }: CardFooterProps) {\n return <div data-slot=\"card-footer\" className={cn(\"flex items-center pt-4\", className)} {...props} />;\n}\n\n/*\n * `Card` mở cả hai lối dùng: API tên rời (Card + CardHeader/CardTitle/…) và API gộp\n * (Card.Header / Card.Title / Card.Description / Card.Content / Card.Footer / Card.Action).\n */\nconst Card = Object.assign(CardBase, {\n Header: CardHeader,\n Title: CardTitle,\n Description: CardDescription,\n Content: CardContent,\n Footer: CardFooter,\n Action: CardAction\n});\n\nexport { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };\n"],"mappings":"AAiDS,cAoCL,YApCK;AAjDT,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAOnB,MAAM,eAAe,IAAI,cAAc;AAAA,EACrC,UAAU;AAAA,IACR,SAAS;AAAA,MACP,SAAS;AAAA,MACT,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF,CAAC;AAoBD,SAAS,SAAS,EAAE,WAAW,SAAS,SAAS,GAAG,MAAM,GAAc;AACtE,SAAO,oBAAC,SAAI,aAAU,QAAO,WAAW,GAAG,aAAa,EAAE,SAAS,QAAQ,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACxG;AAyBA,SAAS,WAAW,EAAE,WAAW,SAAS,kBAAkB,UAAU,GAAG,MAAM,GAAoB;AACjG,MAAI,YAAY,UAAa,YAAY,QAAQ,YAAY,OAAO;AAClE,WACE,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,6BAA6B,SAAS,GAAI,GAAG,OACrF,UACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,6DAA6D,SAAS;AAAA,MACnF,GAAG;AAAA,MAGJ;AAAA,4BAAC,SAAI,aAAU,oBAAmB,WAAU,qCACzC,UACH;AAAA,QACA,oBAAC,SAAI,aAAU,uBAAsB,WAAW,GAAG,oCAAoC,gBAAgB,GACpG,mBACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AAC1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,qEAAqE,SAAS;AAAA,MAC3F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SAAO,oBAAC,OAAE,aAAU,oBAAmB,WAAW,GAAG,iCAAiC,SAAS,GAAI,GAAG,OAAO;AAC/G;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,WAAW,SAAS,GAAI,GAAG,OAAO;AACtF;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,SAAI,aAAU,gBAAe,WAAW,GAAG,QAAQ,SAAS,GAAI,GAAG,OAAO;AACpF;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,0BAA0B,SAAS,GAAI,GAAG,OAAO;AACrG;AAMA,MAAM,OAAO,OAAO,OAAO,UAAU;AAAA,EACnC,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,aAAa;AAAA,EACb,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AACV,CAAC;","names":[]}
@@ -7,6 +7,8 @@ import { SidebarProps } from './sidebar.js';
7
7
  import './user-menu.js';
8
8
  import './types.js';
9
9
  import 'lucide-react';
10
+ import './utils.js';
11
+ import '../components/composite/text-utils.js';
10
12
 
11
13
  type AppShellLayout = "sidebar-first" | "header-first";
12
14
  interface AppShellProps extends ShellProviderProps {
@@ -15,7 +15,7 @@ export { ThemeProvider, ThemeProviderProps } from './theme-provider.js';
15
15
  export { ThemeToggle, ThemeToggleLabels, ThemeToggleProps } from './theme-toggle.js';
16
16
  export { AppDescriptor, DEFAULT_SHELL_LABELS, NavBadgeVariant, NavGroup, NavItem, ShellIcon, ShellLabels, ShellLinkComponent, ShellLinkProps, ShellNav, ShellUser, UserMenuItem, Workspace } from './types.js';
17
17
  export { UserAvatar, UserAvatarProps, UserMenu, UserMenuProps } from './user-menu.js';
18
- export { appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups } from './utils.js';
18
+ export { NavSearchParams, appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups, toSearchParams } from './utils.js';
19
19
  export { WorkspaceSwitcher, WorkspaceSwitcherProps } from './workspace-switcher.js';
20
20
  export { getVietnameseInitials, normalizeVi } from '../components/composite/text-utils.js';
21
21
  import 'react';
@@ -1,7 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { Workspace, AppDescriptor, ShellNav, ShellLinkComponent, ShellUser, ShellLabels, NavGroup } from './types.js';
4
+ import { NavSearchParams } from './utils.js';
4
5
  import 'lucide-react';
6
+ import '../components/composite/text-utils.js';
5
7
 
6
8
  interface ShellContextValue {
7
9
  workspaces: Workspace[];
@@ -16,6 +18,12 @@ interface ShellContextValue {
16
18
  onUpsell?: (appId: string) => void;
17
19
  nav: NavGroup[];
18
20
  pathname: string;
21
+ /**
22
+ * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.
23
+ * `null` KHÁC rỗng: rỗng nghĩa là "URL không có query", `null` nghĩa là "shell không biết"
24
+ * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).
25
+ */
26
+ searchParams: URLSearchParams | null;
19
27
  LinkComponent: ShellLinkComponent;
20
28
  onNavigate?: (href: string) => void;
21
29
  user: ShellUser | null;
@@ -55,6 +63,25 @@ interface ShellProviderProps {
55
63
  * của next/navigation (trả `null` khi không có App Router → coi như "").
56
64
  */
57
65
  pathname?: string;
66
+ /**
67
+ * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query
68
+ * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn
69
+ * cùng sáng.
70
+ *
71
+ * Nhận `useSearchParams()` của Next, chuỗi `"?tab=x"`, hoặc object `searchParams` của
72
+ * Server Component.
73
+ *
74
+ * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang
75
+ * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do
76
+ * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:
77
+ *
78
+ * ```tsx
79
+ * "use client";
80
+ * const searchParams = useSearchParams();
81
+ * return <AppShell searchParams={searchParams} …>{children}</AppShell>;
82
+ * ```
83
+ */
84
+ searchParams?: NavSearchParams | null;
58
85
  /** Component link thay `next/link` (app không-Next). */
59
86
  LinkComponent?: ShellLinkComponent;
60
87
  /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */
@@ -80,6 +107,6 @@ interface ShellProviderProps {
80
107
  * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback
81
108
  * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.
82
109
  */
83
- declare function ShellProvider({ workspaces, currentWorkspaceId, onWorkspaceChange, onCreateWorkspace, onWorkspaceSettings, onInviteMembers, apps, currentAppId, onAppSelect, onUpsell, nav, pathname: pathnameProp, LinkComponent, onNavigate, user, onLogout, defaultSidebarCollapsed, persistSidebarState, enableShortcuts, labels: labelsProp, children }: ShellProviderProps): React.JSX.Element;
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;
84
111
 
85
112
  export { type ShellContextValue, ShellProvider, type ShellProviderProps, useShell };
@@ -7,7 +7,7 @@ import { TooltipProvider } from "../components/ui/tooltip.js";
7
7
  import {
8
8
  DEFAULT_SHELL_LABELS
9
9
  } from "./types.js";
10
- import { getEntitledApps, resolveCurrentApp, toNavGroups } from "./utils.js";
10
+ import { getEntitledApps, resolveCurrentApp, toNavGroups, toSearchParams } from "./utils.js";
11
11
  function DefaultShellLink({ href, children, ...props }) {
12
12
  return /* @__PURE__ */ jsx(NextLink, { href, ...props, children });
13
13
  }
@@ -46,6 +46,7 @@ function ShellProvider({
46
46
  onUpsell,
47
47
  nav,
48
48
  pathname: pathnameProp,
49
+ searchParams: searchParamsProp,
49
50
  LinkComponent = DefaultShellLink,
50
51
  onNavigate,
51
52
  user = null,
@@ -58,6 +59,7 @@ function ShellProvider({
58
59
  }) {
59
60
  const nextPathname = usePathname();
60
61
  const pathname = pathnameProp ?? nextPathname ?? "";
62
+ const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);
61
63
  const currentWorkspace = useMemo(
62
64
  () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,
63
65
  [workspaces, currentWorkspaceId]
@@ -165,6 +167,7 @@ function ShellProvider({
165
167
  onUpsell,
166
168
  nav: navGroups,
167
169
  pathname,
170
+ searchParams,
168
171
  LinkComponent,
169
172
  onNavigate,
170
173
  user,
@@ -193,6 +196,7 @@ function ShellProvider({
193
196
  onUpsell,
194
197
  navGroups,
195
198
  pathname,
199
+ searchParams,
196
200
  LinkComponent,
197
201
  onNavigate,
198
202
  user,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\nimport NextLink from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport {\n type AppDescriptor,\n DEFAULT_SHELL_LABELS,\n type NavGroup,\n type ShellLabels,\n type ShellLinkComponent,\n type ShellLinkProps,\n type ShellNav,\n type ShellUser,\n type Workspace\n} from \"./types.js\";\nimport { getEntitledApps, resolveCurrentApp, toNavGroups } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (đã trộn mặc định tiếng Việt với override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /** Override chuỗi hiển thị (mặc định tiếng Việt). */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useMemo<ShellLabels>(() => ({ ...DEFAULT_SHELL_LABELS, ...labelsProp }), [labelsProp]);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n // Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n useEffect(() => {\n if (!persistSidebarState) return;\n const stored = readStoredCollapsed(currentWorkspace?.id);\n if (stored !== null) setSidebarCollapsed(stored);\n }, [persistSidebarState, currentWorkspace?.id]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/shell-context.tsx"],"sourcesContent":["\"use client\";\n\nimport NextLink from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { TooltipProvider } from \"../components/ui/tooltip.js\";\nimport {\n type AppDescriptor,\n DEFAULT_SHELL_LABELS,\n type NavGroup,\n type ShellLabels,\n type ShellLinkComponent,\n type ShellLinkProps,\n type ShellNav,\n type ShellUser,\n type Workspace\n} from \"./types.js\";\nimport { getEntitledApps, type NavSearchParams, resolveCurrentApp, toNavGroups, toSearchParams } from \"./utils.js\";\n\n/* ────────────────────────────────────────────────────────────────────────────\n Link mặc định\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Bọc `next/link` thay vì truyền thẳng: hình dạng props của NextLink rộng hơn\n * `ShellLinkProps` (href nhận cả UrlObject), bọc lại cho khớp type và để chỗ\n * thay adapter router khác về sau.\n */\nfunction DefaultShellLink({ href, children, ...props }: ShellLinkProps) {\n return (\n <NextLink href={href} {...props}>\n {children}\n </NextLink>\n );\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Context\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellContextValue {\n /* Trục 1 — workspace */\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /* Trục 2 — app */\n apps: AppDescriptor[];\n currentApp: AppDescriptor | null;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /* Điều hướng trong app hiện tại */\n nav: NavGroup[];\n pathname: string;\n /**\n * Query hiện tại đã chuẩn hoá, hoặc `null` khi app không truyền.\n * `null` KHÁC rỗng: rỗng nghĩa là \"URL không có query\", `null` nghĩa là \"shell không biết\"\n * — và active matching xử lý hai ca đó khác nhau (xem `isNavItemActive`).\n */\n searchParams: URLSearchParams | null;\n LinkComponent: ShellLinkComponent;\n onNavigate?: (href: string) => void;\n\n /* Người dùng */\n user: ShellUser | null;\n onLogout?: () => void;\n\n /* Trạng thái khung */\n sidebarCollapsed: boolean;\n setSidebarCollapsed: (collapsed: boolean) => void;\n toggleSidebar: () => void;\n mobileMenuOpen: boolean;\n setMobileMenuOpen: (open: boolean) => void;\n commandPaletteOpen: boolean;\n setCommandPaletteOpen: (open: boolean) => void;\n appLauncherOpen: boolean;\n setAppLauncherOpen: (open: boolean) => void;\n\n /* Chuỗi hiển thị (đã trộn mặc định tiếng Việt với override của app) */\n labels: ShellLabels;\n}\n\nconst ShellContext = createContext<ShellContextValue | undefined>(undefined);\n\n/** Truy cập state của shell — phải gọi bên trong `<ShellProvider>`. */\nexport function useShell(): ShellContextValue {\n const context = useContext(ShellContext);\n if (!context) {\n throw new Error(\"useShell phải được dùng bên trong <ShellProvider> (hoặc <AppShell>).\");\n }\n return context;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Ghi nhớ trạng thái thu gọn sidebar\n ──────────────────────────────────────────────────────────────────────────── */\n\n/**\n * Khoá localStorage theo workspace — mỗi workspace nhớ riêng trạng thái sidebar\n * (spec §7.6: KHÔNG dùng cookie `sidebar_state` của shadcn để tránh hai nguồn sự thật).\n */\nfunction sidebarStorageKey(workspaceId: string | undefined): string {\n return `comitor-sidebar:${workspaceId ?? \"default\"}`;\n}\n\nfunction readStoredCollapsed(workspaceId: string | undefined): boolean | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(sidebarStorageKey(workspaceId));\n if (raw === \"1\") return true;\n if (raw === \"0\") return false;\n return null;\n } catch {\n // Safari private mode / storage bị chặn — coi như chưa có giá trị lưu.\n return null;\n }\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Provider\n ──────────────────────────────────────────────────────────────────────────── */\n\nexport interface ShellProviderProps {\n /** Trục 1 — danh sách workspace của user. Rỗng/1 phần tử thì WorkspaceSwitcher tự ẩn danh sách. */\n workspaces?: Workspace[];\n /** Workspace đang mở; bỏ trống → lấy phần tử đầu. */\n currentWorkspaceId?: string;\n onWorkspaceChange?: (workspaceId: string) => void;\n onCreateWorkspace?: () => void;\n onWorkspaceSettings?: (workspaceId: string) => void;\n onInviteMembers?: (workspaceId: string) => void;\n\n /** Trục 2 — danh sách app của hệ sinh thái (kể cả app chưa mở khoá). */\n apps?: AppDescriptor[];\n /** App đang mở; bỏ trống → suy từ `pathname` (khớp prefix dài nhất). */\n currentAppId?: string;\n onAppSelect?: (app: AppDescriptor) => void;\n onUpsell?: (appId: string) => void;\n\n /** Menu của app hiện tại — `NavItem[]` hoặc `NavGroup[]`. */\n nav?: ShellNav;\n /**\n * Ghi đè pathname (Storybook, app không-Next). Bỏ trống → dùng `usePathname()`\n * của next/navigation (trả `null` khi không có App Router → coi như \"\").\n */\n pathname?: string;\n /**\n * Query hiện tại — CHỈ cần khi menu có mục phân biệt nhau bằng query\n * (`/cai-dat?tab=chung` ⇄ `/cai-dat?tab=thanh-vien`). Bỏ trống thì mọi mục cùng đường dẫn\n * cùng sáng.\n *\n * Nhận `useSearchParams()` của Next, chuỗi `\"?tab=x\"`, hoặc object `searchParams` của\n * Server Component.\n *\n * ⚠ Shell KHÔNG tự gọi `useSearchParams()`: hook đó ép trang bọc `<Suspense>` và đẩy trang\n * sang render phía client khi build tĩnh — cái giá đó phải do app tự chọn, không phải do\n * việc lỡ import shell. Truyền vào từ một client component đã có sẵn boundary:\n *\n * ```tsx\n * \"use client\";\n * const searchParams = useSearchParams();\n * return <AppShell searchParams={searchParams} …>{children}</AppShell>;\n * ```\n */\n searchParams?: NavSearchParams | null;\n /** Component link thay `next/link` (app không-Next). */\n LinkComponent?: ShellLinkComponent;\n /** Gọi sau mỗi lần người dùng bấm một mục điều hướng (analytics, đóng panel…). */\n onNavigate?: (href: string) => void;\n\n user?: ShellUser | null;\n onLogout?: () => void;\n\n /** Trạng thái thu gọn ban đầu (SSR). Sau khi mount, localStorage của workspace thắng. */\n defaultSidebarCollapsed?: boolean;\n /** Tắt ghi nhớ localStorage (Storybook, chế độ kiosk). */\n persistSidebarState?: boolean;\n /** Tắt bộ phím tắt toàn cục. */\n enableShortcuts?: boolean;\n\n /** Override chuỗi hiển thị (mặc định tiếng Việt). */\n labels?: Partial<ShellLabels>;\n\n children: ReactNode;\n}\n\n/**\n * Provider của app shell Comitor: giữ hai trục điều hướng (workspace × app),\n * trạng thái khung (sidebar/mobile/command palette/app launcher), phím tắt toàn cục,\n * và bọc sẵn `TooltipProvider`.\n *\n * KHÔNG dùng `useRouter()` của next/navigation: hook đó *ném lỗi* khi thiếu App Router\n * (Storybook, test). Mọi điều hướng chủ động là việc của app qua callback\n * `onWorkspaceChange` / `onAppSelect` / `onNavigate`; link tĩnh đi qua `LinkComponent`.\n */\nexport function ShellProvider({\n workspaces = [],\n currentWorkspaceId,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps = [],\n currentAppId,\n onAppSelect,\n onUpsell,\n nav,\n pathname: pathnameProp,\n searchParams: searchParamsProp,\n LinkComponent = DefaultShellLink,\n onNavigate,\n user = null,\n onLogout,\n defaultSidebarCollapsed = false,\n persistSidebarState = true,\n enableShortcuts = true,\n labels: labelsProp,\n children\n}: ShellProviderProps) {\n // `usePathname()` trả null khi không có App Router → fallback chuỗi rỗng thay vì crash.\n const nextPathname = usePathname();\n const pathname = pathnameProp ?? nextPathname ?? \"\";\n\n // Chuẩn hoá MỘT lần ở đây thay vì mỗi lần `isNavItemActive` chạy: prop có thể là chuỗi\n // hoặc object, mà mỗi lần dựng `URLSearchParams` mới là một identity mới → mọi memo bên\n // dưới hỏng. Khoá memo là chính prop app truyền vào.\n const searchParams = useMemo(() => toSearchParams(searchParamsProp), [searchParamsProp]);\n\n const currentWorkspace = useMemo(\n () => workspaces.find((workspace) => workspace.id === currentWorkspaceId) ?? workspaces[0] ?? null,\n [workspaces, currentWorkspaceId]\n );\n\n const currentApp = useMemo(() => {\n if (currentAppId) return apps.find((app) => app.id === currentAppId) ?? null;\n return resolveCurrentApp(apps, pathname);\n }, [apps, currentAppId, pathname]);\n\n const navGroups = useMemo(() => toNavGroups(nav), [nav]);\n const labels = useMemo<ShellLabels>(() => ({ ...DEFAULT_SHELL_LABELS, ...labelsProp }), [labelsProp]);\n\n const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);\n const [appLauncherOpen, setAppLauncherOpen] = useState(false);\n\n // Nạp trạng thái đã lưu SAU khi mount (đọc localStorage lúc render đầu sẽ lệch hydration).\n useEffect(() => {\n if (!persistSidebarState) return;\n const stored = readStoredCollapsed(currentWorkspace?.id);\n if (stored !== null) setSidebarCollapsed(stored);\n }, [persistSidebarState, currentWorkspace?.id]);\n\n const persistCollapsed = useCallback(\n (collapsed: boolean) => {\n if (!persistSidebarState || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(sidebarStorageKey(currentWorkspace?.id), collapsed ? \"1\" : \"0\");\n } catch {\n // Bỏ qua: không ghi được localStorage thì trạng thái chỉ sống trong phiên.\n }\n },\n [persistSidebarState, currentWorkspace?.id]\n );\n\n const applySidebarCollapsed = useCallback(\n (collapsed: boolean) => {\n setSidebarCollapsed(collapsed);\n persistCollapsed(collapsed);\n },\n [persistCollapsed]\n );\n\n const toggleSidebar = useCallback(() => {\n setSidebarCollapsed((prev) => {\n const next = !prev;\n persistCollapsed(next);\n return next;\n });\n }, [persistCollapsed]);\n\n // Phím tắt toàn cục (§7.4)\n useEffect(() => {\n if (!enableShortcuts) return;\n\n const entitledApps = getEntitledApps(apps);\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const target = event.target;\n // Đang gõ trong ô nhập → nhường phím cho ô nhập.\n const isTyping =\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable);\n\n if (event.key === \"Escape\") {\n setCommandPaletteOpen(false);\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n return;\n }\n\n if (isTyping) return;\n\n const mod = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n // Cmd/Ctrl + Shift + K — AppLauncher (phải xét TRƯỚC Cmd+K vì cùng phím \"k\")\n if (mod && event.shiftKey && key === \"k\") {\n event.preventDefault();\n setAppLauncherOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + K — Command Palette\n if (mod && !event.shiftKey && key === \"k\") {\n event.preventDefault();\n setCommandPaletteOpen((prev) => !prev);\n return;\n }\n\n // Cmd/Ctrl + B — thu gọn/mở sidebar\n if (mod && !event.shiftKey && key === \"b\") {\n event.preventDefault();\n toggleSidebar();\n return;\n }\n\n // Alt + 1…9 — nhảy tới app thứ n đã được cấp quyền.\n // Dùng `event.code` chứ không `event.key`: trên macOS Alt+1 cho ra \"¡\".\n if (event.altKey && !event.ctrlKey && !event.metaKey) {\n const match = /^Digit([1-9])$/.exec(event.code);\n if (match?.[1]) {\n const index = Number.parseInt(match[1], 10) - 1;\n const app = entitledApps[index];\n if (app) {\n event.preventDefault();\n onAppSelect?.(app);\n }\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [enableShortcuts, apps, onAppSelect, toggleSidebar]);\n\n // Đổi route → đóng mọi overlay đang mở.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chủ đích chạy theo MỌI lần đổi pathname\n useEffect(() => {\n setMobileMenuOpen(false);\n setAppLauncherOpen(false);\n setCommandPaletteOpen(false);\n }, [pathname]);\n\n const value = useMemo<ShellContextValue>(\n () => ({\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n nav: navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n setSidebarCollapsed: applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n setMobileMenuOpen,\n commandPaletteOpen,\n setCommandPaletteOpen,\n appLauncherOpen,\n setAppLauncherOpen,\n labels\n }),\n [\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n apps,\n currentApp,\n onAppSelect,\n onUpsell,\n navGroups,\n pathname,\n searchParams,\n LinkComponent,\n onNavigate,\n user,\n onLogout,\n sidebarCollapsed,\n applySidebarCollapsed,\n toggleSidebar,\n mobileMenuOpen,\n commandPaletteOpen,\n appLauncherOpen,\n labels\n ]\n );\n\n return (\n <ShellContext.Provider value={value}>\n <TooltipProvider delayDuration={200}>{children}</TooltipProvider>\n </ShellContext.Provider>\n );\n}\n"],"mappings":"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","names":[]}
@@ -24,9 +24,9 @@ function SidebarMenuItem({
24
24
  onNavigate,
25
25
  className
26
26
  }) {
27
- const { pathname } = useShell();
28
- const active = isNavItemActive(item, pathname, siblings);
29
- const branchActive = isNavBranchActive(item, pathname, siblings);
27
+ const { pathname, searchParams } = useShell();
28
+ const active = isNavItemActive(item, pathname, siblings, searchParams);
29
+ const branchActive = isNavBranchActive(item, pathname, siblings, searchParams);
30
30
  const hasChildren = Boolean(item.children?.length);
31
31
  const [open, setOpen] = useState(branchActive);
32
32
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/sidebar-menu-item.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { NavBadgeVariant, NavItem } from \"./types.js\";\nimport { isNavBranchActive, isNavItemActive } from \"./utils.js\";\n\n/** Variant badge của NavItem → variant của primitive Badge (tên trùng nhau, map tường minh cho an toàn). */\nconst BADGE_VARIANT: Record<NavBadgeVariant, \"default\" | \"destructive\" | \"warning\" | \"success\"> = {\n default: \"default\",\n destructive: \"destructive\",\n warning: \"warning\",\n success: \"success\"\n};\n\n/**\n * Vòng focus cho mục đã khoá.\n *\n * Mục khoá vẫn nằm trong tab order (`tabIndex={0}` + `aria-disabled`) theo đúng khuyến nghị\n * của ARIA APG: người dùng bàn phím phải *biết* mục đó tồn tại và đang bị khoá, thay vì mục\n * lặng lẽ biến mất. Không có href và không có handler nên Enter/click không đi đâu cả.\n */\nconst FOCUS_RING = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\";\n\nexport interface SidebarMenuItemProps {\n item: NavItem;\n /** Sidebar đang thu gọn (chỉ icon) — chỉ áp dụng cho mục cấp 0. */\n collapsed?: boolean;\n /** Cấp lồng (0 = gốc). Cấp > 0 thụt lề và không có icon vuông. */\n depth?: number;\n /**\n * Tập mục để đối chiếu khớp CHÍNH XÁC pathname — nên truyền toàn bộ mục của app\n * hiện tại. Thiếu nó, `/tasks` sẽ vẫn sáng khi đang ở `/tasks/inbox`.\n */\n siblings?: NavItem[];\n /** Gọi khi người dùng bấm vào một LINK thật (không gọi khi chỉ mở/đóng nhánh con) —\n * `MobileMenu` dùng để tự đóng. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Một mục menu sidebar: active matching thông minh, badge số, mục con `Collapsible`,\n * khi sidebar thu gọn thì hiện `Tooltip` + chấm badge.\n */\nexport function SidebarMenuItem({\n item,\n collapsed = false,\n depth = 0,\n siblings = [],\n onNavigate,\n className\n}: SidebarMenuItemProps) {\n const { pathname } = useShell();\n\n const active = isNavItemActive(item, pathname, siblings);\n const branchActive = isNavBranchActive(item, pathname, siblings);\n const hasChildren = Boolean(item.children?.length);\n\n const [open, setOpen] = useState(branchActive);\n\n // Điều hướng tới một mục con → tự mở nhánh chứa nó.\n useEffect(() => {\n if (branchActive) setOpen(true);\n }, [branchActive]);\n\n const Icon = item.icon;\n const disabled = Boolean(item.disabled);\n const rowActive = active || (branchActive && !hasChildren);\n\n const rowClass = cn(\n \"group relative flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n depth > 0 && \"pl-8 text-[13px]\",\n rowActive ? \"bg-sidebar-accent font-medium text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n // Mục đã khoá thì KHÔNG gắn hover — đổi màu khi rê chuột là hứa hẹn bấm được.\n !disabled && !rowActive && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n // Mục khoá tô bằng MỘT bậc chữ mờ hơn, KHÔNG phải `opacity-50` chồng lên màu sẵn có.\n // Vì sao: mục khoá vẫn nằm trong tab order (`role=\"link\" aria-disabled tabIndex={0}` — xem\n // chú thích FOCUS_RING) nên KHÔNG được hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3,\n // mà `opacity-50` chồng lên `text-sidebar-foreground/70` cho độ mờ thực 35% → 2,28:1 (light)\n // / 3,10:1 (dark) ✗ cho chữ, và 27,5% trên icon → 1,86:1 / 2,39:1 ✗.\n // `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này: 5,01:1 (light) / 5,35:1\n // (dark) trên `--sidebar` ✓. Tín hiệu \"khoá\" vẫn còn nguyên ở con trỏ và ở `aria-disabled`.\n disabled && \"cursor-not-allowed text-muted-foreground\",\n className\n );\n\n /**\n * Màu icon/vạch active dùng `--sidebar-accent-foreground`, KHÔNG dùng `--app-accent`.\n *\n * Accent mặc định (= `--primary` = gold #E8B824) chỉ đạt **1,70:1** trên nền hàng active\n * (`--sidebar-accent` #FBF5DC) ở light — dưới ngưỡng 3:1 của WCAG 1.4.11 cho thành phần\n * phi-văn-bản, nên vạch chỉ mục active gần như vô hình đúng lúc nó phải nói lên trạng thái.\n * (Ở dark thì gold #F5D060 trên #1E2128 đạt 10,8:1 — chỉ light hỏng.)\n * `--sidebar-accent-foreground` đạt 17,3:1 (light) và 15,1:1 (dark), lại là token semantic\n * nên app đổi theme vẫn đúng. Cùng cách xử lý mà `stat-card.tsx` đã chọn (`text-foreground`).\n *\n * Muốn vạch active mang màu riêng của app trở lại thì phải sửa ở tầng token: cho\n * `--app-accent` một bậc đủ tối ở light (vd gold-500 #7A5800 = 6,0:1), rồi mới quay lại\n * `bg-app-accent` ở đây.\n */\n const iconClass = cn(\n \"size-4 shrink-0\",\n active\n ? \"text-sidebar-accent-foreground\"\n : disabled\n ? \"text-muted-foreground\"\n : \"text-sidebar-foreground/55 group-hover:text-sidebar-foreground\"\n );\n\n /** Vạch dọc chỉ mục đang mở, nằm sát mép trái hàng. */\n const activeMark = <span className=\"absolute inset-y-1 left-0 w-0.5 rounded-r-full bg-sidebar-accent-foreground\" />;\n\n const badge =\n item.badge === undefined || item.badge === null || item.badge === \"\" ? null : (\n <Badge variant={BADGE_VARIANT[item.badgeVariant ?? \"default\"]} size=\"sm\" className=\"ml-auto shrink-0\">\n {item.badge}\n </Badge>\n );\n\n /* ── Sidebar thu gọn: icon vuông + tooltip ─────────────────────────────── */\n if (collapsed && depth === 0) {\n const tileClass = cn(\n \"relative flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out\",\n active ? \"bg-sidebar-accent text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n !disabled && !active && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n // Cùng lý do với `rowClass` ở trên: bậc chữ mờ hơn thay cho `opacity-50`.\n disabled && \"cursor-not-allowed text-muted-foreground\"\n );\n\n const tileBody = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n {/* Thu gọn không đủ chỗ cho số → chỉ chấm báo có nội dung mới */}\n {item.badge ? <span className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive\" /> : null}\n </>\n );\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n {disabled ? (\n // Mục khoá: KHÔNG render <a> — xem chú thích ở nhánh mục lá bên dưới.\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-label={item.label}\n tabIndex={0}\n className={cn(tileClass, FOCUS_RING)}\n >\n {tileBody}\n </span>\n ) : (\n <ShellNavLink\n href={item.href}\n external={item.external}\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n aria-label={item.label}\n className={tileClass}\n >\n {tileBody}\n </ShellNavLink>\n )}\n </TooltipTrigger>\n <TooltipContent side=\"right\" className=\"flex items-center gap-2\">\n <span>{item.label}</span>\n {item.badge ? <span className=\"text-xs opacity-70\">{item.badge}</span> : null}\n </TooltipContent>\n </Tooltip>\n );\n }\n\n /* ── Mục lá ────────────────────────────────────────────────────────────── */\n if (!hasChildren) {\n const leafBody = (\n <>\n {active ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n </>\n );\n\n /*\n * Mục khoá phải dựng bằng <span role=\"link\" aria-disabled>, KHÔNG phải <a> tô mờ:\n * `pointer-events-none` chỉ chặn CHUỘT — người dùng bàn phím vẫn Tab tới thẻ <a> và\n * Enter vẫn điều hướng, lại không có tín hiệu nào cho trình đọc màn hình. Bỏ hẳn href\n * là cách duy nhất chặn được cả hai đường.\n */\n if (disabled) {\n return (\n <span role=\"link\" aria-disabled=\"true\" tabIndex={0} className={cn(rowClass, FOCUS_RING)}>\n {leafBody}\n </span>\n );\n }\n\n return (\n <ShellNavLink\n href={item.href}\n external={item.external}\n // Phải gọi `onNavigate` ở ĐÂY nữa, không chỉ ở nhánh thu gọn: `MobileMenu` truyền\n // `onNavigate={closeMenu}` cho mọi mục, thiếu nó thì bấm một mục điều hướng thường\n // trong menu mobile mà menu không đóng.\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n className={rowClass}\n >\n {leafBody}\n </ShellNavLink>\n );\n }\n\n /* ── Mục có con ────────────────────────────────────────────────────────── */\n return (\n <Collapsible open={open} onOpenChange={setOpen} disabled={disabled}>\n <CollapsibleTrigger asChild>\n {/* `disabled` thật trên <button>: bỏ khỏi tab order và chặn cả Enter/Space,\n khác hẳn `pointer-events-none` vốn chỉ chặn chuột. */}\n <button type=\"button\" disabled={disabled} className={rowClass} aria-expanded={open}>\n {branchActive ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n {/* `text-muted-foreground` chứ không `text-sidebar-foreground/40`: mũi tên MANG THÔNG TIN\n (nhánh đang đóng hay mở) nên chịu ngưỡng 3:1 của WCAG 1.4.11, mà /40 chỉ được\n 2,60:1 ở light ✗ (dark 3,64:1 — lại một chỗ mỗi theme hỏng một nửa).\n `--muted-foreground`: 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓ */}\n <ChevronRight\n className={cn(\n \"ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform duration-150\",\n badge && \"ml-1\",\n open && \"rotate-90\"\n )}\n />\n </button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n <div className=\"mt-0.5 flex flex-col gap-0.5\">\n {item.children?.map((child) => (\n <SidebarMenuItem\n key={child.id}\n item={child}\n depth={depth + 1}\n siblings={siblings}\n onNavigate={onNavigate}\n />\n ))}\n </div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/sidebar-menu-item.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../components/ui/collapsible.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { NavBadgeVariant, NavItem } from \"./types.js\";\nimport { isNavBranchActive, isNavItemActive } from \"./utils.js\";\n\n/** Variant badge của NavItem → variant của primitive Badge (tên trùng nhau, map tường minh cho an toàn). */\nconst BADGE_VARIANT: Record<NavBadgeVariant, \"default\" | \"destructive\" | \"warning\" | \"success\"> = {\n default: \"default\",\n destructive: \"destructive\",\n warning: \"warning\",\n success: \"success\"\n};\n\n/**\n * Vòng focus cho mục đã khoá.\n *\n * Mục khoá vẫn nằm trong tab order (`tabIndex={0}` + `aria-disabled`) theo đúng khuyến nghị\n * của ARIA APG: người dùng bàn phím phải *biết* mục đó tồn tại và đang bị khoá, thay vì mục\n * lặng lẽ biến mất. Không có href và không có handler nên Enter/click không đi đâu cả.\n */\nconst FOCUS_RING = \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\";\n\nexport interface SidebarMenuItemProps {\n item: NavItem;\n /** Sidebar đang thu gọn (chỉ icon) — chỉ áp dụng cho mục cấp 0. */\n collapsed?: boolean;\n /** Cấp lồng (0 = gốc). Cấp > 0 thụt lề và không có icon vuông. */\n depth?: number;\n /**\n * Tập mục để đối chiếu khớp CHÍNH XÁC pathname — nên truyền toàn bộ mục của app\n * hiện tại. Thiếu nó, `/tasks` sẽ vẫn sáng khi đang ở `/tasks/inbox`.\n */\n siblings?: NavItem[];\n /** Gọi khi người dùng bấm vào một LINK thật (không gọi khi chỉ mở/đóng nhánh con) —\n * `MobileMenu` dùng để tự đóng. */\n onNavigate?: () => void;\n className?: string;\n}\n\n/**\n * Một mục menu sidebar: active matching thông minh, badge số, mục con `Collapsible`,\n * khi sidebar thu gọn thì hiện `Tooltip` + chấm badge.\n */\nexport function SidebarMenuItem({\n item,\n collapsed = false,\n depth = 0,\n siblings = [],\n onNavigate,\n className\n}: SidebarMenuItemProps) {\n const { pathname, searchParams } = useShell();\n\n const active = isNavItemActive(item, pathname, siblings, searchParams);\n const branchActive = isNavBranchActive(item, pathname, siblings, searchParams);\n const hasChildren = Boolean(item.children?.length);\n\n const [open, setOpen] = useState(branchActive);\n\n // Điều hướng tới một mục con → tự mở nhánh chứa nó.\n useEffect(() => {\n if (branchActive) setOpen(true);\n }, [branchActive]);\n\n const Icon = item.icon;\n const disabled = Boolean(item.disabled);\n const rowActive = active || (branchActive && !hasChildren);\n\n const rowClass = cn(\n \"group relative flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n depth > 0 && \"pl-8 text-[13px]\",\n rowActive ? \"bg-sidebar-accent font-medium text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n // Mục đã khoá thì KHÔNG gắn hover — đổi màu khi rê chuột là hứa hẹn bấm được.\n !disabled && !rowActive && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n // Mục khoá tô bằng MỘT bậc chữ mờ hơn, KHÔNG phải `opacity-50` chồng lên màu sẵn có.\n // Vì sao: mục khoá vẫn nằm trong tab order (`role=\"link\" aria-disabled tabIndex={0}` — xem\n // chú thích FOCUS_RING) nên KHÔNG được hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3,\n // mà `opacity-50` chồng lên `text-sidebar-foreground/70` cho độ mờ thực 35% → 2,28:1 (light)\n // / 3,10:1 (dark) ✗ cho chữ, và 27,5% trên icon → 1,86:1 / 2,39:1 ✗.\n // `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò này: 5,01:1 (light) / 5,35:1\n // (dark) trên `--sidebar` ✓. Tín hiệu \"khoá\" vẫn còn nguyên ở con trỏ và ở `aria-disabled`.\n disabled && \"cursor-not-allowed text-muted-foreground\",\n className\n );\n\n /**\n * Màu icon/vạch active dùng `--sidebar-accent-foreground`, KHÔNG dùng `--app-accent`.\n *\n * Accent mặc định (= `--primary` = gold #E8B824) chỉ đạt **1,70:1** trên nền hàng active\n * (`--sidebar-accent` #FBF5DC) ở light — dưới ngưỡng 3:1 của WCAG 1.4.11 cho thành phần\n * phi-văn-bản, nên vạch chỉ mục active gần như vô hình đúng lúc nó phải nói lên trạng thái.\n * (Ở dark thì gold #F5D060 trên #1E2128 đạt 10,8:1 — chỉ light hỏng.)\n * `--sidebar-accent-foreground` đạt 17,3:1 (light) và 15,1:1 (dark), lại là token semantic\n * nên app đổi theme vẫn đúng. Cùng cách xử lý mà `stat-card.tsx` đã chọn (`text-foreground`).\n *\n * Muốn vạch active mang màu riêng của app trở lại thì phải sửa ở tầng token: cho\n * `--app-accent` một bậc đủ tối ở light (vd gold-500 #7A5800 = 6,0:1), rồi mới quay lại\n * `bg-app-accent` ở đây.\n */\n const iconClass = cn(\n \"size-4 shrink-0\",\n active\n ? \"text-sidebar-accent-foreground\"\n : disabled\n ? \"text-muted-foreground\"\n : \"text-sidebar-foreground/55 group-hover:text-sidebar-foreground\"\n );\n\n /** Vạch dọc chỉ mục đang mở, nằm sát mép trái hàng. */\n const activeMark = <span className=\"absolute inset-y-1 left-0 w-0.5 rounded-r-full bg-sidebar-accent-foreground\" />;\n\n const badge =\n item.badge === undefined || item.badge === null || item.badge === \"\" ? null : (\n <Badge variant={BADGE_VARIANT[item.badgeVariant ?? \"default\"]} size=\"sm\" className=\"ml-auto shrink-0\">\n {item.badge}\n </Badge>\n );\n\n /* ── Sidebar thu gọn: icon vuông + tooltip ─────────────────────────────── */\n if (collapsed && depth === 0) {\n const tileClass = cn(\n \"relative flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out\",\n active ? \"bg-sidebar-accent text-sidebar-accent-foreground\" : \"text-sidebar-foreground/70\",\n !disabled && !active && \"hover:bg-sidebar-accent/60 hover:text-sidebar-foreground\",\n // Cùng lý do với `rowClass` ở trên: bậc chữ mờ hơn thay cho `opacity-50`.\n disabled && \"cursor-not-allowed text-muted-foreground\"\n );\n\n const tileBody = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n {/* Thu gọn không đủ chỗ cho số → chỉ chấm báo có nội dung mới */}\n {item.badge ? <span className=\"absolute right-1.5 top-1.5 size-1.5 rounded-full bg-destructive\" /> : null}\n </>\n );\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n {disabled ? (\n // Mục khoá: KHÔNG render <a> — xem chú thích ở nhánh mục lá bên dưới.\n <span\n role=\"link\"\n aria-disabled=\"true\"\n aria-label={item.label}\n tabIndex={0}\n className={cn(tileClass, FOCUS_RING)}\n >\n {tileBody}\n </span>\n ) : (\n <ShellNavLink\n href={item.href}\n external={item.external}\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n aria-label={item.label}\n className={tileClass}\n >\n {tileBody}\n </ShellNavLink>\n )}\n </TooltipTrigger>\n <TooltipContent side=\"right\" className=\"flex items-center gap-2\">\n <span>{item.label}</span>\n {item.badge ? <span className=\"text-xs opacity-70\">{item.badge}</span> : null}\n </TooltipContent>\n </Tooltip>\n );\n }\n\n /* ── Mục lá ────────────────────────────────────────────────────────────── */\n if (!hasChildren) {\n const leafBody = (\n <>\n {active ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n </>\n );\n\n /*\n * Mục khoá phải dựng bằng <span role=\"link\" aria-disabled>, KHÔNG phải <a> tô mờ:\n * `pointer-events-none` chỉ chặn CHUỘT — người dùng bàn phím vẫn Tab tới thẻ <a> và\n * Enter vẫn điều hướng, lại không có tín hiệu nào cho trình đọc màn hình. Bỏ hẳn href\n * là cách duy nhất chặn được cả hai đường.\n */\n if (disabled) {\n return (\n <span role=\"link\" aria-disabled=\"true\" tabIndex={0} className={cn(rowClass, FOCUS_RING)}>\n {leafBody}\n </span>\n );\n }\n\n return (\n <ShellNavLink\n href={item.href}\n external={item.external}\n // Phải gọi `onNavigate` ở ĐÂY nữa, không chỉ ở nhánh thu gọn: `MobileMenu` truyền\n // `onNavigate={closeMenu}` cho mọi mục, thiếu nó thì bấm một mục điều hướng thường\n // trong menu mobile mà menu không đóng.\n onClick={() => onNavigate?.()}\n aria-current={active ? \"page\" : undefined}\n className={rowClass}\n >\n {leafBody}\n </ShellNavLink>\n );\n }\n\n /* ── Mục có con ────────────────────────────────────────────────────────── */\n return (\n <Collapsible open={open} onOpenChange={setOpen} disabled={disabled}>\n <CollapsibleTrigger asChild>\n {/* `disabled` thật trên <button>: bỏ khỏi tab order và chặn cả Enter/Space,\n khác hẳn `pointer-events-none` vốn chỉ chặn chuột. */}\n <button type=\"button\" disabled={disabled} className={rowClass} aria-expanded={open}>\n {branchActive ? activeMark : null}\n {Icon ? <Icon className={iconClass} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {badge}\n {/* `text-muted-foreground` chứ không `text-sidebar-foreground/40`: mũi tên MANG THÔNG TIN\n (nhánh đang đóng hay mở) nên chịu ngưỡng 3:1 của WCAG 1.4.11, mà /40 chỉ được\n 2,60:1 ở light ✗ (dark 3,64:1 — lại một chỗ mỗi theme hỏng một nửa).\n `--muted-foreground`: 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓ */}\n <ChevronRight\n className={cn(\n \"ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform duration-150\",\n badge && \"ml-1\",\n open && \"rotate-90\"\n )}\n />\n </button>\n </CollapsibleTrigger>\n <CollapsibleContent className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n <div className=\"mt-0.5 flex flex-col gap-0.5\">\n {item.children?.map((child) => (\n <SidebarMenuItem\n key={child.id}\n item={child}\n depth={depth + 1}\n siblings={siblings}\n onNavigate={onNavigate}\n />\n ))}\n </div>\n </CollapsibleContent>\n </Collapsible>\n );\n}\n"],"mappings":"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","names":[]}
@@ -15,17 +15,37 @@ import 'lucide-react';
15
15
  declare function toNavGroups(nav: ShellNav | undefined): NavGroup[];
16
16
  /** Gộp mọi item của mọi nhóm thành một mảng phẳng (kể cả mục con). */
17
17
  declare function flattenNavItems(groups: NavGroup[]): NavItem[];
18
+ /**
19
+ * Query string hiện tại, theo mọi hình dạng app thường có sẵn trong tay:
20
+ * `useSearchParams()` của Next (một `URLSearchParams`), chuỗi `"?tab=x"` / `"tab=x"`,
21
+ * hoặc object `searchParams` mà Server Component nhận được.
22
+ */
23
+ type NavSearchParams = string | URLSearchParams | Record<string, string | string[] | undefined>;
24
+ /** Quy mọi hình dạng `NavSearchParams` về `URLSearchParams`. `null`/`undefined` → `null`. */
25
+ declare function toSearchParams(input: NavSearchParams | null | undefined): URLSearchParams | null;
18
26
  /**
19
27
  * Active matching "thông minh": chỉ dùng `startsWith` khi KHÔNG có mục nào khác
20
28
  * khớp CHÍNH XÁC `pathname`. Nếu không, `/tasks` vẫn sáng khi đang đứng ở
21
29
  * `/tasks/inbox` (có mục riêng cho nó).
22
30
  *
31
+ * ─ QUERY TRONG `href` ─
32
+ * `usePathname()` của Next **không bao giờ** chứa `?query`, nên so thẳng `pathname === href`
33
+ * làm mục kiểu `/cai-dat?tab=thanh-vien` vĩnh viễn không sáng. Query bị tách khỏi `href`
34
+ * trước khi so đường dẫn; phần query chỉ được xét khi app truyền `searchParams`.
35
+ *
36
+ * Không truyền `searchParams` → chỉ so đường dẫn: mọi mục cùng `/cai-dat` cùng sáng. Đó là
37
+ * ĐÚNG mức thông tin đang có (shell không thấy query), và vẫn tốt hơn hẳn việc không mục nào
38
+ * sáng. Muốn đúng từng tab thì truyền `searchParams` — xem prop cùng tên của `ShellProvider`.
39
+ *
23
40
  * @param siblings Danh sách mục để đối chiếu khớp chính xác — nên truyền toàn bộ
24
41
  * mục của app hiện tại (`flattenNavItems`), không chỉ mục cùng cấp.
42
+ * @param searchParams Query hiện tại (TUỲ CHỌN). Có nó thì mục có query phải khớp TẬP CON
43
+ * (`?tab=a` sáng ở `?tab=a&q=x`), và mục KHÔNG query nhường cho mục cùng
44
+ * đường dẫn có query khớp — tab mặc định không sáng cùng lúc với tab đang mở.
25
45
  */
26
- declare function isNavItemActive(item: NavItem, pathname: string, siblings?: NavItem[]): boolean;
46
+ declare function isNavItemActive(item: NavItem, pathname: string, siblings?: NavItem[], searchParams?: NavSearchParams | null): boolean;
27
47
  /** Mục cha được coi là active khi chính nó hoặc bất kỳ mục con nào active. */
28
- declare function isNavBranchActive(item: NavItem, pathname: string, siblings?: NavItem[]): boolean;
48
+ declare function isNavBranchActive(item: NavItem, pathname: string, siblings?: NavItem[], searchParams?: NavSearchParams | null): boolean;
29
49
  /** App đã được cấp quyền (`entitled` khác `false`) — thứ tự giữ nguyên như app truyền vào. */
30
50
  declare function getEntitledApps(apps: AppDescriptor[]): AppDescriptor[];
31
51
  /** App chưa mở khoá — hiển thị ở nhóm "Khám phá thêm" của AppLauncher. */
@@ -33,6 +53,10 @@ declare function getLockedApps(apps: AppDescriptor[]): AppDescriptor[];
33
53
  /**
34
54
  * Suy ra app hiện tại từ `pathname` — khớp prefix DÀI NHẤT trước để
35
55
  * "/crm/deals" không rơi nhầm vào app có href "/".
56
+ *
57
+ * Query trong `href` bị tách ra trước khi so, cùng lý do với `isNavItemActive`: `pathname`
58
+ * không mang query, nên app khai `href: "/crm?view=board"` mà so nguyên chuỗi thì không bao
59
+ * giờ được nhận ra. Sắp xếp cũng theo độ dài ĐƯỜNG DẪN (query dài không được đẩy app lên trước).
36
60
  */
37
61
  declare function resolveCurrentApp(apps: AppDescriptor[], pathname: string): AppDescriptor | null;
38
62
  /**
@@ -47,4 +71,4 @@ declare function resolveCurrentApp(apps: AppDescriptor[], pathname: string): App
47
71
  */
48
72
  declare function appAccentStyle(app: Pick<AppDescriptor, "accent" | "accentForeground" | "accentInk"> | null | undefined): CSSProperties | undefined;
49
73
 
50
- export { appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups };
74
+ export { type NavSearchParams, appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups, toSearchParams };