react-scroll-indicators 0.1.0 → 0.2.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 CHANGED
@@ -17,17 +17,25 @@ yarn add react-scroll-indicators
17
17
  - `react` (>=17)
18
18
  - `react-dom` (>=17)
19
19
 
20
- If you use Tailwind CSS, the default indicator styles use Tailwind utility classes. You can override them with `indicatorClassName` or your own CSS.
20
+ No Tailwind or other CSS framework is required. The component uses plain CSS with BEM-style class names (e.g. `overflow-container`, `overflow-container__inner`, `overflow-container__indicator--left`). Import the default styles once in your app, or override with your own CSS via `className` / `containerClassName` / `indicatorClassName`.
21
21
 
22
22
  ## Usage
23
23
 
24
+ ### 1. Import the default styles (once)
25
+
26
+ ```tsx
27
+ import "react-scroll-indicators/styles";
28
+ ```
29
+
30
+ ### 2. Use the component
31
+
24
32
  ```tsx
25
33
  import { OverflowContainer } from "react-scroll-indicators";
26
34
 
27
35
  function MyComponent() {
28
36
  return (
29
- <OverflowContainer className="max-w-md h-48">
30
- <div className="flex gap-4">
37
+ <OverflowContainer className="max-w-md h-48" style={{ maxWidth: "28rem", height: "12rem" }}>
38
+ <div style={{ display: "flex", gap: "1rem" }}>
31
39
  {items.map((item) => (
32
40
  <Card key={item.id} {...item} />
33
41
  ))}
@@ -37,15 +45,32 @@ function MyComponent() {
37
45
  }
38
46
  ```
39
47
 
48
+ ### As a copyable component (shadcn-style)
49
+
50
+ You can use the component file directly so it lives in your codebase and you can edit it:
51
+
52
+ **Option A – Import the source file** (your bundler compiles it):
53
+
54
+ ```tsx
55
+ import OverflowContainer from "react-scroll-indicators/OverflowContainer";
56
+ ```
57
+
58
+ **Option B – Copy the file into your project** (e.g. `components/ui/OverflowContainer.tsx`):
59
+
60
+ 1. Copy from: `node_modules/react-scroll-indicators/src/OverflowContainer.tsx`
61
+ 2. Paste into your project (e.g. `components/ui/OverflowContainer.tsx`).
62
+ 3. Import from that path. The component only depends on `react`; you can keep or replace the small `cn()` helper.
63
+
40
64
  ### With vertical scroll
41
65
 
42
66
  ```tsx
43
67
  <OverflowContainer
44
68
  verticalScrollIndicators
45
69
  horizontalScrollIndicators={false}
46
- className="h-64 w-64"
70
+ className="my-container"
71
+ style={{ height: "16rem", width: "16rem" }}
47
72
  >
48
- <div className="flex flex-col gap-2">{/* tall content */}</div>
73
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>{/* tall content */}</div>
49
74
  </OverflowContainer>
50
75
  ```
51
76
 
