@comitor/ui 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,13 +38,15 @@ function Badge({
38
38
  children,
39
39
  ...props
40
40
  }) {
41
- const Comp = asChild && !onRemove ? Slot : "span";
42
41
  if (onRemove && !removeLabel && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
43
42
  console.warn(
44
43
  "[Badge] `onRemove` c\u1EA7n `removeLabel` \u2014 n\xFAt \u2715 ch\u1EC9 c\xF3 icon th\xEC t\xEAn tr\u1EE3 n\u0103ng l\xE0 th\u1EE9 duy nh\u1EA5t n\xF3i n\xF3 g\u1EE1 g\xEC."
45
44
  );
46
45
  }
47
- return /* @__PURE__ */ jsxs(Comp, { "data-slot": "badge", className: cn(badgeVariants({ variant, size }), onRemove && "pr-1", className), ...props, children: [
46
+ if (asChild && !onRemove) {
47
+ return /* @__PURE__ */ jsx(Slot, { "data-slot": "badge", className: cn(badgeVariants({ variant, size }), className), ...props, children });
48
+ }
49
+ return /* @__PURE__ */ jsxs("span", { "data-slot": "badge", className: cn(badgeVariants({ variant, size }), onRemove && "pr-1", className), ...props, children: [
48
50
  children,
49
51
  onRemove ? (
50
52
  /*
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓ ở bảng\n * TƯƠNG PHẢN CAO; ở bảng MẶC ĐỊNH nó cũng chỉ 1,16:1 (light) / 2,00:1 (dark) trên `--card` (đo trên 3.3.4) —\n * khoản dưới ngưỡng bảng mặc định đã chấp nhận cho viền control, người cần tương phản bật bảng tương phản cao.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. Bị bỏ qua khi có `onRemove`. */\n asChild?: boolean;\n /**\n * Chip GỠ ĐƯỢC (từ 3.7.0): vẽ một nút ✕ ở cuối badge (người theo dõi, nhãn đã gắn…). Đi cùng\n * `removeLabel` — thiếu thì nút không có tên trợ năng và gói cảnh báo ở console lúc phát triển.\n */\n onRemove?: () => void;\n /**\n * Tên trợ năng của nút ✕ — gói KHÔNG có nhãn mặc định, cố ý: nút chỉ có icon thì tên là thứ DUY NHẤT nói\n * nó gỡ CÁI GÌ (\"Gỡ Lan khỏi người theo dõi\"), mà câu đó là chữ nghiệp vụ của app. Một mặc định chung\n * kiểu \"Remove\" lặp N lần trong một danh sách là N nút trùng tên. (Và nó giữ `badge.tsx` là file tĩnh —\n * đọc nhãn từ `ComitorLocaleProvider` sẽ buộc nó thành client.)\n *\n * Vì sao không ép bằng kiểu (union \"có `onRemove` thì bắt buộc `removeLabel`\"): `BadgeProps` là interface\n * đã publish, đổi sang union là gãy mọi `interface X extends BadgeProps` của app.\n */\n removeLabel?: string;\n /** Khoá riêng nút ✕ (vd đang lưu) mà badge vẫn hiện bình thường. */\n removeDisabled?: boolean;\n}\n\nfunction Badge({\n className,\n variant,\n size,\n asChild = false,\n onRemove,\n removeLabel,\n removeDisabled,\n children,\n ...props\n}: BadgeProps) {\n const Comp = asChild && !onRemove ? Slot : \"span\";\n // `typeof process` — cùng lý do với `circular-progress.tsx`: ESM thuần trên trình duyệt không có `process`.\n if (onRemove && !removeLabel && typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[Badge] `onRemove` cần `removeLabel` — nút ✕ chỉ có icon thì tên trợ năng là thứ duy nhất nói nó gỡ gì.\"\n );\n }\n\n return (\n <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), onRemove && \"pr-1\", className)} {...props}>\n {children}\n {onRemove ? (\n /*\n * `opacity-70`, KHÔNG `text-x/70`: icon lucide là nhiều nét cắt nhau, alpha trong `color` tô chỗ\n * giao hai lần (docs/hop-dong-mau.md §7b). Vùng bấm `size-4` nằm gọn trong chiều cao badge `md`.\n */\n <button\n type=\"button\"\n data-slot=\"badge-remove\"\n aria-label={removeLabel}\n disabled={removeDisabled}\n onClick={onRemove}\n className={cn(\n \"inline-flex size-4 shrink-0 items-center justify-center rounded-sm opacity-70 transition-opacity\",\n \"hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50\",\n \"disabled:pointer-events-none disabled:opacity-40\"\n )}\n >\n <X aria-hidden=\"true\" className=\"size-3\" />\n </button>\n ) : null}\n </Comp>\n );\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AAgHI,SAmBM,KAnBN;AAhHJ,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,SAAS;AAElB,SAAS,UAAU;AAyCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAwBA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,OAAO,WAAW,CAAC,WAAW,OAAO;AAE3C,MAAI,YAAY,CAAC,eAAe,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa,cAAc;AACvG,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,YAAY,QAAQ,SAAS,GAAI,GAAG,OACzG;AAAA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,aAAU;AAAA,UACV,cAAY;AAAA,UACZ,UAAU;AAAA,UACV,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UAEA,8BAAC,KAAE,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,MAC3C;AAAA,QACE;AAAA,KACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓ ở bảng\n * TƯƠNG PHẢN CAO; ở bảng MẶC ĐỊNH nó cũng chỉ 1,16:1 (light) / 2,00:1 (dark) trên `--card` (đo trên 3.3.4) —\n * khoản dưới ngưỡng bảng mặc định đã chấp nhận cho viền control, người cần tương phản bật bảng tương phản cao.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. Bị bỏ qua khi có `onRemove`. */\n asChild?: boolean;\n /**\n * Chip GỠ ĐƯỢC (từ 3.7.0): vẽ một nút ✕ ở cuối badge (người theo dõi, nhãn đã gắn…). Đi cùng\n * `removeLabel` — thiếu thì nút không có tên trợ năng và gói cảnh báo ở console lúc phát triển.\n */\n onRemove?: () => void;\n /**\n * Tên trợ năng của nút ✕ — gói KHÔNG có nhãn mặc định, cố ý: nút chỉ có icon thì tên là thứ DUY NHẤT nói\n * nó gỡ CÁI GÌ (\"Gỡ Lan khỏi người theo dõi\"), mà câu đó là chữ nghiệp vụ của app. Một mặc định chung\n * kiểu \"Remove\" lặp N lần trong một danh sách là N nút trùng tên. (Và nó giữ `badge.tsx` là file tĩnh —\n * đọc nhãn từ `ComitorLocaleProvider` sẽ buộc nó thành client.)\n *\n * Vì sao không ép bằng kiểu (union \"có `onRemove` thì bắt buộc `removeLabel`\"): `BadgeProps` là interface\n * đã publish, đổi sang union là gãy mọi `interface X extends BadgeProps` của app.\n */\n removeLabel?: string;\n /** Khoá riêng nút ✕ (vd đang lưu) mà badge vẫn hiện bình thường. */\n removeDisabled?: boolean;\n}\n\nfunction Badge({\n className,\n variant,\n size,\n asChild = false,\n onRemove,\n removeLabel,\n removeDisabled,\n children,\n ...props\n}: BadgeProps) {\n // `typeof process` — cùng lý do với `circular-progress.tsx`: ESM thuần trên trình duyệt không có `process`.\n if (onRemove && !removeLabel && typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[Badge] `onRemove` cần `removeLabel` — nút ✕ chỉ có icon thì tên trợ năng là thứ duy nhất nói nó gỡ gì.\"\n );\n }\n\n /*\n * ⚠ `asChild` đi RIÊNG một nhánh, chỉ trao `children`: `Slot` của Radix đòi ĐÚNG MỘT con. Viết chung một JSX với\n * nhánh `onRemove` thì con của nó là mảng `[children, null]` và `React.Children.only` ném lỗi — mọi\n * `<Badge asChild><a/></Badge>` đổ trang (3.7.0, sửa ở 3.7.1; `smoke:entries` nay render ca này).\n */\n if (asChild && !onRemove) {\n return (\n <Slot data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), className)} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <span data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), onRemove && \"pr-1\", className)} {...props}>\n {children}\n {onRemove ? (\n /*\n * `opacity-70`, KHÔNG `text-x/70`: icon lucide là nhiều nét cắt nhau, alpha trong `color` tô chỗ\n * giao hai lần (docs/hop-dong-mau.md §7b). Vùng bấm `size-4` nằm gọn trong chiều cao badge `md`.\n */\n <button\n type=\"button\"\n data-slot=\"badge-remove\"\n aria-label={removeLabel}\n disabled={removeDisabled}\n onClick={onRemove}\n className={cn(\n \"inline-flex size-4 shrink-0 items-center justify-center rounded-sm opacity-70 transition-opacity\",\n \"hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50\",\n \"disabled:pointer-events-none disabled:opacity-40\"\n )}\n >\n <X aria-hidden=\"true\" className=\"size-3\" />\n </button>\n ) : null}\n </span>\n );\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AAqHM,cAOF,YAPE;AArHN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,SAAS;AAElB,SAAS,UAAU;AAyCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAwBA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AAEb,MAAI,YAAY,CAAC,eAAe,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa,cAAc;AACvG,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAOA,MAAI,WAAW,CAAC,UAAU;AACxB,WACE,oBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OACrF,UACH;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,YAAY,QAAQ,SAAS,GAAI,GAAG,OACzG;AAAA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,aAAU;AAAA,UACV,cAAY;AAAA,UACZ,UAAU;AAAA,UACV,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UAEA,8BAAC,KAAE,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,MAC3C;AAAA,QACE;AAAA,KACN;AAEJ;","names":[]}
@@ -24,6 +24,8 @@ const shellEn = {
24
24
  lockedHint: "Not included in your current plan",
25
25
  account: "Account",
26
26
  profile: "Profile",
27
+ preferences: "Preferences",
28
+ security: "Security",
27
29
  settings: "Settings",
28
30
  appearance: "Appearance",
29
31
  help: "Help",
@@ -60,6 +62,8 @@ const shellVi = {
60
62
  lockedHint: "\u1EE8ng d\u1EE5ng ch\u01B0a c\xF3 trong g\xF3i hi\u1EC7n t\u1EA1i",
61
63
  account: "T\xE0i kho\u1EA3n",
62
64
  profile: "H\u1ED3 s\u01A1",
65
+ preferences: "Tu\u1EF3 ch\u1ECDn hi\u1EC3n th\u1ECB",
66
+ security: "B\u1EA3o m\u1EADt",
63
67
  settings: "C\xE0i \u0111\u1EB7t",
64
68
  appearance: "Giao di\u1EC7n",
65
69
  help: "Tr\u1EE3 gi\xFAp",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho\n * trình đọc màn hình): \"Ứng dụng\" ⊂ \"Mở danh sách ứng dụng\". Bảng tiếng Anh của `3.0.0` hỏng đúng\n * chỗ này — \"Apps\" không nằm trong \"Open app launcher\", nên người điều khiển bằng giọng nói đọc\n * chữ họ THẤY (\"click Apps\") mà không khớp nút nào. Sửa ở `3.0.1`.\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open apps list\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\",\n skipToContent: \"Skip to main content\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\",\n skipToContent: \"Bỏ qua điều hướng, sang nội dung chính\"\n};\n"],"mappings":"AAeO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;","names":[]}
1
+ {"version":3,"sources":["../../../src/i18n/namespaces/shell.ts"],"sourcesContent":["import type { ShellLabels } from \"../../shell/types.js\";\n\n/**\n * Chữ của cả tầng shell — `ShellProvider`/`AppShell` và mọi thứ đọc `useShell().labels`\n * (`Sidebar`, `AppHeader`, `MobileMenu`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`), cộng\n * `ThemeToggle` và `NotificationsButton` khi đứng ngoài shell. Hình dạng + chú thích từng khoá:\n * `ShellLabels`.\n *\n * ⚠ `collapseSidebarShort` PHẢI là một phần của `collapseSidebar` ở MỌI ngôn ngữ (WCAG 2.5.3):\n * \"Collapse\" ⊂ \"Collapse sidebar\", \"Thu gọn\" ⊂ \"Thu gọn thanh bên\".\n * ⚠ Cùng luật cho cặp `apps` (tooltip HIỆN RA của nút mở launcher) ⊂ `appLauncherLabel` (tên cho\n * trình đọc màn hình): \"Ứng dụng\" ⊂ \"Mở danh sách ứng dụng\". Bảng tiếng Anh của `3.0.0` hỏng đúng\n * chỗ này — \"Apps\" không nằm trong \"Open app launcher\", nên người điều khiển bằng giọng nói đọc\n * chữ họ THẤY (\"click Apps\") mà không khớp nút nào. Sửa ở `3.0.1`.\n */\nexport const shellEn: ShellLabels = {\n collapseSidebar: \"Collapse sidebar\",\n collapseSidebarShort: \"Collapse\",\n expandSidebar: \"Expand sidebar\",\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n search: \"Search\",\n searchPlaceholder: \"Search…\",\n notifications: \"Notifications\",\n navigationLabel: \"Main navigation\",\n workspace: \"Workspace\",\n workspaceSwitcherLabel: \"Switch workspace\",\n workspaceSearchPlaceholder: \"Search workspaces…\",\n workspaceEmpty: \"No workspaces found\",\n createWorkspace: \"Create workspace\",\n workspaceSettings: \"Workspace settings\",\n inviteMembers: \"Invite members\",\n members: (count) => `${count} ${count === 1 ? \"member\" : \"members\"}`,\n apps: \"Apps\",\n appLauncherLabel: \"Open apps list\",\n myApps: \"Your apps\",\n discoverApps: \"Discover more\",\n locked: \"Locked\",\n lockedHint: \"Not included in your current plan\",\n account: \"Account\",\n profile: \"Profile\",\n preferences: \"Preferences\",\n security: \"Security\",\n settings: \"Settings\",\n appearance: \"Appearance\",\n help: \"Help\",\n logout: \"Log out\",\n themeLight: \"Light\",\n themeDark: \"Dark\",\n themeSystem: \"System\",\n toggleTheme: \"Toggle theme\",\n skipToContent: \"Skip to main content\"\n};\n\nexport const shellVi: ShellLabels = {\n collapseSidebar: \"Thu gọn thanh bên\",\n collapseSidebarShort: \"Thu gọn\",\n expandSidebar: \"Mở rộng thanh bên\",\n openMenu: \"Mở menu\",\n closeMenu: \"Đóng menu\",\n search: \"Tìm kiếm\",\n searchPlaceholder: \"Tìm kiếm…\",\n notifications: \"Thông báo\",\n navigationLabel: \"Điều hướng chính\",\n workspace: \"Không gian làm việc\",\n workspaceSwitcherLabel: \"Đổi không gian làm việc\",\n workspaceSearchPlaceholder: \"Tìm không gian làm việc…\",\n workspaceEmpty: \"Không tìm thấy không gian làm việc\",\n createWorkspace: \"Tạo không gian làm việc\",\n workspaceSettings: \"Cài đặt không gian làm việc\",\n inviteMembers: \"Mời thành viên\",\n members: (count) => `${count} thành viên`,\n apps: \"Ứng dụng\",\n appLauncherLabel: \"Mở danh sách ứng dụng\",\n myApps: \"Ứng dụng của bạn\",\n discoverApps: \"Khám phá thêm\",\n locked: \"Chưa mở khoá\",\n lockedHint: \"Ứng dụng chưa có trong gói hiện tại\",\n account: \"Tài khoản\",\n profile: \"Hồ sơ\",\n preferences: \"Tuỳ chọn hiển thị\",\n security: \"Bảo mật\",\n settings: \"Cài đặt\",\n appearance: \"Giao diện\",\n help: \"Trợ giúp\",\n logout: \"Đăng xuất\",\n themeLight: \"Sáng\",\n themeDark: \"Tối\",\n themeSystem: \"Hệ thống\",\n toggleTheme: \"Đổi giao diện\",\n skipToContent: \"Bỏ qua điều hướng, sang nội dung chính\"\n};\n"],"mappings":"AAeO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS;AAAA,EAClE,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;AAEO,MAAM,UAAuB;AAAA,EAClC,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,wBAAwB;AAAA,EACxB,4BAA4B;AAAA,EAC5B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,SAAS,CAAC,UAAU,GAAG,KAAK;AAAA,EAC5B,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AACjB;","names":[]}
@@ -204,6 +204,10 @@ interface ShellLabels {
204
204
  lockedHint: string;
205
205
  account: string;
206
206
  profile: string;
207
+ /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/preferences` của Comitor.Account. */
208
+ preferences: string;
209
+ /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/security` của Comitor.Account. */
210
+ security: string;
207
211
  settings: string;
208
212
  appearance: string;
209
213
  help: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n /* Trợ năng */\n /** Chữ của skip link đầu khung (`AppShell`, WCAG 2.4.1) — chỉ hiện khi nhận focus bàn phím */\n skipToContent: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AA+NjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/types.ts"],"sourcesContent":["/**\n * Hợp đồng dữ liệu của Tier 3 (shell).\n *\n * Nguyên tắc: package KHÔNG chứa dữ liệu app. Toàn bộ workspace / danh sách app /\n * menu / user đều là *type* — app truyền vào qua props. File này là **module thuần**\n * (chỉ type + hằng nhãn, không hook, không directive) nên dùng được ở cả server lẫn client.\n *\n * Hai trục điều hướng của Comitor — một user thuộc NHIỀU workspace, và mỗi workspace\n * lại bật NHIỀU sản phẩm. Hai chiều đó vuông góc nhau (đổi workspace không đổi app đang\n * dùng, và ngược lại) nên không gộp được vào một menu phẳng:\n * trục 1 — WORKSPACE: đổi ngữ cảnh dữ liệu (Acme Corp ▸ Beta Ltd)\n * trục 2 — APP: đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR)\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport type { AnchorHTMLAttributes, ComponentType } from \"react\";\nimport { shellEn } from \"../i18n/namespaces/shell.js\";\n\n/**\n * Icon dùng trong shell: `LucideIcon` là trường hợp phổ biến, nhưng app có thể\n * truyền component SVG riêng miễn nhận `className` (để shell điều khiển kích thước).\n */\nexport type ShellIcon = LucideIcon | ComponentType<{ className?: string }>;\n\n/** Workspace (trục 1) — một tổ chức/không gian làm việc mà user thuộc về. */\nexport interface Workspace {\n id: string;\n name: string;\n /** Định danh trên URL/subdomain — shell chỉ hiển thị, không tự điều hướng. */\n slug: string;\n logoUrl?: string;\n /** \"Free\" | \"Pro\" | \"Business\"… — chỉ để hiển thị nhãn gói, shell không suy luận quyền từ đây. */\n plan?: string;\n /** Vai trò của user trong workspace này (chuỗi hiển thị sẵn). */\n role?: string;\n memberCount?: number;\n}\n\n/** Một sản phẩm trong hệ sinh thái (trục 2) — Tasks, Chat, CRM, HR… */\nexport interface AppDescriptor {\n /** \"tasks\" | \"chat\" | \"crm\" | \"hr\" | … */\n id: string;\n /** Tên hiển thị — app truyền theo ngôn ngữ giao diện đang dùng. */\n name: string;\n description?: string;\n icon: ShellIcon;\n href: string;\n /**\n * Tone màu định danh — MỘT tên trong `AVATAR_TONE_NAMES` của `@comitor/ui`.\n *\n * Bỏ trống thì màu TỰ SUY từ `id` bằng bảng tone dùng chung với `LetterAvatar`, nên một sản\n * phẩm mới có màu ngay mà không ai phải cấu hình. Tên lạ cũng rơi về màu suy ra — không ném.\n *\n * ⚠ Hạt giống là `id` (khoá app), KHÔNG phải `name`: tên đổi theo ngôn ngữ giao diện, và đo\n * được 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên. Xem `IconAvatar` prop `seed`.\n */\n tone?: string;\n /**\n * Màu nhấn riêng của app — bất kỳ giá trị màu CSS hợp lệ (nên là `var(--…)`).\n * Shell gán vào biến `--app-accent` bằng inline style, KHÔNG sinh class động.\n *\n * ⚠ `--app-accent` CHỈ để TÔ NỀN. Khai `accent` thì phải khai kèm `accentInk`\n * (và `accentForeground` nếu nền đặc của bạn không mang được mực) — xem\n * `docs/hop-dong-mau.md` §5.\n */\n /** @deprecated Từ 1.10.0 `AppLauncher` KHÔNG đọc prop này nữa — dùng `tone`. Xem ghi chú trên. */\n accent?: string;\n /** Màu chữ/icon nằm TRÊN nền `--app-accent` ĐẶC; mặc định giữ `--app-accent-foreground` của theme. */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentForeground?: string;\n /**\n * Chính màu app khi nó đi lên NỀN TRANG (chữ, icon rời, viền, vạch active, dấu chọn)\n * → biến `--app-accent-ink`. Mặc định giữ bậc ink của theme.\n *\n * Vì sao phải khai riêng chứ không dẫn xuất từ `accent`: bậc nền và bậc mực có ràng buộc\n * NGƯỢC NHAU — nền càng đúng brand thì cùng màu đó làm chữ càng chìm. Gold mặc định là ví\n * dụ: `--app-accent` #E8B824 làm chữ trên nền trắng chỉ **1,86:1**, trên chính tint 15%\n * của nó **1,69:1**; `--app-accent-ink` #7A5800 đạt **6,51:1** trên `--background`,\n * **6,24:1** trên `--sidebar`, **5,96:1** trên `--sidebar-accent`. Bỏ trống trường này mà\n * vẫn đổi `accent` thì mọi chỗ dùng `text-app-accent-ink` giữ nguyên gold — sai màu app.\n */\n /** @deprecated Từ 1.10.0 — dùng `tone`. */\n accentInk?: string;\n /** `false` = workspace chưa bật/chưa mua → hiện khoá và gọi `onUpsell` thay vì điều hướng. */\n entitled?: boolean;\n badge?: number | string;\n}\n\n/** Variant badge của mục menu — map thẳng sang variant của primitive `Badge`. */\nexport type NavBadgeVariant = \"default\" | \"destructive\" | \"warning\" | \"success\";\n\n/** Một mục điều hướng trong sidebar. */\nexport interface NavItem {\n id: string;\n label: string;\n href: string;\n icon?: ShellIcon;\n badge?: number | string;\n badgeVariant?: NavBadgeVariant;\n /** Mục con — render trong `Collapsible`; cha có children thì click cha chỉ mở/đóng. */\n children?: NavItem[];\n /**\n * Ép trạng thái active thay vì để shell tự suy từ `pathname`\n * (dùng khi route thật khác href, vd trang chi tiết `/tasks/123`).\n */\n isActive?: boolean;\n /** Mở link ở tab mới (link ngoài: tài liệu, trạng thái hệ thống…). */\n external?: boolean;\n disabled?: boolean;\n}\n\n/** Nhóm mục menu (nhãn nhỏ + thu gọn được). */\nexport interface NavGroup {\n id: string;\n /** Bỏ trống = nhóm không nhãn (dùng cho nhóm đầu tiên). */\n label?: string;\n items: NavItem[];\n /** Cho phép thu gọn cả nhóm; mặc định true khi có `label`. */\n collapsible?: boolean;\n /** Trạng thái ban đầu khi `collapsible` — mặc định mở. */\n defaultCollapsed?: boolean;\n}\n\n/**\n * Menu app truyền vào: mảng phẳng `NavItem[]` (app đơn giản) hoặc `NavGroup[]`.\n * Shell chuẩn hoá về `NavGroup[]` bằng `toNavGroups()` trong `utils.ts`.\n */\nexport type ShellNav = NavItem[] | NavGroup[];\n\n/** Người dùng đang đăng nhập. */\nexport interface ShellUser {\n id: string;\n name: string;\n email?: string;\n avatarUrl?: string;\n /** Vai trò trong workspace hiện tại — chuỗi hiển thị sẵn, shell không map mã. */\n role?: string;\n}\n\n/** Mục bổ sung do app chèn vào `UserMenu`. */\nexport interface UserMenuItem {\n id: string;\n label: string;\n icon?: ShellIcon;\n /** Có `href` thì render link; không thì render nút gọi `onSelect`. */\n href?: string;\n onSelect?: () => void;\n /** Nhãn phím tắt hiển thị bên phải, vd \"Ctrl+P\" (chỉ hiển thị, không tự gắn phím). */\n shortcut?: string;\n variant?: \"default\" | \"destructive\";\n}\n\n/**\n * Props của component link do app cung cấp (`LinkComponent`).\n * Mặc định shell dùng `next/link`; app không-Next (Vite, Storybook) truyền\n * component router của mình miễn khớp hình dạng props này.\n */\nexport interface ShellLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n /** Chỉ nhận chuỗi — shell không dùng dạng `UrlObject` của Next để adapter khác còn dùng lại được. */\n href: string;\n}\n\n/** Component link thay thế `next/link`. */\nexport type ShellLinkComponent = ComponentType<ShellLinkProps>;\n\n/**\n * Toàn bộ chuỗi hiển thị của shell. Theo `ComitorLocaleProvider` (namespace `shell` — mặc định\n * tiếng Anh, bảng tiếng Việt dựng sẵn); override từng khoá qua prop `labels` của\n * `ShellProvider`/`AppShell`.\n */\nexport interface ShellLabels {\n /* Sidebar & header */\n collapseSidebar: string;\n /**\n * Nhãn NGẮN hiện trên chính nút thu gọn (khi thanh bên đang mở rộng).\n *\n * Tách khỏi `collapseSidebar` vì hai chuỗi có hai việc khác nhau: chuỗi dài là TÊN TRUY CẬP\n * (`aria-label` + tooltip) — nó phải nói rõ thu gọn cái gì, vì người dùng trình đọc màn hình\n * nghe nó tách khỏi ngữ cảnh; chuỗi ngắn là chữ NHÌN THẤY trong một nút rộng 240px nằm ngay\n * dưới thanh bên, nơi ngữ cảnh đã hiển nhiên và chữ dài chỉ làm nút chật.\n *\n * ⚠ WCAG 2.5.3 (Label in Name): chuỗi ngắn PHẢI là một phần của chuỗi dài, nếu không lệnh thoại\n * \"bấm Collapse\" sẽ không khớp tên truy cập. \"Collapse\" ⊂ \"Collapse sidebar\" ✓, \"Thu gọn\" ⊂\n * \"Thu gọn thanh bên\" ✓ — giữ quan hệ đó khi dịch sang ngôn ngữ khác.\n */\n collapseSidebarShort: string;\n expandSidebar: string;\n openMenu: string;\n closeMenu: string;\n search: string;\n searchPlaceholder: string;\n notifications: string;\n navigationLabel: string;\n /* Workspace switcher */\n workspace: string;\n workspaceSwitcherLabel: string;\n workspaceSearchPlaceholder: string;\n workspaceEmpty: string;\n createWorkspace: string;\n workspaceSettings: string;\n inviteMembers: string;\n /**\n * Đơn vị đếm thành viên của một workspace. Là HÀM chứ không phải chuỗi vì nó CHÈN SỐ, và\n * trật tự từ đổi theo ngôn ngữ: tiếng Việt đặt số trước (\"12 thành viên\"), tiếng Anh cũng vậy\n * (\"12 members\"), nhưng nhiều ngôn ngữ thì không — nối `${count} ${đơn_vị}` ở phía component\n * là khoá cứng trật tự của một ngôn ngữ vào mọi bản dịch. Hàm còn chọn được dạng số ít/nhiều\n * (\"1 member\" ⇄ \"2 members\"), thứ tiếng Việt không có mà tiếng Anh thì bắt buộc. Cùng khuôn\n * với `range`/`pageStatus` của `TablePagination` (README §5, ba quy tắc của cơ chế `labels`).\n */\n members: (count: number) => string;\n /* App launcher */\n apps: string;\n appLauncherLabel: string;\n myApps: string;\n discoverApps: string;\n locked: string;\n lockedHint: string;\n /* User menu */\n account: string;\n profile: string;\n /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/preferences` của Comitor.Account. */\n preferences: string;\n /** Mục dựng sẵn khi `UserMenu` có `accountUrl` — trang `/security` của Comitor.Account. */\n security: string;\n settings: string;\n appearance: string;\n help: string;\n logout: string;\n /* Theme */\n themeLight: string;\n themeDark: string;\n themeSystem: string;\n toggleTheme: string;\n /* Trợ năng */\n /** Chữ của skip link đầu khung (`AppShell`, WCAG 2.4.1) — chỉ hiện khi nhận focus bàn phím */\n skipToContent: string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định (từ `3.0.0`; trước đó là tiếng Việt). Bảng tiếng Việt:\n * `comitorMessagesVi.shell`. Shell không đọc thẳng hằng này — `ShellProvider` đọc bảng của\n * `ComitorLocaleProvider` rồi trộn `labels` lên trên; hằng ở đây để app trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_SHELL_LABELS: ShellLabels = { ...shellEn };\n"],"mappings":"AAgBA,SAAS,eAAe;AAmOjB,MAAM,uBAAoC,EAAE,GAAG,QAAQ;","names":[]}
@@ -17,6 +17,16 @@ interface UserMenuProps {
17
17
  className?: string;
18
18
  /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */
19
19
  showName?: boolean;
20
+ /**
21
+ * Gốc của Comitor.Account (vd `https://account.comitor.ai`, không dấu `/` cuối). Có giá trị thì menu
22
+ * dựng sẵn BA mục trỏ sang Account — Hồ sơ (`/profile`), Tuỳ chọn hiển thị (`/preferences`), Bảo mật
23
+ * (`/security`) — đứng đầu menu, trước mục của app. Chính app Account truyền chuỗi rỗng `""` để ra
24
+ * đường dẫn tương đối. `profileHref` vẫn thắng nếu app khai cả hai.
25
+ *
26
+ * Ba trang ấy là của DANH TÍNH dùng chung cho cả hệ sinh thái, nên mọi app hiện cùng ba mục, cùng
27
+ * chữ, cùng thứ tự — gói giữ nhãn để không app nào phải dịch lại.
28
+ */
29
+ accountUrl?: string;
20
30
  /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */
21
31
  profileHref?: string;
22
32
  /** Link cài đặt (nhãn `settings`). */
@@ -49,6 +59,6 @@ interface UserMenuProps {
49
59
  *
50
60
  * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.
51
61
  */
52
- declare function UserMenu({ className, showName, profileHref, settingsHref, helpHref, items, showThemeSubmenu, onLogout, align }: UserMenuProps): React.JSX.Element | null;
62
+ declare function UserMenu({ className, showName, accountUrl, profileHref, settingsHref, helpHref, items, showThemeSubmenu, onLogout, align }: UserMenuProps): React.JSX.Element | null;
53
63
 
54
64
  export { UserAvatar, type UserAvatarProps, UserMenu, type UserMenuProps };
@@ -1,6 +1,18 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from "lucide-react";
3
+ import {
4
+ ChevronDown,
5
+ CircleUser,
6
+ HelpCircle,
7
+ LogOut,
8
+ Monitor,
9
+ Moon,
10
+ Palette,
11
+ Settings,
12
+ ShieldCheck,
13
+ SlidersHorizontal,
14
+ Sun
15
+ } from "lucide-react";
4
16
  import { useTheme } from "next-themes";
5
17
  import { getAvatarToneClasses } from "../components/composite/letter-avatar.js";
6
18
  import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
@@ -29,6 +41,7 @@ function UserAvatar({ user, className }) {
29
41
  function UserMenu({
30
42
  className,
31
43
  showName = false,
44
+ accountUrl,
32
45
  profileHref,
33
46
  settingsHref,
34
47
  helpHref,
@@ -55,7 +68,9 @@ function UserMenu({
55
68
  return /* @__PURE__ */ jsx(DropdownMenuItem, { variant: item.variant, onClick: () => item.onSelect?.(), children: content }, item.id);
56
69
  };
57
70
  const builtInItems = [
58
- profileHref ? { id: "profile", label: labels.profile, icon: CircleUser, href: profileHref } : null,
71
+ profileHref || accountUrl !== void 0 ? { id: "profile", label: labels.profile, icon: CircleUser, href: profileHref ?? `${accountUrl}/profile` } : null,
72
+ accountUrl !== void 0 ? { id: "preferences", label: labels.preferences, icon: SlidersHorizontal, href: `${accountUrl}/preferences` } : null,
73
+ accountUrl !== void 0 ? { id: "security", label: labels.security, icon: ShieldCheck, href: `${accountUrl}/security` } : null,
59
74
  settingsHref ? { id: "settings", label: labels.settings, icon: Settings, href: settingsHref } : null,
60
75
  helpHref ? { id: "help", label: labels.help, icon: HelpCircle, href: helpHref } : null
61
76
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown, CircleUser, HelpCircle, LogOut, Monitor, Moon, Palette, Settings, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref } : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/user-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n ChevronDown,\n CircleUser,\n HelpCircle,\n LogOut,\n Monitor,\n Moon,\n Palette,\n Settings,\n ShieldCheck,\n SlidersHorizontal,\n Sun\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { ShellUser, UserMenuItem } from \"./types.js\";\nimport { getVietnameseInitials } from \"./utils.js\";\n\nexport interface UserAvatarProps {\n user: ShellUser;\n className?: string;\n}\n\n/**\n * Avatar người dùng: ảnh nếu có, không thì chữ cái kiểu VN (HỌ + TÊN) tô màu theo tên.\n *\n * Tô màu theo `user.name` chứ KHÔNG theo email: `LetterAvatar`/`AvatarGroup` ở Tier 2 seed\n * bằng tên, nên lấy khoá khác ở đây là cùng một người ra hai màu trên cùng một trang.\n */\nexport function UserAvatar({ user, className }: UserAvatarProps) {\n return (\n <Avatar className={cn(\"size-8\", className)}>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback className={cn(\"text-xs font-semibold\", getAvatarToneClasses(user.name))}>\n {getVietnameseInitials(user.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface UserMenuProps {\n className?: string;\n /** Hiện tên + vai trò cạnh avatar; màn hình hẹp nên để `false`. */\n showName?: boolean;\n /**\n * Gốc của Comitor.Account (vd `https://account.comitor.ai`, không dấu `/` cuối). Có giá trị thì menu\n * dựng sẵn BA mục trỏ sang Account — Hồ sơ (`/profile`), Tuỳ chọn hiển thị (`/preferences`), Bảo mật\n * (`/security`) — đứng đầu menu, trước mục của app. Chính app Account truyền chuỗi rỗng `\"\"` để ra\n * đường dẫn tương đối. `profileHref` vẫn thắng nếu app khai cả hai.\n *\n * Ba trang ấy là của DANH TÍNH dùng chung cho cả hệ sinh thái, nên mọi app hiện cùng ba mục, cùng\n * chữ, cùng thứ tự — gói giữ nhãn để không app nào phải dịch lại.\n */\n accountUrl?: string;\n /** Link hồ sơ (nhãn `profile`) — bỏ trống thì không render mục này (tránh mục dẫn đi đâu không rõ). */\n profileHref?: string;\n /** Link cài đặt (nhãn `settings`). */\n settingsHref?: string;\n /** Link trợ giúp (nhãn `help`). */\n helpHref?: string;\n /** Mục bổ sung của app — chèn ngay trên mục đăng xuất. */\n items?: UserMenuItem[];\n /**\n * Submenu đổi giao diện (nhãn `appearance`) trong menu người dùng.\n *\n * `\"mobile\"` (mặc định) — CHỈ hiện dưới `md`. Từ `md` trở lên `AppHeader` đã có\n * `ThemeToggle` riêng, để cả hai là hai lối vào cho cùng một thứ.\n * `true` — luôn hiện. Dùng khi app tắt `showThemeToggle` của header, nếu không màn\n * hình rộng sẽ KHÔNG còn chỗ nào đổi giao diện.\n * `false` — không bao giờ hiện.\n *\n * ⚠ Nhánh `\"mobile\"` ẩn bằng CSS (`md:hidden`), không phải bằng điều kiện render: nút gạt theo\n * bề rộng cần `window`, mà đọc `window` lúc render đầu là lệch hydration. `display: none` loại\n * phần tử khỏi CẢ cây trợ năng nên trình đọc màn hình ở desktop cũng không thấy mục thừa.\n */\n showThemeSubmenu?: boolean | \"mobile\";\n /** Ghi đè handler đăng xuất của context. */\n onLogout?: () => void;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * Menu người dùng ở góc phải header: avatar + tên/email, các mục do app khai báo,\n * submenu đổi giao diện, và mục đăng xuất (tô màu destructive).\n *\n * Mục dựng sẵn chỉ hiện khi app cấp `href` tương ứng — package không tự bịa route.\n */\nexport function UserMenu({\n className,\n showName = false,\n accountUrl,\n profileHref,\n settingsHref,\n helpHref,\n items = [],\n showThemeSubmenu = \"mobile\",\n onLogout,\n align = \"end\"\n}: UserMenuProps) {\n const shell = useShell();\n const { theme, setTheme } = useTheme();\n const { user, labels } = shell;\n\n if (!user) return null;\n\n const handleLogout = onLogout ?? shell.onLogout;\n\n const renderItem = (item: UserMenuItem) => {\n const Icon = item.icon;\n const content = (\n <>\n {Icon ? <Icon className=\"size-4\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n {item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}\n </>\n );\n\n if (item.href) {\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} asChild>\n <ShellNavLink href={item.href} onClick={() => item.onSelect?.()}>\n {content}\n </ShellNavLink>\n </DropdownMenuItem>\n );\n }\n\n return (\n <DropdownMenuItem key={item.id} variant={item.variant} onClick={() => item.onSelect?.()}>\n {content}\n </DropdownMenuItem>\n );\n };\n\n const builtInItems: (UserMenuItem | null)[] = [\n profileHref || accountUrl !== undefined\n ? { id: \"profile\", label: labels.profile, icon: CircleUser, href: profileHref ?? `${accountUrl}/profile` }\n : null,\n accountUrl !== undefined\n ? { id: \"preferences\", label: labels.preferences, icon: SlidersHorizontal, href: `${accountUrl}/preferences` }\n : null,\n accountUrl !== undefined\n ? { id: \"security\", label: labels.security, icon: ShieldCheck, href: `${accountUrl}/security` }\n : null,\n settingsHref ? { id: \"settings\", label: labels.settings, icon: Settings, href: settingsHref } : null,\n helpHref ? { id: \"help\", label: labels.help, icon: HelpCircle, href: helpHref } : null\n ];\n const visibleBuiltIns = builtInItems.filter((item): item is UserMenuItem => item !== null);\n\n const hasEntries = visibleBuiltIns.length > 0 || items.length > 0 || showThemeSubmenu;\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.account}\n className={cn(\n \"flex items-center gap-2 rounded-md p-1 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-muted\",\n className\n )}\n >\n <UserAvatar user={user} />\n {showName ? (\n <>\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">{user.name}</span>\n {user.role ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{user.role}</span>\n ) : null}\n </span>\n <ChevronDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n </button>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent align={align} className=\"w-60\">\n <DropdownMenuLabel className=\"flex items-center gap-2 font-normal\">\n <UserAvatar user={user} />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span> : null}\n {user.role ? <span className=\"truncate text-[11px] text-muted-foreground\">{user.role}</span> : null}\n </span>\n </DropdownMenuLabel>\n\n {hasEntries ? <DropdownMenuSeparator /> : null}\n\n {visibleBuiltIns.map(renderItem)}\n {items.map(renderItem)}\n\n {showThemeSubmenu !== false ? (\n <DropdownMenuSub>\n <DropdownMenuSubTrigger className={cn(showThemeSubmenu === \"mobile\" && \"md:hidden\")}>\n <Palette className=\"size-4\" />\n <span>{labels.appearance}</span>\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n <DropdownMenuItem onClick={() => setTheme(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => setTheme(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ) : null}\n\n {handleLogout ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onClick={handleLogout}>\n <LogOut className=\"size-4\" />\n <span>{labels.logout}</span>\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",