polpo 0.1.13 → 0.1.16

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 (204) hide show
  1. package/.turbo/cache/eslint/.cache_16wwsjl +1 -0
  2. package/.turbo/cache/eslint/.cache_rpznjo +1 -0
  3. package/.turbo/turbo-build$colon$code.log +49 -0
  4. package/.turbo/turbo-build$colon$css.log +2 -0
  5. package/.turbo/turbo-build.log +43 -0
  6. package/.turbo/turbo-lint.log +2 -0
  7. package/dist/components.cjs +1 -3655
  8. package/dist/components.css +2 -2173
  9. package/dist/components.js +1 -3576
  10. package/dist/helpers.cjs +1 -311
  11. package/dist/helpers.d.cts +4 -1
  12. package/dist/helpers.d.ts +4 -1
  13. package/dist/helpers.js +1 -276
  14. package/dist/hooks.cjs +1 -1065
  15. package/dist/hooks.js +1 -998
  16. package/dist/layouts.cjs +1 -146
  17. package/dist/layouts.css +2 -5
  18. package/dist/layouts.js +1 -119
  19. package/dist/types.cjs +1 -19
  20. package/dist/types.js +0 -1
  21. package/eslint.config.js +4 -0
  22. package/package.json +16 -11
  23. package/postcss.config.js +3 -0
  24. package/src/components/accordion/accordion-item.tsx +123 -0
  25. package/src/components/accordion/accordion.component.tsx +80 -0
  26. package/src/components/accordion/accordion.styles.css +41 -0
  27. package/src/components/accordion/index.ts +2 -0
  28. package/src/components/button/button.component.tsx +50 -0
  29. package/src/components/button/button.styles.css +317 -0
  30. package/src/components/button/index.ts +1 -0
  31. package/src/components/cards/flip-card/flip-card.component.tsx +66 -0
  32. package/src/components/cards/flip-card/flip-card.styles.css +50 -0
  33. package/src/components/cards/flip-card/index.ts +1 -0
  34. package/src/components/cards/hover-card/hover-card.component.tsx +76 -0
  35. package/src/components/cards/hover-card/hover-card.styles.css +13 -0
  36. package/src/components/cards/hover-card/index.ts +1 -0
  37. package/src/components/cards/index.ts +3 -0
  38. package/src/components/cards/slide-card/index.ts +1 -0
  39. package/src/components/cards/slide-card/slide-card.component.tsx +27 -0
  40. package/src/components/component.types.ts +31 -0
  41. package/src/components/cursor/cursor.component.tsx +49 -0
  42. package/src/components/cursor/cursor.styles.css +73 -0
  43. package/src/components/cursor/index.ts +1 -0
  44. package/src/components/form/checkbox/checkbox.component.tsx +95 -0
  45. package/src/components/form/checkbox/checkbox.styles.css +162 -0
  46. package/src/components/form/checkbox/index.ts +1 -0
  47. package/src/components/form/controller/controller.component.tsx +42 -0
  48. package/src/components/form/controller/index.ts +1 -0
  49. package/src/components/form/date-picker/date-picker.component.tsx +66 -0
  50. package/src/components/form/date-picker/index.ts +1 -0
  51. package/src/components/form/field/field.component.tsx +84 -0
  52. package/src/components/form/field/field.styles.css +98 -0
  53. package/src/components/form/field/field.types.ts +34 -0
  54. package/src/components/form/field/index.ts +2 -0
  55. package/src/components/form/form.types.ts +37 -0
  56. package/src/components/form/index.ts +14 -0
  57. package/src/components/form/input/index.ts +1 -0
  58. package/src/components/form/input/input.component.tsx +75 -0
  59. package/src/components/form/input-color/index.ts +1 -0
  60. package/src/components/form/input-color/input-color.component.tsx +170 -0
  61. package/src/components/form/input-color/input-color.styles.css +92 -0
  62. package/src/components/form/input-file/index.ts +1 -0
  63. package/src/components/form/input-file/input-file.component.tsx +221 -0
  64. package/src/components/form/input-file/input-file.styles.css +143 -0
  65. package/src/components/form/input-number/index.ts +1 -0
  66. package/src/components/form/input-number/input-number.component.tsx +144 -0
  67. package/src/components/form/input-password/index.ts +1 -0
  68. package/src/components/form/input-password/input-password.component.tsx +77 -0
  69. package/src/components/form/radio/index.ts +1 -0
  70. package/src/components/form/radio/radio.component.tsx +92 -0
  71. package/src/components/form/radio/radio.styles.css +117 -0
  72. package/src/components/form/select/index.ts +1 -0
  73. package/src/components/form/select/option.tsx +101 -0
  74. package/src/components/form/select/options.tsx +165 -0
  75. package/src/components/form/select/select.component.tsx +317 -0
  76. package/src/components/form/select/select.styles.css +115 -0
  77. package/src/components/form/select/select.types.ts +97 -0
  78. package/src/components/form/slider/index.ts +1 -0
  79. package/src/components/form/slider/slider.component.tsx +117 -0
  80. package/src/components/form/slider/slider.styles.css +94 -0
  81. package/src/components/form/switch/index.ts +1 -0
  82. package/src/components/form/switch/switch.component.tsx +166 -0
  83. package/src/components/form/switch/switch.styles.css +165 -0
  84. package/src/components/form/textarea/index.ts +1 -0
  85. package/src/components/form/textarea/textarea.component.tsx +80 -0
  86. package/src/components/form/textarea/textarea.styles.css +7 -0
  87. package/src/components/image/image.tsx +7 -0
  88. package/src/components/image/index.ts +1 -0
  89. package/src/components/index.ts +15 -0
  90. package/src/components/infinity-scroll/index.ts +1 -0
  91. package/src/components/infinity-scroll/infinity-scroll.component.tsx +51 -0
  92. package/src/components/infinity-scroll/infinity-scroll.styles.css +25 -0
  93. package/src/components/line/index.ts +1 -0
  94. package/src/components/line/line.component.tsx +77 -0
  95. package/src/components/line/line.styles.css +61 -0
  96. package/src/components/modals/action-modal/action-modal.component.tsx +150 -0
  97. package/src/components/modals/action-modal/action-modal.styles.css +115 -0
  98. package/src/components/modals/action-modal/index.ts +1 -0
  99. package/src/components/modals/aside-modal/aside-modal.component.tsx +63 -0
  100. package/src/components/modals/aside-modal/aside-modal.styles.css +86 -0
  101. package/src/components/modals/aside-modal/index.ts +1 -0
  102. package/src/components/modals/confirmation-modal/confirmation-modal.component.tsx +43 -0
  103. package/src/components/modals/confirmation-modal/confirmation-modal.styles.css +17 -0
  104. package/src/components/modals/confirmation-modal/index.ts +1 -0
  105. package/src/components/modals/index.ts +6 -0
  106. package/src/components/modals/menu/index.ts +1 -0
  107. package/src/components/modals/menu/menu.component.tsx +194 -0
  108. package/src/components/modals/menu/menu.styles.css +101 -0
  109. package/src/components/modals/modal/index.ts +2 -0
  110. package/src/components/modals/modal/modal.backdrop.tsx +67 -0
  111. package/src/components/modals/modal/modal.component.tsx +104 -0
  112. package/src/components/modals/modal/modal.styles.css +70 -0
  113. package/src/components/modals/portal/index.ts +1 -0
  114. package/src/components/modals/portal/portal.component.tsx +19 -0
  115. package/src/components/ripple/index.ts +1 -0
  116. package/src/components/ripple/ripple.component.tsx +70 -0
  117. package/src/components/ripple/ripple.styles.css +33 -0
  118. package/src/components/smart-table/index.ts +2 -0
  119. package/src/components/smart-table/smart-table.column.tsx +62 -0
  120. package/src/components/smart-table/smart-table.component.tsx +116 -0
  121. package/src/components/smart-table/smart-table.helpers.tsx +58 -0
  122. package/src/components/smart-table/smart-table.hooks.ts +27 -0
  123. package/src/components/smart-table/smart-table.row.tsx +28 -0
  124. package/src/components/smart-table/smart-table.styles.css +102 -0
  125. package/src/components/smart-table/smart-table.types.ts +42 -0
  126. package/src/components/tabs/index.ts +1 -0
  127. package/src/components/tabs/tabs-list.tsx +120 -0
  128. package/src/components/tabs/tabs.styles.css +164 -0
  129. package/src/components/tabs/tabs.tsx +119 -0
  130. package/src/components/tag/index.ts +1 -0
  131. package/src/components/tag/tag.component.tsx +34 -0
  132. package/src/components/tag/tag.styles.css +50 -0
  133. package/src/components/tooltips/click-to-copy/click-to-copy.component.tsx +41 -0
  134. package/src/components/tooltips/click-to-copy/index.ts +1 -0
  135. package/src/components/tooltips/index.ts +2 -0
  136. package/src/components/tooltips/tooltip/index.ts +1 -0
  137. package/src/components/tooltips/tooltip/tooltip.component.tsx +64 -0
  138. package/src/components/tooltips/tooltip/tooltip.styles.css +91 -0
  139. package/src/components/typography/index.ts +1 -0
  140. package/src/components/typography/typography.component.tsx +81 -0
  141. package/src/components/typography/typography.constants.ts +53 -0
  142. package/src/components/typography/typography.styles.css +122 -0
  143. package/src/helpers/cn.ts +6 -0
  144. package/src/helpers/format-bytes.ts +11 -0
  145. package/src/helpers/format-dates.ts +47 -0
  146. package/src/helpers/get-modal-position-relative-to-screen.ts +86 -0
  147. package/src/helpers/get-modal-position.ts +211 -0
  148. package/src/helpers/index.ts +6 -0
  149. package/src/helpers/text/index.ts +1 -0
  150. package/src/helpers/text/to-capitalize.ts +17 -0
  151. package/src/hooks/index.ts +30 -0
  152. package/src/hooks/use-async.ts +88 -0
  153. package/src/hooks/use-classnames.ts +13 -0
  154. package/src/hooks/use-click-outside.ts +32 -0
  155. package/src/hooks/use-constant.ts +3 -0
  156. package/src/hooks/use-cookie.ts +124 -0
  157. package/src/hooks/use-debounce-state.ts +13 -0
  158. package/src/hooks/use-debounce.ts +15 -0
  159. package/src/hooks/use-dimensions.ts +19 -0
  160. package/src/hooks/use-dom-container.ts +35 -0
  161. package/src/hooks/use-event-listener.ts +71 -0
  162. package/src/hooks/use-file-reader.ts +69 -0
  163. package/src/hooks/use-geolocation.ts +63 -0
  164. package/src/hooks/use-hover.ts +17 -0
  165. package/src/hooks/use-in-view.ts +18 -0
  166. package/src/hooks/use-input-handlers.ts +52 -0
  167. package/src/hooks/use-intersection-observer.ts +19 -0
  168. package/src/hooks/use-media-query.ts +25 -0
  169. package/src/hooks/use-modal-in-container.ts +85 -0
  170. package/src/hooks/use-modal-transition.ts +64 -0
  171. package/src/hooks/use-modal.ts +21 -0
  172. package/src/hooks/use-mouse-position.ts +64 -0
  173. package/src/hooks/use-online-status.ts +12 -0
  174. package/src/hooks/use-render-count.ts +11 -0
  175. package/src/hooks/use-resize-observer.ts +18 -0
  176. package/src/hooks/use-safe-dispatch.ts +22 -0
  177. package/src/hooks/use-scroll.ts +31 -0
  178. package/src/hooks/use-state-history.ts +22 -0
  179. package/src/hooks/use-toggle-values.ts +14 -0
  180. package/src/hooks/use-toggle.ts +11 -0
  181. package/src/hooks/use-viewport.ts +38 -0
  182. package/src/index.ts +5 -0
  183. package/src/layouts/flex/flex.component.tsx +75 -0
  184. package/src/layouts/flex/index.ts +1 -0
  185. package/src/layouts/grid/grid.component.tsx +89 -0
  186. package/src/layouts/grid/grid.styles.css +5 -0
  187. package/src/layouts/grid/index.ts +1 -0
  188. package/src/layouts/index.ts +2 -0
  189. package/src/types/generics.ts +68 -0
  190. package/src/types/index.ts +1 -0
  191. package/tsconfig.json +10 -0
  192. package/tsup.config.cjs +36 -0
  193. package/dist/components.cjs.map +0 -1
  194. package/dist/components.css.map +0 -1
  195. package/dist/components.js.map +0 -1
  196. package/dist/helpers.cjs.map +0 -1
  197. package/dist/helpers.js.map +0 -1
  198. package/dist/hooks.cjs.map +0 -1
  199. package/dist/hooks.js.map +0 -1
  200. package/dist/layouts.cjs.map +0 -1
  201. package/dist/layouts.css.map +0 -1
  202. package/dist/layouts.js.map +0 -1
  203. package/dist/types.cjs.map +0 -1
  204. package/dist/types.js.map +0 -1
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { UseControllerProps } from 'react-hook-form';
3
+
4
+ export type Props = { [key: string]: unknown };
5
+
6
+ export type SharedProps = {
7
+ name: string;
8
+ className?: string;
9
+ style?: React.CSSProperties;
10
+ autoComplete?: string;
11
+ autoFocus?: boolean;
12
+ placeholder?: string;
13
+ disabled?: boolean;
14
+ readOnly?: boolean;
15
+ onBlur?: (e: React.FocusEvent<unknown>) => void;
16
+ onFocus?: (e: React.FocusEvent<unknown>) => void;
17
+ };
18
+
19
+ export type UnControlledProps<V> = {
20
+ value: V;
21
+ setValue: (value: V) => void;
22
+ invalid?: boolean;
23
+ isTouched?: boolean;
24
+ isDirty?: boolean;
25
+ error?: string;
26
+ };
27
+
28
+ export type ControlledProps<V> = {
29
+ defaultValue?: V;
30
+ };
31
+
32
+ export type UnControlledComponentProps<T extends Props, V> = T & SharedProps & UnControlledProps<V>;
33
+
34
+ export type ControlledComponentProps<T extends Props, V> = T & SharedProps & ControlledProps<V>;
35
+
36
+ export type ControllerGeneratorProps<T extends Props, V> = ControlledComponentProps<T, V> &
37
+ Partial<Pick<UseControllerProps, 'rules'>>;
@@ -0,0 +1,14 @@
1
+ export * from './checkbox';
2
+ export * from './date-picker';
3
+ export * from './field';
4
+ export * from './input';
5
+ export * from './input-password';
6
+ export * from './input-color';
7
+ export * from './input-file';
8
+ export * from './input-number';
9
+ export * from './field';
10
+ export * from './radio';
11
+ export * from './select';
12
+ export * from './slider';
13
+ export * from './textarea';
14
+ export * from './switch';
@@ -0,0 +1 @@
1
+ export * from './input.component';
@@ -0,0 +1,75 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { useInputHandlers } from '../../../hooks';
4
+ import { Controller } from '../controller';
5
+ import { Field, InputFieldProps } from '../field';
6
+ import { ControllerGeneratorProps, UnControlledComponentProps } from '../form.types';
7
+
8
+ type IProps = InputFieldProps<{
9
+ pattern?: string;
10
+ type?: 'email' | 'search' | 'text' | 'url';
11
+ min?: number;
12
+ max?: number;
13
+ }>;
14
+
15
+ export const Input = ({
16
+ name,
17
+ value,
18
+ setValue,
19
+ onBlur,
20
+ onFocus,
21
+ pattern,
22
+ type = 'text',
23
+ className = '',
24
+ style = {},
25
+ autoFocus = false,
26
+ readOnly = false,
27
+ disabled = false,
28
+ placeholder = '',
29
+ autoComplete = 'off',
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ isDirty = false,
32
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
+ isTouched = false,
34
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
35
+ invalid = false,
36
+ error,
37
+ min,
38
+ max,
39
+ ...fieldProps
40
+ }: UnControlledComponentProps<IProps, string>) => {
41
+ const id = useMemo(() => crypto.randomUUID(), []);
42
+ const { handlers, isFocus } = useInputHandlers({
43
+ onChange: e => setValue(e.target.value),
44
+ onBlur: onBlur,
45
+ onFocus: onFocus,
46
+ });
47
+
48
+ return (
49
+ <Field id={id} error={error} isFocus={isFocus} {...fieldProps}>
50
+ <input
51
+ id={id}
52
+ type={type}
53
+ name={name}
54
+ className={className}
55
+ style={style}
56
+ value={value}
57
+ pattern={pattern}
58
+ autoFocus={autoFocus}
59
+ autoComplete={autoComplete}
60
+ placeholder={placeholder}
61
+ disabled={disabled}
62
+ readOnly={readOnly}
63
+ min={min}
64
+ max={max}
65
+ {...handlers}
66
+ />
67
+ </Field>
68
+ );
69
+ };
70
+
71
+ const InputController = ({ rules, ...props }: ControllerGeneratorProps<IProps, string>) => {
72
+ return <Controller Component={Input} defaultValue='' inputProps={props} rules={rules} />;
73
+ };
74
+
75
+ Input.Controller = InputController;
@@ -0,0 +1 @@
1
+ export * from './input-color.component';
@@ -0,0 +1,170 @@
1
+ import Color from 'color';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
4
+ import { FaEyeDropper } from 'react-icons/fa6';
5
+ import useEyeDropper from 'use-eye-dropper';
6
+
7
+ import { useClassNames, useInputHandlers } from '../../../hooks';
8
+ import { Modal } from '../../modals';
9
+ import { Controller } from '../controller';
10
+ import { Field, InputFieldProps } from '../field';
11
+ import { ControllerGeneratorProps, UnControlledComponentProps } from '../form.types';
12
+
13
+ import './input-color.styles.css';
14
+
15
+ type ColorProps = InputFieldProps<{
16
+ showValueText?: boolean;
17
+ }>;
18
+
19
+ export const InputColor = ({
20
+ name,
21
+ value,
22
+ setValue,
23
+ onBlur,
24
+ onFocus,
25
+ showValueText = false,
26
+ className = '',
27
+ style = {},
28
+ autoFocus = false,
29
+ readOnly = false,
30
+ disabled = false,
31
+ placeholder = '',
32
+ autoComplete = 'off',
33
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
34
+ isDirty = false,
35
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
36
+ isTouched = false,
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
+ invalid = false,
39
+ error,
40
+ ...fieldProps
41
+ }: UnControlledComponentProps<ColorProps, string>) => {
42
+ const { open, isSupported } = useEyeDropper();
43
+ const [inputValue, setInputValue] = useState<string>(value);
44
+ const id = useMemo(() => crypto.randomUUID(), []);
45
+ const containerRef = useRef<HTMLElement>(null);
46
+ const [isOpen, setIsOpen] = useState(false);
47
+ const { handlers, isFocus } = useInputHandlers({
48
+ onChange: e => setInputValue(e.target.value),
49
+ onBlur: e => {
50
+ setInputColor();
51
+
52
+ if (onBlur) onBlur(e as React.FocusEvent<HTMLInputElement>);
53
+ },
54
+ onFocus: e => {
55
+ setInputColor();
56
+
57
+ if (onFocus) onFocus(e as React.FocusEvent<HTMLInputElement>);
58
+ },
59
+ });
60
+
61
+ const borderColor = useMemo(() => {
62
+ const color = Color(value === '' ? '#000000' : value);
63
+
64
+ if (color.isLight() || color.alpha() < 0.5) {
65
+ return '#000000';
66
+ }
67
+
68
+ return '#FFFFFF';
69
+ }, [value]);
70
+
71
+ const openEyeDropper = async () => {
72
+ const { sRGBHex } = await open();
73
+ setValue(sRGBHex);
74
+ };
75
+
76
+ useEffect(() => {
77
+ setInputValue(value);
78
+ }, [value]);
79
+
80
+ const setInputColor = useCallback(() => {
81
+ if (inputValue.match(/^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i)) {
82
+ setValue(inputValue);
83
+ } else {
84
+ setInputValue(value);
85
+ }
86
+ }, [inputValue, setValue, value]);
87
+
88
+ const inputBoxClassName = useClassNames({
89
+ 'input-color-box': true,
90
+ [className]: Boolean(className),
91
+ });
92
+
93
+ return (
94
+ <Field id={id} error={error} isFocus={isFocus} {...fieldProps}>
95
+ <section
96
+ className='input-color'
97
+ onClick={e => {
98
+ e.stopPropagation();
99
+ setIsOpen(true);
100
+ }}
101
+ ref={containerRef}
102
+ >
103
+ <section
104
+ className={inputBoxClassName}
105
+ style={{
106
+ borderColor,
107
+ background: value,
108
+ color: value,
109
+ ...style,
110
+ }}
111
+ />
112
+ {showValueText ? (
113
+ <input
114
+ id={id}
115
+ name={name}
116
+ value={inputValue}
117
+ placeholder={placeholder}
118
+ readOnly={readOnly}
119
+ autoFocus={autoFocus}
120
+ disabled={disabled}
121
+ autoComplete={autoComplete}
122
+ className='color-input'
123
+ {...handlers}
124
+ />
125
+ ) : null}
126
+ </section>
127
+
128
+ <Modal
129
+ id='input-color'
130
+ isOpen={isOpen}
131
+ onClose={() => setIsOpen(false)}
132
+ position='bottom right'
133
+ offset={5}
134
+ backdrop='transparent'
135
+ containerRef={containerRef}
136
+ className='input-color-selector'
137
+ >
138
+ <HexAlphaColorPicker id={id} color={value} onChange={setValue} />
139
+ <section className='color-input-container'>
140
+ {isSupported() ? (
141
+ <FaEyeDropper
142
+ onClick={() => {
143
+ void openEyeDropper();
144
+ }}
145
+ />
146
+ ) : (
147
+ <span />
148
+ )}
149
+ <HexColorInput
150
+ className='color-input'
151
+ id={id}
152
+ name={name}
153
+ color={value}
154
+ placeholder='Type a color'
155
+ prefixed
156
+ alpha
157
+ onChange={setValue}
158
+ />
159
+ <span />
160
+ </section>
161
+ </Modal>
162
+ </Field>
163
+ );
164
+ };
165
+
166
+ const InputColorController = ({ rules, ...props }: ControllerGeneratorProps<ColorProps, string>) => {
167
+ return <Controller Component={InputColor} defaultValue='#147EFB' inputProps={props} rules={rules} />;
168
+ };
169
+
170
+ InputColor.Controller = InputColorController;
@@ -0,0 +1,92 @@
1
+ @reference "polpo-tailwind-config/styles";
2
+
3
+ .input-color {
4
+ display: grid;
5
+ grid-auto-flow: column;
6
+ align-items: center;
7
+ justify-content: start;
8
+ gap: 0.5em;
9
+
10
+ .input-color-value {
11
+ margin-left: 10px;
12
+ }
13
+
14
+ .color-input {
15
+ padding-right: 1em;
16
+ border-radius: inherit;
17
+ width: 100%;
18
+ @apply text-label;
19
+ font-family: var(--font-main) sans-serif;
20
+ }
21
+ }
22
+
23
+ .input-color-box {
24
+ background: white;
25
+ border-radius: 5px;
26
+ border: 2px solid;
27
+ width: 25px;
28
+ height: 25px;
29
+ position: relative;
30
+ cursor: pointer;
31
+
32
+ &::before {
33
+ content: '';
34
+ position: absolute;
35
+ border-radius: inherit;
36
+ top: 0;
37
+ left: 0;
38
+ width: 100%;
39
+ height: 100%;
40
+ background: repeating-conic-gradient(#cccccc 0% 25%, white 0% 50%) 50% / 8px 8px;
41
+ z-index: -1;
42
+ }
43
+ }
44
+
45
+ .input-color-selector {
46
+ display: grid;
47
+ gap: 10px;
48
+ background: var(--color-background);
49
+ color: var(--color-foreground);
50
+ padding: 10px;
51
+ border-radius: 0 18px 18px 18px;
52
+ border: 1px solid;
53
+ box-shadow: 0 5px 30px -5px var(--color-gray-900);
54
+
55
+ .react-colorful__pointer {
56
+ width: 1em;
57
+ height: 1em;
58
+ border-radius: 50%;
59
+ }
60
+
61
+ .react-colorful__hue-pointer,
62
+ .react-colorful__alpha-pointer {
63
+ width: 8px;
64
+ height: 100%;
65
+ border-radius: 4px;
66
+ border-width: 2px;
67
+ }
68
+
69
+ .color-input-container {
70
+ display: grid;
71
+ grid-template-columns: 16px 1fr 16px;
72
+ align-items: center;
73
+ gap: 10px;
74
+ border: 1px solid;
75
+ border-radius: 8px;
76
+ padding: 0.5em;
77
+ width: 100%;
78
+ max-width: 200px;
79
+ }
80
+
81
+ .color-input {
82
+ padding: 0;
83
+ border-radius: inherit;
84
+ background: var(--color-background);
85
+ color: var(--color-foreground);
86
+ width: 100%;
87
+ @apply text-label;
88
+ font-family: var(--font-main) sans-serif;
89
+ letter-spacing: 2px;
90
+ text-align: center;
91
+ }
92
+ }
@@ -0,0 +1 @@
1
+ export * from './input-file.component';
@@ -0,0 +1,221 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { IconType } from 'react-icons';
3
+ import { FaMagnifyingGlass, FaRegTrashCan } from 'react-icons/fa6';
4
+ import { IoIosWarning } from 'react-icons/io';
5
+ import { PiFilePdfFill, PiFilePngFill } from 'react-icons/pi';
6
+
7
+ import { formatBytes } from '../../../helpers';
8
+ import { FileTypeEnum, useInputHandlers } from '../../../hooks';
9
+ import { Grid } from '../../../layouts';
10
+ import { Typography } from '../../typography';
11
+ import { Controller } from '../controller';
12
+ import { ControllerGeneratorProps, UnControlledComponentProps } from '../form.types';
13
+
14
+ import './input-file.styles.css';
15
+
16
+ type InputFileProps = {
17
+ label?: string;
18
+ accept?: string;
19
+ multiple?: boolean;
20
+ limitSize?: number;
21
+ errorTimeout?: number;
22
+ };
23
+
24
+ type InputFileValue = { [key: string]: File };
25
+
26
+ export const InputFile = ({
27
+ name,
28
+ value = {},
29
+ setValue,
30
+ onBlur,
31
+ onFocus,
32
+ className = '',
33
+ style = {},
34
+ autoFocus = false,
35
+ readOnly = false,
36
+ disabled = false,
37
+ placeholder = 'Click to upload or drag and drop',
38
+ autoComplete = 'off',
39
+ accept,
40
+ multiple = false,
41
+ limitSize = 5000000,
42
+ errorTimeout = 3000,
43
+ label,
44
+ /*
45
+ * isDirty = false,
46
+ * isTouched = false,
47
+ * invalid = false,
48
+ * error,
49
+ */
50
+ }: UnControlledComponentProps<InputFileProps, InputFileValue>) => {
51
+ const [error, setError] = useState<string | null>(null);
52
+ const id = useMemo(() => crypto.randomUUID(), []);
53
+ const { handlers } = useInputHandlers<HTMLInputElement>({
54
+ onBlur,
55
+ onFocus,
56
+ onChange: e => {
57
+ const { files } = e.target;
58
+
59
+ if (files) saveFiles(files);
60
+ },
61
+ });
62
+
63
+ useEffect(() => {
64
+ let intervalId: ReturnType<typeof setTimeout> | null = null;
65
+
66
+ if (error !== null) {
67
+ intervalId = setTimeout(() => {
68
+ setError(null);
69
+ }, errorTimeout);
70
+ }
71
+
72
+ return () => {
73
+ if (intervalId !== null) {
74
+ clearTimeout(intervalId);
75
+ }
76
+ };
77
+ }, [error, errorTimeout]);
78
+
79
+ const saveFiles = (files: FileList) => {
80
+ const finalFiles = multiple ? { ...value } : {};
81
+
82
+ const filesToCheck = multiple ? files.length : 1;
83
+
84
+ for (let i = 0; i < filesToCheck; i++) {
85
+ const file = files.item(i);
86
+
87
+ if (!file) {
88
+ continue;
89
+ }
90
+
91
+ if (file.size < limitSize) {
92
+ finalFiles[file.name] = file;
93
+ } else {
94
+ setError(`File '${file.name}' size is larger than ${formatBytes(limitSize)}`);
95
+ }
96
+ }
97
+
98
+ setValue(finalFiles);
99
+ };
100
+
101
+ const handleDrop = (e: React.DragEvent<HTMLInputElement>) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+
105
+ if (e.dataTransfer.files && e.dataTransfer.files.length) {
106
+ saveFiles(e.dataTransfer.files);
107
+ }
108
+ };
109
+
110
+ const getIconType = useCallback((type: FileTypeEnum): IconType => {
111
+ const iconTypes: Record<FileTypeEnum, IconType> = {
112
+ [FileTypeEnum.PNG]: PiFilePngFill,
113
+ [FileTypeEnum.PDF]: PiFilePdfFill,
114
+ };
115
+
116
+ return iconTypes[type] ?? IoIosWarning;
117
+ }, []);
118
+
119
+ const deleteFile = (file: File) => {
120
+ const prevValue = { ...value };
121
+ delete prevValue[file.name];
122
+ setValue(prevValue);
123
+ };
124
+
125
+ const deleteAll = () => {
126
+ setValue({});
127
+ };
128
+
129
+ const totalSize = useMemo(() => Object.values(value).reduce((prev, { size }) => prev + size, 0), [value]);
130
+
131
+ const renderFileToCard = (file: File, key: number) => {
132
+ const Icon = getIconType(file.type as FileTypeEnum);
133
+
134
+ return (
135
+ <section className='input-file-card' key={key}>
136
+ <section className='file-card--image'>
137
+ <Icon />
138
+ </section>
139
+ <Grid>
140
+ <Typography variant='label' noPadding nowrap>
141
+ {file.name}
142
+ </Typography>
143
+ <Typography noPadding variant='small'>
144
+ {formatBytes(file.size)}
145
+ </Typography>
146
+ </Grid>
147
+ <section className='file-card--delete' onClick={() => deleteFile(file)}>
148
+ <FaRegTrashCan />
149
+ </section>
150
+ </section>
151
+ );
152
+ };
153
+
154
+ return (
155
+ <section className='input-file-container'>
156
+ {Boolean(label) && (
157
+ <Typography variant='label-form' htmlFor={id} noPadding>
158
+ {label}
159
+ </Typography>
160
+ )}
161
+ <section className='input-file--box' onDrop={handleDrop}>
162
+ <section className='input-file--box--icon'>
163
+ <FaMagnifyingGlass />
164
+ </section>
165
+ <Grid>
166
+ <Typography variant='label-form' htmlFor={id} noPadding weight='bold'>
167
+ {placeholder}
168
+ </Typography>
169
+ <Typography variant='small' noPadding>
170
+ Max size ({formatBytes(limitSize)})
171
+ </Typography>
172
+ </Grid>
173
+ <input
174
+ id={id}
175
+ type='file'
176
+ name={name}
177
+ value=''
178
+ placeholder={placeholder}
179
+ readOnly={readOnly}
180
+ autoFocus={autoFocus}
181
+ disabled={disabled}
182
+ autoComplete={autoComplete}
183
+ className={className}
184
+ style={style}
185
+ accept={accept}
186
+ multiple={multiple}
187
+ title=''
188
+ {...handlers}
189
+ />
190
+ </section>
191
+ {error !== null && (
192
+ <Typography variant='small' noPadding className='error'>
193
+ {error}
194
+ </Typography>
195
+ )}
196
+ {Object.keys(value).length > 0 && (
197
+ <>
198
+ {multiple && (
199
+ <span className='total-info'>
200
+ <Typography variant='small' noPadding>
201
+ {Object.keys(value).length} files - {formatBytes(totalSize)}
202
+ </Typography>
203
+ <span className='total-info--delete' onClick={deleteAll}>
204
+ <FaRegTrashCan />
205
+ </span>
206
+ </span>
207
+ )}
208
+ <section className='input-file--files'>
209
+ <section className='input-file--files--content'>{Object.values(value).map(renderFileToCard)}</section>
210
+ </section>
211
+ </>
212
+ )}
213
+ </section>
214
+ );
215
+ };
216
+
217
+ const InputFileController = ({ rules, ...props }: ControllerGeneratorProps<InputFileProps, InputFileValue>) => {
218
+ return <Controller Component={InputFile} defaultValue={{}} inputProps={props} rules={rules} />;
219
+ };
220
+
221
+ InputFile.Controller = InputFileController;