@hanzo/ui 3.6.5 → 3.7.22

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.
@@ -1,15 +1,15 @@
1
1
  import React from 'react'
2
2
 
3
3
  import type { LinkDef, ButtonDef} from '../../types'
4
- import { type ButtonSizes, ActionButton, LinkElement } from '../../primitives'
4
+ import { buttonVariants, ActionButton, LinkElement } from '../../primitives'
5
5
  import type { CTABlock } from '../def'
6
- import { cn, containsToken } from '../../util'
6
+ import { cn, containsToken, type VariantProps } from '../../util'
7
7
 
8
8
  import type BlockComponentProps from './block-component-props'
9
9
 
10
10
  const CtaBlockComponent: React.FC<BlockComponentProps & {
11
11
  itemClasses?: string
12
- itemSize?: ButtonSizes,
12
+ itemSize?: VariantProps<typeof buttonVariants>['size'],
13
13
  renderLink?: (def: LinkDef, key: any) => JSX.Element
14
14
  renderButton?: (def: ButtonDef, key: any) => JSX.Element
15
15
  }> = ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/ui",
3
- "version": "3.6.5",
3
+ "version": "3.7.22",
4
4
  "description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -31,9 +31,16 @@
31
31
  "scripts": {
32
32
  "lat": "npm show @hanzo/ui version",
33
33
  "pub": "npm publish",
34
- "build": "tsc",
35
- "tc": "tsc",
36
- "clean": "rm -rf dist && rm -rf node_modules"
34
+ "tc": "tsc"
35
+ },
36
+ "exports": {
37
+ "./blocks": "./blocks/index.ts",
38
+ "./primitives": "./primitives/index.ts",
39
+ "./style/": "./style/*",
40
+ "./tailwind": "./tailwind/index.ts",
41
+ "./types": "./types/index.ts",
42
+ "./util": "./util/index.ts",
43
+ "./util-client": "./util/index-client.ts"
37
44
  },
38
45
  "dependencies": {
39
46
  "@next/third-parties": "^14.1.0",
@@ -63,32 +70,33 @@
63
70
  "class-variance-authority": "^0.7.0",
64
71
  "clsx": "^2.1.0",
65
72
  "cmdk": "^0.2.0",
66
- "embla-carousel-react": "8.0.1",
73
+ "embla-carousel-react": "8.0.2",
67
74
  "input-otp": "^1.0.1",
68
75
  "lodash.castarray": "^4.4.0",
69
76
  "lodash.isplainobject": "^4.0.6",
70
77
  "lodash.merge": "^4.6.2",
71
- "markdown-to-jsx": "^7.3.2",
72
- "postcss-selector-parser": "^6.0.13",
73
- "react-day-picker": "^8.7.1",
74
- "react-intersection-observer": "^9.7.0",
75
- "sonner": "^1.2.3",
76
- "tailwind-merge": "^2.2.0",
78
+ "markdown-to-jsx": "^7.4.7",
79
+ "postcss-selector-parser": "^6.0.16",
80
+ "react-day-picker": "^8.10.1",
81
+ "react-intersection-observer": "^9.8.2",
82
+ "sonner": "^1.4.41",
83
+ "tailwind-merge": "^2.3.0",
77
84
  "tailwindcss-animate": "^1.0.7",
78
85
  "tailwindcss-interaction-media": "^0.1.0",
79
- "vaul": "^0.9.0"
86
+ "@hanzo/react-drawer": "0.9.1"
80
87
  },
81
88
  "peerDependencies": {
82
89
  "@hookform/resolvers": "^3.3.2",
83
- "embla-carousel": "^8.0.1",
90
+ "embla-carousel": "^8.0.2",
84
91
  "lucide-react": "^0.344.0",
92
+ "mobx": "^6.12.0",
85
93
  "next": "14.1.3",
86
94
  "next-themes": "^0.2.1",
87
- "react": "^18.2.0",
88
- "react-dom": "^18.2.0",
89
- "react-hook-form": "^7.47.0",
95
+ "react": "^18.3.1",
96
+ "react-dom": "^18.3.1",
97
+ "react-hook-form": "^7.51.4",
90
98
  "validator": "^13.11.0",
91
- "zod": "3.21.4"
99
+ "zod": "3.23.8"
92
100
  },
