sh-ui-cli 0.52.1 → 0.52.3

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 (88) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/_smoke/vanilla-extract.test.ts +33 -0
  3. package/data/registry/react/components/input/styles.css.ts +6 -6
  4. package/data/registry/react/registry.json +35 -852
  5. package/package.json +1 -1
  6. package/src/api.d.ts +3 -4
  7. package/src/constants.js +9 -5
  8. package/src/mcp.mjs +0 -1
  9. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
  10. package/data/registry/react/components/accordion/styles.css.ts +0 -131
  11. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
  12. package/data/registry/react/components/avatar/styles.css.ts +0 -68
  13. package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
  14. package/data/registry/react/components/badge/styles.css.ts +0 -71
  15. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
  16. package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
  17. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
  18. package/data/registry/react/components/calendar/styles.css.ts +0 -250
  19. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
  20. package/data/registry/react/components/carousel/styles.css.ts +0 -169
  21. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
  22. package/data/registry/react/components/checkbox/styles.css.ts +0 -74
  23. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
  24. package/data/registry/react/components/code-editor/styles.css.ts +0 -97
  25. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
  26. package/data/registry/react/components/code-panel/styles.css.ts +0 -151
  27. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
  28. package/data/registry/react/components/color-picker/styles.css.ts +0 -169
  29. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
  30. package/data/registry/react/components/combobox/styles.css.ts +0 -174
  31. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
  32. package/data/registry/react/components/context-menu/styles.css.ts +0 -167
  33. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
  34. package/data/registry/react/components/date-picker/styles.css.ts +0 -111
  35. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
  36. package/data/registry/react/components/dialog/styles.css.ts +0 -140
  37. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
  38. package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
  39. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
  40. package/data/registry/react/components/file-upload/styles.css.ts +0 -193
  41. package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
  42. package/data/registry/react/components/form/styles.css.ts +0 -56
  43. package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
  44. package/data/registry/react/components/header/styles.css.ts +0 -413
  45. package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
  46. package/data/registry/react/components/label/styles.css.ts +0 -141
  47. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
  48. package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
  49. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
  50. package/data/registry/react/components/menubar/styles.css.ts +0 -53
  51. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
  52. package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
  53. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
  54. package/data/registry/react/components/page-toc/styles.css.ts +0 -97
  55. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
  56. package/data/registry/react/components/pagination/styles.css.ts +0 -113
  57. package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
  58. package/data/registry/react/components/popover/styles.css.ts +0 -78
  59. package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
  60. package/data/registry/react/components/progress/styles.css.ts +0 -53
  61. package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
  62. package/data/registry/react/components/radio/styles.css.ts +0 -79
  63. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
  64. package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
  65. package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
  66. package/data/registry/react/components/select/styles.css.ts +0 -225
  67. package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
  68. package/data/registry/react/components/separator/styles.css.ts +0 -24
  69. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
  70. package/data/registry/react/components/sidebar/styles.css.ts +0 -578
  71. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
  72. package/data/registry/react/components/skeleton/styles.css.ts +0 -30
  73. package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
  74. package/data/registry/react/components/slider/styles.css.ts +0 -75
  75. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
  76. package/data/registry/react/components/spinner/styles.css.ts +0 -60
  77. package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
  78. package/data/registry/react/components/switch/styles.css.ts +0 -87
  79. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
  80. package/data/registry/react/components/tabs/styles.css.ts +0 -145
  81. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
  82. package/data/registry/react/components/textarea/styles.css.ts +0 -55
  83. package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
  84. package/data/registry/react/components/toast/styles.css.ts +0 -307
  85. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
  86. package/data/registry/react/components/toggle/styles.css.ts +0 -109
  87. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
  88. package/data/registry/react/components/tooltip/styles.css.ts +0 -59
