sh-ui-cli 0.44.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 +12 -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/code-editor/index.tailwind.tsx +168 -0
- package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -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/markdown-editor/index.tailwind.tsx +118 -0
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +211 -0
- package/data/registry/react/components/sidebar/index.tailwind.tsx +635 -0
- package/data/registry/react/components/toast/index.tailwind.tsx +215 -0
- package/data/registry/react/registry.json +187 -24
- package/package.json +1 -1
- package/src/mcp.mjs +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
type Orientation = "horizontal" | "vertical";
|
|
10
|
+
|
|
11
|
+
interface CarouselContextValue {
|
|
12
|
+
orientation: Orientation; loop: boolean;
|
|
13
|
+
index: number; count: number;
|
|
14
|
+
goTo: (i: number) => void; goPrev: () => void; goNext: () => void;
|
|
15
|
+
registerItem: () => () => void;
|
|
16
|
+
setContentEl: (el: HTMLDivElement | null) => void;
|
|
17
|
+
contentRef: React.RefObject<HTMLDivElement | null>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CarouselContext = React.createContext<CarouselContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
function useCarousel() {
|
|
23
|
+
const ctx = React.useContext(CarouselContext);
|
|
24
|
+
if (!ctx) throw new Error("Carousel parts must be used within <Carousel>");
|
|
25
|
+
return ctx;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useCarouselState() {
|
|
29
|
+
const { orientation, loop, index, count, goTo, goPrev, goNext } = useCarousel();
|
|
30
|
+
return { orientation, loop, index, count, goTo, goPrev, goNext };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
34
|
+
orientation?: Orientation;
|
|
35
|
+
loop?: boolean;
|
|
36
|
+
autoPlay?: boolean | number;
|
|
37
|
+
defaultIndex?: number;
|
|
38
|
+
index?: number;
|
|
39
|
+
onIndexChange?: (index: number) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
43
|
+
({ className, orientation = "horizontal", loop = false, autoPlay, defaultIndex = 0, index: controlledIndex, onIndexChange, children, onKeyDown: userOnKeyDown, ...props }, ref) => {
|
|
44
|
+
const isControlled = controlledIndex !== undefined;
|
|
45
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultIndex);
|
|
46
|
+
const index = isControlled ? controlledIndex : uncontrolled;
|
|
47
|
+
const [count, setCount] = React.useState(0);
|
|
48
|
+
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
|
49
|
+
const scrollTargetRef = React.useRef<number | null>(null);
|
|
50
|
+
|
|
51
|
+
const setContentEl = React.useCallback((el: HTMLDivElement | null) => { contentRef.current = el; }, []);
|
|
52
|
+
const registerItem = React.useCallback(() => {
|
|
53
|
+
setCount((c) => c + 1);
|
|
54
|
+
return () => setCount((c) => Math.max(0, c - 1));
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const setIndex = React.useCallback((next: number) => {
|
|
58
|
+
if (!isControlled) setUncontrolled(next);
|
|
59
|
+
onIndexChange?.(next);
|
|
60
|
+
}, [isControlled, onIndexChange]);
|
|
61
|
+
|
|
62
|
+
const clamp = React.useCallback((i: number) => {
|
|
63
|
+
if (count <= 0) return 0;
|
|
64
|
+
if (loop) return ((i % count) + count) % count;
|
|
65
|
+
return Math.max(0, Math.min(count - 1, i));
|
|
66
|
+
}, [count, loop]);
|
|
67
|
+
|
|
68
|
+
const goTo = React.useCallback((i: number) => setIndex(clamp(i)), [clamp, setIndex]);
|
|
69
|
+
const goPrev = React.useCallback(() => setIndex(clamp(index - 1)), [clamp, index, setIndex]);
|
|
70
|
+
const goNext = React.useCallback(() => setIndex(clamp(index + 1)), [clamp, index, setIndex]);
|
|
71
|
+
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
const el = contentRef.current;
|
|
74
|
+
if (!el) return;
|
|
75
|
+
const child = el.children[index] as HTMLElement | undefined;
|
|
76
|
+
if (!child) return;
|
|
77
|
+
scrollTargetRef.current = index;
|
|
78
|
+
if (orientation === "horizontal") el.scrollTo({ left: child.offsetLeft, behavior: "smooth" });
|
|
79
|
+
else el.scrollTo({ top: child.offsetTop, behavior: "smooth" });
|
|
80
|
+
}, [index, orientation, count]);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const el = contentRef.current;
|
|
84
|
+
if (!el) return;
|
|
85
|
+
let raf = 0;
|
|
86
|
+
const onScroll = () => {
|
|
87
|
+
cancelAnimationFrame(raf);
|
|
88
|
+
raf = requestAnimationFrame(() => {
|
|
89
|
+
const children = Array.from(el.children) as HTMLElement[];
|
|
90
|
+
if (children.length === 0) return;
|
|
91
|
+
const axis = orientation === "horizontal" ? "offsetLeft" : "offsetTop";
|
|
92
|
+
const scroll = orientation === "horizontal" ? el.scrollLeft : el.scrollTop;
|
|
93
|
+
let nearest = 0; let minDelta = Infinity;
|
|
94
|
+
children.forEach((c, i) => {
|
|
95
|
+
const d = Math.abs(c[axis] - scroll);
|
|
96
|
+
if (d < minDelta) { minDelta = d; nearest = i; }
|
|
97
|
+
});
|
|
98
|
+
if (nearest !== index) {
|
|
99
|
+
if (scrollTargetRef.current !== null && nearest !== scrollTargetRef.current) return;
|
|
100
|
+
scrollTargetRef.current = null;
|
|
101
|
+
setIndex(nearest);
|
|
102
|
+
} else scrollTargetRef.current = null;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
106
|
+
return () => { cancelAnimationFrame(raf); el.removeEventListener("scroll", onScroll); };
|
|
107
|
+
}, [orientation, index, setIndex]);
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!autoPlay || count <= 1) return;
|
|
111
|
+
const delay = autoPlay === true ? 4000 : autoPlay;
|
|
112
|
+
const el = contentRef.current;
|
|
113
|
+
let paused = false;
|
|
114
|
+
const pause = () => (paused = true);
|
|
115
|
+
const resume = () => (paused = false);
|
|
116
|
+
el?.addEventListener("mouseenter", pause);
|
|
117
|
+
el?.addEventListener("mouseleave", resume);
|
|
118
|
+
el?.addEventListener("focusin", pause);
|
|
119
|
+
el?.addEventListener("focusout", resume);
|
|
120
|
+
const id = window.setInterval(() => { if (paused) return; setIndex(clamp(index + 1)); }, delay);
|
|
121
|
+
return () => {
|
|
122
|
+
window.clearInterval(id);
|
|
123
|
+
el?.removeEventListener("mouseenter", pause);
|
|
124
|
+
el?.removeEventListener("mouseleave", resume);
|
|
125
|
+
el?.removeEventListener("focusin", pause);
|
|
126
|
+
el?.removeEventListener("focusout", resume);
|
|
127
|
+
};
|
|
128
|
+
}, [autoPlay, count, index, clamp, setIndex]);
|
|
129
|
+
|
|
130
|
+
const onKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
131
|
+
userOnKeyDown?.(event);
|
|
132
|
+
if (event.defaultPrevented) return;
|
|
133
|
+
const prevKey = orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
|
|
134
|
+
const nextKey = orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
|
|
135
|
+
if (event.key === prevKey) { event.preventDefault(); goPrev(); }
|
|
136
|
+
else if (event.key === nextKey) { event.preventDefault(); goNext(); }
|
|
137
|
+
}, [orientation, goPrev, goNext, userOnKeyDown]);
|
|
138
|
+
|
|
139
|
+
const value = React.useMemo<CarouselContextValue>(() => ({
|
|
140
|
+
orientation, loop, index, count, goTo, goPrev, goNext, registerItem, setContentEl, contentRef,
|
|
141
|
+
}), [orientation, loop, index, count, goTo, goPrev, goNext, registerItem, setContentEl]);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<CarouselContext.Provider value={value}>
|
|
145
|
+
<div
|
|
146
|
+
ref={ref}
|
|
147
|
+
className={cx("relative w-full", className)}
|
|
148
|
+
data-orientation={orientation}
|
|
149
|
+
role="region"
|
|
150
|
+
aria-roledescription="carousel"
|
|
151
|
+
onKeyDown={onKeyDown}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</div>
|
|
156
|
+
</CarouselContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
Carousel.displayName = "Carousel";
|
|
161
|
+
|
|
162
|
+
export const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
163
|
+
({ className, ...props }, ref) => {
|
|
164
|
+
const { orientation, setContentEl } = useCarousel();
|
|
165
|
+
const mergedRef = React.useCallback((el: HTMLDivElement | null) => {
|
|
166
|
+
setContentEl(el);
|
|
167
|
+
if (typeof ref === "function") ref(el);
|
|
168
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
169
|
+
}, [ref, setContentEl]);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
ref={mergedRef}
|
|
174
|
+
className={cx(
|
|
175
|
+
"flex gap-[var(--space-4)] overflow-x-auto overflow-y-hidden snap-x snap-mandatory scroll-smooth [scrollbar-width:none] [-ms-overflow-style:none] [-webkit-overflow-scrolling:touch] overscroll-x-contain [&::-webkit-scrollbar]:hidden motion-reduce:scroll-auto",
|
|
176
|
+
orientation === "vertical" && "flex-col overflow-x-hidden overflow-y-auto snap-y snap-mandatory h-80",
|
|
177
|
+
className,
|
|
178
|
+
)}
|
|
179
|
+
data-orientation={orientation}
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
CarouselContent.displayName = "CarouselContent";
|
|
186
|
+
|
|
187
|
+
export const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
188
|
+
({ className, ...props }, ref) => {
|
|
189
|
+
const { orientation, registerItem } = useCarousel();
|
|
190
|
+
React.useEffect(() => registerItem(), [registerItem]);
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
ref={ref}
|
|
194
|
+
role="group"
|
|
195
|
+
aria-roledescription="slide"
|
|
196
|
+
className={cx(
|
|
197
|
+
"flex-[0_0_100%] min-w-0 snap-start snap-always",
|
|
198
|
+
orientation === "vertical" && "basis-auto",
|
|
199
|
+
className,
|
|
200
|
+
)}
|
|
201
|
+
data-orientation={orientation}
|
|
202
|
+
{...props}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
CarouselItem.displayName = "CarouselItem";
|
|
208
|
+
|
|
209
|
+
const navClasses =
|
|
210
|
+
"absolute top-1/2 w-8 h-8 inline-flex items-center justify-center bg-background text-foreground border border-border rounded-full cursor-pointer -translate-y-1/2 z-[1] transition-[opacity,background-color] duration-[var(--duration-fast)] hover:not-disabled:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 disabled:opacity-40 disabled:cursor-not-allowed motion-reduce:transition-none data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:[top:auto]";
|
|
211
|
+
|
|
212
|
+
export const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
213
|
+
({ className, onClick, disabled, children, ...props }, ref) => {
|
|
214
|
+
const { goPrev, orientation, index, count, loop } = useCarousel();
|
|
215
|
+
const atStart = !loop && index <= 0;
|
|
216
|
+
return (
|
|
217
|
+
<button
|
|
218
|
+
ref={ref}
|
|
219
|
+
type="button"
|
|
220
|
+
aria-label="이전"
|
|
221
|
+
className={cx(
|
|
222
|
+
navClasses,
|
|
223
|
+
orientation === "horizontal" ? "-left-4" : "-top-4 left-1/2",
|
|
224
|
+
className,
|
|
225
|
+
)}
|
|
226
|
+
data-orientation={orientation}
|
|
227
|
+
disabled={disabled || atStart || count === 0}
|
|
228
|
+
onClick={(e) => { onClick?.(e); if (!e.defaultPrevented) goPrev(); }}
|
|
229
|
+
{...props}
|
|
230
|
+
>
|
|
231
|
+
{children ?? (
|
|
232
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
233
|
+
<path d={orientation === "horizontal" ? "M10 4l-4 4 4 4" : "M4 10l4-4 4 4"} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
234
|
+
</svg>
|
|
235
|
+
)}
|
|
236
|
+
</button>
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
CarouselPrevious.displayName = "CarouselPrevious";
|
|
241
|
+
|
|
242
|
+
export const CarouselNext = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
243
|
+
({ className, onClick, disabled, children, ...props }, ref) => {
|
|
244
|
+
const { goNext, orientation, index, count, loop } = useCarousel();
|
|
245
|
+
const atEnd = !loop && index >= count - 1;
|
|
246
|
+
return (
|
|
247
|
+
<button
|
|
248
|
+
ref={ref}
|
|
249
|
+
type="button"
|
|
250
|
+
aria-label="다음"
|
|
251
|
+
className={cx(
|
|
252
|
+
navClasses,
|
|
253
|
+
orientation === "horizontal" ? "-right-4" : "-bottom-4 left-1/2 [top:auto]",
|
|
254
|
+
className,
|
|
255
|
+
)}
|
|
256
|
+
data-orientation={orientation}
|
|
257
|
+
disabled={disabled || atEnd || count === 0}
|
|
258
|
+
onClick={(e) => { onClick?.(e); if (!e.defaultPrevented) goNext(); }}
|
|
259
|
+
{...props}
|
|
260
|
+
>
|
|
261
|
+
{children ?? (
|
|
262
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
263
|
+
<path d={orientation === "horizontal" ? "M6 4l4 4-4 4" : "M4 6l4 4 4-4"} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
264
|
+
</svg>
|
|
265
|
+
)}
|
|
266
|
+
</button>
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
CarouselNext.displayName = "CarouselNext";
|
|
271
|
+
|
|
272
|
+
export interface CarouselIndicatorsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
273
|
+
labelFor?: (index: number) => string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const CarouselIndicators = React.forwardRef<HTMLDivElement, CarouselIndicatorsProps>(
|
|
277
|
+
({ className, labelFor, ...props }, ref) => {
|
|
278
|
+
const { count, index, goTo, orientation } = useCarousel();
|
|
279
|
+
if (count <= 0) return null;
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
ref={ref}
|
|
283
|
+
role="tablist"
|
|
284
|
+
aria-label="슬라이드 선택"
|
|
285
|
+
className={cx(
|
|
286
|
+
"flex justify-center items-center gap-[var(--space-2)] mt-[var(--space-3)]",
|
|
287
|
+
orientation === "vertical" && "absolute top-1/2 right-2 mt-0 flex-col -translate-y-1/2",
|
|
288
|
+
className,
|
|
289
|
+
)}
|
|
290
|
+
data-orientation={orientation}
|
|
291
|
+
{...props}
|
|
292
|
+
>
|
|
293
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
294
|
+
<button
|
|
295
|
+
key={i}
|
|
296
|
+
type="button"
|
|
297
|
+
role="tab"
|
|
298
|
+
aria-selected={i === index}
|
|
299
|
+
aria-label={labelFor ? labelFor(i) : `${i + 1}번 슬라이드`}
|
|
300
|
+
className="w-2 h-2 p-0 bg-border border-none rounded-full cursor-pointer transition-[background-color,transform] duration-[var(--duration-fast)] hover:bg-border-strong data-[active]:bg-foreground data-[active]:scale-[1.2] focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none"
|
|
301
|
+
data-active={i === index || undefined}
|
|
302
|
+
onClick={() => goTo(i)}
|
|
303
|
+
/>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
CarouselIndicators.displayName = "CarouselIndicators";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { Compartment, EditorState, type Extension } from "@codemirror/state";
|
|
5
|
+
import { EditorView, placeholder as placeholderExt } from "@codemirror/view";
|
|
6
|
+
import { basicSetup } from "codemirror";
|
|
7
|
+
import { javascript } from "@codemirror/lang-javascript";
|
|
8
|
+
import { json } from "@codemirror/lang-json";
|
|
9
|
+
import { css as cssLang } from "@codemirror/lang-css";
|
|
10
|
+
import { html } from "@codemirror/lang-html";
|
|
11
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
12
|
+
|
|
13
|
+
export type CodeEditorLanguage =
|
|
14
|
+
| "text" | "javascript" | "typescript" | "jsx" | "tsx"
|
|
15
|
+
| "json" | "css" | "html" | "markdown";
|
|
16
|
+
|
|
17
|
+
export interface CodeEditorProps {
|
|
18
|
+
value?: string;
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
language?: CodeEditorLanguage;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
showLineNumbers?: boolean;
|
|
25
|
+
minHeight?: string;
|
|
26
|
+
maxHeight?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
"aria-label"?: string;
|
|
30
|
+
"aria-labelledby"?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cx(...args: (string | undefined | false | null)[]) {
|
|
34
|
+
return args.filter(Boolean).join(" ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function languageExtension(language: CodeEditorLanguage): Extension {
|
|
38
|
+
switch (language) {
|
|
39
|
+
case "javascript": return javascript();
|
|
40
|
+
case "typescript": return javascript({ typescript: true });
|
|
41
|
+
case "jsx": return javascript({ jsx: true });
|
|
42
|
+
case "tsx": return javascript({ jsx: true, typescript: true });
|
|
43
|
+
case "json": return json();
|
|
44
|
+
case "css": return cssLang();
|
|
45
|
+
case "html": return html();
|
|
46
|
+
case "markdown": return markdown();
|
|
47
|
+
case "text":
|
|
48
|
+
default: return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* CodeMirror 6 기반 인라인 코드 에디터 (Tailwind 변종).
|
|
54
|
+
* CodeMirror 자체의 .cm-* descendant 클래스는 Tailwind utility 로 표현 어려워
|
|
55
|
+
* <style> 태그로 한 번 inject — 컴포넌트의 outer wrapper 만 Tailwind utility.
|
|
56
|
+
*/
|
|
57
|
+
export function CodeEditor({
|
|
58
|
+
value: valueProp, defaultValue, onChange, language = "text", placeholder, readOnly = false,
|
|
59
|
+
showLineNumbers = true, minHeight, maxHeight, className, id,
|
|
60
|
+
"aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy,
|
|
61
|
+
}: CodeEditorProps) {
|
|
62
|
+
const isControlled = valueProp !== undefined;
|
|
63
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
65
|
+
const onChangeRef = useRef(onChange);
|
|
66
|
+
onChangeRef.current = onChange;
|
|
67
|
+
const initialDocRef = useRef(valueProp ?? defaultValue ?? "");
|
|
68
|
+
|
|
69
|
+
const compartments = useMemo(() => ({
|
|
70
|
+
language: new Compartment(),
|
|
71
|
+
readOnly: new Compartment(),
|
|
72
|
+
lineNumbers: new Compartment(),
|
|
73
|
+
placeholder: new Compartment(),
|
|
74
|
+
}), []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!hostRef.current) return;
|
|
78
|
+
const extensions: Extension[] = [
|
|
79
|
+
basicSetup,
|
|
80
|
+
EditorView.lineWrapping,
|
|
81
|
+
EditorView.updateListener.of((update) => {
|
|
82
|
+
if (update.docChanged) onChangeRef.current?.(update.state.doc.toString());
|
|
83
|
+
}),
|
|
84
|
+
compartments.language.of(languageExtension(language)),
|
|
85
|
+
compartments.readOnly.of(EditorState.readOnly.of(readOnly)),
|
|
86
|
+
compartments.lineNumbers.of(showLineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } })),
|
|
87
|
+
compartments.placeholder.of(placeholder ? placeholderExt(placeholder) : []),
|
|
88
|
+
];
|
|
89
|
+
const view = new EditorView({
|
|
90
|
+
state: EditorState.create({ doc: initialDocRef.current, extensions }),
|
|
91
|
+
parent: hostRef.current,
|
|
92
|
+
});
|
|
93
|
+
viewRef.current = view;
|
|
94
|
+
return () => { view.destroy(); viewRef.current = null; };
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!isControlled) return;
|
|
100
|
+
const view = viewRef.current;
|
|
101
|
+
if (!view) return;
|
|
102
|
+
const current = view.state.doc.toString();
|
|
103
|
+
if (current === valueProp) return;
|
|
104
|
+
view.dispatch({ changes: { from: 0, to: current.length, insert: valueProp ?? "" } });
|
|
105
|
+
}, [isControlled, valueProp]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
viewRef.current?.dispatch({ effects: compartments.language.reconfigure(languageExtension(language)) });
|
|
109
|
+
}, [language, compartments.language]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
viewRef.current?.dispatch({ effects: compartments.readOnly.reconfigure(EditorState.readOnly.of(readOnly)) });
|
|
113
|
+
}, [readOnly, compartments.readOnly]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
viewRef.current?.dispatch({
|
|
117
|
+
effects: compartments.lineNumbers.reconfigure(showLineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } })),
|
|
118
|
+
});
|
|
119
|
+
}, [showLineNumbers, compartments.lineNumbers]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
viewRef.current?.dispatch({
|
|
123
|
+
effects: compartments.placeholder.reconfigure(placeholder ? placeholderExt(placeholder) : []),
|
|
124
|
+
});
|
|
125
|
+
}, [placeholder, compartments.placeholder]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const view = viewRef.current;
|
|
129
|
+
if (!view) return;
|
|
130
|
+
const node = view.contentDOM;
|
|
131
|
+
if (id) node.id = id;
|
|
132
|
+
if (ariaLabel) node.setAttribute("aria-label", ariaLabel); else node.removeAttribute("aria-label");
|
|
133
|
+
if (ariaLabelledBy) node.setAttribute("aria-labelledby", ariaLabelledBy); else node.removeAttribute("aria-labelledby");
|
|
134
|
+
}, [id, ariaLabel, ariaLabelledBy]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div
|
|
138
|
+
ref={hostRef}
|
|
139
|
+
className={cx(
|
|
140
|
+
"sh-ui-code-editor relative border border-border rounded-[var(--radius)] bg-background text-[0.8125rem] leading-relaxed overflow-hidden transition-[border-color] duration-[var(--duration-fast)] focus-within:border-foreground focus-within:outline-[length:var(--border-width-strong)] focus-within:outline-foreground focus-within:outline-offset-2 data-[readonly]:bg-background-subtle motion-reduce:transition-none",
|
|
141
|
+
className,
|
|
142
|
+
)}
|
|
143
|
+
data-readonly={readOnly || undefined}
|
|
144
|
+
style={{ "--sh-ui-code-editor-min-height": minHeight, "--sh-ui-code-editor-max-height": maxHeight } as React.CSSProperties}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-sh-ui-code-editor]")) {
|
|
150
|
+
const style = document.createElement("style");
|
|
151
|
+
style.setAttribute("data-sh-ui-code-editor", "");
|
|
152
|
+
style.textContent = `
|
|
153
|
+
.sh-ui-code-editor .cm-editor { background: transparent; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
154
|
+
.sh-ui-code-editor .cm-editor.cm-focused { outline: none; }
|
|
155
|
+
.sh-ui-code-editor .cm-scroller { font-family: inherit; min-height: var(--sh-ui-code-editor-min-height, 7.5rem); max-height: var(--sh-ui-code-editor-max-height, 25rem); }
|
|
156
|
+
.sh-ui-code-editor .cm-content { caret-color: var(--foreground); color: var(--foreground); padding: var(--space-3) 0; }
|
|
157
|
+
.sh-ui-code-editor .cm-line { padding: 0 var(--space-3); }
|
|
158
|
+
.sh-ui-code-editor .cm-gutters { background: var(--background-subtle); color: var(--foreground-muted); border-right: 1px solid var(--border); }
|
|
159
|
+
.sh-ui-code-editor .cm-activeLineGutter, .sh-ui-code-editor .cm-activeLine { background: var(--background-muted); }
|
|
160
|
+
.sh-ui-code-editor .cm-cursor, .sh-ui-code-editor .cm-dropCursor { border-left-color: var(--foreground); }
|
|
161
|
+
.sh-ui-code-editor .cm-selectionBackground, .sh-ui-code-editor .cm-editor .cm-selectionBackground, .sh-ui-code-editor .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .sh-ui-code-editor ::selection { background: var(--background-muted) !important; }
|
|
162
|
+
.sh-ui-code-editor .cm-placeholder { color: var(--foreground-muted); }
|
|
163
|
+
.sh-ui-code-editor .cm-tooltip { background: var(--background); border: 1px solid var(--border); color: var(--foreground); border-radius: calc(var(--radius) - 2px); }
|
|
164
|
+
.sh-ui-code-editor .cm-tooltip-autocomplete > ul > li[aria-selected] { background: var(--background-muted); color: var(--foreground); }
|
|
165
|
+
.sh-ui-code-editor .cm-matchingBracket, .sh-ui-code-editor .cm-nonmatchingBracket { background: var(--background-muted); color: var(--foreground); }
|
|
166
|
+
`;
|
|
167
|
+
document.head.appendChild(style);
|
|
168
|
+
}
|