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,32 @@
1
+ import { RefObject } from 'react';
2
+
3
+ import { useEventListener } from './use-event-listener';
4
+
5
+ const checkIsOutside = (ref: RefObject<HTMLElement | null>, target: Node) => {
6
+ return ref.current && !ref.current.contains(target);
7
+ };
8
+
9
+ export const useClickOutside = <T extends HTMLElement>(
10
+ ref: RefObject<T | null> | Array<RefObject<T | null>>,
11
+ callback: () => void,
12
+ ) => {
13
+ useEventListener('keydown', e => {
14
+ if (e.key === 'Escape') {
15
+ callback();
16
+ }
17
+ });
18
+
19
+ useEventListener('mousedown', event => {
20
+ const target = event.target as Node;
21
+
22
+ if (!target?.isConnected) {
23
+ return;
24
+ }
25
+
26
+ const isOutside = (Array.isArray(ref) ? ref : [ref]).every(r => checkIsOutside(r, target));
27
+
28
+ if (isOutside) {
29
+ callback();
30
+ }
31
+ });
32
+ };
@@ -0,0 +1,3 @@
1
+ import { useState } from 'react';
2
+
3
+ export const useConstant = <T>(initializer: T | (() => T)): T => useState(initializer)[0];
@@ -0,0 +1,124 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export type CookieOptions = {
4
+ days?: number;
5
+ expires?: Date;
6
+ maxAge?: number;
7
+ path?: string;
8
+ domain?: string;
9
+ secure?: boolean;
10
+ sameSite?: 'strict' | 'lax' | 'none';
11
+ };
12
+
13
+ const generateCookieAttributes = (options: CookieOptions): string => {
14
+ const parts: Array<string> = [];
15
+
16
+ if (options.expires instanceof Date) {
17
+ parts.push(`expires=${options.expires.toUTCString()}`);
18
+ } else if (typeof options.days === 'number') {
19
+ const date = new Date();
20
+ date.setTime(date.getTime() + options.days * 24 * 60 * 60 * 1000);
21
+ parts.push(`expires=${date.toUTCString()}`);
22
+ }
23
+
24
+ if (typeof options.maxAge === 'number') {
25
+ parts.push(`max-age=${options.maxAge}`);
26
+ }
27
+
28
+ parts.push(`path=${options.path ?? '/'}`);
29
+
30
+ if (options.domain) {
31
+ parts.push(`domain=${options.domain}`);
32
+ }
33
+
34
+ if (options.secure) {
35
+ parts.push('secure');
36
+ }
37
+
38
+ if (options.sameSite) {
39
+ parts.push(`SameSite=${options.sameSite}`);
40
+ }
41
+
42
+ return parts.length > 0 ? `; ${parts.join('; ')}` : '';
43
+ };
44
+
45
+ const setCookie = (name: string, value: string, options: CookieOptions = {}): void => {
46
+ if (typeof document === 'undefined') {
47
+ return;
48
+ }
49
+
50
+ const encodedValue = encodeURIComponent(value);
51
+ const attributes = generateCookieAttributes(options);
52
+ document.cookie = `${name}=${encodedValue}${attributes}`;
53
+ };
54
+
55
+ const getCookie = (name: string): string | null => {
56
+ if (typeof document === 'undefined') {
57
+ return null;
58
+ }
59
+
60
+ const nameEq = `${name}=`;
61
+ const cookies = document.cookie ? document.cookie.split(';') : [];
62
+
63
+ for (const cookie of cookies) {
64
+ const cookieTrimmed = cookie.trim();
65
+
66
+ if (cookieTrimmed.indexOf(nameEq) === 0) {
67
+ return decodeURIComponent(cookieTrimmed.substring(nameEq.length));
68
+ }
69
+ }
70
+
71
+ return null;
72
+ };
73
+
74
+ const deleteCookie = (name: string, options: CookieOptions = {}) => {
75
+ if (typeof document === 'undefined') {
76
+ return;
77
+ }
78
+
79
+ // Ensure the same path/domain so that the deletion matches the existing cookie.
80
+ setCookie(name, '', {
81
+ ...options,
82
+ days: -1,
83
+ });
84
+ };
85
+
86
+ type UpdateCookie<T> = (value: T, overrideOptions?: CookieOptions) => void;
87
+ type RemoveCookie = () => void;
88
+
89
+ export const useCookie = <T = unknown>(
90
+ cookieName: string,
91
+ defaultValue?: T,
92
+ options: CookieOptions = {},
93
+ ): [T, UpdateCookie<T>, RemoveCookie] => {
94
+ const [cookieValue, setCookieValue] = useState<T>(() => {
95
+ const rawCookie = getCookie(cookieName);
96
+
97
+ if (rawCookie !== null) {
98
+ try {
99
+ return JSON.parse(rawCookie) as T;
100
+ } catch {
101
+ return defaultValue as T;
102
+ }
103
+ }
104
+
105
+ return defaultValue as T;
106
+ });
107
+
108
+ const updateCookie = useCallback<UpdateCookie<T>>(
109
+ (value: T, overrideOptions: CookieOptions = {}) => {
110
+ const mergedOptions = { ...options, ...overrideOptions };
111
+ const stringValue = JSON.stringify(value);
112
+ setCookie(cookieName, stringValue, mergedOptions);
113
+ setCookieValue(value);
114
+ },
115
+ [cookieName, options],
116
+ );
117
+
118
+ const removeCookie = useCallback<RemoveCookie>(() => {
119
+ deleteCookie(cookieName, options);
120
+ setCookieValue(defaultValue as T);
121
+ }, [cookieName, defaultValue, options]);
122
+
123
+ return [cookieValue, updateCookie, removeCookie];
124
+ };
@@ -0,0 +1,13 @@
1
+ import { useState } from 'react';
2
+
3
+ import { useDebounce } from './use-debounce';
4
+
5
+ export const useDebounceState = <T>(
6
+ defaultValue: T,
7
+ delay: number = 500,
8
+ ): [T, T, React.Dispatch<React.SetStateAction<T>>] => {
9
+ const [value, setValue] = useState<T>(defaultValue);
10
+ const debouncedValue = useDebounce(value, delay);
11
+
12
+ return [value, debouncedValue, setValue];
13
+ };
@@ -0,0 +1,15 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useDebounce = <T>(value: T, delay: number = 500): T => {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
8
+
9
+ return () => {
10
+ clearTimeout(timer);
11
+ };
12
+ }, [value, delay]);
13
+
14
+ return debouncedValue;
15
+ };
@@ -0,0 +1,19 @@
1
+ import { useState } from 'react';
2
+
3
+ import { useResizeObserver } from './use-resize-observer';
4
+
5
+ export const useDimensions = (ref: React.RefObject<HTMLElement>) => {
6
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
7
+
8
+ useResizeObserver(ref, ([entry]) => {
9
+ if ((entry?.borderBoxSize ?? [])[0]) {
10
+ const { inlineSize: width, blockSize: height } = entry.borderBoxSize[0];
11
+ setDimensions({ width, height });
12
+ } else if (entry.contentRect) {
13
+ const { width, height } = entry.contentRect;
14
+ setDimensions({ width, height });
15
+ }
16
+ });
17
+
18
+ return dimensions;
19
+ };
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const createContainer = (containerID: string) => {
4
+ let domContainer = document.getElementById(containerID);
5
+
6
+ if (domContainer === null) {
7
+ domContainer = document.createElement('div');
8
+ domContainer.setAttribute('id', containerID);
9
+ document.body.appendChild(domContainer);
10
+ }
11
+
12
+ return domContainer;
13
+ };
14
+
15
+ export const useDomContainer = (containerID: string) => {
16
+ const [container, setContainer] = useState<HTMLElement | null>(() => createContainer(containerID));
17
+
18
+ useEffect(() => {
19
+ const domContainer = document.getElementById(containerID);
20
+
21
+ if (container === null || domContainer === null) {
22
+ const domContainer = createContainer(containerID);
23
+
24
+ setContainer(domContainer);
25
+ }
26
+
27
+ return () => {
28
+ if (container && container.parentNode && process.env.NODE_ENV === 'production') {
29
+ document.body.removeChild(container);
30
+ }
31
+ };
32
+ }, [container, containerID]);
33
+
34
+ return container;
35
+ };
@@ -0,0 +1,71 @@
1
+ import { RefObject, useEffect, useLayoutEffect, useRef } from 'react';
2
+
3
+ function useEventListener<EventName extends keyof MediaQueryListEventMap>(
4
+ eventName: EventName,
5
+ callback: (event: MediaQueryListEventMap[EventName]) => void,
6
+ element: RefObject<MediaQueryList | null> | undefined,
7
+ options?: boolean | AddEventListenerOptions,
8
+ ): void;
9
+
10
+ function useEventListener<EventName extends keyof WindowEventMap>(
11
+ eventName: EventName,
12
+ callback: (event: WindowEventMap[EventName]) => void,
13
+ element?: undefined,
14
+ options?: boolean | AddEventListenerOptions,
15
+ ): void;
16
+
17
+ function useEventListener<EventName extends keyof HTMLElementEventMap, ElementRef extends HTMLElement = HTMLDivElement>(
18
+ eventName: EventName,
19
+ callback: (event: HTMLElementEventMap[EventName]) => void,
20
+ element: RefObject<ElementRef | null> | undefined,
21
+ options?: boolean | AddEventListenerOptions,
22
+ ): void;
23
+
24
+ function useEventListener<EventName extends keyof DocumentEventMap>(
25
+ eventName: EventName,
26
+ callback: (event: DocumentEventMap[EventName]) => void,
27
+ element: RefObject<Document | null> | undefined,
28
+ options?: boolean | AddEventListenerOptions,
29
+ ): void;
30
+
31
+ function useEventListener<
32
+ MediaQueryEventName extends keyof MediaQueryListEventMap,
33
+ WindowEventName extends keyof WindowEventMap,
34
+ ElementEventName extends keyof HTMLElementEventMap,
35
+ DocumentEventName extends keyof DocumentEventMap,
36
+ ElementRef extends HTMLElement | MediaQueryList | Document | Window | void = void,
37
+ >(
38
+ eventName: WindowEventName | DocumentEventName | ElementEventName | MediaQueryEventName,
39
+ callback: (
40
+ event:
41
+ | MediaQueryListEventMap[MediaQueryEventName]
42
+ | WindowEventMap[WindowEventName]
43
+ | HTMLElementEventMap[ElementEventName]
44
+ | DocumentEventMap[DocumentEventName]
45
+ | Event,
46
+ ) => void,
47
+ element?: RefObject<ElementRef | null> | undefined,
48
+ options?: boolean | AddEventListenerOptions,
49
+ ) {
50
+ const callbackRef = useRef<EventListener>(callback);
51
+
52
+ useEffect(() => {
53
+ callbackRef.current = callback;
54
+ }, [callback]);
55
+
56
+ useLayoutEffect(() => {
57
+ const targetElement = element?.current ?? window;
58
+
59
+ if (!(targetElement && targetElement.addEventListener)) return;
60
+
61
+ const listener: typeof callback = event => callbackRef.current(event);
62
+
63
+ targetElement.addEventListener(eventName, listener, options);
64
+
65
+ return () => {
66
+ targetElement.removeEventListener(eventName, listener, options);
67
+ };
68
+ }, [eventName, element, options]);
69
+ }
70
+
71
+ export { useEventListener };
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { formatBytes } from '../helpers';
4
+
5
+ export enum FileTypeEnum {
6
+ PNG = 'image/png',
7
+ PDF = 'application/pdf',
8
+ }
9
+
10
+ export type FileResolvedT = {
11
+ name: string;
12
+ size: number;
13
+ formatSize: string;
14
+ type: FileTypeEnum;
15
+ url: FileReader['result'];
16
+ };
17
+
18
+ export const useFileReader = (inputFiles: Array<File>) => {
19
+ const [files, setFiles] = useState<Array<FileResolvedT>>([]);
20
+
21
+ useEffect(() => {
22
+ const fileReaders: Array<FileReader> = [];
23
+ let isCancel = false;
24
+
25
+ if (!Array.isArray(inputFiles) || inputFiles.length === 0) {
26
+ setFiles([]);
27
+ } else {
28
+ const promises = inputFiles.map(file => {
29
+ return new Promise<FileResolvedT>((resolve, reject) => {
30
+ const fileReader = new FileReader();
31
+ fileReaders.push(fileReader);
32
+ fileReader.onload = (e: ProgressEvent<FileReader>) => {
33
+ if (e.target?.result) {
34
+ resolve({
35
+ name: file.name,
36
+ size: file.size,
37
+ formatSize: formatBytes(file.size),
38
+ type: file.type as FileTypeEnum,
39
+ url: e.target.result,
40
+ });
41
+ }
42
+ };
43
+ fileReader.onabort = () => {
44
+ reject(new Error('File reading aborted'));
45
+ };
46
+ fileReader.onerror = () => {
47
+ reject(new Error('Failed to read file'));
48
+ };
49
+ fileReader.readAsDataURL(file);
50
+ });
51
+ });
52
+
53
+ Promise.all(promises).then(files => {
54
+ if (!isCancel) {
55
+ setFiles(files);
56
+ }
57
+ });
58
+ }
59
+
60
+ return () => {
61
+ isCancel = true;
62
+ fileReaders.forEach(fileReader => {
63
+ fileReader.abort();
64
+ });
65
+ };
66
+ }, [inputFiles]);
67
+
68
+ return files;
69
+ };
@@ -0,0 +1,63 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ type GeolocationData = GeolocationPosition['coords'] & {
4
+ timestamp: GeolocationPosition['timestamp'];
5
+ };
6
+
7
+ interface GeolocationError {
8
+ code: number;
9
+ message: string;
10
+ }
11
+
12
+ type UseGeolocationReturn = {
13
+ data: GeolocationData | null;
14
+ error: GeolocationError | null;
15
+ isLoading: boolean;
16
+ };
17
+
18
+ export const useGeolocation = (): UseGeolocationReturn => {
19
+ const [data, setData] = useState<GeolocationData | null>(null);
20
+ const [error, setError] = useState<GeolocationError | null>(null);
21
+ const [isLoading, setIsLoading] = useState<boolean>(true);
22
+ const watchIdRef = useRef<number>(null);
23
+
24
+ useEffect(() => {
25
+ if (!navigator.geolocation) {
26
+ setError({
27
+ code: 0,
28
+ message: 'Geolocation is not supported by your browser.',
29
+ });
30
+ setIsLoading(false);
31
+
32
+ return;
33
+ }
34
+
35
+ const handleSuccess = (position: GeolocationPosition) => {
36
+ setData({
37
+ ...position.coords,
38
+ timestamp: position.timestamp,
39
+ });
40
+ setIsLoading(false);
41
+ };
42
+
43
+ const handleError = (geolocationError: GeolocationPositionError) => {
44
+ setError({
45
+ code: geolocationError.code,
46
+ message: geolocationError.message,
47
+ });
48
+ setIsLoading(false);
49
+ };
50
+
51
+ navigator.geolocation.getCurrentPosition(handleSuccess, handleError);
52
+
53
+ watchIdRef.current = navigator.geolocation.watchPosition(handleSuccess, handleError);
54
+
55
+ return () => {
56
+ if (watchIdRef.current !== null) {
57
+ navigator.geolocation.clearWatch(watchIdRef.current);
58
+ }
59
+ };
60
+ }, []);
61
+
62
+ return { data, error, isLoading };
63
+ };
@@ -0,0 +1,17 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ import { useEventListener } from './use-event-listener';
4
+
5
+ export const useHover = () => {
6
+ const [isHovered, setIsHovered] = useState(false);
7
+ const ref = useRef<HTMLElement>(null);
8
+
9
+ const handleMouseEnter = useCallback(() => setIsHovered(true), []);
10
+
11
+ const handleMouseLeave = useCallback(() => setIsHovered(false), []);
12
+
13
+ useEventListener('mouseenter', handleMouseEnter, ref);
14
+ useEventListener('mouseleave', handleMouseLeave, ref);
15
+
16
+ return isHovered;
17
+ };
@@ -0,0 +1,18 @@
1
+ import { useRef, useState } from 'react';
2
+
3
+ import { useIntersectionObserver } from './use-intersection-observer';
4
+
5
+ export const useInView = (initOptions: IntersectionObserverInit = {}) => {
6
+ const [inView, setInView] = useState(false);
7
+ const ref = useRef<HTMLElement>(null);
8
+
9
+ useIntersectionObserver(
10
+ ref,
11
+ ([entry]) => {
12
+ setInView(entry.isIntersecting);
13
+ },
14
+ initOptions,
15
+ );
16
+
17
+ return { ref, inView };
18
+ };
@@ -0,0 +1,52 @@
1
+ import React, { useCallback, useState } from 'react';
2
+
3
+ type InputTypes = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
4
+
5
+ type UseInputHandlersProps<T extends InputTypes> = {
6
+ onBlur?: (e: React.FocusEvent<T>) => void;
7
+ onFocus?: (e: React.FocusEvent<T>) => void;
8
+ onChange?: (e: React.ChangeEvent<T>) => void;
9
+ };
10
+
11
+ export const useInputHandlers = <T extends InputTypes>({
12
+ onBlur,
13
+ onFocus,
14
+ onChange,
15
+ }: UseInputHandlersProps<T> = {}) => {
16
+ const [isFocus, setIsFocus] = useState(false);
17
+
18
+ const handleFocus = useCallback(
19
+ (e: React.FocusEvent<T>) => {
20
+ setIsFocus(true);
21
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
22
+ onFocus && onFocus(e);
23
+ },
24
+ [onFocus],
25
+ );
26
+
27
+ const handleBlur = useCallback(
28
+ (e: React.FocusEvent<T>) => {
29
+ setIsFocus(false);
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
31
+ onBlur && onBlur(e);
32
+ },
33
+ [onBlur],
34
+ );
35
+
36
+ const handleChange = useCallback(
37
+ (e: React.ChangeEvent<T>) => {
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
39
+ onChange && onChange(e);
40
+ },
41
+ [onChange],
42
+ );
43
+
44
+ return {
45
+ isFocus,
46
+ handlers: {
47
+ onFocus: handleFocus,
48
+ onBlur: handleBlur,
49
+ onChange: handleChange,
50
+ },
51
+ };
52
+ };
@@ -0,0 +1,19 @@
1
+ import { RefObject, useEffect } from 'react';
2
+
3
+ export const useIntersectionObserver = <T extends Element>(
4
+ ref: RefObject<T | null> | Array<RefObject<T | null>>,
5
+ callback: IntersectionObserverCallback,
6
+ initOptions: IntersectionObserverInit = {},
7
+ ) => {
8
+ useEffect(() => {
9
+ const refs = Array.isArray(ref) ? ref : [ref];
10
+
11
+ const observer = new IntersectionObserver(callback, initOptions);
12
+
13
+ refs.forEach(r => r.current && observer.observe(r.current));
14
+
15
+ return () => {
16
+ observer.disconnect();
17
+ };
18
+ }, [callback, initOptions, ref]);
19
+ };
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useMediaQuery = (query: string): boolean => {
4
+ const [matches, setMatches] = useState(false);
5
+
6
+ useEffect(() => {
7
+ const media = window.matchMedia(query);
8
+
9
+ if (media.matches !== matches) {
10
+ setMatches(media.matches);
11
+ }
12
+
13
+ const listener = () => {
14
+ setMatches(media.matches);
15
+ };
16
+
17
+ media.addEventListener('change', listener);
18
+
19
+ return () => {
20
+ media.removeEventListener('change', listener);
21
+ };
22
+ }, [matches, query]);
23
+
24
+ return matches;
25
+ };
@@ -0,0 +1,85 @@
1
+ import { RefObject, useCallback, useLayoutEffect, useRef } from 'react';
2
+
3
+ import {
4
+ getModalPositionRelativeToContainer,
5
+ getModalPositionRelativeToScreen,
6
+ PositionContainer,
7
+ PositionObject,
8
+ } from '../helpers';
9
+
10
+ import { useEventListener } from './use-event-listener';
11
+ import { useResizeObserver } from './use-resize-observer';
12
+
13
+ const convertDOMRectToPosition = (rect: DOMRectReadOnly): PositionObject => ({
14
+ x: rect.x,
15
+ y: rect.y,
16
+ w: rect.width,
17
+ h: rect.height,
18
+ top: rect.top,
19
+ left: rect.left,
20
+ });
21
+
22
+ export type UseModalInContainerParams<
23
+ Container extends HTMLElement = HTMLElement,
24
+ Modal extends HTMLElement = Container,
25
+ > = {
26
+ windowOffset?: number;
27
+ offset?: number;
28
+ position?: `${PositionContainer}`;
29
+ modalRef: RefObject<Modal | null>;
30
+ containerRef?: RefObject<Container | null>;
31
+ isOpen: boolean;
32
+ };
33
+
34
+ export const useModalInContainer = <
35
+ Container extends HTMLElement = HTMLElement,
36
+ Modal extends HTMLElement = Container,
37
+ >({
38
+ offset = 0,
39
+ windowOffset = 0,
40
+ position = PositionContainer.BOTTOM,
41
+ modalRef,
42
+ containerRef,
43
+ isOpen,
44
+ }: UseModalInContainerParams<Container, Modal>) => {
45
+ const containerTemporalRef = useRef<Container>(null);
46
+
47
+ const getPosition = useCallback(
48
+ (modalRef: RefObject<Modal | null>, containerRef: RefObject<Container | null>) => {
49
+ const modal = modalRef.current?.getClientRects()[0];
50
+ const container = containerRef.current?.getClientRects()[0];
51
+
52
+ if (!modal) {
53
+ return;
54
+ }
55
+
56
+ const modalStyle: Record<string, string> = !container
57
+ ? getModalPositionRelativeToScreen({ position: position as PositionContainer, windowOffset })
58
+ : getModalPositionRelativeToContainer({
59
+ c: convertDOMRectToPosition(container),
60
+ m: convertDOMRectToPosition(modal),
61
+ offset,
62
+ windowOffset,
63
+ position: position as PositionContainer,
64
+ });
65
+
66
+ Object.keys(modalStyle).forEach(key => {
67
+ modalRef.current?.style.setProperty(key, modalStyle[key]);
68
+ });
69
+ },
70
+ [position, windowOffset, offset],
71
+ );
72
+
73
+ const callback = useCallback(() => {
74
+ if (isOpen) {
75
+ getPosition(modalRef, containerRef ?? containerTemporalRef);
76
+ }
77
+ }, [getPosition, isOpen, containerRef, modalRef]);
78
+
79
+ useLayoutEffect(callback, [callback]);
80
+
81
+ useResizeObserver<Container>(containerRef ?? containerTemporalRef, callback);
82
+ useResizeObserver<Modal>(modalRef, callback);
83
+ useEventListener('resize', callback);
84
+ useEventListener('scroll', callback, modalRef);
85
+ };