@goodmanlabs/react-swipe-row 0.1.2 → 0.1.4

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
@@ -15,6 +15,8 @@ This was built to solve a common UI problem: you want a horizontally scrollable
15
15
  - Optional left/right controls appear automatically on “desktop-like” pointers
16
16
  - Control styling (buttons) and item spacing are exposed via props (no need to target internal DOM or CSS)
17
17
 
18
+ > *Written in TypeScript and ships with types; works in both TypeScript and plain JavaScript projects.*
19
+
18
20
  ---
19
21
 
20
22
  ## Install
@@ -43,7 +45,12 @@ export default function Example() {
43
45
  );
44
46
  }
45
47
  ```
46
- > Note: You must import the provided CSS for base layout and snapping behavior.
48
+ > Note: *You must import the provided CSS for base layout, snapping behavior, and default control styling.
49
+ For Next.js users, (App Router): import this CSS in app/layout.(js|tsx) (or your global stylesheet) per Next’s global CSS rules.*
50
+
51
+ ## Framework notes
52
+
53
+ This component uses React hooks. In Next.js App Router, it’s treated as a client component automatically—no extra "use client" wrapper is needed to consume it. In non-Next React apps (Vite, CRA, Remix, etc.), this has no special impact; it behaves like a normal React component.
47
54
 
48
55
  ## Props
49
56
 
@@ -73,6 +80,12 @@ type SwipeRowClassNames = {
73
80
  nextButton?: string; // next button
74
81
  };
75
82
  ```
83
+ ### `Styling controls (buttons)`
84
+
85
+ SwipeRow ships with neutral default button styles.
86
+
87
+ - If you provide any of classNames.controlButton, classNames.prevButton, or classNames.nextButton, the default button styling is automatically disabled so your classes take full control (no !important needed).
88
+ - Use prevButton / nextButton for positioning (e.g. left-2, right-2) and controlButton for shared button styling.
76
89
 
77
90
  ## Accessibility & Behavior
78
91
 
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ function SwipeRow({
29
29
  if (items) return items;
30
30
  return React.Children.toArray(children);
31
31
  }, [items, children]);
