@bearmenu/ui 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,10 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const alertVariants: (props?: ({
6
- variant?: "default" | "destructive" | "warning" | "success" | null | undefined;
6
+ variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
7
7
  } & class_variance_authority_types.ClassProp) | undefined) => string;
8
8
  declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
9
- variant?: "default" | "destructive" | "warning" | "success" | null | undefined;
9
+ variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
11
11
  declare const AlertTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
12
12
  declare const AlertDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "default" | "destructive" | "warning" | "success" | "outline" | "secondary" | "glass" | "brown" | "overlay" | null | undefined;
7
+ variant?: "default" | "destructive" | "outline" | "secondary" | "glass" | "brown" | "success" | "warning" | "overlay" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
10
10
  }
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "default" | "destructive" | "link" | "outline" | "secondary" | "ghost" | "glass" | "brown" | "antagonist" | null | undefined;
6
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "glass" | "brown" | "antagonist" | null | undefined;
7
7
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
8
8
  fullWidth?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -3,12 +3,12 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const cardVariants: (props?: ({
6
- variant?: "muted" | "outline" | "glass" | "surface" | null | undefined;
6
+ variant?: "outline" | "glass" | "muted" | "surface" | null | undefined;
7
7
  size?: "default" | "sm" | "lg" | null | undefined;
8
8
  interactive?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
11
- variant?: "muted" | "outline" | "glass" | "surface" | null | undefined;
11
+ variant?: "outline" | "glass" | "muted" | "surface" | null | undefined;
12
12
  size?: "default" | "sm" | "lg" | null | undefined;
13
13
  interactive?: boolean | null | undefined;
14
14
  } & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
@@ -4,11 +4,11 @@ import { DialogProps } from '@radix-ui/react-dialog';
4
4
 
5
5
  declare const Command: React.ForwardRefExoticComponent<Omit<{
6
6
  children?: React.ReactNode;
7
- } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
7
+ } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
8
8
  ref?: React.Ref<HTMLDivElement>;
9
9
  } & {
10
10
  asChild?: boolean;
11
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
11
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
12
12
  label?: string;
13
13
  shouldFilter?: boolean;
14
14
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -28,61 +28,61 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
28
28
  ref?: React.Ref<HTMLInputElement>;
29
29
  } & {
30
30
  asChild?: boolean;
31
- }, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "type" | "value"> & {
31
+ }, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "type" | "value"> & {
32
32
  value?: string;
33
33
  onValueChange?: (search: string) => void;
34
34
  } & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
35
35
  declare const CommandList: React.ForwardRefExoticComponent<Omit<{
36
36
  children?: React.ReactNode;
37
- } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
37
+ } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
38
38
  ref?: React.Ref<HTMLDivElement>;
39
39
  } & {
40
40
  asChild?: boolean;
41
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
41
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
42
42
  label?: string;
43
43
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
44
44
  declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
45
45
  children?: React.ReactNode;
46
- } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
46
+ } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
47
47
  ref?: React.Ref<HTMLDivElement>;
48
48
  } & {
49
49
  asChild?: boolean;
50
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
51
51
  declare const CommandLoading: React.ForwardRefExoticComponent<Omit<{
52
52
  children?: React.ReactNode;
53
- } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
53
+ } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
54
54
  ref?: React.Ref<HTMLDivElement>;
55
55
  } & {
56
56
  asChild?: boolean;
57
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
57
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
58
58
  progress?: number;
59
59
  label?: string;
60
60
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
61
61
  declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
62
62
  children?: React.ReactNode;
63
- } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
63
+ } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
64
64
  ref?: React.Ref<HTMLDivElement>;
