@jho951/ui-components 0.1.0

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.
@@ -0,0 +1,81 @@
1
+ // hook/useScroll.ts
2
+ import { useEffect, useState } from "react";
3
+ function useScrollY() {
4
+ const [scrollY, setScrollY] = useState(0);
5
+ useEffect(() => {
6
+ const handleScroll = () => setScrollY(window.scrollY);
7
+ handleScroll();
8
+ window.addEventListener("scroll", handleScroll, { passive: true });
9
+ return () => window.removeEventListener("scroll", handleScroll);
10
+ }, []);
11
+ return scrollY;
12
+ }
13
+ function useScrollThresholdReached(threshold) {
14
+ const [reached, setReached] = useState(false);
15
+ useEffect(() => {
16
+ const handler = () => {
17
+ setReached(window.scrollY >= threshold);
18
+ };
19
+ window.addEventListener("scroll", handler, { passive: true });
20
+ return () => window.removeEventListener("scroll", handler);
21
+ }, [threshold]);
22
+ return reached;
23
+ }
24
+ function useScrollLock(lock) {
25
+ useEffect(() => {
26
+ const originalStyle = document.body.style.overflow;
27
+ if (lock) {
28
+ document.body.style.overflow = "hidden";
29
+ }
30
+ return () => {
31
+ document.body.style.overflow = originalStyle;
32
+ };
33
+ }, [lock]);
34
+ }
35
+ function useScrollSyncIndex(containerRef, setIndex, debounceMs = 100) {
36
+ useEffect(() => {
37
+ const container = containerRef.current;
38
+ if (!container) return;
39
+ let timeoutId;
40
+ const handleScroll = () => {
41
+ if (timeoutId) clearTimeout(timeoutId);
42
+ timeoutId = window.setTimeout(() => {
43
+ if (!container) return;
44
+ const newIndex = Math.round(container.scrollLeft / container.offsetWidth);
45
+ setIndex(newIndex);
46
+ }, debounceMs);
47
+ };
48
+ container.addEventListener("scroll", handleScroll);
49
+ return () => {
50
+ container.removeEventListener("scroll", handleScroll);
51
+ if (timeoutId) clearTimeout(timeoutId);
52
+ };
53
+ }, [containerRef, setIndex, debounceMs]);
54
+ }
55
+ function useScrollDetect(delay = 100) {
56
+ const [isScrolling, setIsScrolling] = useState(false);
57
+ useEffect(() => {
58
+ let timeout;
59
+ const handleScroll = () => {
60
+ setIsScrolling(true);
61
+ clearTimeout(timeout);
62
+ timeout = setTimeout(() => {
63
+ setIsScrolling(false);
64
+ }, delay);
65
+ };
66
+ window.addEventListener("scroll", handleScroll, { passive: true });
67
+ return () => {
68
+ window.removeEventListener("scroll", handleScroll);
69
+ clearTimeout(timeout);
70
+ };
71
+ }, [delay]);
72
+ return isScrolling;
73
+ }
74
+
75
+ export {
76
+ useScrollY,
77
+ useScrollThresholdReached,
78
+ useScrollLock,
79
+ useScrollSyncIndex,
80
+ useScrollDetect
81
+ };
@@ -0,0 +1,10 @@
1
+ <svg
2
+ viewBox="0 0 32 32"
3
+ fill="none"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ aria-hidden="true"
6
+ focusable="false"
7
+ >
8
+ <line x1="8" y1="8" x2="24" y2="24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
9
+ <line x1="24" y1="8" x2="8" y2="24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
10
+ </svg>
@@ -0,0 +1,11 @@
1
+ declare function useScrollY(): number;
2
+ declare function useScrollThresholdReached(threshold: number): boolean;
3
+ /**
4
+ * 스크롤 잠금 훅
5
+ * @param lock - true일 때 document.body의 스크롤을 막습니다.
6
+ */
7
+ declare function useScrollLock(lock: boolean): void;
8
+ declare function useScrollSyncIndex(containerRef: React.RefObject<HTMLElement | null>, setIndex: (index: number) => void, debounceMs?: number): void;
9
+ declare function useScrollDetect(delay?: number): boolean;
10
+
11
+ export { useScrollDetect, useScrollLock, useScrollSyncIndex, useScrollThresholdReached, useScrollY };
@@ -0,0 +1,14 @@
1
+ import {
2
+ useScrollDetect,
3
+ useScrollLock,
4
+ useScrollSyncIndex,
5
+ useScrollThresholdReached,
6
+ useScrollY
7
+ } from "../chunk-VRIA2QTM.js";
8
+ export {
9
+ useScrollDetect,
10
+ useScrollLock,
11
+ useScrollSyncIndex,
12
+ useScrollThresholdReached,
13
+ useScrollY
14
+ };
package/dist/index.css ADDED
@@ -0,0 +1,401 @@
1
+ /* ui/icon/Icon.module.css */
2
+ .icon {
3
+ display: inline-block;
4
+ vertical-align: middle;
5
+ flex: 0 0 auto;
6
+ }
7
+ .registry {
8
+ }
9
+ .remote {
10
+ }
11
+ .placeholder {
12
+ border-radius: 6px;
13
+ background: currentColor;
14
+ opacity: 0.12;
15
+ }
16
+
17
+ /* ui/arrow/Arrow.module.css */
18
+ .container {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ transition: transform 0.3s ease-in-out;
23
+ transform: rotate(var(--arrow-rotation, 0deg));
24
+ }
25
+ .rotate {
26
+ transform: rotate(180deg);
27
+ }
28
+
29
+ /* ui/backdrop/BackDrop.module.css */
30
+ .backdrop {
31
+ position: fixed;
32
+ top: 0;
33
+ left: 0;
34
+ right: 0;
35
+ bottom: 0;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ z-index: calc(var(--z-index, 1000) + 5);
40
+ opacity: 0;
41
+ visibility: hidden;
42
+ transition: opacity 0.3s ease, visibility 0.3s ease;
43
+ will-change: opacity, backdrop-filter;
44
+ }
45
+ .backdrop.is-active {
46
+ opacity: 1;
47
+ visibility: visible;
48
+ pointer-events: auto;
49
+ }
50
+ .blur {
51
+ background-color: rgba(var(--color-black-rgb, 0, 0, 0), 0.4);
52
+ -webkit-backdrop-filter: blur(var(--blur, 8px)) saturate(160%);
53
+ backdrop-filter: blur(var(--blur, 8px)) saturate(160%);
54
+ }
55
+ .transparent {
56
+ background-color: transparent;
57
+ -webkit-backdrop-filter: none;
58
+ backdrop-filter: none;
59
+ }
60
+
61
+ /* ui/spinner/Spinner.module.css */
62
+ .spinner {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ animation: spin 1s linear infinite;
67
+ }
68
+ @keyframes spin {
69
+ from {
70
+ transform: rotate(0deg);
71
+ }
72
+ to {
73
+ transform: rotate(360deg);
74
+ }
75
+ }
76
+ @media (prefers-reduced-motion: reduce) {
77
+ .spinner {
78
+ animation-duration: 2s;
79
+ }
80
+ }
81
+
82
+ /* ui/button/Button.module.css */
83
+ .button {
84
+ display: inline-flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ border: none;
88
+ border-radius: var(--border-radius, 2rem);
89
+ color: var(--color-white, #ffffff);
90
+ transition: background-color 0.3s ease, transform 0.2s ease;
91
+ cursor: pointer;
92
+ }
93
+ .primary {
94
+ background-color: var(--color-primary, #f0f0f0);
95
+ }
96
+ .secondary {
97
+ background-color: var(--color-second-gray, #f0f0f0);
98
+ color: var(--color-white, #ffffff);
99
+ }
100
+ .ghost {
101
+ background-color: transparent;
102
+ color: var(--color-primary, #f0f0f0);
103
+ border: 1px solid var(--color-primary, #f0f0f0);
104
+ }
105
+ .text {
106
+ background-color: transparent;
107
+ color: var(--color-black, #f0f0f0);
108
+ }
109
+ .disabled {
110
+ background-color: var(--color-gray, #e6e9ef);
111
+ cursor: not-allowed;
112
+ }
113
+ .s {
114
+ font-size: var(--font-s, 1.4rem);
115
+ padding: var(--button-size-s, 0.5rem 1rem);
116
+ }
117
+ .m {
118
+ font-size: var(--font-m, 1.6rem);
119
+ padding: var(--button-size-m, 0.7rem 1.8rem);
120
+ }
121
+ .l {
122
+ font-size: var(--font-l, 1.8rem);
123
+ padding: var(--button-size-l, 0.9rem 2.5rem);
124
+ }
125
+
126
+ /* ui/card/Card.module.css */
127
+ .card {
128
+ display: flex;
129
+ flex-direction: column;
130
+ padding: 2rem;
131
+ border-radius: 1.2rem;
132
+ }
133
+ .header {
134
+ margin-bottom: 1rem;
135
+ font-weight: bold;
136
+ font-size: 1.8rem;
137
+ }
138
+ .footer {
139
+ margin-top: 1.5rem;
140
+ display: flex;
141
+ justify-content: flex-end;
142
+ gap: 1rem;
143
+ }
144
+
145
+ /* ui/label/Label.module.css */
146
+ .label {
147
+ display: block;
148
+ font-size: 0.95rem;
149
+ font-weight: 500;
150
+ color: var(--color-text);
151
+ margin-bottom: 0.4rem;
152
+ }
153
+ .required {
154
+ color: var(--color-danger);
155
+ margin-left: 0.25rem;
156
+ }
157
+ .default {
158
+ text-align: left;
159
+ }
160
+ .inline {
161
+ display: inline-block;
162
+ margin-bottom: 0;
163
+ margin-right: 0.5rem;
164
+ vertical-align: middle;
165
+ }
166
+ .capsule {
167
+ display: inline-block;
168
+ background: rgba(var(--color-primary-rgb), 0.1);
169
+ color: var(--color-primary);
170
+ padding: 0.2rem 0.6rem;
171
+ border-radius: 999px;
172
+ font-size: 0.75rem;
173
+ font-weight: 600;
174
+ }
175
+ .error {
176
+ color: var(--color-danger);
177
+ }
178
+ .floating {
179
+ position: absolute;
180
+ top: 0.5rem;
181
+ left: 1rem;
182
+ font-size: 0.75rem;
183
+ color: var(--color-secondary-gray);
184
+ pointer-events: none;
185
+ transition: 0.2s ease;
186
+ }
187
+ .filled {
188
+ display: block;
189
+ background: var(--color-surface, #f5f7fb);
190
+ color: var(--color-primary, #414e92);
191
+ border-radius: 0.5rem 0.5rem 0 0;
192
+ padding: 0.5rem 1rem 0.25rem 1rem;
193
+ font-weight: 600;
194
+ }
195
+ .outlined {
196
+ display: block;
197
+ color: var(--color-primary, #414e92);
198
+ border: 1.5px solid var(--color-primary, #414e92);
199
+ border-radius: 0.5rem;
200
+ padding: 0.45rem 1rem 0.3rem 1rem;
201
+ background: transparent;
202
+ font-weight: 600;
203
+ }
204
+
205
+ /* ui/checkbox/CheckBox.module.css */
206
+ .container {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ cursor: pointer;
210
+ gap: 1rem;
211
+ }
212
+ .checkBox {
213
+ width: 18px;
214
+ height: 18px;
215
+ border: 2px solid #d9d9d9;
216
+ border-radius: 2px;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ transition: all 0.2s;
221
+ background-color: #fff;
222
+ }
223
+ .checked + .checkBox,
224
+ .indeterminate + .checkBox {
225
+ background-color: #1890ff;
226
+ border-color: #1890ff;
227
+ }
228
+ .error {
229
+ border-color: var(--error-color);
230
+ }
231
+ .disabled {
232
+ cursor: not-allowed;
233
+ opacity: 0.5;
234
+ }
235
+
236
+ /* ui/divider/Divider.module.css */
237
+ .divider {
238
+ height: 0.1rem;
239
+ background: var(--color-border, 0.1rem);
240
+ margin: 0.2rem 0.6rem;
241
+ opacity: 0.7;
242
+ }
243
+
244
+ /* ui/form/Form.module.css */
245
+ .container {
246
+ width: 100%;
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: var(--gap);
250
+ }
251
+
252
+ /* ui/modal/Modal.module.css */
253
+ .container {
254
+ position: fixed;
255
+ inset: 0;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ z-index: calc(var(--z-index) + 100);
260
+ pointer-events: none;
261
+ }
262
+ .overlay {
263
+ pointer-events: auto;
264
+ }
265
+ .modal {
266
+ width: 90%;
267
+ max-height: 85vh;
268
+ display: flex;
269
+ flex-direction: column;
270
+ background-color: var(--color-bg);
271
+ border-radius: var(--radius);
272
+ box-shadow: 0 20px 25px -5px rgba(var(--color-text), 0.1), 0 10px 10px -5px rgba(var(--color-text), 0.04);
273
+ opacity: 0;
274
+ transform: scale(0.95) translateY(10px);
275
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
276
+ pointer-events: auto;
277
+ }
278
+ .modal.show {
279
+ opacity: 1;
280
+ transform: scale(1) translateY(0);
281
+ }
282
+ .small {
283
+ max-width: 400px;
284
+ }
285
+ .medium {
286
+ max-width: 640px;
287
+ }
288
+ .large {
289
+ max-width: 1024px;
290
+ }
291
+ .full {
292
+ width: 100vw;
293
+ height: 100vh;
294
+ max-height: 100vh;
295
+ border-radius: 0;
296
+ }
297
+ .header {
298
+ padding: 1rem 1.5rem;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ border-bottom: 1px solid var(--color-border);
303
+ }
304
+ .title {
305
+ font-size: 1.25rem;
306
+ font-weight: 600;
307
+ margin: 0;
308
+ }
309
+ .content {
310
+ padding: 1.5rem;
311
+ overflow-y: auto;
312
+ flex: 1;
313
+ }
314
+ .closeBtn {
315
+ padding: 4px;
316
+ border-radius: 6px;
317
+ transition: background 0.2s;
318
+ }
319
+ .closeBtn:hover {
320
+ background-color: var(--color-gray-hover);
321
+ }
322
+
323
+ /* ui/tag/Tag.module.css */
324
+ .tag {
325
+ display: inline-flex;
326
+ align-items: center;
327
+ padding: 0.2em 0.8em;
328
+ font-size: 0.88rem;
329
+ font-weight: 500;
330
+ border-radius: 1em;
331
+ background: var(--color-tag-bg, #f3f4f7);
332
+ color: var(--color-tag-text, #444);
333
+ margin: 0 0.2em 0.2em 0;
334
+ transition: background 0.18s, color 0.18s;
335
+ cursor: default;
336
+ }
337
+ .default {
338
+ background: var(--color-tag-bg, #f3f4f7);
339
+ color: var(--color-tag-text, #444);
340
+ }
341
+ .primary {
342
+ background: var(--color-primary-bg, #eaf1fb);
343
+ color: var(--color-primary, #2955c7);
344
+ }
345
+ .secondary {
346
+ background: var(--color-secondary-bg, #f1eaff);
347
+ color: var(--color-secondary, #6f3ed4);
348
+ }
349
+ .danger {
350
+ background: var(--color-danger-bg, #ffe7e7);
351
+ color: var(--color-danger, #e74c3c);
352
+ }
353
+ .active {
354
+ box-shadow: 0 0 0 2px var(--color-primary, #2955c7);
355
+ background: var(--color-active-bg, #e3edfd);
356
+ color: var(--color-primary-dark, #1a305d);
357
+ }
358
+
359
+ /* ui/textarea/Textarea.module.css */
360
+ .textarea {
361
+ width: 100%;
362
+ padding: 0.6rem 1.5rem;
363
+ border: 1px solid var(--color-bg);
364
+ background-color: rgba(var(--color-gray-rgb), 0.4);
365
+ border-radius: var(--base-radius);
366
+ outline: none;
367
+ transition: border-color 0.2s ease;
368
+ font-family: inherit;
369
+ font-size: 1.2rem;
370
+ resize: vertical;
371
+ }
372
+ .textarea:focus {
373
+ border: 1px solid var(--color-primary);
374
+ background: #fff;
375
+ }
376
+ .textarea.error {
377
+ border-color: var(--color-danger);
378
+ outline-color: var(--color-danger);
379
+ }
380
+ .textarea:disabled {
381
+ background: #f5f5f5;
382
+ color: #bbb;
383
+ cursor: not-allowed;
384
+ opacity: 0.7;
385
+ }
386
+ .textarea::placeholder {
387
+ color: var(--color-secondary-gray);
388
+ font-size: inherit;
389
+ }
390
+ .textarea.sm {
391
+ font-size: 1.2rem;
392
+ }
393
+ .textarea.md {
394
+ font-size: 1.6rem;
395
+ }
396
+ .textarea.lg {
397
+ font-size: 2rem;
398
+ }
399
+ .autoResize {
400
+ resize: none;
401
+ }
@@ -0,0 +1,6 @@
1
+ export { Arrow, ArrowDirection, ArrowProps, BUTTON_SIZES, BUTTON_VARIANTS, BackDrop, BackDropProps, BaseButtonProps, Button, ButtonProps, ButtonSize, ButtonVariant, Card, CardComponent, CardProps, CardSectionType, Checkbox, CheckboxProps, DIRECTION_MAP, Divider, Form, FormCardProps, FormProps, Icon, IconData, IconElement, IconName, IconProps, IconRegistry, IconSource, Label, LabelProps, LabelVariant, Modal, ModalProps, SectionProps, Spinner, SpinnerProps, SvgTag, Tag, TagColor, TagProps, Textarea, TextareaProps, createCardSection, extractSvgInner, extractViewBox, getAriaProps, getRegistryIcon, isExternalSvgPath, resolveIconSrc, useInlineSvg } from './ui/index.js';
2
+ export { SVG_ASSETS } from './assert/index.js';
3
+ export { MOD, cn, deepFreeze, ensurePortalRoot, generateId } from './lib/index.js';
4
+ export { useScrollDetect, useScrollLock, useScrollSyncIndex, useScrollThresholdReached, useScrollY } from './hook/index.js';
5
+ import 'react/jsx-runtime';
6
+ import 'react';
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ import "./chunk-DZ3NPY6X.js";
2
+ import {
3
+ Arrow,
4
+ BUTTON_SIZES,
5
+ BUTTON_VARIANTS,
6
+ BackDrop,
7
+ Button,
8
+ Card,
9
+ Checkbox,
10
+ DIRECTION_MAP,
11
+ Divider,
12
+ Form,
13
+ Icon,
14
+ Label,
15
+ Modal,
16
+ Spinner,
17
+ Tag,
18
+ Textarea,
19
+ createCardSection,
20
+ extractSvgInner,
21
+ extractViewBox,
22
+ getAriaProps,
23
+ getRegistryIcon,
24
+ isExternalSvgPath,
25
+ resolveIconSrc,
26
+ useInlineSvg
27
+ } from "./chunk-SOIXNSFH.js";
28
+ import {
29
+ SVG_ASSETS
30
+ } from "./chunk-PLGHTHAJ.js";
31
+ import {
32
+ useScrollDetect,
33
+ useScrollLock,
34
+ useScrollSyncIndex,
35
+ useScrollThresholdReached,
36
+ useScrollY
37
+ } from "./chunk-VRIA2QTM.js";
38
+ import {
39
+ MOD,
40
+ cn,
41
+ deepFreeze,
42
+ ensurePortalRoot,
43
+ generateId
44
+ } from "./chunk-Q55QXUNN.js";
45
+ export {
46
+ Arrow,
47
+ BUTTON_SIZES,
48
+ BUTTON_VARIANTS,
49
+ BackDrop,
50
+ Button,
51
+ Card,
52
+ Checkbox,
53
+ DIRECTION_MAP,
54
+ Divider,
55
+ Form,
56
+ Icon,
57
+ Label,
58
+ MOD,
59
+ Modal,
60
+ SVG_ASSETS,
61
+ Spinner,
62
+ Tag,
63
+ Textarea,
64
+ cn,
65
+ createCardSection,
66
+ deepFreeze,
67
+ ensurePortalRoot,
68
+ extractSvgInner,
69
+ extractViewBox,
70
+ generateId,
71
+ getAriaProps,
72
+ getRegistryIcon,
73
+ isExternalSvgPath,
74
+ resolveIconSrc,
75
+ useInlineSvg,
76
+ useScrollDetect,
77
+ useScrollLock,
78
+ useScrollSyncIndex,
79
+ useScrollThresholdReached,
80
+ useScrollY
81
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 전달받은 인자들 중 유효한 문자열만 합쳐서 하나의 클래스 문자열로 반환합니다.
3
+ * deepFreeze를 사용하여 처리 과정의 안정성을 높였습니다.
4
+ */
5
+ declare function cn(...inputs: (string | undefined | null | boolean | object)[]): string;
6
+
7
+ /** 객체를 재귀적으로 동결하여 완전한 읽기 전용 상태로 만듭니다. */
8
+ declare function deepFreeze<T>(obj: T, seen?: WeakSet<object>): T;
9
+
10
+ declare function ensurePortalRoot(id: string): HTMLElement;
11
+
12
+ /**
13
+ * 전역 유니크 ID 생성 함수
14
+ */
15
+ declare function generateId(): string;
16
+
17
+ type MOD_TYPE = "Meta" | "Control";
18
+ declare const MOD: MOD_TYPE;
19
+
20
+ export { MOD, cn, deepFreeze, ensurePortalRoot, generateId };
@@ -0,0 +1,14 @@
1
+ import {
2
+ MOD,
3
+ cn,
4
+ deepFreeze,
5
+ ensurePortalRoot,
6
+ generateId
7
+ } from "../chunk-Q55QXUNN.js";
8
+ export {
9
+ MOD,
10
+ cn,
11
+ deepFreeze,
12
+ ensurePortalRoot,
13
+ generateId
14
+ };
@@ -0,0 +1,7 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
3
+ fill="currentColor"
4
+ stroke="currentColor"
5
+ stroke-width="2"
6
+ stroke-linejoin="round"/>
7
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg
2
+ width="24"
3
+ height="24"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="3"
8
+ stroke-linecap="round"
9
+ stroke-linejoin="round"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <circle
13
+ cx="12"
14
+ cy="12"
15
+ r="10"
16
+ stroke="currentColor"
17
+ opacity="0.25"
18
+ fill="none"
19
+ />
20
+ <path
21
+ d="M12 2a10 10 0 0 1 10 10"
22
+ stroke="currentColor"
23
+ />
24
+ </svg>