@comitor/ui 0.2.2 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +98 -20
  2. package/dist/components/ui/badge.js +3 -3
  3. package/dist/components/ui/badge.js.map +1 -1
  4. package/dist/components/ui/button.js +4 -4
  5. package/dist/components/ui/button.js.map +1 -1
  6. package/dist/components/ui/calendar.js +1 -1
  7. package/dist/components/ui/calendar.js.map +1 -1
  8. package/dist/components/ui/checkbox.js +2 -2
  9. package/dist/components/ui/checkbox.js.map +1 -1
  10. package/dist/components/ui/command.js +1 -1
  11. package/dist/components/ui/command.js.map +1 -1
  12. package/dist/components/ui/input-group.js +1 -1
  13. package/dist/components/ui/input-group.js.map +1 -1
  14. package/dist/components/ui/input-otp.js +1 -1
  15. package/dist/components/ui/input-otp.js.map +1 -1
  16. package/dist/components/ui/navigation-menu.js +1 -1
  17. package/dist/components/ui/navigation-menu.js.map +1 -1
  18. package/dist/components/ui/radio-group.js +1 -1
  19. package/dist/components/ui/radio-group.js.map +1 -1
  20. package/dist/components/ui/resizable.js +1 -1
  21. package/dist/components/ui/resizable.js.map +1 -1
  22. package/dist/components/ui/slider.js +1 -1
  23. package/dist/components/ui/slider.js.map +1 -1
  24. package/dist/components/ui/switch.js +1 -1
  25. package/dist/components/ui/switch.js.map +1 -1
  26. package/dist/components/ui/textarea.js +1 -1
  27. package/dist/components/ui/textarea.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/shell/app-header.d.ts +10 -2
  30. package/dist/shell/app-header.js +72 -67
  31. package/dist/shell/app-header.js.map +1 -1
  32. package/dist/shell/contrast-constants.d.ts +30 -0
  33. package/dist/shell/contrast-constants.js +16 -0
  34. package/dist/shell/contrast-constants.js.map +1 -0
  35. package/dist/shell/contrast-provider.d.ts +60 -0
  36. package/dist/shell/contrast-provider.js +61 -0
  37. package/dist/shell/contrast-provider.js.map +1 -0
  38. package/dist/shell/contrast-toggle.d.ts +24 -0
  39. package/dist/shell/contrast-toggle.js +29 -0
  40. package/dist/shell/contrast-toggle.js.map +1 -0
  41. package/dist/shell/index.d.ts +3 -0
  42. package/dist/shell/index.js +3 -0
  43. package/dist/shell/index.js.map +1 -1
  44. package/dist/shell/sidebar-menu-item.js +0 -3
  45. package/dist/shell/sidebar-menu-item.js.map +1 -1
  46. package/dist/tokens.d.ts +170 -70
  47. package/dist/tokens.js +124 -47
  48. package/dist/tokens.js.map +1 -1
  49. package/package.json +1 -1
  50. package/styles.css +255 -120
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/input-otp.tsx"],"sourcesContent":["\"use client\";\n\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * InputOTP — ô nhập mã OTP (input-otp). Dùng cho luồng 2FA / xác thực email.\n *\n * Khác bản comitor-ds: đọc slot bằng optional chaining thay vì `?? {}` — tsconfig\n * bật `noUncheckedIndexedAccess` nên `slots[index]` là `Slot | undefined`.\n */\n\nexport type InputOTPProps = React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string;\n};\nexport type InputOTPGroupProps = React.ComponentProps<\"div\">;\nexport type InputOTPSlotProps = React.ComponentProps<\"div\"> & { index: number };\nexport type InputOTPSeparatorProps = React.ComponentProps<\"div\">;\n\nfunction InputOTP({ className, containerClassName, ...props }: InputOTPProps) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\"flex items-center gap-2 has-disabled:opacity-50\", containerClassName)}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n );\n}\n\nfunction InputOTPGroup({ className, ...props }: InputOTPGroupProps) {\n return <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />;\n}\n\nfunction InputOTPSlot({ index, className, ...props }: InputOTPSlotProps) {\n const inputOTPContext = React.useContext(OTPInputContext);\n const slot = inputOTPContext?.slots[index];\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={slot?.isActive}\n className={cn(\n \"data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n className\n )}\n {...props}\n >\n {slot?.char}\n {slot?.hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction InputOTPSeparator({ ...props }: InputOTPSeparatorProps) {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: dấu phân cách tĩnh giữa các nhóm ô, không phải splitter kéo được nên không cần focus\n // biome-ignore lint/a11y/useAriaPropsForRole: separator tĩnh không có giá trị → không cần aria-valuenow\n <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n <MinusIcon />\n </div>\n );\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };\n"],"mappings":";AAuBI,cAkBA,YAlBA;AArBJ,SAAS,UAAU,uBAAuB;AAC1C,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,UAAU;AAgBnB,SAAS,SAAS,EAAE,WAAW,oBAAoB,GAAG,MAAM,GAAkB;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,oBAAoB,GAAG,mDAAmD,kBAAkB;AAAA,MAC5F,WAAW,GAAG,+BAA+B,SAAS;AAAA,MACrD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,cAAc,EAAE,WAAW,GAAG,MAAM,GAAuB;AAClE,SAAO,oBAAC,SAAI,aAAU,mBAAkB,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,OAAO;AACpG;AAEA,SAAS,aAAa,EAAE,OAAO,WAAW,GAAG,MAAM,GAAsB;AACvE,QAAM,kBAAkB,MAAM,WAAW,eAAe;AACxD,QAAM,OAAO,iBAAiB,MAAM,KAAK;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa,MAAM;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,cAAM;AAAA,QACN,MAAM,gBACL,oBAAC,SAAI,WAAU,yEACb,8BAAC,SAAI,WAAU,4DAA2D,GAC5E;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D;AAAA;AAAA;AAAA,IAGE,oBAAC,SAAI,aAAU,uBAAsB,MAAK,aAAa,GAAG,OACxD,8BAAC,aAAU,GACb;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/input-otp.tsx"],"sourcesContent":["\"use client\";\n\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * InputOTP — ô nhập mã OTP (input-otp). Dùng cho luồng 2FA / xác thực email.\n *\n * Khác bản comitor-ds: đọc slot bằng optional chaining thay vì `?? {}` — tsconfig\n * bật `noUncheckedIndexedAccess` nên `slots[index]` là `Slot | undefined`.\n */\n\nexport type InputOTPProps = React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string;\n};\nexport type InputOTPGroupProps = React.ComponentProps<\"div\">;\nexport type InputOTPSlotProps = React.ComponentProps<\"div\"> & { index: number };\nexport type InputOTPSeparatorProps = React.ComponentProps<\"div\">;\n\nfunction InputOTP({ className, containerClassName, ...props }: InputOTPProps) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\"flex items-center gap-2 has-disabled:opacity-50\", containerClassName)}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n );\n}\n\nfunction InputOTPGroup({ className, ...props }: InputOTPGroupProps) {\n return <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />;\n}\n\nfunction InputOTPSlot({ index, className, ...props }: InputOTPSlotProps) {\n const inputOTPContext = React.useContext(OTPInputContext);\n const slot = inputOTPContext?.slots[index];\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={slot?.isActive}\n className={cn(\n \"data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n className\n )}\n {...props}\n >\n {slot?.char}\n {slot?.hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction InputOTPSeparator({ ...props }: InputOTPSeparatorProps) {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: dấu phân cách tĩnh giữa các nhóm ô, không phải splitter kéo được nên không cần focus\n // biome-ignore lint/a11y/useAriaPropsForRole: separator tĩnh không có giá trị → không cần aria-valuenow\n <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n <MinusIcon />\n </div>\n );\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };\n"],"mappings":";AAuBI,cAkBA,YAlBA;AArBJ,SAAS,UAAU,uBAAuB;AAC1C,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,UAAU;AAgBnB,SAAS,SAAS,EAAE,WAAW,oBAAoB,GAAG,MAAM,GAAkB;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,oBAAoB,GAAG,mDAAmD,kBAAkB;AAAA,MAC5F,WAAW,GAAG,+BAA+B,SAAS;AAAA,MACrD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,cAAc,EAAE,WAAW,GAAG,MAAM,GAAuB;AAClE,SAAO,oBAAC,SAAI,aAAU,mBAAkB,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,OAAO;AACpG;AAEA,SAAS,aAAa,EAAE,OAAO,WAAW,GAAG,MAAM,GAAsB;AACvE,QAAM,kBAAkB,MAAM,WAAW,eAAe;AACxD,QAAM,OAAO,iBAAiB,MAAM,KAAK;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa,MAAM;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,cAAM;AAAA,QACN,MAAM,gBACL,oBAAC,SAAI,WAAU,yEACb,8BAAC,SAAI,WAAU,4DAA2D,GAC5E;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D;AAAA;AAAA;AAAA,IAGE,oBAAC,SAAI,aAAU,uBAAsB,MAAK,aAAa,GAAG,OACxD,8BAAC,aAAU,GACb;AAAA;AAEJ;","names":[]}
@@ -105,7 +105,7 @@ function NavigationMenuIndicator({ className, ...props }) {
105
105
  className
106
106
  ),
107
107
  ...props,
108
- children: /* @__PURE__ */ jsx("div", { className: "bg-primary-ink relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" })
108
+ children: /* @__PURE__ */ jsx("div", { className: "bg-control-edge relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" })
109
109
  }
110
110
  );
