@clicktap/ui 0.14.12 → 0.14.13

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.
Files changed (191) hide show
  1. package/components/Accordion/Accordion.tsx +82 -0
  2. package/components/Accordion/index.ts +3 -0
  3. package/components/Avatar/Avatar.stories.tsx +99 -0
  4. package/components/Avatar/Avatar.tsx +120 -0
  5. package/components/Avatar/Avatar.types.ts +3 -0
  6. package/components/Avatar/AvatarGroup/AvatarGroup.tsx +32 -0
  7. package/components/Avatar/AvatarGroup/AvatarGroup.types.ts +8 -0
  8. package/components/Avatar/index.ts +4 -0
  9. package/components/Badge/Badge.stories.tsx +72 -0
  10. package/components/Badge/Badge.tsx +169 -0
  11. package/components/Badge/Badge.types.ts +3 -0
  12. package/components/Badge/index.ts +2 -0
  13. package/components/Breadcrumbs/BreadcrumbEllipsis.tsx +47 -0
  14. package/components/Breadcrumbs/BreadcrumbEllipsis.types.ts +5 -0
  15. package/components/Breadcrumbs/BreadcrumbItem.tsx +23 -0
  16. package/components/Breadcrumbs/BreadcrumbItem.types.ts +3 -0
  17. package/components/Breadcrumbs/BreadcrumbLink.tsx +30 -0
  18. package/components/Breadcrumbs/BreadcrumbLink.types.ts +3 -0
  19. package/components/Breadcrumbs/BreadcrumbSeparator.tsx +41 -0
  20. package/components/Breadcrumbs/BreadcrumbSeparator.types.ts +9 -0
  21. package/components/Breadcrumbs/Breadcrumbs.tsx +28 -0
  22. package/components/Breadcrumbs/Breadcrumbs.types.ts +6 -0
  23. package/components/Breadcrumbs/index.ts +10 -0
  24. package/components/Button/Button.tsx +72 -0
  25. package/components/Button/Button.types.ts +7 -0
  26. package/components/Button/index.ts +2 -0
  27. package/components/Card/Card.tsx +15 -0
  28. package/components/Card/Card.types.ts +3 -0
  29. package/components/Card/index.ts +2 -0
  30. package/components/Checkbox/Checkbox.tsx +122 -0
  31. package/components/Checkbox/Checkbox.types.ts +15 -0
  32. package/components/Checkbox/index.ts +2 -0
  33. package/components/Collapsible/Collapsible.tsx +34 -0
  34. package/components/Collapsible/Collapsible.types.ts +5 -0
  35. package/components/Collapsible/CollapsibleTrigger.tsx +57 -0
  36. package/components/Collapsible/CollapsibleTrigger.types.ts +14 -0
  37. package/components/Collapsible/index.ts +10 -0
  38. package/components/Container/Container.tsx +26 -0
  39. package/components/Container/Container.types.ts +3 -0
  40. package/components/Container/index.ts +2 -0
  41. package/components/ContextMenu/ContextMenu.tsx +74 -0
  42. package/components/ContextMenu/ContextMenu.types.ts +17 -0
  43. package/components/ContextMenu/index.ts +2 -0
  44. package/components/CreditCardExpirationInput/CreditCardExpirationInput.tsx +115 -0
  45. package/components/CreditCardExpirationInput/CreditCardExpirationInput.types.ts +10 -0
  46. package/components/CreditCardExpirationInput/index.ts +2 -0
  47. package/components/CreditCardInput/CreditCardInput.tsx +147 -0
  48. package/components/CreditCardInput/CreditCardInput.types.ts +12 -0
  49. package/components/CreditCardInput/index.ts +2 -0
  50. package/components/DateInput/DateInput.tsx +81 -0
  51. package/components/DateInput/DateInput.types.ts +15 -0
  52. package/components/DateInput/index.ts +2 -0
  53. package/components/DateTimeFormat/DateTimeFormat.tsx +16 -0
  54. package/components/DateTimeFormat/DateTimeFormat.types.ts +7 -0
  55. package/components/DateTimeFormat/index.ts +2 -0
  56. package/components/Dialog/Dialog.tsx +65 -0
  57. package/components/Dialog/Dialog.types.ts +9 -0
  58. package/components/Dialog/index.ts +2 -0
  59. package/components/DialogTrigger/DialogTrigger.tsx +45 -0
  60. package/components/DialogTrigger/DialogTrigger.types.ts +6 -0
  61. package/components/DialogTrigger/index.ts +5 -0
  62. package/components/Divider/Divider.stories.tsx +37 -0
  63. package/components/Divider/Divider.tsx +34 -0
  64. package/components/Divider/Divider.types.ts +5 -0
  65. package/components/Divider/index.ts +2 -0
  66. package/components/DobInput/DobInput.tsx +120 -0
  67. package/components/DobInput/index.ts +2 -0
  68. package/components/Drawer/Drawer.tsx +126 -0
  69. package/components/Drawer/Drawer.types.ts +11 -0
  70. package/components/Drawer/index.ts +2 -0
  71. package/components/Icon/Account.tsx +50 -0
  72. package/components/Icon/Cart.tsx +43 -0
  73. package/components/Icon/Checkmark.tsx +34 -0
  74. package/components/Icon/Cross.tsx +36 -0
  75. package/components/Icon/DownArrow.tsx +23 -0
  76. package/components/Icon/Hamburger.tsx +23 -0
  77. package/components/Icon/Icon.types.ts +8 -0
  78. package/components/Icon/LinkArrow.tsx +32 -0
  79. package/components/Icon/Minus.tsx +20 -0
  80. package/components/Icon/Plus.tsx +20 -0
  81. package/components/Icon/Search.tsx +36 -0
  82. package/components/Icon/Trash.tsx +27 -0
  83. package/components/Icon/Verified.tsx +20 -0
  84. package/components/Icon/index.ts +14 -0
  85. package/components/Image/Image.tsx +32 -0
  86. package/components/Image/index.ts +2 -0
  87. package/components/Input/Input.tsx +109 -0
  88. package/components/Input/Input.types.ts +17 -0
  89. package/components/Input/index.ts +2 -0
  90. package/components/Link/Link.stories.tsx +96 -0
  91. package/components/Link/Link.tsx +34 -0
  92. package/components/Link/Link.types.ts +3 -0
  93. package/components/Link/index.ts +2 -0
  94. package/components/Loader/CircularEasing.tsx +66 -0
  95. package/components/Loader/CircularEasing.types.ts +8 -0
  96. package/components/Loader/Pulse.tsx +45 -0
  97. package/components/Loader/Pulse.types.ts +5 -0
  98. package/components/Loader/index.ts +4 -0
  99. package/components/Menu/ContextMenu.tsx +83 -0
  100. package/components/Menu/Menu.tsx +143 -0
  101. package/components/Menu/Menu.types.ts +44 -0
  102. package/components/Menu/index.ts +4 -0
  103. package/components/Meter/Meter.stories.tsx +111 -0
  104. package/components/Meter/Meter.tsx +68 -0
  105. package/components/Meter/Meter.types.ts +10 -0
  106. package/components/Meter/index.ts +2 -0
  107. package/components/Modal/Modal.tsx +16 -0
  108. package/components/Modal/Modal.types.ts +6 -0
  109. package/components/Modal/index.ts +2 -0
  110. package/components/ModalOverlay/ModalOverlay.tsx +121 -0
  111. package/components/ModalOverlay/ModalOverlay.types.ts +18 -0
  112. package/components/ModalOverlay/index.ts +2 -0
  113. package/components/NumberFormat/NumberFormat.tsx +19 -0
  114. package/components/NumberFormat/NumberFormat.types.ts +8 -0
  115. package/components/NumberFormat/index.ts +2 -0
  116. package/components/NumberInput/NumberInput.tsx +164 -0
  117. package/components/NumberInput/NumberInput.types.ts +22 -0
  118. package/components/NumberInput/index.ts +2 -0
  119. package/components/NumberTicker/DigitResolver.tsx +119 -0
  120. package/components/NumberTicker/DigitResolver.types.ts +18 -0
  121. package/components/NumberTicker/NumberTicker.tsx +56 -0
  122. package/components/NumberTicker/NumberTicker.types.ts +96 -0
  123. package/components/NumberTicker/hooks/useColumnTransition.ts +36 -0
  124. package/components/NumberTicker/hooks/useNumberDelta.ts +19 -0
  125. package/components/NumberTicker/hooks/useNumberTicker.ts +36 -0
  126. package/components/NumberTicker/index.ts +10 -0
  127. package/components/Pagination/Pagination.tsx +44 -0
  128. package/components/Pagination/index.ts +2 -0
  129. package/components/PasswordCheck/PasswordCheck.tsx +59 -0
  130. package/components/PasswordCheck/PasswordCheck.types.ts +4 -0
  131. package/components/PasswordCheck/PasswordCheck.utils.ts +47 -0
  132. package/components/PasswordCheck/index.ts +2 -0
  133. package/components/PhoneInput/PhoneInput.tsx +191 -0
  134. package/components/PhoneInput/index.ts +2 -0
  135. package/components/PinInput/PinInput.tsx +314 -0
  136. package/components/PinInput/PinInput.types.ts +21 -0
  137. package/components/PinInput/index.ts +2 -0
  138. package/components/Progressbar/CircularProgressbar.tsx +71 -0
  139. package/components/Progressbar/CircularProgressbar.types.ts +10 -0
  140. package/components/Progressbar/LinearProgressbar.tsx +75 -0
  141. package/components/Progressbar/LinearProgressbar.types.ts +11 -0
  142. package/components/Progressbar/index.ts +4 -0
  143. package/components/Radio/Radio.tsx +88 -0
  144. package/components/Radio/Radio.types.ts +16 -0
  145. package/components/Radio/index.ts +2 -0
  146. package/components/RadioGroup/RadioGroup.tsx +49 -0
  147. package/components/RadioGroup/RadioGroup.types.ts +7 -0
  148. package/components/RadioGroup/index.ts +2 -0
  149. package/components/Select/Option.tsx +32 -0
  150. package/components/Select/Option.types.ts +3 -0
  151. package/components/Select/Select.tsx +253 -0
  152. package/components/Select/Select.types.ts +42 -0
  153. package/components/Select/index.ts +8 -0
  154. package/components/Skeleton/Skeleton.tsx +15 -0
  155. package/components/Skeleton/Skeleton.types.ts +3 -0
  156. package/components/Skeleton/index.ts +2 -0
  157. package/components/Slider/Slider.tsx +110 -0
  158. package/components/Slider/Slider.types.ts +11 -0
  159. package/components/Slider/index.ts +2 -0
  160. package/components/Switch/Switch.tsx +63 -0
  161. package/components/Switch/Switch.types.ts +8 -0
  162. package/components/Switch/index.ts +2 -0
  163. package/components/Table/Table.tsx +52 -0
  164. package/components/Table/Table.types.ts +22 -0
  165. package/components/Table/index.ts +2 -0
  166. package/components/Tabs/Tab.tsx +118 -0
  167. package/components/Tabs/Tab.types.ts +10 -0
  168. package/components/Tabs/TabList.tsx +51 -0
  169. package/components/Tabs/TabList.types.ts +12 -0
  170. package/components/Tabs/TabPanel.tsx +19 -0
  171. package/components/Tabs/TabPanel.types.ts +3 -0
  172. package/components/Tabs/Tabs.context.tsx +9 -0
  173. package/components/Tabs/Tabs.tsx +39 -0
  174. package/components/Tabs/Tabs.types.ts +3 -0
  175. package/components/Tabs/index.ts +9 -0
  176. package/components/TimeInput/TimeInput.stories.tsx +125 -0
  177. package/components/TimeInput/TimeInput.tsx +81 -0
  178. package/components/TimeInput/TimeInput.types.ts +15 -0
  179. package/components/TimeInput/index.ts +2 -0
  180. package/components/ToggleButton/ToggleButton.stories.tsx +89 -0
  181. package/components/ToggleButton/ToggleButton.tsx +69 -0
  182. package/components/ToggleButton/ToggleButton.types.ts +6 -0
  183. package/components/ToggleButton/index.ts +2 -0
  184. package/components/Tooltip/Tooltip.tsx +59 -0
  185. package/components/Tooltip/Tooltip.types.ts +3 -0
  186. package/components/Tooltip/index.ts +2 -0
  187. package/components/UploadImage/UploadImage.tsx +206 -0
  188. package/components/UploadImage/UploadImage.types.ts +15 -0
  189. package/components/UploadImage/index.ts +2 -0
  190. package/package.json +1 -1
  191. package/tailwind.config.js +3 -1
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import type { ImageProps } from 'next/image';
4
+ import { useState, useEffect } from 'react';
5
+ import NextImage from 'next/image';
6
+ import { cn } from '../../utils/cn';
7
+
8
+ export function Image({ src, className, ...rest }: ImageProps) {
9
+ const [loadingImg, setLoadingImg] = useState(true);
10
+ const [image, setImage] = useState(src);
11
+ const [isClient, setIsClient] = useState(false);
12
+
13
+ useEffect(() => setImage(src), [src]);
14
+ useEffect(() => setIsClient(true), []);
15
+
16
+ return (
17
+ <NextImage
18
+ src={image}
19
+ className={cn(
20
+ 'transition-[filter] ease-linear duration-200',
21
+ isClient && loadingImg && 'blur-md',
22
+ className
23
+ )}
24
+ onError={() => setImage('/images/placeholder.jpg')}
25
+ onLoad={() => setLoadingImg(false)}
26
+ // eslint-disable-next-line react/jsx-props-no-spreading
27
+ {...rest}
28
+ />
29
+ );
30
+ }
31
+
32
+ export default Image;
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export { Image } from './Image';
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import {
4
+ FieldError,
5
+ Input as AriaInput,
6
+ Label,
7
+ Text,
8
+ TextField,
9
+ } from 'react-aria-components';
10
+ import { forwardRef } from 'react';
11
+ import { cn } from '../../utils/cn';
12
+ import type { InputProps } from './Input.types';
13
+ import { Skeleton } from '../Skeleton/Skeleton';
14
+ import { useIsClient } from '../../hooks/useIsClient';
15
+
16
+ function InputSkeleton({
17
+ className,
18
+ }: {
19
+ className: NonNullable<InputProps['classNames']>['skeleton'];
20
+ }) {
21
+ return (
22
+ <Skeleton
23
+ className={cn('w-full h-10 rounded-md z-20 relative', className)}
24
+ />
25
+ );
26
+ }
27
+
28
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
29
+ (
30
+ {
31
+ inputProps,
32
+ label,
33
+ description,
34
+ errorMessage,
35
+ placeholder,
36
+ className,
37
+ classNames,
38
+ ...props
39
+ },
40
+ ref
41
+ ) => {
42
+ const isClient = useIsClient();
43
+
44
+ if (!isClient) {
45
+ return (
46
+ <div className={cn('flex flex-col w-full text-slate-900', className)}>
47
+ {label ? (
48
+ <Label
49
+ className={cn('flex text-slate-500 text-xs', classNames?.label)}
50
+ >
51
+ {label}
52
+ </Label>
53
+ ) : null}
54
+ <InputSkeleton className={classNames?.skeleton} />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <TextField
61
+ className={cn('flex flex-col w-full text-slate-900', className)}
62
+ // eslint-disable-next-line react/jsx-props-no-spreading
63
+ {...props}
64
+ >
65
+ <Label className={cn('flex text-slate-500 text-xs', classNames?.label)}>
66
+ {label}
67
+ </Label>
68
+ <AriaInput
69
+ ref={ref}
70
+ placeholder={placeholder}
71
+ className={cn(
72
+ 'border-solid border border-slate-300 rounded-md',
73
+ 'text-sm text-slate-900 placeholder-slate-400',
74
+ 'h-10 px-2 py-0 m-0 w-full',
75
+ 'bg-white',
76
+ 'transition-all duration-200 ease-in-out',
77
+ 'hover:border-slate-400',
78
+ 'focus:outline-2 focus:outline focus:outline-slate-200 focus:border-slate-400',
79
+ 'disabled:border-slate-200 disabled:bg-slate-100',
80
+ 'invalid:border-red-500 invalid:bg-red-100 invalid:text-red-600',
81
+ 'invalid:hover:border-red-600 invalid:focus:border-red-600 invalid:focus:outline-red-200',
82
+ classNames?.input
83
+ )}
84
+ // eslint-disable-next-line react/jsx-props-no-spreading
85
+ {...inputProps}
86
+ />
87
+
88
+ {description && (
89
+ <Text
90
+ className={cn(
91
+ 'flex text-slate-500 text-sm',
92
+ classNames?.description
93
+ )}
94
+ slot="description"
95
+ >
96
+ {description}
97
+ </Text>
98
+ )}
99
+ <FieldError
100
+ className={cn('flex text-red-500 text-sm', classNames?.error)}
101
+ >
102
+ {errorMessage}
103
+ </FieldError>
104
+ </TextField>
105
+ );
106
+ }
107
+ );
108
+
109
+ export default Input;
@@ -0,0 +1,17 @@
1
+ import type {
2
+ InputProps as AriaInputProps,
3
+ TextFieldProps,
4
+ ValidationResult,
5
+ } from 'react-aria-components';
6
+ import type { SlotsToClasses } from '../../types/SlotsToClasses';
7
+
8
+ export interface InputProps extends TextFieldProps {
9
+ label?: string;
10
+ description?: string;
11
+ errorMessage?: string | ((validation: ValidationResult) => string);
12
+ placeholder?: string;
13
+ classNames?: SlotsToClasses<
14
+ 'label' | 'input' | 'description' | 'error' | 'skeleton'
15
+ >;
16
+ inputProps?: AriaInputProps;
17
+ }
@@ -0,0 +1,2 @@
1
+ export { Input } from './Input';
2
+ export type { InputProps } from './Input.types';
@@ -0,0 +1,96 @@
1
+ import { LinkProps } from 'react-aria-components';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { action } from '@storybook/addon-actions';
4
+ import { Link } from './Link';
5
+
6
+ type Story = StoryObj<typeof Link>;
7
+
8
+ function Component({ children, ...props }: LinkProps) {
9
+ return <Link {...props}>{children}</Link>;
10
+ }
11
+
12
+ const meta: Meta<typeof Link> = {
13
+ component: Component,
14
+ };
15
+
16
+ export default meta;
17
+
18
+ export const Example: Story = {
19
+ argTypes: {
20
+ href: {
21
+ control: 'text',
22
+ },
23
+ target: {
24
+ options: ['_blank', '_self', '_parent', '_top'],
25
+ control: 'select',
26
+ },
27
+ isDisabled: {
28
+ control: 'boolean',
29
+ },
30
+ autoFocus: {
31
+ control: 'boolean',
32
+ },
33
+ rel: {
34
+ control: 'text',
35
+ },
36
+ download: {
37
+ control: 'object',
38
+ },
39
+ ping: {
40
+ control: 'text',
41
+ },
42
+ referrerPolicy: {
43
+ options: [
44
+ 'no-referrer',
45
+ 'origin-when-cross-origin',
46
+ 'same-origin',
47
+ 'strict-origin',
48
+ 'strict-origin-when-cross-origin',
49
+ 'unsafe-url',
50
+ ],
51
+ control: 'select',
52
+ },
53
+ children: {
54
+ control: 'text',
55
+ },
56
+ className: {
57
+ control: 'object',
58
+ },
59
+ style: {
60
+ control: 'object',
61
+ },
62
+ onPress: {},
63
+ onBlur: {},
64
+ onFocus: {},
65
+ onFocusChange: {},
66
+ onHoverChange: {},
67
+ onHoverEnd: {},
68
+ onHoverStart: {},
69
+ onKeyDown: {},
70
+ onKeyUp: {},
71
+ onPressChange: {},
72
+ onPressEnd: {},
73
+ onPressStart: {},
74
+ onPressUp: {},
75
+ },
76
+ args: {
77
+ href: '/',
78
+ target: '_blank',
79
+ isDisabled: false,
80
+ autoFocus: false,
81
+ children: 'Press me',
82
+ onPress: action('onPress'),
83
+ onBlur: action('onBlur'),
84
+ onFocus: action('onFocus'),
85
+ onFocusChange: action('onFocusChange'),
86
+ onHoverChange: action('onHoverChange'),
87
+ onHoverEnd: action('onHoverEnd'),
88
+ onHoverStart: action('onHoverStart'),
89
+ onKeyDown: action('onKeyDown'),
90
+ onKeyUp: action('onKeyUp'),
91
+ onPressChange: action('onPressChange'),
92
+ onPressEnd: action('onPressEnd'),
93
+ onPressStart: action('onPressStart'),
94
+ onPressUp: action('onPressUp'),
95
+ },
96
+ };
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { forwardRef } from 'react';
4
+ import { Link as UiLink } from 'react-aria-components';
5
+ import { cn } from '../../utils/cn';
6
+ import type { LinkProps } from './Link.types';
7
+
8
+ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
9
+ ({ children, isDisabled, className, ...props }, ref) => (
10
+ <UiLink
11
+ // eslint-disable-next-line react/jsx-props-no-spreading
12
+ {...props}
13
+ isDisabled={isDisabled}
14
+ className={cn(
15
+ 'flex items-center cursor-pointer',
16
+ 'text-slate-500',
17
+ 'no-underline',
18
+ 'transition-colors duration-300',
19
+ 'hover:text-slate-800',
20
+ [
21
+ 'data-[disabled="true"]:cursor-default',
22
+ 'data-[disabled="true"]:text-slate-300',
23
+ 'data-[disabled="true"]:hover:text-slate-300',
24
+ ],
25
+ className
26
+ )}
27
+ ref={ref}
28
+ >
29
+ {children}
30
+ </UiLink>
31
+ )
32
+ );
33
+
34
+ export default Link;
@@ -0,0 +1,3 @@
1
+ import type { LinkProps as UILinkProps } from 'react-aria-components';
2
+
3
+ export type LinkProps = UILinkProps;
@@ -0,0 +1,2 @@
1
+ export { Link } from './Link';
2
+ export type { LinkProps } from './Link.types';
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+ import type { CSSProperties } from 'react';
5
+ import { cn } from '../../utils/cn';
6
+ import type { CircularEasingProps } from './CircularEasing.types';
7
+
8
+ export function CircularEasing({
9
+ width,
10
+ stroke,
11
+ strokeLinecap = 'round',
12
+ strokeWidth = 5,
13
+ className,
14
+ style,
15
+ ...props
16
+ }: CircularEasingProps) {
17
+ return (
18
+ <div
19
+ style={{ '--circularWidth': `${width}px`, ...style } as CSSProperties}
20
+ // eslint-disable-next-line react/jsx-props-no-spreading
21
+ {...props}
22
+ className={cn(
23
+ 'relative',
24
+ 'm-0',
25
+ 'w-[--circularWidth]',
26
+ 'aspect-square',
27
+ className
28
+ )}
29
+ >
30
+ <motion.svg
31
+ animate={{
32
+ transform: 'rotate(360deg)',
33
+ transition: { repeat: Infinity, duration: 2, ease: 'linear' },
34
+ }}
35
+ viewBox="25 25 50 50"
36
+ className={cn(
37
+ 'w-full h-full',
38
+ 'absolute inset-x-0 inset-y-0',
39
+ 'origin-center',
40
+ 'm-auto'
41
+ )}
42
+ >
43
+ <motion.circle
44
+ animate={{
45
+ strokeDasharray: ['1, 200', '89, 200', '89, 200'],
46
+ strokeDashoffset: [0, -35, -124],
47
+ transition: { repeat: Infinity, duration: 1.5, ease: 'easeInOut' },
48
+ }}
49
+ className="path"
50
+ cx="50"
51
+ cy="50"
52
+ fill="none"
53
+ r="20"
54
+ strokeDasharray="1, 200"
55
+ strokeDashoffset="0"
56
+ stroke={stroke}
57
+ strokeLinecap={strokeLinecap}
58
+ strokeMiterlimit="10"
59
+ strokeWidth={strokeWidth}
60
+ />
61
+ </motion.svg>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export default CircularEasing;
@@ -0,0 +1,8 @@
1
+ import type { HTMLAttributes } from 'react';
2
+
3
+ export type CircularEasingProps = HTMLAttributes<HTMLDivElement> & {
4
+ stroke: string;
5
+ strokeLinecap?: 'butt' | 'round' | 'square';
6
+ strokeWidth?: number;
7
+ width: number;
8
+ };
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+ import { cn } from '../../utils/cn';
5
+ import type { PulseProps } from './Pulse.types';
6
+
7
+ export function Pulse({ classNames }: PulseProps) {
8
+ return (
9
+ <span className={cn(classNames?.base)}>
10
+ {Array.from(new Array(3)).map((_, i) => (
11
+ <motion.div
12
+ // eslint-disable-next-line react/no-array-index-key
13
+ key={i}
14
+ animate={{
15
+ opacity: [1, 1, 0.7, 1, 1],
16
+ transform: [
17
+ 'scale(1)',
18
+ 'scale(1)',
19
+ 'scale(0.1)',
20
+ 'scale(1)',
21
+ 'scale(1)',
22
+ ],
23
+ transition: {
24
+ repeat: Infinity,
25
+ duration: 0.75,
26
+ delay: i * 0.1,
27
+ // ease: [0.2, 0.68, 0.18, 1.08],
28
+ },
29
+ }}
30
+ className={cn(
31
+ 'bg-slate-300',
32
+ 'inline-block',
33
+ 'w-2',
34
+ 'h-2',
35
+ 'm-0.5',
36
+ 'rounded-lg',
37
+ classNames?.dot
38
+ )}
39
+ />
40
+ ))}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ export default Pulse;
@@ -0,0 +1,5 @@
1
+ import type { SlotsToClasses } from '../../types/SlotsToClasses';
2
+
3
+ export interface PulseProps {
4
+ classNames?: SlotsToClasses<'base' | 'dot'>;
5
+ }
@@ -0,0 +1,4 @@
1
+ export { CircularEasing } from './CircularEasing';
2
+ export { Pulse } from './Pulse';
3
+ export type { CircularEasingProps } from './CircularEasing.types';
4
+ export type { PulseProps } from './Pulse.types';
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import { cloneElement } from 'react';
4
+ import { AnimatePresence } from 'framer-motion';
5
+ import { cn } from '../../utils/cn';
6
+ import { Menu } from './Menu';
7
+ import type { MenuProps } from './Menu.types';
8
+
9
+ export function ContextMenu({
10
+ slots,
11
+ role = 'tooltip',
12
+ arrow,
13
+ ...rest
14
+ }: MenuProps) {
15
+ const arrowConfig = {
16
+ width: 24,
17
+ height: 8,
18
+ className: cn('fill-slate-50', arrow?.className),
19
+ ...arrow,
20
+ };
21
+
22
+ const ContentSlot = slots?.submenu
23
+ ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
24
+ cloneElement(slots?.submenu, {
25
+ ...slots.submenu.props,
26
+ })
27
+ : null;
28
+
29
+ const SubmenuSlot = slots?.contentRoot ? (
30
+ cloneElement(
31
+ slots.contentRoot,
32
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
33
+ {
34
+ ...slots.contentRoot.props,
35
+ },
36
+ ContentSlot
37
+ )
38
+ ) : (
39
+ <div className="relative rounded-md py-4 px-6 bg-gray-50 shadow">
40
+ {ContentSlot}
41
+ </div>
42
+ );
43
+
44
+ return (
45
+ <AnimatePresence>
46
+ <Menu
47
+ role={role}
48
+ arrow={arrowConfig}
49
+ hidden={{
50
+ opacity: 0,
51
+ y: -5,
52
+ visibility: 'hidden',
53
+ transition: {
54
+ type: 'spring',
55
+ duration: 0.3,
56
+ bounce: 0,
57
+ },
58
+ }}
59
+ visible={{
60
+ opacity: 1,
61
+ y: 0,
62
+ visibility: 'visible',
63
+ transition: {
64
+ type: 'spring',
65
+ duration: 0.75,
66
+ bounce: 0,
67
+ staggerChildren: 0.075,
68
+ delayChildren: 0.1,
69
+ },
70
+ }}
71
+ slots={{
72
+ submenu: SubmenuSlot,
73
+ submenuRoot: slots?.submenuRoot,
74
+ content: slots?.content,
75
+ }}
76
+ // eslint-disable-next-line react/jsx-props-no-spreading
77
+ {...rest}
78
+ />
79
+ </AnimatePresence>
80
+ );
81
+ }
82
+
83
+ export default ContextMenu;
@@ -0,0 +1,143 @@
1
+ 'use client';
2
+
3
+ import { cloneElement, useState, useRef, useId } from 'react';
4
+ import { motion } from 'framer-motion';
5
+ import {
6
+ flip,
7
+ shift,
8
+ arrow as arrowMiddleware,
9
+ offset as offsetMiddleware,
10
+ useFocus,
11
+ useRole,
12
+ useFloating,
13
+ useInteractions,
14
+ useHover,
15
+ useClick,
16
+ useDismiss,
17
+ useDelayGroupContext,
18
+ useDelayGroup,
19
+ FloatingArrow,
20
+ autoUpdate,
21
+ } from '@floating-ui/react';
22
+ import type { ButtonProps } from 'react-aria-components';
23
+ import type { MenuProps } from './Menu.types';
24
+
25
+ export function Menu({
26
+ closeDelay = 300,
27
+ offset = 20,
28
+ openDelay = 0,
29
+ placement = 'bottom-start',
30
+ slots,
31
+ trigger = ['hover', 'focus'],
32
+ withArrow = true,
33
+ arrow,
34
+ role = 'dialog',
35
+ visible = {},
36
+ hidden = {},
37
+ floatingOptions = {},
38
+ isOpen,
39
+ onOpenChange,
40
+ }: MenuProps) {
41
+ const [isOpenUncontrolled, setIsOpenUncontrolled] = useState(false);
42
+ const arrowRef = useRef(null);
43
+ const id = useId();
44
+ const { x, y, strategy, refs, context } = useFloating({
45
+ placement,
46
+ middleware: [
47
+ offsetMiddleware(offset),
48
+ flip(),
49
+ shift(),
50
+ arrowMiddleware({
51
+ element: arrowRef,
52
+ }),
53
+ ],
54
+ open: isOpen || isOpenUncontrolled,
55
+ onOpenChange: onOpenChange || setIsOpenUncontrolled,
56
+ whileElementsMounted: autoUpdate,
57
+ ...floatingOptions,
58
+ });
59
+
60
+ const dismissConfig = useDismiss(context);
61
+ const roleConfig = useRole(context, {
62
+ role,
63
+ });
64
+ const { delay: delayGroup } = useDelayGroupContext();
65
+ const delay = {
66
+ open: openDelay,
67
+ close: closeDelay,
68
+ };
69
+ const hoverConfig = useHover(context, { delay: delayGroup || delay });
70
+ const focusConfig = useFocus(context);
71
+ const clickConfig = useClick(context);
72
+
73
+ const interactions = [
74
+ trigger.find((value) => value === 'hover') ? hoverConfig : undefined,
75
+ trigger.find((value) => value === 'click') ? clickConfig : undefined,
76
+ trigger.find((value) => value === 'focus') ? focusConfig : undefined,
77
+ dismissConfig,
78
+ roleConfig,
79
+ ];
80
+
81
+ const { getReferenceProps, getFloatingProps } = useInteractions(interactions);
82
+
83
+ useDelayGroup(context, { id });
84
+
85
+ // https://github.com/floating-ui/floating-ui/issues/2646
86
+ const { onClick, ...referenceProps } = getReferenceProps();
87
+ const keyboardEvents = String(
88
+ (slots?.content?.props as ButtonProps).className
89
+ ).includes('pressable')
90
+ ? { onPress: onClick }
91
+ : { onClick };
92
+
93
+ const ContentSlot = slots?.content ? (
94
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
95
+ cloneElement(slots.content, {
96
+ ...slots.content.props,
97
+ ref: refs.setReference,
98
+ ...referenceProps,
99
+ ...keyboardEvents,
100
+ })
101
+ ) : (
102
+ // eslint-disable-next-line react/jsx-props-no-spreading
103
+ <div ref={refs.setReference} {...getReferenceProps()}>
104
+ Menu Action
105
+ </div>
106
+ );
107
+
108
+ const SubmenuSlot = slots?.submenu ? (
109
+ <motion.nav
110
+ ref={refs.setFloating}
111
+ style={{
112
+ position: strategy,
113
+ top: y ?? 0,
114
+ left: x ?? 0,
115
+ width: 'max-content',
116
+ zIndex: 50,
117
+ }}
118
+ // eslint-disable-next-line react/jsx-props-no-spreading
119
+ {...getFloatingProps()}
120
+ variants={{ hidden, visible }}
121
+ initial="hidden"
122
+ animate={isOpen || isOpenUncontrolled ? 'visible' : 'hidden'}
123
+ >
124
+ {withArrow && (
125
+ // eslint-disable-next-line react/jsx-props-no-spreading
126
+ <FloatingArrow ref={arrowRef} context={context} {...arrow} />
127
+ )}
128
+
129
+ {slots.submenu}
130
+ </motion.nav>
131
+ ) : (
132
+ <nav>Submenu</nav>
133
+ );
134
+
135
+ return (
136
+ <>
137
+ {ContentSlot}
138
+ {SubmenuSlot}
139
+ </>
140
+ );
141
+ }
142
+
143
+ export default Menu;