65
65
  } & {
66
66
  asChild?: boolean;
67
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "heading" | "value"> & {
67
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
68
68
  heading?: React.ReactNode;
69
69
  value?: string;
70
70
  forceMount?: boolean;
71
71
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
72
- declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
72
+ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
73
73
  ref?: React.Ref<HTMLDivElement>;
74
74
  } & {
75
75
  asChild?: boolean;
76
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key"> & {
76
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
77
77
  alwaysRender?: boolean;
78
78
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
79
79
  declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
80
80
  children?: React.ReactNode;
81
- } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof React.HTMLAttributes<HTMLDivElement> | "key"> & {
81
+ } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
82
82
  ref?: React.Ref<HTMLDivElement>;
83
83
  } & {
84
84
  asChild?: boolean;
85
- }, keyof React.HTMLAttributes<HTMLDivElement> | "asChild" | "key">, "onSelect" | "disabled" | "value"> & {
85
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
86
86
  disabled?: boolean;
87
87
  onSelect?: (value: string) => void;
88
88
  value?: string;
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
- declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
3
+ declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
4
4
  /** Native input type. Defaults to "date". */
5
5
  type?: "date" | "datetime-local" | "time" | "month" | "week";
6
6
  size?: "sm" | "default" | "lg";
@@ -3,12 +3,12 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const itemVariants: (props?: ({
6
- variant?: "default" | "muted" | "outline" | null | undefined;
6
+ variant?: "default" | "outline" | "muted" | null | undefined;
7
7
  size?: "default" | "sm" | "lg" | null | undefined;
8
8
  interactive?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  declare const Item: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
11
- variant?: "default" | "muted" | "outline" | null | undefined;
11
+ variant?: "default" | "outline" | "muted" | null | undefined;
12
12
  size?: "default" | "sm" | "lg" | null | undefined;
13
13
  interactive?: boolean | null | undefined;
14
14
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+
3
+ type Orientation = "vertical" | "horizontal";
4
+ type FadeColor = "background" | "card" | "muted";
5
+ type Size = "sm" | "md" | "lg";
6
+ interface ScrollFadeProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ orientation?: Orientation;
8
+ fadeColor?: FadeColor;
9
+ size?: Size;
10
+ /** Class names applied to the inner scrollable element (where overflow lives). */
11
+ scrollClassName?: string;
12
+ }
13
+ declare const ScrollFade: React.ForwardRefExoticComponent<ScrollFadeProps & React.RefAttributes<HTMLDivElement>>;
14
+
15
+ export { ScrollFade, type ScrollFadeProps };
@@ -0,0 +1,116 @@
1
+ import { cn } from '../chunk-FEK5XGZW.js';
2
+ import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
3
+ import * as React from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ var fromMap = {
7
+ background: "from-background",
8
+ card: "from-card",
9
+ muted: "from-muted"
10
+ };
11
+ var sizeMap = {
12
+ vertical: { sm: "h-6", md: "h-12", lg: "h-20" },
13
+ horizontal: { sm: "w-8", md: "w-16", lg: "w-28" }
14
+ };
15
+ var ScrollFade = React.forwardRef(
16
+ (_a, ref) => {
17
+ var _b = _a, {
18
+ orientation = "vertical",
19
+ fadeColor = "background",
20
+ size = "md",
21
+ className,
22
+ scrollClassName,
23
+ children
24
+ } = _b, props = __objRest(_b, [
25
+ "orientation",
26
+ "fadeColor",
27
+ "size",
28
+ "className",
29
+ "scrollClassName",
30
+ "children"
31
+ ]);
32
+ const innerRef = React.useRef(null);
33
+ React.useImperativeHandle(ref, () => innerRef.current);
34
+ const [atStart, setAtStart] = React.useState(true);
35
+ const [atEnd, setAtEnd] = React.useState(true);
36
+ const update = React.useCallback(() => {
37
+ const el = innerRef.current;
38
+ if (!el) return;
39
+ if (orientation === "vertical") {
40
+ const overflow = el.scrollHeight - el.clientHeight;
41
+ setAtStart(el.scrollTop <= 1);
42
+ setAtEnd(overflow <= 1 || el.scrollTop >= overflow - 1);
43
+ } else {
44
+ const overflow = el.scrollWidth - el.clientWidth;
45
+ setAtStart(el.scrollLeft <= 1);
46
+ setAtEnd(overflow <= 1 || el.scrollLeft >= overflow - 1);
47
+ }
48
+ }, [orientation]);
49
+ React.useEffect(() => {
50
+ const el = innerRef.current;
51
+ if (!el) return;
52
+ update();
53
+ el.addEventListener("scroll", update, { passive: true });
54
+ const ro = new ResizeObserver(update);
55
+ ro.observe(el);
56
+ return () => {
57
+ el.removeEventListener("scroll", update);
58
+ ro.disconnect();
59
+ };
60
+ }, [update]);
61
+ const fromClass = fromMap[fadeColor];
62
+ const fadeSize = sizeMap[orientation][size];
63
+ return /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: cn(
67
+ "relative isolate flex",
68
+ orientation === "vertical" ? "flex-col" : "flex-row",
69
+ className
70
+ ),
71
+ children: [
72
+ /* @__PURE__ */ jsx(
73
+ "div",
74
+ __spreadProps(__spreadValues({
75
+ ref: innerRef,
76
+ className: cn(
77
+ "min-h-0 min-w-0 flex-1",
78
+ orientation === "vertical" ? "overflow-y-auto" : "overflow-x-auto",
79
+ scrollClassName
80
+ )
81
+ }, props), {
82
+ children
83
+ })
84
+ ),
85
+ /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ "aria-hidden": true,
89
+ className: cn(
90
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
91
+ orientation === "vertical" ? `inset-x-0 top-0 ${fadeSize} bg-gradient-to-b` : `inset-y-0 left-0 ${fadeSize} bg-gradient-to-r`,
92
+ fromClass,
93
+ atStart ? "opacity-0" : "opacity-100"
94
+ )
95
+ }
96
+ ),
97
+ /* @__PURE__ */ jsx(
98
+ "div",
99
+ {
100
+ "aria-hidden": true,
101
+ className: cn(
102
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
103
+ orientation === "vertical" ? `inset-x-0 bottom-0 ${fadeSize} bg-gradient-to-t` : `inset-y-0 right-0 ${fadeSize} bg-gradient-to-l`,
104
+ fromClass,
105
+ atEnd ? "opacity-0" : "opacity-100"
106
+ )
107
+ }
108
+ )
109
+ ]
110
+ }
111
+ );
112
+ }
113
+ );
114
+ ScrollFade.displayName = "ScrollFade";
115
+
116
+ export { ScrollFade };
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const spinnerVariants: (props?: ({
6
6
  size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
7
- tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
7
+ tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
10
10
  size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
11
- tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
11
+ tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  /** Visually-hidden label for screen readers. Defaults to "Loading". */
14
14
  label?: string;
@@ -3,11 +3,11 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const tagVariants: (props?: ({
6
- variant?: "default" | "destructive" | "warning" | "success" | "primary" | "outline" | null | undefined;
6
+ variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | null | undefined;
7
7
  size?: "default" | "sm" | "lg" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
10
- variant?: "default" | "destructive" | "warning" | "success" | "primary" | "outline" | null | undefined;
10
+ variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | null | undefined;
11
11
  size?: "default" | "sm" | "lg" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  onRemove?: () => void;
@@ -4,9 +4,9 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const textVariants: (props?: ({
7
- variant?: "small" | "display" | "h1" | "h2" | "h3" | "h4" | "body-lg" | "body" | "body-sm" | "caption" | "label" | "overline" | null | undefined;
8
- color?: "default" | "destructive" | "inherit" | "muted" | "primary" | "primary-foreground" | null | undefined;
9
- weight?: "bold" | "normal" | "medium" | "semibold" | null | undefined;
7
+ variant?: "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "small" | "display" | "body-lg" | "body-sm" | "overline" | null | undefined;
8
+ color?: "inherit" | "default" | "destructive" | "muted" | "primary" | "primary-foreground" | null | undefined;
9
+ weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
10
10
  leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
11
11
  tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
12
12
  wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
@@ -1,5 +1,5 @@
1
- import { useToast } from '../chunk-EBAMCZDW.js';
2
1
  import { ToastProvider, Toast, ToastTitle, ToastDescription, ToastClose, ToastViewport } from '../chunk-7PPCH42H.js';
2
+ import { useToast } from '../chunk-EBAMCZDW.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import { __objRest, __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -67,6 +67,7 @@
67
67
  "./components/radio-group": "./src/components/radio-group.tsx",
68
68
  "./components/rating": "./src/components/rating.tsx",
69
69
  "./components/scroll-area": "./src/components/scroll-area.tsx",
70
+ "./components/scroll-fade": "./src/components/scroll-fade.tsx",
70
71
  "./components/search-bar": "./src/components/search-bar.tsx",
71
72
  "./components/searchable-select": "./src/components/searchable-select.tsx",
72
73
  "./components/select": "./src/components/select.tsx",
@@ -0,0 +1,98 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { ScrollFade } from "./scroll-fade";
3
+
4
+ const meta = {
5
+ title: "Components/ScrollFade",
6
+ component: ScrollFade,
7
+ tags: ["autodocs"],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ "Scroll-position-aware fade overlay. Hides when at the corresponding edge, fades in when there's more content to scroll. Works vertically or horizontally and matches a chosen surface color.",
13
+ },
14
+ },
15
+ },
16
+ } satisfies Meta<typeof ScrollFade>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ const longList = Array.from({ length: 30 });
22
+ const wideList = Array.from({ length: 20 });
23
+
24
+ export const Vertical: Story = {
25
+ args: {
26
+ orientation: "vertical",
27
+ fadeColor: "background",
28
+ size: "md",
29
+ },
30
+ render: (args) => (
31
+ <ScrollFade {...args} className="h-[320px] w-[360px] rounded-lg border" scrollClassName="p-4">
32
+ <ul className="space-y-3">
33
+ {longList.map((_, i) => (
34
+ <li key={i} className="text-sm text-muted-foreground">
35
+ Item #{i + 1} — scroll to reveal the fades at the edges.
36
+ </li>
37
+ ))}
38
+ </ul>
39
+ </ScrollFade>
40
+ ),
41
+ };
42
+
43
+ export const Horizontal: Story = {
44
+ args: {
45
+ orientation: "horizontal",
46
+ fadeColor: "background",
47
+ size: "md",
48
+ },
49
+ render: (args) => (
50
+ <ScrollFade {...args} className="w-[480px] rounded-lg border" scrollClassName="flex gap-3 p-4">
51
+ {wideList.map((_, i) => (
52
+ <div
53
+ key={i}
54
+ className="flex h-24 w-32 shrink-0 items-center justify-center rounded-md bg-muted text-sm"
55
+ >
56
+ Card {i + 1}
57
+ </div>
58
+ ))}
59
+ </ScrollFade>
60
+ ),
61
+ };
62
+
63
+ export const InsideCard: Story = {
64
+ args: {
65
+ orientation: "vertical",
66
+ fadeColor: "card",
67
+ size: "md",
68
+ },
69
+ render: (args) => (
70
+ <div className="bg-card text-card-foreground w-[360px] rounded-2xl border shadow-md">
71
+ <div className="border-border border-b p-4 font-semibold">Drawer header</div>
72
+ <ScrollFade {...args} className="h-[280px]" scrollClassName="p-4">
73
+ <ul className="space-y-3">
74
+ {longList.map((_, i) => (
75
+ <li key={i} className="text-sm text-muted-foreground">
76
+ Row #{i + 1} — fades use <code>fadeColor=&quot;card&quot;</code> to match the surface.
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ </ScrollFade>
81
+ </div>
82
+ ),
83
+ };
84
+
85
+ export const NoOverflow: Story = {
86
+ args: {
87
+ orientation: "vertical",
88
+ fadeColor: "background",
89
+ size: "md",
90
+ },
91
+ render: (args) => (
92
+ <ScrollFade {...args} className="h-[200px] w-[320px] rounded-lg border" scrollClassName="p-4">
93
+ <p className="text-sm text-muted-foreground">
94
+ Content fits inside the box — both fades stay hidden.
95
+ </p>
96
+ </ScrollFade>
97
+ ),
98
+ };
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ type Orientation = "vertical" | "horizontal";
7
+ type FadeColor = "background" | "card" | "muted";
8
+ type Size = "sm" | "md" | "lg";
9
+
10
+ interface ScrollFadeProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ orientation?: Orientation;
12
+ fadeColor?: FadeColor;
13
+ size?: Size;
14
+ /** Class names applied to the inner scrollable element (where overflow lives). */
15
+ scrollClassName?: string;
16
+ }
17
+
18
+ // Static maps so Tailwind can statically analyze the class names.
19
+ const fromMap: Record<FadeColor, string> = {
20
+ background: "from-background",
21
+ card: "from-card",
22
+ muted: "from-muted",
23
+ };
24
+
25
+ const sizeMap: Record<Orientation, Record<Size, string>> = {
26
+ vertical: { sm: "h-6", md: "h-12", lg: "h-20" },
27
+ horizontal: { sm: "w-8", md: "w-16", lg: "w-28" },
28
+ };
29
+
30
+ const ScrollFade = React.forwardRef<HTMLDivElement, ScrollFadeProps>(
31
+ (
32
+ {
33
+ orientation = "vertical",
34
+ fadeColor = "background",
35
+ size = "md",
36
+ className,
37
+ scrollClassName,
38
+ children,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const innerRef = React.useRef<HTMLDivElement>(null);
44
+ React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);
45
+
46
+ const [atStart, setAtStart] = React.useState(true);
47
+ const [atEnd, setAtEnd] = React.useState(true);
48
+
49
+ const update = React.useCallback(() => {
50
+ const el = innerRef.current;
51
+ if (!el) return;
52
+ if (orientation === "vertical") {
53
+ const overflow = el.scrollHeight - el.clientHeight;
54
+ setAtStart(el.scrollTop <= 1);
55
+ setAtEnd(overflow <= 1 || el.scrollTop >= overflow - 1);
56
+ } else {
57
+ const overflow = el.scrollWidth - el.clientWidth;
58
+ setAtStart(el.scrollLeft <= 1);
59
+ setAtEnd(overflow <= 1 || el.scrollLeft >= overflow - 1);
60
+ }
61
+ }, [orientation]);
62
+
63
+ React.useEffect(() => {
64
+ const el = innerRef.current;
65
+ if (!el) return;
66
+ update();
67
+ el.addEventListener("scroll", update, { passive: true });
68
+ const ro = new ResizeObserver(update);
69
+ ro.observe(el);
70
+ return () => {
71
+ el.removeEventListener("scroll", update);
72
+ ro.disconnect();
73
+ };
74
+ }, [update]);
75
+
76
+ const fromClass = fromMap[fadeColor];
77
+ const fadeSize = sizeMap[orientation][size];
78
+
79
+ return (
80
+ <div
81
+ className={cn(
82
+ "relative isolate flex",
83
+ orientation === "vertical" ? "flex-col" : "flex-row",
84
+ className
85
+ )}
86
+ >
87
+ <div
88
+ ref={innerRef}
89
+ className={cn(
90
+ "min-h-0 min-w-0 flex-1",
91
+ orientation === "vertical" ? "overflow-y-auto" : "overflow-x-auto",
92
+ scrollClassName
93
+ )}
94
+ {...props}
95
+ >
96
+ {children}
97
+ </div>
98
+ <div
99
+ aria-hidden
100
+ className={cn(
101
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
102
+ orientation === "vertical"
103
+ ? `inset-x-0 top-0 ${fadeSize} bg-gradient-to-b`
104
+ : `inset-y-0 left-0 ${fadeSize} bg-gradient-to-r`,
105
+ fromClass,
106
+ atStart ? "opacity-0" : "opacity-100"
107
+ )}
108
+ />
109
+ <div
110
+ aria-hidden
111
+ className={cn(
112
+ "pointer-events-none absolute z-10 transition-opacity duration-300",
113
+ orientation === "vertical"
114
+ ? `inset-x-0 bottom-0 ${fadeSize} bg-gradient-to-t`
115
+ : `inset-y-0 right-0 ${fadeSize} bg-gradient-to-l`,
116
+ fromClass,
117
+ atEnd ? "opacity-0" : "opacity-100"
118
+ )}
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+ );
124
+ ScrollFade.displayName = "ScrollFade";
125
+
126
+ export { ScrollFade };
127
+ export type { ScrollFadeProps };