93
101
  "devDependencies": {
94
102
  "@mdx-js/loader": "^3.0.0",
@@ -96,11 +104,11 @@
96
104
  "@types/facebook-pixel": "^0.0.30",
97
105
  "@types/gtag.js": "^0.0.19",
98
106
  "@types/lodash.merge": "^4.6.9",
99
- "@types/mdx": "^2.0.9",
100
- "@types/react": "^18.2.64",
101
- "@types/react-dom": "^18.2.18",
102
- "embla-carousel": "^8.0.1",
103
- "tailwindcss": "^3.4.1",
107
+ "@types/mdx": "^2.0.13",
108
+ "@types/react": "^18.3.1",
109
+ "@types/react-dom": "^18.3.0",
110
+ "embla-carousel": "^8.0.2",
111
+ "tailwindcss": "^3.4.3",
104
112
  "typescript": "5.3.3"
105
113
  }
106
114
  }
@@ -1,10 +1,10 @@
1
1
  import React from 'react'
2
2
  import dynamic from 'next/dynamic'
3
3
 
4
- import { cn } from '../util'
4
+ import { cn, type VariantProps } from '../util'
5
5
 
6
6
  import type { ButtonDef, ButtonModalDef } from '../types'
7
- import type { ButtonSizes } from './button'
7
+ import type { buttonVariants } from './button'
8
8
 
9
9
  // The DVC must be rendered client-side since it accesses the DOM directly.
10
10
  // There is no need for a loading UI since the dialog only opens
@@ -12,26 +12,27 @@ import type { ButtonSizes } from './button'
12
12
  // https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
13
13
  const DynamicDVC = dynamic(() => (import('./dialog-video-controller')), {ssr: false})
14
14
 
