playbook_ui 10.19.0 → 10.21.0.pre.alpha.lightbox

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -0
  3. data/app/pb_kits/playbook/data/menu.yml +1 -0
  4. data/app/pb_kits/playbook/index.js +2 -1
  5. data/app/pb_kits/playbook/pb_avatar/_avatar.jsx +1 -1
  6. data/app/pb_kits/playbook/pb_avatar/_avatar.scss +2 -2
  7. data/app/pb_kits/playbook/pb_avatar/avatar.rb +1 -1
  8. data/app/pb_kits/playbook/pb_avatar/avatar.test.js +1 -1
  9. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.jsx +4 -0
  10. data/app/pb_kits/playbook/pb_bar_graph/bar_graph.rb +1 -0
  11. data/app/pb_kits/playbook/pb_body/_body.jsx +3 -1
  12. data/app/pb_kits/playbook/pb_button/_button.jsx +3 -3
  13. data/app/pb_kits/playbook/pb_button/_button.scss +18 -1
  14. data/app/pb_kits/playbook/pb_button/button.rb +11 -3
  15. data/app/pb_kits/playbook/pb_button/button.test.js +13 -0
  16. data/app/pb_kits/playbook/pb_button/docs/_button_size.html.erb +3 -0
  17. data/app/pb_kits/playbook/pb_button/docs/_button_size.jsx +26 -0
  18. data/app/pb_kits/playbook/pb_button/docs/_button_size.md +1 -0
  19. data/app/pb_kits/playbook/pb_button/docs/example.yml +2 -0
  20. data/app/pb_kits/playbook/pb_button/docs/index.js +1 -0
  21. data/app/pb_kits/playbook/pb_caption/_caption.jsx +2 -1
  22. data/app/pb_kits/playbook/pb_caption/_caption.scss +7 -0
  23. data/app/pb_kits/playbook/pb_caption/caption.rb +5 -2
  24. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.jsx +3 -0
  25. data/app/pb_kits/playbook/pb_circle_chart/circle_chart.rb +1 -0
  26. data/app/pb_kits/playbook/pb_dashboard/pbChartsDarkTheme.js +215 -0
  27. data/app/pb_kits/playbook/pb_date/_date.jsx +1 -1
  28. data/app/pb_kits/playbook/pb_date/date.html.erb +1 -1
  29. data/app/pb_kits/playbook/pb_file_upload/_file_upload.jsx +17 -10
  30. data/app/pb_kits/playbook/pb_file_upload/fileupload.test.js +40 -0
  31. data/app/pb_kits/playbook/pb_gauge/_gauge.jsx +3 -0
  32. data/app/pb_kits/playbook/pb_gauge/gauge.rb +1 -0
  33. data/app/pb_kits/playbook/pb_image/_image.jsx +1 -1
  34. data/app/pb_kits/playbook/pb_image/_image.scss +3 -3
  35. data/app/pb_kits/playbook/pb_image/image.rb +1 -1
  36. data/app/pb_kits/playbook/pb_image/image.test.js +1 -1
  37. data/app/pb_kits/playbook/pb_lightbox/Carousel/Slide.jsx +55 -0
  38. data/app/pb_kits/playbook/pb_lightbox/Carousel/Slides.jsx +54 -0
  39. data/app/pb_kits/playbook/pb_lightbox/Carousel/Thumbnail.jsx +39 -0
  40. data/app/pb_kits/playbook/pb_lightbox/Carousel/Thumbnails.jsx +82 -0
  41. data/app/pb_kits/playbook/pb_lightbox/Carousel/index.jsx +59 -0
  42. data/app/pb_kits/playbook/pb_lightbox/Carousel/styles.scss +110 -0
  43. data/app/pb_kits/playbook/pb_lightbox/Carousel/useSlides.js +66 -0
  44. data/app/pb_kits/playbook/pb_lightbox/_lightbox.jsx +112 -0
  45. data/app/pb_kits/playbook/pb_lightbox/_lightbox_context.jsx +3 -0
  46. data/app/pb_kits/playbook/pb_lightbox/_lightbox_header.jsx +71 -0
  47. data/app/pb_kits/playbook/pb_lightbox/_lightbox_header_icon.jsx +26 -0
  48. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_compound_component.jsx +95 -0
  49. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_default.jsx +64 -0
  50. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_default.md +1 -0
  51. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_multiple.jsx +64 -0
  52. data/app/pb_kits/playbook/pb_lightbox/docs/example.yml +7 -0
  53. data/app/pb_kits/playbook/pb_lightbox/docs/index.js +3 -0
  54. data/app/pb_kits/playbook/pb_lightbox/hooks/useVisibility.js +21 -0
  55. data/app/pb_kits/playbook/pb_lightbox/hooks/useWindowSize.js +25 -0
  56. data/app/pb_kits/playbook/pb_lightbox/lightbox.scss +90 -0
  57. data/app/pb_kits/playbook/pb_lightbox/lightbox.test.jsx +30 -0
  58. data/app/pb_kits/playbook/pb_line_graph/_line_graph.jsx +4 -0
  59. data/app/pb_kits/playbook/pb_line_graph/line_graph.rb +1 -0
  60. data/app/pb_kits/playbook/pb_popover/_popover.jsx +2 -4
  61. data/app/pb_kits/playbook/pb_popover/docs/_popover_close.html.erb +7 -7
  62. data/app/pb_kits/playbook/pb_popover/index.js +4 -9
  63. data/app/pb_kits/playbook/pb_popover/popover.html.erb +1 -1
  64. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.jsx +4 -0
  65. data/app/pb_kits/playbook/pb_text_input/_text_input.scss +2 -2
  66. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +14 -0
  67. data/app/pb_kits/playbook/pb_title/_title.jsx +2 -1
  68. data/app/pb_kits/playbook/pb_title/title.rb +5 -2
  69. data/app/pb_kits/playbook/playbook-doc.js +2 -0
  70. data/app/pb_kits/playbook/plugins/pb_chart.js +8 -4
  71. data/lib/playbook/version.rb +2 -2
  72. metadata +30 -4