111
111
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/navigation-menu.tsx"],"sourcesContent":["import * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * NavigationMenu — thanh điều hướng có menu bung (Radix NavigationMenu).\n *\n * Dùng cho site marketing / trang công khai. Trong app đã đăng nhập thì điều hướng\n * là Sidebar + AppLauncher, không dùng component này.\n *\n * KHÔNG có directive \"use client\": file chỉ khai báo bố cục, phần state nằm trong\n * gói Radix (bản thân gói đã đánh dấu client) → dùng được trực tiếp trong RSC.\n *\n * `viewport` (mặc định true): nội dung menu render vào một khung chung phía dưới\n * thanh, có hiệu ứng đổi kích thước. Đặt false khi muốn mỗi mục tự bung popover\n * riêng — style tương ứng bám theo `group-data-[viewport=false]`.\n */\n\nexport type NavigationMenuProps = React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {\n viewport?: boolean;\n};\n\nfunction NavigationMenu({ className, children, viewport = true, ...props }: NavigationMenuProps) {\n return (\n <NavigationMenuPrimitive.Root\n data-slot=\"navigation-menu\"\n data-viewport={viewport}\n className={cn(\"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center\", className)}\n {...props}\n >\n {children}\n {viewport && <NavigationMenuViewport />}\n </NavigationMenuPrimitive.Root>\n );\n}\n\nexport type NavigationMenuListProps = React.ComponentProps<typeof NavigationMenuPrimitive.List>;\n\nfunction NavigationMenuList({ className, ...props }: NavigationMenuListProps) {\n return (\n <NavigationMenuPrimitive.List\n data-slot=\"navigation-menu-list\"\n className={cn(\"group flex flex-1 list-none items-center justify-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuItemProps = React.ComponentProps<typeof NavigationMenuPrimitive.Item>;\n\nfunction NavigationMenuItem({ className, ...props }: NavigationMenuItemProps) {\n return (\n <NavigationMenuPrimitive.Item data-slot=\"navigation-menu-item\" className={cn(\"relative\", className)} {...props} />\n );\n}\n\n/** Style dùng lại cho mục điều hướng không có menu con (vd bọc thẻ <a>). */\nconst navigationMenuTriggerStyle = cva(\n \"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 outline-none transition-colors duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n);\n\nexport type NavigationMenuTriggerProps = React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>;\n\nfunction NavigationMenuTrigger({ className, children, ...props }: NavigationMenuTriggerProps) {\n return (\n <NavigationMenuPrimitive.Trigger\n data-slot=\"navigation-menu-trigger\"\n className={cn(navigationMenuTriggerStyle(), \"group\", className)}\n {...props}\n >\n {children}{\" \"}\n <ChevronDownIcon\n className=\"relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180\"\n aria-hidden=\"true\"\n />\n </NavigationMenuPrimitive.Trigger>\n );\n}\n\nexport type NavigationMenuContentProps = React.ComponentProps<typeof NavigationMenuPrimitive.Content>;\n\nfunction NavigationMenuContent({ className, ...props }: NavigationMenuContentProps) {\n return (\n <NavigationMenuPrimitive.Content\n data-slot=\"navigation-menu-content\"\n className={cn(\n \"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto\",\n \"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow-overlay group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuViewportProps = React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>;\n\nfunction NavigationMenuViewport({ className, ...props }: NavigationMenuViewportProps) {\n return (\n <div className=\"absolute top-full left-0 isolate z-50 flex justify-center\">\n <NavigationMenuPrimitive.Viewport\n data-slot=\"navigation-menu-viewport\"\n className={cn(\n \"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow-overlay md:w-[var(--radix-navigation-menu-viewport-width)]\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type NavigationMenuLinkProps = React.ComponentProps<typeof NavigationMenuPrimitive.Link>;\n\nfunction NavigationMenuLink({ className, ...props }: NavigationMenuLinkProps) {\n return (\n <NavigationMenuPrimitive.Link\n data-slot=\"navigation-menu-link\"\n className={cn(\n \"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-colors duration-150 ease-out outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuIndicatorProps = React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>;\n\nfunction NavigationMenuIndicator({ className, ...props }: NavigationMenuIndicatorProps) {\n return (\n <NavigationMenuPrimitive.Indicator\n data-slot=\"navigation-menu-indicator\"\n className={cn(\n \"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden\",\n className\n )}\n {...props}\n >\n {/*\n Hình vuông xoay 45° chỉ vào trigger ĐANG MỞ — theo Radix đây là dấu \"highlight the\n currently active trigger\", tức thông tin trạng thái, ngưỡng 3:1 (WCAG 1.4.11).\n `bg-border` là bậc TRANG TRÍ (1,16:1 light · 1,17:1 dark với --background) nên dấu này\n vốn không hiện ở cả hai theme. Dùng `--primary-ink` — cùng token mà `Tabs` dùng cho vạch\n active (`data-[state=active]:border-primary-ink`): 6,51:1 (light) · 12,67:1 (dark) trên\n --background, 6,09:1 · 10,80:1 trên --muted ✓\n */}\n <div className=\"bg-primary-ink relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\" />\n </NavigationMenuPrimitive.Indicator>\n );\n}\n\nexport {\n NavigationMenu,\n NavigationMenuContent,\n NavigationMenuIndicator,\n NavigationMenuItem,\n NavigationMenuLink,\n NavigationMenuList,\n NavigationMenuTrigger,\n NavigationMenuViewport,\n navigationMenuTriggerStyle\n};\n"],"mappings":"AA0BI,SAOe,KAPf;AA1BJ,YAAY,6BAA6B;AACzC,SAAS,WAAW;AACpB,SAAS,uBAAuB;AAEhC,SAAS,UAAU;AAoBnB,SAAS,eAAe,EAAE,WAAW,UAAU,WAAW,MAAM,GAAG,MAAM,GAAwB;AAC/F,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,iBAAe;AAAA,MACf,WAAW,GAAG,oFAAoF,SAAS;AAAA,MAC1G,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,YAAY,oBAAC,0BAAuB;AAAA;AAAA;AAAA,EACvC;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iEAAiE,SAAS;AAAA,MACvF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE,oBAAC,wBAAwB,MAAxB,EAA6B,aAAU,wBAAuB,WAAW,GAAG,YAAY,SAAS,GAAI,GAAG,OAAO;AAEpH;AAGA,MAAM,6BAA6B;AAAA,EACjC;AACF;AAIA,SAAS,sBAAsB,EAAE,WAAW,UAAU,GAAG,MAAM,GAA+B;AAC5F,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2BAA2B,GAAG,SAAS,SAAS;AAAA,MAC7D,GAAG;AAAA,MAEH;AAAA;AAAA,QAAU;AAAA,QACX;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA;AAAA,QACd;AAAA;AAAA;AAAA,EACF;AAEJ;AAIA,SAAS,sBAAsB,EAAE,WAAW,GAAG,MAAM,GAA+B;AAClF,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,uBAAuB,EAAE,WAAW,GAAG,MAAM,GAAgC;AACpF,SACE,oBAAC,SAAI,WAAU,6DACb;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN,GACF;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,wBAAwB,EAAE,WAAW,GAAG,MAAM,GAAiC;AACtF,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAUJ,8BAAC,SAAI,WAAU,+EAA8E;AAAA;AAAA,EAC/F;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/navigation-menu.tsx"],"sourcesContent":["import * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * NavigationMenu — thanh điều hướng có menu bung (Radix NavigationMenu).\n *\n * Dùng cho site marketing / trang công khai. Trong app đã đăng nhập thì điều hướng\n * là Sidebar + AppLauncher, không dùng component này.\n *\n * KHÔNG có directive \"use client\": file chỉ khai báo bố cục, phần state nằm trong\n * gói Radix (bản thân gói đã đánh dấu client) → dùng được trực tiếp trong RSC.\n *\n * `viewport` (mặc định true): nội dung menu render vào một khung chung phía dưới\n * thanh, có hiệu ứng đổi kích thước. Đặt false khi muốn mỗi mục tự bung popover\n * riêng — style tương ứng bám theo `group-data-[viewport=false]`.\n */\n\nexport type NavigationMenuProps = React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {\n viewport?: boolean;\n};\n\nfunction NavigationMenu({ className, children, viewport = true, ...props }: NavigationMenuProps) {\n return (\n <NavigationMenuPrimitive.Root\n data-slot=\"navigation-menu\"\n data-viewport={viewport}\n className={cn(\"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center\", className)}\n {...props}\n >\n {children}\n {viewport && <NavigationMenuViewport />}\n </NavigationMenuPrimitive.Root>\n );\n}\n\nexport type NavigationMenuListProps = React.ComponentProps<typeof NavigationMenuPrimitive.List>;\n\nfunction NavigationMenuList({ className, ...props }: NavigationMenuListProps) {\n return (\n <NavigationMenuPrimitive.List\n data-slot=\"navigation-menu-list\"\n className={cn(\"group flex flex-1 list-none items-center justify-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuItemProps = React.ComponentProps<typeof NavigationMenuPrimitive.Item>;\n\nfunction NavigationMenuItem({ className, ...props }: NavigationMenuItemProps) {\n return (\n <NavigationMenuPrimitive.Item data-slot=\"navigation-menu-item\" className={cn(\"relative\", className)} {...props} />\n );\n}\n\n/** Style dùng lại cho mục điều hướng không có menu con (vd bọc thẻ <a>). */\nconst navigationMenuTriggerStyle = cva(\n \"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 outline-none transition-colors duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n);\n\nexport type NavigationMenuTriggerProps = React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>;\n\nfunction NavigationMenuTrigger({ className, children, ...props }: NavigationMenuTriggerProps) {\n return (\n <NavigationMenuPrimitive.Trigger\n data-slot=\"navigation-menu-trigger\"\n className={cn(navigationMenuTriggerStyle(), \"group\", className)}\n {...props}\n >\n {children}{\" \"}\n <ChevronDownIcon\n className=\"relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180\"\n aria-hidden=\"true\"\n />\n </NavigationMenuPrimitive.Trigger>\n );\n}\n\nexport type NavigationMenuContentProps = React.ComponentProps<typeof NavigationMenuPrimitive.Content>;\n\nfunction NavigationMenuContent({ className, ...props }: NavigationMenuContentProps) {\n return (\n <NavigationMenuPrimitive.Content\n data-slot=\"navigation-menu-content\"\n className={cn(\n \"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto\",\n \"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow-overlay group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuViewportProps = React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>;\n\nfunction NavigationMenuViewport({ className, ...props }: NavigationMenuViewportProps) {\n return (\n <div className=\"absolute top-full left-0 isolate z-50 flex justify-center\">\n <NavigationMenuPrimitive.Viewport\n data-slot=\"navigation-menu-viewport\"\n className={cn(\n \"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow-overlay md:w-[var(--radix-navigation-menu-viewport-width)]\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type NavigationMenuLinkProps = React.ComponentProps<typeof NavigationMenuPrimitive.Link>;\n\nfunction NavigationMenuLink({ className, ...props }: NavigationMenuLinkProps) {\n return (\n <NavigationMenuPrimitive.Link\n data-slot=\"navigation-menu-link\"\n className={cn(\n \"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-colors duration-150 ease-out outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type NavigationMenuIndicatorProps = React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>;\n\nfunction NavigationMenuIndicator({ className, ...props }: NavigationMenuIndicatorProps) {\n return (\n <NavigationMenuPrimitive.Indicator\n data-slot=\"navigation-menu-indicator\"\n className={cn(\n \"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden\",\n className\n )}\n {...props}\n >\n {/*\n Hình vuông xoay 45° chỉ vào trigger ĐANG MỞ — theo Radix đây là dấu \"highlight the\n currently active trigger\", tức thông tin trạng thái, ngưỡng 3:1 (WCAG 1.4.11).\n `bg-border` là bậc TRANG TRÍ (1,16:1 light · 1,17:1 dark với --background) nên dấu này\n vốn không hiện ở cả hai theme. Dùng `--primary-ink` — cùng token mà `Tabs` dùng cho vạch\n active (`data-[state=active]:border-primary-ink`): 6,51:1 (light) · 12,67:1 (dark) trên\n --background, 6,09:1 · 10,80:1 trên --muted ✓\n */}\n <div className=\"bg-control-edge relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\" />\n </NavigationMenuPrimitive.Indicator>\n );\n}\n\nexport {\n NavigationMenu,\n NavigationMenuContent,\n NavigationMenuIndicator,\n NavigationMenuItem,\n NavigationMenuLink,\n NavigationMenuList,\n NavigationMenuTrigger,\n NavigationMenuViewport,\n navigationMenuTriggerStyle\n};\n"],"mappings":"AA0BI,SAOe,KAPf;AA1BJ,YAAY,6BAA6B;AACzC,SAAS,WAAW;AACpB,SAAS,uBAAuB;AAEhC,SAAS,UAAU;AAoBnB,SAAS,eAAe,EAAE,WAAW,UAAU,WAAW,MAAM,GAAG,MAAM,GAAwB;AAC/F,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,iBAAe;AAAA,MACf,WAAW,GAAG,oFAAoF,SAAS;AAAA,MAC1G,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,YAAY,oBAAC,0BAAuB;AAAA;AAAA;AAAA,EACvC;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iEAAiE,SAAS;AAAA,MACvF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE,oBAAC,wBAAwB,MAAxB,EAA6B,aAAU,wBAAuB,WAAW,GAAG,YAAY,SAAS,GAAI,GAAG,OAAO;AAEpH;AAGA,MAAM,6BAA6B;AAAA,EACjC;AACF;AAIA,SAAS,sBAAsB,EAAE,WAAW,UAAU,GAAG,MAAM,GAA+B;AAC5F,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2BAA2B,GAAG,SAAS,SAAS;AAAA,MAC7D,GAAG;AAAA,MAEH;AAAA;AAAA,QAAU;AAAA,QACX;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA;AAAA,QACd;AAAA;AAAA;AAAA,EACF;AAEJ;AAIA,SAAS,sBAAsB,EAAE,WAAW,GAAG,MAAM,GAA+B;AAClF,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,uBAAuB,EAAE,WAAW,GAAG,MAAM,GAAgC;AACpF,SACE,oBAAC,SAAI,WAAU,6DACb;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN,GACF;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,wBAAwB,EAAE,WAAW,GAAG,MAAM,GAAiC;AACtF,SACE;AAAA,IAAC,wBAAwB;AAAA,IAAxB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAUJ,8BAAC,SAAI,WAAU,gFAA+E;AAAA;AAAA,EAChG;AAEJ;","names":[]}
@@ -12,7 +12,7 @@ function RadioGroupItem({ className, ...props }) {
12
12
  {
13
13
  "data-slot": "radio-group-item",
14
14
  className: cn(
15
- "border-input text-control-on focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-colors duration-150 ease-out outline-none disabled:cursor-not-allowed disabled:opacity-50",
15
+ "border-input text-control-on focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink dark:bg-input-fill aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-colors duration-150 ease-out outline-none disabled:cursor-not-allowed disabled:opacity-50",
16
16
  className
17
17
  ),
18
18
  ...props,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { CircleIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * ─ CHẤM ĐÃ CHỌN: `--control-on`, KHÔNG PHẢI `--primary` ─\n *\n * Ô radio để nền trong suốt, nên \"đã chọn hay chưa\" chỉ còn được kể bằng cái CHẤM ở giữa —\n * thông tin thị giác đúng nghĩa của WCAG 1.4.11, ngưỡng 3:1. `fill-primary` (gold #E8B824)\n * trên nền trắng chỉ **1,86:1** ✗: ở light người dùng nhìn ô đã chọn gần như y hệt ô chưa chọn.\n * (Ở dark thì lọt lưới nên lỗi này chỉ lộ ở một nửa số theme.)\n * `--control-on` (= `--primary-ink`) là token của vai trò \"control ở trạng thái BẬT/ĐÃ CHỌN\".\n * Đo với nền TRANG, lấy ca tệ nhất trong 5 nền (`--muted`) và ca thường gặp (`--background`):\n *\n * chấm ⇄ nền trang : 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) ✓\n * viền ô `border-input` ⇄ nền trang : 3,83:1 (light) · 4,21–4,94:1 (dark) ✓\n *\n * ─ VÌ SAO BỎ `dark:bg-input/30` ─\n * `--input` là alias của `--control-edge` — token VIỀN control (hợp đồng màu §3), không phải bậc\n * tô nền. Đem nó tô ruột ở dark vừa sai vai trò, vừa nâng mặt nền LÊN SÁNG hơn mọi nền trang, kéo\n * `--muted-foreground` (token chữ, hiệu chỉnh theo nền TRANG — §6) xuống dưới ngưỡng ở mọi nền:\n * 3,81 · 3,54 · 3,22:1 ✗. Bỏ lớp tô thì ô radio vẫn được khoanh vùng bằng chính viền `border-input`\n * (4,21–4,94:1 ở dark, thừa ngưỡng 3:1), và chấm đã chọn còn nổi hơn trước (8,47 → 10,80–12,67:1).\n *\n * `text-control-on` trên Item giữ cho `fill-current`/icon do app truyền vào cũng đúng màu.\n * Viền lỗi dùng `--destructive-ink` (7,15:1 / 6,84:1) thay `--destructive` (5,03:1 nhưng chỉ\n * 3,76:1 ở dark) — xem hợp đồng màu §1: bậc đi lên nền TRANG luôn là `-ink`.\n */\nexport type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;\nexport type RadioGroupItemProps = React.ComponentProps<typeof RadioGroupPrimitive.Item>;\n\nfunction RadioGroup({ className, ...props }: RadioGroupProps) {\n return <RadioGroupPrimitive.Root data-slot=\"radio-group\" className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nfunction RadioGroupItem({ className, ...props }: RadioGroupItemProps) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"border-input text-control-on focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-colors duration-150 ease-out outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"relative flex items-center justify-center\"\n >\n <CircleIcon className=\"fill-control-on absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"],"mappings":";AAmCS;AAjCT,YAAY,yBAAyB;AACrC,SAAS,kBAAkB;AAE3B,SAAS,UAAU;AA6BnB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,oBAAoB,MAApB,EAAyB,aAAU,eAAc,WAAW,GAAG,cAAc,SAAS,GAAI,GAAG,OAAO;AAC9G;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,oBAAoB;AAAA,QAApB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAEV,8BAAC,cAAW,WAAU,sFAAqF;AAAA;AAAA,MAC7G;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { CircleIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * ─ CHẤM ĐÃ CHỌN: `--control-on`, KHÔNG PHẢI `--primary` ─\n *\n * Ô radio để nền trong suốt, nên \"đã chọn hay chưa\" chỉ còn được kể bằng cái CHẤM ở giữa —\n * thông tin thị giác đúng nghĩa của WCAG 1.4.11, ngưỡng 3:1. `fill-primary` (gold #E8B824)\n * trên nền trắng chỉ **1,86:1** ✗: ở light người dùng nhìn ô đã chọn gần như y hệt ô chưa chọn.\n * (Ở dark thì lọt lưới nên lỗi này chỉ lộ ở một nửa số theme.)\n * `--control-on` là token của vai trò \"control ở trạng thái BẬT/ĐÃ CHỌN\"; giá trị của nó đổi theo\n * bảng màu (styles.css khối 5b), nên ĐỪNG viết thẳng `fill-primary` / `bg-gold-300` ở đây.\n * Đo với nền TRANG, lấy ca tệ nhất trong 5 nền (`--muted`) và ca thường gặp (`--background`):\n *\n * bảng MẶC ĐỊNH chấm ⇄ nền trang : 1,73–1,86:1 ✗ (bản duyệt, đã chấp nhận)\n * bảng TƯƠNG PHẢN CAO chấm ⇄ nền trang : 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) ✓\n *\n * ─ LỚP TÔ Ở DARK: `dark:bg-input-fill` ─\n * comitor-ds viết `dark:bg-input/30`, và ở bảng mặc định lớp tô đó là thứ DUY NHẤT còn khoanh\n * vùng ô radio: viền `--input` (#1E2128) trên `--card` (#16181D) chỉ 1,10:1. Nhưng viết thẳng\n * `bg-input/30` thì ở bảng tương phản cao (`--input` = #7E8290) lớp tô sáng hơn mọi nền trang và\n * kéo `--muted-foreground` xuống 3,81 · 3,54 · 3,22:1 ✗ — nên nó đi qua `--input-fill`, biến này\n * trong suốt ở bảng tương phản cao (bên đó viền tự đủ đậm: 4,94:1).\n *\n * `text-control-on` trên Item giữ cho `fill-current`/icon do app truyền vào cũng đúng màu.\n * Viền lỗi dùng `--destructive-ink`: ở bảng mặc định nó bằng `--destructive` (đúng `border-red`\n * của comitor-ds), ở bảng tương phản cao nó là bậc tối riêng (7,15:1 / 6,84:1) — xem hợp đồng\n * màu §1: bậc đi lên nền TRANG luôn là `-ink`.\n */\nexport type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;\nexport type RadioGroupItemProps = React.ComponentProps<typeof RadioGroupPrimitive.Item>;\n\nfunction RadioGroup({ className, ...props }: RadioGroupProps) {\n return <RadioGroupPrimitive.Root data-slot=\"radio-group\" className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nfunction RadioGroupItem({ className, ...props }: RadioGroupItemProps) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"border-input text-control-on focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink dark:bg-input-fill aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-colors duration-150 ease-out outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"relative flex items-center justify-center\"\n >\n <CircleIcon className=\"fill-control-on absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"],"mappings":";AAqCS;AAnCT,YAAY,yBAAyB;AACrC,SAAS,kBAAkB;AAE3B,SAAS,UAAU;AA+BnB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SAAO,oBAAC,oBAAoB,MAApB,EAAyB,aAAU,eAAc,WAAW,GAAG,cAAc,SAAS,GAAI,GAAG,OAAO;AAC9G;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC,oBAAoB;AAAA,IAApB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,oBAAoB;AAAA,QAApB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAEV,8BAAC,cAAW,WAAU,sFAAqF;AAAA;AAAA,MAC7G;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -26,7 +26,7 @@ function ResizableHandle({ withHandle, className, ...props }) {
26
26
  className
27
27
  ),
