sh-ui-cli 0.43.0 → 0.45.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.
- package/data/changelog/versions.json +24 -0
- package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
- package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
- package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
- package/data/registry/react/components/calendar/index.tailwind.tsx +498 -0
- package/data/registry/react/components/carousel/index.tailwind.tsx +309 -0
- package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
- package/data/registry/react/components/code-editor/index.tailwind.tsx +168 -0
- package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
- package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -0
- package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
- package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
- package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
- package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
- package/data/registry/react/components/file-upload/index.tailwind.tsx +290 -0
- package/data/registry/react/components/form/field.tailwind.tsx +165 -0
- package/data/registry/react/components/form/form.tailwind.tsx +129 -0
- package/data/registry/react/components/form/index.tailwind.tsx +49 -0
- package/data/registry/react/components/header/index.tailwind.tsx +550 -0
- package/data/registry/react/components/label/index.tailwind.tsx +78 -0
- package/data/registry/react/components/markdown-editor/index.tailwind.tsx +118 -0
- package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
- package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
- package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
- package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
- package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
- package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +211 -0
- package/data/registry/react/components/select/index.tailwind.tsx +199 -0
- package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
- package/data/registry/react/components/sidebar/index.tailwind.tsx +635 -0
- package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
- package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
- package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
- package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
- package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
- package/data/registry/react/components/toast/index.tailwind.tsx +215 -0
- package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
- package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
- package/data/registry/react/registry.json +696 -98
- package/package.json +1 -1
- package/src/mcp.mjs +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
6
|
+
return args.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function clamp(n: number, min: number, max: number) {
|
|
10
|
+
return Math.min(max, Math.max(min, n));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function snap(value: number, step: number, min: number) {
|
|
14
|
+
if (step <= 0) return value;
|
|
15
|
+
return min + Math.round((value - min) / step) * step;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SliderContextValue {
|
|
19
|
+
value: number;
|
|
20
|
+
setValue: (next: number) => void;
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
step: number;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
ariaLabel?: string;
|
|
26
|
+
trackRef: React.RefObject<HTMLDivElement | null>;
|
|
27
|
+
setTrackEl: (el: HTMLDivElement | null) => void;
|
|
28
|
+
percent: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SliderContext = React.createContext<SliderContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
function useSliderContext(): SliderContextValue {
|
|
34
|
+
const ctx = React.useContext(SliderContext);
|
|
35
|
+
if (!ctx) throw new Error("Slider 하위 컴포넌트는 <Slider> 안에서만 사용할 수 있습니다.");
|
|
36
|
+
return ctx;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useSliderState(): Pick<
|
|
40
|
+
SliderContextValue,
|
|
41
|
+
"value" | "min" | "max" | "step" | "disabled"
|
|
42
|
+
> {
|
|
43
|
+
const { value, min, max, step, disabled } = useSliderContext();
|
|
44
|
+
return { value, min, max, step, disabled };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SliderProps
|
|
48
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
|
|
49
|
+
value?: number;
|
|
50
|
+
defaultValue?: number;
|
|
51
|
+
onValueChange?: (value: number) => void;
|
|
52
|
+
min?: number;
|
|
53
|
+
max?: number;
|
|
54
|
+
step?: number;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
"aria-label"?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
60
|
+
function Slider(
|
|
61
|
+
{
|
|
62
|
+
value: valueProp,
|
|
63
|
+
defaultValue = 0,
|
|
64
|
+
onValueChange,
|
|
65
|
+
min = 0,
|
|
66
|
+
max = 100,
|
|
67
|
+
step = 1,
|
|
68
|
+
disabled = false,
|
|
69
|
+
className,
|
|
70
|
+
children,
|
|
71
|
+
"aria-label": ariaLabel,
|
|
72
|
+
...rest
|
|
73
|
+
},
|
|
74
|
+
ref,
|
|
75
|
+
) {
|
|
76
|
+
const isControlled = valueProp !== undefined;
|
|
77
|
+
const [internal, setInternal] = React.useState(defaultValue);
|
|
78
|
+
const rawValue = isControlled ? (valueProp as number) : internal;
|
|
79
|
+
const value = clamp(rawValue, min, max);
|
|
80
|
+
|
|
81
|
+
const trackRef = React.useRef<HTMLDivElement | null>(null);
|
|
82
|
+
const setTrackEl = React.useCallback((el: HTMLDivElement | null) => {
|
|
83
|
+
trackRef.current = el;
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const setValue = React.useCallback(
|
|
87
|
+
(next: number) => {
|
|
88
|
+
const snapped = clamp(snap(next, step, min), min, max);
|
|
89
|
+
if (snapped === value) return;
|
|
90
|
+
if (!isControlled) setInternal(snapped);
|
|
91
|
+
onValueChange?.(snapped);
|
|
92
|
+
},
|
|
93
|
+
[isControlled, max, min, onValueChange, step, value],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const ratio = max === min ? 0 : (value - min) / (max - min);
|
|
97
|
+
const percent = `${ratio * 100}%`;
|
|
98
|
+
|
|
99
|
+
const ctxValue = React.useMemo<SliderContextValue>(
|
|
100
|
+
() => ({
|
|
101
|
+
value, setValue, min, max, step, disabled, ariaLabel,
|
|
102
|
+
trackRef, setTrackEl, percent,
|
|
103
|
+
}),
|
|
104
|
+
[ariaLabel, disabled, max, min, percent, setTrackEl, setValue, step, value],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<SliderContext.Provider value={ctxValue}>
|
|
109
|
+
<div
|
|
110
|
+
ref={ref}
|
|
111
|
+
{...rest}
|
|
112
|
+
className={cx(
|
|
113
|
+
"relative w-full py-[var(--space-2)] select-none",
|
|
114
|
+
disabled && "opacity-[var(--opacity-disabled)] pointer-events-none",
|
|
115
|
+
className,
|
|
116
|
+
)}
|
|
117
|
+
data-disabled={disabled || undefined}
|
|
118
|
+
>
|
|
119
|
+
{children ?? (
|
|
120
|
+
<SliderTrack>
|
|
121
|
+
<SliderRange />
|
|
122
|
+
<SliderThumb />
|
|
123
|
+
</SliderTrack>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</SliderContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
Slider.displayName = "Slider";
|
|
131
|
+
|
|
132
|
+
export const SliderTrack = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
133
|
+
function SliderTrack({ className, onPointerDown: userOnPointerDown, children, ...props }, ref) {
|
|
134
|
+
const { disabled, setValue, min, max, setTrackEl, trackRef } = useSliderContext();
|
|
135
|
+
|
|
136
|
+
const mergedRef = React.useCallback(
|
|
137
|
+
(el: HTMLDivElement | null) => {
|
|
138
|
+
setTrackEl(el);
|
|
139
|
+
if (typeof ref === "function") ref(el);
|
|
140
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
141
|
+
},
|
|
142
|
+
[ref, setTrackEl],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const moveToClient = (clientX: number) => {
|
|
146
|
+
const el = trackRef.current;
|
|
147
|
+
if (!el) return;
|
|
148
|
+
const r = el.getBoundingClientRect();
|
|
149
|
+
const ratio = clamp((clientX - r.left) / r.width, 0, 1);
|
|
150
|
+
setValue(min + ratio * (max - min));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
154
|
+
userOnPointerDown?.(e);
|
|
155
|
+
if (e.defaultPrevented || disabled) return;
|
|
156
|
+
const el = trackRef.current;
|
|
157
|
+
if (!el) return;
|
|
158
|
+
el.setPointerCapture(e.pointerId);
|
|
159
|
+
moveToClient(e.clientX);
|
|
160
|
+
|
|
161
|
+
const onMove = (ev: PointerEvent) => moveToClient(ev.clientX);
|
|
162
|
+
const onUp = (ev: PointerEvent) => {
|
|
163
|
+
el.releasePointerCapture(ev.pointerId);
|
|
164
|
+
el.removeEventListener("pointermove", onMove);
|
|
165
|
+
el.removeEventListener("pointerup", onUp);
|
|
166
|
+
};
|
|
167
|
+
el.addEventListener("pointermove", onMove);
|
|
168
|
+
el.addEventListener("pointerup", onUp);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
ref={mergedRef}
|
|
174
|
+
className={cx(
|
|
175
|
+
"relative w-full h-1.5 bg-background-muted rounded-full cursor-pointer touch-none",
|
|
176
|
+
className,
|
|
177
|
+
)}
|
|
178
|
+
onPointerDown={onPointerDown}
|
|
179
|
+
{...props}
|
|
180
|
+
>
|
|
181
|
+
{children ?? (
|
|
182
|
+
<>
|
|
183
|
+
<SliderRange />
|
|
184
|
+
<SliderThumb />
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
SliderTrack.displayName = "SliderTrack";
|
|
192
|
+
|
|
193
|
+
export const SliderRange = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
194
|
+
function SliderRange({ className, style, ...props }, ref) {
|
|
195
|
+
const { percent } = useSliderContext();
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
ref={ref}
|
|
199
|
+
className={cx(
|
|
200
|
+
"absolute top-0 left-0 h-full bg-primary rounded-full pointer-events-none",
|
|
201
|
+
className,
|
|
202
|
+
)}
|
|
203
|
+
style={{ width: percent, ...style }}
|
|
204
|
+
{...props}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
SliderRange.displayName = "SliderRange";
|
|
210
|
+
|
|
211
|
+
export const SliderThumb = React.forwardRef<
|
|
212
|
+
HTMLDivElement,
|
|
213
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "role" | "tabIndex">
|
|
214
|
+
>(function SliderThumb({ className, style, onKeyDown: userOnKeyDown, ...props }, ref) {
|
|
215
|
+
const { value, setValue, min, max, step, disabled, ariaLabel, percent } = useSliderContext();
|
|
216
|
+
|
|
217
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
218
|
+
userOnKeyDown?.(e);
|
|
219
|
+
if (e.defaultPrevented || disabled) return;
|
|
220
|
+
const big = e.shiftKey ? step * 10 : step;
|
|
221
|
+
switch (e.key) {
|
|
222
|
+
case "ArrowRight":
|
|
223
|
+
case "ArrowUp":
|
|
224
|
+
e.preventDefault(); setValue(value + big); break;
|
|
225
|
+
case "ArrowLeft":
|
|
226
|
+
case "ArrowDown":
|
|
227
|
+
e.preventDefault(); setValue(value - big); break;
|
|
228
|
+
case "Home": e.preventDefault(); setValue(min); break;
|
|
229
|
+
case "End": e.preventDefault(); setValue(max); break;
|
|
230
|
+
case "PageUp": e.preventDefault(); setValue(value + step * 10); break;
|
|
231
|
+
case "PageDown": e.preventDefault(); setValue(value - step * 10); break;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div
|
|
237
|
+
ref={ref}
|
|
238
|
+
role="slider"
|
|
239
|
+
tabIndex={disabled ? -1 : 0}
|
|
240
|
+
aria-label={ariaLabel}
|
|
241
|
+
aria-valuemin={min}
|
|
242
|
+
aria-valuemax={max}
|
|
243
|
+
aria-valuenow={value}
|
|
244
|
+
aria-disabled={disabled || undefined}
|
|
245
|
+
onKeyDown={onKeyDown}
|
|
246
|
+
className={cx(
|
|
247
|
+
"absolute top-1/2 w-4 h-4 -ml-2 -translate-y-1/2 bg-background border-2 border-primary rounded-full shadow-[0_1px_2px_rgba(0,0,0,0.1)] cursor-grab transition-transform duration-[80ms] active:cursor-grabbing active:scale-110 active:-translate-y-1/2 focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 [@media(hover:none)_and_(pointer:coarse)]:w-5 [@media(hover:none)_and_(pointer:coarse)]:h-5 [@media(hover:none)_and_(pointer:coarse)]:-ml-2.5",
|
|
248
|
+
className,
|
|
249
|
+
)}
|
|
250
|
+
style={{ left: percent, ...style }}
|
|
251
|
+
{...props}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
SliderThumb.displayName = "SliderThumb";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
5
|
+
return args.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const spinnerVariants = cva(
|
|
9
|
+
"inline-flex items-center justify-center align-middle text-current",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
sm: "w-3.5 h-3.5",
|
|
14
|
+
md: "w-[1.125rem] h-[1.125rem]",
|
|
15
|
+
lg: "w-6 h-6",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: { size: "md" },
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type SpinnerSize = NonNullable<VariantProps<typeof spinnerVariants>["size"]>;
|
|
23
|
+
|
|
24
|
+
export interface SpinnerProps
|
|
25
|
+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "role"> {
|
|
26
|
+
size?: SpinnerSize;
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
|
|
31
|
+
function Spinner(
|
|
32
|
+
{ size = "md", className, "aria-label": ariaLabel = "로딩 중", ...props },
|
|
33
|
+
ref,
|
|
34
|
+
) {
|
|
35
|
+
const ringBorder = size === "sm" ? "border-[1.5px]" : "border-2";
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
ref={ref}
|
|
39
|
+
role="status"
|
|
40
|
+
aria-live="polite"
|
|
41
|
+
aria-label={ariaLabel}
|
|
42
|
+
className={cx(spinnerVariants({ size }), className)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<span
|
|
46
|
+
aria-hidden
|
|
47
|
+
className={cx(
|
|
48
|
+
"inline-block w-full h-full rounded-full border-current border-t-transparent opacity-80 animate-[sh-ui-spinner-rotate_0.8s_linear_infinite] motion-reduce:[animation-duration:3s]",
|
|
49
|
+
ringBorder,
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
Spinner.displayName = "Spinner";
|
|
57
|
+
|
|
58
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-spinner]")) {
|
|
59
|
+
const style = document.createElement("style");
|
|
60
|
+
style.setAttribute("data-sh-ui-spinner", "");
|
|
61
|
+
style.textContent = `@keyframes sh-ui-spinner-rotate { to { transform: rotate(360deg) } }`;
|
|
62
|
+
document.head.appendChild(style);
|
|
63
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Switch as BaseSwitch } from "@base-ui/react/switch";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
6
|
+
return args.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const switchRoot = cva(
|
|
10
|
+
"inline-flex items-center border-none rounded-full bg-background-muted cursor-pointer shrink-0 p-0.5 transition-colors duration-150 hover:not-data-[disabled]:bg-border-strong focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[checked]:bg-primary data-[checked]:hover:not-data-[disabled]:bg-primary-hover data-[disabled]:opacity-[var(--opacity-disabled)] data-[disabled]:cursor-not-allowed motion-reduce:transition-none",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
sm: "w-8 h-[1.125rem]",
|
|
15
|
+
md: "w-10 h-[1.375rem]",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: { size: "md" },
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const switchThumb = cva(
|
|
23
|
+
"block rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.12)] transition-transform duration-150 ease-out motion-reduce:transition-none",
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
sm: "w-3.5 h-3.5",
|
|
28
|
+
md: "w-[1.125rem] h-[1.125rem]",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: { size: "md" },
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
type SwitchSize = NonNullable<VariantProps<typeof switchRoot>["size"]>;
|
|
36
|
+
|
|
37
|
+
export type SwitchProps = Omit<
|
|
38
|
+
React.ComponentPropsWithoutRef<typeof BaseSwitch.Root>,
|
|
39
|
+
"className"
|
|
40
|
+
> & {
|
|
41
|
+
className?: string;
|
|
42
|
+
size?: SwitchSize;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Switch = React.forwardRef<HTMLElement, SwitchProps>(
|
|
46
|
+
({ className, size = "md", ...props }, ref) => (
|
|
47
|
+
<BaseSwitch.Root
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cx(switchRoot({ size }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<BaseSwitch.Thumb
|
|
53
|
+
className={cx(
|
|
54
|
+
switchThumb({ size }),
|
|
55
|
+
size === "sm" && "data-[checked]:translate-x-3.5",
|
|
56
|
+
size === "md" && "data-[checked]:translate-x-[1.125rem]",
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
</BaseSwitch.Root>
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
Switch.displayName = "Switch";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
|
5
|
+
|
|
6
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
7
|
+
return args.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
11
|
+
|
|
12
|
+
export type TabsVariant = "underline" | "pill" | "plain";
|
|
13
|
+
|
|
14
|
+
interface TabsContextValue { variant: TabsVariant; }
|
|
15
|
+
const TabsContext = React.createContext<TabsContextValue>({ variant: "underline" });
|
|
16
|
+
|
|
17
|
+
export type TabsProps = WithStringClassName<
|
|
18
|
+
React.ComponentPropsWithoutRef<typeof BaseTabs.Root>
|
|
19
|
+
> & {
|
|
20
|
+
variant?: TabsVariant;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
|
|
24
|
+
({ className, variant = "underline", ...props }, ref) => (
|
|
25
|
+
<TabsContext.Provider value={{ variant }}>
|
|
26
|
+
<BaseTabs.Root
|
|
27
|
+
ref={ref}
|
|
28
|
+
data-variant={variant}
|
|
29
|
+
className={cx("flex flex-col gap-[var(--space-3)] w-full data-[orientation=vertical]:flex-row", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
</TabsContext.Provider>
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
Tabs.displayName = "Tabs";
|
|
36
|
+
|
|
37
|
+
const listVariantClasses =
|
|
38
|
+
"data-[variant=underline]:[&]:w-full data-[variant=underline]:[&]:gap-0 data-[variant=underline]:[&]:shadow-[inset_0_-1px_0_var(--border)] data-[variant=pill]:[&]:p-[var(--space-1)] data-[variant=pill]:[&]:bg-[var(--background-muted,var(--background))] data-[variant=pill]:[&]:border data-[variant=pill]:[&]:border-border data-[variant=pill]:[&]:rounded-[var(--radius)] [[data-orientation=vertical]_&]:flex-col [[data-orientation=vertical]_&]:items-stretch [[data-orientation=vertical][data-variant=underline]_&]:w-auto [[data-orientation=vertical][data-variant=underline]_&]:shadow-[inset_-1px_0_0_var(--border)]";
|
|
39
|
+
|
|
40
|
+
export const TabsList = React.forwardRef<
|
|
41
|
+
HTMLDivElement,
|
|
42
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.List>>
|
|
43
|
+
>(({ className, ...props }, ref) => {
|
|
44
|
+
const { variant } = React.useContext(TabsContext);
|
|
45
|
+
return (
|
|
46
|
+
<BaseTabs.List
|
|
47
|
+
ref={ref}
|
|
48
|
+
data-variant={variant}
|
|
49
|
+
className={cx(
|
|
50
|
+
"relative inline-flex items-center gap-[var(--space-1)] w-fit",
|
|
51
|
+
listVariantClasses,
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
TabsList.displayName = "TabsList";
|
|
59
|
+
|
|
60
|
+
const triggerVariantClasses =
|
|
61
|
+
"[[data-variant=underline]_&]:py-2.5 [[data-variant=underline]_&]:px-[var(--space-4)] [[data-variant=pill]_&]:py-1.5 [[data-variant=pill]_&]:px-[var(--space-3)] [[data-variant=pill]_&]:rounded-[calc(var(--radius)-2px)] [[data-variant=plain]_&]:py-1.5 [[data-variant=plain]_&]:px-[var(--space-2)] [[data-variant=plain]_&]:rounded-[calc(var(--radius)-2px)] data-[selected]:[[data-variant=plain]_&]:bg-[var(--background-muted,transparent)]";
|
|
62
|
+
|
|
63
|
+
export const TabsTrigger = React.forwardRef<
|
|
64
|
+
HTMLButtonElement,
|
|
65
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Tab>>
|
|
66
|
+
>(({ className, ...props }, ref) => (
|
|
67
|
+
<BaseTabs.Tab
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cx(
|
|
70
|
+
"relative z-[1] inline-flex items-center justify-center gap-1.5 py-[var(--space-2)] px-[var(--space-3)] bg-transparent text-foreground-muted border-0 text-[length:var(--text-sm)] font-medium leading-none cursor-pointer transition-[color,background-color] duration-[var(--duration-fast)] select-none whitespace-nowrap hover:not-disabled:not-data-[selected]:text-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 data-[selected]:text-foreground disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed",
|
|
71
|
+
triggerVariantClasses,
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
));
|
|
77
|
+
TabsTrigger.displayName = "TabsTrigger";
|
|
78
|
+
|
|
79
|
+
const indicatorVariantClasses =
|
|
80
|
+
"[[data-variant=underline]_&]:shadow-[inset_0_-2px_0_var(--foreground)] [[data-variant=pill]_&]:bg-background [[data-variant=pill]_&]:rounded-[calc(var(--radius)-2px)] [[data-variant=pill]_&]:shadow-[0_1px_2px_rgba(0,0,0,0.06)] [[data-variant=plain]_&]:hidden [[data-orientation=vertical][data-variant=underline]_&]:shadow-[inset_-2px_0_0_var(--foreground)]";
|
|
81
|
+
|
|
82
|
+
export const TabsIndicator = React.forwardRef<
|
|
83
|
+
HTMLSpanElement,
|
|
84
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Indicator>>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<BaseTabs.Indicator
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cx(
|
|
89
|
+
"absolute z-0 pointer-events-none transition-[top,left,width,height] duration-[180ms] data-[activation-direction=none]:transition-none top-[var(--active-tab-top)] left-[var(--active-tab-left)] w-[var(--active-tab-width)] h-[var(--active-tab-height)]",
|
|
90
|
+
indicatorVariantClasses,
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
));
|
|
96
|
+
TabsIndicator.displayName = "TabsIndicator";
|
|
97
|
+
|
|
98
|
+
export const TabsContent = React.forwardRef<
|
|
99
|
+
HTMLDivElement,
|
|
100
|
+
WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseTabs.Panel>>
|
|
101
|
+
>(({ className, ...props }, ref) => (
|
|
102
|
+
<BaseTabs.Panel
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cx(
|
|
105
|
+
"outline-none focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 focus-visible:rounded-[var(--radius)]",
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
));
|
|
111
|
+
TabsContent.displayName = "TabsContent";
|
|
112
|
+
|
|
113
|
+
export const useTabsVariant = () => React.useContext(TabsContext).variant;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
4
|
+
|
|
5
|
+
function cx(...args: (string | undefined | false)[]) {
|
|
6
|
+
return args.filter(Boolean).join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
10
|
+
({ className, ...props }, ref) => (
|
|
11
|
+
<textarea
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cx(
|
|
14
|
+
"block w-full min-h-20 px-[var(--space-3)] py-[var(--space-2)] bg-background text-foreground border border-border rounded-[var(--radius)] font-[inherit] text-[length:var(--text-sm)] leading-normal resize-y transition-[border-color,box-shadow] duration-[var(--duration-fast)] placeholder:text-foreground-subtle hover:not-disabled:not-focus:border-border-strong focus:outline-none focus:border-foreground focus:shadow-[0_0_0_1px_var(--foreground)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed disabled:bg-background-subtle read-only:bg-background-subtle aria-[invalid=true]:border-danger aria-[invalid=true]:focus:shadow-[0_0_0_1px_var(--danger)] [@media(hover:none)_and_(pointer:coarse)]:text-[length:var(--text-base)]",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
Textarea.displayName = "Textarea";
|