polpo 0.1.1 → 0.1.2

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 (121) hide show
  1. package/.storybook/theme.ts +2 -2
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/README.md +2 -5
  4. package/dist/chunk-CFYQBHH5.js +3 -0
  5. package/dist/chunk-CFYQBHH5.js.map +1 -0
  6. package/dist/chunk-MAWW6AA7.js +3 -0
  7. package/dist/chunk-MAWW6AA7.js.map +1 -0
  8. package/dist/get-modal-position-drle0OjP.d.cts +49 -0
  9. package/dist/get-modal-position-drle0OjP.d.ts +49 -0
  10. package/dist/helpers.cjs +1 -1
  11. package/dist/helpers.cjs.map +1 -1
  12. package/dist/helpers.d.cts +9 -2
  13. package/dist/helpers.d.ts +9 -2
  14. package/dist/helpers.js +1 -1
  15. package/dist/hooks.cjs +1 -1
  16. package/dist/hooks.cjs.map +1 -1
  17. package/dist/hooks.d.cts +59 -21
  18. package/dist/hooks.d.ts +59 -21
  19. package/dist/hooks.js +1 -1
  20. package/dist/ui.cjs +601 -389
  21. package/dist/ui.cjs.map +1 -1
  22. package/dist/ui.d.cts +97 -77
  23. package/dist/ui.d.ts +97 -77
  24. package/dist/ui.js +585 -373
  25. package/dist/ui.js.map +1 -1
  26. package/dist/use-modal-in-container-DiNW1PE_.d.cts +34 -0
  27. package/dist/use-modal-in-container-neGo-kMk.d.ts +34 -0
  28. package/package.json +2 -2
  29. package/src/components/buttons/button/button.stories.tsx +4 -4
  30. package/src/components/buttons/button/button.style.ts +10 -5
  31. package/src/components/buttons/button/button.tsx +7 -19
  32. package/src/components/cards/flip-card/flip-card.tsx +1 -1
  33. package/src/components/cursor/cursor.stories.tsx +35 -0
  34. package/src/components/cursor/cursor.style.ts +73 -0
  35. package/src/components/cursor/cursor.tsx +49 -0
  36. package/src/components/cursor/index.ts +1 -0
  37. package/src/components/form/checkbox/checkbox.stories.tsx +51 -0
  38. package/src/components/form/checkbox/checkbox.style.ts +73 -37
  39. package/src/components/form/checkbox/checkbox.tsx +38 -4
  40. package/src/components/form/field/field.stories.tsx +5 -1
  41. package/src/components/form/field/field.style.ts +12 -0
  42. package/src/components/form/field/field.tsx +3 -1
  43. package/src/components/form/field/field.types.ts +6 -0
  44. package/src/components/form/input-color/input-color.style.ts +5 -4
  45. package/src/components/form/input-color/input-color.tsx +41 -44
  46. package/src/components/form/radio/radio.stories.tsx +29 -5
  47. package/src/components/form/radio/radio.style.ts +45 -24
  48. package/src/components/form/radio/radio.tsx +22 -3
  49. package/src/components/form/select/options.tsx +119 -67
  50. package/src/components/form/select/select.stories.tsx +103 -42
  51. package/src/components/form/select/select.style.ts +10 -92
  52. package/src/components/form/select/select.tsx +19 -42
  53. package/src/components/form/select/select.types.ts +4 -21
  54. package/src/components/form/slider/slider.style.ts +2 -0
  55. package/src/components/icon/icons/social.tsx +17 -1
  56. package/src/components/index.ts +1 -0
  57. package/src/components/infinity-scroll/infinity-scroll.tsx +1 -1
  58. package/src/components/line/line.stories.tsx +3 -4
  59. package/src/components/modals/action-modal/action-modal.stories.tsx +58 -39
  60. package/src/components/modals/action-modal/action-modal.style.ts +13 -25
  61. package/src/components/modals/action-modal/action-modal.tsx +68 -70
  62. package/src/components/modals/aside-modal/aside-modal.stories.tsx +11 -15
  63. package/src/components/modals/aside-modal/aside-modal.style.ts +17 -37
  64. package/src/components/modals/aside-modal/aside-modal.tsx +41 -43
  65. package/src/components/modals/confirmation-modal/confirmation-modal.stories.tsx +21 -9
  66. package/src/components/modals/index.ts +2 -0
  67. package/src/components/modals/menu/index.ts +1 -0
  68. package/src/components/modals/menu/menu.stories.tsx +69 -0
  69. package/src/components/modals/menu/menu.style.ts +62 -0
  70. package/src/components/modals/menu/menu.tsx +142 -0
  71. package/src/components/modals/modal/backdrop.tsx +70 -0
  72. package/src/components/modals/modal/index.ts +1 -0
  73. package/src/components/modals/modal/modal.stories.tsx +325 -0
  74. package/src/components/modals/modal/modal.style.ts +62 -2
  75. package/src/components/modals/modal/modal.tsx +82 -123
  76. package/src/components/modals/portal/index.ts +1 -0
  77. package/src/components/modals/portal/portal.tsx +18 -0
  78. package/src/components/tabs/tabs-list.tsx +13 -10
  79. package/src/components/tabs/tabs.style.ts +48 -43
  80. package/src/components/tag/tag.stories.tsx +11 -12
  81. package/src/components/tag/tag.style.ts +9 -4
  82. package/src/components/tag/tag.tsx +2 -12
  83. package/src/components/tooltips/tooltip/tooltip.stories.tsx +5 -2
  84. package/src/components/tooltips/tooltip/tooltip.style.ts +37 -6
  85. package/src/components/tooltips/tooltip/tooltip.tsx +33 -19
  86. package/src/components/typography/typography.stories.tsx +3 -1
  87. package/src/components/typography/typography.tsx +21 -0
  88. package/src/contexts/theme-context/theme.animations.ts +91 -2
  89. package/src/contexts/theme-context/theme.defaults.ts +1 -1
  90. package/src/core/http-client.ts +49 -47
  91. package/src/core/variants/color.ts +3 -30
  92. package/src/core/variants/radius.ts +12 -41
  93. package/src/core/variants/size.ts +8 -33
  94. package/src/helpers/get-modal-position-relative-to-screen.ts +86 -0
  95. package/src/helpers/get-modal-position.ts +173 -28
  96. package/src/helpers/index.ts +1 -0
  97. package/src/hooks/index.ts +9 -3
  98. package/src/hooks/use-click-outside.ts +32 -0
  99. package/src/hooks/use-cookie.ts +124 -0
  100. package/src/hooks/use-dimensions.ts +11 -14
  101. package/src/hooks/use-dom-container.ts +32 -0
  102. package/src/hooks/use-event-listener.ts +4 -4
  103. package/src/hooks/use-geolocation.ts +63 -0
  104. package/src/hooks/use-in-view.ts +9 -11
  105. package/src/hooks/use-intersection-observer.ts +19 -0
  106. package/src/hooks/use-modal-in-container.ts +60 -52
  107. package/src/hooks/use-modal-transition.ts +54 -0
  108. package/src/hooks/use-modal.ts +21 -0
  109. package/src/hooks/use-mouse-position.ts +55 -7
  110. package/src/hooks/use-resize-observer.ts +18 -0
  111. package/src/stories/GettingStarted.mdx +2 -6
  112. package/svg/Name=npm, Category=social.svg +3 -0
  113. package/dist/chunk-M4KRSYE7.js +0 -3
  114. package/dist/chunk-M4KRSYE7.js.map +0 -1
  115. package/dist/chunk-U5XSMSKZ.js +0 -3
  116. package/dist/chunk-U5XSMSKZ.js.map +0 -1
  117. package/dist/get-modal-position-DPftPoU2.d.cts +0 -28
  118. package/dist/get-modal-position-DPftPoU2.d.ts +0 -28
  119. package/src/components/form/select/select-option.tsx +0 -84
  120. package/src/hooks/use-observer.ts +0 -18
  121. package/src/hooks/use-on-click-outside-ref.ts +0 -17