15
- const ActionButton: React.FC<{
16
- def: ButtonDef
17
- size?: ButtonSizes
18
- className?: string
19
- }> = ({
15
+ const ActionButton: React.FC<
16
+ VariantProps<typeof buttonVariants> &
17
+ {
18
+ def: ButtonDef
19
+ className?: string
20
+ }
21
+ > = ({
20
22
  def,
21
- size, // no default. overrides!
22
- className=''
23
+ className='',
24
+ ...rest
23
25
  }) => {
24
26
  if (def.action.type === 'modal') {
25
27
  const m = def.action.def as ButtonModalDef
26
28
  const Modal = m.Comp
27
- const sizeToSpread = size ? {size} : {}
28
29
  return (
29
30
  <DynamicDVC>
30
31
  <Modal
31
32
  title={m.title}
32
33
  byline={m.byline}
33
34
  buttonText={def.text}
34
- buttonProps={{...def.props, ...sizeToSpread, className: cn((def.props?.className ?? ''), className)}}
35
+ buttonProps={{...def.props, ...rest, className: cn((def.props?.className ?? ''), className)}}
35
36
  action={m.action}
36
37
  actionEnclosure={m.actionEnclosure}
37
38
  {...m.props}
@@ -52,10 +52,6 @@ const buttonVariants = cva(
52
52
  }
53
53
  )
54
54
 
55
- type ButtonVariants = keyof typeof variant
56
- type ButtonSizes = keyof typeof size
57
- type ButtonRoundedValue = keyof typeof rounded
58
-
59
55
  interface ButtonProps extends
60
56
  React.ButtonHTMLAttributes<HTMLButtonElement>,
61
57
  VariantProps<typeof buttonVariants>
@@ -81,8 +77,5 @@ Button.displayName = "Button"
81
77
  export {
82
78
  Button as default,
83
79
  type ButtonProps,
84
- type ButtonVariants,
85
- type ButtonSizes,
86
- type ButtonRoundedValue,
87
80
  buttonVariants,
88
81
  }
@@ -1,12 +1,12 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Drawer as DrawerPrimitive } from 'vaul'
4
+ import { Drawer as DrawerPrimitive, useDrawerContext } from '@hanzo/react-drawer'
5
5
 
6
6
  import { cn } from '../util'
7
7
 
8
8
  const Drawer = ({
9
- shouldScaleBackground = true,
9
+ shouldScaleBackground = false,
10
10
  ...props
11
11
  }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12
12
  <DrawerPrimitive.Root
@@ -18,6 +18,7 @@ Drawer.displayName = 'Drawer'
18
18
 
19
19
  const DrawerTrigger = DrawerPrimitive.Trigger
20
20
  const DrawerPortal = DrawerPrimitive.Portal
21
+ const DrawerHandle = DrawerPrimitive.Handle
21
22
  const DrawerClose = DrawerPrimitive.Close
22
23
 
23
24
  const DrawerOverlay = React.forwardRef<
@@ -36,24 +37,38 @@ const DrawerContent = React.forwardRef<
36
37
  React.ElementRef<typeof DrawerPrimitive.Content>,
37
38
  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & {
38
39
  overlayClx?: string
40
+ defaultHandle?: boolean
39
41
  }
40
- >(({ className, children, overlayClx='', ...props }, ref) => (
41
- <DrawerPortal>
42
- {/* If no or same z index, overlay should precede content */}
43
- <DrawerOverlay className={cn('z-below-modal', overlayClx)}/>
44
- <DrawerPrimitive.Content
45
- ref={ref}
46
- className={cn('fixed left-0 right-0 bottom-0 z-modal',
47
- 'mt-24 flex flex-col h-[80%] rounded-t-[10px] pt-6 border bg-background',
48
- className
49
- )}
50
- {...props}
51
- >
52
- <div className='absolute left-0 right-0 mx-auto top-2 h-2 w-[100px] rounded-full bg-level-3 shrink-0' />
53
- {children}
54
- </DrawerPrimitive.Content>
55
- </DrawerPortal>
56
- ))
42
+ >(({
43
+ className,
44
+ children,
45
+ overlayClx='',
46
+ defaultHandle=true,
47
+ ...props
48
+ }, ref) => {
49
+
50
+ return (
51
+ <DrawerPortal>
52
+ {/* If no or same z index, overlay should precede content */}
53
+ <DrawerOverlay className={cn('z-below-modal', overlayClx)}/>
54
+ <DrawerPrimitive.Content
55
+ ref={ref}
56
+ className={cn('fixed left-0 right-0 bottom-0 z-modal',
57
+ 'mt-24 flex flex-col rounded-t-[10px] pt-6 border bg-background',
58
+ // 'h-[80%]'
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ {defaultHandle && (
64
+ <div className='absolute left-0 right-0 mx-auto top-2 h-2 w-[100px] rounded-full bg-level-3 shrink-0' />
65
+ )}
66
+ {children}
67
+ </DrawerPrimitive.Content>
68
+ </DrawerPortal>
69
+ )
70
+ })
71
+
57
72
  DrawerContent.displayName = 'DrawerContent'
58
73
 
59
74
  const DrawerHeader = ({
@@ -105,15 +120,20 @@ const DrawerDescription = React.forwardRef<
105
120
  ))
106
121
  DrawerDescription.displayName = DrawerPrimitive.Description.displayName
107
122
 
123
+ type DrawerProps = React.ComponentProps<typeof DrawerPrimitive.Root>
124
+
108
125
  export {
126
+ type DrawerProps,
109
127
  Drawer,
110
128
  DrawerPortal,
111
129
  DrawerOverlay,
112
130
  DrawerTrigger,
113
131
  DrawerClose,
114
132
  DrawerContent,
133
+ DrawerHandle,
115
134
  DrawerHeader,
116
135
  DrawerFooter,
117
136
  DrawerTitle,
118
137
  DrawerDescription,
138
+ useDrawerContext
119
139
  }
@@ -24,8 +24,6 @@ export {
24
24
  export {
25
25
  default as Button,
26
26
  type ButtonProps,
27
- type ButtonVariants,
28
- type ButtonSizes,
29
27
  buttonVariants,
30
28
  } from './button'
31
29
 
@@ -61,16 +59,19 @@ export {
61
59
  } from './command'
62
60
 
63
61
  export {
62
+ type DrawerProps,
64
63
  Drawer,
65
64
  DrawerPortal,
66
65
  DrawerOverlay,
67
66
  DrawerTrigger,
68
67
  DrawerClose,
69
68
  DrawerContent,
69
+ DrawerHandle,
70
70
  DrawerHeader,
71
71
  DrawerFooter,
72
72
  DrawerTitle,
73
73
  DrawerDescription,
74
+ useDrawerContext
74
75
  } from './drawer'
75
76
 
76
77
  export {
@@ -2,24 +2,26 @@ import React, { type PropsWithChildren } from 'react'
2
2
  import Link from 'next/link'
3
3
 
4
4
  import type { LinkDef, Icon } from '../types'
5
- import { buttonVariants, type ButtonSizes, type ButtonVariants } from './button'
6
- import { cn } from '../util'
5
+ import { buttonVariants } from './button'
6
+ import { cn, type VariantProps } from '../util'
7
7
 
8
8
  /**
9
9
  * If this is rendered directly (and not auto generated in a Block)
10
10
  * and it has any children, title, icon, and iconAfter
11
11
  * are ignore.
12
12
  */
13
- const LinkElement: React.FC<PropsWithChildren & {
13
+ const LinkElement: React.FC<
14
+ PropsWithChildren &
15
+ VariantProps<typeof buttonVariants> &
16
+ {
14
17
  def: LinkDef,
15
- /** overrides def */
16
- variant? : ButtonVariants
17
- /** overrides def */
18
- size?: ButtonSizes
19
- /** To trigger other events in addition to the
20
- * link action itself. (eg, to also close a drawer menu)
18
+
19
+ /**
20
+ * Use to trigger other events in addition to the
21
+ * link action itself. For example, to also close a drawer menu.
21
22
  */
22
23
  onClick?: () => void
24
+
23
25
  /** overrides def (eg, for title area)*/
24
26
  icon?: Icon
25
27
  /** overrides def */
@@ -28,9 +30,10 @@ const LinkElement: React.FC<PropsWithChildren & {
28
30
  }> = ({
29
31
  def,
30
32
  // DO NOT provide a default to any of the props that also appear in def!
31
- size,
32
33
  onClick,
34
+ size,
33
35
  variant,
36
+ rounded,
34
37
  icon,
35
38
  iconAfter,
36
39
  className = '',
@@ -42,6 +45,7 @@ const LinkElement: React.FC<PropsWithChildren & {
42
45
  newTab,
43
46
  variant: defVariant,
44
47
  size: defSize,
48
+ rounded: defRounded,
45
49
  title
46
50
  } = def
47
51
 
@@ -88,7 +92,8 @@ const LinkElement: React.FC<PropsWithChildren & {
88
92
  size: (!defVariant || defVariant.includes('link') || variant?.includes('link')) ?
89
93
  'link'
90
94
  :
91
- (size ? size : defSize)
95
+ (size ? size : defSize),
96
+ rounded: rounded ? rounded : (defRounded ? defRounded : 'md'),
92
97
  }),
93
98
  // This is a "label only" LinkDef. cf: footer"
94
99
  ((href.length > 0 || onClick) ? '' : 'pointer-events-none'),
@@ -0,0 +1,163 @@
1
+ /* This is a copy of vaul/src/style.css */
2
+
3
+ [vaul-drawer] {
4
+ touch-action: none;
5
+ transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
6
+ }
7
+
8
+ [vaul-drawer][vaul-drawer-direction='bottom'] {
9
+ transform: translate3d(0, 100%, 0);
10
+ }
11
+
12
+ [vaul-drawer][vaul-drawer-direction='top'] {
13
+ transform: translate3d(0, -100%, 0);
14
+ }
15
+
16
+ [vaul-drawer][vaul-drawer-direction='left'] {
17
+ transform: translate3d(-100%, 0, 0);
18
+ }
19
+
20
+ [vaul-drawer][vaul-drawer-direction='right'] {
21
+ transform: translate3d(100%, 0, 0);
22
+ }
23
+
24
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='top'] {
25
+ overflow-y: hidden !important;
26
+ }
27
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='bottom'] {
28
+ overflow-y: hidden !important;
29
+ }
30
+
31
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='left'] {
32
+ overflow-x: hidden !important;
33
+ }
34
+
35
+ .vaul-dragging .vaul-scrollable [vault-drawer-direction='right'] {
36
+ overflow-x: hidden !important;
37
+ }
38
+
39
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='top'] {
40
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
41
+ }
42
+
43
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='bottom'] {
44
+ transform: translate3d(0, var(--snap-point-height, 0), 0);
45
+ }
46
+
47
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='left'] {
48
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
49
+ }
50
+
51
+ [vaul-drawer][vaul-drawer-visible='true'][vaul-drawer-direction='right'] {
52
+ transform: translate3d(var(--snap-point-height, 0), 0, 0);
53
+ }
54
+
55
+ [vaul-overlay] {
56
+ opacity: 0;
57
+ transition: opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1);
58
+ }
59
+
60
+ [vaul-overlay][vaul-drawer-visible='true'] {
61
+ opacity: 1;
62
+ }
63
+
64
+ [vaul-drawer]::after {
65
+ content: '';
66
+ position: absolute;
67
+ background: inherit;
68
+ background-color: inherit;
69
+ }
70
+
71
+ [vaul-drawer][vaul-drawer-direction='top']::after {
72
+ top: initial;
73
+ bottom: 100%;
74
+ left: 0;
75
+ right: 0;
76
+ height: 200%;
77
+ }
78
+
79
+ [vaul-drawer][vaul-drawer-direction='bottom']::after {
80
+ top: 100%;
81
+ bottom: initial;
82
+ left: 0;
83
+ right: 0;
84
+ height: 200%;
85
+ }
86
+
87
+ [vaul-drawer][vaul-drawer-direction='left']::after {
88
+ left: initial;
89
+ right: 100%;
90
+ top: 0;
91
+ bottom: 0;
92
+ width: 200%;
93
+ }
94
+
95
+ [vaul-drawer][vaul-drawer-direction='right']::after {
96
+ left: 100%;
97
+ right: initial;
98
+ top: 0;
99
+ bottom: 0;
100
+ width: 200%;
101
+ }
102
+
103
+ [vaul-handle] {
104
+ display: block;
105
+ position: relative;
106
+ opacity: 0.8;
107
+ margin-left: auto;
108
+ margin-right: auto;
109
+ height: 5px;
110
+ width: 56px;
111
+ border-radius: 1rem;
112
+ touch-action: pan-y;
113
+ cursor: grab;
114
+ }
115
+
116
+ [vaul-handle]:hover,
117
+ [vaul-handle]:active {
118
+ opacity: 1;
119
+ }
120
+
121
+ [vaul-handle]:active {
122
+ cursor: grabbing;
123
+ }
124
+
125
+ [vaul-handle-hitarea] {
126
+ position: absolute;
127
+ left: 50%;
128
+ top: 50%;
129
+ transform: translate(-50%, -50%);
130
+ width: max(100%, 2.75rem); /* 44px */
131
+ height: max(100%, 2.75rem); /* 44px */
132
+ touch-action: inherit;
133
+ }
134
+
135
+ [vaul-overlay][vaul-snap-points='true']:not([vaul-snap-points-overlay='true']):not([data-state='closed']) {
136
+ opacity: 0;
137
+ }
138
+
139
+ [vaul-overlay][vaul-snap-points-overlay='true']:not([vaul-drawer-visible='false']) {
140
+ opacity: 1;
141
+ }
142
+
143
+ /* This will allow us to not animate via animation, but still benefit from delaying unmount via Radix. */
144
+ @keyframes fake-animation {
145
+ from {
146
+ }
147
+ to {
148
+ }
149
+ }
150
+
151
+ @media (pointer: fine) {
152
+ [vaul-handle-hitarea] {
153
+ width: 100%;
154
+ height: 100%;
155
+ }
156
+ }
157
+
158
+ @media (hover: hover) and (pointer: fine) {
159
+ [vaul-drawer] {
160
+ user-select: none;
161
+ }
162
+ }
163
+
@@ -107,11 +107,6 @@ export default {
107
107
  }),
108
108
  borderOpacity: ({ theme }) => theme('opacity'),
109
109
  borderRadius: {
110
- /* shadcn's:
111
- lg: `var(--radius)`,
112
- md: `calc(var(--radius) - 2px)`,
113
- sm: "calc(var(--radius) - 4px)",
114
- */
115
110
  none: '0px',
116
111
  sm: '0.25rem',
117
112
  DEFAULT: '0.5rem',
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../tsconfig.hanzo-modules.base.json",
2
+ "extends": "../tsconfig.hanzo.base.json",
3
3
  "include": [
4
4
  "**/*.ts",
5
5
  "**/*.tsx",
@@ -1,4 +1,3 @@
1
- import type { ButtonRoundedValue } from '../primitives/button'
2
1
  import type Dimensions from './dimensions'
3
2
 
4
3
  /**
@@ -25,7 +24,7 @@ interface ImageDef {
25
24
  */
26
25
  dim: Dimensions
27
26
 
28
- rounded?: ButtonRoundedValue
27
+ rounded?: string // any key from tailwind.config.borderRadius
29
28
  }
30
29
 
31
30
  export {
package/types/link-def.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { ButtonVariants, ButtonSizes } from '../primitives/button'
1
+ import type { VariantProps } from 'class-variance-authority'
2
+ import type { buttonVariants } from '../primitives/button'
2
3
  import type Icon from './icon'
3
4
 
4
5
  /**
@@ -6,7 +7,7 @@ import type Icon from './icon'
6
7
  *
7
8
  *
8
9
  */
9
- interface LinkDef {
10
+ interface LinkDef extends VariantProps<typeof buttonVariants> {
10
11
  /**
11
12
  * If the LinkElement is rendered directly and has children,
12
13
  * the title, icon, iconAfter fields in the supplied LinkDef
@@ -49,9 +50,6 @@ interface LinkDef {
49
50
  * rendered as a disabled link, shows default cursor, and eats pointer events.
50
51
  */
51
52
  disabled?: boolean
52
-
53
- variant?: ButtonVariants
54
- size?: ButtonSizes
55
53
  }
56
54
 
57
55
  export {
@@ -0,0 +1,3 @@
1
+ export * from './index'
2
+ // Must be imported from 'use client'
3
+ export * from './step-animation'
package/util/index.ts CHANGED
@@ -2,6 +2,8 @@ import { compiler as mdCompiler } from 'markdown-to-jsx'
2
2
 
3
3
  import { clsx, type ClassValue } from 'clsx'
4
4
  import { twMerge } from 'tailwind-merge'
5
+ export { cva, type VariantProps } from 'class-variance-authority'
6
+
5
7
  import type { Dimensions } from '../types'
6
8
 
7
9
  // @ts-ignore
@@ -71,4 +73,7 @@ export const capitalize = (str: string): string => (
71
73
  str.charAt(0).toUpperCase() + str.slice(1)
72
74
  )
73
75
 
74
- export { default as spreadToTransform } from './spread-to-transform'
76
+ export { default as spreadToTransform } from './spread-to-transform'
77
+ // Must be imported from 'use client'
78
+ // export * from './step-animation'
79
+
@@ -0,0 +1,90 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { makeObservable, reaction, computed, type IReactionDisposer, observable, action } from 'mobx'
3
+
4
+ interface StepAnimation {
5
+ notPast(step: number): boolean
6
+ }
7
+
8
+ class MyStepAnimation implements StepAnimation {
9
+
10
+ _step: number = -1
11
+ _reactionDisposer: IReactionDisposer | undefined = undefined
12
+
13
+ _initialStep: () => boolean
14
+ _intervals: number[]
15
+
16
+ /** initialStep: false -> true: step 0
17
+ true -> false: step 1
18
+ after intervals[0] : step 2
19
+ after intervals[1] : step 3
20
+
21
+ initialStep must contain at least one mobx observable and return boolean
22
+ see: https://mobx.js.org/reactions.html#reaction
23
+ */
24
+ constructor(initialStep: () => boolean, intervals: number[]) {
25
+
26
+ this._initialStep = initialStep
27
+ this._intervals = intervals
28
+
29
+ makeObservable(this, {
30
+ _step: observable,
31
+ _setStep: action
32
+ })
33
+ }
34
+
35
+ // This is separated out because reactions have to be created
36
+ // once we have a valid doc / window etc. (mobx internals)
37
+ // Can't just do it in constructor and assign to ref
38
+ initialize = () => {
39
+
40
+ const fireNext = () => {
41
+ this._setStep(this._step + 1)
42
+ if (this._step <= this._intervals.length) {
43
+ // No need to call clearTimeout(): https://stackoverflow.com/a/7391588/11645689
44
+ setTimeout(() => { fireNext() }, this._intervals[this._step - 1])
45
+ }
46
+ }
47
+
48
+ this._reactionDisposer = reaction(
49
+ this._initialStep,
50
+ (triggered: boolean) => {
51
+ if (triggered && this._step === -1) {
52
+ this._setStep(0)
53
+ }
54
+ // extra safe
55
+ else if (this._step === 0) {
56
+ fireNext()
57
+ }
58
+ }
59
+ )
60
+ }
61
+
62
+ _setStep = (v: number): void => {this._step = v}
63
+
64
+ dispose = () => {
65
+ if (this._reactionDisposer) {
66
+ this._reactionDisposer()
67
+ }
68
+ }
69
+
70
+ // https://mobx.js.org/computeds-with-args.html#2-close-over-the-arguments
71
+ notPast = (step: number): boolean => (
72
+ computed(() => (this._step > -1 && this._step <= step)).get()
73
+ )
74
+ }
75
+
76
+ const useStepAnimation = (initialStep: () => boolean, intervals: number[]): StepAnimation => {
77
+
78
+ const animRef = useRef<MyStepAnimation>(new MyStepAnimation(initialStep, intervals))
79
+ useEffect(() => {
80
+ animRef.current.initialize()
81
+ return animRef.current.dispose
82
+ }, [])
83
+
84
+ return animRef.current
85
+ }
86
+
87
+ export {
88
+ type StepAnimation,
89
+ useStepAnimation
90
+ }
@@ -0,0 +1,19 @@
1
+ class TwoWayReadonlyMap<T, K> {
2
+ map: Map<T, K>;
3
+ reverseMap: Map<K, T>;
4
+ constructor(map: Map<T, K>) {
5
+ this.map = map;
6
+ this.reverseMap = new Map<K, T>();
7
+ map.forEach((value, key) => {
8
+ this.reverseMap.set(value, key);
9
+ });
10
+ }
11
+ get(key: T) {
12
+ return this.map.get(key);
13
+ }
14
+ revGet(key: K) {
15
+ return this.reverseMap.get(key);
16
+ }
17
+ }
18
+
19
+ export default TwoWayReadonlyMap