From 3aaed9b8f37a668b0429f1e2165c23fab963bd97 Mon Sep 17 00:00:00 2001 From: 2hu12 Date: Sun, 4 Jan 2026 16:42:02 +0800 Subject: [PATCH 1/3] feat: migrate base-ui to @base-ui/react and simplify scroll masks --- components/ui/tabs.tsx | 2 +- package.json | 2 +- pnpm-lock.yaml | 79 ++++++++++++++++++++-------- registry/base-ui/scroll-area.tsx | 90 +++++++------------------------- 4 files changed, 77 insertions(+), 96 deletions(-) diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index b4ad7cb..7f6f54f 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -2,7 +2,7 @@ import * as React from "react"; -import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; +import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; import posthog from "posthog-js"; import { cn } from "@/lib/utils"; diff --git a/package.json b/package.json index 840e5b9..84b1057 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache" }, "dependencies": { - "@base-ui-components/react": "1.0.0-beta.2", + "@base-ui/react": "^1.0.0", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f7cd2c..4ca85b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@base-ui-components/react': - specifier: 1.0.0-beta.2 - version: 1.0.0-beta.2(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@base-ui/react': + specifier: ^1.0.0 + version: 1.0.0(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -82,7 +82,7 @@ importers: version: 1.1.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zustand: specifier: ^5.0.7 - version: 5.0.7(@types/react@19.1.9)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + version: 5.0.7(@types/react@19.1.9)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@changesets/cli': specifier: ^2.29.5 @@ -247,6 +247,10 @@ packages: resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -259,8 +263,8 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@base-ui-components/react@1.0.0-beta.2': - resolution: {integrity: sha512-jfAUfSgXvsfr8mQi7r/6gLG8U1Ybr77NN8WK5IXXM0c/hBvFDBtvUfwDJACV0gXiYbSKpA+dRzZz01V1tULobA==} + '@base-ui/react@1.0.0': + resolution: {integrity: sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17 || ^18 || ^19 @@ -270,8 +274,8 @@ packages: '@types/react': optional: true - '@base-ui-components/utils@0.1.0': - resolution: {integrity: sha512-9+uaWyF1o/PgXqHLJnC81IIG0HlV3o9eFCQ5hWZDMx5NHrFk0rrwqEFGQOB8lti/rnbxNPi+kYYw1D4e8xSn/Q==} + '@base-ui/utils@0.2.3': + resolution: {integrity: sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -397,12 +401,21 @@ packages: '@floating-ui/dom@1.7.3': resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/react-dom@2.1.5': resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} @@ -4009,8 +4022,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -4193,6 +4206,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4466,6 +4484,8 @@ snapshots: '@babel/runtime@7.28.2': {} + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4489,28 +4509,28 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@base-ui-components/react@1.0.0-beta.2(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@base-ui/react@1.0.0(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.28.2 - '@base-ui-components/utils': 0.1.0(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@floating-ui/react-dom': 2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@babel/runtime': 7.28.4 + '@base-ui/utils': 0.2.3(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@floating-ui/utils': 0.2.10 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) reselect: 5.1.1 - tabbable: 6.2.0 - use-sync-external-store: 1.5.0(react@19.1.0) + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.9 - '@base-ui-components/utils@0.1.0(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@base-ui/utils@0.2.3(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 '@floating-ui/utils': 0.2.10 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) reselect: 5.1.1 - use-sync-external-store: 1.5.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) optionalDependencies: '@types/react': 19.1.9 @@ -4738,12 +4758,23 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + '@floating-ui/react-dom@2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/dom': 1.7.3 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@floating-ui/react-dom@2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@floating-ui/utils@0.2.10': {} '@humanfs/core@0.19.1': {} @@ -8763,7 +8794,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tabbable@6.2.0: {} + tabbable@6.4.0: {} tailwind-merge@3.3.1: {} @@ -8981,6 +9012,10 @@ snapshots: dependencies: react: 19.1.0 + use-sync-external-store@1.6.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} vary@1.1.2: {} @@ -9101,10 +9136,10 @@ snapshots: zod@3.25.76: {} - zustand@5.0.7(@types/react@19.1.9)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + zustand@5.0.7(@types/react@19.1.9)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)): optionalDependencies: '@types/react': 19.1.9 react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) zwitch@2.0.4: {} diff --git a/registry/base-ui/scroll-area.tsx b/registry/base-ui/scroll-area.tsx index 836fa80..c1de11e 100644 --- a/registry/base-ui/scroll-area.tsx +++ b/registry/base-ui/scroll-area.tsx @@ -2,19 +2,12 @@ import * as React from "react"; -import { ScrollArea as ScrollAreaPrimitive } from "@base-ui-components/react/scroll-area"; +import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; import { cn } from "@/lib/utils"; import { useTouchPrimary } from "@/hooks/use-has-primary-touch"; -type Mask = { - top: boolean; - bottom: boolean; - left: boolean; - right: boolean; -}; - export type ScrollAreaContextProps = { isTouch: boolean; type: "auto" | "always" | "scroll" | "hover"; @@ -39,54 +32,9 @@ const ScrollArea = React.forwardRef< maskClassName?: string; } >(({ className, children, type = "hover", maskHeight = 30, maskClassName, viewportClassName, ...props }, ref) => { - const [showMask, setShowMask] = React.useState({ - top: false, - bottom: false, - left: false, - right: false, - }); - const viewportRef = React.useRef(null); const isTouch = useTouchPrimary(); - const checkScrollability = React.useCallback(() => { - const element = viewportRef.current; - if (!element) return; - - const { scrollTop, scrollLeft, scrollWidth, clientWidth, scrollHeight, clientHeight } = element; - setShowMask((prev) => ({ - ...prev, - top: scrollTop > 0, - bottom: scrollTop + clientHeight < scrollHeight - 1, - left: scrollLeft > 0, - right: scrollLeft + clientWidth < scrollWidth - 1, - })); - }, []); - - React.useEffect(() => { - if (typeof window === "undefined") return; - - const element = viewportRef.current; - if (!element) return; - - const controller = new AbortController(); - const { signal } = controller; - - const resizeObserver = new ResizeObserver(checkScrollability); - resizeObserver.observe(element); - - element.addEventListener("scroll", checkScrollability, { signal }); - window.addEventListener("resize", checkScrollability, { signal }); - - // Run an initial check whenever dependencies change (including pointer mode) - checkScrollability(); - - return () => { - controller.abort(); - resizeObserver.disconnect(); - }; - }, [checkScrollability, isTouch]); - return ( {isTouch ? ( @@ -96,18 +44,18 @@ const ScrollArea = React.forwardRef< role="group" data-slot="scroll-area" aria-roledescription="scroll area" - className={cn("relative overflow-hidden", className)} + className={cn("group relative overflow-hidden", className)} >
{children}
- {maskHeight > 0 && } + {maskHeight > 0 && } ) : ( - {maskHeight > 0 && } + {maskHeight > 0 && } )}
@@ -166,13 +114,11 @@ const ScrollBar = React.forwardRef< ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName; const ScrollMask = ({ - showMask, - maskHeight, + size, className, ...props }: React.ComponentProps<"div"> & { - showMask: Mask; - maskHeight: number; + size: number; }) => { return ( <> @@ -181,17 +127,17 @@ const ScrollMask = ({ aria-hidden="true" style={ { - "--top-fade-height": showMask.top ? `${maskHeight}px` : "0px", - "--bottom-fade-height": showMask.bottom ? `${maskHeight}px` : "0px", + "--vertical-max-mask-height": `${size}px`, } as React.CSSProperties } className={cn( "pointer-events-none absolute inset-0 z-10", "before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']", "after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']", - "before:h-(--top-fade-height) after:h-(--bottom-fade-height)", - showMask.top ? "before:opacity-100" : "before:opacity-0", - showMask.bottom ? "after:opacity-100" : "after:opacity-0", + "before:h-0 group-data-overflow-y-start:before:h-(--vertical-max-mask-height)", + "after:h-0 group-data-overflow-y-end:after:h-(--vertical-max-mask-height)", + "before:opacity-0 group-data-overflow-y-start:before:opacity-100", + "after:opacity-0 group-data-overflow-y-end:after:opacity-100", "before:from-background before:bg-gradient-to-b before:to-transparent", "after:from-background after:bg-gradient-to-t after:to-transparent", className @@ -202,24 +148,24 @@ const ScrollMask = ({ aria-hidden="true" style={ { - "--left-fade-width": showMask.left ? `${maskHeight}px` : "0px", - "--right-fade-width": showMask.right ? `${maskHeight}px` : "0px", + "--horizontal-mask-width": `${size}px`, } as React.CSSProperties } className={cn( "pointer-events-none absolute inset-0 z-10", "before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']", "after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']", - "before:w-(--left-fade-width) after:w-(--right-fade-width)", - showMask.left ? "before:opacity-100" : "before:opacity-0", - showMask.right ? "after:opacity-100" : "after:opacity-0", + "before:w-0 group-data-overflow-x-start:before:w-(--horizontal-mask-width)", + "after:w-0 group-data-overflow-x-end:after:w-(--horizontal-mask-width)", + "before:opacity-0 group-data-overflow-x-start:before:opacity-100", + "after:opacity-0 group-data-overflow-x-end:after:opacity-100", "before:from-background before:bg-gradient-to-r before:to-transparent", "after:from-background after:bg-gradient-to-l after:to-transparent", className )} /> - ); + ) }; export { ScrollArea, ScrollBar }; From dda59795cae4e94f27a446d5c70a176057f15754 Mon Sep 17 00:00:00 2001 From: 2hu Date: Mon, 5 Jan 2026 01:13:06 +0800 Subject: [PATCH 2/3] add missing semicolon --- registry/base-ui/scroll-area.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/base-ui/scroll-area.tsx b/registry/base-ui/scroll-area.tsx index c1de11e..05002e8 100644 --- a/registry/base-ui/scroll-area.tsx +++ b/registry/base-ui/scroll-area.tsx @@ -165,7 +165,7 @@ const ScrollMask = ({ )} /> - ) + ); }; export { ScrollArea, ScrollBar }; From aedc16352c311a3aca3dd4d78f0e9791ed3a9573 Mon Sep 17 00:00:00 2001 From: 2hu12 Date: Mon, 5 Jan 2026 11:01:14 +0800 Subject: [PATCH 3/3] update CSS variable name --- registry/base-ui/scroll-area.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/base-ui/scroll-area.tsx b/registry/base-ui/scroll-area.tsx index 05002e8..e9e68b4 100644 --- a/registry/base-ui/scroll-area.tsx +++ b/registry/base-ui/scroll-area.tsx @@ -127,15 +127,15 @@ const ScrollMask = ({ aria-hidden="true" style={ { - "--vertical-max-mask-height": `${size}px`, + "--vertical-mask-height": `${size}px`, } as React.CSSProperties } className={cn( "pointer-events-none absolute inset-0 z-10", "before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']", "after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']", - "before:h-0 group-data-overflow-y-start:before:h-(--vertical-max-mask-height)", - "after:h-0 group-data-overflow-y-end:after:h-(--vertical-max-mask-height)", + "before:h-0 group-data-overflow-y-start:before:h-(--vertical-mask-height)", + "after:h-0 group-data-overflow-y-end:after:h-(--vertical-mask-height)", "before:opacity-0 group-data-overflow-y-start:before:opacity-100", "after:opacity-0 group-data-overflow-y-end:after:opacity-100", "before:from-background before:bg-gradient-to-b before:to-transparent",