@ilokesto/utilinent 0.0.26 → 1.0.1

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.
package/README.md CHANGED
@@ -1,285 +1,90 @@
1
- # @ilokesto/utilinent
1
+ [![Build Size](https://img.shields.io/bundlephobia/minzip/@ilokesto/utilinent?label=bundle%20size&style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=@ilokesto/utilinent)
2
+ [![Version](https://img.shields.io/npm/v/@ilokesto/utilinent?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@ilokesto/utilinent)
3
+ [![Downloads](https://img.shields.io/npm/dt/@ilokesto/utilinent.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@ilokesto/utilinent)
2
4
 
3
- React를 위한 유용하고 재사용 가능한 컴포넌트 및 훅 모음입니다.
4
5
 
5
- ## 소개
6
+  
6
7
 
7
- `@ilokesto/utilinent`는 React 애플리케이션 개발 시 반복적으로 필요한 로직과 패턴을 선언적이고 읽기 쉬운 컴포넌트로 제공합니다. 조건부 렌더링, 리스트 렌더링, 지연 로딩 등의 작업을 간편하게 처리할 수 있도록 돕습니다.
8
+ [Official documents](https://ilokesto.vercel.app/utilinent)
8
9
 
9
- ## 설치
10
+  
10
11
 
11
- ```bash
12
- npm install @ilokesto/utilinent
13
- # 또는
14
- yarn add @ilokesto/utilinent
15
- # 또는
16
- pnpm add @ilokesto/utilinent
17
- ```
18
-
19
- ## 주요 기능
20
-
21
- ### 기본 컴포넌트
22
-
23
- 기본적으로 다음 컴포넌트들을 가져와 사용할 수 있습니다.
24
-
25
- ```tsx
26
- import { Show, For, Repeat, Observer, OptionalWrapper, useIntersectionObserver } from '@ilokesto/utilinent';
27
- ```
28
-
29
- #### `<Show>`
30
-
31
- 조건부 렌더링을 위한 컴포넌트입니다. `when` prop이 `true`일 때만 `children`을 렌더링합니다.
32
-
33
- 또한, `Show.div`, `Show.span`과 같이 HTML 태그를 직접 사용하여 래퍼(wrapper) 컴포넌트를 지정할 수 있습니다. 이 경우, `when` prop이 `true`일 때 해당 태그로 감싸진 `children`이 렌더링됩니다.
34
-
35
- **사용 예시:**
36
-
37
- ```tsx
38
- import { Show } from '@ilokesto/utilinent';
39
-
40
- function UserProfile({ user }) {
41
- return (
42
- <div>
43
- <Show when={user.isLoggedIn} fallback={<div>로그인이 필요합니다.</div>}>
44
- <h1>{user.name}님, 환영합니다!</h1>
45
- </Show>
46
-
47
- {/* HTML 태그와 함께 사용 */}
48
- <Show.div when={user.isAdmin} className="admin-badge">
49
- 관리자
50
- </Show.div>
51
- </div>
52
- );
53
- }
54
- ```
55
-
56
- #### `<For>`
57
-
58
- 배열을 순회하며 각 항목을 렌더링합니다. `Array.prototype.map`과 유사하지만, 배열이 비어있을 경우를 위한 `fallback`을 지원합니다.
12
+ As React apps grow, JSX often becomes cluttered with nested ternary operators and bloated map callbacks, which rapidly degrades readability. utilinent was created to solve these recurring UI patterns by providing small, declarative components.
59
13
 
60
- `<Show>`와 유사하게, `For.ul`, `For.div`와 같이 HTML 태그를 사용하여 렌더링되는 항목들을 감싸는 컨테이너 엘리먼트를 지정할 수 있습니다.
14
+ Inspired by the concise and expressive style of SolidJS, utilinent encapsulates common tasks — conditional rendering, list rendering, and lazy loading — into clear APIs. For example, replace complex ternaries with a `Show` component, or use `For` to render arrays along with a built-in fallback for empty data.
61
15
 
62
- **사용 예시:**
16
+ By moving noisy logic out of views and into reusable components, utilinent improves readability, maintainability, and lets developers focus more on business logic while boosting team productivity.
63
17
 
64
- ```tsx
65
- import { For } from '@ilokesto/utilinent';
66
-
67
- function TodoList({ todos }) {
68
- return (
69
- <For.ul each={todos} fallback={<li>할 일이 없습니다.</li>} className="todo-list">
70
- {(todo, index) => <li key={index}>{todo.text}</li>}
71
- </For.ul>
72
- );
73
- }
74
- ```
75
-
76
- #### `<Repeat>`
77
-
78
- 주어진 횟수(`times`)만큼 `children` 함수를 반복하여 렌더링합니다.
18
+ &nbsp;
79
19
 
80
- 다른 컴포넌트들과 마찬가지로, `Repeat.div`와 같이 HTML 태그를 사용하여 반복되는 항목들을 감싸는 컨테이너를 지정할 수 있습니다.
81
-
82
- **사용 예시:**
83
-
84
- ```tsx
85
- import { Repeat } from '@ilokesto/utilinent';
86
-
87
- function StarRating({ rating }) {
88
- return (
89
- <Repeat.div times={rating} className="star-container">
90
- {(index) => <span key={index}>⭐️</span>}
91
- </Repeat.div>
92
- );
93
- }
94
- ```
20
+ ## Installation
95
21
 
96
- #### `<Observer>`
97
-
98
- 컴포넌트가 화면에 보일 때(intersect) `children`을 렌더링합니다. Intersection Observer API를 기반으로 하며, 지연 로딩(lazy loading) 구현에 유용합니다.
99
-
100
- **사용 예시:**
101
-
102
- ```tsx
103
- import { Observer } from '@ilokesto/utilinent';
104
-
105
- function LazyComponent() {
106
- return (
107
- <Observer fallback={<div>로딩 중...</div>}>
108
- {/* 화면에 보일 때 렌더링될 무거운 컴포넌트 */}
109
- <HeavyComponent />
110
- </Observer>
111
- );
112
- }
113
- ```
114
-
115
- #### `<OptionalWrapper>`
116
-
117
- `when` prop이 `true`일 때만 `children`을 `wrapper` 함수로 감싸줍니다.
118
-
119
- **사용 예시:**
120
-
121
- ```tsx
122
- import { OptionalWrapper } from '@ilokesto/utilinent';
22
+ utilinent can be installed using several methods listed below.
123
23
 
124
- function Post({ post, withLink }) {
125
- return (
126
- <OptionalWrapper
127
- when={withLink}
128
- wrapper={(children) => <a href={`/posts/${post.id}`}>{children}</a>}
129
- >
130
- <h2>{post.title}</h2>
131
- <p>{post.summary}</p>
132
- </OptionalWrapper>
133
- );
134
- }
24
+ ```bash
25
+ npm install @ilokesto/utilinent
26
+ pnpm add @ilokesto/utilinent
27
+ yarn add @ilokesto/utilinent
28
+ bun add @ilokesto/utilinent
135
29
  ```
136
30
 
137
- ### 훅(Hooks)
138
-
139
- #### `useIntersectionObserver`
31
+ &nbsp;
140
32
 
141
- Intersection Observer API를 React 훅으로 감싼 것입니다. 컴포넌트의 뷰포트 내 가시성을 추적하는 데 사용됩니다.
33
+ ## Quick start
142
34
 
143
- **사용 예시:**
35
+ When handling async data in React, it's common to render different UI for loading, empty, or populated states. The examples below show typical patterns and how utilinent components simplify them.
144
36
 
145
37
  ```tsx
146
- import { useIntersectionObserver } from '@ilokesto/utilinent';
147
- import { useRef } from 'react';
148
-
149
- function MyComponent() {
150
- const { ref, isIntersecting } = useIntersectionObserver({ threshold: 0.5 });
38
+ import React, { useState, useEffect } from 'react';
39
+
40
+ const UserList = () => {
41
+ const { data: users } = useQuery( ... )
151
42
 
152
43
  return (
153
- <div ref={ref} style={{ transition: 'opacity 0.5s', opacity: isIntersecting ? 1 : 0.2 }}>
154
- {isIntersecting ? '이제 화면에 보입니다!' : '화면 밖에 있습니다.'}
44
+ <div>
45
+ <h2>User List</h2>
46
+ {loading ? (
47
+ <p>Loading users...</p>
48
+ ) : (
49
+ users.length > 0 ? (
50
+ <ul>
51
+ {users.map(user => (
52
+ <li key={user.id}>{user.name}</li>
53
+ ))}
54
+ </ul>
55
+ ) : (
56
+ <p>No users found.</p>
57
+ )
58
+ )}
155
59
  </div>
156
60
  );
157
- }
158
- ```
159
-
160
- ### 실험적 기능
161
-
162
- 실험적인 컴포넌트 및 훅은 `experimental` 경로에서 가져올 수 있습니다. 이 기능들은 API가 변경될 수 있습니다.
163
-
164
- ```tsx
165
- import { Mount, Slacker, createSwitcher, Slot, Slottable } from '@ilokesto/utilinent/experimental';
166
- ```
167
-
168
- #### `<Slot>` 및 `<Slottable>`
169
-
170
- 자식 컴포넌트에 props를 전달하고 병합하는 Radix UI의 `<Slot>`과 유사한 패턴을 구현합니다. `<Slot>`은 자신의 props를 자식 요소에 병합합니다. 여러 자식 중 특정 자식에게 props를 전달하고 싶을 때는 `<Slottable>`로 감싸주면 됩니다.
171
-
172
- Props 병합 규칙은 다음과 같습니다:
173
- * **`className`**: 부모와 자식의 `className`이 합쳐집니다.
174
- * **`style`**: 부모와 자식의 `style` 객체가 병합됩니다 (부모 우선).
175
- * **이벤트 핸들러**: 부모와 자식의 이벤트 핸들러가 모두 순차적으로 호출됩니다.
176
- * **`ref`**: 부모와 자식의 `ref`가 모두 연결됩니다.
177
- * **기타 props**: 부모의 props가 자식의 props를 덮어씁니다.
178
-
179
- **사용 예시:**
180
-
181
- ```tsx
182
- import { Slot, Slottable } from '@ilokesto/utilinent/experimental';
183
-
184
- const Button = ({ asChild = false, ...props }) => {
185
- const Comp = asChild ? Slot : 'button';
186
- return <Comp {...props} />;
187
61
  };
188
-
189
- // 기본 버튼으로 사용
190
- <Button onClick={() => alert('Clicked!')}>Click Me</Button>
191
-
192
- // 다른 컴포넌트(a 태그)를 렌더링하면서 props를 전달
193
- <Button asChild>
194
- <a href="/home">Go Home</a>
195
- </Button>
196
-
197
- // 여러 자식 중 특정 자식에게 props 전달
198
- <Button asChild>
199
- <div>
200
- <span>Icon</span>
201
- <Slottable>Text</Slottable>
202
- </div>
203
- </Button>
62
+
63
+ export default UserList;
204
64
  ```
205
65
 
206
- #### `<Mount>`
207
-
208
- 컴포넌트가 마운트될 때 비동기적으로 `children`을 렌더링합니다. `children`으로 비동기 함수를 전달할 수 있습니다.
209
-
210
- **사용 예시:**
211
-
212
- ```tsx
213
- import { Mount } from '@ilokesto/utilinent/experimental';
214
-
215
- function AsyncComponent() {
216
- return (
217
- <Mount fallback={<div>로딩 중...</div>}>
218
- {async () => {
219
- const { HeavyComponent } = await import('./HeavyComponent');
220
- return <HeavyComponent />;
221
- }}
222
- </Mount>
223
- );
224
- }
225
- ```
226
-
227
- #### `<Slacker>`
228
-
229
- 컴포넌트나 데이터의 지연 로딩(lazy loading)을 위한 고급 컴포넌트입니다. 뷰포트에 들어왔을 때 `loader` 함수를 실행하여 비동기 작업을 처리하고, 로딩이 완료되면 결과를 `children`에게 전달하여 렌더링합니다.
230
-
231
- **사용 예시:**
232
-
233
- ```tsx
234
- import { Slacker } from '@ilokesto/utilinent/experimental';
235
-
236
- // 데이터 지연 로딩
237
- function LazyUserData({ userId }) {
238
- return (
239
- <Slacker
240
- fallback={<div>사용자 정보 로딩 중...</div>}
241
- loader={async () => {
242
- const response = await fetch(`/api/users/${userId}`);
243
- return response.json();
244
- }}
245
- >
246
- {(user) => (
247
- <div>
248
- <h1>{user.name}</h1>
249
- <p>{user.email}</p>
250
- </div>
251
- )}
252
- </Slacker>
253
- );
254
- }
255
- ```
256
-
257
- #### `createSwitcher` / `<Switch>` / `<Match>`
258
-
259
- 주어진 데이터와 조건에 따라 여러 `<Match>` 컴포넌트 중 하나를 선택하여 렌더링합니다. `createSwitcher` 팩토리 함수를 통해 `<Switch>`와 `<Match>` 컴포넌트를 생성하여 사용합니다.
260
-
261
- **사용 예시:**
66
+ utilinent's `Show` and `For` components make conditional and list rendering more declarative and concise. They help express loading and list states clearly so your UI intent is easier to read and maintain.
262
67
 
263
68
  ```tsx
264
- import { createSwitcher } from '@ilokesto/utilinent/experimental';
69
+ import React, { useState, useEffect } from 'react';
70
+ import { Show, For } from '@ilokesto/utilinent';
265
71
 
266
- const data = { type: 'image', src: 'image.jpg' };
267
- const { Switch, Match } = createSwitcher(data);
72
+ const UserListAfter = () => {
73
+ const { data: users } = useQuery( ... )
268
74
 
269
- function Media() {
270
75
  return (
271
- <Switch when="type" fallback={<div>지원하지 않는 형식입니다.</div>}>
272
- <Match case="image">
273
- {(data) => <img src={data.src} />}
274
- </Match>
275
- <Match case="video">
276
- {(data) => <video src={data.src} controls />}
277
- </Match>
278
- </Switch>
76
+ <div>
77
+ <h2>User List</h2>
78
+ <Show when={!loading} fallback={<p>Loading users...</p>}>
79
+ <For.ul each={users} fallback={<p>No users found.</p>}>
80
+ {(user) => (
81
+ <li key={user.id}>{user.name}</li>
82
+ )}
83
+ </For.ul>
84
+ </Show>
85
+ </div>
279
86
  );
280
- }
281
- ```
282
-
283
- ## 라이선스
87
+ };
284
88
 
285
- [MIT](./LICENSE)
89
+ export default UserListAfter;
90
+ ```
@@ -6,6 +6,7 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
6
6
  const onChangeRef = useRef(onChange);
7
7
  const isFirstCallbackRef = useRef(true);
8
8
  const isFrozen = useRef(false);
9
+ const prevIsIntersectingRef = useRef(initialIsIntersecting);
9
10
  // Keep callback ref updated
10
11
  useEffect(() => {
11
12
  onChangeRef.current = onChange;
@@ -34,6 +35,8 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
34
35
  : [observer.thresholds];
35
36
  const isCurrentlyIntersecting = intersectionEntry.isIntersecting &&
36
37
  thresholds.some((t) => intersectionEntry.intersectionRatio >= t);
38
+ const wasIntersecting = prevIsIntersectingRef.current;
39
+ prevIsIntersectingRef.current = isCurrentlyIntersecting;
37
40
  // Update state
38
41
  setIsIntersecting(isCurrentlyIntersecting);
39
42
  setEntry(intersectionEntry);
@@ -43,7 +46,9 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
43
46
  return;
44
47
  }
45
48
  // Call onChange callback
46
- onChangeRef.current?.(isCurrentlyIntersecting, intersectionEntry);
49
+ if (!wasIntersecting && isCurrentlyIntersecting) {
50
+ onChangeRef.current?.(isCurrentlyIntersecting, intersectionEntry);
51
+ }
47
52
  // Freeze if triggerOnce and now intersecting
48
53
  if (freezeOnceVisible && isCurrentlyIntersecting) {
49
54
  isFrozen.current = true;
@@ -62,6 +67,7 @@ export function useIntersectionObserver({ threshold = 0, root = null, rootMargin
62
67
  setIsIntersecting(initialIsIntersecting);
63
68
  setEntry(undefined);
64
69
  isFirstCallbackRef.current = true;
70
+ prevIsIntersectingRef.current = initialIsIntersecting;
65
71
  if (!freezeOnceVisible) {
66
72
  isFrozen.current = false;
67
73
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ilokesto/utilinent",
3
- "version": "0.0.26",
3
+ "version": "1.0.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ilokesto/utilinent.git"
7
7
  },
8
+ "homepage": "https://ilokesto.vercel.app/utilinent",
8
9
  "sideEffects": false,
9
10
  "types": "dist/index.d.ts",
10
11
  "module": "dist/index.js",