sh-ui-cli 0.49.0 → 0.50.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 (85) hide show
  1. package/data/changelog/versions.json +14 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  12. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  14. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  16. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  18. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  20. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  22. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  24. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  26. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  28. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  30. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  32. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  34. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  35. package/data/registry/react/components/form/styles.css.ts +56 -0
  36. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  37. package/data/registry/react/components/header/styles.css.ts +413 -0
  38. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  39. package/data/registry/react/components/label/styles.css.ts +141 -0
  40. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  42. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  44. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  46. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  48. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  50. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.css.ts +78 -0
  52. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.css.ts +53 -0
  54. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.css.ts +79 -0
  56. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  58. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  59. package/data/registry/react/components/select/styles.css.ts +225 -0
  60. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.css.ts +24 -0
  62. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  64. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  66. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.css.ts +75 -0
  68. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  70. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.css.ts +87 -0
  72. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  74. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  76. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.css.ts +307 -0
  78. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  80. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  82. package/data/registry/react/registry.json +853 -36
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. package/src/constants.js +4 -3
@@ -0,0 +1,169 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const carousel = style({
4
+ position: "relative",
5
+ width: "100%",
6
+ });
7
+
8
+ export const carousel__content = style({
9
+ display: "flex",
10
+ gap: "var(--space-4)",
11
+ overflowX: "auto",
12
+ overflowY: "hidden",
13
+ scrollSnapType: "x mandatory",
14
+ scrollBehavior: "smooth",
15
+ scrollbarWidth: "none",
16
+ MsOverflowStyle: "none",
17
+ WebkitOverflowScrolling: "touch",
18
+ overscrollBehaviorInline: "contain",
19
+ selectors: {
20
+ "&::-webkit-scrollbar": {
21
+ display: "none",
22
+ },
23
+ "&[data-orientation="vertical"]": {
24
+ flexDirection: "column",
25
+ overflowX: "hidden",
26
+ overflowY: "auto",
27
+ scrollSnapType: "y mandatory",
28
+ height: "20rem",
29
+ },
30
+ },
31
+ "@media": {
32
+ "(prefers-reduced-motion: reduce)": {
33
+ scrollBehavior: "auto",
34
+ },
35
+ },
36
+ });
37
+
38
+ export const carousel__item = style({
39
+ flex: "0 0 100%",
40
+ minWidth: 0,
41
+ scrollSnapAlign: "start",
42
+ scrollSnapStop: "always",
43
+ selectors: {
44
+ "&[data-orientation="vertical"]": {
45
+ flexBasis: "auto",
46
+ },
47
+ },
48
+ });
49
+
50
+ export const carousel__nav = style({
51
+ position: "absolute",
52
+ top: "50%",
53
+ width: "2rem",
54
+ height: "2rem",
55
+ display: "inline-flex",
56
+ alignItems: "center",
57
+ justifyContent: "center",
58
+ background: "var(--background)",
59
+ color: "var(--foreground)",
60
+ border: "1px solid var(--border)",
61
+ borderRadius: "999px",
62
+ cursor: "pointer",
63
+ transform: "translateY(-50%)",
64
+ zIndex: 1,
65
+ transition: "opacity var(--duration-fast) ease,\n background var(--duration-fast) ease",
66
+ selectors: {
67
+ "&:hover:not(:disabled)": {
68
+ background: "var(--background-muted)",
69
+ },
70
+ "&:focus-visible": {
71
+ outline: "var(--border-width-strong) solid var(--foreground)",
72
+ outlineOffset: "2px",
73
+ },
74
+ "&:disabled": {
75
+ opacity: 0.4,
76
+ cursor: "not-allowed",
77
+ },
78
+ "&[data-orientation="vertical"]": {
79
+ top: "auto",
80
+ left: "50%",
81
+ transform: "translateX(-50%)",
82
+ },
83
+ },
84
+ "@media": {
85
+ "(prefers-reduced-motion: reduce)": {
86
+ transition: "none",
87
+ },
88
+ },
89
+ });
90
+
91
+ export const carouselNavPrev = style({
92
+ left: "-1rem",
93
+ selectors: {
94
+ "&[data-orientation="vertical"]": {
95
+ top: "-1rem",
96
+ left: "50%",
97
+ },
98
+ },
99
+ });
100
+
101
+ export const carouselNavNext = style({
102
+ right: "-1rem",
103
+ selectors: {
104
+ "&[data-orientation="vertical"]": {
105
+ bottom: "-1rem",
106
+ top: "auto",
107
+ left: "50%",
108
+ },
109
+ },
110
+ });
111
+
112
+ export const carousel__indicators = style({
113
+ display: "flex",
114
+ justifyContent: "center",
115
+ alignItems: "center",
116
+ gap: "var(--space-2)",
117
+ marginTop: "var(--space-3)",
118
+ selectors: {
119
+ "&[data-orientation="vertical"]": {
120
+ position: "absolute",
121
+ top: "50%",
122
+ right: "0.5rem",
123
+ marginTop: 0,
124
+ flexDirection: "column",
125
+ transform: "translateY(-50%)",
126
+ },
127
+ },
128
+ });
129
+
130
+ export const carousel__indicator = style({
131
+ width: "0.5rem",
132
+ height: "0.5rem",
133
+ padding: 0,
134
+ background: "var(--border)",
135
+ border: "none",
136
+ borderRadius: "999px",
137
+ cursor: "pointer",
138
+ transition: "background var(--duration-fast) ease,\n transform var(--duration-fast) ease",
139
+ selectors: {
140
+ "&:hover": {
141
+ background: "var(--border-strong)",
142
+ },
143
+ "&[data-active]": {
144
+ background: "var(--foreground)",
145
+ transform: "scale(1.2)",
146
+ },
147
+ "&:focus-visible": {
148
+ outline: "var(--border-width-strong) solid var(--foreground)",
149
+ outlineOffset: "2px",
150
+ },
151
+ },
152
+ "@media": {
153
+ "(prefers-reduced-motion: reduce)": {
154
+ transition: "none",
155
+ },
156
+ },
157
+ });
158
+
159
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
160
+ export const byKey: Record<string, string> = {
161
+ "carousel": carousel,
162
+ "carousel__content": carousel__content,
163
+ "carousel__item": carousel__item,
164
+ "carousel__nav": carousel__nav,
165
+ "carousel__nav--prev": carouselNavPrev,
166
+ "carousel__nav--next": carouselNavNext,
167
+ "carousel__indicators": carousel__indicators,
168
+ "carousel__indicator": carousel__indicator,
169
+ };
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+ import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
3
+ import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group";
4
+ import { byKey, checkbox, checkbox__indicator, checkboxGroup } from "./styles.css";
5
+
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ /* ───────────── Checkbox ───────────── */
9
+
10
+ export type CheckboxProps = Omit<
11
+ React.ComponentPropsWithoutRef<typeof BaseCheckbox.Root>,
12
+ "className"
13
+ > & {
14
+ className?: string;
15
+ };
16
+
17
+ /**
18
+ * 폼 제출 시 함께 적용되는 다중 선택. `indeterminate`로 부분 선택 상태를
19
+ * 표현할 수 있고, 여러 개를 묶을 때는 `CheckboxGroup`으로 감싸 그룹 단위 상태를 관리한다.
20
+ */
21
+ export const Checkbox = React.forwardRef<HTMLElement, CheckboxProps>(
22
+ ({ className, ...props }, ref) => (
23
+ <BaseCheckbox.Root
24
+ ref={ref}
25
+ className={cn(checkbox, className)}
26
+ {...props}
27
+ >
28
+ <BaseCheckbox.Indicator className={checkbox__indicator}>
29
+ {props.indeterminate ? <MinusIcon /> : <CheckIcon />}
30
+ </BaseCheckbox.Indicator>
31
+ </BaseCheckbox.Root>
32
+ ),
33
+ );
34
+ Checkbox.displayName = "Checkbox";
35
+
36
+ /* ───────────── CheckboxGroup ───────────── */
37
+
38
+ export type CheckboxGroupProps = Omit<
39
+ React.ComponentPropsWithoutRef<typeof BaseCheckboxGroup>,
40
+ "className"
41
+ > & {
42
+ className?: string;
43
+ /**
44
+ * 그룹 내 체크박스 배치 방향.
45
+ * - `vertical` — 세로 나열 (기본)
46
+ * - `horizontal` — 가로 나열. 짧은 라벨 2~3개에만 권장
47
+ *
48
+ * @default "vertical"
49
+ */
50
+ orientation?: "horizontal" | "vertical";
51
+ };
52
+
53
+ /**
54
+ * 여러 Checkbox를 묶는 컨테이너. `value`/`onValueChange`로 선택된 값 배열을 관리하고,
55
+ * `orientation`으로 가로/세로 배치를 정한다. 그룹 라벨은 외부 `<Label>`로 제공할 것.
56
+ */
57
+ export const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
58
+ ({ className, orientation = "vertical", ...props }, ref) => (
59
+ <BaseCheckboxGroup
60
+ ref={ref}
61
+ className={cn(checkboxGroup, className)}
62
+ data-orientation={orientation}
63
+ {...props}
64
+ />
65
+ ),
66
+ );
67
+ CheckboxGroup.displayName = "CheckboxGroup";
68
+
69
+ /* ───────────── Icons ───────────── */
70
+
71
+ function CheckIcon() {
72
+ return (
73
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
74
+ <path
75
+ d="M3.5 8.5l3 3 6-7"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ />
81
+ </svg>
82
+ );
83
+ }
84
+
85
+ function MinusIcon() {
86
+ return (
87
+ <svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden>
88
+ <path
89
+ d="M4 8h8"
90
+ stroke="currentColor"
91
+ strokeWidth="2"
92
+ strokeLinecap="round"
93
+ />
94
+ </svg>
95
+ );
96
+ }
@@ -0,0 +1,74 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const checkbox = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ justifyContent: "center",
7
+ width: "1.125rem",
8
+ height: "1.125rem",
9
+ border: "1px solid var(--border-strong)",
10
+ borderRadius: "calc(var(--radius) - 2px)",
11
+ background: "var(--background)",
12
+ color: "var(--primary-foreground)",
13
+ cursor: "pointer",
14
+ flexShrink: 0,
15
+ transition: "background-color var(--duration-fast), border-color var(--duration-fast)",
16
+ WebkitTapHighlightColor: "transparent",
17
+ selectors: {
18
+ "&:hover:not([data-disabled])": {
19
+ borderColor: "var(--foreground)",
20
+ },
21
+ "&:focus-visible": {
22
+ outline: "var(--border-width-strong) solid var(--foreground)",
23
+ outlineOffset: "2px",
24
+ },
25
+ "&[data-checked]": {
26
+ background: "var(--primary)",
27
+ borderColor: "var(--primary)",
28
+ },
29
+ "&[data-indeterminate]": {
30
+ background: "var(--primary)",
31
+ borderColor: "var(--primary)",
32
+ },
33
+ "&[data-disabled]": {
34
+ opacity: "var(--opacity-disabled)",
35
+ cursor: "not-allowed",
36
+ },
37
+ },
38
+ "@media": {
39
+ "(hover: none) and (pointer: coarse)": {
40
+ width: "1.25rem",
41
+ height: "1.25rem",
42
+ },
43
+ "(prefers-reduced-motion: reduce)": {
44
+ transition: "none",
45
+ },
46
+ },
47
+ });
48
+
49
+ export const checkbox__indicator = style({
50
+ display: "inline-flex",
51
+ alignItems: "center",
52
+ justifyContent: "center",
53
+ });
54
+
55
+ export const checkboxGroup = style({
56
+ display: "flex",
57
+ gap: "0.625rem",
58
+ selectors: {
59
+ "&[data-orientation="vertical"]": {
60
+ flexDirection: "column",
61
+ },
62
+ "&[data-orientation="horizontal"]": {
63
+ flexDirection: "row",
64
+ flexWrap: "wrap",
65
+ },
66
+ },
67
+ });
68
+
69
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
70
+ export const byKey: Record<string, string> = {
71
+ "checkbox": checkbox,
72
+ "checkbox__indicator": checkbox__indicator,
73
+ "checkbox-group": checkboxGroup,
74
+ };
@@ -0,0 +1,230 @@
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
+ import { byKey, codeEditor } from "./styles.css";
13
+
14
+ import { cn } from "@SH_UI_UTILS@";
15
+ export type CodeEditorLanguage =
16
+ | "text"
17
+ | "javascript"
18
+ | "typescript"
19
+ | "jsx"
20
+ | "tsx"
21
+ | "json"
22
+ | "css"
23
+ | "html"
24
+ | "markdown";
25
+
26
+ export interface CodeEditorProps {
27
+ /**
28
+ * Controlled — 현재 코드. 명시 시 value 가 진실원천이 되고 onChange 로 외부에서 갱신해야 한다.
29
+ * 미지정이면 uncontrolled — 에디터가 자체 내부 문서로 동작.
30
+ */
31
+ value?: string;
32
+ /**
33
+ * Uncontrolled 초기값. value 미지정 시에만 사용된다.
34
+ * @default ""
35
+ */
36
+ defaultValue?: string;
37
+ /** 코드가 바뀔 때마다 호출 (controlled · uncontrolled 모두). */
38
+ onChange?: (value: string) => void;
39
+ /**
40
+ * 신택스 하이라이팅 언어.
41
+ * @default "text"
42
+ */
43
+ language?: CodeEditorLanguage;
44
+ /** 비어 있을 때 표시할 placeholder. */
45
+ placeholder?: string;
46
+ /** 읽기 전용. 키 입력은 막지만 선택·복사는 가능. */
47
+ readOnly?: boolean;
48
+ /**
49
+ * 좌측 줄 번호 표시 여부.
50
+ * @default true
51
+ */
52
+ showLineNumbers?: boolean;
53
+ /** 에디터 최소 높이 (CSS 길이 단위). */
54
+ minHeight?: string;
55
+ /** 에디터 최대 높이 (CSS 길이 단위). 초과 시 내부 스크롤. */
56
+ maxHeight?: string;
57
+ className?: string;
58
+ id?: string;
59
+ "aria-label"?: string;
60
+ "aria-labelledby"?: string;
61
+ }
62
+
63
+
64
+ function languageExtension(language: CodeEditorLanguage): Extension {
65
+ switch (language) {
66
+ case "javascript":
67
+ return javascript();
68
+ case "typescript":
69
+ return javascript({ typescript: true });
70
+ case "jsx":
71
+ return javascript({ jsx: true });
72
+ case "tsx":
73
+ return javascript({ jsx: true, typescript: true });
74
+ case "json":
75
+ return json();
76
+ case "css":
77
+ return cssLang();
78
+ case "html":
79
+ return html();
80
+ case "markdown":
81
+ return markdown();
82
+ case "text":
83
+ default:
84
+ return [];
85
+ }
86
+ }
87
+
88
+ /**
89
+ * CodeMirror 6 기반 인라인 코드 에디터.
90
+ *
91
+ * Controlled (value/onChange) · Uncontrolled (defaultValue + 선택 onChange) 모두 지원.
92
+ * 라우터·외부 상태와 동기화할 게 없는 경우 defaultValue 한 줄로 끝 — useState 불필요.
93
+ *
94
+ * 신택스 하이라이팅·자동 들여쓰기·괄호 매칭 등은 CodeMirror `basicSetup` 을 그대로 사용,
95
+ * 컬러·여백은 sh-ui 토큰(`--background`, `--foreground`, `--border` 등)으로 매핑돼 테마에 자동 추종.
96
+ */
97
+ export function CodeEditor({
98
+ value: valueProp,
99
+ defaultValue,
100
+ onChange,
101
+ language = "text",
102
+ placeholder,
103
+ readOnly = false,
104
+ showLineNumbers = true,
105
+ minHeight,
106
+ maxHeight,
107
+ className,
108
+ id,
109
+ "aria-label": ariaLabel,
110
+ "aria-labelledby": ariaLabelledBy,
111
+ }: CodeEditorProps) {
112
+ const isControlled = valueProp !== undefined;
113
+ const hostRef = useRef<HTMLDivElement>(null);
114
+ const viewRef = useRef<EditorView | null>(null);
115
+ const onChangeRef = useRef(onChange);
116
+ onChangeRef.current = onChange;
117
+ const initialDocRef = useRef(valueProp ?? defaultValue ?? "");
118
+
119
+ const compartments = useMemo(
120
+ () => ({
121
+ language: new Compartment(),
122
+ readOnly: new Compartment(),
123
+ lineNumbers: new Compartment(),
124
+ placeholder: new Compartment(),
125
+ }),
126
+ [],
127
+ );
128
+
129
+ useEffect(() => {
130
+ if (!hostRef.current) return;
131
+
132
+ const extensions: Extension[] = [
133
+ basicSetup,
134
+ EditorView.lineWrapping,
135
+ EditorView.updateListener.of((update) => {
136
+ if (update.docChanged) {
137
+ onChangeRef.current?.(update.state.doc.toString());
138
+ }
139
+ }),
140
+ compartments.language.of(languageExtension(language)),
141
+ compartments.readOnly.of(EditorState.readOnly.of(readOnly)),
142
+ compartments.lineNumbers.of(
143
+ showLineNumbers
144
+ ? []
145
+ : EditorView.theme({ ".cm-gutters": { display: "none" } }),
146
+ ),
147
+ compartments.placeholder.of(placeholder ? placeholderExt(placeholder) : []),
148
+ ];
149
+
150
+ const view = new EditorView({
151
+ state: EditorState.create({ doc: initialDocRef.current, extensions }),
152
+ parent: hostRef.current,
153
+ });
154
+ viewRef.current = view;
155
+
156
+ return () => {
157
+ view.destroy();
158
+ viewRef.current = null;
159
+ };
160
+ // 초기 마운트 1회만 — 후속 동기화는 별도 이펙트가 처리
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ }, []);
163
+
164
+ // controlled 모드에서만 외부 value 를 에디터 doc 에 동기화. uncontrolled 면 에디터가 자체 source-of-truth.
165
+ useEffect(() => {
166
+ if (!isControlled) return;
167
+ const view = viewRef.current;
168
+ if (!view) return;
169
+ const current = view.state.doc.toString();
170
+ if (current === valueProp) return;
171
+ view.dispatch({
172
+ changes: { from: 0, to: current.length, insert: valueProp ?? "" },
173
+ });
174
+ }, [isControlled, valueProp]);
175
+
176
+ useEffect(() => {
177
+ viewRef.current?.dispatch({
178
+ effects: compartments.language.reconfigure(languageExtension(language)),
179
+ });
180
+ }, [language, compartments.language]);
181
+
182
+ useEffect(() => {
183
+ viewRef.current?.dispatch({
184
+ effects: compartments.readOnly.reconfigure(EditorState.readOnly.of(readOnly)),
185
+ });
186
+ }, [readOnly, compartments.readOnly]);
187
+
188
+ useEffect(() => {
189
+ viewRef.current?.dispatch({
190
+ effects: compartments.lineNumbers.reconfigure(
191
+ showLineNumbers
192
+ ? []
193
+ : EditorView.theme({ ".cm-gutters": { display: "none" } }),
194
+ ),
195
+ });
196
+ }, [showLineNumbers, compartments.lineNumbers]);
197
+
198
+ useEffect(() => {
199
+ viewRef.current?.dispatch({
200
+ effects: compartments.placeholder.reconfigure(
201
+ placeholder ? placeholderExt(placeholder) : [],
202
+ ),
203
+ });
204
+ }, [placeholder, compartments.placeholder]);
205
+
206
+ useEffect(() => {
207
+ const view = viewRef.current;
208
+ if (!view) return;
209
+ const node = view.contentDOM;
210
+ if (id) node.id = id;
211
+ if (ariaLabel) node.setAttribute("aria-label", ariaLabel);
212
+ else node.removeAttribute("aria-label");
213
+ if (ariaLabelledBy) node.setAttribute("aria-labelledby", ariaLabelledBy);
214
+ else node.removeAttribute("aria-labelledby");
215
+ }, [id, ariaLabel, ariaLabelledBy]);
216
+
217
+ return (
218
+ <div
219
+ ref={hostRef}
220
+ className={cn(codeEditor, className)}
221
+ data-readonly={readOnly || undefined}
222
+ style={
223
+ {
224
+ "--sh-ui-code-editor-min-height": minHeight,
225
+ "--sh-ui-code-editor-max-height": maxHeight,
226
+ } as React.CSSProperties
227
+ }
228
+ />
229
+ );
230
+ }
@@ -0,0 +1,97 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const codeEditor = style({
4
+ position: "relative",
5
+ border: "1px solid var(--border)",
6
+ borderRadius: "var(--radius)",
7
+ background: "var(--background)",
8
+ fontSize: "0.8125rem",
9
+ lineHeight: 1.6,
10
+ overflow: "hidden",
11
+ transition: "border-color var(--duration-fast)",
12
+ selectors: {
13
+ "&:focus-within": {
14
+ borderColor: "var(--foreground)",
15
+ outline: "var(--border-width-strong) solid var(--foreground)",
16
+ outlineOffset: "2px",
17
+ },
18
+ "&[data-readonly]": {
19
+ background: "var(--background-subtle)",
20
+ },
21
+ "& .cm-editor": {
22
+ background: "transparent",
23
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
24
+ },
25
+ "& .cm-editor.cm-focused": {
26
+ outline: "none",
27
+ },
28
+ "& .cm-scroller": {
29
+ fontFamily: "inherit",
30
+ minHeight: "var(--sh-ui-code-editor-min-height, 7.5rem)",
31
+ maxHeight: "var(--sh-ui-code-editor-max-height, 25rem)",
32
+ },
33
+ "& .cm-content": {
34
+ caretColor: "var(--foreground)",
35
+ color: "var(--foreground)",
36
+ padding: "var(--space-3) 0",
37
+ },
38
+ "& .cm-line": {
39
+ padding: "0 var(--space-3)",
40
+ },
41
+ "& .cm-gutters": {
42
+ background: "var(--background-subtle)",
43
+ color: "var(--foreground-muted)",
44
+ borderRight: "1px solid var(--border)",
45
+ },
46
+ "& .cm-activeLineGutter": {
47
+ background: "var(--background-muted)",
48
+ },
49
+ "& .cm-activeLine": {
50
+ background: "var(--background-muted)",
51
+ },
52
+ "& .cm-cursor": {
53
+ borderLeftColor: "var(--foreground)",
54
+ },
55
+ "& .cm-dropCursor": {
56
+ borderLeftColor: "var(--foreground)",
57
+ },
58
+ "& .cm-selectionBackground": {
59
+ background: "var(--background-muted) !important",
60
+ },
61
+ "& .cm-editor .cm-selectionBackground": {
62
+ background: "var(--background-muted) !important",
63
+ },
64
+ "& .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": {
65
+ background: "var(--background-muted) !important",
66
+ },
67
+ "& ::selection": {
68
+ background: "var(--background-muted) !important",
69
+ },
70
+ "& .cm-placeholder": {
71
+ color: "var(--foreground-muted)",
72
+ },
73
+ "& .cm-tooltip": {
74
+ background: "var(--background)",
75
+ border: "1px solid var(--border)",
76
+ color: "var(--foreground)",
77
+ borderRadius: "calc(var(--radius) - 2px)",
78
+ },
79
+ "& .cm-tooltip-autocomplete > ul > li[aria-selected]": {
80
+ background: "var(--background-muted)",
81
+ color: "var(--foreground)",
82
+ },
83
+ "& .cm-matchingBracket": {
84
+ background: "var(--background-muted)",
85
+ color: "var(--foreground)",
86
+ },
87
+ "& .cm-nonmatchingBracket": {
88
+ background: "var(--background-muted)",
89
+ color: "var(--foreground)",
90
+ },
91
+ },
92
+ });
93
+
94
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
95
+ export const byKey: Record<string, string> = {
96
+ "code-editor": codeEditor,
97
+ };