32
+ const hasCustomControls = !!(classNames == null ? void 0 : classNames.controlButton) || !!(classNames == null ? void 0 : classNames.prevButton) || !!(classNames == null ? void 0 : classNames.nextButton);
32
33
  useEffect(() => {
33
34
  const el = scrollerRef.current;
34
35
  if (!el) return;
@@ -124,6 +125,7 @@ function SwipeRow({
124
125
  "aria-label": "Scroll left",
125
126
  className: cx(
126
127
  "rsr-control",
128
+ hasCustomControls && "rsr-control--custom",
127
129
  "rsr-prev",
128
130
  classNames == null ? void 0 : classNames.controlButton,
129
131
  classNames == null ? void 0 : classNames.prevButton
@@ -141,6 +143,7 @@ function SwipeRow({
141
143
  "aria-label": "Scroll right",
142
144
  className: cx(
143
145
  "rsr-control",
146
+ hasCustomControls && "rsr-control--custom",
144
147
  "rsr-next",
145
148
  classNames == null ? void 0 : classNames.controlButton,
146
149
  classNames == null ? void 0 : classNames.nextButton
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/SwipeRow.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useId, useMemo, useRef, useState } from 'react';\n\nexport type ShowControlsMode = 'auto' | 'always' | 'never';\n\nexport type SwipeRowClassNames = {\n /** Outer wrapper around the scroller + controls */\n root?: string;\n /** The horizontal scroller element */\n scroller?: string;\n /** Wrapper around each item (the snap target) */\n item?: string;\n /** Shared class applied to both buttons */\n controlButton?: string;\n /** Prev button */\n prevButton?: string;\n /** Next button */\n nextButton?: string;\n};\n\nexport type SwipeRowProps = {\n /** Optional array of React nodes (alternative to children) */\n items?: React.ReactNode[];\n children?: React.ReactNode;\n\n ariaLabel?: string;\n\n /** Enable CSS scroll-snap */\n snap?: boolean;\n\n /** How far arrows / keyboard page (fraction of visible width) */\n pageFactor?: number;\n\n /** When to show left/right controls */\n showControls?: ShowControlsMode;\n\n /** Optional stable id for aria-controls */\n id?: string;\n\n /** Extra class on outer wrapper */\n className?: string;\n\n /**\n * Spacing between items.\n * This is a class hook (NOT Tailwind-specific). For non-Tailwind consumers,\n * they'll typically use the provided default CSS and ignore this.\n */\n gapClassName?: string;\n\n /** More granular class hooks */\n classNames?: SwipeRowClassNames;\n\n /**\n * Optional inline style passthrough for the scroller.\n * Useful for consumers that want scrollbarGutter, etc.\n */\n scrollerStyle?: React.CSSProperties;\n};\n\nfunction cx(...parts: Array<string | undefined | false | null>) {\n return parts.filter(Boolean).join(' ');\n}\n\nexport default function SwipeRow({\n items,\n children,\n ariaLabel = 'Scrollable content',\n className,\n gapClassName,\n snap = true,\n pageFactor = 0.9,\n showControls = 'auto',\n id,\n classNames,\n scrollerStyle,\n }: SwipeRowProps) {\n const scrollerRef = useRef<HTMLDivElement | null>(null);\n\n const [canLeft, setCanLeft] = useState(false);\n const [canRight, setCanRight] = useState(false);\n const [controlsOn, setControlsOn] = useState(showControls === 'always');\n\n const autoId = useId();\n const regionId = id ?? autoId;\n\n const content = useMemo(() => {\n if (items) return items;\n return React.Children.toArray(children);\n }, [items, children]);\n\n // Enable/disable arrows based on scroll position\n useEffect(() => {\n const el = scrollerRef.current;\n if (!el) return;\n\n const update = () => {\n // tolerance: helps avoid off-by-1 due to subpixel rounding\n const max = el.scrollWidth - el.clientWidth - 1;\n setCanLeft(el.scrollLeft > 0);\n setCanRight(el.scrollLeft < max);\n };\n\n update();\n el.addEventListener('scroll', update, { passive: true });\n window.addEventListener('resize', update);\n\n return () => {\n el.removeEventListener('scroll', update);\n window.removeEventListener('resize', update);\n };\n }, []);\n\n // Decide when to show controls (desktop-ish)\n useEffect(() => {\n if (showControls !== 'auto') {\n setControlsOn(showControls === 'always');\n return;\n }\n\n const mq = window.matchMedia('(hover: hover) and (pointer: fine)');\n const set = () => setControlsOn(mq.matches);\n\n set();\n\n // Safari fallback\n if (mq.addEventListener) mq.addEventListener('change', set);\n // eslint-disable-next-line deprecation/deprecation\n else mq.addListener?.(set);\n\n return () => {\n if (mq.removeEventListener) mq.removeEventListener('change', set);\n // eslint-disable-next-line deprecation/deprecation\n else mq.removeListener?.(set);\n };\n }, [showControls]);\n\n const page = (dir: -1 | 1) => {\n const el = scrollerRef.current;\n if (!el) return;\n const step = Math.round(el.clientWidth * pageFactor);\n el.scrollBy({ left: dir * step, behavior: 'smooth' });\n };\n\n // Keyboard paging (a11y)\n const onKeyDown = (e: React.KeyboardEvent) => {\n const el = scrollerRef.current;\n if (!el) return;\n const step = Math.round(el.clientWidth * pageFactor);\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n el.scrollBy({ left: step, behavior: 'smooth' });\n }\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n el.scrollBy({ left: -step, behavior: 'smooth' });\n }\n };\n\n return (\n <div className={cx('rsr-root', className, classNames?.root)}>\n <div\n ref={scrollerRef}\n role=\"region\"\n aria-label={ariaLabel}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className={cx(\n 'rsr-scroller',\n snap && 'rsr-snap',\n gapClassName,\n classNames?.scroller\n )}\n style={{\n WebkitOverflowScrolling: 'touch',\n // matches your original (optional, but harmless)\n scrollbarGutter: 'stable both-edges',\n ...scrollerStyle,\n }}\n id={regionId}\n >\n {content.map((node, i) => (\n <div\n key={i}\n className={cx('rsr-item', snap && 'rsr-snap-item', classNames?.item)}\n >\n {node}\n </div>\n ))}\n </div>\n\n {controlsOn && (\n <>\n <button\n type=\"button\"\n onClick={() => page(-1)}\n disabled={!canLeft}\n aria-controls={regionId}\n aria-label=\"Scroll left\"\n className={cx(\n 'rsr-control',\n 'rsr-prev',\n classNames?.controlButton,\n classNames?.prevButton\n )}\n >\n ‹\n </button>\n\n <button\n type=\"button\"\n onClick={() => page(1)}\n disabled={!canRight}\n aria-controls={regionId}\n aria-label=\"Scroll right\"\n className={cx(\n 'rsr-control',\n 'rsr-next',\n classNames?.controlButton,\n classNames?.nextButton\n )}\n >\n ›\n </button>\n </>\n )}\n </div>\n );\n}\n"],"mappings":";;;AAEA,OAAO,SAAS,WAAW,OAAO,SAAS,QAAQ,gBAAgB;AAqL/C,SAUJ,UAVI,KAUJ,YAVI;AA3HpB,SAAS,MAAM,OAAiD;AAC5D,SAAO,MAAM,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;AAEe,SAAR,SAA0B;AAAA,EACI;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,aAAa;AAAA,EACb,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACJ,GAAkB;AAC/C,QAAM,cAAc,OAA8B,IAAI;AAEtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,iBAAiB,QAAQ;AAEtE,QAAM,SAAS,MAAM;AACrB,QAAM,WAAW,kBAAM;AAEvB,QAAM,UAAU,QAAQ,MAAM;AAC1B,QAAI,MAAO,QAAO;AAClB,WAAO,MAAM,SAAS,QAAQ,QAAQ;AAAA,EAC1C,GAAG,CAAC,OAAO,QAAQ,CAAC;AAGpB,YAAU,MAAM;AACZ,UAAM,KAAK,YAAY;AACvB,QAAI,CAAC,GAAI;AAET,UAAM,SAAS,MAAM;AAEjB,YAAM,MAAM,GAAG,cAAc,GAAG,cAAc;AAC9C,iBAAW,GAAG,aAAa,CAAC;AAC5B,kBAAY,GAAG,aAAa,GAAG;AAAA,IACnC;AAEA,WAAO;AACP,OAAG,iBAAiB,UAAU,QAAQ,EAAE,SAAS,KAAK,CAAC;AACvD,WAAO,iBAAiB,UAAU,MAAM;AAExC,WAAO,MAAM;AACT,SAAG,oBAAoB,UAAU,MAAM;AACvC,aAAO,oBAAoB,UAAU,MAAM;AAAA,IAC/C;AAAA,EACJ,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AAlHpB;AAmHQ,QAAI,iBAAiB,QAAQ;AACzB,oBAAc,iBAAiB,QAAQ;AACvC;AAAA,IACJ;AAEA,UAAM,KAAK,OAAO,WAAW,oCAAoC;AACjE,UAAM,MAAM,MAAM,cAAc,GAAG,OAAO;AAE1C,QAAI;AAGJ,QAAI,GAAG,iBAAkB,IAAG,iBAAiB,UAAU,GAAG;AAAA,QAErD,UAAG,gBAAH,4BAAiB;AAEtB,WAAO,MAAM;AAlIrB,UAAAA;AAmIY,UAAI,GAAG,oBAAqB,IAAG,oBAAoB,UAAU,GAAG;AAAA,UAE3D,EAAAA,MAAA,GAAG,mBAAH,gBAAAA,IAAA,SAAoB;AAAA,IAC7B;AAAA,EACJ,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,OAAO,CAAC,QAAgB;AAC1B,UAAM,KAAK,YAAY;AACvB,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,KAAK,MAAM,GAAG,cAAc,UAAU;AACnD,OAAG,SAAS,EAAE,MAAM,MAAM,MAAM,UAAU,SAAS,CAAC;AAAA,EACxD;AAGA,QAAM,YAAY,CAAC,MAA2B;AAC1C,UAAM,KAAK,YAAY;AACvB,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,KAAK,MAAM,GAAG,cAAc,UAAU;AAEnD,QAAI,EAAE,QAAQ,cAAc;AACxB,QAAE,eAAe;AACjB,SAAG,SAAS,EAAE,MAAM,MAAM,UAAU,SAAS,CAAC;AAAA,IAClD;AACA,QAAI,EAAE,QAAQ,aAAa;AACvB,QAAE,eAAe;AACjB,SAAG,SAAS,EAAE,MAAM,CAAC,MAAM,UAAU,SAAS,CAAC;AAAA,IACnD;AAAA,EACJ;AAEA,SACI,qBAAC,SAAI,WAAW,GAAG,YAAY,WAAW,yCAAY,IAAI,GACtD;AAAA;AAAA,MAAC;AAAA;AAAA,QACG,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,UAAU;AAAA,QACV;AAAA,QACA,WAAW;AAAA,UACP;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,yCAAY;AAAA,QAChB;AAAA,QACA,OAAO;AAAA,UACH,yBAAyB;AAAA;AAAA,UAEzB,iBAAiB;AAAA,UACjB,GAAG;AAAA,QACP;AAAA,QACA,IAAI;AAAA,QAEH,kBAAQ,IAAI,CAAC,MAAM,MAChB;AAAA,UAAC;AAAA;AAAA,YAEG,WAAW,GAAG,YAAY,QAAQ,iBAAiB,yCAAY,IAAI;AAAA,YAElE;AAAA;AAAA,UAHI;AAAA,QAIT,CACH;AAAA;AAAA,IACL;AAAA,IAEC,cACG,iCACI;AAAA;AAAA,QAAC;AAAA;AAAA,UACG,MAAK;AAAA,UACL,SAAS,MAAM,KAAK,EAAE;AAAA,UACtB,UAAU,CAAC;AAAA,UACX,iBAAe;AAAA,UACf,cAAW;AAAA,UACX,WAAW;AAAA,YACP;AAAA,YACA;AAAA,YACA,yCAAY;AAAA,YACZ,yCAAY;AAAA,UAChB;AAAA,UACH;AAAA;AAAA,MAED;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACG,MAAK;AAAA,UACL,SAAS,MAAM,KAAK,CAAC;AAAA,UACrB,UAAU,CAAC;AAAA,UACX,iBAAe;AAAA,UACf,cAAW;AAAA,UACX,WAAW;AAAA,YACP;AAAA,YACA;AAAA,YACA,yCAAY;AAAA,YACZ,yCAAY;AAAA,UAChB;AAAA,UACH;AAAA;AAAA,MAED;AAAA,OACJ;AAAA,KAER;AAER;","names":["_a"]}
1
+ {"version":3,"sources":["../src/SwipeRow.tsx"],"sourcesContent":["'use client';\n\nimport React, { useEffect, useId, useMemo, useRef, useState } from 'react';\n\nexport type ShowControlsMode = 'auto' | 'always' | 'never';\n\nexport type SwipeRowClassNames = {\n /** Outer wrapper around the scroller + controls */\n root?: string;\n /** The horizontal scroller element */\n scroller?: string;\n /** Wrapper around each item (the snap target) */\n item?: string;\n /** Shared class applied to both buttons */\n controlButton?: string;\n /** Prev button */\n prevButton?: string;\n /** Next button */\n nextButton?: string;\n};\n\nexport type SwipeRowProps = {\n /** Optional array of React nodes (alternative to children) */\n items?: React.ReactNode[];\n children?: React.ReactNode;\n\n ariaLabel?: string;\n\n /** Enable CSS scroll-snap */\n snap?: boolean;\n\n /** How far arrows / keyboard page (fraction of visible width) */\n pageFactor?: number;\n\n /** When to show left/right controls */\n showControls?: ShowControlsMode;\n\n /** Optional stable id for aria-controls */\n id?: string;\n\n /** Extra class on outer wrapper */\n className?: string;\n\n /**\n * Spacing between items.\n * This is a class hook (NOT Tailwind-specific). For non-Tailwind consumers,\n * they'll typically use the provided default CSS and ignore this.\n */\n gapClassName?: string;\n\n /** More granular class hooks */\n classNames?: SwipeRowClassNames;\n\n /**\n * Optional inline style passthrough for the scroller.\n * Useful for consumers that want scrollbarGutter, etc.\n */\n scrollerStyle?: React.CSSProperties;\n};\n\nfunction cx(...parts: Array<string | undefined | false | null>) {\n return parts.filter(Boolean).join(' ');\n}\n\nexport default function SwipeRow({\n items,\n children,\n ariaLabel = 'Scrollable content',\n className,\n gapClassName,\n snap = true,\n pageFactor = 0.9,\n showControls = 'auto',\n id,\n classNames,\n scrollerStyle,\n }: SwipeRowProps) {\n const scrollerRef = useRef<HTMLDivElement | null>(null);\n\n const [canLeft, setCanLeft] = useState(false);\n const [canRight, setCanRight] = useState(false);\n const [controlsOn, setControlsOn] = useState(showControls === 'always');\n\n const autoId = useId();\n const regionId = id ?? autoId;\n\n const content = useMemo(() => {\n if (items) return items;\n return React.Children.toArray(children);\n }, [items, children]);\n\n const hasCustomControls =\n !!classNames?.controlButton || !!classNames?.prevButton || !!classNames?.nextButton;\n\n // Enable/disable arrows based on scroll position\n useEffect(() => {\n const el = scrollerRef.current;\n if (!el) return;\n\n const update = () => {\n // tolerance: helps avoid off-by-1 due to subpixel rounding\n const max = el.scrollWidth - el.clientWidth - 1;\n setCanLeft(el.scrollLeft > 0);\n setCanRight(el.scrollLeft < max);\n };\n\n update();\n el.addEventListener('scroll', update, { passive: true });\n window.addEventListener('resize', update);\n\n return () => {\n el.removeEventListener('scroll', update);\n window.removeEventListener('resize', update);\n };\n }, []);\n\n // Decide when to show controls (desktop-ish)\n useEffect(() => {\n if (showControls !== 'auto') {\n setControlsOn(showControls === 'always');\n return;\n }\n\n const mq = window.matchMedia('(hover: hover) and (pointer: fine)');\n const set = () => setControlsOn(mq.matches);\n\n set();\n\n // Safari fallback\n if (mq.addEventListener) mq.addEventListener('change', set);\n // eslint-disable-next-line deprecation/deprecation\n else mq.addListener?.(set);\n\n return () => {\n if (mq.removeEventListener) mq.removeEventListener('change', set);\n // eslint-disable-next-line deprecation/deprecation\n else mq.removeListener?.(set);\n };\n }, [showControls]);\n\n const page = (dir: -1 | 1) => {\n const el = scrollerRef.current;\n if (!el) return;\n const step = Math.round(el.clientWidth * pageFactor);\n el.scrollBy({ left: dir * step, behavior: 'smooth' });\n };\n\n // Keyboard paging (a11y)\n const onKeyDown = (e: React.KeyboardEvent) => {\n const el = scrollerRef.current;\n if (!el) return;\n const step = Math.round(el.clientWidth * pageFactor);\n\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n el.scrollBy({ left: step, behavior: 'smooth' });\n }\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n el.scrollBy({ left: -step, behavior: 'smooth' });\n }\n };\n\n return (\n <div className={cx('rsr-root', className, classNames?.root)}>\n <div\n ref={scrollerRef}\n role=\"region\"\n aria-label={ariaLabel}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className={cx(\n 'rsr-scroller',\n snap && 'rsr-snap',\n gapClassName,\n classNames?.scroller\n )}\n style={{\n WebkitOverflowScrolling: 'touch',\n // matches your original (optional, but harmless)\n scrollbarGutter: 'stable both-edges',\n ...scrollerStyle,\n }}\n id={regionId}\n >\n {content.map((node, i) => (\n <div\n key={i}\n className={cx('rsr-item', snap && 'rsr-snap-item', classNames?.item)}\n >\n {node}\n </div>\n ))}\n </div>\n\n {controlsOn && (\n <>\n <button\n type=\"button\"\n onClick={() => page(-1)}\n disabled={!canLeft}\n aria-controls={regionId}\n aria-label=\"Scroll left\"\n className={cx(\n 'rsr-control',\n hasCustomControls && 'rsr-control--custom',\n 'rsr-prev',\n classNames?.controlButton,\n classNames?.prevButton\n )}\n >\n ‹\n </button>\n\n <button\n type=\"button\"\n onClick={() => page(1)}\n disabled={!canRight}\n aria-controls={regionId}\n aria-label=\"Scroll right\"\n className={cx(\n 'rsr-control',\n hasCustomControls && 'rsr-control--custom',\n 'rsr-next',\n classNames?.controlButton,\n classNames?.nextButton\n )}\n >\n ›\n </button>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_a"]}
package/dist/style.css CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
  /* prevents parent page swipe-back stealing horizontal scroll */
23
23
  overscroll-behavior-x: contain;
24
+ scrollbar-width: none; /* Firefox */
24
25
  }
25
26
 
26
27
  /* Snap behavior */
@@ -36,39 +37,45 @@
36
37
  scroll-snap-align: start;
37
38
  }
38
39
 
39
- /* Controls (neutral default styling) */
40
+ /* Always-on layout & behavior */
40
41
  .rsr-control {
41
42
  position: absolute;
42
43
  top: 50%;
43
44
  transform: translateY(-50%);
44
45
  z-index: 10;
45
-
46
46
  display: inline-flex;
47
47
  align-items: center;
48
48
  justify-content: center;
49
+ cursor: pointer;
50
+ user-select: none;
51
+ -webkit-tap-highlight-color: transparent;
52
+ }
49
53
 
54
+ /* Default visual skin (opt-out) */
55
+ .rsr-control:not(.rsr-control--custom) {
50
56
  width: 36px;
51
57
  height: 36px;
52
58
  border-radius: 9999px;
53
-
54
59
  border: 1px solid rgba(0, 0, 0, 0.12);
55
60
  background: rgba(255, 255, 255, 0.9);
56
61
  color: rgba(0, 0, 0, 0.85);
57
-
58
- cursor: pointer;
59
- user-select: none;
60
- -webkit-tap-highlight-color: transparent;
61
62
  }
62
63
 
63
- .rsr-control:hover {
64
+ /* Default hover (opt-out) */
65
+ .rsr-control:not(.rsr-control--custom):hover:not(:disabled) {
64
66
  background: rgba(255, 255, 255, 1);
65
67
  }
66
68
 
69
+ /* Always-on disabled behavior */
67
70
  .rsr-control:disabled {
68
- opacity: 0.45;
69
71
  cursor: default;
70
72
  }
71
73
 
74
+ /* Default disabled visuals (opt-out) */
75
+ .rsr-control:not(.rsr-control--custom):disabled {
76
+ opacity: 0.45;
77
+ }
78
+
72
79
  .rsr-prev {
73
80
  left: 8px;
74
81
  }
@@ -77,10 +84,6 @@
77
84
  right: 8px;
78
85
  }
79
86
 
80
- /* Optional: hide scrollbar in browsers that support it */
81
- .rsr-scroller {
82
- scrollbar-width: none; /* Firefox */
83
- }
84
87
  .rsr-scroller::-webkit-scrollbar {
85
88
  width: 0;
86
89
  height: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodmanlabs/react-swipe-row",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "author": "Glenn Goodman",
5
5
  "description": "A native-feeling horizontal scroll-snap card rail for React",
6
6
  "license": "MIT",