sh-ui-cli 0.52.0 → 0.52.2

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 (89) hide show
  1. package/data/changelog/versions.json +25 -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 +2 -2
  6. package/src/api.d.ts +3 -4
  7. package/src/constants.js +9 -5
  8. package/src/create/plugins/pluginSchema.js +5 -3
  9. package/src/mcp.mjs +4 -3
  10. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
  11. package/data/registry/react/components/accordion/styles.css.ts +0 -131
  12. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
  13. package/data/registry/react/components/avatar/styles.css.ts +0 -68
  14. package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
  15. package/data/registry/react/components/badge/styles.css.ts +0 -71
  16. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
  17. package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
  18. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
  19. package/data/registry/react/components/calendar/styles.css.ts +0 -250
  20. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
  21. package/data/registry/react/components/carousel/styles.css.ts +0 -169
  22. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
  23. package/data/registry/react/components/checkbox/styles.css.ts +0 -74
  24. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
  25. package/data/registry/react/components/code-editor/styles.css.ts +0 -97
  26. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
  27. package/data/registry/react/components/code-panel/styles.css.ts +0 -151
  28. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
  29. package/data/registry/react/components/color-picker/styles.css.ts +0 -169
  30. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
  31. package/data/registry/react/components/combobox/styles.css.ts +0 -174
  32. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
  33. package/data/registry/react/components/context-menu/styles.css.ts +0 -167
  34. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
  35. package/data/registry/react/components/date-picker/styles.css.ts +0 -111
  36. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
  37. package/data/registry/react/components/dialog/styles.css.ts +0 -140
  38. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
  39. package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
  40. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
  41. package/data/registry/react/components/file-upload/styles.css.ts +0 -193
  42. package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
  43. package/data/registry/react/components/form/styles.css.ts +0 -56
  44. package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
  45. package/data/registry/react/components/header/styles.css.ts +0 -413
  46. package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
  47. package/data/registry/react/components/label/styles.css.ts +0 -141
  48. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
  49. package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
  50. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
  51. package/data/registry/react/components/menubar/styles.css.ts +0 -53
  52. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
  53. package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
  54. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
  55. package/data/registry/react/components/page-toc/styles.css.ts +0 -97
  56. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
  57. package/data/registry/react/components/pagination/styles.css.ts +0 -113
  58. package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
  59. package/data/registry/react/components/popover/styles.css.ts +0 -78
  60. package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
  61. package/data/registry/react/components/progress/styles.css.ts +0 -53
  62. package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
  63. package/data/registry/react/components/radio/styles.css.ts +0 -79
  64. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
  65. package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
  66. package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
  67. package/data/registry/react/components/select/styles.css.ts +0 -225
  68. package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
  69. package/data/registry/react/components/separator/styles.css.ts +0 -24
  70. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
  71. package/data/registry/react/components/sidebar/styles.css.ts +0 -578
  72. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
  73. package/data/registry/react/components/skeleton/styles.css.ts +0 -30
  74. package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
  75. package/data/registry/react/components/slider/styles.css.ts +0 -75
  76. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
  77. package/data/registry/react/components/spinner/styles.css.ts +0 -60
  78. package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
  79. package/data/registry/react/components/switch/styles.css.ts +0 -87
  80. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
  81. package/data/registry/react/components/tabs/styles.css.ts +0 -145
  82. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
  83. package/data/registry/react/components/textarea/styles.css.ts +0 -55
  84. package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
  85. package/data/registry/react/components/toast/styles.css.ts +0 -307
  86. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
  87. package/data/registry/react/components/toggle/styles.css.ts +0 -109
  88. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
  89. package/data/registry/react/components/tooltip/styles.css.ts +0 -59