@@ -67,6 +92,21 @@ function MyComponent() {
67
92
 
68
93
  All standard `div` HTML attributes are also supported (e.g. `style`, `aria-*`).
69
94
 
95
+ ### Class names (for custom CSS)
96
+
97
+ If you skip the default styles and style the component yourself, use these class names:
98
+
99
+ | Class | Element |
100
+ |-------|--------|
101
+ | `overflow-container` | Outer wrapper |
102
+ | `overflow-container__inner` | Scrollable content area |
103
+ | `overflow-container__indicators` | Layer that holds the indicator overlays |
104
+ | `overflow-container__indicator` | Base class for each indicator |
105
+ | `overflow-container__indicator--left` | Left edge indicator |
106
+ | `overflow-container__indicator--right` | Right edge indicator |
107
+ | `overflow-container__indicator--up` | Top edge indicator |
108
+ | `overflow-container__indicator--down` | Bottom edge indicator |
109
+
70
110
  ## Build
71
111
 
72
112
  ```bash
package/dist/index.cjs CHANGED
@@ -136,15 +136,15 @@ var OverflowContainer = (0, import_react.forwardRef)(
136
136
  {
137
137
  role: "region",
138
138
  "aria-label": "Scrollable content",
139
- className: cn("flex relative overflow-hidden", className),
139
+ className: cn("overflow-container", className),
140
140
  ...props,
141
141
  children: [
142
- showScrollIndicators && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "absolute left-0 top-0 flex w-full h-full z-50 bg-transparent pointer-events-none", children: [
142
+ showScrollIndicators && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "overflow-container__indicators", children: [
143
143
  canScrollLeft && horizontalScrollIndicators && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
144
144
  "div",
145
145
  {
146
146
  className: cn(
147
- "absolute h-full w-4 left-0 top-0 bg-gradient-to-l from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
147
+ "overflow-container__indicator overflow-container__indicator--left",
148
148
  indicatorClassName
149
149
  ),
150
150
  onMouseEnter: () => scrollOnHover && startScroll("left"),
@@ -155,7 +155,7 @@ var OverflowContainer = (0, import_react.forwardRef)(
155
155
  "div",
156
156
  {
157
157
  className: cn(
158
- "absolute h-full w-4 right-0 top-0 bg-gradient-to-r from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
158
+ "overflow-container__indicator overflow-container__indicator--right",
159
159
  indicatorClassName
160
160
  ),
161
161
  onMouseEnter: () => scrollOnHover && startScroll("right"),
@@ -166,7 +166,7 @@ var OverflowContainer = (0, import_react.forwardRef)(
166
166
  "div",
167
167
  {
168
168
  className: cn(
169
- "absolute w-full h-5 left-0 top-0 bg-gradient-to-t from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
169
+ "overflow-container__indicator overflow-container__indicator--up",
170
170
  indicatorClassName
171
171
  ),
172
172
  onMouseEnter: () => scrollOnHover && startScroll("up"),
@@ -177,7 +177,7 @@ var OverflowContainer = (0, import_react.forwardRef)(
177
177
  "div",
178
178
  {
179
179
  className: cn(
180
- "absolute w-full h-5 left-0 bottom-0 bg-gradient-to-b from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
180
+ "overflow-container__indicator overflow-container__indicator--down",
181
181
  indicatorClassName
182
182
  ),
183
183
  onMouseEnter: () => scrollOnHover && startScroll("down"),
@@ -188,10 +188,7 @@ var OverflowContainer = (0, import_react.forwardRef)(
188
188
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
189
189
  "div",
190
190
  {
191
- className: cn(
192
- "flex relative overflow-auto w-full",
193
- containerClassName
194
- ),
191
+ className: cn("overflow-container__inner", containerClassName),
195
192
  ref: containerRef,
196
193
  onScroll: updateScrollButtons,
197
194
  children
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/OverflowContainer.tsx"],"sourcesContent":["export { default as OverflowContainer } from \"./OverflowContainer\";\nexport type { OverflowContainerProps } from \"./OverflowContainer\";\n","/**\n * OverflowContainer – React component for smooth scrolling with hover-activated\n * indicators when content overflows. Supports horizontal and vertical scrolling.\n * @license MIT\n */\n\nimport {\n type ForwardedRef,\n forwardRef,\n useEffect,\n useRef,\n useState,\n} from \"react\";\n\ndeclare const process: { env?: { NODE_ENV?: string } };\n\nfunction cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\nexport interface OverflowContainerProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Custom className for the outer container */\n className?: string;\n\n /** Content to be rendered inside the scroll container */\n children: React.ReactNode;\n\n /**\n * Speed of the scroll animation in milliseconds. Lower = faster. Positive only.\n * @default 10\n */\n scrollSpeed?: number;\n\n /**\n * Distance to scroll in pixels per interval. Positive only.\n * @default 10\n */\n scrollDistance?: number;\n\n /**\n * Padding in pixels before the end to stop scrolling. Positive only.\n * @default 10\n */\n scrollEndPadding?: number;\n\n /** Custom className for the inner scrollable container */\n containerClassName?: string;\n\n /**\n * Whether to show scroll indicators when content overflows.\n * @default true\n */\n showScrollIndicators?: boolean;\n\n /**\n * Whether to show horizontal scroll indicators.\n * @default true\n */\n horizontalScrollIndicators?: boolean;\n\n /**\n * Whether to show vertical scroll indicators.\n * @default false\n */\n verticalScrollIndicators?: boolean;\n\n /** Custom className for scroll indicators (overrides default styles) */\n indicatorClassName?: string;\n\n /**\n * Whether to scroll when hovering over indicators.\n * @default true\n */\n scrollOnHover?: boolean;\n}\n\nconst OverflowContainer = forwardRef(\n (\n {\n className,\n children,\n scrollSpeed = 10,\n scrollDistance = 10,\n scrollEndPadding = 10,\n containerClassName,\n showScrollIndicators = true,\n horizontalScrollIndicators = true,\n verticalScrollIndicators = false,\n indicatorClassName,\n scrollOnHover = true,\n ...props\n }: OverflowContainerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n const [canScrollUp, setCanScrollUp] = useState(false);\n const [canScrollDown, setCanScrollDown] = useState(false);\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n useEffect(() => {\n if (process.env?.NODE_ENV === \"development\") {\n if (scrollSpeed < 0) {\n console.warn(\n \"OverflowContainer: scrollSpeed should be a positive number\"\n );\n }\n if (scrollDistance < 0) {\n console.warn(\n \"OverflowContainer: scrollDistance should be a positive number\"\n );\n }\n if (scrollEndPadding < 0) {\n console.warn(\n \"OverflowContainer: scrollEndPadding should be a positive number\"\n );\n }\n }\n }, [scrollSpeed, scrollDistance, scrollEndPadding]);\n\n const startScroll = (scrollDirection: \"left\" | \"right\" | \"up\" | \"down\") => {\n stopScroll();\n const isHorizontal =\n scrollDirection === \"left\" || scrollDirection === \"right\";\n const isBackward = scrollDirection === \"left\" || scrollDirection === \"up\";\n const scrollAmount = isBackward ? -scrollDistance : scrollDistance;\n const container = containerRef.current;\n if (!container) return;\n\n intervalRef.current = setInterval(() => {\n if (!container) return;\n\n const scrollPos = isHorizontal\n ? container.scrollLeft\n : container.scrollTop;\n const scrollSize = isHorizontal\n ? container.scrollWidth\n : container.scrollHeight;\n const clientSize = isHorizontal\n ? container.clientWidth\n : container.clientHeight;\n\n const canScroll = isBackward\n ? scrollPos > 0\n : scrollPos < scrollSize - clientSize;\n\n if (!canScroll) {\n stopScroll();\n return;\n }\n updateScrollButtons();\n container.scrollBy({\n [isHorizontal ? \"left\" : \"top\"]: scrollAmount,\n });\n }, scrollSpeed);\n };\n\n const updateScrollButtons = () => {\n const container = containerRef.current;\n if (!container) return;\n\n const canScrollLeftVal =\n container.scrollLeft > scrollEndPadding;\n const canScrollRightVal =\n container.scrollLeft <\n container.scrollWidth - container.clientWidth - scrollEndPadding;\n const canScrollUpVal = container.scrollTop > scrollEndPadding;\n const canScrollDownVal =\n container.scrollTop <\n container.scrollHeight - container.clientHeight - scrollEndPadding;\n\n setCanScrollLeft(canScrollLeftVal);\n setCanScrollRight(canScrollRightVal);\n setCanScrollUp(canScrollUpVal);\n setCanScrollDown(canScrollDownVal);\n };\n\n const stopScroll = () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n\n useEffect(() => {\n if (!forwardedRef) return;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(containerRef.current);\n } else {\n forwardedRef.current = containerRef.current;\n }\n }, [forwardedRef]);\n\n useEffect(() => {\n updateScrollButtons();\n const container = containerRef.current;\n if (!container) return;\n\n const resizeObserver = new ResizeObserver(updateScrollButtons);\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n stopScroll();\n };\n }, []);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Scrollable content\"\n className={cn(\"flex relative overflow-hidden\", className)}\n {...props}\n >\n {showScrollIndicators && (\n <div className=\"absolute left-0 top-0 flex w-full h-full z-50 bg-transparent pointer-events-none\">\n {canScrollLeft && horizontalScrollIndicators && (\n <div\n className={cn(\n \"absolute h-full w-4 left-0 top-0 bg-gradient-to-l from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"left\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollRight && horizontalScrollIndicators && (\n <div\n className={cn(\n \"absolute h-full w-4 right-0 top-0 bg-gradient-to-r from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"right\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollUp && verticalScrollIndicators && (\n <div\n className={cn(\n \"absolute w-full h-5 left-0 top-0 bg-gradient-to-t from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"up\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollDown && verticalScrollIndicators && (\n <div\n className={cn(\n \"absolute w-full h-5 left-0 bottom-0 bg-gradient-to-b from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"down\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n </div>\n )}\n <div\n className={cn(\n \"flex relative overflow-auto w-full\",\n containerClassName\n )}\n ref={containerRef}\n onScroll={updateScrollButtons}\n >\n {children}\n </div>\n </div>\n );\n }\n);\n\nOverflowContainer.displayName = \"OverflowContainer\";\n\nexport default OverflowContainer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMA,mBAMO;AA6MG;AAzMV,SAAS,MAAM,SAAwD;AACrE,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;AA2DA,IAAM,wBAAoB;AAAA,EACxB,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB;AAAA,IACA,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,2BAA2B;AAAA,IAC3B;AAAA,IACA,gBAAgB;AAAA,IAChB,GAAG;AAAA,EACL,GACA,iBACG;AACH,UAAM,mBAAe,qBAAuB,IAAI;AAChD,UAAM,CAAC,eAAe,gBAAgB,QAAI,uBAAS,KAAK;AACxD,UAAM,CAAC,gBAAgB,iBAAiB,QAAI,uBAAS,KAAK;AAC1D,UAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AACpD,UAAM,CAAC,eAAe,gBAAgB,QAAI,uBAAS,KAAK;AACxD,UAAM,kBAAc,qBAA8C,IAAI;AAEtE,gCAAU,MAAM;AACd,UAAI,QAAQ,KAAK,aAAa,eAAe;AAC3C,YAAI,cAAc,GAAG;AACnB,kBAAQ;AAAA,YACN;AAAA,UACF;AAAA,QACF;AACA,YAAI,iBAAiB,GAAG;AACtB,kBAAQ;AAAA,YACN;AAAA,UACF;AAAA,QACF;AACA,YAAI,mBAAmB,GAAG;AACxB,kBAAQ;AAAA,YACN;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,GAAG,CAAC,aAAa,gBAAgB,gBAAgB,CAAC;AAElD,UAAM,cAAc,CAAC,oBAAsD;AACzE,iBAAW;AACX,YAAM,eACJ,oBAAoB,UAAU,oBAAoB;AACpD,YAAM,aAAa,oBAAoB,UAAU,oBAAoB;AACrE,YAAM,eAAe,aAAa,CAAC,iBAAiB;AACpD,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,UAAW;AAEhB,kBAAY,UAAU,YAAY,MAAM;AACtC,YAAI,CAAC,UAAW;AAEhB,cAAM,YAAY,eACd,UAAU,aACV,UAAU;AACd,cAAM,aAAa,eACf,UAAU,cACV,UAAU;AACd,cAAM,aAAa,eACf,UAAU,cACV,UAAU;AAEd,cAAM,YAAY,aACd,YAAY,IACZ,YAAY,aAAa;AAE7B,YAAI,CAAC,WAAW;AACd,qBAAW;AACX;AAAA,QACF;AACA,4BAAoB;AACpB,kBAAU,SAAS;AAAA,UACjB,CAAC,eAAe,SAAS,KAAK,GAAG;AAAA,QACnC,CAAC;AAAA,MACH,GAAG,WAAW;AAAA,IAChB;AAEA,UAAM,sBAAsB,MAAM;AAChC,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,UAAW;AAEhB,YAAM,mBACJ,UAAU,aAAa;AACzB,YAAM,oBACJ,UAAU,aACV,UAAU,cAAc,UAAU,cAAc;AAClD,YAAM,iBAAiB,UAAU,YAAY;AAC7C,YAAM,mBACJ,UAAU,YACV,UAAU,eAAe,UAAU,eAAe;AAEpD,uBAAiB,gBAAgB;AACjC,wBAAkB,iBAAiB;AACnC,qBAAe,cAAc;AAC7B,uBAAiB,gBAAgB;AAAA,IACnC;AAEA,UAAM,aAAa,MAAM;AACvB,UAAI,YAAY,SAAS;AACvB,sBAAc,YAAY,OAAO;AACjC,oBAAY,UAAU;AAAA,MACxB;AAAA,IACF;AAEA,gCAAU,MAAM;AACd,UAAI,CAAC,aAAc;AACnB,UAAI,OAAO,iBAAiB,YAAY;AACtC,qBAAa,aAAa,OAAO;AAAA,MACnC,OAAO;AACL,qBAAa,UAAU,aAAa;AAAA,MACtC;AAAA,IACF,GAAG,CAAC,YAAY,CAAC;AAEjB,gCAAU,MAAM;AACd,0BAAoB;AACpB,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,UAAW;AAEhB,YAAM,iBAAiB,IAAI,eAAe,mBAAmB;AAC7D,qBAAe,QAAQ,SAAS;AAEhC,aAAO,MAAM;AACX,uBAAe,WAAW;AAC1B,mBAAW;AAAA,MACb;AAAA,IACF,GAAG,CAAC,CAAC;AAEL,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAW;AAAA,QACX,WAAW,GAAG,iCAAiC,SAAS;AAAA,QACvD,GAAG;AAAA,QAEH;AAAA,kCACC,6CAAC,SAAI,WAAU,oFACZ;AAAA,6BAAiB,8BAChB;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBACA,cAAc,MAAM,iBAAiB,YAAY,MAAM;AAAA,gBACvD,cAAc,MAAM,iBAAiB,WAAW;AAAA;AAAA,YAClD;AAAA,YAED,kBAAkB,8BACjB;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBACA,cAAc,MAAM,iBAAiB,YAAY,OAAO;AAAA,gBACxD,cAAc,MAAM,iBAAiB,WAAW;AAAA;AAAA,YAClD;AAAA,YAED,eAAe,4BACd;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBACA,cAAc,MAAM,iBAAiB,YAAY,IAAI;AAAA,gBACrD,cAAc,MAAM,iBAAiB,WAAW;AAAA;AAAA,YAClD;AAAA,YAED,iBAAiB,4BAChB;AAAA,cAAC;AAAA;AAAA,gBACC,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBACA,cAAc,MAAM,iBAAiB,YAAY,MAAM;AAAA,gBACvD,cAAc,MAAM,iBAAiB,WAAW;AAAA;AAAA,YAClD;AAAA,aAEJ;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cACA,KAAK;AAAA,cACL,UAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;AAEA,kBAAkB,cAAc;AAEhC,IAAO,4BAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/OverflowContainer.tsx"],"sourcesContent":["export { default as OverflowContainer } from \"./OverflowContainer\";\nexport type { OverflowContainerProps } from \"./OverflowContainer\";\n","/**\n * OverflowContainer – React component for smooth scrolling with hover-activated\n * indicators when content overflows. Supports horizontal and vertical scrolling.\n * @license MIT\n */\n\nimport {\n type ForwardedRef,\n forwardRef,\n useEffect,\n useRef,\n useState,\n} from \"react\";\n\ndeclare const process: { env?: { NODE_ENV?: string } };\n\nfunction cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\nexport interface OverflowContainerProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Custom className for the outer container */\n className?: string;\n\n /** Content to be rendered inside the scroll container */\n children: React.ReactNode;\n\n /**\n * Speed of the scroll animation in milliseconds. Lower = faster. Positive only.\n * @default 10\n */\n scrollSpeed?: number;\n\n /**\n * Distance to scroll in pixels per interval. Positive only.\n * @default 10\n */\n scrollDistance?: number;\n\n /**\n * Padding in pixels before the end to stop scrolling. Positive only.\n * @default 10\n */\n scrollEndPadding?: number;\n\n /** Custom className for the inner scrollable container */\n containerClassName?: string;\n\n /**\n * Whether to show scroll indicators when content overflows.\n * @default true\n */\n showScrollIndicators?: boolean;\n\n /**\n * Whether to show horizontal scroll indicators.\n * @default true\n */\n horizontalScrollIndicators?: boolean;\n\n /**\n * Whether to show vertical scroll indicators.\n * @default false\n */\n verticalScrollIndicators?: boolean;\n\n /** Custom className for scroll indicators (overrides default styles) */\n indicatorClassName?: string;\n\n /**\n * Whether to scroll when hovering over indicators.\n * @default true\n */\n scrollOnHover?: boolean;\n}\n\nconst OverflowContainer = forwardRef(\n (\n {\n className,\n children,\n scrollSpeed = 10,\n scrollDistance = 10,\n scrollEndPadding = 10,\n containerClassName,\n showScrollIndicators = true,\n horizontalScrollIndicators = true,\n verticalScrollIndicators = false,\n indicatorClassName,\n scrollOnHover = true,\n ...props\n }: OverflowContainerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n const [canScrollUp, setCanScrollUp] = useState(false);\n const [canScrollDown, setCanScrollDown] = useState(false);\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n useEffect(() => {\n if (process.env?.NODE_ENV === \"development\") {\n if (scrollSpeed < 0) {\n console.warn(\n \"OverflowContainer: scrollSpeed should be a positive number\"\n );\n }\n if (scrollDistance < 0) {\n console.warn(\n \"OverflowContainer: scrollDistance should be a positive number\"\n );\n }\n if (scrollEndPadding < 0) {\n console.warn(\n \"OverflowContainer: scrollEndPadding should be a positive number\"\n );\n }\n }\n }, [scrollSpeed, scrollDistance, scrollEndPadding]);\n\n const startScroll = (scrollDirection: \"left\" | \"right\" | \"up\" | \"down\") => {\n stopScroll();\n const isHorizontal =\n scrollDirection === \"left\" || scrollDirection === \"right\";\n const isBackward = scrollDirection === \"left\" || scrollDirection === \"up\";\n const scrollAmount = isBackward ? -scrollDistance : scrollDistance;\n const container = containerRef.current;\n if (!container) return;\n\n intervalRef.current = setInterval(() => {\n if (!container) return;\n\n const scrollPos = isHorizontal\n ? container.scrollLeft\n : container.scrollTop;\n const scrollSize = isHorizontal\n ? container.scrollWidth\n : container.scrollHeight;\n const clientSize = isHorizontal\n ? container.clientWidth\n : container.clientHeight;\n\n const canScroll = isBackward\n ? scrollPos > 0\n : scrollPos < scrollSize - clientSize;\n\n if (!canScroll) {\n stopScroll();\n return;\n }\n updateScrollButtons();\n container.scrollBy({\n [isHorizontal ? \"left\" : \"top\"]: scrollAmount,\n });\n }, scrollSpeed);\n };\n\n const updateScrollButtons = () => {\n const container = containerRef.current;\n if (!container) return;\n\n const canScrollLeftVal =\n container.scrollLeft > scrollEndPadding;\n const canScrollRightVal =\n container.scrollLeft <\n container.scrollWidth - container.clientWidth - scrollEndPadding;\n const canScrollUpVal = container.scrollTop > scrollEndPadding;\n const canScrollDownVal =\n container.scrollTop <\n container.scrollHeight - container.clientHeight - scrollEndPadding;\n\n setCanScrollLeft(canScrollLeftVal);\n setCanScrollRight(canScrollRightVal);\n setCanScrollUp(canScrollUpVal);\n setCanScrollDown(canScrollDownVal);\n };\n\n const stopScroll = () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n\n useEffect(() => {\n if (!forwardedRef) return;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(containerRef.current);\n } else {\n forwardedRef.current = containerRef.current;\n }\n }, [forwardedRef]);\n\n useEffect(() => {\n updateScrollButtons();\n const container = containerRef.current;\n if (!container) return;\n\n const resizeObserver = new ResizeObserver(updateScrollButtons);\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n stopScroll();\n };\n }, []);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Scrollable content\"\n className={cn(\"overflow-container\", className)}\n {...props}\n >\n {showScrollIndicators && (\n <div className=\"overflow-container__indicators\">\n {canScrollLeft && horizontalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--left\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"left\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollRight && horizontalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--right\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"right\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollUp && verticalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--up\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"up\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollDown && verticalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--down\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"down\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n </div>\n )}\n <div\n className={cn(\"overflow-container__inner\", containerClassName)}\n ref={containerRef}\n onScroll={updateScrollButtons}\n >\n {children}\n </div>\n </div>\n );\n }\n);\n\nOverflowContainer.displayName = \"OverflowContainer\";\n\nexport default OverflowContainer;\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -116,15 +116,15 @@ var OverflowContainer = forwardRef(
116
116
  {
117
117
  role: "region",
118
118
  "aria-label": "Scrollable content",
119
- className: cn("flex relative overflow-hidden", className),
119
+ className: cn("overflow-container", className),
120
120
  ...props,
121
121
  children: [
122
- showScrollIndicators && /* @__PURE__ */ jsxs("div", { className: "absolute left-0 top-0 flex w-full h-full z-50 bg-transparent pointer-events-none", children: [
122
+ showScrollIndicators && /* @__PURE__ */ jsxs("div", { className: "overflow-container__indicators", children: [
123
123
  canScrollLeft && horizontalScrollIndicators && /* @__PURE__ */ jsx(
124
124
  "div",
125
125
  {
126
126
  className: cn(
127
- "absolute h-full w-4 left-0 top-0 bg-gradient-to-l from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
127
+ "overflow-container__indicator overflow-container__indicator--left",
128
128
  indicatorClassName
129
129
  ),
130
130
  onMouseEnter: () => scrollOnHover && startScroll("left"),
@@ -135,7 +135,7 @@ var OverflowContainer = forwardRef(
135
135
  "div",
136
136
  {
137
137
  className: cn(
138
- "absolute h-full w-4 right-0 top-0 bg-gradient-to-r from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
138
+ "overflow-container__indicator overflow-container__indicator--right",
139
139
  indicatorClassName
140
140
  ),
141
141
  onMouseEnter: () => scrollOnHover && startScroll("right"),
@@ -146,7 +146,7 @@ var OverflowContainer = forwardRef(
146
146
  "div",
147
147
  {
148
148
  className: cn(
149
- "absolute w-full h-5 left-0 top-0 bg-gradient-to-t from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
149
+ "overflow-container__indicator overflow-container__indicator--up",
150
150
  indicatorClassName
151
151
  ),
152
152
  onMouseEnter: () => scrollOnHover && startScroll("up"),
@@ -157,7 +157,7 @@ var OverflowContainer = forwardRef(
157
157
  "div",
158
158
  {
159
159
  className: cn(
160
- "absolute w-full h-5 left-0 bottom-0 bg-gradient-to-b from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto",
160
+ "overflow-container__indicator overflow-container__indicator--down",
161
161
  indicatorClassName
162
162
  ),
163
163
  onMouseEnter: () => scrollOnHover && startScroll("down"),
@@ -168,10 +168,7 @@ var OverflowContainer = forwardRef(
168
168
  /* @__PURE__ */ jsx(
169
169
  "div",
170
170
  {
171
- className: cn(
172
- "flex relative overflow-auto w-full",
173
- containerClassName
174
- ),
171
+ className: cn("overflow-container__inner", containerClassName),
175
172
  ref: containerRef,
176
173
  onScroll: updateScrollButtons,
177
174
  children
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/OverflowContainer.tsx"],"sourcesContent":["/**\n * OverflowContainer – React component for smooth scrolling with hover-activated\n * indicators when content overflows. Supports horizontal and vertical scrolling.\n * @license MIT\n */\n\nimport {\n type ForwardedRef,\n forwardRef,\n useEffect,\n useRef,\n useState,\n} from \"react\";\n\ndeclare const process: { env?: { NODE_ENV?: string } };\n\nfunction cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\nexport interface OverflowContainerProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Custom className for the outer container */\n className?: string;\n\n /** Content to be rendered inside the scroll container */\n children: React.ReactNode;\n\n /**\n * Speed of the scroll animation in milliseconds. Lower = faster. Positive only.\n * @default 10\n */\n scrollSpeed?: number;\n\n /**\n * Distance to scroll in pixels per interval. Positive only.\n * @default 10\n */\n scrollDistance?: number;\n\n /**\n * Padding in pixels before the end to stop scrolling. Positive only.\n * @default 10\n */\n scrollEndPadding?: number;\n\n /** Custom className for the inner scrollable container */\n containerClassName?: string;\n\n /**\n * Whether to show scroll indicators when content overflows.\n * @default true\n */\n showScrollIndicators?: boolean;\n\n /**\n * Whether to show horizontal scroll indicators.\n * @default true\n */\n horizontalScrollIndicators?: boolean;\n\n /**\n * Whether to show vertical scroll indicators.\n * @default false\n */\n verticalScrollIndicators?: boolean;\n\n /** Custom className for scroll indicators (overrides default styles) */\n indicatorClassName?: string;\n\n /**\n * Whether to scroll when hovering over indicators.\n * @default true\n */\n scrollOnHover?: boolean;\n}\n\nconst OverflowContainer = forwardRef(\n (\n {\n className,\n children,\n scrollSpeed = 10,\n scrollDistance = 10,\n scrollEndPadding = 10,\n containerClassName,\n showScrollIndicators = true,\n horizontalScrollIndicators = true,\n verticalScrollIndicators = false,\n indicatorClassName,\n scrollOnHover = true,\n ...props\n }: OverflowContainerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n const [canScrollUp, setCanScrollUp] = useState(false);\n const [canScrollDown, setCanScrollDown] = useState(false);\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n useEffect(() => {\n if (process.env?.NODE_ENV === \"development\") {\n if (scrollSpeed < 0) {\n console.warn(\n \"OverflowContainer: scrollSpeed should be a positive number\"\n );\n }\n if (scrollDistance < 0) {\n console.warn(\n \"OverflowContainer: scrollDistance should be a positive number\"\n );\n }\n if (scrollEndPadding < 0) {\n console.warn(\n \"OverflowContainer: scrollEndPadding should be a positive number\"\n );\n }\n }\n }, [scrollSpeed, scrollDistance, scrollEndPadding]);\n\n const startScroll = (scrollDirection: \"left\" | \"right\" | \"up\" | \"down\") => {\n stopScroll();\n const isHorizontal =\n scrollDirection === \"left\" || scrollDirection === \"right\";\n const isBackward = scrollDirection === \"left\" || scrollDirection === \"up\";\n const scrollAmount = isBackward ? -scrollDistance : scrollDistance;\n const container = containerRef.current;\n if (!container) return;\n\n intervalRef.current = setInterval(() => {\n if (!container) return;\n\n const scrollPos = isHorizontal\n ? container.scrollLeft\n : container.scrollTop;\n const scrollSize = isHorizontal\n ? container.scrollWidth\n : container.scrollHeight;\n const clientSize = isHorizontal\n ? container.clientWidth\n : container.clientHeight;\n\n const canScroll = isBackward\n ? scrollPos > 0\n : scrollPos < scrollSize - clientSize;\n\n if (!canScroll) {\n stopScroll();\n return;\n }\n updateScrollButtons();\n container.scrollBy({\n [isHorizontal ? \"left\" : \"top\"]: scrollAmount,\n });\n }, scrollSpeed);\n };\n\n const updateScrollButtons = () => {\n const container = containerRef.current;\n if (!container) return;\n\n const canScrollLeftVal =\n container.scrollLeft > scrollEndPadding;\n const canScrollRightVal =\n container.scrollLeft <\n container.scrollWidth - container.clientWidth - scrollEndPadding;\n const canScrollUpVal = container.scrollTop > scrollEndPadding;\n const canScrollDownVal =\n container.scrollTop <\n container.scrollHeight - container.clientHeight - scrollEndPadding;\n\n setCanScrollLeft(canScrollLeftVal);\n setCanScrollRight(canScrollRightVal);\n setCanScrollUp(canScrollUpVal);\n setCanScrollDown(canScrollDownVal);\n };\n\n const stopScroll = () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n\n useEffect(() => {\n if (!forwardedRef) return;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(containerRef.current);\n } else {\n forwardedRef.current = containerRef.current;\n }\n }, [forwardedRef]);\n\n useEffect(() => {\n updateScrollButtons();\n const container = containerRef.current;\n if (!container) return;\n\n const resizeObserver = new ResizeObserver(updateScrollButtons);\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n stopScroll();\n };\n }, []);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Scrollable content\"\n className={cn(\"flex relative overflow-hidden\", className)}\n {...props}\n >\n {showScrollIndicators && (\n <div className=\"absolute left-0 top-0 flex w-full h-full z-50 bg-transparent pointer-events-none\">\n {canScrollLeft && horizontalScrollIndicators && (\n <div\n className={cn(\n \"absolute h-full w-4 left-0 top-0 bg-gradient-to-l from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"left\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollRight && horizontalScrollIndicators && (\n <div\n className={cn(\n \"absolute h-full w-4 right-0 top-0 bg-gradient-to-r from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"right\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollUp && verticalScrollIndicators && (\n <div\n className={cn(\n \"absolute w-full h-5 left-0 top-0 bg-gradient-to-t from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"up\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollDown && verticalScrollIndicators && (\n <div\n className={cn(\n \"absolute w-full h-5 left-0 bottom-0 bg-gradient-to-b from-black/0 to-black/10 cursor-pointer z-50 pointer-events-auto\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"down\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n </div>\n )}\n <div\n className={cn(\n \"flex relative overflow-auto w-full\",\n containerClassName\n )}\n ref={containerRef}\n onScroll={updateScrollButtons}\n >\n {children}\n </div>\n </div>\n );\n }\n);\n\nOverflowContainer.displayName = \"OverflowContainer\";\n\nexport default OverflowContainer;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/OverflowContainer.tsx"],"sourcesContent":["/**\n * OverflowContainer – React component for smooth scrolling with hover-activated\n * indicators when content overflows. Supports horizontal and vertical scrolling.\n * @license MIT\n */\n\nimport {\n type ForwardedRef,\n forwardRef,\n useEffect,\n useRef,\n useState,\n} from \"react\";\n\ndeclare const process: { env?: { NODE_ENV?: string } };\n\nfunction cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n\nexport interface OverflowContainerProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Custom className for the outer container */\n className?: string;\n\n /** Content to be rendered inside the scroll container */\n children: React.ReactNode;\n\n /**\n * Speed of the scroll animation in milliseconds. Lower = faster. Positive only.\n * @default 10\n */\n scrollSpeed?: number;\n\n /**\n * Distance to scroll in pixels per interval. Positive only.\n * @default 10\n */\n scrollDistance?: number;\n\n /**\n * Padding in pixels before the end to stop scrolling. Positive only.\n * @default 10\n */\n scrollEndPadding?: number;\n\n /** Custom className for the inner scrollable container */\n containerClassName?: string;\n\n /**\n * Whether to show scroll indicators when content overflows.\n * @default true\n */\n showScrollIndicators?: boolean;\n\n /**\n * Whether to show horizontal scroll indicators.\n * @default true\n */\n horizontalScrollIndicators?: boolean;\n\n /**\n * Whether to show vertical scroll indicators.\n * @default false\n */\n verticalScrollIndicators?: boolean;\n\n /** Custom className for scroll indicators (overrides default styles) */\n indicatorClassName?: string;\n\n /**\n * Whether to scroll when hovering over indicators.\n * @default true\n */\n scrollOnHover?: boolean;\n}\n\nconst OverflowContainer = forwardRef(\n (\n {\n className,\n children,\n scrollSpeed = 10,\n scrollDistance = 10,\n scrollEndPadding = 10,\n containerClassName,\n showScrollIndicators = true,\n horizontalScrollIndicators = true,\n verticalScrollIndicators = false,\n indicatorClassName,\n scrollOnHover = true,\n ...props\n }: OverflowContainerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n const [canScrollUp, setCanScrollUp] = useState(false);\n const [canScrollDown, setCanScrollDown] = useState(false);\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n useEffect(() => {\n if (process.env?.NODE_ENV === \"development\") {\n if (scrollSpeed < 0) {\n console.warn(\n \"OverflowContainer: scrollSpeed should be a positive number\"\n );\n }\n if (scrollDistance < 0) {\n console.warn(\n \"OverflowContainer: scrollDistance should be a positive number\"\n );\n }\n if (scrollEndPadding < 0) {\n console.warn(\n \"OverflowContainer: scrollEndPadding should be a positive number\"\n );\n }\n }\n }, [scrollSpeed, scrollDistance, scrollEndPadding]);\n\n const startScroll = (scrollDirection: \"left\" | \"right\" | \"up\" | \"down\") => {\n stopScroll();\n const isHorizontal =\n scrollDirection === \"left\" || scrollDirection === \"right\";\n const isBackward = scrollDirection === \"left\" || scrollDirection === \"up\";\n const scrollAmount = isBackward ? -scrollDistance : scrollDistance;\n const container = containerRef.current;\n if (!container) return;\n\n intervalRef.current = setInterval(() => {\n if (!container) return;\n\n const scrollPos = isHorizontal\n ? container.scrollLeft\n : container.scrollTop;\n const scrollSize = isHorizontal\n ? container.scrollWidth\n : container.scrollHeight;\n const clientSize = isHorizontal\n ? container.clientWidth\n : container.clientHeight;\n\n const canScroll = isBackward\n ? scrollPos > 0\n : scrollPos < scrollSize - clientSize;\n\n if (!canScroll) {\n stopScroll();\n return;\n }\n updateScrollButtons();\n container.scrollBy({\n [isHorizontal ? \"left\" : \"top\"]: scrollAmount,\n });\n }, scrollSpeed);\n };\n\n const updateScrollButtons = () => {\n const container = containerRef.current;\n if (!container) return;\n\n const canScrollLeftVal =\n container.scrollLeft > scrollEndPadding;\n const canScrollRightVal =\n container.scrollLeft <\n container.scrollWidth - container.clientWidth - scrollEndPadding;\n const canScrollUpVal = container.scrollTop > scrollEndPadding;\n const canScrollDownVal =\n container.scrollTop <\n container.scrollHeight - container.clientHeight - scrollEndPadding;\n\n setCanScrollLeft(canScrollLeftVal);\n setCanScrollRight(canScrollRightVal);\n setCanScrollUp(canScrollUpVal);\n setCanScrollDown(canScrollDownVal);\n };\n\n const stopScroll = () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n\n useEffect(() => {\n if (!forwardedRef) return;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(containerRef.current);\n } else {\n forwardedRef.current = containerRef.current;\n }\n }, [forwardedRef]);\n\n useEffect(() => {\n updateScrollButtons();\n const container = containerRef.current;\n if (!container) return;\n\n const resizeObserver = new ResizeObserver(updateScrollButtons);\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n stopScroll();\n };\n }, []);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Scrollable content\"\n className={cn(\"overflow-container\", className)}\n {...props}\n >\n {showScrollIndicators && (\n <div className=\"overflow-container__indicators\">\n {canScrollLeft && horizontalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--left\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"left\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollRight && horizontalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--right\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"right\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollUp && verticalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--up\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"up\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n {canScrollDown && verticalScrollIndicators && (\n <div\n className={cn(\n \"overflow-container__indicator overflow-container__indicator--down\",\n indicatorClassName\n )}\n onMouseEnter={() => scrollOnHover && startScroll(\"down\")}\n onMouseLeave={() => scrollOnHover && stopScroll()}\n />\n )}\n </div>\n )}\n <div\n className={cn(\"overflow-container__inner\", containerClassName)}\n ref={containerRef}\n onScroll={updateScrollButtons}\n >\n {children}\n </div>\n </div>\n );\n }\n);\n\nOverflowContainer.displayName = \"OverflowContainer\";\n\nexport default OverflowContainer;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-scroll-indicators",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "React overflow container with hover-activated scroll indicators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -11,11 +11,21 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
+ },
15
+ "./OverflowContainer": {
16
+ "types": "./src/OverflowContainer.tsx",
17
+ "import": "./src/OverflowContainer.tsx",
18
+ "default": "./src/OverflowContainer.tsx"
19
+ },
20
+ "./styles": {
21
+ "import": "./src/overflow-container.css",
22
+ "default": "./src/overflow-container.css"
14
23
  }
15
24
  },
16
25
  "files": [
17
26
  "dist",
18
- "README.md"
27
+ "README.md",
28
+ "src"
19
29
  ],
20
30
  "scripts": {
21
31
  "build": "tsup",
@@ -0,0 +1,275 @@
1
+ /**
2
+ * OverflowContainer – React component for smooth scrolling with hover-activated
3
+ * indicators when content overflows. Supports horizontal and vertical scrolling.
4
+ * @license MIT
5
+ */
6
+
7
+ import {
8
+ type ForwardedRef,
9
+ forwardRef,
10
+ useEffect,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+
15
+ declare const process: { env?: { NODE_ENV?: string } };
16
+
17
+ function cn(...classes: (string | undefined | null | false)[]): string {
18
+ return classes.filter(Boolean).join(" ");
19
+ }
20
+
21
+ export interface OverflowContainerProps
22
+ extends React.HTMLAttributes<HTMLDivElement> {
23
+ /** Custom className for the outer container */
24
+ className?: string;
25
+
26
+ /** Content to be rendered inside the scroll container */
27
+ children: React.ReactNode;
28
+
29
+ /**
30
+ * Speed of the scroll animation in milliseconds. Lower = faster. Positive only.
31
+ * @default 10
32
+ */
33
+ scrollSpeed?: number;
34
+
35
+ /**
36
+ * Distance to scroll in pixels per interval. Positive only.
37
+ * @default 10
38
+ */
39
+ scrollDistance?: number;
40
+
41
+ /**
42
+ * Padding in pixels before the end to stop scrolling. Positive only.
43
+ * @default 10
44
+ */
45
+ scrollEndPadding?: number;
46
+
47
+ /** Custom className for the inner scrollable container */
48
+ containerClassName?: string;
49
+
50
+ /**
51
+ * Whether to show scroll indicators when content overflows.
52
+ * @default true
53
+ */
54
+ showScrollIndicators?: boolean;
55
+
56
+ /**
57
+ * Whether to show horizontal scroll indicators.
58
+ * @default true
59
+ */
60
+ horizontalScrollIndicators?: boolean;
61
+
62
+ /**
63
+ * Whether to show vertical scroll indicators.
64
+ * @default false
65
+ */
66
+ verticalScrollIndicators?: boolean;
67
+
68
+ /** Custom className for scroll indicators (overrides default styles) */
69
+ indicatorClassName?: string;
70
+
71
+ /**
72
+ * Whether to scroll when hovering over indicators.
73
+ * @default true
74
+ */
75
+ scrollOnHover?: boolean;
76
+ }
77
+
78
+ const OverflowContainer = forwardRef(
79
+ (
80
+ {
81
+ className,
82
+ children,
83
+ scrollSpeed = 10,
84
+ scrollDistance = 10,
85
+ scrollEndPadding = 10,
86
+ containerClassName,
87
+ showScrollIndicators = true,
88
+ horizontalScrollIndicators = true,
89
+ verticalScrollIndicators = false,
90
+ indicatorClassName,
91
+ scrollOnHover = true,
92
+ ...props
93
+ }: OverflowContainerProps,
94
+ forwardedRef: ForwardedRef<HTMLDivElement>
95
+ ) => {
96
+ const containerRef = useRef<HTMLDivElement>(null);
97
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
98
+ const [canScrollRight, setCanScrollRight] = useState(false);
99
+ const [canScrollUp, setCanScrollUp] = useState(false);
100
+ const [canScrollDown, setCanScrollDown] = useState(false);
101
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
102
+
103
+ useEffect(() => {
104
+ if (process.env?.NODE_ENV === "development") {
105
+ if (scrollSpeed < 0) {
106
+ console.warn(
107
+ "OverflowContainer: scrollSpeed should be a positive number"
108
+ );
109
+ }
110
+ if (scrollDistance < 0) {
111
+ console.warn(
112
+ "OverflowContainer: scrollDistance should be a positive number"
113
+ );
114
+ }
115
+ if (scrollEndPadding < 0) {
116
+ console.warn(
117
+ "OverflowContainer: scrollEndPadding should be a positive number"
118
+ );
119
+ }
120
+ }
121
+ }, [scrollSpeed, scrollDistance, scrollEndPadding]);
122
+
123
+ const startScroll = (scrollDirection: "left" | "right" | "up" | "down") => {
124
+ stopScroll();
125
+ const isHorizontal =
126
+ scrollDirection === "left" || scrollDirection === "right";
127
+ const isBackward = scrollDirection === "left" || scrollDirection === "up";
128
+ const scrollAmount = isBackward ? -scrollDistance : scrollDistance;
129
+ const container = containerRef.current;
130
+ if (!container) return;
131
+
132
+ intervalRef.current = setInterval(() => {
133
+ if (!container) return;
134
+
135
+ const scrollPos = isHorizontal
136
+ ? container.scrollLeft
137
+ : container.scrollTop;
138
+ const scrollSize = isHorizontal
139
+ ? container.scrollWidth
140
+ : container.scrollHeight;
141
+ const clientSize = isHorizontal
142
+ ? container.clientWidth
143
+ : container.clientHeight;
144
+
145
+ const canScroll = isBackward
146
+ ? scrollPos > 0
147
+ : scrollPos < scrollSize - clientSize;
148
+
149
+ if (!canScroll) {
150
+ stopScroll();
151
+ return;
152
+ }
153
+ updateScrollButtons();
154
+ container.scrollBy({
155
+ [isHorizontal ? "left" : "top"]: scrollAmount,
156
+ });
157
+ }, scrollSpeed);
158
+ };
159
+
160
+ const updateScrollButtons = () => {
161
+ const container = containerRef.current;
162
+ if (!container) return;
163
+
164
+ const canScrollLeftVal =
165
+ container.scrollLeft > scrollEndPadding;
166
+ const canScrollRightVal =
167
+ container.scrollLeft <
168
+ container.scrollWidth - container.clientWidth - scrollEndPadding;
169
+ const canScrollUpVal = container.scrollTop > scrollEndPadding;
170
+ const canScrollDownVal =
171
+ container.scrollTop <
172
+ container.scrollHeight - container.clientHeight - scrollEndPadding;
173
+
174
+ setCanScrollLeft(canScrollLeftVal);
175
+ setCanScrollRight(canScrollRightVal);
176
+ setCanScrollUp(canScrollUpVal);
177
+ setCanScrollDown(canScrollDownVal);
178
+ };
179
+
180
+ const stopScroll = () => {
181
+ if (intervalRef.current) {
182
+ clearInterval(intervalRef.current);
183
+ intervalRef.current = null;
184
+ }
185
+ };
186
+
187
+ useEffect(() => {
188
+ if (!forwardedRef) return;
189
+ if (typeof forwardedRef === "function") {
190
+ forwardedRef(containerRef.current);
191
+ } else {
192
+ forwardedRef.current = containerRef.current;
193
+ }
194
+ }, [forwardedRef]);
195
+
196
+ useEffect(() => {
197
+ updateScrollButtons();
198
+ const container = containerRef.current;
199
+ if (!container) return;
200
+
201
+ const resizeObserver = new ResizeObserver(updateScrollButtons);
202
+ resizeObserver.observe(container);
203
+
204
+ return () => {
205
+ resizeObserver.disconnect();
206
+ stopScroll();
207
+ };
208
+ }, []);
209
+
210
+ return (
211
+ <div
212
+ role="region"
213
+ aria-label="Scrollable content"
214
+ className={cn("overflow-container", className)}
215
+ {...props}
216
+ >
217
+ {showScrollIndicators && (
218
+ <div className="overflow-container__indicators">
219
+ {canScrollLeft && horizontalScrollIndicators && (
220
+ <div
221
+ className={cn(
222
+ "overflow-container__indicator overflow-container__indicator--left",
223
+ indicatorClassName
224
+ )}
225
+ onMouseEnter={() => scrollOnHover && startScroll("left")}
226
+ onMouseLeave={() => scrollOnHover && stopScroll()}
227
+ />
228
+ )}
229
+ {canScrollRight && horizontalScrollIndicators && (
230
+ <div
231
+ className={cn(
232
+ "overflow-container__indicator overflow-container__indicator--right",
233
+ indicatorClassName
234
+ )}
235
+ onMouseEnter={() => scrollOnHover && startScroll("right")}
236
+ onMouseLeave={() => scrollOnHover && stopScroll()}
237
+ />
238
+ )}
239
+ {canScrollUp && verticalScrollIndicators && (
240
+ <div
241
+ className={cn(
242
+ "overflow-container__indicator overflow-container__indicator--up",
243
+ indicatorClassName
244
+ )}
245
+ onMouseEnter={() => scrollOnHover && startScroll("up")}
246
+ onMouseLeave={() => scrollOnHover && stopScroll()}
247
+ />
248
+ )}
249
+ {canScrollDown && verticalScrollIndicators && (
250
+ <div
251
+ className={cn(
252
+ "overflow-container__indicator overflow-container__indicator--down",
253
+ indicatorClassName
254
+ )}
255
+ onMouseEnter={() => scrollOnHover && startScroll("down")}
256
+ onMouseLeave={() => scrollOnHover && stopScroll()}
257
+ />
258
+ )}
259
+ </div>
260
+ )}
261
+ <div
262
+ className={cn("overflow-container__inner", containerClassName)}
263
+ ref={containerRef}
264
+ onScroll={updateScrollButtons}
265
+ >
266
+ {children}
267
+ </div>
268
+ </div>
269
+ );
270
+ }
271
+ );
272
+
273
+ OverflowContainer.displayName = "OverflowContainer";
274
+
275
+ export default OverflowContainer;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default as OverflowContainer } from "./OverflowContainer";
2
+ export type { OverflowContainerProps } from "./OverflowContainer";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * react-scroll-indicators – default styles for OverflowContainer
3
+ * Import once in your app: import "react-scroll-indicators/styles";
4
+ */
5
+
6
+ .overflow-container {
7
+ display: flex;
8
+ position: relative;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .overflow-container__inner {
13
+ display: flex;
14
+ position: relative;
15
+ overflow: auto;
16
+ width: 100%;
17
+ }
18
+
19
+ .overflow-container__indicators {
20
+ position: absolute;
21
+ left: 0;
22
+ top: 0;
23
+ display: flex;
24
+ width: 100%;
25
+ height: 100%;
26
+ z-index: 50;
27
+ background: transparent;
28
+ pointer-events: none;
29
+ }
30
+
31
+ .overflow-container__indicator {
32
+ position: absolute;
33
+ z-index: 50;
34
+ cursor: pointer;
35
+ pointer-events: auto;
36
+ }
37
+
38
+ .overflow-container__indicator--left {
39
+ left: 0;
40
+ top: 0;
41
+ width: 16px;
42
+ height: 100%;
43
+ background: linear-gradient(to left, transparent, rgba(0, 0, 0, 0.1));
44
+ }
45
+
46
+ .overflow-container__indicator--right {
47
+ right: 0;
48
+ top: 0;
49
+ width: 16px;
50
+ height: 100%;
51
+ background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.1));
52
+ }
53
+
54
+ .overflow-container__indicator--up {
55
+ left: 0;
56
+ top: 0;
57
+ width: 100%;
58
+ height: 20px;
59
+ background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.1));
60
+ }
61
+
62
+ .overflow-container__indicator--down {
63
+ left: 0;
64
+ bottom: 0;
65
+ width: 100%;
66
+ height: 20px;
67
+ background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));
68
+ }