@comitor/ui 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -20
- package/dist/components/ui/badge.js +3 -3
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/button.js +4 -4
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/checkbox.js +2 -2
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/input-otp.js +1 -1
- package/dist/components/ui/input-otp.js.map +1 -1
- package/dist/components/ui/navigation-menu.js +1 -1
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/radio-group.js +1 -1
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/resizable.js +1 -1
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/shell/contrast-constants.d.ts +30 -0
- package/dist/shell/contrast-constants.js +16 -0
- package/dist/shell/contrast-constants.js.map +1 -0
- package/dist/shell/contrast-provider.d.ts +60 -0
- package/dist/shell/contrast-provider.js +61 -0
- package/dist/shell/contrast-provider.js.map +1 -0
- package/dist/shell/contrast-toggle.d.ts +24 -0
- package/dist/shell/contrast-toggle.js +29 -0
- package/dist/shell/contrast-toggle.js.map +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +3 -0
- package/dist/shell/index.js.map +1 -1
- package/dist/tokens.d.ts +170 -70
- package/dist/tokens.js +124 -47
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
- 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-
|
|
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`
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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';
|
|
@@ -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":[]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React$1 from 'react';
|
|
2
|
+
import { ContrastMode } from './contrast-constants.js';
|
|
3
|
+
|
|
4
|
+
interface ContrastContextValue {
|
|
5
|
+
/** Chế độ đang áp dụng. Trước khi mount luôn là giá trị mặc định, không phải giá trị đã lưu. */
|
|
6
|
+
contrast: ContrastMode;
|
|
7
|
+
setContrast: (mode: ContrastMode) => void;
|
|
8
|
+
/** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration, xem ContrastToggle. */
|
|
9
|
+
mounted: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ContrastProviderProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Bảng màu khi người dùng chưa chọn gì. Mặc định `normal` (comitor-ds). */
|
|
14
|
+
defaultContrast?: ContrastMode;
|
|
15
|
+
/** Khoá localStorage. Đổi khi một app muốn tách lựa chọn khỏi các app khác cùng tên miền. */
|
|
16
|
+
storageKey?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Trục thứ HAI của giao diện, độc lập với sáng/tối: chọn BẢNG MÀU.
|
|
20
|
+
*
|
|
21
|
+
* ─ VÌ SAO KHÔNG DÙNG `next-themes` CHO TRỤC NÀY ─
|
|
22
|
+
* `next-themes` giữ **một context singleton** cho toàn cây. Lồng hai `<ThemeProvider>` với
|
|
23
|
+
* `attribute`/`storageKey` khác nhau thì provider trong cùng thắng và `useTheme()` của app đọc
|
|
24
|
+
* nhầm trục — không có lỗi biên dịch nào, chỉ có nút đổi sáng/tối lặng lẽ ngừng ăn. Trục tương
|
|
25
|
+
* phản đơn giản hơn theme (không có "system", không đọc `prefers-color-scheme`) nên tự viết
|
|
26
|
+
* ~60 dòng là đủ và không phải trả cái giá đó.
|
|
27
|
+
*
|
|
28
|
+
* ─ CHỐNG NHÁY ─
|
|
29
|
+
* Bảng màu phải có mặt **trước** lần sơn đầu tiên, nếu không người bật tương phản cao sẽ thấy một
|
|
30
|
+
* khung hình bảng mặc định rồi mới đổi. Effect của React chạy SAU khi sơn, nên phải là một script
|
|
31
|
+
* đồng bộ — provider TỰ render nó (`<ContrastScript />`) làm con đầu tiên, app không phải làm gì.
|
|
32
|
+
*
|
|
33
|
+
* ⚠ Script nằm trong `<body>`, KHÔNG phải `<head>` — và đó là chỗ duy nhất đúng. Đặt vào `<head>`
|
|
34
|
+
* thì React 19 kêu "Encountered a script tag while rendering React component" ở mỗi lần render
|
|
35
|
+
* client (script inline không thuộc nhóm phần tử React hoisted được), tức console đỏ ở mọi trang.
|
|
36
|
+
* Trong `<body>` thì không, và về chống nháy vẫn tương đương: trình duyệt chạy script ngay khi
|
|
37
|
+
* parser đi tới nó, mà nó đứng TRƯỚC toàn bộ markup của app. Đây cũng đúng chỗ next-themes đặt
|
|
38
|
+
* script của nó.
|
|
39
|
+
*
|
|
40
|
+
* `<html>` phải có `suppressHydrationWarning` (cùng lý do với ThemeProvider: script sửa DOM
|
|
41
|
+
* trước khi React hydrate nên HTML client luôn khác HTML server).
|
|
42
|
+
*/
|
|
43
|
+
declare function ContrastProvider({ children, defaultContrast, storageKey }: ContrastProviderProps): React$1.JSX.Element;
|
|
44
|
+
/**
|
|
45
|
+
* Script chống nháy. `<ContrastProvider>` đã render sẵn nó nên **app thường không cần gọi tay** —
|
|
46
|
+
* export ra chỉ cho trường hợp app tự dựng provider hoặc cần đặt nó ở một vị trí khác trong body.
|
|
47
|
+
*
|
|
48
|
+
* ⚠ ĐỪNG đặt vào `<head>`: React 19 báo lỗi console ở mọi lần render client (xem chú thích của
|
|
49
|
+
* `ContrastProvider`). Gọi hai lần cũng vô hại — script idempotent — chỉ tốn một thẻ.
|
|
50
|
+
*/
|
|
51
|
+
declare function ContrastScript({ storageKey }: {
|
|
52
|
+
storageKey?: string;
|
|
53
|
+
}): React$1.JSX.Element;
|
|
54
|
+
/**
|
|
55
|
+
* Đọc/đổi bảng màu. Ném lỗi nếu gọi ngoài `<ContrastProvider>` — im lặng trả mặc định sẽ khiến
|
|
56
|
+
* nút gạt trông như hỏng mà không có gì chỉ ra nguyên nhân.
|
|
57
|
+
*/
|
|
58
|
+
declare function useContrast(): ContrastContextValue;
|
|
59
|
+
|
|
60
|
+
export { type ContrastContextValue, ContrastProvider, type ContrastProviderProps, ContrastScript, useContrast };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { CONTRAST_ATTRIBUTE, CONTRAST_STORAGE_KEY, isContrastMode } from "./contrast-constants.js";
|
|
5
|
+
const ContrastContext = createContext(null);
|
|
6
|
+
function ContrastProvider({
|
|
7
|
+
children,
|
|
8
|
+
defaultContrast = "normal",
|
|
9
|
+
storageKey = CONTRAST_STORAGE_KEY
|
|
10
|
+
}) {
|
|
11
|
+
const [contrast, setContrastState] = useState(defaultContrast);
|
|
12
|
+
const [mounted, setMounted] = useState(false);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let stored = null;
|
|
15
|
+
try {
|
|
16
|
+
stored = window.localStorage.getItem(storageKey);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
if (isContrastMode(stored)) setContrastState(stored);
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}, [storageKey]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!mounted) return;
|
|
24
|
+
const root = document.documentElement;
|
|
25
|
+
if (contrast === "normal") root.removeAttribute(CONTRAST_ATTRIBUTE);
|
|
26
|
+
else root.setAttribute(CONTRAST_ATTRIBUTE, contrast);
|
|
27
|
+
}, [contrast, mounted]);
|
|
28
|
+
const setContrast = useCallback(
|
|
29
|
+
(mode) => {
|
|
30
|
+
setContrastState(mode);
|
|
31
|
+
try {
|
|
32
|
+
window.localStorage.setItem(storageKey, mode);
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[storageKey]
|
|
37
|
+
);
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({ contrast, setContrast, mounted }),
|
|
40
|
+
[contrast, setContrast, mounted]
|
|
41
|
+
);
|
|
42
|
+
return /* @__PURE__ */ jsxs(ContrastContext.Provider, { value, children: [
|
|
43
|
+
/* @__PURE__ */ jsx(ContrastScript, { storageKey }),
|
|
44
|
+
children
|
|
45
|
+
] });
|
|
46
|
+
}
|
|
47
|
+
function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY }) {
|
|
48
|
+
const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v==="high")document.documentElement.setAttribute(${JSON.stringify(CONTRAST_ATTRIBUTE)},"high")}catch(e){}})();`;
|
|
49
|
+
return /* @__PURE__ */ jsx("script", { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: script } });
|
|
50
|
+
}
|
|
51
|
+
function useContrast() {
|
|
52
|
+
const ctx = useContext(ContrastContext);
|
|
53
|
+
if (!ctx) throw new Error("useContrast() ph\u1EA3i n\u1EB1m trong <ContrastProvider>. B\u1ECDc n\xF3 \u1EDF root layout.");
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
ContrastProvider,
|
|
58
|
+
ContrastScript,
|
|
59
|
+
useContrast
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=contrast-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/contrast-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { CONTRAST_ATTRIBUTE, CONTRAST_STORAGE_KEY, type ContrastMode, isContrastMode } from \"./contrast-constants.js\";\n\nexport interface ContrastContextValue {\n /** Chế độ đang áp dụng. Trước khi mount luôn là giá trị mặc định, không phải giá trị đã lưu. */\n contrast: ContrastMode;\n setContrast: (mode: ContrastMode) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration, xem ContrastToggle. */\n mounted: boolean;\n}\n\nconst ContrastContext = createContext<ContrastContextValue | null>(null);\n\nexport interface ContrastProviderProps {\n children: React.ReactNode;\n /** Bảng màu khi người dùng chưa chọn gì. Mặc định `normal` (comitor-ds). */\n defaultContrast?: ContrastMode;\n /** Khoá localStorage. Đổi khi một app muốn tách lựa chọn khỏi các app khác cùng tên miền. */\n storageKey?: string;\n}\n\n/**\n * Trục thứ HAI của giao diện, độc lập với sáng/tối: chọn BẢNG MÀU.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` CHO TRỤC NÀY ─\n * `next-themes` giữ **một context singleton** cho toàn cây. Lồng hai `<ThemeProvider>` với\n * `attribute`/`storageKey` khác nhau thì provider trong cùng thắng và `useTheme()` của app đọc\n * nhầm trục — không có lỗi biên dịch nào, chỉ có nút đổi sáng/tối lặng lẽ ngừng ăn. Trục tương\n * phản đơn giản hơn theme (không có \"system\", không đọc `prefers-color-scheme`) nên tự viết\n * ~60 dòng là đủ và không phải trả cái giá đó.\n *\n * ─ CHỐNG NHÁY ─\n * Bảng màu phải có mặt **trước** lần sơn đầu tiên, nếu không người bật tương phản cao sẽ thấy một\n * khung hình bảng mặc định rồi mới đổi. Effect của React chạy SAU khi sơn, nên phải là một script\n * đồng bộ — provider TỰ render nó (`<ContrastScript />`) làm con đầu tiên, app không phải làm gì.\n *\n * ⚠ Script nằm trong `<body>`, KHÔNG phải `<head>` — và đó là chỗ duy nhất đúng. Đặt vào `<head>`\n * thì React 19 kêu \"Encountered a script tag while rendering React component\" ở mỗi lần render\n * client (script inline không thuộc nhóm phần tử React hoisted được), tức console đỏ ở mọi trang.\n * Trong `<body>` thì không, và về chống nháy vẫn tương đương: trình duyệt chạy script ngay khi\n * parser đi tới nó, mà nó đứng TRƯỚC toàn bộ markup của app. Đây cũng đúng chỗ next-themes đặt\n * script của nó.\n *\n * `<html>` phải có `suppressHydrationWarning` (cùng lý do với ThemeProvider: script sửa DOM\n * trước khi React hydrate nên HTML client luôn khác HTML server).\n */\nexport function ContrastProvider({\n children,\n defaultContrast = \"normal\",\n storageKey = CONTRAST_STORAGE_KEY\n}: ContrastProviderProps) {\n const [contrast, setContrastState] = useState<ContrastMode>(defaultContrast);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được ContrastScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (isContrastMode(stored)) setContrastState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n if (contrast === \"normal\") root.removeAttribute(CONTRAST_ATTRIBUTE);\n else root.setAttribute(CONTRAST_ATTRIBUTE, contrast);\n }, [contrast, mounted]);\n\n const setContrast = useCallback(\n (mode: ContrastMode) => {\n setContrastState(mode);\n try {\n window.localStorage.setItem(storageKey, mode);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const value = useMemo<ContrastContextValue>(\n () => ({ contrast, setContrast, mounted }),\n [contrast, setContrast, mounted]\n );\n\n return (\n <ContrastContext.Provider value={value}>\n <ContrastScript storageKey={storageKey} />\n {children}\n </ContrastContext.Provider>\n );\n}\n\n/**\n * Script chống nháy. `<ContrastProvider>` đã render sẵn nó nên **app thường không cần gọi tay** —\n * export ra chỉ cho trường hợp app tự dựng provider hoặc cần đặt nó ở một vị trí khác trong body.\n *\n * ⚠ ĐỪNG đặt vào `<head>`: React 19 báo lỗi console ở mọi lần render client (xem chú thích của\n * `ContrastProvider`). Gọi hai lần cũng vô hại — script idempotent — chỉ tốn một thẻ.\n */\nexport function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY }: { storageKey?: string }) {\n // Chuỗi tự gọi, không phụ thuộc React: chạy ngay lúc trình duyệt gặp thẻ <script>.\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v===\"high\")document.documentElement.setAttribute(${JSON.stringify(CONTRAST_ATTRIBUTE)},\"high\")}catch(e){}})();`;\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n return <script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: script }} />;\n}\n\n/**\n * Đọc/đổi bảng màu. Ném lỗi nếu gọi ngoài `<ContrastProvider>` — im lặng trả mặc định sẽ khiến\n * nút gạt trông như hỏng mà không có gì chỉ ra nguyên nhân.\n */\nexport function useContrast(): ContrastContextValue {\n const ctx = useContext(ContrastContext);\n if (!ctx) throw new Error(\"useContrast() phải nằm trong <ContrastProvider>. Bọc nó ở root layout.\");\n return ctx;\n}\n"],"mappings":";AA8FI,SACE,KADF;AA5FJ,SAAS,eAAe,aAAa,YAAY,WAAW,SAAS,gBAAgB;AACrF,SAAS,oBAAoB,sBAAyC,sBAAsB;AAU5F,MAAM,kBAAkB,cAA2C,IAAI;AAmChE,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,kBAAkB;AAAA,EAClB,aAAa;AACf,GAA0B;AACxB,QAAM,CAAC,UAAU,gBAAgB,IAAI,SAAuB,eAAe;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAI5C,YAAU,MAAM;AACd,QAAI,SAAwB;AAC5B,QAAI;AACF,eAAS,OAAO,aAAa,QAAQ,UAAU;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI,eAAe,MAAM,EAAG,kBAAiB,MAAM;AACnD,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,UAAU,CAAC;AAEf,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,SAAS;AACtB,QAAI,aAAa,SAAU,MAAK,gBAAgB,kBAAkB;AAAA,QAC7D,MAAK,aAAa,oBAAoB,QAAQ;AAAA,EACrD,GAAG,CAAC,UAAU,OAAO,CAAC;AAEtB,QAAM,cAAc;AAAA,IAClB,CAAC,SAAuB;AACtB,uBAAiB,IAAI;AACrB,UAAI;AACF,eAAO,aAAa,QAAQ,YAAY,IAAI;AAAA,MAC9C,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,UAAU,aAAa,QAAQ;AAAA,IACxC,CAAC,UAAU,aAAa,OAAO;AAAA,EACjC;AAEA,SACE,qBAAC,gBAAgB,UAAhB,EAAyB,OACxB;AAAA,wBAAC,kBAAe,YAAwB;AAAA,IACvC;AAAA,KACH;AAEJ;AASO,SAAS,eAAe,EAAE,aAAa,qBAAqB,GAA4B;AAE7F,QAAM,SAAS,8CAA8C,KAAK,UAAU,UAAU,CAAC,yDAAyD,KAAK,UAAU,kBAAkB,CAAC;AAElL,SAAO,oBAAC,YAAO,0BAAwB,MAAC,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AACvF;AAMO,SAAS,cAAoC;AAClD,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+FAAwE;AAClG,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ContrastToggleLabels, ContrastMode } from './contrast-constants.js';
|
|
3
|
+
|
|
4
|
+
interface ContrastToggleProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Override chuỗi hiển thị (mặc định tiếng Việt). */
|
|
7
|
+
labels?: Partial<ContrastToggleLabels>;
|
|
8
|
+
/** Gọi sau khi người dùng đổi — để app hiện toast hoặc ghi log. */
|
|
9
|
+
onContrastChange?: (mode: ContrastMode) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Nút gạt chọn BẢNG MÀU (bản duyệt comitor-ds ⇄ bản tương phản cao). Trục này độc lập với
|
|
13
|
+
* sáng/tối: hai bảng màu đều có bản light và bản dark riêng.
|
|
14
|
+
*
|
|
15
|
+
* Đặt ở trang cài đặt của app, cạnh `<ThemeToggle>`. Phải nằm trong `<ContrastProvider>`.
|
|
16
|
+
*
|
|
17
|
+
* Chống lệch hydration bằng cờ `mounted` của provider — cùng lối với `ThemeToggle`: lựa chọn thật
|
|
18
|
+
* chỉ đọc được ở client (localStorage), render thẳng trạng thái nút ngay lần đầu sẽ lệch với HTML
|
|
19
|
+
* do server sinh ra. Trước khi mount thì nút hiện ở trạng thái mặc định và bị khoá, để layout
|
|
20
|
+
* không nhảy và người dùng không bấm trúng một trạng thái sắp bị thay.
|
|
21
|
+
*/
|
|
22
|
+
declare function ContrastToggle({ className, labels: labelsProp, onContrastChange }: ContrastToggleProps): React.JSX.Element;
|
|
23
|
+
|
|
24
|
+
export { ContrastToggle, type ContrastToggleProps };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Switch } from "../components/ui/switch.js";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { DEFAULT_CONTRAST_TOGGLE_LABELS } from "./contrast-constants.js";
|
|
6
|
+
import { useContrast } from "./contrast-provider.js";
|
|
7
|
+
function ContrastToggle({ className, labels: labelsProp, onContrastChange }) {
|
|
8
|
+
const { contrast, setContrast, mounted } = useContrast();
|
|
9
|
+
const labels = { ...DEFAULT_CONTRAST_TOGGLE_LABELS, ...labelsProp };
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
Switch,
|
|
12
|
+
{
|
|
13
|
+
className: cn(className),
|
|
14
|
+
checked: contrast === "high",
|
|
15
|
+
disabled: !mounted,
|
|
16
|
+
label: labels.label,
|
|
17
|
+
description: labels.description,
|
|
18
|
+
onCheckedChange: (checked) => {
|
|
19
|
+
const next = checked ? "high" : "normal";
|
|
20
|
+
setContrast(next);
|
|
21
|
+
onContrastChange?.(next);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
ContrastToggle
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=contrast-toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/contrast-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { Switch } from \"../components/ui/switch.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { type ContrastMode, type ContrastToggleLabels, DEFAULT_CONTRAST_TOGGLE_LABELS } from \"./contrast-constants.js\";\nimport { useContrast } from \"./contrast-provider.js\";\n\nexport interface ContrastToggleProps {\n className?: string;\n /** Override chuỗi hiển thị (mặc định tiếng Việt). */\n labels?: Partial<ContrastToggleLabels>;\n /** Gọi sau khi người dùng đổi — để app hiện toast hoặc ghi log. */\n onContrastChange?: (mode: ContrastMode) => void;\n}\n\n/**\n * Nút gạt chọn BẢNG MÀU (bản duyệt comitor-ds ⇄ bản tương phản cao). Trục này độc lập với\n * sáng/tối: hai bảng màu đều có bản light và bản dark riêng.\n *\n * Đặt ở trang cài đặt của app, cạnh `<ThemeToggle>`. Phải nằm trong `<ContrastProvider>`.\n *\n * Chống lệch hydration bằng cờ `mounted` của provider — cùng lối với `ThemeToggle`: lựa chọn thật\n * chỉ đọc được ở client (localStorage), render thẳng trạng thái nút ngay lần đầu sẽ lệch với HTML\n * do server sinh ra. Trước khi mount thì nút hiện ở trạng thái mặc định và bị khoá, để layout\n * không nhảy và người dùng không bấm trúng một trạng thái sắp bị thay.\n */\nexport function ContrastToggle({ className, labels: labelsProp, onContrastChange }: ContrastToggleProps) {\n const { contrast, setContrast, mounted } = useContrast();\n const labels: ContrastToggleLabels = { ...DEFAULT_CONTRAST_TOGGLE_LABELS, ...labelsProp };\n\n return (\n <Switch\n className={cn(className)}\n checked={contrast === \"high\"}\n disabled={!mounted}\n label={labels.label}\n description={labels.description}\n onCheckedChange={(checked) => {\n const next: ContrastMode = checked ? \"high\" : \"normal\";\n setContrast(next);\n onContrastChange?.(next);\n }}\n />\n );\n}\n"],"mappings":";AA+BI;AA7BJ,SAAS,cAAc;AACvB,SAAS,UAAU;AACnB,SAAuD,sCAAsC;AAC7F,SAAS,mBAAmB;AAqBrB,SAAS,eAAe,EAAE,WAAW,QAAQ,YAAY,iBAAiB,GAAwB;AACvG,QAAM,EAAE,UAAU,aAAa,QAAQ,IAAI,YAAY;AACvD,QAAM,SAA+B,EAAE,GAAG,gCAAgC,GAAG,WAAW;AAExF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,SAAS;AAAA,MACvB,SAAS,aAAa;AAAA,MACtB,UAAU,CAAC;AAAA,MACX,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAAqB,UAAU,SAAS;AAC9C,oBAAY,IAAI;AAChB,2BAAmB,IAAI;AAAA,MACzB;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export { AppLauncher, AppLauncherPanel, AppLauncherPanelProps, AppLauncherProps
|
|
|
4
4
|
export { AppShell, AppShellLayout, AppShellProps } from './app-shell.js';
|
|
5
5
|
export { ComitorLockup, ComitorLockupProps, ComitorLockupSize, ComitorLockupVariant } from './comitor-lockup.js';
|
|
6
6
|
export { ComitorLogo, ComitorLogoProps, ComitorLogoVariant } from './comitor-logo.js';
|
|
7
|
+
export { CONTRAST_ATTRIBUTE, CONTRAST_MODES, CONTRAST_STORAGE_KEY, ContrastMode, ContrastToggleLabels, DEFAULT_CONTRAST_TOGGLE_LABELS, isContrastMode } from './contrast-constants.js';
|
|
8
|
+
export { ContrastContextValue, ContrastProvider, ContrastProviderProps, ContrastScript, useContrast } from './contrast-provider.js';
|
|
9
|
+
export { ContrastToggle, ContrastToggleProps } from './contrast-toggle.js';
|
|
7
10
|
export { MobileMenu, MobileMenuProps } from './mobile-menu.js';
|
|
8
11
|
export { NotificationsButton, NotificationsButtonProps } from './notifications-button.js';
|
|
9
12
|
export { ShellContextValue, ShellProvider, ShellProviderProps, useShell } from './shell-context.js';
|
package/dist/shell/index.js
CHANGED
|
@@ -4,6 +4,9 @@ export * from "./app-launcher.js";
|
|
|
4
4
|
export * from "./app-shell.js";
|
|
5
5
|
export * from "./comitor-lockup.js";
|
|
6
6
|
export * from "./comitor-logo.js";
|
|
7
|
+
export * from "./contrast-constants.js";
|
|
8
|
+
export * from "./contrast-provider.js";
|
|
9
|
+
export * from "./contrast-toggle.js";
|
|
7
10
|
export * from "./mobile-menu.js";
|
|
8
11
|
export * from "./notifications-button.js";
|
|
9
12
|
export * from "./shell-context.js";
|
package/dist/shell/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/shell\" — Tier 3: khung ứng dụng (AppShell, Sidebar, AppHeader,\n * WorkspaceSwitcher, AppLauncher, UserMenu, ThemeProvider…).\n *\n * Tách khỏi entry chính vì tầng này kéo theo `next/navigation` và `next-themes` — hai\n * peer OPTIONAL. App không chạy trên Next thì đừng import entry này (hoặc tự cấp\n * `ShellLinkComponent` riêng qua `ShellProvider`).\n *\n * Toàn bộ file ở đây là client component: có state, có routing, có phím tắt.\n */\n\n/**\n * Toaster bám theo next-themes — về tier thì nó là primitive (`components/ui/sonner.tsx`),\n * nhưng đặt cửa ra ở entry shell vì nó gọi `useTheme()`, mà `next-themes` chỉ là peer\n * OPTIONAL: để ở entry chính là ép mọi app phải cài next-themes.\n * Dùng chung với `ThemeProvider` ở trên. Nếu app KHÔNG dùng next-themes thì lấy composite\n * `Toaster` ở \"@comitor/ui\" và tự truyền prop `theme`.\n */\nexport { SonnerToaster, type SonnerToasterProps } from \"../components/ui/sonner.js\";\nexport * from \"./app-header.js\";\nexport * from \"./app-launcher.js\";\nexport * from \"./app-shell.js\";\nexport * from \"./comitor-lockup.js\";\nexport * from \"./comitor-logo.js\";\nexport * from \"./mobile-menu.js\";\nexport * from \"./notifications-button.js\";\nexport * from \"./shell-context.js\";\nexport * from \"./shell-link.js\";\nexport * from \"./sidebar.js\";\nexport * from \"./sidebar-group.js\";\nexport * from \"./sidebar-menu-item.js\";\nexport * from \"./theme-provider.js\";\nexport * from \"./theme-toggle.js\";\nexport * from \"./types.js\";\nexport * from \"./user-menu.js\";\nexport * from \"./utils.js\";\nexport * from \"./workspace-switcher.js\";\n"],"mappings":"AAkBA,SAAS,qBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/shell\" — Tier 3: khung ứng dụng (AppShell, Sidebar, AppHeader,\n * WorkspaceSwitcher, AppLauncher, UserMenu, ThemeProvider…).\n *\n * Tách khỏi entry chính vì tầng này kéo theo `next/navigation` và `next-themes` — hai\n * peer OPTIONAL. App không chạy trên Next thì đừng import entry này (hoặc tự cấp\n * `ShellLinkComponent` riêng qua `ShellProvider`).\n *\n * Toàn bộ file ở đây là client component: có state, có routing, có phím tắt.\n */\n\n/**\n * Toaster bám theo next-themes — về tier thì nó là primitive (`components/ui/sonner.tsx`),\n * nhưng đặt cửa ra ở entry shell vì nó gọi `useTheme()`, mà `next-themes` chỉ là peer\n * OPTIONAL: để ở entry chính là ép mọi app phải cài next-themes.\n * Dùng chung với `ThemeProvider` ở trên. Nếu app KHÔNG dùng next-themes thì lấy composite\n * `Toaster` ở \"@comitor/ui\" và tự truyền prop `theme`.\n */\nexport { SonnerToaster, type SonnerToasterProps } from \"../components/ui/sonner.js\";\nexport * from \"./app-header.js\";\nexport * from \"./app-launcher.js\";\nexport * from \"./app-shell.js\";\nexport * from \"./comitor-lockup.js\";\nexport * from \"./comitor-logo.js\";\n/**\n * Hằng của trục tương phản xuất THẲNG từ module thuần — root layout là Server Component và nó\n * đọc CONTRAST_STORAGE_KEY. Đi vòng qua contrast-provider.tsx (\"use client\") là hằng thành proxy\n * và ra `undefined` mà không có lỗi nào.\n */\nexport * from \"./contrast-constants.js\";\nexport * from \"./contrast-provider.js\";\nexport * from \"./contrast-toggle.js\";\nexport * from \"./mobile-menu.js\";\nexport * from \"./notifications-button.js\";\nexport * from \"./shell-context.js\";\nexport * from \"./shell-link.js\";\nexport * from \"./sidebar.js\";\nexport * from \"./sidebar-group.js\";\nexport * from \"./sidebar-menu-item.js\";\nexport * from \"./theme-provider.js\";\nexport * from \"./theme-toggle.js\";\nexport * from \"./types.js\";\nexport * from \"./user-menu.js\";\nexport * from \"./utils.js\";\nexport * from \"./workspace-switcher.js\";\n"],"mappings":"AAkBA,SAAS,qBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAMd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|