@@ -1,298 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { byKey, slider, sliderDisabled, slider__track, slider__range, slider__thumb } from "./styles.css";
5
-
6
-
7
- import { cn } from "@SH_UI_UTILS@";
8
- function clamp(n: number, min: number, max: number) {
9
- return Math.min(max, Math.max(min, n));
10
- }
11
-
12
- function snap(value: number, step: number, min: number) {
13
- if (step <= 0) return value;
14
- return min + Math.round((value - min) / step) * step;
15
- }
16
-
17
- interface SliderContextValue {
18
- value: number;
19
- setValue: (next: number) => void;
20
- min: number;
21
- max: number;
22
- step: number;
23
- disabled: boolean;
24
- ariaLabel?: string;
25
- trackRef: React.RefObject<HTMLDivElement | null>;
26
- setTrackEl: (el: HTMLDivElement | null) => void;
27
- percent: string;
28
- }
29
-
30
- const SliderContext = React.createContext<SliderContextValue | null>(null);
31
-
32
- function useSliderContext(): SliderContextValue {
33
- const ctx = React.useContext(SliderContext);
34
- if (!ctx) {
35
- throw new Error("Slider 하위 컴포넌트는 <Slider> 안에서만 사용할 수 있습니다.");
36
- }
37
- return ctx;
38
- }
39
-
40
- /** 현재 Slider 상태를 읽을 수 있는 hook. 외부 라벨·커스텀 컨트롤에 사용. */
41
- export function useSliderState(): Pick<
42
- SliderContextValue,
43
- "value" | "min" | "max" | "step" | "disabled"
44
- > {
45
- const { value, min, max, step, disabled } = useSliderContext();
46
- return { value, min, max, step, disabled };
47
- }
48
-
49
- export interface SliderProps
50
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
51
- value?: number;
52
- defaultValue?: number;
53
- onValueChange?: (value: number) => void;
54
- min?: number;
55
- max?: number;
56
- step?: number;
57
- disabled?: boolean;
58
- /** 접근성: aria-label. Thumb으로 전달됨. */
59
- "aria-label"?: string;
60
- }
61
-
62
- /**
63
- * 값 범위 안에서 단일 값을 선택. 마우스/터치 드래그 + 키보드(화살표/Home/End/PageUp·Down) 지원.
64
- *
65
- * 기본 렌더(자식 생략 시) — `<SliderTrack><SliderRange /><SliderThumb /></SliderTrack>`.
66
- * 커스텀 레이아웃이 필요하면 하위 컴포넌트를 직접 조합한다.
67
- */
68
- export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
69
- function Slider(
70
- {
71
- value: valueProp,
72
- defaultValue = 0,
73
- onValueChange,
74
- min = 0,
75
- max = 100,
76
- step = 1,
77
- disabled = false,
78
- className,
79
- children,
80
- "aria-label": ariaLabel,
81
- ...rest
82
- },
83
- ref,
84
- ) {
85
- const isControlled = valueProp !== undefined;
86
- const [internal, setInternal] = React.useState(defaultValue);
87
- const rawValue = isControlled ? (valueProp as number) : internal;
88
- const value = clamp(rawValue, min, max);
89
-
90
- const trackRef = React.useRef<HTMLDivElement | null>(null);
91
- const setTrackEl = React.useCallback((el: HTMLDivElement | null) => {
92
- trackRef.current = el;
93
- }, []);
94
-
95
- const setValue = React.useCallback(
96
- (next: number) => {
97
- const snapped = clamp(snap(next, step, min), min, max);
98
- if (snapped === value) return;
99
- if (!isControlled) setInternal(snapped);
100
- onValueChange?.(snapped);
101
- },
102
- [isControlled, max, min, onValueChange, step, value],
103
- );
104
-
105
- const ratio = max === min ? 0 : (value - min) / (max - min);
106
- const percent = `${ratio * 100}%`;
107
-
108
- const ctxValue = React.useMemo<SliderContextValue>(
109
- () => ({
110
- value,
111
- setValue,
112
- min,
113
- max,
114
- step,
115
- disabled,
116
- ariaLabel,
117
- trackRef,
118
- setTrackEl,
119
- percent,
120
- }),
121
- [ariaLabel, disabled, max, min, percent, setTrackEl, setValue, step, value],
122
- );
123
-
124
- return (
125
- <SliderContext.Provider value={ctxValue}>
126
- <div
127
- ref={ref}
128
- {...rest}
129
- className={cn(
130
- slider,
131
- disabled && sliderDisabled,
132
- className,
133
- )}
134
- data-disabled={disabled || undefined}
135
- >
136
- {children ?? (
137
- <SliderTrack>
138
- <SliderRange />
139
- <SliderThumb />
140
- </SliderTrack>
141
- )}
142
- </div>
143
- </SliderContext.Provider>
144
- );
145
- },
146
- );
147
- Slider.displayName = "Slider";
148
-
149
- /* ───────── SliderTrack ─────────
150
- * 클릭/드래그 수신을 담당. 내부에 SliderRange + SliderThumb을 자식으로 조합.
151
- */
152
- export const SliderTrack = React.forwardRef<
153
- HTMLDivElement,
154
- React.HTMLAttributes<HTMLDivElement>
155
- >(function SliderTrack(
156
- { className, onPointerDown: userOnPointerDown, children, ...props },
157
- ref,
158
- ) {
159
- const { disabled, setValue, min, max, setTrackEl, trackRef } = useSliderContext();
160
-
161
- const mergedRef = React.useCallback(
162
- (el: HTMLDivElement | null) => {
163
- setTrackEl(el);
164
- if (typeof ref === "function") ref(el);
165
- else if (ref)
166
- (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
167
- },
168
- [ref, setTrackEl],
169
- );
170
-
171
- const moveToClient = (clientX: number) => {
172
- const el = trackRef.current;
173
- if (!el) return;
174
- const r = el.getBoundingClientRect();
175
- const ratio = clamp((clientX - r.left) / r.width, 0, 1);
176
- setValue(min + ratio * (max - min));
177
- };
178
-
179
- const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
180
- userOnPointerDown?.(e);
181
- if (e.defaultPrevented || disabled) return;
182
- const el = trackRef.current;
183
- if (!el) return;
184
- el.setPointerCapture(e.pointerId);
185
- moveToClient(e.clientX);
186
-
187
- const onMove = (ev: PointerEvent) => moveToClient(ev.clientX);
188
- const onUp = (ev: PointerEvent) => {
189
- el.releasePointerCapture(ev.pointerId);
190
- el.removeEventListener("pointermove", onMove);
191
- el.removeEventListener("pointerup", onUp);
192
- };
193
- el.addEventListener("pointermove", onMove);
194
- el.addEventListener("pointerup", onUp);
195
- };
196
-
197
- return (
198
- <div
199
- ref={mergedRef}
200
- className={cn(slider__track, className)}
201
- onPointerDown={onPointerDown}
202
- {...props}
203
- >
204
- {children ?? (
205
- <>
206
- <SliderRange />
207
- <SliderThumb />
208
- </>
209
- )}
210
- </div>
211
- );
212
- });
213
- SliderTrack.displayName = "SliderTrack";
214
-
215
- /* ───────── SliderRange ─────────
216
- * 선택된 값까지의 진행 바. 넓이는 Context의 percent로 계산.
217
- */
218
- export const SliderRange = React.forwardRef<
219
- HTMLDivElement,
220
- React.HTMLAttributes<HTMLDivElement>
221
- >(function SliderRange({ className, style, ...props }, ref) {
222
- const { percent } = useSliderContext();
223
- return (
224
- <div
225
- ref={ref}
226
- className={cn(slider__range, className)}
227
- style={{ width: percent, ...style }}
228
- {...props}
229
- />
230
- );
231
- });
232
- SliderRange.displayName = "SliderRange";
233
-
234
- /* ───────── SliderThumb ─────────
235
- * 키보드 포커스 및 조작 수신. aria-valuenow 등 접근성 속성을 담는다.
236
- */
237
- export const SliderThumb = React.forwardRef<
238
- HTMLDivElement,
239
- Omit<React.HTMLAttributes<HTMLDivElement>, "role" | "tabIndex">
240
- >(function SliderThumb(
241
- { className, style, onKeyDown: userOnKeyDown, ...props },
242
- ref,
243
- ) {
244
- const { value, setValue, min, max, step, disabled, ariaLabel, percent } =
245
- useSliderContext();
246
-
247
- const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
248
- userOnKeyDown?.(e);
249
- if (e.defaultPrevented || disabled) return;
250
- const big = e.shiftKey ? step * 10 : step;
251
- switch (e.key) {
252
- case "ArrowRight":
253
- case "ArrowUp":
254
- e.preventDefault();
255
- setValue(value + big);
256
- break;
257
- case "ArrowLeft":
258
- case "ArrowDown":
259
- e.preventDefault();
260
- setValue(value - big);
261
- break;
262
- case "Home":
263
- e.preventDefault();
264
- setValue(min);
265
- break;
266
- case "End":
267
- e.preventDefault();
268
- setValue(max);
269
- break;
270
- case "PageUp":
271
- e.preventDefault();
272
- setValue(value + step * 10);
273
- break;
274
- case "PageDown":
275
- e.preventDefault();
276
- setValue(value - step * 10);
277
- break;
278
- }
279
- };
280
-
281
- return (
282
- <div
283
- ref={ref}
284
- role="slider"
285
- tabIndex={disabled ? -1 : 0}
286
- aria-label={ariaLabel}
287
- aria-valuemin={min}
288
- aria-valuemax={max}
289
- aria-valuenow={value}
290
- aria-disabled={disabled || undefined}
291
- onKeyDown={onKeyDown}
292
- className={cn(slider__thumb, className)}
293
- style={{ left: percent, ...style }}
294
- {...props}
295
- />
296
- );
297
- });
298
- SliderThumb.displayName = "SliderThumb";
@@ -1,75 +0,0 @@
1
- import { style } from "@vanilla-extract/css";
2
-
3
- export const slider = style({
4
- position: "relative",
5
- width: "100%",
6
- padding: "var(--space-2) 0",
7
- userSelect: "none",
8
- WebkitUserSelect: "none",
9
- });
10
-
11
- export const sliderDisabled = style({
12
- opacity: "var(--opacity-disabled)",
13
- pointerEvents: "none",
14
- });
15
-
16
- export const slider__track = style({
17
- position: "relative",
18
- width: "100%",
19
- height: "0.375rem",
20
- background: "var(--background-muted)",
21
- borderRadius: "999px",
22
- cursor: "pointer",
23
- touchAction: "none",
24
- });
25
-
26
- export const slider__range = style({
27
- position: "absolute",
28
- top: 0,
29
- left: 0,
30
- height: "100%",
31
- background: "var(--primary)",
32
- borderRadius: "999px",
33
- pointerEvents: "none",
34
- });
35
-
36
- export const slider__thumb = style({
37
- position: "absolute",
38
- top: "50%",
39
- width: "1rem",
40
- height: "1rem",
41
- marginLeft: "-0.5rem",
42
- transform: "translateY(-50%)",
43
- background: "var(--background)",
44
- border: "2px solid var(--primary)",
45
- borderRadius: "50%",
46
- boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)",
47
- cursor: "grab",
48
- transition: "transform 80ms",
49
- selectors: {
50
- "&:active": {
51
- cursor: "grabbing",
52
- transform: "translateY(-50%) scale(1.1)",
53
- },
54
- "&:focus-visible": {
55
- outline: "var(--border-width-strong) solid var(--foreground)",
56
- outlineOffset: "2px",
57
- },
58
- },
59
- "@media": {
60
- "(hover: none) and (pointer: coarse)": {
61
- width: "1.25rem",
62
- height: "1.25rem",
63
- marginLeft: "-0.625rem",
64
- },
65
- },
66
- });
67
-
68
- /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
69
- export const byKey: Record<string, string> = {
70
- "slider": slider,
71
- "slider--disabled": sliderDisabled,
72
- "slider__track": slider__track,
73
- "slider__range": slider__range,
74
- "slider__thumb": slider__thumb,
75
- };
@@ -1,38 +0,0 @@
1
- import * as React from "react";
2
- import { byKey, spinner, spinnerSm, spinnerMd, spinnerLg, spinner__ring } from "./styles.css";
3
-
4
-
5
- import { cn } from "@SH_UI_UTILS@";
6
- export type SpinnerSize = "sm" | "md" | "lg";
7
-
8
- export interface SpinnerProps
9
- extends Omit<React.HTMLAttributes<HTMLSpanElement>, "role"> {
10
- size?: SpinnerSize;
11
- /** 접근성: 로딩 중임을 알리는 라벨. 기본 "로딩 중". */
12
- "aria-label"?: string;
13
- }
14
-
15
- /**
16
- * 짧은 비동기 작업의 로딩 표시. 200ms 이상 걸리는 요청에는 즉시 피드백을 준다는
17
- * 원칙(ui-states.md)에 맞춰 버튼·입력 등에 인라인으로 사용.
18
- */
19
- export const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
20
- function Spinner(
21
- { size = "md", className, "aria-label": ariaLabel = "로딩 중", ...props },
22
- ref,
23
- ) {
24
- return (
25
- <span
26
- ref={ref}
27
- role="status"
28
- aria-live="polite"
29
- aria-label={ariaLabel}
30
- className={cn(spinner, byKey[`spinner--${size}`], className)}
31
- {...props}
32
- >
33
- <span className={spinner__ring} aria-hidden />
34
- </span>
35
- );
36
- },
37
- );
38
- Spinner.displayName = "Spinner";
@@ -1,60 +0,0 @@
1
- import { style, keyframes } from "@vanilla-extract/css";
2
-
3
- export const shUiSpinnerRotate = keyframes({
4
- "to": {
5
- transform: "rotate(360deg)",
6
- },
7
- });
8
-
9
- export const spinner = style({
10
- display: "inline-flex",
11
- alignItems: "center",
12
- justifyContent: "center",
13
- verticalAlign: "middle",
14
- color: "currentColor",
15
- });
16
-
17
- export const spinnerSm = style({
18
- width: "0.875rem",
19
- height: "0.875rem",
20
- selectors: {
21
- [`& ${spinner__ring}`]: {
22
- borderWidth: "1.5px",
23
- },
24
- },
25
- });
26
-
27
- export const spinnerMd = style({
28
- width: "1.125rem",
29
- height: "1.125rem",
30
- });
31
-
32
- export const spinnerLg = style({
33
- width: "1.5rem",
34
- height: "1.5rem",
35
- });
36
-
37
- export const spinner__ring = style({
38
- display: "inline-block",
39
- width: "100%",
40
- height: "100%",
41
- border: "2px solid currentColor",
42
- borderRadius: "999px",
43
- borderTopColor: "transparent",
44
- opacity: 0.8,
45
- animation: "sh-ui-spinner-rotate 0.8s linear infinite",
46
- "@media": {
47
- "(prefers-reduced-motion: reduce)": {
48
- animationDuration: "3s",
49
- },
50
- },
51
- });
52
-
53
- /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
54
- export const byKey: Record<string, string> = {
55
- "spinner": spinner,
56
- "spinner--sm": spinnerSm,
57
- "spinner--md": spinnerMd,
58
- "spinner--lg": spinnerLg,
59
- "spinner__ring": spinner__ring,
60
- };
@@ -1,39 +0,0 @@
1
- import * as React from "react";
2
- import { Switch as BaseSwitch } from "@base-ui/react/switch";
3
- import { byKey, switch_, switchSm, switchMd, switch__thumb } from "./styles.css";
4
-
5
-
6
- import { cn } from "@SH_UI_UTILS@";
7
- /* ───────────── Switch ───────────── */
8
-
9
- export type SwitchProps = Omit<
10
- React.ComponentPropsWithoutRef<typeof BaseSwitch.Root>,
11
- "className"
12
- > & {
13
- className?: string;
14
- /**
15
- * 크기.
16
- * - `sm` — 조밀한 폼이나 툴바
17
- * - `md` — 일반 (기본)
18
- *
19
- * @default "md"
20
- */
21
- size?: "sm" | "md";
22
- };
23
-
24
- /**
25
- * 즉시 반영되는 on/off 토글. 변경이 즉시 적용되는 설정에 사용하고, 폼 제출과
26
- * 함께 적용되는 선택에는 Checkbox를 권장. label과 연결해 접근성 텍스트를 함께 제공할 것.
27
- */
28
- export const Switch = React.forwardRef<HTMLElement, SwitchProps>(
29
- ({ className, size = "md", ...props }, ref) => (
30
- <BaseSwitch.Root
31
- ref={ref}
32
- className={cn(switch_, byKey[`switch--${size}`], className)}
33
- {...props}
34
- >
35
- <BaseSwitch.Thumb className={switch__thumb} />
36
- </BaseSwitch.Root>
37
- ),
38
- );
39
- Switch.displayName = "Switch";
@@ -1,87 +0,0 @@
1
- import { style } from "@vanilla-extract/css";
2
-
3
- export const switch_ = style({
4
- display: "inline-flex",
5
- alignItems: "center",
6
- border: "none",
7
- borderRadius: "999px",
8
- background: "var(--background-muted)",
9
- cursor: "pointer",
10
- flexShrink: 0,
11
- padding: "0.125rem",
12
- transition: "background-color 150ms",
13
- WebkitTapHighlightColor: "transparent",
14
- selectors: {
15
- "&:hover:not([data-disabled])": {
16
- background: "var(--border-strong)",
17
- },
18
- "&:focus-visible": {
19
- outline: "var(--border-width-strong) solid var(--foreground)",
20
- outlineOffset: "2px",
21
- },
22
- "&[data-checked]": {
23
- background: "var(--primary)",
24
- },
25
- "&[data-checked]:hover:not([data-disabled])": {
26
- background: "var(--primary-hover)",
27
- },
28
- "&[data-disabled]": {
29
- opacity: "var(--opacity-disabled)",
30
- cursor: "not-allowed",
31
- },
32
- },
33
- "@media": {
34
- "(prefers-reduced-motion: reduce)": {
35
- transitionDuration: "0.01ms !important",
36
- },
37
- },
38
- });
39
-
40
- export const switchSm = style({
41
- width: "2rem",
42
- height: "1.125rem",
43
- selectors: {
44
- [`& ${switch__thumb}`]: {
45
- width: "0.875rem",
46
- height: "0.875rem",
47
- },
48
- [`&[data-checked] ${switch__thumb}`]: {
49
- transform: "translateX(0.875rem)",
50
- },
51
- },
52
- });
53
-
54
- export const switchMd = style({
55
- width: "2.5rem",
56
- height: "1.375rem",
57
- selectors: {
58
- [`& ${switch__thumb}`]: {
59
- width: "1.125rem",
60
- height: "1.125rem",
61
- },
62
- [`&[data-checked] ${switch__thumb}`]: {
63
- transform: "translateX(1.125rem)",
64
- },
65
- },
66
- });
67
-
68
- export const switch__thumb = style({
69
- display: "block",
70
- borderRadius: "999px",
71
- background: "white",
72
- boxShadow: "0 1px 2px rgba(0, 0, 0, 0.12)",
73
- transition: "transform 150ms ease-out",
74
- "@media": {
75
- "(prefers-reduced-motion: reduce)": {
76
- transitionDuration: "0.01ms !important",
77
- },
78
- },
79
- });
80
-
81
- /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
82
- export const byKey: Record<string, string> = {
83
- "switch": switch_,
84
- "switch--sm": switchSm,
85
- "switch--md": switchMd,
86
- "switch__thumb": switch__thumb,
87
- };
@@ -1,91 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { Tabs as BaseTabs } from "@base-ui/react/tabs";
5
- import { byKey, tabs, tabs__list, tabs__trigger, tabs__indicator, tabs__content } from "./styles.css";
6
-
7
-
8
- import { cn } from "@SH_UI_UTILS@";
9
- type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
10
-
11
- export type TabsVariant = "underline" | "pill" | "plain";
12
-
13
- interface TabsContextValue {
14
- variant: TabsVariant;
15
- }
16
- const TabsContext = React.createContext<TabsContextValue>({ variant: "underline" });
17
-
18
- export type TabsProps = WithStringClassName<
19
- React.ComponentPropsWithoutRef<typeof BaseTabs.Root>
20
- > & {
21
- /**
22
- * 외형 변형.
23
- * - `underline` — 활성 탭 하단 underline (기본). 일반 탭 UI
24
- * - `pill` — 활성 탭 둥근 배경. 세그먼트 컨트롤 스타일
25
- * - `plain` — 시각 강조 없음. 직접 스타일링용
26
- *
27
- * @default "underline"
28
- */
29
- variant?: TabsVariant;
30
- };
31
-
32
- /**
33
- * 한 영역에 여러 패널을 배치하고 탭으로 전환하는 컴파운드 컴포넌트. 같은 페이지의 동일 평면
34
- * 정보를 분류할 때 사용한다(라우트 분기는 라우팅으로). 자식 구조: TabsList > TabsTrigger × n, TabsContent × n.
35
- */
36
- export const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
37
- ({ className, variant = "underline", ...props }, ref) => (
38
- <TabsContext.Provider value={{ variant }}>
39
- <BaseTabs.Root
40
- ref={ref}
41
- data-variant={variant}
42
- className={cn(tabs, className)}
43
- {...props}
44
- />
45
- </TabsContext.Provider>
46
- ),
47
- );
48
- Tabs.displayName = "Tabs";
49
-
50
- /** 탭 트리거들을 묶는 컨테이너. 키보드 화살표·Home·End로 트리거 간 이동이 가능하다. */
51
- export const TabsList = React.forwardRef<
52
- HTMLDivElement,
53
- WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.List>>
54
- >(({ className, ...props }, ref) => (
55
- <BaseTabs.List ref={ref} className={cn(tabs__list, className)} {...props} />
56
- ));
57
- TabsList.displayName = "TabsList";
58
-
59
- /** 한 탭의 트리거 버튼. `value` prop으로 매칭되는 TabsContent와 연결된다. */
60
- export const TabsTrigger = React.forwardRef<
61
- HTMLButtonElement,
62
- WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Tab>>
63
- >(({ className, ...props }, ref) => (
64
- <BaseTabs.Tab ref={ref} className={cn(tabs__trigger, className)} {...props} />
65
- ));
66
- TabsTrigger.displayName = "TabsTrigger";
67
-
68
- /** 활성 탭 위치를 시각적으로 강조하는 인디케이터(보통 underline). TabsList 안에 둔다. */
69
- export const TabsIndicator = React.forwardRef<
70
- HTMLSpanElement,
71
- WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Indicator>>
72
- >(({ className, ...props }, ref) => (
73
- <BaseTabs.Indicator
74
- ref={ref}
75
- className={cn(tabs__indicator, className)}
76
- {...props}
77
- />
78
- ));
79
- TabsIndicator.displayName = "TabsIndicator";
80
-
81
- /** 한 탭의 패널. 같은 `value`의 TabsTrigger가 활성일 때 노출된다. */
82
- export const TabsContent = React.forwardRef<
83
- HTMLDivElement,
84
- WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Panel>>
85
- >(({ className, ...props }, ref) => (
86
- <BaseTabs.Panel ref={ref} className={cn(tabs__content, className)} {...props} />
87
- ));
88
- TabsContent.displayName = "TabsContent";
89
-
90
- /** 현재 Tabs의 variant를 자식에서 읽기 위한 훅. 커스텀 트리거를 만들 때 유용. */
91
- export const useTabsVariant = () => React.useContext(TabsContext).variant;