sh-ui-cli 0.42.1 → 0.44.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 (53) hide show
  1. package/README.md +6 -1
  2. package/data/changelog/versions.json +25 -0
  3. package/data/registry/flutter/registry.json +1 -1
  4. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  5. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  6. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  7. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  8. package/data/registry/react/components/button/index.tailwind.tsx +70 -0
  9. package/data/registry/react/components/card/index.tailwind.tsx +111 -0
  10. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  11. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
  12. package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
  13. package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
  14. package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
  15. package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
  16. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
  17. package/data/registry/react/components/input/index.tailwind.tsx +405 -0
  18. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  19. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  20. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  21. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  22. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  23. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  24. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  25. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  26. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  27. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  28. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  29. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  30. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  31. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  32. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  33. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  34. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  35. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  36. package/data/registry/react/peer-versions.json +1 -0
  37. package/data/registry/react/registry.json +530 -72
  38. package/data/tokens/build.mjs +66 -0
  39. package/package.json +1 -1
  40. package/src/add.mjs +54 -6
  41. package/src/api.d.ts +14 -0
  42. package/src/api.js +4 -0
  43. package/src/constants.js +19 -0
  44. package/src/create/cli-args.js +18 -2
  45. package/src/create/generator.js +55 -6
  46. package/src/create/index.mjs +3 -1
  47. package/src/init.mjs +25 -7
  48. package/src/mcp.mjs +13 -2
  49. package/templates/flutter-standalone/sh-ui.config.json +1 -1
  50. package/templates/nextjs-standalone/app/globals.css +1 -21
  51. package/templates/nextjs-standalone/sh-ui.config.json +1 -1
  52. package/templates/ui-app-template/sh-ui.config.json +1 -1
  53. package/templates/ui-app-template/src/styles/globals.css +1 -21
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+
3
+ function cx(...args: (string | undefined | false | null)[]) {
4
+ return args.filter(Boolean).join(" ");
5
+ }
6
+
7
+ export type SeparatorOrientation = "horizontal" | "vertical";
8
+
9
+ export interface SeparatorProps
10
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "role"> {
11
+ orientation?: SeparatorOrientation;
12
+ /**
13
+ * 의미 없는 시각적 구분선인지 여부. 기본 true(aria-hidden).
14
+ * 스크린리더에도 섹션 구분을 알려야 하면 false.
15
+ */
16
+ decorative?: boolean;
17
+ }
18
+
19
+ /**
20
+ * 시각적 구분선 (Tailwind utility 변종). 가로(height=1px) / 세로(width=1px).
21
+ */
22
+ export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
23
+ function Separator(
24
+ { className, orientation = "horizontal", decorative = true, ...props },
25
+ ref,
26
+ ) {
27
+ const sizing =
28
+ orientation === "horizontal" ? "w-full h-px" : "w-px h-full self-stretch";
29
+ return (
30
+ <div
31
+ ref={ref}
32
+ role={decorative ? undefined : "separator"}
33
+ aria-orientation={decorative ? undefined : orientation}
34
+ aria-hidden={decorative || undefined}
35
+ data-orientation={orientation}
36
+ className={cx("bg-border shrink-0", sizing, className)}
37
+ {...props}
38
+ />
39
+ );
40
+ },
41
+ );
42
+ Separator.displayName = "Separator";
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+
3
+ function cx(...args: (string | undefined | false | null)[]) {
4
+ return args.filter(Boolean).join(" ");
5
+ }
6
+
7
+ /**
8
+ * 로딩 중 콘텐츠 자리를 채우는 placeholder 박스 (Tailwind utility 변종).
9
+ * `aria-hidden`이 기본 적용되므로 스크린리더에 노출되지 않는다.
10
+ */
11
+ export const Skeleton = React.forwardRef<
12
+ HTMLDivElement,
13
+ React.HTMLAttributes<HTMLDivElement>
14
+ >(({ className, ...props }, ref) => (
15
+ <div
16
+ ref={ref}
17
+ aria-hidden="true"
18
+ className={cx(
19
+ "block w-full h-4 bg-background-muted rounded-[calc(var(--radius)-2px)] animate-[sh-ui-skeleton-pulse_1.6s_ease-in-out_infinite] motion-reduce:animate-none",
20
+ className,
21
+ )}
22
+ style={
23
+ {
24
+ ...(props.style as React.CSSProperties),
25
+ }
26
+ }
27
+ {...props}
28
+ />
29
+ ));
30
+ Skeleton.displayName = "Skeleton";
31
+
32
+ /* keyframes — Tailwind 4 의 @theme 가 keyframe 도 가져가지만,
33
+ * 사용자 토큰에는 없으므로 컴포넌트 옆에 한 번만 inject. */
34
+ if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-skeleton]")) {
35
+ const style = document.createElement("style");
36
+ style.setAttribute("data-sh-ui-skeleton", "");
37
+ style.textContent = `@keyframes sh-ui-skeleton-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.55 } }`;
38
+ document.head.appendChild(style);
39
+ }
@@ -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";