sh-ui-cli 0.48.0 → 0.50.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.
Files changed (93) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,430 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { byKey, carousel, carousel__content, carousel__item, carousel__nav, carouselNavPrev, carouselNavNext, carousel__indicators, carousel__indicator } from "./styles.css";
5
+
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ type Orientation = "horizontal" | "vertical";
9
+
10
+ interface CarouselContextValue {
11
+ orientation: Orientation;
12
+ loop: boolean;
13
+ index: number;
14
+ count: number;
15
+ goTo: (i: number) => void;
16
+ goPrev: () => void;
17
+ goNext: () => void;
18
+ registerItem: () => () => void;
19
+ setContentEl: (el: HTMLDivElement | null) => void;
20
+ contentRef: React.RefObject<HTMLDivElement | null>;
21
+ }
22
+
23
+ const CarouselContext = React.createContext<CarouselContextValue | null>(null);
24
+
25
+ function useCarousel() {
26
+ const ctx = React.useContext(CarouselContext);
27
+ if (!ctx) {
28
+ throw new Error("Carousel parts must be used within <Carousel>");
29
+ }
30
+ return ctx;
31
+ }
32
+
33
+ /** 현재 캐러셀 상태를 읽을 수 있는 hook. 커스텀 컨트롤을 만들 때 사용. */
34
+ export function useCarouselState(): Pick<
35
+ CarouselContextValue,
36
+ "orientation" | "loop" | "index" | "count" | "goTo" | "goPrev" | "goNext"
37
+ > {
38
+ const { orientation, loop, index, count, goTo, goPrev, goNext } =
39
+ useCarousel();
40
+ return { orientation, loop, index, count, goTo, goPrev, goNext };
41
+ }
42
+
43
+ export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
44
+ /** 스크롤 방향. 기본 `horizontal`. */
45
+ orientation?: Orientation;
46
+ /** 끝에서 다시 처음으로 이어지는 무한 루프. */
47
+ loop?: boolean;
48
+ /** 자동 재생 간격(ms). `true`면 4000ms. */
49
+ autoPlay?: boolean | number;
50
+ /** 초기 인덱스. */
51
+ defaultIndex?: number;
52
+ /** 제어 모드 인덱스. */
53
+ index?: number;
54
+ /** 인덱스 변경 콜백. */
55
+ onIndexChange?: (index: number) => void;
56
+ }
57
+
58
+ /**
59
+ * 순수 CSS scroll-snap 기반 캐러셀. 터치 드래그·키보드 화살표·자동 재생을
60
+ * 지원하며, prefers-reduced-motion 환경에서는 스냅 전환이 즉시 이동한다.
61
+ *
62
+ * 구성 요소: Carousel / CarouselContent / CarouselItem /
63
+ * CarouselPrevious / CarouselNext / CarouselIndicators.
64
+ */
65
+ export const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
66
+ (
67
+ {
68
+ className,
69
+ orientation = "horizontal",
70
+ loop = false,
71
+ autoPlay,
72
+ defaultIndex = 0,
73
+ index: controlledIndex,
74
+ onIndexChange,
75
+ children,
76
+ onKeyDown: userOnKeyDown,
77
+ ...props
78
+ },
79
+ ref,
80
+ ) => {
81
+ const isControlled = controlledIndex !== undefined;
82
+ const [uncontrolled, setUncontrolled] = React.useState(defaultIndex);
83
+ const index = isControlled ? controlledIndex : uncontrolled;
84
+
85
+ const [count, setCount] = React.useState(0);
86
+ const contentRef = React.useRef<HTMLDivElement | null>(null);
87
+ const scrollTargetRef = React.useRef<number | null>(null);
88
+
89
+ const setContentEl = React.useCallback((el: HTMLDivElement | null) => {
90
+ contentRef.current = el;
91
+ }, []);
92
+
93
+ const registerItem = React.useCallback(() => {
94
+ setCount((c) => c + 1);
95
+ return () => setCount((c) => Math.max(0, c - 1));
96
+ }, []);
97
+
98
+ const setIndex = React.useCallback(
99
+ (next: number) => {
100
+ if (!isControlled) setUncontrolled(next);
101
+ onIndexChange?.(next);
102
+ },
103
+ [isControlled, onIndexChange],
104
+ );
105
+
106
+ const clamp = React.useCallback(
107
+ (i: number) => {
108
+ if (count <= 0) return 0;
109
+ if (loop) {
110
+ return ((i % count) + count) % count;
111
+ }
112
+ return Math.max(0, Math.min(count - 1, i));
113
+ },
114
+ [count, loop],
115
+ );
116
+
117
+ const goTo = React.useCallback(
118
+ (i: number) => setIndex(clamp(i)),
119
+ [clamp, setIndex],
120
+ );
121
+ const goPrev = React.useCallback(
122
+ () => setIndex(clamp(index - 1)),
123
+ [clamp, index, setIndex],
124
+ );
125
+ const goNext = React.useCallback(
126
+ () => setIndex(clamp(index + 1)),
127
+ [clamp, index, setIndex],
128
+ );
129
+
130
+ // 인덱스 변경 시 프로그래매틱 스크롤
131
+ React.useEffect(() => {
132
+ const el = contentRef.current;
133
+ if (!el) return;
134
+ const child = el.children[index] as HTMLElement | undefined;
135
+ if (!child) return;
136
+ scrollTargetRef.current = index;
137
+ if (orientation === "horizontal") {
138
+ el.scrollTo({ left: child.offsetLeft, behavior: "smooth" });
139
+ } else {
140
+ el.scrollTo({ top: child.offsetTop, behavior: "smooth" });
141
+ }
142
+ }, [index, orientation, count]);
143
+
144
+ // 사용자 스와이프/스크롤 → 인덱스 동기화
145
+ React.useEffect(() => {
146
+ const el = contentRef.current;
147
+ if (!el) return;
148
+ let raf = 0;
149
+ const onScroll = () => {
150
+ cancelAnimationFrame(raf);
151
+ raf = requestAnimationFrame(() => {
152
+ const children = Array.from(el.children) as HTMLElement[];
153
+ if (children.length === 0) return;
154
+ const axis = orientation === "horizontal" ? "offsetLeft" : "offsetTop";
155
+ const scroll =
156
+ orientation === "horizontal" ? el.scrollLeft : el.scrollTop;
157
+ let nearest = 0;
158
+ let minDelta = Infinity;
159
+ children.forEach((c, i) => {
160
+ const d = Math.abs(c[axis] - scroll);
161
+ if (d < minDelta) {
162
+ minDelta = d;
163
+ nearest = i;
164
+ }
165
+ });
166
+ if (nearest !== index) {
167
+ // 프로그래매틱 이동 중이면 target 도착까지 무시
168
+ if (
169
+ scrollTargetRef.current !== null &&
170
+ nearest !== scrollTargetRef.current
171
+ ) {
172
+ return;
173
+ }
174
+ scrollTargetRef.current = null;
175
+ setIndex(nearest);
176
+ } else {
177
+ scrollTargetRef.current = null;
178
+ }
179
+ });
180
+ };
181
+ el.addEventListener("scroll", onScroll, { passive: true });
182
+ return () => {
183
+ cancelAnimationFrame(raf);
184
+ el.removeEventListener("scroll", onScroll);
185
+ };
186
+ }, [orientation, index, setIndex]);
187
+
188
+ // 자동 재생 — 호버·키보드 포커스 시 일시정지(WCAG 2.2.2).
189
+ React.useEffect(() => {
190
+ if (!autoPlay || count <= 1) return;
191
+ const delay = autoPlay === true ? 4000 : autoPlay;
192
+ const el = contentRef.current;
193
+ let paused = false;
194
+ const pause = () => (paused = true);
195
+ const resume = () => (paused = false);
196
+ el?.addEventListener("mouseenter", pause);
197
+ el?.addEventListener("mouseleave", resume);
198
+ el?.addEventListener("focusin", pause);
199
+ el?.addEventListener("focusout", resume);
200
+ const id = window.setInterval(() => {
201
+ if (paused) return;
202
+ setIndex(clamp(index + 1));
203
+ }, delay);
204
+ return () => {
205
+ window.clearInterval(id);
206
+ el?.removeEventListener("mouseenter", pause);
207
+ el?.removeEventListener("mouseleave", resume);
208
+ el?.removeEventListener("focusin", pause);
209
+ el?.removeEventListener("focusout", resume);
210
+ };
211
+ }, [autoPlay, count, index, clamp, setIndex]);
212
+
213
+ const onKeyDown = React.useCallback(
214
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
215
+ userOnKeyDown?.(event);
216
+ if (event.defaultPrevented) return;
217
+ const prevKey = orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
218
+ const nextKey = orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
219
+ if (event.key === prevKey) {
220
+ event.preventDefault();
221
+ goPrev();
222
+ } else if (event.key === nextKey) {
223
+ event.preventDefault();
224
+ goNext();
225
+ }
226
+ },
227
+ [orientation, goPrev, goNext, userOnKeyDown],
228
+ );
229
+
230
+ const value = React.useMemo<CarouselContextValue>(
231
+ () => ({
232
+ orientation,
233
+ loop,
234
+ index,
235
+ count,
236
+ goTo,
237
+ goPrev,
238
+ goNext,
239
+ registerItem,
240
+ setContentEl,
241
+ contentRef,
242
+ }),
243
+ [
244
+ orientation,
245
+ loop,
246
+ index,
247
+ count,
248
+ goTo,
249
+ goPrev,
250
+ goNext,
251
+ registerItem,
252
+ setContentEl,
253
+ ],
254
+ );
255
+
256
+ return (
257
+ <CarouselContext.Provider value={value}>
258
+ <div
259
+ ref={ref}
260
+ className={cn(carousel, className)}
261
+ data-orientation={orientation}
262
+ role="region"
263
+ aria-roledescription="carousel"
264
+ onKeyDown={onKeyDown}
265
+ {...props}
266
+ >
267
+ {children}
268
+ </div>
269
+ </CarouselContext.Provider>
270
+ );
271
+ },
272
+ );
273
+ Carousel.displayName = "Carousel";
274
+
275
+ export const CarouselContent = React.forwardRef<
276
+ HTMLDivElement,
277
+ React.HTMLAttributes<HTMLDivElement>
278
+ >(({ className, ...props }, ref) => {
279
+ const { orientation, setContentEl } = useCarousel();
280
+
281
+ const mergedRef = React.useCallback(
282
+ (el: HTMLDivElement | null) => {
283
+ setContentEl(el);
284
+ if (typeof ref === "function") ref(el);
285
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
286
+ },
287
+ [ref, setContentEl],
288
+ );
289
+
290
+ return (
291
+ <div
292
+ ref={mergedRef}
293
+ className={cn(carousel__content, className)}
294
+ data-orientation={orientation}
295
+ {...props}
296
+ />
297
+ );
298
+ });
299
+ CarouselContent.displayName = "CarouselContent";
300
+
301
+ export const CarouselItem = React.forwardRef<
302
+ HTMLDivElement,
303
+ React.HTMLAttributes<HTMLDivElement>
304
+ >(({ className, ...props }, ref) => {
305
+ const { orientation, registerItem } = useCarousel();
306
+
307
+ React.useEffect(() => registerItem(), [registerItem]);
308
+
309
+ return (
310
+ <div
311
+ ref={ref}
312
+ role="group"
313
+ aria-roledescription="slide"
314
+ className={cn(carousel__item, className)}
315
+ data-orientation={orientation}
316
+ {...props}
317
+ />
318
+ );
319
+ });
320
+ CarouselItem.displayName = "CarouselItem";
321
+
322
+ export const CarouselPrevious = React.forwardRef<
323
+ HTMLButtonElement,
324
+ React.ButtonHTMLAttributes<HTMLButtonElement>
325
+ >(({ className, onClick, disabled, children, ...props }, ref) => {
326
+ const { goPrev, orientation, index, count, loop } = useCarousel();
327
+ const atStart = !loop && index <= 0;
328
+ return (
329
+ <button
330
+ ref={ref}
331
+ type="button"
332
+ aria-label="이전"
333
+ className={cn(carousel__nav, carouselNavPrev, className)}
334
+ data-orientation={orientation}
335
+ disabled={disabled || atStart || count === 0}
336
+ onClick={(e) => {
337
+ onClick?.(e);
338
+ if (!e.defaultPrevented) goPrev();
339
+ }}
340
+ {...props}
341
+ >
342
+ {children ?? (
343
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
344
+ <path
345
+ d={orientation === "horizontal" ? "M10 4l-4 4 4 4" : "M4 10l4-4 4 4"}
346
+ stroke="currentColor"
347
+ strokeWidth="1.5"
348
+ strokeLinecap="round"
349
+ strokeLinejoin="round"
350
+ />
351
+ </svg>
352
+ )}
353
+ </button>
354
+ );
355
+ });
356
+ CarouselPrevious.displayName = "CarouselPrevious";
357
+
358
+ export const CarouselNext = React.forwardRef<
359
+ HTMLButtonElement,
360
+ React.ButtonHTMLAttributes<HTMLButtonElement>
361
+ >(({ className, onClick, disabled, children, ...props }, ref) => {
362
+ const { goNext, orientation, index, count, loop } = useCarousel();
363
+ const atEnd = !loop && index >= count - 1;
364
+ return (
365
+ <button
366
+ ref={ref}
367
+ type="button"
368
+ aria-label="다음"
369
+ className={cn(carousel__nav, carouselNavNext, className)}
370
+ data-orientation={orientation}
371
+ disabled={disabled || atEnd || count === 0}
372
+ onClick={(e) => {
373
+ onClick?.(e);
374
+ if (!e.defaultPrevented) goNext();
375
+ }}
376
+ {...props}
377
+ >
378
+ {children ?? (
379
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
380
+ <path
381
+ d={orientation === "horizontal" ? "M6 4l4 4-4 4" : "M4 6l4 4 4-4"}
382
+ stroke="currentColor"
383
+ strokeWidth="1.5"
384
+ strokeLinecap="round"
385
+ strokeLinejoin="round"
386
+ />
387
+ </svg>
388
+ )}
389
+ </button>
390
+ );
391
+ });
392
+ CarouselNext.displayName = "CarouselNext";
393
+
394
+ export interface CarouselIndicatorsProps
395
+ extends React.HTMLAttributes<HTMLDivElement> {
396
+ /** 각 도트 버튼에 전달할 aria-label 빌더. */
397
+ labelFor?: (index: number) => string;
398
+ }
399
+
400
+ export const CarouselIndicators = React.forwardRef<
401
+ HTMLDivElement,
402
+ CarouselIndicatorsProps
403
+ >(({ className, labelFor, ...props }, ref) => {
404
+ const { count, index, goTo, orientation } = useCarousel();
405
+ if (count <= 0) return null;
406
+ return (
407
+ <div
408
+ ref={ref}
409
+ role="tablist"
410
+ aria-label="슬라이드 선택"
411
+ className={cn(carousel__indicators, className)}
412
+ data-orientation={orientation}
413
+ {...props}
414
+ >
415
+ {Array.from({ length: count }).map((_, i) => (
416
+ <button
417
+ key={i}
418
+ type="button"
419
+ role="tab"
420
+ aria-selected={i === index}
421
+ aria-label={labelFor ? labelFor(i) : `${i + 1}번 슬라이드`}
422
+ className={carousel__indicator}
423
+ data-active={i === index || undefined}
424
+ onClick={() => goTo(i)}
425
+ />
426
+ ))}
427
+ </div>
428
+ );
429
+ });
430
+ CarouselIndicators.displayName = "CarouselIndicators";
@@ -0,0 +1,169 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const carousel = style({
4
+ position: "relative",
5
+ width: "100%",
6
+ });
7
+
8
+ export const carousel__content = style({
9
+ display: "flex",
10
+ gap: "var(--space-4)",
11
+ overflowX: "auto",
12
+ overflowY: "hidden",
13
+ scrollSnapType: "x mandatory",
14
+ scrollBehavior: "smooth",
15
+ scrollbarWidth: "none",
16
+ MsOverflowStyle: "none",
17
+ WebkitOverflowScrolling: "touch",
18
+ overscrollBehaviorInline: "contain",
19
+ selectors: {
20
+ "&::-webkit-scrollbar": {
21
+ display: "none",
22
+ },
23
+ "&[data-orientation="vertical"]": {
24
+ flexDirection: "column",
25
+ overflowX: "hidden",
26
+ overflowY: "auto",
27
+ scrollSnapType: "y mandatory",
28
+ height: "20rem",
29
+ },
30
+ },
31
+ "@media": {
32
+ "(prefers-reduced-motion: reduce)": {
33
+ scrollBehavior: "auto",
34
+ },
35
+ },
36
+ });
37
+
38
+ export const carousel__item = style({
39
+ flex: "0 0 100%",
40
+ minWidth: 0,
41
+ scrollSnapAlign: "start",
42
+ scrollSnapStop: "always",
43
+ selectors: {
44
+ "&[data-orientation="vertical"]": {
45
+ flexBasis: "auto",
46
+ },
47
+ },
48
+ });
49
+
50
+ export const carousel__nav = style({
51
+ position: "absolute",
52
+ top: "50%",
53
+ width: "2rem",
54
+ height: "2rem",
55
+ display: "inline-flex",
56
+ alignItems: "center",
57
+ justifyContent: "center",
58
+ background: "var(--background)",
59
+ color: "var(--foreground)",
60
+ border: "1px solid var(--border)",
61
+ borderRadius: "999px",
62
+ cursor: "pointer",
63
+ transform: "translateY(-50%)",
64
+ zIndex: 1,
65
+ transition: "opacity var(--duration-fast) ease,\n background var(--duration-fast) ease",
66
+ selectors: {
67
+ "&:hover:not(:disabled)": {
68
+ background: "var(--background-muted)",
69
+ },
70
+ "&:focus-visible": {
71
+ outline: "var(--border-width-strong) solid var(--foreground)",
72
+ outlineOffset: "2px",
73
+ },
74
+ "&:disabled": {
75
+ opacity: 0.4,
76
+ cursor: "not-allowed",
77
+ },
78
+ "&[data-orientation="vertical"]": {
79
+ top: "auto",
80
+ left: "50%",
81
+ transform: "translateX(-50%)",
82
+ },
83
+ },
84
+ "@media": {
85
+ "(prefers-reduced-motion: reduce)": {
86
+ transition: "none",
87
+ },
88
+ },
89
+ });
90
+
91
+ export const carouselNavPrev = style({
92
+ left: "-1rem",
93
+ selectors: {
94
+ "&[data-orientation="vertical"]": {
95
+ top: "-1rem",
96
+ left: "50%",
97
+ },
98
+ },
99
+ });
100
+
101
+ export const carouselNavNext = style({
102
+ right: "-1rem",
103
+ selectors: {
104
+ "&[data-orientation="vertical"]": {
105
+ bottom: "-1rem",
106
+ top: "auto",
107
+ left: "50%",
108
+ },
109
+ },
110
+ });
111
+
112
+ export const carousel__indicators = style({
113
+ display: "flex",
114
+ justifyContent: "center",
115
+ alignItems: "center",
116
+ gap: "var(--space-2)",
117
+ marginTop: "var(--space-3)",
118
+ selectors: {
119
+ "&[data-orientation="vertical"]": {
120
+ position: "absolute",
121
+ top: "50%",
122
+ right: "0.5rem",
123
+ marginTop: 0,
124
+ flexDirection: "column",
125
+ transform: "translateY(-50%)",
126
+ },
127
+ },
128
+ });
129
+
130
+ export const carousel__indicator = style({
131
+ width: "0.5rem",
132
+ height: "0.5rem",
133
+ padding: 0,
134
+ background: "var(--border)",
135
+ border: "none",
136
+ borderRadius: "999px",
137
+ cursor: "pointer",
138
+ transition: "background var(--duration-fast) ease,\n transform var(--duration-fast) ease",
139
+ selectors: {
140
+ "&:hover": {
141
+ background: "var(--border-strong)",
142
+ },
143
+ "&[data-active]": {
144
+ background: "var(--foreground)",
145
+ transform: "scale(1.2)",
146
+ },
147
+ "&:focus-visible": {
148
+ outline: "var(--border-width-strong) solid var(--foreground)",
149
+ outlineOffset: "2px",
150
+ },
151
+ },
152
+ "@media": {
153
+ "(prefers-reduced-motion: reduce)": {
154
+ transition: "none",
155
+ },
156
+ },
157
+ });
158
+
159
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
160
+ export const byKey: Record<string, string> = {
161
+ "carousel": carousel,
162
+ "carousel__content": carousel__content,
163
+ "carousel__item": carousel__item,
164
+ "carousel__nav": carousel__nav,
165
+ "carousel__nav--prev": carouselNavPrev,
166
+ "carousel__nav--next": carouselNavNext,
167
+ "carousel__indicators": carousel__indicators,
168
+ "carousel__indicator": carousel__indicator,
169
+ };
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+ import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
3
+ import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group";
4
+ import { byKey, checkbox, checkbox__indicator, checkboxGroup } from "./styles.css";
5
+
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ /* ───────────── Checkbox ───────────── */
9
+
10
+ export type CheckboxProps = Omit<
11
+ React.ComponentPropsWithoutRef<typeof BaseCheckbox.Root>,
12
+ "className"
13
+ > & {
14
+ className?: string;
15
+ };
16
+
17
+ /**
18
+ * 폼 제출 시 함께 적용되는 다중 선택. `indeterminate`로 부분 선택 상태를
19
+ * 표현할 수 있고, 여러 개를 묶을 때는 `CheckboxGroup`으로 감싸 그룹 단위 상태를 관리한다.
20
+ */
21
+ export const Checkbox = React.forwardRef<HTMLElement, CheckboxProps>(
22
+ ({ className, ...props }, ref) => (
23
+ <BaseCheckbox.Root
24
+ ref={ref}
25
+ className={cn(checkbox, className)}
26
+ {...props}
27
+ >
28
+ <BaseCheckbox.Indicator className={checkbox__indicator}>
29
+ {props.indeterminate ? <MinusIcon /> : <CheckIcon />}
30
+ </BaseCheckbox.Indicator>
31
+ </BaseCheckbox.Root>
32
+ ),
33
+ );
34
+ Checkbox.displayName = "Checkbox";
35
+
36
+ /* ───────────── CheckboxGroup ───────────── */
37
+
38
+ export type CheckboxGroupProps = Omit<
39
+ React.ComponentPropsWithoutRef<typeof BaseCheckboxGroup>,
40
+ "className"
41
+ > & {
42
+ className?: string;
43
+ /**
44
+ * 그룹 내 체크박스 배치 방향.
45
+ * - `vertical` — 세로 나열 (기본)
46
+ * - `horizontal` — 가로 나열. 짧은 라벨 2~3개에만 권장
47
+ *
48
+ * @default "vertical"
49
+ */
50
+ orientation?: "horizontal" | "vertical";
51
+ };
52
+
53
+ /**
54
+ * 여러 Checkbox를 묶는 컨테이너. `value`/`onValueChange`로 선택된 값 배열을 관리하고,
55
+ * `orientation`으로 가로/세로 배치를 정한다. 그룹 라벨은 외부 `<Label>`로 제공할 것.
56
+ */
57
+ export const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
58
+ ({ className, orientation = "vertical", ...props }, ref) => (
59
+ <BaseCheckboxGroup
60
+ ref={ref}
61
+ className={cn(checkboxGroup, className)}
62
+ data-orientation={orientation}
63
+ {...props}
64
+ />
65
+ ),
66
+ );
67
+ CheckboxGroup.displayName = "CheckboxGroup";
68
+
69
+ /* ───────────── Icons ───────────── */
70
+
71
+ function CheckIcon() {
72
+ return (
73
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
74
+ <path
75
+ d="M3.5 8.5l3 3 6-7"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ />
81
+ </svg>
82
+ );
83
+ }
84
+
85
+ function MinusIcon() {
86
+ return (
87
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
88
+ <path
89
+ d="M4 8h8"
90
+ stroke="currentColor"
91
+ strokeWidth="2"
92
+ strokeLinecap="round"
93
+ />
94
+ </svg>
95
+ );
96
+ }