@@ -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
+ };
@@ -1,22 +1,19 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import { useState } from 'react';
2
2
 
3
- import { useEventListener } from './use-event-listener';
4
- import { useObserver } from './use-observer';
3
+ import { useResizeObserver } from './use-resize-observer';
5
4
 
6
5
  export const useDimensions = (ref: React.RefObject<HTMLElement>) => {
7
6
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
8
7
 
9
- const getSize = () => {
10
- setDimensions({
11
- width: ref.current?.offsetWidth ?? 0,
12
- height: ref.current?.offsetHeight ?? 0,
13
- });
14
- };
15
-
16
- useEventListener('resize', getSize);
17
- useObserver(ref, getSize);
18
-
19
- useEffect(getSize, [ref]);
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
+ });
20
17
 
21
18
  return dimensions;
22
19
  };
@@ -0,0 +1,32 @@
1
+ import { ForwardedRef, useEffect, useMemo } from 'react';
2
+
3
+ export const useDomContainer = (containerID: string, ref?: ForwardedRef<HTMLElement>) => {
4
+ useEffect(() => {
5
+ return () => {
6
+ const domContainer = document.getElementById(containerID);
7
+
8
+ if (domContainer?.parentNode === document.body) {
9
+ document.body.removeChild(domContainer);
10
+ }
11
+ };
12
+ }, [containerID, ref]);
13
+
14
+ return useMemo(() => {
15
+ let domContainer = document.getElementById(containerID);
16
+
17
+ if (!domContainer) {
18
+ domContainer = document.createElement('div');
19
+ domContainer.setAttribute('id', containerID);
20
+
21
+ if (typeof ref === 'function') {
22
+ ref(domContainer);
23
+ } else if (ref) {
24
+ ref.current = domContainer;
25
+ }
26
+
27
+ document.body.appendChild(domContainer);
28
+ }
29
+
30
+ return domContainer;
31
+ }, [containerID, ref]);
32
+ };
@@ -3,7 +3,7 @@ import { RefObject, useEffect, useLayoutEffect, useRef } from 'react';
3
3
  function useEventListener<EventName extends keyof MediaQueryListEventMap>(
4
4
  eventName: EventName,
5
5
  callback: (event: MediaQueryListEventMap[EventName]) => void,
6
- element: RefObject<MediaQueryList>,
6
+ element: RefObject<MediaQueryList> | undefined,
7
7
  options?: boolean | AddEventListenerOptions,
8
8
  ): void;