@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'
4
4
  import { useDropzone } from 'react-dropzone'
5
5
  import classnames from 'classnames'
6
6
 
7
- import { buildCss, noop } from '../utilities/props'
7
+ import { buildCss, buildDataProps, noop } from '../utilities/props'
8
8
  import { globalProps } from '../utilities/globalProps'
9
9
  import type { Callback } from '../types'
10
10
 
@@ -14,15 +14,17 @@ import Card from '../pb_card/_card'
14
14
  type FileUploadProps = {
15
15
  accept?: array<string>,
16
16
  className?: string,
17
+ data?: object,
17
18
  acceptedFilesDescription?: string,
18
19
  onFilesAccepted: Callback,
19
20
  }
20
21
 
21
22
  const FileUpload = (props: FileUploadProps) => {
22
23
  const {
23
- accept = ['*'],
24
+ accept = null,
24
25
  acceptedFilesDescription = '',
25
26
  className,
27
+ data = {},
26
28
  onFilesAccepted = noop,
27
29
  } = props
28
30
  const onDrop = useCallback((files) => {
@@ -34,17 +36,22 @@ const FileUpload = (props: FileUploadProps) => {
34
36
  onDrop,
35
37
  })
36
38
 
37
- const acceptedFileTypes = accept.map((fileType) => {
38
- if (fileType.startsWith('image/')) {
39
- return fileType.replace('image/', ' ')
40
- } else {
41
- return fileType
42
- }
43
- })
39
+ const acceptedFileTypes = () => {
40
+ return accept.map((fileType) => {
41
+ if (fileType.startsWith('image/')) {
42
+ return fileType.replace('image/', ' ')
43
+ } else {
44
+ return fileType
45
+ }
46
+ })
47
+ }
48
+
49
+ const dataProps = buildDataProps(data)
44
50
 
45
51
  return (
46
52
  <div
47
53
  className={classnames(buildCss('pb_file_upload_kit'), globalProps(props), className)}
54
+ {...dataProps}
48
55
  {...getRootProps()}
49
56
  >
50
57
  <Card>
@@ -53,7 +60,7 @@ const FileUpload = (props: FileUploadProps) => {
53
60
  <If condition={isDragActive}>
54
61
  <p>{'Drop the files here ...'}</p>
55
62
  <Else />
56
- <p>{`Choose a file or drag it here. The accepted file types are: ${acceptedFilesDescription || acceptedFileTypes}`}</p>
63
+ <p>{accept === null ? 'Choose a file or drag it here' : `Choose a file or drag it here. The accepted file types are: ${acceptedFilesDescription || acceptedFileTypes()}`}</p>
57
64
  </If>
58
65
  </Body>
59
66
  </Card>
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+ import { render, screen } from '../utilities/test-utils'
3
+
4
+ import FileUpload from './_file_upload'
5
+
6
+ const testid = 'fileupload-test'
7
+
8
+ test('returns namespaced class name', () => {
9
+ render(
10
+ <FileUpload
11
+ data={{ testid: testid }}
12
+ />
13
+ )
14
+
15
+ const kit = screen.getByTestId(testid)
16
+ expect(kit).toHaveClass('pb_file_upload_kit')
17
+ })
18
+
19
+ test('shows default drag text', () => {
20
+ render(
21
+ <FileUpload
22
+ data={{ testid: testid }}
23
+ />
24
+ )
25
+
26
+ const kit = screen.getByTestId(testid)
27
+ expect(kit).toHaveTextContent('Choose a file or drag it here')
28
+ })
29
+
30
+ test('shows type-specific drag text', () => {
31
+ render(
32
+ <FileUpload
33
+ accept={['image/svg+xml']}
34
+ data={{ testid: testid }}
35
+ />
36
+ )
37
+
38
+ const kit = screen.getByTestId(testid)
39
+ expect(kit).toHaveTextContent('Choose a file or drag it here. The accepted file types are: svg+xml')
40
+ })
@@ -12,6 +12,7 @@ type GaugeProps = {
12
12
  aria: Object,
13
13
  className?: string,
14
14
  chartData?: array,
15
+ dark?: Boolean,
15
16
  data?: Object,
16
17
  disableAnimation: boolean,
17
18
  fullCircle: boolean,
@@ -33,6 +34,7 @@ const Gauge = (props: GaugeProps) => {
33
34
  aria = {},
34
35
  className,
35
36
  chartData = [{ name: 'Name', value: 0 }],
37
+ dark = false,
36
38
  data = {},
37
39
  disableAnimation = false,
38
40
  fullCircle = false,
@@ -67,6 +69,7 @@ const Gauge = (props: GaugeProps) => {
67
69
  id: id,
68
70
  chartData: formattedChartData,
69
71
  circumference: fullCircle ? [0, 360] : [-100, 100],
72
+ dark,
70
73
  disableAnimation: disableAnimation,
71
74
  height: height,
72
75
  min: min,
@@ -32,6 +32,7 @@ module Playbook
32
32
  id: id,
33
33
  chartData: chart_data_formatted,
34
34
  circumference: full_circle ? [0, 360] : [-100, 100],
35
+ dark: dark ? "dark" : "",
35
36
  disableAnimation: disable_animation,
36
37
  height: height,
37
38
  min: min,
@@ -34,7 +34,7 @@ const Image = (props: ImageProps) => {
34
34
 
35
35
  const ariaProps = buildAriaProps(aria)
36
36
  const classes = classnames(
37
- buildCss('pb_image_kit', size),
37
+ buildCss('pb_image_kit', size ? `size_${size}` : null),
38
38
  'lazyload',
39
39
  transition,
40
40
  { rounded },
@@ -14,7 +14,7 @@ $image-sizes: (
14
14
  object-fit: cover;
15
15
 
16
16
  @each $name, $size in $image-sizes {
17
- &[class*=_#{$name}] {
17
+ &[class*=size_#{$name}] {
18
18
  width: $size;
19
19
  height: $size;
20
20
  object-fit: cover;
@@ -35,7 +35,7 @@ $image-sizes: (
35
35
  transition: opacity 300ms ease-in;
36
36
  }
37
37
  }
38
-
38
+
39
39
  &.blur {
40
40
  filter: blur(5px);
41
41
  &.lazyloaded {
@@ -44,7 +44,7 @@ $image-sizes: (
44
44
  transition: filter 300ms ease-in;
45
45
  }
46
46
  }
47
-
47
+
48
48
  &.scale {
49
49
  opacity: 0;
50
50
  transform: scale(0.9);
@@ -27,7 +27,7 @@ module Playbook
27
27
  end
28
28
 
29
29
  def size_class
30
- size == "none" ? nil : "_#{size}"
30
+ size == "none" ? nil : "_size_#{size}"
31
31
  end
32
32
 
33
33
  def transition_class
@@ -26,7 +26,7 @@ test('default classname', () => {
26
26
 
27
27
  test('size = xs', () => {
28
28
  const kit = renderKit(Image, props, { size: 'xs' })
29
- expect(kit).toHaveClass('pb_image_kit_xs lazyload')
29
+ expect(kit).toHaveClass('pb_image_kit_size_xs lazyload')
30
30
  })
31
31
 
32
32
  test('transition = blur', () => {
@@ -0,0 +1,55 @@
1
+ /* @flow */
2
+
3
+ import React from 'react'
4
+ import { noop } from 'lodash'
5
+ import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'
6
+ import Image from '../../pb_image/_image'
7
+
8
+ import styles from './styles.scss'
9
+
10
+ type SlideType = {
11
+ alt: string,
12
+ current: number,
13
+ onClick: () => void,
14
+ onChange: (index: number) => void,
15
+ onZoom: (zoom: number) => void,
16
+ zooming: boolean,
17
+ url: string,
18
+ }
19
+
20
+ export default function Slide({
21
+ alt,
22
+ onClick = noop,
23
+ onZoom = noop,
24
+ url,
25
+ zooming = false,
26
+ }: SlideType) {
27
+ const handlePinchingStop = (e) => {
28
+ const isZooming = e.state.scale > 1
29
+ onZoom(isZooming)
30
+ }
31
+
32
+ return (
33
+ <TransformWrapper
34
+ doubleClick={{ mode: 'reset' }}
35
+ initialScale={1}
36
+ onPinchingStop={handlePinchingStop}
37
+ panning={{ disabled: !zooming }}
38
+ >
39
+ <button
40
+ className={styles.Slide}
41
+ onClick={onClick}
42
+ onDoubleClick={() => onZoom(false)}
43
+ tabIndex={-1}
44
+ >
45
+ <TransformComponent className={styles.TransformComponent}>
46
+ <Image
47
+ alt={alt}
48
+ url={url}
49
+ zIndex="3"
50
+ />
51
+ </TransformComponent>
52
+ </button>
53
+ </TransformWrapper>
54
+ )
55
+ }
@@ -0,0 +1,54 @@
1
+ /* @flow */
2
+
3
+ import { noop } from 'lodash'
4
+ import { motion } from 'framer-motion'
5
+ import React, { useState } from 'react'
6
+
7
+ import Slide from './Slide'
8
+ import styles from './styles.scss'
9
+ import useSlides from './useSlides'
10
+
11
+ type SlidesType = {
12
+ urls: Array<string>,
13
+ current: number,
14
+ onChange: (index: number) => void,
15
+ onClick: (index: number) => void,
16
+ }
17
+
18
+ export default function Slides({
19
+ urls = [],
20
+ current = 0,
21
+ onClick = noop,
22
+ onChange = noop,
23
+ }: SlidesType) {
24
+ const [zooming, setZooming] = useState(false)
25
+ const { controls, dragConstraints, handleDragEnd } = useSlides({
26
+ current,
27
+ pagesCount: urls.length,
28
+ onChange,
29
+ })
30
+
31
+ const handleZoom = (isZooming) => setZooming(isZooming)
32
+
33
+ return (
34
+ <motion.div
35
+ animate={controls}
36
+ className={styles.Slides}
37
+ drag={!zooming && 'x'}
38
+ dragConstraints={dragConstraints}
39
+ dragElastic={0.05}
40
+ onDragEnd={handleDragEnd}
41
+ transition={{ type: 'spring', bounce: 0 }}
42
+ >
43
+ {urls.map((url, i) => (
44
+ <Slide
45
+ key={i}
46
+ onClick={() => onClick(i)}
47
+ onZoom={handleZoom}
48
+ url={url}
49
+ zooming={zooming}
50
+ />
51
+ ))}
52
+ </motion.div>
53
+ )
54
+ }
@@ -0,0 +1,39 @@
1
+ /* @flow */
2
+
3
+ import React from 'react'
4
+ import { noop } from 'lodash'
5
+ import classnames from 'classnames'
6
+ import Image from '../../pb_image/_image'
7
+
8
+ import styles from './styles.scss'
9
+
10
+ type ThumbnailType = {
11
+ active?: boolean,
12
+ alt?: string,
13
+ onClick: () => void,
14
+ url: string,
15
+ width?: string,
16
+ }
17
+
18
+ export default function Thumbnail({
19
+ active = false,
20
+ alt,
21
+ width,
22
+ url,
23
+ onClick = noop,
24
+ }: ThumbnailType) {
25
+ return (
26
+ <button
27
+ className={classnames(styles.Thumbnail, { [styles.active]: active })}
28
+ onClick={onClick}
29
+ style={{ width }}
30
+ type="button"
31
+ >
32
+ <Image
33
+ alt={alt}
34
+ size="sm"
35
+ url={url}
36
+ />
37
+ </button>
38
+ )
39
+ }
@@ -0,0 +1,82 @@
1
+ /* @flow */
2
+
3
+ import { noop } from 'lodash'
4
+ import classnames from 'classnames'
5
+ import React, { useEffect } from 'react'
6
+ import { motion, useAnimation } from 'framer-motion'
7
+ import { useWindowSize } from '../hooks/useWindowSize'
8
+
9
+ import Thumbnail from './Thumbnail'
10
+
11
+ import styles from './styles.scss'
12
+
13
+ export const indexWithinBounds = (
14
+ current: number,
15
+ min: number,
16
+ max: number
17
+ ): number => {
18
+ if (current < min) return 0
19
+ if (current > max) return max
20
+ return current
21
+ }
22
+
23
+ type ThumbnailsType = {
24
+ current: number,
25
+ onChange: () => null,
26
+ urls: [],
27
+ }
28
+
29
+ export default function Thumbnails({
30
+ current = 0,
31
+ onChange = noop,
32
+ urls = [],
33
+ }: ThumbnailsType) {
34
+ const controls = useAnimation()
35
+ const viewportSize = useWindowSize()
36
+ const thumbnailWidth = viewportSize.width / 8
37
+ const draggable = thumbnailWidth * urls.length > viewportSize.width
38
+ const css = classnames(styles.Thumbnails, {
39
+ [styles.draggable]: draggable,
40
+ })
41
+ const dragConstraints = {
42
+ left: -1 * (thumbnailWidth * urls.length - viewportSize.width),
43
+ right: 0,
44
+ }
45
+
46
+ const modifyTarget = (target) => {
47
+ const nextIndex = Math.round(Math.abs(target) / thumbnailWidth)
48
+ const snapTargetIndex = indexWithinBounds(nextIndex, 0, urls.length)
49
+ const snapTarget = snapTargetIndex * thumbnailWidth
50
+ const direction = Math.sign(target)
51
+ return direction * snapTarget
52
+ }
53
+
54
+ useEffect(() => {
55
+ if (draggable) {
56
+ const x = Math.max(-current * thumbnailWidth, dragConstraints.left)
57
+ controls.start({ x })
58
+ }
59
+ }, [controls, current, draggable, dragConstraints.left, thumbnailWidth])
60
+
61
+ return (
62
+ <motion.div
63
+ animate={controls}
64
+ className={css}
65
+ drag={draggable && 'x'}
66
+ dragConstraints={dragConstraints}
67
+ dragElastic={0.05}
68
+ dragTransition={{ modifyTarget }}
69
+ transition={{ type: 'spring', bounce: 0 }}
70
+ >
71
+ {urls.map((url, i) => (
72
+ <Thumbnail
73
+ active={i === current}
74
+ alt={i}
75
+ key={i}
76
+ onClick={() => onChange(i)}
77
+ url={url}
78
+ />
79
+ ))}
80
+ </motion.div>
81
+ )
82
+ }
@@ -0,0 +1,59 @@
1
+ /* eslint-disable jsx-control-statements/jsx-use-if-tag */
2
+ /* @flow */
3
+
4
+ import { noop } from 'lodash'
5
+ import React, { useEffect, useState } from 'react'
6
+
7
+ import Slides from './Slides'
8
+ import Thumbnails from './Thumbnails'
9
+ import styles from './styles.scss'
10
+
11
+ type CarouselType = {
12
+ initialPhoto: string,
13
+ onClose: Function,
14
+ icon: string,
15
+ iconSize: number,
16
+ current: number,
17
+ photos: Array<string>,
18
+ onChange: (index: number) => void,
19
+ onClick: (index: number) => void,
20
+ }
21
+
22
+ export default function Carousel({
23
+ current = 0,
24
+ photos,
25
+ onClick = noop,
26
+ onChange = noop,
27
+ }: CarouselType) {
28
+ useEffect(() => {
29
+ document.body.style.overflow = 'hidden'
30
+
31
+ return () => {
32
+ document.body.style.overflow = 'initial'
33
+ }
34
+ }, [])
35
+
36
+ const [currentIndex, setCurrentIndex] = useState(current)
37
+ const handleChange = (index) => {
38
+ setCurrentIndex(index)
39
+ onChange(index)
40
+ }
41
+
42
+ return (
43
+ <div className={styles.Lightbox}>
44
+ <Slides
45
+ current={currentIndex}
46
+ onChange={handleChange}
47
+ onClick={onClick}
48
+ urls={photos.map((photo) => photo.url)}
49
+ />
50
+ {photos.length > 1 ? (
51
+ <Thumbnails
52
+ current={currentIndex}
53
+ onChange={handleChange}
54
+ urls={photos.map((photo) => photo.thumbnail)}
55
+ />
56
+ ) : null}
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,110 @@
1
+ .Lightbox {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ position: fixed;
5
+ left: 0;
6
+ top: 0;
7
+ display: flex;
8
+ align-items: center;
9
+ flex-direction: column;
10
+ background-color: black;
11
+ z-index: 1;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .Slides {
16
+ display: flex;
17
+ flex-grow: 1;
18
+ height: calc(100% - 100px);
19
+ width: 100%;
20
+ z-index: 1;
21
+
22
+ [class^="react-transform-wrapper"] {
23
+ flex-shrink: 0;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
28
+ [class^="react-transform-content"] {
29
+ width: 100%;
30
+ height: 100%;
31
+ }
32
+ }
33
+
34
+ .Slide,
35
+ .Thumbnail {
36
+ flex-shrink: 0;
37
+ border: none;
38
+ margin: 0;
39
+ padding: 0;
40
+ cursor: pointer;
41
+ background-color: transparent;
42
+ }
43
+
44
+ .Slide {
45
+ display: flex;
46
+ justify-content: center;
47
+ align-items: center;
48
+ width: 100%;
49
+ height: 100%;
50
+ overflow: hidden;
51
+
52
+ img {
53
+ width: 100vw;
54
+ height: 100vh;
55
+ object-fit: contain;
56
+ }
57
+ }
58
+
59
+ .BackBtn,
60
+ .NextBtn {
61
+ z-index: 2;
62
+ color: black;
63
+ position: absolute;
64
+ width: 50px;
65
+ height: 50px;
66
+ top: calc(50vh - 5px);
67
+ border: none;
68
+ border-radius: 50%;
69
+ background-color: white;
70
+ }
71
+
72
+ .BackBtn::before,
73
+ .NextBtn::before {
74
+ content: "▸";
75
+ }
76
+
77
+ .BackBtn {
78
+ left: 30px;
79
+ transform: rotate(180deg);
80
+ }
81
+
82
+ .NextBtn {
83
+ right: 30px;
84
+ }
85
+
86
+ .Thumbnails {
87
+ display: flex;
88
+ padding: 3px;
89
+ }
90
+
91
+ .Thumbnails.draggable {
92
+ align-self: flex-start;
93
+ }
94
+
95
+ .Thumbnail {
96
+ padding: 3px;
97
+ height: 100%;
98
+
99
+ img {
100
+ width: 100%;
101
+ height: 100%;
102
+ }
103
+ }
104
+
105
+ .Thumbnail.active {
106
+ padding: 6px;
107
+ background-color: white;
108
+ box-shadow: 0 0 6px white;
109
+ }
110
+
@@ -0,0 +1,66 @@
1
+ // @flow
2
+
3
+ import { noop } from 'lodash'
4
+ import { useEffect, useState } from 'react'
5
+ import { useAnimation } from 'framer-motion'
6
+ import { useWindowSize } from '../hooks/useWindowSize'
7
+
8
+ const cycleIndex = (current: number, min: number, max: number): number => {
9
+ if (current < min) return max
10
+ if (current > max) return min
11
+ return current
12
+ }
13
+
14
+ const swipeConfidenceThreshold = 10000
15
+ const swipePower = (offset: number, velocity: number) => {
16
+ return Math.abs(offset) * velocity
17
+ }
18
+
19
+ export default function useSlides({
20
+ current = 0,
21
+ pagesCount = 0,
22
+ onChange = noop,
23
+ }) {
24
+ const controls = useAnimation()
25
+ const viewportSize = useWindowSize()
26
+ const [currentIndex, setCurrentIndex] = useState(current)
27
+ const dragConstraints = {
28
+ left: -viewportSize.width * (pagesCount - 1),
29
+ right: 0,
30
+ }
31
+
32
+ const paginate = (newDirection: number) => {
33
+ const nextIndex = currentIndex + newDirection
34
+ const cycledNextIndex = cycleIndex(nextIndex, 0, pagesCount - 1)
35
+ setCurrentIndex(cycledNextIndex)
36
+ return cycledNextIndex
37
+ }
38
+
39
+ const handleDragEnd = (e, { offset, velocity }) => {
40
+ let nextIndex = currentIndex
41
+ const swipe = swipePower(offset.x, velocity.x)
42
+
43
+ if (swipe < -swipeConfidenceThreshold) {
44
+ nextIndex = paginate(1)
45
+ } else if (swipe > swipeConfidenceThreshold) {
46
+ nextIndex = paginate(-1)
47
+ }
48
+
49
+ controls.start({ x: -viewportSize.width * nextIndex })
50
+
51
+ if (nextIndex !== currentIndex) {
52
+ onChange(nextIndex)
53
+ }
54
+ }
55
+
56
+ useEffect(() => {
57
+ controls.set({ x: -viewportSize.width * current })
58
+ setCurrentIndex(current)
59
+ }, [controls, current, viewportSize.width])
60
+
61
+ return {
62
+ controls,
63
+ dragConstraints,
64
+ handleDragEnd,
65
+ }
66
+ }