28
28
  ...props,
29
- children: withHandle && /* @__PURE__ */ jsx("div", { className: "bg-control-edge z-10 flex h-4 w-3 items-center justify-center rounded-xs [&_svg]:text-control-knob", children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-2.5" }) })
29
+ children: withHandle && /* @__PURE__ */ jsx("div", { className: "bg-control-edge z-10 flex h-4 w-3 items-center justify-center rounded-xs border", children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-2.5" }) })
30
30
  }
31
31
  );
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Resizable (react-resizable-panels) — bố cục nhiều panel kéo giãn được.\n * Dùng cho layout 3 cột kiểu Linear/Lark (danh sách ▸ chi tiết ▸ panel phụ).\n * Muốn nhớ kích thước: đặt `autoSaveId` trên `ResizablePanelGroup`.\n *\n * ─ MÀU: `--control-edge`, KHÔNG PHẢI `--border` ─\n * Thanh chia này là CONTROL THẬT chứ không phải đường kẻ trang trí: react-resizable-panels\n * render nó với `role=\"separator\"` + `tabIndex=0`, kéo được bằng chuột lẫn bàn phím. Nên nó\n * chịu ngưỡng 3:1 của WCAG 1.4.11, và theo hợp đồng ở styles.css §RANH GIỚI thì ranh giới\n * control phải đi qua `--control-edge` — `--border` là bậc TRANG TRÍ, cố ý nhạt:\n *\n * `bg-border` ⇄ --background 1,16:1 (light) · 1,17:1 (dark) ✗\n * ⇄ --card 1,16:1 · 1,10:1 ✗ · ⇄ --muted 1,08:1 · 1,00:1 ✗\n * `bg-control-edge` ⇄ --background 3,83:1 · 4,94:1 ✓ · ⇄ --card 3,83:1 · 4,64:1 ✓\n * ⇄ --muted 3,58:1 · 4,21:1 ✓\n *\n * Núm `withHandle` cũng vậy: tô đặc `bg-control-edge` (tự khoanh vùng, không cần `border`),\n * vạch grip bên trong lấy `--control-knob` (= nền trang) — 3,83:1 / 4,94:1 trên núm ✓.\n */\n\nexport type ResizablePanelGroupProps = React.ComponentProps<typeof ResizablePrimitive.PanelGroup>;\nexport type ResizablePanelProps = React.ComponentProps<typeof ResizablePrimitive.Panel>;\nexport type ResizableHandleProps = React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n /** Hiện tay nắm có gạch dọc ở giữa thanh chia. */\n withHandle?: boolean;\n};\n\nfunction ResizablePanelGroup({ className, ...props }: ResizablePanelGroupProps) {\n return (\n <ResizablePrimitive.PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePanelProps) {\n return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({ withHandle, className, ...props }: ResizableHandleProps) {\n return (\n <ResizablePrimitive.PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"bg-control-edge relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-control-edge z-10 flex h-4 w-3 items-center justify-center rounded-xs [&_svg]:text-control-knob\">\n <GripVerticalIcon className=\"size-2.5\" />\n </div>\n )}\n </ResizablePrimitive.PanelResizeHandle>\n );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"],"mappings":";AAoCI;AAlCJ,SAAS,wBAAwB;AAEjC,YAAY,wBAAwB;AACpC,SAAS,UAAU;AA6BnB,SAAS,oBAAoB,EAAE,WAAW,GAAG,MAAM,GAA6B;AAC9E,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,qEAAqE,SAAS;AAAA,MAC3F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe,EAAE,GAAG,MAAM,GAAwB;AACzD,SAAO,oBAAC,mBAAmB,OAAnB,EAAyB,aAAU,mBAAmB,GAAG,OAAO;AAC1E;AAEA,SAAS,gBAAgB,EAAE,YAAY,WAAW,GAAG,MAAM,GAAyB;AAClF,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH,wBACC,oBAAC,SAAI,WAAU,sGACb,8BAAC,oBAAiB,WAAU,YAAW,GACzC;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/resizable.tsx"],"sourcesContent":["\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Resizable (react-resizable-panels) — bố cục nhiều panel kéo giãn được.\n * Dùng cho layout 3 cột kiểu Linear/Lark (danh sách ▸ chi tiết ▸ panel phụ).\n * Muốn nhớ kích thước: đặt `autoSaveId` trên `ResizablePanelGroup`.\n *\n * ─ MÀU: `--control-edge`, KHÔNG PHẢI `--border` ─\n * Thanh chia này là CONTROL THẬT chứ không phải đường kẻ trang trí: react-resizable-panels\n * render nó với `role=\"separator\"` + `tabIndex=0`, kéo được bằng chuột lẫn bàn phím. Nên nó\n * chịu ngưỡng 3:1 của WCAG 1.4.11, và theo hợp đồng ở styles.css §RANH GIỚI thì ranh giới\n * control phải đi qua `--control-edge` — `--border` là bậc TRANG TRÍ, cố ý nhạt:\n *\n * `bg-border` ⇄ --background 1,16:1 (light) · 1,17:1 (dark) ✗\n * ⇄ --card 1,16:1 · 1,10:1 ✗ · ⇄ --muted 1,08:1 · 1,00:1 ✗\n * `bg-control-edge` ⇄ --background 3,83:1 · 4,94:1 ✓ · ⇄ --card 3,83:1 · 4,64:1 ✓\n * ⇄ --muted 3,58:1 · 4,21:1 ✓\n *\n * Núm `withHandle` cũng vậy: tô đặc `bg-control-edge` (tự khoanh vùng, không cần `border`),\n * vạch grip bên trong lấy `--control-knob` (= nền trang) — 3,83:1 / 4,94:1 trên núm ✓.\n */\n\nexport type ResizablePanelGroupProps = React.ComponentProps<typeof ResizablePrimitive.PanelGroup>;\nexport type ResizablePanelProps = React.ComponentProps<typeof ResizablePrimitive.Panel>;\nexport type ResizableHandleProps = React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n /** Hiện tay nắm có gạch dọc ở giữa thanh chia. */\n withHandle?: boolean;\n};\n\nfunction ResizablePanelGroup({ className, ...props }: ResizablePanelGroupProps) {\n return (\n <ResizablePrimitive.PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePanelProps) {\n return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({ withHandle, className, ...props }: ResizableHandleProps) {\n return (\n <ResizablePrimitive.PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"bg-control-edge relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-control-edge z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n <GripVerticalIcon className=\"size-2.5\" />\n </div>\n )}\n </ResizablePrimitive.PanelResizeHandle>\n );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"],"mappings":";AAoCI;AAlCJ,SAAS,wBAAwB;AAEjC,YAAY,wBAAwB;AACpC,SAAS,UAAU;AA6BnB,SAAS,oBAAoB,EAAE,WAAW,GAAG,MAAM,GAA6B;AAC9E,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,qEAAqE,SAAS;AAAA,MAC3F,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe,EAAE,GAAG,MAAM,GAAwB;AACzD,SAAO,oBAAC,mBAAmB,OAAnB,EAAyB,aAAU,mBAAmB,GAAG,OAAO;AAC1E;AAEA,SAAS,gBAAgB,EAAE,YAAY,WAAW,GAAG,MAAM,GAAyB;AAClF,SACE;AAAA,IAAC,mBAAmB;AAAA,IAAnB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH,wBACC,oBAAC,SAAI,WAAU,mFACb,8BAAC,oBAAiB,WAAU,YAAW,GACzC;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -26,7 +26,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
26
26
  SliderPrimitive.Track,
27
27
  {
28
28
  "data-slot": "slider-track",
29
- className: "bg-control-edge relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
29
+ className: "bg-slider-track relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
30
30
  children: /* @__PURE__ */ jsx(
31
31
  SliderPrimitive.Range,
32
32
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Slider (Radix) — hỗ trợ 1 hoặc nhiều thumb (range) và cả hướng dọc.\n * Số thumb suy ra từ `value`/`defaultValue`; không truyền gì thì mặc định 2 thumb\n * ứng với [min, max].\n *\n * ─ MÀU: CÙNG BỘ TOKEN VỚI `Switch`/`Checkbox` ─\n *\n * Thanh trượt là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1). Bản trước hỏng cả ba phía\n * ở light: rãnh `bg-muted` chỉ **1,07:1** với nền trắng (không nhìn thấy cái slider), đoạn đã\n * chọn `bg-primary` **1,86:1** với nền và **1,73:1** với rãnh, núm `bg-white` **1,86:1** với\n * đoạn đã chọn — và `bg-white` cứng thì ở dark núm ăn 1,49:1 với đoạn gold sáng.\n *\n * Nay đúng bộ ba của hợp đồng màu (docs/hop-dong-mau.md §3–§4):\n *\n * rãnh `bg-control-edge` : 3,83:1 (light) · 4,94:1 (dark) với nền trang ✓\n * đã chọn `bg-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n * núm `bg-control-knob` : 6,51 / 12,67:1 với đoạn đã chọn · 3,83 / 4,94:1 với rãnh ✓\n * viền núm `border-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n *\n * ⚠ Đoạn ĐÃ CHỌN ⇄ rãnh chỉ 1,70:1 (light) / 2,57:1 (dark) — giống hệt cặp rãnh BẬT ⇄ rãnh TẮT\n * của `Switch`, và chấp nhận vì cùng lý do: VỊ TRÍ NÚM mới là thứ mang giá trị, còn hai màu kia\n * đều tự đạt ≥3:1 với nền trang và với núm. Với nền trang trắng thì không tồn tại cặp màu nào\n * vừa ≥3:1 với nhau vừa ≥3:1 với nền.\n */\n\nexport type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root>;\n\nfunction Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderProps) {\n const _values = React.useMemo(\n () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),\n [value, defaultValue, min, max]\n );\n\n return (\n <SliderPrimitive.Root\n data-slot=\"slider\"\n defaultValue={defaultValue}\n value={value}\n min={min}\n max={max}\n className={cn(\n \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n className\n )}\n {...props}\n >\n <SliderPrimitive.Track\n data-slot=\"slider-track\"\n className=\"bg-control-edge relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\"\n >\n <SliderPrimitive.Range\n data-slot=\"slider-range\"\n className=\"bg-control-on absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n />\n </SliderPrimitive.Track>\n {Array.from({ length: _values.length }, (_, index) => (\n <SliderPrimitive.Thumb\n data-slot=\"slider-thumb\"\n // biome-ignore lint/suspicious/noArrayIndexKey: thumb chỉ phân biệt bằng vị trí, không có id\n key={index}\n className=\"border-control-on ring-ring/50 block size-4 shrink-0 rounded-full border bg-control-knob shadow-sm transition-colors duration-150 ease-out hover:ring-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\"\n />\n ))}\n </SliderPrimitive.Root>\n );\n}\n\nexport { Slider };\n"],"mappings":";AAwCI,SAgBI,KAhBJ;AAtCJ,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AA6BnB,SAAS,OAAO,EAAE,WAAW,cAAc,OAAO,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,GAAgB;AAC7F,QAAM,UAAU,MAAM;AAAA,IACpB,MAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,KAAK,GAAG;AAAA,IAC5F,CAAC,OAAO,cAAc,KAAK,GAAG;AAAA,EAChC;AAEA,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YACV,WAAU;AAAA,YAEV;AAAA,cAAC,gBAAgB;AAAA,cAAhB;AAAA,gBACC,aAAU;AAAA,gBACV,WAAU;AAAA;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,GAAG,CAAC,GAAG,UAC1C;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YAGV,WAAU;AAAA;AAAA,UADL;AAAA,QAEP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/slider.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Slider (Radix) — hỗ trợ 1 hoặc nhiều thumb (range) và cả hướng dọc.\n * Số thumb suy ra từ `value`/`defaultValue`; không truyền gì thì mặc định 2 thumb\n * ứng với [min, max].\n *\n * ─ MÀU: CÙNG BỘ TOKEN VỚI `Switch`/`Checkbox` ─\n *\n * Thanh trượt là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1). Bản trước hỏng cả ba phía\n * ở light: rãnh `bg-muted` chỉ **1,07:1** với nền trắng (không nhìn thấy cái slider), đoạn đã\n * chọn `bg-primary` **1,86:1** với nền và **1,73:1** với rãnh, núm `bg-white` **1,86:1** với\n * đoạn đã chọn — và `bg-white` cứng thì ở dark núm ăn 1,49:1 với đoạn gold sáng.\n *\n * Nay đúng bộ ba của hợp đồng màu (docs/hop-dong-mau.md §3–§4):\n *\n * rãnh `bg-control-edge` : 3,83:1 (light) · 4,94:1 (dark) với nền trang ✓\n * đã chọn `bg-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n * núm `bg-control-knob` : 6,51 / 12,67:1 với đoạn đã chọn · 3,83 / 4,94:1 với rãnh ✓\n * viền núm `border-control-on` : 6,51:1 (light) · 12,67:1 (dark) với nền trang ✓\n *\n * ⚠ Đoạn ĐÃ CHỌN ⇄ rãnh chỉ 1,70:1 (light) / 2,57:1 (dark) — giống hệt cặp rãnh BẬT ⇄ rãnh TẮT\n * của `Switch`, và chấp nhận vì cùng lý do: VỊ TRÍ NÚM mới là thứ mang giá trị, còn hai màu kia\n * đều tự đạt ≥3:1 với nền trang và với núm. Với nền trang trắng thì không tồn tại cặp màu nào\n * vừa ≥3:1 với nhau vừa ≥3:1 với nền.\n */\n\nexport type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root>;\n\nfunction Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderProps) {\n const _values = React.useMemo(\n () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),\n [value, defaultValue, min, max]\n );\n\n return (\n <SliderPrimitive.Root\n data-slot=\"slider\"\n defaultValue={defaultValue}\n value={value}\n min={min}\n max={max}\n className={cn(\n \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n className\n )}\n {...props}\n >\n <SliderPrimitive.Track\n data-slot=\"slider-track\"\n className=\"bg-slider-track relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\"\n >\n <SliderPrimitive.Range\n data-slot=\"slider-range\"\n className=\"bg-control-on absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n />\n </SliderPrimitive.Track>\n {Array.from({ length: _values.length }, (_, index) => (\n <SliderPrimitive.Thumb\n data-slot=\"slider-thumb\"\n // biome-ignore lint/suspicious/noArrayIndexKey: thumb chỉ phân biệt bằng vị trí, không có id\n key={index}\n className=\"border-control-on ring-ring/50 block size-4 shrink-0 rounded-full border bg-control-knob shadow-sm transition-colors duration-150 ease-out hover:ring-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\"\n />\n ))}\n </SliderPrimitive.Root>\n );\n}\n\nexport { Slider };\n"],"mappings":";AAwCI,SAgBI,KAhBJ;AAtCJ,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AA6BnB,SAAS,OAAO,EAAE,WAAW,cAAc,OAAO,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,GAAgB;AAC7F,QAAM,UAAU,MAAM;AAAA,IACpB,MAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,KAAK,GAAG;AAAA,IAC5F,CAAC,OAAO,cAAc,KAAK,GAAG;AAAA,EAChC;AAEA,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YACV,WAAU;AAAA,YAEV;AAAA,cAAC,gBAAgB;AAAA,cAAhB;AAAA,gBACC,aAAU;AAAA,gBACV,WAAU;AAAA;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,GAAG,CAAC,GAAG,UAC1C;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,aAAU;AAAA,YAGV,WAAU;AAAA;AAAA,UADL;AAAA,QAEP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -20,7 +20,7 @@ function Switch({ className, size = "md", label, description, id, ...props }) {
20
20
  "data-slot": "switch",
21
21
  id: switchId,
22
22
  className: cn(
23
- "peer inline-flex shrink-0 items-center rounded-full bg-control-edge data-[state=checked]:bg-control-on",
23
+ "peer inline-flex shrink-0 items-center rounded-full bg-switch-track data-[state=checked]:bg-control-on",
24
24
  "transition-colors duration-150 ease-out",
25
25
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
26
26
  "disabled:cursor-not-allowed disabled:opacity-50",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/switch.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/* Bảng cỡ tách ra ngoài component để không dựng lại object mỗi lần render. */\nconst trackSizes = {\n sm: \"h-4 w-7\",\n md: \"h-5 w-9\"\n} as const;\n\nconst thumbSizes = {\n sm: \"size-3 data-[state=checked]:translate-x-3\",\n md: \"size-4 data-[state=checked]:translate-x-4\"\n} as const;\n\nexport interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root> {\n /** Cỡ công tắc. Mặc định `md`. */\n size?: \"sm\" | \"md\";\n /** Nhãn đặt cạnh công tắc; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n /** Mô tả phụ dưới nhãn. */\n description?: string;\n}\n\n/*\n * Style port từ comitor-ds (đổi gray-* → ash-* theo namespace của package).\n *\n * ─ CÔNG TẮC LÀ THÀNH PHẦN PHI VĂN BẢN: BA PHÉP ĐO, KHÔNG PHẢI MỘT ─\n *\n * WCAG 1.4.11 đòi 3:1 cho \"thông tin thị giác cần để nhận ra thành phần và TRẠNG THÁI của nó\".\n * Với công tắc, đó là ba cặp — hỏng cặp nào cũng là hỏng:\n *\n * (1) rãnh ⇄ nền trang → có nhìn thấy cái công tắc không\n * (2) núm ⇄ rãnh → núm đang nằm bên nào\n * (3) rãnh BẬT ⇄ rãnh TẮT → hai trạng thái có khác nhau không (ở đây khác cả màu lẫn vị trí)\n *\n * ─ RÃNH KHI TẮT: `--control-edge` ─\n * Bản đầu dùng `bg-ash-300 dark:bg-ash-700`, hỏng cả (1) lẫn (2): 1,62:1 ở light và 1,79:1 ở dark.\n * Bản kế dùng `bg-muted-foreground` — đúng số nhưng SAI token: `--muted-foreground` là bậc CHỮ,\n * nó bị kéo tối/sáng theo nhu cầu đọc chữ (và đã đổi một lần: #7E8290 → #686C7A / #888D9B), tức\n * ràng buộc \"núm trắng còn nổi\" không có gì bảo vệ. `--control-edge` #7E8290 là token sinh ra\n * đúng cho vai trò này và ghim MỘT giá trị ở cả hai theme nên không cần biến thể `dark:`:\n *\n * rãnh ⇄ nền : 3,83:1 (light, nền trắng) · 4,94:1 (dark, #0E1116) ✓\n * núm ⇄ rãnh : 3,83:1 (light) · 4,94:1 (dark) ✓\n *\n * ─ RÃNH KHI BẬT: `--control-on`, KHÔNG PHẢI `--primary` ─\n * `--primary` (gold #E8B824 ở light) là màu TÔ NỀN cho khối lớn; làm rãnh công tắc thì hỏng cả\n * (1) lẫn (2) ở light — **1,86:1** so với nền trắng và **1,86:1** so với núm trắng — nên trạng\n * thái BẬT chỉ còn phân biệt được bằng VỊ TRÍ núm. Ở dark rãnh #F5D060 nổi trên nền, nhưng núm\n * trắng lại chìm vào rãnh (**1,49:1** ✗). `--control-on` (= `--primary-ink`: gold-500 ở light,\n * = `--primary` ở dark) là token dành đúng cho vai trò \"nền control khi BẬT\", đạt cả hai phía\n * ở cả hai theme:\n *\n * rãnh ⇄ nền : 6,51:1 (light) · 12,67:1 (dark) ✓\n * núm ⇄ rãnh : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * ⚠ (3) rãnh BẬT ⇄ rãnh TẮT chỉ 1,70:1 (light) / 2,57:1 (dark) — cố ý và chấp nhận được: hai\n * trạng thái còn khác nhau bằng VỊ TRÍ núm, và cả hai rãnh đều tự đạt ≥3:1 với nền trang lẫn\n * với núm. Không có cặp màu nào vừa đạt (1) vừa đạt (3) khi nền trang là trắng.\n *\n * ─ NÚM: `bg-control-knob`, KHÔNG PHẢI `bg-white` ─\n * Núm mang màu NỀN TRANG (`--control-knob` = `--background`: trắng ở light, #0E1116 ở dark) nên\n * nó luôn tương phản đúng chiều với rãnh — bốn tổ hợp trạng thái × theme ở trên đều là số đo của\n * chính lựa chọn này. `bg-white` cứng chỉ đúng ở light: ở dark nó ăn 1,49:1 với rãnh BẬT.\n *\n * Đúng luật \"chọn ĐỌC ĐƯỢC hơn ĐÚNG BRAND\": rãnh BẬT ở light nay là gold sẫm chứ không phải gold\n * rực của brand book. Đổi lại người dùng nhìn thấy trạng thái. Xem docs/quyet-dinh-tich-hop.md §30.\n * ⚠ App ghi đè `--primary` phải ghi đè kèm `--primary-ink`, nếu không công tắc giữ nguyên gold\n * mặc định — sai màu mà không sai chuẩn, đúng thứ tự ưu tiên của repo.\n */\nfunction Switch({ className, size = \"md\", label, description, id, ...props }: SwitchProps) {\n const generatedId = React.useId();\n const switchId = id ?? generatedId;\n\n const switchElement = (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n id={switchId}\n className={cn(\n \"peer inline-flex shrink-0 items-center rounded-full bg-control-edge data-[state=checked]:bg-control-on\",\n \"transition-colors duration-150 ease-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n trackSizes[size],\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block translate-x-0.5 rounded-full bg-control-knob shadow-sm ring-0\",\n \"transition-transform duration-150 ease-out\",\n thumbSizes[size]\n )}\n />\n </SwitchPrimitive.Root>\n );\n\n if (!label && !description) {\n return switchElement;\n }\n\n return (\n <div className=\"flex items-start gap-2.5\">\n {switchElement}\n <div className=\"flex flex-col gap-0.5\">\n {label && (\n <label\n htmlFor={switchId}\n className=\"text-sm font-medium text-foreground cursor-pointer select-none leading-none\"\n >\n {label}\n </label>\n )}\n {description && <p className=\"text-xs text-muted-foreground leading-relaxed\">{description}</p>}\n </div>\n </div>\n );\n}\n\nexport { Switch };\n"],"mappings":";AA2FM,cAkBA,YAlBA;AAzFN,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AAGnB,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AA0DA,SAAS,OAAO,EAAE,WAAW,OAAO,MAAM,OAAO,aAAa,IAAI,GAAG,MAAM,GAAgB;AACzF,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,WAAW,MAAM;AAEvB,QAAM,gBACJ;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,IAAI;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,IAAI;AAAA,QACf;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,gBAAgB;AAAA,QAAhB;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,WAAW,IAAI;AAAA,UACjB;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAGF,MAAI,CAAC,SAAS,CAAC,aAAa;AAC1B,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,SAAI,WAAU,4BACZ;AAAA;AAAA,IACD,qBAAC,SAAI,WAAU,yBACZ;AAAA,eACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,eAAe,oBAAC,OAAE,WAAU,iDAAiD,uBAAY;AAAA,OAC5F;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/switch.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/* Bảng cỡ tách ra ngoài component để không dựng lại object mỗi lần render. */\nconst trackSizes = {\n sm: \"h-4 w-7\",\n md: \"h-5 w-9\"\n} as const;\n\nconst thumbSizes = {\n sm: \"size-3 data-[state=checked]:translate-x-3\",\n md: \"size-4 data-[state=checked]:translate-x-4\"\n} as const;\n\nexport interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root> {\n /** Cỡ công tắc. Mặc định `md`. */\n size?: \"sm\" | \"md\";\n /** Nhãn đặt cạnh công tắc; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n /** Mô tả phụ dưới nhãn. */\n description?: string;\n}\n\n/*\n * Style port từ comitor-ds (đổi gray-* → ash-* theo namespace của package).\n *\n * ─ BA LỚP MÀU, BA TOKEN VAI TRÒ — KHÔNG viết thẳng thang màu ─\n *\n * rãnh TẮT `bg-switch-track` (comitor-ds: bg-gray-300 / dark:bg-gray-700)\n * rãnh BẬT `data-[state=checked]:bg-control-on` (comitor-ds: bg-gold-300)\n * núm `bg-control-knob` (comitor-ds: bg-white)\n *\n * Ba token này là chỗ DUY NHẤT phân biệt hai bảng màu của gói (styles.css khối 5b). Viết thẳng\n * `bg-ash-300` / `bg-gold-300` / `bg-white` là khoá cứng bảng mặc định và bảng tương phản cao\n * lặng lẽ không đổi được công tắc — không có gì báo đỏ.\n *\n * ─ CÔNG TẮC LÀ THÀNH PHẦN PHI VĂN BẢN: BA PHÉP ĐO, KHÔNG PHẢI MỘT ─\n *\n * WCAG 1.4.11 đòi 3:1 cho \"thông tin thị giác cần để nhận ra thành phần và TRẠNG THÁI của nó\".\n * Với công tắc, đó là ba cặp:\n *\n * (1) rãnh ⇄ nền trang → có nhìn thấy cái công tắc không\n * (2) núm ⇄ rãnh → núm đang nằm bên nào\n * (3) rãnh BẬT ⇄ rãnh TẮT → hai trạng thái có khác nhau không\n *\n * Số đo của hai bảng — bảng mặc định là bản duyệt comitor-ds, nó KHÔNG đạt (1) và (2), và đó là\n * quyết định đã chấp nhận (xem khối NGUỒN SỰ THẬT đầu styles.css):\n *\n * bảng MẶC ĐỊNH (light) bảng TƯƠNG PHẢN CAO (light · dark)\n * rãnh TẮT ⇄ nền 1,52:1 ✗ 3,83:1 · 4,94:1 ✓\n * rãnh BẬT ⇄ nền 1,86:1 ✗ 6,51:1 · 12,67:1 ✓\n * núm ⇄ rãnh BẬT 1,86:1 ✗ 6,51:1 · 12,67:1 ✓\n *\n * ⚠ Ở bảng tương phản cao, (3) rãnh BẬT ⇄ rãnh TẮT chỉ 1,70:1 (light) / 2,57:1 (dark) — cố ý:\n * hai trạng thái còn khác nhau bằng VỊ TRÍ núm, và cả hai rãnh đều tự đạt ≥3:1 với nền trang lẫn\n * với núm. Không có cặp màu nào vừa đạt (1) vừa đạt (3) khi nền trang là trắng.\n *\n * ⚠ ĐỪNG dùng `--muted-foreground` cho rãnh TẮT dù số có vẻ vừa: đó là bậc CHỮ, nó bị kéo\n * tối/sáng theo nhu cầu đọc chữ (đã đổi một lần #7E8290 → #686C7A / #888D9B), tức ràng buộc\n * \"núm còn nổi trên rãnh\" không có gì bảo vệ.\n */\nfunction Switch({ className, size = \"md\", label, description, id, ...props }: SwitchProps) {\n const generatedId = React.useId();\n const switchId = id ?? generatedId;\n\n const switchElement = (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n id={switchId}\n className={cn(\n \"peer inline-flex shrink-0 items-center rounded-full bg-switch-track data-[state=checked]:bg-control-on\",\n \"transition-colors duration-150 ease-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n trackSizes[size],\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block translate-x-0.5 rounded-full bg-control-knob shadow-sm ring-0\",\n \"transition-transform duration-150 ease-out\",\n thumbSizes[size]\n )}\n />\n </SwitchPrimitive.Root>\n );\n\n if (!label && !description) {\n return switchElement;\n }\n\n return (\n <div className=\"flex items-start gap-2.5\">\n {switchElement}\n <div className=\"flex flex-col gap-0.5\">\n {label && (\n <label\n htmlFor={switchId}\n className=\"text-sm font-medium text-foreground cursor-pointer select-none leading-none\"\n >\n {label}\n </label>\n )}\n {description && <p className=\"text-xs text-muted-foreground leading-relaxed\">{description}</p>}\n </div>\n </div>\n );\n}\n\nexport { Switch };\n"],"mappings":";AAkFM,cAkBA,YAlBA;AAhFN,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AAGnB,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AAiDA,SAAS,OAAO,EAAE,WAAW,OAAO,MAAM,OAAO,aAAa,IAAI,GAAG,MAAM,GAAgB;AACzF,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,WAAW,MAAM;AAEvB,QAAM,gBACJ;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,IAAI;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,IAAI;AAAA,QACf;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,gBAAgB;AAAA,QAAhB;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,WAAW,IAAI;AAAA,UACjB;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAGF,MAAI,CAAC,SAAS,CAAC,aAAa;AAC1B,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,SAAI,WAAU,4BACZ;AAAA;AAAA,IACD,qBAAC,SAAI,WAAU,yBACZ;AAAA,eACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,eAAe,oBAAC,OAAE,WAAU,iDAAiD,uBAAY;AAAA,OAC5F;AAAA,KACF;AAEJ;","names":[]}
@@ -6,7 +6,7 @@ function Textarea({ className, ...props }) {
6
6
  {
7
7
  "data-slot": "textarea",
8
8
  className: cn(
9
- "border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-colors duration-150 ease-out focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
9
+ "border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink dark:bg-input-fill flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-colors duration-150 ease-out focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
10
10
  className
11
11
  ),
12
12
  ...props
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/textarea.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * ─ KHÔNG CÓ `dark:bg-input/30` ─\n * `--input` là alias của `--control-edge`, tức token VIỀN control (hợp đồng màu §3) — không phải\n * bậc tô nền. Bản trước mượn nó tô ruột ô ở dark, và lớp tô ấy SÁNG hơn mọi nền trang nên kéo\n * placeholder `--muted-foreground` (token chữ, hiệu chỉnh theo nền TRANG — §6) xuống dưới ngưỡng\n * 4,5:1 ở TẤT CẢ nền dark, không riêng ca tệ nhất:\n *\n * cũ #888D9B trên `input/30` hợp lên nền: 3,81 (background) · 3,54 (card) · 3,22:1 (muted) ✗\n * nay #888D9B trên chính nền trang : 5,70 (background) · 5,35 (card) · 4,86:1 (muted) ✓\n *\n * Ô vẫn được khoanh vùng bằng `border-input` — 3,83:1 (light) · 4,21–4,94:1 (dark), thừa ngưỡng\n * 3:1 của thành phần phi văn bản, đúng vai trò mà §3 giao cho viền control.\n */\nexport type TextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction Textarea({ className, ...props }: TextareaProps) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-colors duration-150 ease-out focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Textarea };\n"],"mappings":"AAoBI;AAnBJ,SAAS,UAAU;AAiBnB,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAkB;AACxD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/textarea.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * ─ LỚP TÔ Ở DARK ĐI QUA `--input-fill`, KHÔNG PHẢI `bg-input/30` ─\n *\n * comitor-ds viết `dark:bg-input/30` cho ô nhập, và ở bảng MẶC ĐỊNH lớp tô đó cần thiết thật:\n * viền `--input` (#1E2128) trên `--card` (#16181D) chỉ 1,10:1, nên nó là thứ gần như duy nhất\n * còn khoanh vùng ô ở dark.\n *\n * Nhưng ở bảng TƯƠNG PHẢN CAO, `--input` là #7E8290 — tô 30% ra một mảng xám SÁNG hơn mọi nền\n * trang và kéo placeholder `--muted-foreground` xuống dưới AA:\n *\n * bg-input/30 ở bảng tương phản cao: 3,81 (background) · 3,54 (card) · 3,22:1 (muted) ✗\n * --input-fill trong suốt : 5,70 (background) · 5,35 (card) · 4,86:1 (muted) ✓\n *\n * Nên lớp tô là token riêng: `--input-fill` = `color-mix(--input 30%, transparent)` ở bảng mặc\n * định và `transparent` ở bảng tương phản cao (bên đó viền đã tự đủ đậm để khoanh vùng).\n */\nexport type TextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction Textarea({ className, ...props }: TextareaProps) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"border-input placeholder:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink dark:bg-input-fill flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-colors duration-150 ease-out focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Textarea };\n"],"mappings":"AAuBI;AAtBJ,SAAS,UAAU;AAoBnB,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAkB;AACxD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
package/dist/index.d.ts CHANGED
@@ -77,7 +77,7 @@ export { Toggle, ToggleProps, ToggleVariantProps, toggleVariants } from './compo
77
77
  export { ToggleGroup, ToggleGroupItem, ToggleGroupItemProps, ToggleGroupProps, ToggleGroupVariantProps } from './components/ui/toggle-group.js';
78
78
  export { Tooltip, TooltipContent, TooltipContentProps, TooltipProps, TooltipProvider, TooltipProviderProps, TooltipTrigger, TooltipTriggerProps } from './components/ui/tooltip.js';
79
79
  export { cn } from './lib/cn.js';
80
- export { ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, layout, semantic, status, typography } from './tokens.js';
80
+ export { ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, highContrast, layout, semantic, status, typography } from './tokens.js';
81
81
  export { AvatarToneColors, getAvatarToneIndex } from './lib/avatar-tone.js';
82
82
  export { DateRange } from 'react-day-picker';
83
83
  import 'react';
@@ -33,8 +33,16 @@ interface AppHeaderProps {
33
33
  * Header cao 56px (`--header-height`), nền `bg-background/80 backdrop-blur`.
34
34
  * Comitor là hệ sáng — **không** dùng header luôn-tối.
35
35
  *
36
- * Trái: hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề.
37
- * Phải: ô tìm (Cmd/Ctrl+K) · AppLauncher · thông báo · ThemeToggle · UserMenu.
36
+ * BA VÙNG, và ô tìm nằm ở vùng GIỮA — đúng bố cục AppHeader ở mục Patterns của comitor-ds:
37
+ *
38
+ * trái hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề
39
+ * giữa ô tìm (Cmd/Ctrl+K), bo tròn hết cỡ
40
+ * phải AppLauncher · thông báo · ThemeToggle · UserMenu
41
+ *
42
+ * Hai vùng ngoài đều `flex-1`, vùng giữa `shrink-0` — đó là điều kiện để vùng giữa nằm CHÍNH GIỮA
43
+ * header bất kể breadcrumb bên trái dài ngắn thế nào. Nếu để ô tìm trong cụm phải (bản trước) thì
44
+ * nó trôi theo số lượng nút bên phải; nếu căn giữa bằng `absolute left-1/2` thì nó đè lên
45
+ * breadcrumb dài. Cách này không mắc cả hai.
38
46
  */
39
47
  declare function AppHeader({ className, title, breadcrumb, logo, searchSlot, showSearch, showSidebarToggle, notificationsSlot, notificationCount, onNotificationsClick, extraSlot, showAppLauncher, showThemeToggle, showUserMenu, userMenuProps }: AppHeaderProps): React.JSX.Element;
40
48
 
@@ -39,88 +39,93 @@ function AppHeader({
39
39
  className
40
40
  ),
41
41
  children: [
42
- /* @__PURE__ */ jsx(
43
- "button",
44
- {
45
- type: "button",
46
- onClick: () => setMobileMenuOpen(true),
47
- "aria-label": labels.openMenu,
48
- className: cn(
49
- "-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden",
50
- "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground"
51
- ),
52
- children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
53
- }
54
- ),
55
- showSidebarToggle ? /* @__PURE__ */ jsxs(Tooltip, { children: [
56
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
42
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
43
+ /* @__PURE__ */ jsx(
57
44
  "button",
58
45
  {
59
46
  type: "button",
60
- onClick: toggleSidebar,
61
- "aria-label": sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
47
+ onClick: () => setMobileMenuOpen(true),
48
+ "aria-label": labels.openMenu,
62
49
  className: cn(
63
- "hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex",
50
+ "-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden",
64
51
  "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground"
65
52
  ),
66
- children: /* @__PURE__ */ jsx(PanelLeft, { className: "size-4" })
53
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5" })
67
54
  }
68
- ) }),
69
- /* @__PURE__ */ jsxs(TooltipContent, { side: "bottom", children: [
70
- sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
71
- " \xB7 ",
72
- modKey,
73
- " + B"
74
- ] })
75
- ] }) : null,
76
- logo ? /* @__PURE__ */ jsx("div", { className: "shrink-0", children: logo }) : null,
77
- /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center", children: breadcrumb ?? (title ? /* @__PURE__ */ jsx("h1", { className: "truncate text-sm font-semibold text-foreground", children: title }) : null) }),
78
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
79
- extraSlot,
80
- showSearch ? searchSlot ?? /* @__PURE__ */ jsxs(Fragment, { children: [
81
- /* @__PURE__ */ jsxs(
55
+ ),
56
+ showSidebarToggle ? /* @__PURE__ */ jsxs(Tooltip, { children: [
57
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
82
58
  "button",
83
59
  {
84
60
  type: "button",
85
- onClick: () => setCommandPaletteOpen(true),
61
+ onClick: toggleSidebar,
62
+ "aria-label": sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
86
63
  className: cn(
87
- "hidden h-8 w-52 items-center gap-2 rounded-md border border-input bg-muted/40 px-2.5 sm:flex",
88
- "text-sm text-muted-foreground transition-colors duration-150 ease-out",
89
- // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`
90
- // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là
91
- // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê
92
- // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là
93
- // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành
94
- // 10,19:1 — mỗi theme một hành vi.
95
- // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:
96
- // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1
97
- // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.
98
- "hover:border-control-edge-strong hover:bg-muted"
99
- ),
100
- children: [
101
- /* @__PURE__ */ jsx(Search, { className: "size-3.5 shrink-0" }),
102
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-left", children: labels.searchPlaceholder }),
103
- /* @__PURE__ */ jsxs(Kbd, { children: [
104
- modKey,
105
- "K"
106
- ] })
107
- ]
108
- }
109
- ),
110
- /* @__PURE__ */ jsx(
111
- "button",
112
- {
113
- type: "button",
114
- onClick: () => setCommandPaletteOpen(true),
115
- "aria-label": labels.search,
116
- className: cn(
117
- "flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden",
64
+ "hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex",
118
65
  "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground"
119
66
  ),
120
- children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
67
+ children: /* @__PURE__ */ jsx(PanelLeft, { className: "size-4" })
121
68
  }
122
- )
69
+ ) }),
70
+ /* @__PURE__ */ jsxs(TooltipContent, { side: "bottom", children: [
71
+ sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
72
+ " \xB7 ",
73
+ modKey,
74
+ " + B"
75
+ ] })
123
76
  ] }) : null,
77
+ logo ? /* @__PURE__ */ jsx("div", { className: "shrink-0", children: logo }) : null,
78
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center", children: breadcrumb ?? (title ? /* @__PURE__ */ jsx("h1", { className: "truncate text-sm font-semibold text-foreground", children: title }) : null) })
79
+ ] }),
80
+ /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center justify-center", children: showSearch ? searchSlot ?? /* @__PURE__ */ jsxs(Fragment, { children: [
81
+ /* @__PURE__ */ jsxs(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ onClick: () => setCommandPaletteOpen(true),
86
+ className: cn(
87
+ // `rounded-full` + `w-64`: đúng ô tìm của AppHeader ở mục Patterns
88
+ // (`<Input className="rounded-full w-64" />`). Pill cần nhiều đệm ngang hơn ô
89
+ // vuông góc nên `px-3` thay vì `px-2.5`.
90
+ "hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 sm:flex",
91
+ "text-sm text-muted-foreground transition-colors duration-150 ease-out",
92
+ // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`
93
+ // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là
94
+ // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê
95
+ // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là
96
+ // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành
97
+ // 10,19:1 — mỗi theme một hành vi.
98
+ // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:
99
+ // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1
100
+ // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.
101
+ "hover:border-control-edge-strong hover:bg-muted"
102
+ ),
103
+ children: [
104
+ /* @__PURE__ */ jsx(Search, { className: "size-3.5 shrink-0" }),
105
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-left", children: labels.searchPlaceholder }),
106
+ /* @__PURE__ */ jsxs(Kbd, { children: [
107
+ modKey,
108
+ "K"
109
+ ] })
110
+ ]
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ onClick: () => setCommandPaletteOpen(true),
118
+ "aria-label": labels.search,
119
+ className: cn(
120
+ "flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden",
121
+ "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground"
122
+ ),
123
+ children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
124
+ }
125
+ )
126
+ ] }) : null }),
127
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-end gap-1", children: [
128
+ extraSlot,
124
129
  showAppLauncher ? /* @__PURE__ */ jsx(AppLauncher, {}) : null,