9
9
 
@@ -17,14 +17,14 @@ function useEventListener<EventName extends keyof WindowEventMap>(
17
17
  function useEventListener<EventName extends keyof HTMLElementEventMap, ElementRef extends HTMLElement = HTMLDivElement>(
18
18
  eventName: EventName,
19
19
  callback: (event: HTMLElementEventMap[EventName]) => void,
20
- element: RefObject<ElementRef>,
20
+ element: RefObject<ElementRef> | undefined,
21
21
  options?: boolean | AddEventListenerOptions,
22
22
  ): void;
23
23
 
24
24
  function useEventListener<EventName extends keyof DocumentEventMap>(
25
25
  eventName: EventName,
26
26
  callback: (event: DocumentEventMap[EventName]) => void,
27
- element: RefObject<Document>,
27
+ element: RefObject<Document> | undefined,
28
28
  options?: boolean | AddEventListenerOptions,
29
29
  ): void;
30
30
 
@@ -44,7 +44,7 @@ function useEventListener<
44
44
  | DocumentEventMap[DocumentEventName]
45
45
  | Event,
46
46
  ) => void,
47
- element?: RefObject<ElementRef>,
47
+ element?: RefObject<ElementRef> | undefined,
48
48
  options?: boolean | AddEventListenerOptions,
49
49
  ) {
50
50
  const callbackRef = useRef<EventListener>(callback);
@@ -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>(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
+ };
@@ -1,20 +1,18 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useRef, useState } from 'react';
2
+
3
+ import { useIntersectionObserver } from './use-intersection-observer';
2
4
 
