@ilokesto/utilinent 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,13 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
2
  import { createElement, forwardRef } from "react";
2
3
  import { htmlTags } from "../constants/htmlTags";
3
4
  function BaseFor({ each, children, fallback = null, }) {
4
- return each && each.length > 0 ? each.map(children) : fallback;
5
+ return _jsx(_Fragment, { children: each && each.length > 0 ? each.map(children) : fallback });
5
6
  }
6
7
  const renderForTag = (tag) =>
7
8
  // forward ref so consumers can attach a ref to the underlying DOM element
8
9
  forwardRef(({ each, children, fallback = null, ...props }, ref) => {
9
- if (!each || each.length === 0)
10
- return fallback;
11
- const content = each.map(children);
10
+ const content = each && each.length > 0 ? each.map(children) : fallback;
12
11
  return createElement(tag, { ...props, ref }, content);
13
12
  });
14
13
  const tagEntries = htmlTags.reduce((acc, tag) => {
@@ -1,2 +1,4 @@
1
1
  import type { ObserverProps } from "../types";
2
- export declare function Observer({ children, fallback, threshold, rootMargin, triggerOnce: freezeOnceVisible, onIntersect: onChange, }: ObserverProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare const Observer: import("react").ForwardRefExoticComponent<ObserverProps & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
3
+ ref?: ((instance: HTMLDivElement | null) => void | import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof import("react").DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | import("react").RefObject<HTMLDivElement> | null | undefined;
4
+ }, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,21 +1,29 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback } from "react";
2
3
  import { useIntersectionObserver } from "../hooks/useIntersectionObserver";
3
4
  import { Show } from "./Show";
4
- export function Observer({ children, fallback = null, threshold = 0, rootMargin = "0px", triggerOnce: freezeOnceVisible = false, onIntersect: onChange, }) {
5
- const { ref, isIntersecting } = useIntersectionObserver({
5
+ export const Observer = forwardRef(function Observer({ children, fallback = null, threshold = 0, rootMargin = "0px", triggerOnce: freezeOnceVisible = false, onIntersect: onChange, style, ...props }, forwardedRef) {
6
+ const { ref: observerRef, isIntersecting } = useIntersectionObserver({
6
7
  threshold,
7
8
  rootMargin,
8
9
  freezeOnceVisible,
9
10
  onChange,
10
11
  });
11
- return (_jsx(Show.div, { ref: ref, style:
12
- // fallback이 없고 isIntersecting이 false인 경우
13
- !fallback && !isIntersecting
14
- ? {
15
- minHeight: "1px",
16
- minWidth: "1px",
17
- flexShrink: 0, // flex 컨테이너에서 축소되지 않도록
18
- display: "block", // inline 요소가 되지 않도록
19
- }
20
- : undefined, when: isIntersecting, fallback: fallback, children: typeof children === "function" ? children(isIntersecting) : children }));
21
- }
12
+ const mergedRef = useCallback((node) => {
13
+ // Set observer ref
14
+ observerRef(node);
15
+ // Handle forwarded ref
16
+ if (typeof forwardedRef === "function") {
17
+ forwardedRef(node);
18
+ }
19
+ else if (forwardedRef) {
20
+ forwardedRef.current = node;
21
+ }
22
+ }, [observerRef, forwardedRef]);
23
+ return (_jsx(Show.div, { ref: mergedRef, when: isIntersecting, fallback: fallback, style: {
24
+ ...style,
25
+ minHeight: style?.minHeight ?? "1px",
26
+ minWidth: style?.minWidth ?? "1px",
27
+ display: style?.display ?? "block",
28
+ }, ...props, children: typeof children === "function" ? children(isIntersecting) : children }));
29
+ });
@@ -1,19 +1,19 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createElement, forwardRef } from "react";
3
3
  import { htmlTags } from "../constants/htmlTags";
4
+ import { For } from "./For";
4
5
  function BaseRepeat({ times, children, fallback = null }) {
5
6
  if (!times || times <= 0 || !Number.isInteger(times)) {
6
7
  return fallback ?? null;
7
8
  }
8
- return _jsx(_Fragment, { children: Array.from({ length: times }, (_, i) => children(i)) });
9
+ return _jsx(For, { each: Array.from({ length: times }, (_, i) => i), children: children });
9
10
  }
10
11
  const renderForTag = (tag) =>
11
12
  // forward ref so consumers can attach a ref to the underlying DOM element
12
13
  forwardRef(({ times, children, fallback = null, ...props }, ref) => {
13
- if (!times || times <= 0 || !Number.isInteger(times)) {
14
- return fallback ?? null;
15
- }
16
- const content = Array.from({ length: times }, (_, i) => children(i));
14
+ const content = times && times > 0 && Number.isInteger(times)
15
+ ? Array.from({ length: times }, (_, i) => children(i))
16
+ : fallback ?? null;
17
17
  return createElement(tag, { ...props, ref }, content);
18
18
  });
19
19
  const tagEntries = htmlTags.reduce((acc, tag) => {
@@ -12,9 +12,11 @@ const renderForTag = (tag) =>
12
12
  // forward ref so consumers like Observer can pass a ref to the real DOM element
13
13
  forwardRef(function Render({ when, children, fallback = null, ...props }, ref) {
14
14
  const shouldRender = Array.isArray(when) ? when.every(Boolean) : !!when;
15
- if (!shouldRender)
16
- return fallback;
17
- const content = typeof children === "function" ? children(when) : children;
15
+ const content = shouldRender
16
+ ? typeof children === "function"
17
+ ? children(when)
18
+ : children
19
+ : fallback;
18
20
  return createElement(tag, { ...props, ref }, content);
19
21
  });
20
22
  const tagEntries = htmlTags.reduce((acc, tag) => {
@@ -40,9 +40,15 @@ export const Slot = forwardRef((props, ref) => {
40
40
  const childrenArray = Children.toArray(children);
41
41
  const slottable = childrenArray.find(isSlottable);
42
42
  if (slottable) {
43
- const newElement = cloneElement(slottable, {
44
- ...mergeProps(slotProps, slottable.props),
45
- ref: composeRefs(ref, slottable.ref)
43
+ // Slottable의 children을 가져와서 병합
44
+ const slottableChild = slottable.props.children;
45
+ if (!isValidElement(slottableChild)) {
46
+ return null;
47
+ }
48
+ const newElement = cloneElement(slottableChild, {
49
+ ...mergeProps(slotProps, slottableChild.props),
50
+ ref: ref ? composeRefs(ref, slottableChild.ref) : slottableChild.ref,
51
+ key: slottable.key
46
52
  });
47
53
  const newChildren = childrenArray.map((child) => {
48
54
  if (child === slottable) {
@@ -58,7 +64,7 @@ export const Slot = forwardRef((props, ref) => {
58
64
  }
59
65
  return cloneElement(child, {
60
66
  ...mergeProps(slotProps, child.props),
61
- ref: composeRefs(ref, child.ref)
67
+ ref: ref ? composeRefs(ref, child.ref) : child.ref
62
68
  });
63
69
  });
64
70
  function isSlottable(child) {
@@ -1,73 +1,2 @@
1
1
  import { SlackerProps } from "../types";
2
- /**
3
- * Slacker 컴포넌트 - Lazy Loading 전용
4
- *
5
- * 뷰포트에 보이지 않을 때는 fallback을 표시하고,
6
- * 뷰포트에 들어오면 loader를 실행하여 데이터를 로드한 후
7
- * children 함수에 로드된 데이터를 전달하여 렌더링합니다.
8
- * 한 번 로드되면 다시 되돌리지 않습니다 (triggerOnce=true).
9
- *
10
- * @example
11
- * ```tsx
12
- * // 컴포넌트 lazy loading
13
- * <Slacker
14
- * fallback={<ChartSkeleton />}
15
- * loader={async () => {
16
- * const { HeavyChart } = await import('./HeavyChart');
17
- * return HeavyChart;
18
- * }}
19
- * >
20
- * {(Component) => <Component data={data} />}
21
- * </Slacker>
22
- *
23
- * // 데이터 lazy loading
24
- * <Slacker
25
- * fallback={<div>Loading data...</div>}
26
- * loader={async () => {
27
- * const response = await fetch('/api/data');
28
- * return response.json();
29
- * }}
30
- * >
31
- * {(data) => (
32
- * <div>
33
- * <h2>{data.title}</h2>
34
- * <p>{data.description}</p>
35
- * </div>
36
- * )}
37
- * </Slacker>
38
- *
39
- * // 라이브러리와 데이터 함께 로딩
40
- * <Slacker
41
- * fallback={<div>Loading chart library...</div>}
42
- * loader={async () => {
43
- * const [{ Chart }, chartData] = await Promise.all([
44
- * import('chart.js'),
45
- * fetch('/api/chart-data').then(r => r.json())
46
- * ]);
47
- * return { Chart, data: chartData };
48
- * }}
49
- * >
50
- * {({ Chart, data }) => <Chart data={data} />}
51
- * </Slacker>
52
- *
53
- * // 이미지와 메타데이터 함께 로딩
54
- * <Slacker
55
- * fallback={<ImageSkeleton />}
56
- * loader={async () => {
57
- * const [imageUrl, metadata] = await Promise.all([
58
- * loadHighResImage(id),
59
- * fetch(`/api/images/${id}/metadata`).then(r => r.json())
60
- * ]);
61
- * return { imageUrl, metadata };
62
- * }}
63
- * >
64
- * {({ imageUrl, metadata }) => (
65
- * <div>
66
- * <img src={imageUrl} alt={metadata.title} />
67
- * <p>{metadata.description}</p>
68
- * </div>
69
- * )}
70
- * </Slacker>
71
- * ```
72
- */
73
- export declare function Slacker({ children, fallback, loader, threshold, rootMargin, }: SlackerProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function Slacker<T = any>({ children, errorFallback, loadingFallback, loader, threshold, rootMargin, onError, maxRetries, retryDelay, }: SlackerProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -1,96 +1,62 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useCallback, useState } from "react";
3
3
  import { Observer } from "../components/Observer";
4
- /**
5
- * Slacker 컴포넌트 - Lazy Loading 전용
6
- *
7
- * 뷰포트에 보이지 않을 때는 fallback을 표시하고,
8
- * 뷰포트에 들어오면 loader를 실행하여 데이터를 로드한 후
9
- * children 함수에 로드된 데이터를 전달하여 렌더링합니다.
10
- * 한 번 로드되면 다시 되돌리지 않습니다 (triggerOnce=true).
11
- *
12
- * @example
13
- * ```tsx
14
- * // 컴포넌트 lazy loading
15
- * <Slacker
16
- * fallback={<ChartSkeleton />}
17
- * loader={async () => {
18
- * const { HeavyChart } = await import('./HeavyChart');
19
- * return HeavyChart;
20
- * }}
21
- * >
22
- * {(Component) => <Component data={data} />}
23
- * </Slacker>
24
- *
25
- * // 데이터 lazy loading
26
- * <Slacker
27
- * fallback={<div>Loading data...</div>}
28
- * loader={async () => {
29
- * const response = await fetch('/api/data');
30
- * return response.json();
31
- * }}
32
- * >
33
- * {(data) => (
34
- * <div>
35
- * <h2>{data.title}</h2>
36
- * <p>{data.description}</p>
37
- * </div>
38
- * )}
39
- * </Slacker>
40
- *
41
- * // 라이브러리와 데이터 함께 로딩
42
- * <Slacker
43
- * fallback={<div>Loading chart library...</div>}
44
- * loader={async () => {
45
- * const [{ Chart }, chartData] = await Promise.all([
46
- * import('chart.js'),
47
- * fetch('/api/chart-data').then(r => r.json())
48
- * ]);
49
- * return { Chart, data: chartData };
50
- * }}
51
- * >
52
- * {({ Chart, data }) => <Chart data={data} />}
53
- * </Slacker>
54
- *
55
- * // 이미지와 메타데이터 함께 로딩
56
- * <Slacker
57
- * fallback={<ImageSkeleton />}
58
- * loader={async () => {
59
- * const [imageUrl, metadata] = await Promise.all([
60
- * loadHighResImage(id),
61
- * fetch(`/api/images/${id}/metadata`).then(r => r.json())
62
- * ]);
63
- * return { imageUrl, metadata };
64
- * }}
65
- * >
66
- * {({ imageUrl, metadata }) => (
67
- * <div>
68
- * <img src={imageUrl} alt={metadata.title} />
69
- * <p>{metadata.description}</p>
70
- * </div>
71
- * )}
72
- * </Slacker>
73
- * ```
74
- */
75
- export function Slacker({ children, fallback, loader, threshold = 0.1, rootMargin = "50px", }) {
4
+ export function Slacker({ children, errorFallback, loadingFallback, loader, threshold = 0.1, rootMargin = "50px", onError, maxRetries = 0, retryDelay = 1000, }) {
76
5
  const [loadedData, setLoadedData] = useState(null);
77
6
  const [isLoading, setIsLoading] = useState(false);
78
- const [hasLoaded, setHasLoaded] = useState(false);
79
- const handleIntersect = async (isIntersecting) => {
80
- if (isIntersecting && !hasLoaded) {
81
- setIsLoading(true);
82
- try {
83
- const data = await loader();
84
- setLoadedData(data);
85
- setHasLoaded(true);
86
- }
87
- catch (error) {
88
- console.error('Slacker loader failed:', error);
89
- }
90
- finally {
91
- setIsLoading(false);
7
+ const [error, setError] = useState(null);
8
+ const [retryCount, setRetryCount] = useState(0);
9
+ const load = useCallback(async () => {
10
+ setIsLoading(true);
11
+ setError(null);
12
+ try {
13
+ const data = await loader();
14
+ setLoadedData(data);
15
+ setRetryCount(0); // 성공 시 재시도 카운트 리셋
16
+ }
17
+ catch (err) {
18
+ const error = err instanceof Error ? err : new Error(String(err));
19
+ setError(error);
20
+ console.error('Slacker loader failed:', error);
21
+ onError?.(error);
22
+ }
23
+ finally {
24
+ setIsLoading(false);
25
+ }
26
+ }, [loader, onError]);
27
+ const retry = useCallback(async () => {
28
+ if (retryCount < maxRetries) {
29
+ setRetryCount(prev => prev + 1);
30
+ if (retryDelay > 0) {
31
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
92
32
  }
33
+ await load();
34
+ }
35
+ }, [retryCount, maxRetries, retryDelay, load]);
36
+ const handleIntersect = useCallback(async (isIntersecting) => {
37
+ if (isIntersecting && !isLoading && loadedData === null) {
38
+ await load();
39
+ }
40
+ }, [isLoading, loadedData, load]);
41
+ // 자동 재시도
42
+ const shouldAutoRetry = error && maxRetries > 0 && retryCount < maxRetries;
43
+ if (shouldAutoRetry && !isLoading) {
44
+ retry();
45
+ }
46
+ // 렌더링할 내용 결정
47
+ const renderContent = () => {
48
+ if (loadedData !== null) {
49
+ return children(loadedData);
50
+ }
51
+ if (error) {
52
+ return typeof errorFallback === 'function'
53
+ ? errorFallback({ isLoading, error, retry })
54
+ : errorFallback;
55
+ }
56
+ if (isLoading) {
57
+ return loadingFallback;
93
58
  }
59
+ return null;
94
60
  };
95
- return (_jsx(Observer, { threshold: threshold, rootMargin: rootMargin, fallback: fallback, triggerOnce: true, onIntersect: handleIntersect, children: hasLoaded ? children(loadedData) : null }));
61
+ return (_jsx(Observer, { threshold: threshold, rootMargin: rootMargin, triggerOnce: true, onIntersect: handleIntersect, children: renderContent() }));
96
62
  }
@@ -1,4 +1,3 @@
1
1
  export * from './experimental/Mount';
2
2
  export * from './experimental/Slacker';
3
3
  export * from './experimental/Switch';
4
- export * from './experimental/Slot';
@@ -1,4 +1,3 @@
1
1
  export * from './experimental/Mount';
2
2
  export * from './experimental/Slacker';
3
3
  export * from './experimental/Switch';
4
- export * from './experimental/Slot';
@@ -3,26 +3,26 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
3
3
  const [element, setElement] = useState(null);
4
4
  const [isIntersecting, setIsIntersecting] = useState(initialIsIntersecting);
5
5
  const [entry, setEntry] = useState();
6
- const observerRef = useRef(null);
7
- const changeCallbackRef = useRef(onChange);
8
- // Memoize options to prevent unnecessary observer recreation
9
- const observerOptions = useMemo(() => ({
10
- threshold,
11
- root,
12
- rootMargin,
13
- }), [threshold, root, rootMargin]);
14
- const isFrozen = freezeOnceVisible && isIntersecting;
6
+ const onChangeRef = useRef(onChange);
7
+ const isFirstCallbackRef = useRef(true);
8
+ const isFrozen = useRef(false);
15
9
  // Keep callback ref updated
16
10
  useEffect(() => {
17
- changeCallbackRef.current = onChange;
11
+ onChangeRef.current = onChange;
18
12
  }, [onChange]);
13
+ // Memoize options
14
+ const observerOptions = useMemo(() => ({ threshold, root, rootMargin }), [threshold, root, rootMargin]);
19
15
  // Ref callback to set the element
20
16
  const ref = useCallback((node) => {
21
17
  setElement(node);
22
18
  }, []);
23
19
  // Main intersection observer effect
24
20
  useEffect(() => {
25
- if (!element || !("IntersectionObserver" in window) || isFrozen) {
21
+ if (!element || !("IntersectionObserver" in window)) {
22
+ return;
23
+ }
24
+ // If frozen (triggerOnce + already intersected), skip observation
25
+ if (isFrozen.current) {
26
26
  return;
27
27
  }
28
28
  const observer = new IntersectionObserver((entries) => {
@@ -34,28 +34,38 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
34
34
  : [observer.thresholds];
35
35
  const isCurrentlyIntersecting = intersectionEntry.isIntersecting &&
36
36
  thresholds.some((t) => intersectionEntry.intersectionRatio >= t);
37
+ // Update state
37
38
  setIsIntersecting(isCurrentlyIntersecting);
38
39
  setEntry(intersectionEntry);
39
- // Call onChange callback if provided
40
- changeCallbackRef.current?.(isCurrentlyIntersecting, intersectionEntry);
40
+ // Skip the first callback (initial observation)
41
+ if (isFirstCallbackRef.current) {
42
+ isFirstCallbackRef.current = false;
43
+ return;
44
+ }
45
+ // Call onChange callback
46
+ onChangeRef.current?.(isCurrentlyIntersecting, intersectionEntry);
47
+ // Freeze if triggerOnce and now intersecting
48
+ if (freezeOnceVisible && isCurrentlyIntersecting) {
49
+ isFrozen.current = true;
50
+ // Immediately disconnect to stop further observations
51
+ observer.disconnect();
52
+ }
41
53
  }, observerOptions);
42
54
  observer.observe(element);
43
- observerRef.current = observer;
44
55
  return () => {
45
56
  observer.disconnect();
46
- observerRef.current = null;
47
57
  };
48
- }, [element, observerOptions, isFrozen]);
49
- // Reset state when element is removed and not frozen
58
+ }, [element, observerOptions, freezeOnceVisible]);
59
+ // Reset when element is removed
50
60
  useEffect(() => {
51
- if (!element && !freezeOnceVisible && !isFrozen) {
61
+ if (!element) {
52
62
  setIsIntersecting(initialIsIntersecting);
53
63
  setEntry(undefined);
64
+ isFirstCallbackRef.current = true;
65
+ if (!freezeOnceVisible) {
66
+ isFrozen.current = false;
67
+ }
54
68
  }
55
- }, [element, freezeOnceVisible, isFrozen, initialIsIntersecting]);
56
- return useMemo(() => ({
57
- ref,
58
- isIntersecting,
59
- entry,
60
- }), [ref, isIntersecting, entry]);
69
+ }, [element, freezeOnceVisible, initialIsIntersecting]);
70
+ return useMemo(() => ({ ref, isIntersecting, entry }), [ref, isIntersecting, entry]);
61
71
  }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from './components/Observer';
3
3
  export * from './components/OptionalWrapper';
4
4
  export * from './components/Repeat';
5
5
  export * from './components/Show';
6
+ export * from './components/Slot';
6
7
  export * from './hooks/useIntersectionObserver';
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export * from './components/Observer';
3
3
  export * from './components/OptionalWrapper';
4
4
  export * from './components/Repeat';
5
5
  export * from './components/Show';
6
+ export * from './components/Slot';
6
7
  export * from './hooks/useIntersectionObserver';
@@ -32,11 +32,20 @@ export type GetLiteralKeys<T> = {
32
32
  [K in keyof T]: T[K] extends string ? string extends T[K] ? never : K : T[K] extends number ? number extends T[K] ? never : K : T[K] extends boolean ? boolean extends T[K] ? never : K : T[K] extends bigint ? bigint extends T[K] ? never : K : T[K] extends symbol ? symbol extends T[K] ? never : K : never;
33
33
  }[keyof T];
34
34
  export type LiteralKeys<T> = [GetLiteralKeys<T>] extends [never] ? keyof T : GetLiteralKeys<T>;
35
- export type SlackerProps = {
36
- children: (loaded: any) => React.ReactNode;
37
- fallback?: React.ReactNode;
35
+ export type SlackerFallbackProps = {
36
+ isLoading: boolean;
37
+ error: Error | null;
38
+ retry: () => void;
39
+ };
40
+ export type SlackerProps<T = any> = {
41
+ children: (loaded: T) => React.ReactNode;
42
+ errorFallback?: React.ReactNode | ((props: SlackerFallbackProps) => React.ReactNode);
43
+ loadingFallback?: React.ReactNode;
38
44
  threshold?: number | number[];
39
45
  rootMargin?: string;
40
- loader: () => Promise<any> | any;
46
+ loader: () => Promise<T> | T;
47
+ onError?: (error: Error) => void;
48
+ maxRetries?: number;
49
+ retryDelay?: number;
41
50
  };
42
51
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilokesto/utilinent",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ilokesto/utilinent.git"
File without changes