125
130
  notificationsSlot ?? (notificationCount === void 0 ? null : /* @__PURE__ */ jsx(
126
131
  NotificationsButton,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/app-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { Menu, PanelLeft, Search } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n// `useIsMac` nhập thẳng từ Tier 2: 0.2.0 chỉ còn MỘT cửa xuất cho nó (entry `.`),\n// `/shell` không re-export nữa.\nimport { useIsMac } from \"../components/composite/keyboard-hint.js\";\nimport { Kbd } from \"../components/ui/kbd.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { NotificationsButton } from \"./notifications-button.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\n\nexport interface AppHeaderProps {\n className?: string;\n /** Tiêu đề trang — dùng khi không truyền `breadcrumb`. */\n title?: ReactNode;\n /** Slot breadcrumb (composite `PageHeader` của app) — ưu tiên hơn `title`. */\n breadcrumb?: ReactNode;\n /** Slot ngoài cùng bên trái (vd `<ComitorLockup size=\"xs\" variant=\"auto\" />`). */\n logo?: ReactNode;\n\n /** Thay ô tìm kiếm dựng sẵn bằng UI riêng của app. */\n searchSlot?: ReactNode;\n showSearch?: boolean;\n /** Nút thu gọn sidebar ở header (layout sidebar-first thường để ở sidebar). */\n showSidebarToggle?: boolean;\n\n /** Slot thông báo (app tự render Popover + panel). Có slot thì `notificationCount` bị bỏ qua. */\n notificationsSlot?: ReactNode;\n notificationCount?: number;\n onNotificationsClick?: () => void;\n\n /** Slot phụ bên phải, đứng trước AppLauncher. */\n extraSlot?: ReactNode;\n\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n showUserMenu?: boolean;\n /** Chuyển tiếp props cho `UserMenu` (profileHref, settingsHref, items…). */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Header cao 56px (`--header-height`), nền `bg-background/80 backdrop-blur`.\n * Comitor là hệ sáng — **không** dùng header luôn-tối.\n *\n * Trái: hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề.\n * Phải: ô tìm (Cmd/Ctrl+K) · AppLauncher · thông báo · ThemeToggle · UserMenu.\n */\nexport function AppHeader({\n className,\n title,\n breadcrumb,\n logo,\n searchSlot,\n showSearch = true,\n showSidebarToggle = false,\n notificationsSlot,\n notificationCount,\n onNotificationsClick,\n extraSlot,\n showAppLauncher = true,\n showThemeToggle = true,\n showUserMenu = true,\n userMenuProps\n}: AppHeaderProps) {\n const { setMobileMenuOpen, setCommandPaletteOpen, toggleSidebar, sidebarCollapsed, labels, user } = useShell();\n const isMac = useIsMac();\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n return (\n <header\n className={cn(\n \"sticky top-0 z-40 flex h-header shrink-0 items-center gap-2 px-3 sm:px-4\",\n \"border-b border-border bg-background/80 backdrop-blur-sm\",\n className\n )}\n >\n {/* Mở menu off-canvas — chỉ màn hình hẹp */}\n <button\n type=\"button\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label={labels.openMenu}\n className={cn(\n \"-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Menu className=\"size-5\" />\n </button>\n\n {showSidebarToggle ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <PanelLeft className=\"size-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · {modKey} + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n\n {logo ? <div className=\"shrink-0\">{logo}</div> : null}\n\n {/* Breadcrumb thắng title khi có cả hai */}\n <div className=\"flex min-w-0 flex-1 items-center\">\n {breadcrumb ?? (title ? <h1 className=\"truncate text-sm font-semibold text-foreground\">{title}</h1> : null)}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {extraSlot}\n\n {showSearch\n ? (searchSlot ?? (\n <>\n {/* ≥sm: ô giả lập trường tìm kiếm, kèm gợi ý phím tắt */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n className={cn(\n \"hidden h-8 w-52 items-center gap-2 rounded-md border border-input bg-muted/40 px-2.5 sm:flex\",\n \"text-sm text-muted-foreground transition-colors duration-150 ease-out\",\n // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`\n // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là\n // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê\n // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là\n // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành\n // 10,19:1 — mỗi theme một hành vi.\n // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:\n // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1\n // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.\n \"hover:border-control-edge-strong hover:bg-muted\"\n )}\n >\n <Search className=\"size-3.5 shrink-0\" />\n <span className=\"flex-1 truncate text-left\">{labels.searchPlaceholder}</span>\n <Kbd>{modKey}K</Kbd>\n </button>\n\n {/* <sm: chỉ icon */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n aria-label={labels.search}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Search className=\"size-4\" />\n </button>\n </>\n ))\n : null}\n\n {showAppLauncher ? <AppLauncher /> : null}\n\n {notificationsSlot ??\n (notificationCount === undefined ? null : (\n <NotificationsButton\n count={notificationCount}\n label={labels.notifications}\n onClick={onNotificationsClick}\n />\n ))}\n\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n\n {showUserMenu && user ? (\n <>\n <span className=\"mx-1 hidden h-5 w-px bg-border sm:block\" aria-hidden=\"true\" />\n <UserMenu {...userMenuProps} />\n </>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":";AA4FQ,SAoCM,UApCN,KAkBE,YAlBF;AA1FR,SAAS,MAAM,WAAW,cAAc;AAIxC,SAAS,gBAAgB;AACzB,SAAS,WAAW;AACpB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AACpC,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AAuCtC,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf;AACF,GAAmB;AACjB,QAAM,EAAE,mBAAmB,uBAAuB,eAAe,kBAAkB,QAAQ,KAAK,IAAI,SAAS;AAC7G,QAAM,QAAQ,SAAS;AACvB,QAAM,SAAS,QAAQ,WAAM;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAGA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,kBAAkB,IAAI;AAAA,YACrC,cAAY,OAAO;AAAA,YACnB,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YACF;AAAA,YAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,QAC3B;AAAA,QAEC,oBACC,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MACrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,cAC7D,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,aAAU,WAAU,UAAS;AAAA;AAAA,UAChC,GACF;AAAA,UACA,qBAAC,kBAAe,MAAK,UAClB;AAAA,+BAAmB,OAAO,gBAAgB,OAAO;AAAA,YAAgB;AAAA,YAAI;AAAA,YAAO;AAAA,aAC/E;AAAA,WACF,IACE;AAAA,QAEH,OAAO,oBAAC,SAAI,WAAU,YAAY,gBAAK,IAAS;AAAA,QAGjD,oBAAC,SAAI,WAAU,oCACZ,yBAAe,QAAQ,oBAAC,QAAG,WAAU,kDAAkD,iBAAM,IAAQ,OACxG;AAAA,QAEA,qBAAC,SAAI,WAAU,oCACZ;AAAA;AAAA,UAEA,aACI,cACC,iCAEE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,sBAAsB,IAAI;AAAA,gBACzC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAUA;AAAA,gBACF;AAAA,gBAEA;AAAA,sCAAC,UAAO,WAAU,qBAAoB;AAAA,kBACtC,oBAAC,UAAK,WAAU,6BAA6B,iBAAO,mBAAkB;AAAA,kBACtE,qBAAC,OAAK;AAAA;AAAA,oBAAO;AAAA,qBAAC;AAAA;AAAA;AAAA,YAChB;AAAA,YAGA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM,sBAAsB,IAAI;AAAA,gBACzC,cAAY,OAAO;AAAA,gBACnB,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBAEA,8BAAC,UAAO,WAAU,UAAS;AAAA;AAAA,YAC7B;AAAA,aACF,IAEF;AAAA,UAEH,kBAAkB,oBAAC,eAAY,IAAK;AAAA,UAEpC,sBACE,sBAAsB,SAAY,OACjC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,cACd,SAAS;AAAA;AAAA,UACX;AAAA,UAGH,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,UAEpD,gBAAgB,OACf,iCACE;AAAA,gCAAC,UAAK,WAAU,2CAA0C,eAAY,QAAO;AAAA,YAC7E,oBAAC,YAAU,GAAG,eAAe;AAAA,aAC/B,IACE;AAAA,WACN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/app-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { Menu, PanelLeft, Search } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n// `useIsMac` nhập thẳng từ Tier 2: 0.2.0 chỉ còn MỘT cửa xuất cho nó (entry `.`),\n// `/shell` không re-export nữa.\nimport { useIsMac } from \"../components/composite/keyboard-hint.js\";\nimport { Kbd } from \"../components/ui/kbd.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { NotificationsButton } from \"./notifications-button.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\n\nexport interface AppHeaderProps {\n className?: string;\n /** Tiêu đề trang — dùng khi không truyền `breadcrumb`. */\n title?: ReactNode;\n /** Slot breadcrumb (composite `PageHeader` của app) — ưu tiên hơn `title`. */\n breadcrumb?: ReactNode;\n /** Slot ngoài cùng bên trái (vd `<ComitorLockup size=\"xs\" variant=\"auto\" />`). */\n logo?: ReactNode;\n\n /** Thay ô tìm kiếm dựng sẵn bằng UI riêng của app. */\n searchSlot?: ReactNode;\n showSearch?: boolean;\n /** Nút thu gọn sidebar ở header (layout sidebar-first thường để ở sidebar). */\n showSidebarToggle?: boolean;\n\n /** Slot thông báo (app tự render Popover + panel). Có slot thì `notificationCount` bị bỏ qua. */\n notificationsSlot?: ReactNode;\n notificationCount?: number;\n onNotificationsClick?: () => void;\n\n /** Slot phụ bên phải, đứng trước AppLauncher. */\n extraSlot?: ReactNode;\n\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n showUserMenu?: boolean;\n /** Chuyển tiếp props cho `UserMenu` (profileHref, settingsHref, items…). */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Header cao 56px (`--header-height`), nền `bg-background/80 backdrop-blur`.\n * Comitor là hệ sáng — **không** dùng header luôn-tối.\n *\n * BA VÙNG, và ô tìm nằm ở vùng GIỮA — đúng bố cục AppHeader ở mục Patterns của comitor-ds:\n *\n * trái hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề\n * giữa ô tìm (Cmd/Ctrl+K), bo tròn hết cỡ\n * phải AppLauncher · thông báo · ThemeToggle · UserMenu\n *\n * Hai vùng ngoài đều `flex-1`, vùng giữa `shrink-0` — đó là điều kiện để vùng giữa nằm CHÍNH GIỮA\n * header bất kể breadcrumb bên trái dài ngắn thế nào. Nếu để ô tìm trong cụm phải (bản trước) thì\n * nó trôi theo số lượng nút bên phải; nếu căn giữa bằng `absolute left-1/2` thì nó đè lên\n * breadcrumb dài. Cách này không mắc cả hai.\n */\nexport function AppHeader({\n className,\n title,\n breadcrumb,\n logo,\n searchSlot,\n showSearch = true,\n showSidebarToggle = false,\n notificationsSlot,\n notificationCount,\n onNotificationsClick,\n extraSlot,\n showAppLauncher = true,\n showThemeToggle = true,\n showUserMenu = true,\n userMenuProps\n}: AppHeaderProps) {\n const { setMobileMenuOpen, setCommandPaletteOpen, toggleSidebar, sidebarCollapsed, labels, user } = useShell();\n const isMac = useIsMac();\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n return (\n <header\n className={cn(\n \"sticky top-0 z-40 flex h-header shrink-0 items-center gap-2 px-3 sm:px-4\",\n \"border-b border-border bg-background/80 backdrop-blur-sm\",\n className\n )}\n >\n {/* ── VÙNG TRÁI ─────────────────────────────────────────────────────── */}\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {/* Mở menu off-canvas — chỉ màn hình hẹp */}\n <button\n type=\"button\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label={labels.openMenu}\n className={cn(\n \"-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Menu className=\"size-5\" />\n </button>\n\n {showSidebarToggle ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <PanelLeft className=\"size-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · {modKey} + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n\n {logo ? <div className=\"shrink-0\">{logo}</div> : null}\n\n {/* Breadcrumb thắng title khi có cả hai */}\n <div className=\"flex min-w-0 items-center\">\n {breadcrumb ?? (title ? <h1 className=\"truncate text-sm font-semibold text-foreground\">{title}</h1> : null)}\n </div>\n </div>\n\n {/* ── VÙNG GIỮA: ô tìm ──────────────────────────────────────────────── */}\n <div className=\"flex shrink-0 items-center justify-center\">\n {showSearch\n ? (searchSlot ?? (\n <>\n {/* ≥sm: ô giả lập trường tìm kiếm, kèm gợi ý phím tắt */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n className={cn(\n // `rounded-full` + `w-64`: đúng ô tìm của AppHeader ở mục Patterns\n // (`<Input className=\"rounded-full w-64\" />`). Pill cần nhiều đệm ngang hơn ô\n // vuông góc nên `px-3` thay vì `px-2.5`.\n \"hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 sm:flex\",\n \"text-sm text-muted-foreground transition-colors duration-150 ease-out\",\n // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`\n // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là\n // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê\n // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là\n // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành\n // 10,19:1 — mỗi theme một hành vi.\n // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:\n // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1\n // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.\n \"hover:border-control-edge-strong hover:bg-muted\"\n )}\n >\n <Search className=\"size-3.5 shrink-0\" />\n <span className=\"flex-1 truncate text-left\">{labels.searchPlaceholder}</span>\n <Kbd>{modKey}K</Kbd>\n </button>\n\n {/* <sm: chỉ icon */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n aria-label={labels.search}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Search className=\"size-4\" />\n </button>\n </>\n ))\n : null}\n </div>\n\n {/* ── VÙNG PHẢI ─────────────────────────────────────────────────────── */}\n <div className=\"flex min-w-0 flex-1 items-center justify-end gap-1\">\n {extraSlot}\n\n {showAppLauncher ? <AppLauncher /> : null}\n\n {notificationsSlot ??\n (notificationCount === undefined ? null : (\n <NotificationsButton\n count={notificationCount}\n label={labels.notifications}\n onClick={onNotificationsClick}\n />\n ))}\n\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n\n {showUserMenu && user ? (\n <>\n <span className=\"mx-1 hidden h-5 w-px bg-border sm:block\" aria-hidden=\"true\" />\n <UserMenu {...userMenuProps} />\n </>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Hằng và kiểu của trục TƯƠNG PHẢN — module THUẦN, cố ý KHÔNG có `"use client"`.
3
+ *
4
+ * Ranh giới RSC cắt theo FILE: mọi export của một module `"use client"` khi đi tới Server
5
+ * Component đều thành client-reference proxy, nên `CONTRAST_STORAGE_KEY` sẽ ra `undefined` mà
6
+ * KHÔNG throw và KHÔNG warning (xem CHANGELOG 0.2.2 §2). Root layout là Server Component và nó
7
+ * cần đúng khoá này để render `<ContrastScript />`, nên hằng phải sống ở đây.
8
+ *
9
+ * ⚠ Barrel `shell/index.ts` phải xuất thẳng từ file này, KHÔNG vòng qua contrast-provider.tsx —
10
+ * đi qua một file client là ranh giới biến hằng lại thành proxy (cổng `verify:directives` phép
11
+ * kiểm 3 canh đúng chuyện này).
12
+ */
13
+ /** Hai bảng màu của hệ sinh thái. `normal` = comitor-ds (bản duyệt), `high` = bảng đã đo WCAG. */
14
+ type ContrastMode = "normal" | "high";
15
+ declare const CONTRAST_MODES: readonly ContrastMode[];
16
+ /** Thuộc tính trên `<html>` mà styles.css nhìn vào: `[data-contrast="high"]`. */
17
+ declare const CONTRAST_ATTRIBUTE = "data-contrast";
18
+ /** Khoá localStorage mặc định — cùng lối đặt tên với `comitor-theme` của ThemeProvider. */
19
+ declare const CONTRAST_STORAGE_KEY = "comitor-contrast";
20
+ /** Nhãn của riêng ContrastToggle — không nằm trong `ShellLabels` vì nút này dùng được ngoài shell. */
21
+ interface ContrastToggleLabels {
22
+ /** Nhãn chính, đứng cạnh nút gạt. */
23
+ label: string;
24
+ /** Dòng mô tả bên dưới nhãn. */
25
+ description: string;
26
+ }
27
+ declare const DEFAULT_CONTRAST_TOGGLE_LABELS: ContrastToggleLabels;
28
+ declare const isContrastMode: (value: unknown) => value is ContrastMode;
29
+
30
+ export { CONTRAST_ATTRIBUTE, CONTRAST_MODES, CONTRAST_STORAGE_KEY, type ContrastMode, type ContrastToggleLabels, DEFAULT_CONTRAST_TOGGLE_LABELS, isContrastMode };
@@ -0,0 +1,16 @@
1
+ const CONTRAST_MODES = ["normal", "high"];
2
+ const CONTRAST_ATTRIBUTE = "data-contrast";
3
+ const CONTRAST_STORAGE_KEY = "comitor-contrast";
4
+ const DEFAULT_CONTRAST_TOGGLE_LABELS = {
5
+ label: "T\u01B0\u01A1ng ph\u1EA3n cao",
6
+ description: "\u0110\u1EADm m\xE0u vi\u1EC1n, c\xF4ng t\u1EAFc v\xE0 ch\u1EEF ph\u1EE5. H\u1EEFu \xEDch tr\xEAn m\xE0n h\xECnh \u0111\u1ED9 s\xE1ng th\u1EA5p ho\u1EB7c khi d\xF9ng ngo\xE0i tr\u1EDDi."
7
+ };
8
+ const isContrastMode = (value) => typeof value === "string" && CONTRAST_MODES.includes(value);
9
+ export {
10
+ CONTRAST_ATTRIBUTE,
11
+ CONTRAST_MODES,
12
+ CONTRAST_STORAGE_KEY,
13
+ DEFAULT_CONTRAST_TOGGLE_LABELS,
14
+ isContrastMode
15
+ };
16
+ //# sourceMappingURL=contrast-constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/contrast-constants.ts"],"sourcesContent":["/**\n * Hằng và kiểu của trục TƯƠNG PHẢN — module THUẦN, cố ý KHÔNG có `\"use client\"`.\n *\n * Ranh giới RSC cắt theo FILE: mọi export của một module `\"use client\"` khi đi tới Server\n * Component đều thành client-reference proxy, nên `CONTRAST_STORAGE_KEY` sẽ ra `undefined` mà\n * KHÔNG throw và KHÔNG warning (xem CHANGELOG 0.2.2 §2). Root layout là Server Component và nó\n * cần đúng khoá này để render `<ContrastScript />`, nên hằng phải sống ở đây.\n *\n * ⚠ Barrel `shell/index.ts` phải xuất thẳng từ file này, KHÔNG vòng qua contrast-provider.tsx —\n * đi qua một file client là ranh giới biến hằng lại thành proxy (cổng `verify:directives` phép\n * kiểm 3 canh đúng chuyện này).\n */\n\n/** Hai bảng màu của hệ sinh thái. `normal` = comitor-ds (bản duyệt), `high` = bảng đã đo WCAG. */\nexport type ContrastMode = \"normal\" | \"high\";\n\nexport const CONTRAST_MODES: readonly ContrastMode[] = [\"normal\", \"high\"];\n\n/** Thuộc tính trên `<html>` mà styles.css nhìn vào: `[data-contrast=\"high\"]`. */\nexport const CONTRAST_ATTRIBUTE = \"data-contrast\";\n\n/** Khoá localStorage mặc định — cùng lối đặt tên với `comitor-theme` của ThemeProvider. */\nexport const CONTRAST_STORAGE_KEY = \"comitor-contrast\";\n\n/** Nhãn của riêng ContrastToggle — không nằm trong `ShellLabels` vì nút này dùng được ngoài shell. */\nexport interface ContrastToggleLabels {\n /** Nhãn chính, đứng cạnh nút gạt. */\n label: string;\n /** Dòng mô tả bên dưới nhãn. */\n description: string;\n}\n\nexport const DEFAULT_CONTRAST_TOGGLE_LABELS: ContrastToggleLabels = {\n label: \"Tương phản cao\",\n description: \"Đậm màu viền, công tắc và chữ phụ. Hữu ích trên màn hình độ sáng thấp hoặc khi dùng ngoài trời.\"\n};\n\nexport const isContrastMode = (value: unknown): value is ContrastMode =>\n typeof value === \"string\" && (CONTRAST_MODES as readonly string[]).includes(value);\n"],"mappings":"AAgBO,MAAM,iBAA0C,CAAC,UAAU,MAAM;AAGjE,MAAM,qBAAqB;AAG3B,MAAM,uBAAuB;AAU7B,MAAM,iCAAuD;AAAA,EAClE,OAAO;AAAA,EACP,aAAa;AACf;AAEO,MAAM,iBAAiB,CAAC,UAC7B,OAAO,UAAU,YAAa,eAAqC,SAAS,KAAK;","names":[]}