3
5
  export const useInView = (initOptions: IntersectionObserverInit = {}) => {
4
6
  const [inView, setInView] = useState(false);
5
7
  const ref = useRef<Element>(null);
6
8
 
7
- useEffect(() => {
8
- const observer = new IntersectionObserver(([entry]) => {
9
+ useIntersectionObserver(
10
+ ref,
11
+ ([entry]) => {
9
12
  setInView(entry.isIntersecting);
10
- }, initOptions);
11
-
12
- ref.current && observer.observe(ref.current);
13
-
14
- return () => {
15
- observer.disconnect();
16
- };
17
- }, [initOptions]);
13
+ },
14
+ initOptions,
15
+ );
18
16
 
19
17
  return { ref, inView };
20
18
  };
@@ -0,0 +1,19 @@
1
+ import { RefObject, useEffect } from 'react';
2
+
3
+ export const useIntersectionObserver = <T extends Element>(
4
+ ref: RefObject<T> | Array<RefObject<T>>,
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
+ };
@@ -1,10 +1,16 @@
1
- import React, { RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
1
+ import { RefObject, useCallback, useLayoutEffect, useRef } from 'react';
2
2
 
3
+ import { useClickOutside } from './use-click-outside';
3
4
  import { useEventListener } from './use-event-listener';
4
- import { useObserver } from './use-observer';
5
- import { useOnClickOutsideRef } from './use-on-click-outside-ref';
5
+ import { useModalTransition } from './use-modal-transition';
6
+ import { useResizeObserver } from './use-resize-observer';
6
7
 
7
- import { getModalPosition, GetModalPositionParams, PositionObject } from '@polpo/helpers';
8
+ import {
9
+ getModalPositionRelativeToContainer,
10
+ getModalPositionRelativeToScreen,
11
+ PositionContainer,
12
+ PositionObject,
13
+ } from '@polpo/helpers';
8
14
 
9
15
  const convertDOMRectToPosition = (rect: DOMRectReadOnly): PositionObject => ({
10
16
  x: rect.x,
@@ -15,80 +21,82 @@ const convertDOMRectToPosition = (rect: DOMRectReadOnly): PositionObject => ({
15
21
  left: rect.left,
16
22
  });
17
23
 
18
- type useModalInContainerParams = Partial<
19
- Pick<GetModalPositionParams, 'position' | 'distancePercentage' | 'offset' | 'windowOffset'>
20
- > & {
24
+ export type UseModalInContainerParams<
25
+ Container extends HTMLElement = HTMLElement,
26
+ Modal extends HTMLElement = Container,
27
+ > = {
21
28
  closeOnClickOutside?: boolean;
29
+ transitionDuration?: number;
30
+ windowOffset?: number;
31
+ offset?: number;
32
+ position?: `${PositionContainer}`;
33
+ modalRef: RefObject<Modal>;
34
+ containerRef?: RefObject<Container>;
35
+ onClose?: () => void;
22
36
  };
23
37
 
24
- export const useModalInContainer = <Container extends HTMLElement, Modal extends HTMLElement = Container>({
38
+ export const useModalInContainer = <
39
+ Container extends HTMLElement = HTMLElement,
40
+ Modal extends HTMLElement = Container,
41
+ >({
25
42
  closeOnClickOutside = true,
26
- offset = 5,
27
- windowOffset = 10,
28
- position,
29
- distancePercentage = 50,
30
- }: useModalInContainerParams = {}) => {
31
- const [isVisible, setIsVisible] = useState<boolean>(false);
32
- const [modalStyle, setModalStyle] = useState<React.CSSProperties>({});
33
-
34
- const modalRef = useRef<Modal>(null);
35
- const containerRef = useRef<Container>(null);
36
-
37
- useEffect(() => {
38
- if (closeOnClickOutside) {
39
- document.documentElement.style.overflow = isVisible ? 'hidden' : 'auto';
40
- }
41
- }, [isVisible, closeOnClickOutside]);
42
-
43
- useOnClickOutsideRef<Modal>(modalRef, () => {
43
+ offset = 0,
44
+ windowOffset = 0,
45
+ position = PositionContainer.BOTTOM,
46
+ transitionDuration = 0,
47
+ modalRef,
48
+ containerRef,
49
+ onClose,
50
+ }: UseModalInContainerParams<Container, Modal>) => {
51
+ const containerTemporalRef = useRef<Container>(null);
52
+ const modalState = useModalTransition(transitionDuration, onClose);
53
+
54
+ const { isVisible, closeModal } = modalState;
55
+
56
+ useClickOutside<Modal>(modalRef, () => {
44
57
  if (isVisible && closeOnClickOutside) {
45
- setIsVisible(false);
58
+ closeModal();
46
59
  }
47
60
  });
48
61
 
49
62
  const getPosition = useCallback(
50
- (modalRef: RefObject<HTMLElement>) => {
63
+ (modalRef: RefObject<Modal>, containerRef: RefObject<Container>) => {
51
64
  const modal = modalRef.current?.getClientRects()[0];
52
65
  const container = containerRef.current?.getClientRects()[0];
53
66
 
54
- if (!container || !modal) {
55
- setModalStyle({});
56
-
67
+ if (!modal) {
57
68
  return;
58
69
  }
59
70
 
60
- const nextStyles = getModalPosition({
61
- c: convertDOMRectToPosition(container),
62
- m: convertDOMRectToPosition(modal),
63
- distancePercentage,
64
- windowOffset,
65
- position,
66
- offset,
71
+ const modalStyle: Record<string, string> = !container
72
+ ? getModalPositionRelativeToScreen({ position: position as PositionContainer, windowOffset })
73
+ : getModalPositionRelativeToContainer({
74
+ c: convertDOMRectToPosition(container),
75
+ m: convertDOMRectToPosition(modal),
76
+ offset,
77
+ windowOffset,
78
+ position: position as PositionContainer,
79
+ });
80
+
81
+ Object.keys(modalStyle).forEach(key => {
82
+ modalRef.current?.style.setProperty(key, modalStyle[key]);
67
83
  });
68
-
69
- setModalStyle(nextStyles);
70
84
  },
71
- [distancePercentage, windowOffset, position, offset],
85
+ [position, windowOffset, offset],
72
86
  );
73
87
 
74
88
  const callback = useCallback(() => {
75
89
  if (isVisible) {
76
- getPosition(modalRef);
90
+ getPosition(modalRef, containerRef ?? containerTemporalRef);
77
91
  }
78
- }, [getPosition, isVisible]);
92
+ }, [getPosition, isVisible, containerRef, modalRef]);
79
93
 
80
94
  useLayoutEffect(callback, [callback]);
81
95
 
82
- useObserver<Container>(containerRef, callback);
83
- useObserver<Modal>(modalRef, callback);
96
+ useResizeObserver<Container>(containerRef ?? containerTemporalRef, callback);
97
+ useResizeObserver<Modal>(modalRef, callback);
84
98
  useEventListener('resize', callback);
85
99
  useEventListener('scroll', callback, modalRef);
86
100
 
87
- return {
88
- isVisible,
89
- setIsVisible,
90
- modalStyle,
91
- containerRef,
92
- modalRef,
93
- };
101
+ return modalState;
94
102
  };
@@ -0,0 +1,54 @@
1
+ import React, { useCallback, useEffect, useMemo } from 'react';
2
+
3
+ export enum ModalState {
4
+ OPENING = 'OPENING',
5
+ OPEN = 'OPEN',
6
+ CLOSING = 'CLOSING',
7
+ CLOSED = 'CLOSED',
8
+ }
9
+
10
+ export const useModalTransition = (transitionDuration: number = 0, onClose: () => void = () => null) => {
11
+ const [modalState, setModalState] = React.useState<ModalState>(ModalState.CLOSED);
12
+
13
+ const isVisible = useMemo(() => {
14
+ return modalState !== ModalState.CLOSED;
15
+ }, [modalState]);
16
+
17
+ useEffect(() => {
18
+ document.documentElement.style.overflow = isVisible ? 'hidden' : 'auto';
19
+ }, [isVisible]);
20
+
21
+ const closeModal = useCallback(() => {
22
+ if ([ModalState.OPENING, ModalState.OPEN].includes(modalState)) {
23
+ if (transitionDuration > 0) {
24
+ setModalState(ModalState.CLOSING);
25
+ setTimeout(() => {
26
+ setModalState(ModalState.CLOSED);
27
+ onClose();
28
+ }, transitionDuration);
29
+ } else {
30
+ setModalState(ModalState.CLOSED);
31
+ }
32
+ }
33
+ }, [onClose, modalState, transitionDuration]);
34
+
35
+ const openModal = useCallback(() => {
36
+ if ([ModalState.CLOSING, ModalState.CLOSED].includes(modalState)) {
37
+ if (transitionDuration > 0) {
38
+ setModalState(ModalState.OPENING);
39
+ setTimeout(() => {
40
+ setModalState(ModalState.OPEN);
41
+ }, transitionDuration);
42
+ } else {
43
+ setModalState(ModalState.OPEN);
44
+ }
45
+ }
46
+ }, [modalState, transitionDuration]);
47
+
48
+ return {
49
+ isVisible,
50
+ closeModal,
51
+ openModal,
52
+ modalState,
53
+ };
54
+ };
@@ -0,0 +1,21 @@
1
+ import { useRef, useState } from 'react';
2
+
3
+ export const useModal = <T extends HTMLElement>() => {
4
+ const containerRef = useRef<T>(null);
5
+ const [isOpen, setIsOpen] = useState(false);
6
+
7
+ const openModal = () => {
8
+ setIsOpen(true);
9
+ };
10
+
11
+ const closeModal = () => {
12
+ setIsOpen(false);
13
+ };
14
+
15
+ return {
16
+ containerRef,
17
+ isOpen,
18
+ openModal,
19
+ closeModal,
20
+ };
21
+ };
@@ -1,16 +1,64 @@
1
- import React, { useState } from 'react';
1
+ import React, { useRef, useState } from 'react';
2
2
 
3
3
  import { useEventListener } from './use-event-listener';
4
4
 
5
- export const useMousePosition = () => {
6
- const [{ x, y }, setPosition] = useState({ x: 0, y: 0 });
5
+ type MousePosition = {
6
+ x: null | number;
7
+ y: null | number;
8
+ elementX: number | null;
9
+ elementY: number | null;
10
+ elementPositionX: number | null;
11
+ elementPositionY: number | null;
12
+ };
13
+
14
+ const getMousePosition = (domRect: DOMRect, e: MouseEvent) => {
15
+ const { left, top } = domRect;
16
+ const containerPositionX = left + window.scrollX;
17
+ const containerPositionY = top + window.scrollY;
18
+ const containerX = e.pageX - containerPositionX;
19
+ const containerY = e.pageY - containerPositionY;
7
20
 
8
- const mouseMove = (e: React.MouseEvent) => {
21
+ return {
22
+ x: e.pageX,
23
+ y: e.pageY,
24
+ elementX: containerX,
25
+ elementY: containerY,
26
+ elementPositionX: containerPositionX,
27
+ elementPositionY: containerPositionY,
28
+ };
29
+ };
30
+
31
+ export const useMousePosition = (containerRef?: React.RefObject<HTMLElement | SVGElement | null>) => {
32
+ const ref = useRef<HTMLElement>(null);
33
+ const [position, setPosition] = useState<MousePosition>({
34
+ x: null,
35
+ y: null,
36
+ elementX: null,
37
+ elementY: null,
38
+ elementPositionX: null,
39
+ elementPositionY: null,
40
+ });
41
+
42
+ const mouseMove = (e: MouseEvent) => {
9
43
  const { clientX, clientY } = e;
10
- setPosition({ x: clientX, y: clientY });
44
+ setPosition(prev => ({ ...prev, x: clientX, y: clientY }));
45
+
46
+ if (containerRef?.current instanceof Element) {
47
+ const newState = getMousePosition(containerRef.current.getBoundingClientRect(), e);
48
+ setPosition(prev => ({
49
+ ...prev,
50
+ ...newState,
51
+ }));
52
+ } else if (ref.current instanceof Element) {
53
+ const newState = getMousePosition(ref.current.getBoundingClientRect(), e);
54
+ setPosition(prev => ({
55
+ ...prev,
56
+ ...newState,
57
+ }));
58
+ }
11
59
  };
12
60
 
13
- useEventListener('mousemove', mouseMove as unknown as EventListener);
61
+ useEventListener('mousemove', mouseMove);
14
62
 
15
- return [x, y];
63
+ return { ...position, ref };
16
64
  };
@@ -0,0 +1,18 @@
1
+ import { RefObject, useEffect } from 'react';
2
+
3
+ export const useResizeObserver = <T extends Element>(
4
+ ref: RefObject<T> | Array<RefObject<T>>,
5
+ callback: ResizeObserverCallback,
6
+ ) => {
7
+ useEffect(() => {
8
+ const refs = Array.isArray(ref) ? ref : [ref];
9
+
10
+ const observer = new ResizeObserver(callback);
11
+
12
+ refs.forEach(r => r.current && observer.observe(r.current));
13
+
14
+ return () => {
15
+ observer.disconnect();
16
+ };
17
+ }, [ref, callback]);
18
+ };
@@ -4,13 +4,9 @@ import { Meta } from "@storybook/blocks";
4
4
 
5
5
  # Polpo UI
6
6
 
7
- [![npm version](https://badge.fury.io/js/polpo.svg)](https://www.npmjs.com/package/polpo)
8
-
9
-
10
- ## Description
11
-
12
- `polpo` is a lightweight and customizable UI component library designed for React applications. This library provides a set of reusable components to speed up development and maintain a consistent user interface across your projects.
7
+ A lightweight and customizable UI component library designed for React applications. This library provides a set of reusable components to speed up development and maintain a consistent user interface across your projects.
13
8
 
9
+ [![npm version](https://badge.fury.io/js/polpo.svg)](https://www.npmjs.com/package/polpo)
14
10
 
15
11
  ## Features
16
12
 
@@ -0,0 +1,3 @@
1
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0 9.93333C0 9.41787 0.421379 9 0.941176 9H31.0588C31.5786 9 32 9.41787 32 9.93333V19.3704C32 19.8858 31.5786 20.3037 31.0588 20.3037H15.5722V22.0667C15.5722 22.5821 15.1508 23 14.631 23H9.15508C8.63528 23 8.2139 22.5821 8.2139 22.0667V20.3037H0.941176C0.421379 20.3037 0 19.8858 0 19.3704V9.93333ZM5.9893 18.437V13.9778C5.9893 13.4623 5.56793 13.0444 5.04813 13.0444C4.52833 13.0444 4.10695 13.4623 4.10695 13.9778V18.437H1.88235V10.8667H8.2139V18.437H5.9893ZM10.0963 10.8667L10.0963 21.1333H13.6898V19.3704C13.6898 18.8549 14.1112 18.437 14.631 18.437H17.7968V10.8667H10.0963ZM19.6791 10.8667V18.437H21.9037V13.9778C21.9037 13.4623 22.3251 13.0444 22.8449 13.0444C23.3647 13.0444 23.7861 13.4623 23.7861 13.9778V18.437H26.0107V13.9778C26.0107 13.4623 26.4321 13.0444 26.9519 13.0444C27.4717 13.0444 27.893 13.4623 27.893 13.9778V18.437H30.1176V10.8667H19.6791ZM14.631 13.0444C15.1508 13.0444 15.5722 13.4623 15.5722 13.9778V15.3259C15.5722 15.8414 15.1508 16.2593 14.631 16.2593C14.1112 16.2593 13.6898 15.8414 13.6898 15.3259V13.9778C13.6898 13.4623 14.1112 13.0444 14.631 13.0444Z" fill="white"/>
3
+ </svg>