@@ -1,250 +0,0 @@
1
- import { style } from "@vanilla-extract/css";
2
-
3
- export const calendar = style({
4
- display: "inline-flex",
5
- gap: "var(--space-4)",
6
- userSelect: "none",
7
- });
8
-
9
- export const calendarMulti = style({
10
- flexWrap: "wrap",
11
- });
12
-
13
- export const calendar__month = style({
14
- width: "17.5rem",
15
- });
16
-
17
- export const calendar__header = style({
18
- display: "flex",
19
- alignItems: "center",
20
- justifyContent: "space-between",
21
- gap: "var(--space-1)",
22
- marginBottom: "var(--space-2)",
23
- });
24
-
25
- export const calendar__title = style({
26
- display: "inline-flex",
27
- alignItems: "center",
28
- gap: "var(--space-1)",
29
- flex: "1 1 auto",
30
- justifyContent: "center",
31
- });
32
-
33
- export const calendar__nav = style({
34
- display: "inline-flex",
35
- alignItems: "center",
36
- justifyContent: "center",
37
- width: "1.75rem",
38
- height: "1.75rem",
39
- padding: 0,
40
- border: "none",
41
- borderRadius: "calc(var(--radius) - 2px)",
42
- background: "transparent",
43
- color: "var(--foreground-muted)",
44
- cursor: "pointer",
45
- flexShrink: 0,
46
- transition: "background-color var(--duration-fast), color var(--duration-fast)",
47
- selectors: {
48
- "&:hover:not(:disabled)": {
49
- background: "var(--background-muted)",
50
- color: "var(--foreground)",
51
- },
52
- "&:focus-visible": {
53
- outline: "var(--border-width-strong) solid var(--foreground)",
54
- outlineOffset: "2px",
55
- },
56
- },
57
- });
58
-
59
- export const calendarNavPlaceholder = style({
60
- visibility: "hidden",
61
- pointerEvents: "none",
62
- });
63
-
64
- export const calendarSelectTrigger = style({
65
- selectors: {
66
- "&.select__trigger": {
67
- minWidth: 0,
68
- height: "1.75rem",
69
- gap: "var(--space-1)",
70
- padding: "0 var(--space-2)",
71
- background: "transparent",
72
- borderColor: "transparent",
73
- fontWeight: "var(--weight-semibold)",
74
- fontSize: "var(--text-sm)",
75
- color: "var(--foreground)",
76
- },
77
- "&.select__trigger:hover:not(:disabled)": {
78
- background: "var(--background-muted)",
79
- borderColor: "transparent",
80
- },
81
- "&.select__trigger[data-popup-open]": {
82
- background: "var(--background-muted)",
83
- borderColor: "transparent",
84
- },
85
- },
86
- });
87
-
88
- export const select__positioner = style({
89
- selectors: {
90
- [`&:has(${calendarSelectPopup})`]: {
91
- zIndex: "var(--z-popover)",
92
- },
93
- },
94
- });
95
-
96
- export const calendar__weekdays = style({
97
- display: "grid",
98
- gridTemplateColumns: "repeat(7, 1fr)",
99
- marginBottom: "var(--space-1)",
100
- });
101
-
102
- export const calendar__weekday = style({
103
- display: "flex",
104
- alignItems: "center",
105
- justifyContent: "center",
106
- height: "2rem",
107
- fontSize: "var(--text-xs)",
108
- fontWeight: "var(--weight-medium)",
109
- color: "var(--foreground-muted)",
110
- });
111
-
112
- export const calendar__grid = style({
113
- display: "grid",
114
- gridTemplateColumns: "repeat(7, 1fr)",
115
- outline: "none",
116
- selectors: {
117
- "&:focus-visible": {
118
- outline: "var(--border-width-strong) solid var(--foreground)",
119
- outlineOffset: "2px",
120
- borderRadius: "calc(var(--radius) - 2px)",
121
- },
122
- },
123
- });
124
-
125
- export const calendar__cell = style({
126
- display: "flex",
127
- alignItems: "center",
128
- justifyContent: "center",
129
- width: "100%",
130
- height: "2.375rem",
131
- minWidth: 0,
132
- });
133
-
134
- export const calendarCellInRange = style({
135
- background: "color-mix(in srgb, var(--primary) 12%, transparent)",
136
- });
137
-
138
- export const calendarCellRangeStart = style({
139
- background: "color-mix(in srgb, var(--primary) 12%, transparent)",
140
- selectors: {
141
- [`&:not(${calendarCellRangeEnd})`]: {
142
- borderRadius: "calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px)",
143
- },
144
- [`&${calendarCellRangeEnd}`]: {
145
- borderRadius: "calc(var(--radius) - 2px)",
146
- },
147
- },
148
- });
149
-
150
- export const calendarCellRangeEnd = style({
151
- background: "color-mix(in srgb, var(--primary) 12%, transparent)",
152
- selectors: {
153
- [`&:not(${calendarCellRangeStart})`]: {
154
- borderRadius: "0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0",
155
- },
156
- },
157
- });
158
-
159
- export const calendar__day = style({
160
- display: "flex",
161
- alignItems: "center",
162
- justifyContent: "center",
163
- width: "2.25rem",
164
- height: "2.25rem",
165
- padding: 0,
166
- border: "none",
167
- borderRadius: "calc(var(--radius) - 2px)",
168
- background: "transparent",
169
- color: "var(--foreground)",
170
- fontSize: "0.8125rem",
171
- fontFamily: "inherit",
172
- cursor: "pointer",
173
- transition: "background-color var(--duration-fast), color var(--duration-fast)",
174
- selectors: {
175
- "&:hover:not(:disabled)": {
176
- background: "var(--background-muted)",
177
- },
178
- "&:focus-visible": {
179
- outline: "var(--border-width-strong) solid var(--foreground)",
180
- outlineOffset: "2px",
181
- },
182
- "&:disabled": {
183
- opacity: 0.3,
184
- cursor: "not-allowed",
185
- },
186
- },
187
- });
188
-
189
- export const calendarDayOutside = style({
190
- color: "var(--foreground-subtle, var(--foreground-muted))",
191
- opacity: 0.4,
192
- });
193
-
194
- export const calendarDayToday = style({
195
- fontWeight: "var(--weight-bold)",
196
- textDecoration: "underline",
197
- textUnderlineOffset: "0.125rem",
198
- });
199
-
200
- export const calendarDaySelected = style({
201
- background: "var(--primary)",
202
- color: "var(--primary-foreground)",
203
- fontWeight: "var(--weight-semibold)",
204
- selectors: {
205
- "&:hover:not(:disabled)": {
206
- background: "var(--primary-hover)",
207
- color: "var(--primary-foreground)",
208
- },
209
- },
210
- });
211
-
212
- export const calendarSelectValue = style({
213
- });
214
-
215
- export const calendarSelectPopup = style({
216
- });
217
-
218
- export const calendarGridWrap = style({
219
- });
220
-
221
- export const calendarCellHidden = style({
222
- });
223
-
224
- /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
225
- export const byKey: Record<string, string> = {
226
- "calendar": calendar,
227
- "calendar--multi": calendarMulti,
228
- "calendar__month": calendar__month,
229
- "calendar__header": calendar__header,
230
- "calendar__title": calendar__title,
231
- "calendar__nav": calendar__nav,
232
- "calendar__nav--placeholder": calendarNavPlaceholder,
233
- "calendar__select-trigger": calendarSelectTrigger,
234
- "select__positioner": select__positioner,
235
- "calendar__weekdays": calendar__weekdays,
236
- "calendar__weekday": calendar__weekday,
237
- "calendar__grid": calendar__grid,
238
- "calendar__cell": calendar__cell,
239
- "calendar__cell--in-range": calendarCellInRange,
240
- "calendar__cell--range-start": calendarCellRangeStart,
241
- "calendar__cell--range-end": calendarCellRangeEnd,
242
- "calendar__day": calendar__day,
243
- "calendar__day--outside": calendarDayOutside,
244
- "calendar__day--today": calendarDayToday,
245
- "calendar__day--selected": calendarDaySelected,
246
- "calendar__select-value": calendarSelectValue,
247
- "calendar__select-popup": calendarSelectPopup,
248
- "calendar__grid-wrap": calendarGridWrap,
249
- "calendar__cell--hidden": calendarCellHidden,
250
- };
@@ -1,430 +0,0 @@
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";