sh-ui-cli 0.46.0 → 0.48.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 +25 -0
  2. package/data/registry/react/components/accordion/index.module.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.module.css +111 -0
  4. package/data/registry/react/components/avatar/index.module.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.module.css +36 -0
  6. package/data/registry/react/components/badge/index.module.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.module.css +57 -0
  8. package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
  10. package/data/registry/react/components/calendar/index.module.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.module.css +213 -0
  12. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.module.css +155 -0
  14. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  16. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  18. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  20. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  22. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.module.css +151 -0
  24. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  26. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  28. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.module.css +127 -0
  30. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  32. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  34. package/data/registry/react/components/form/index.module.tsx +61 -0
  35. package/data/registry/react/components/form/styles.module.css +47 -0
  36. package/data/registry/react/components/header/index.module.tsx +805 -0
  37. package/data/registry/react/components/header/styles.module.css +350 -0
  38. package/data/registry/react/components/label/index.module.tsx +52 -0
  39. package/data/registry/react/components/label/styles.module.css +90 -0
  40. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  42. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.module.css +45 -0
  44. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  46. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  48. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.module.css +105 -0
  50. package/data/registry/react/components/popover/index.module.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.module.css +65 -0
  52. package/data/registry/react/components/progress/index.module.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.module.css +41 -0
  54. package/data/registry/react/components/radio/index.module.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.module.css +80 -0
  56. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  58. package/data/registry/react/components/select/index.module.tsx +234 -0
  59. package/data/registry/react/components/select/styles.module.css +193 -0
  60. package/data/registry/react/components/separator/index.module.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.module.css +15 -0
  62. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  64. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  66. package/data/registry/react/components/slider/index.module.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.module.css +64 -0
  68. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.module.css +37 -0
  70. package/data/registry/react/components/switch/index.module.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.module.css +83 -0
  72. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.module.css +148 -0
  74. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.module.css +54 -0
  76. package/data/registry/react/components/toast/index.module.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.module.css +290 -0
  78. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.module.css +85 -0
  80. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  82. package/data/registry/react/registry.json +560 -0
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. package/src/constants.js +4 -3
@@ -0,0 +1,155 @@
1
+ .carousel {
2
+ position: relative;
3
+ width: 100%;
4
+ }
5
+
6
+ .carousel__content {
7
+ display: flex;
8
+ gap: var(--space-4);
9
+ overflow-x: auto;
10
+ overflow-y: hidden;
11
+ scroll-snap-type: x mandatory;
12
+ scroll-behavior: smooth;
13
+ scrollbar-width: none;
14
+ -ms-overflow-style: none;
15
+ -webkit-overflow-scrolling: touch;
16
+ overscroll-behavior-inline: contain;
17
+ }
18
+
19
+ .carousel__content::-webkit-scrollbar {
20
+ display: none;
21
+ }
22
+
23
+ .carousel__content[data-orientation="vertical"] {
24
+ flex-direction: column;
25
+ overflow-x: hidden;
26
+ overflow-y: auto;
27
+ scroll-snap-type: y mandatory;
28
+ height: 20rem;
29
+ }
30
+
31
+ .carousel__item {
32
+ flex: 0 0 100%;
33
+ min-width: 0;
34
+ scroll-snap-align: start;
35
+ scroll-snap-stop: always;
36
+ }
37
+
38
+ .carousel__item[data-orientation="vertical"] {
39
+ flex-basis: auto;
40
+ }
41
+
42
+ /* 네비게이션 버튼 — 기본은 플로팅 원형. 원하면 className으로 덮어쓰기. */
43
+ .carousel__nav {
44
+ position: absolute;
45
+ top: 50%;
46
+ width: 2rem;
47
+ height: 2rem;
48
+ display: inline-flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ background: var(--background);
52
+ color: var(--foreground);
53
+ border: 1px solid var(--border);
54
+ border-radius: 999px;
55
+ cursor: pointer;
56
+ transform: translateY(-50%);
57
+ z-index: 1;
58
+ transition:
59
+ opacity var(--duration-fast) ease,
60
+ background var(--duration-fast) ease;
61
+ }
62
+
63
+ .carousel__nav:hover:not(:disabled) {
64
+ background: var(--background-muted);
65
+ }
66
+
67
+ .carousel__nav:focus-visible {
68
+ outline: var(--border-width-strong) solid var(--foreground);
69
+ outline-offset: 2px;
70
+ }
71
+
72
+ .carousel__nav:disabled {
73
+ opacity: 0.4;
74
+ cursor: not-allowed;
75
+ }
76
+
77
+ .carousel__nav--prev {
78
+ left: -1rem;
79
+ }
80
+
81
+ .carousel__nav--next {
82
+ right: -1rem;
83
+ }
84
+
85
+ .carousel__nav[data-orientation="vertical"] {
86
+ top: auto;
87
+ left: 50%;
88
+ transform: translateX(-50%);
89
+ }
90
+
91
+ .carousel__nav--prev[data-orientation="vertical"] {
92
+ top: -1rem;
93
+ left: 50%;
94
+ }
95
+
96
+ .carousel__nav--next[data-orientation="vertical"] {
97
+ bottom: -1rem;
98
+ top: auto;
99
+ left: 50%;
100
+ }
101
+
102
+ /* 인디케이터 */
103
+ .carousel__indicators {
104
+ display: flex;
105
+ justify-content: center;
106
+ align-items: center;
107
+ gap: var(--space-2);
108
+ margin-top: var(--space-3);
109
+ }
110
+
111
+ .carousel__indicators[data-orientation="vertical"] {
112
+ position: absolute;
113
+ top: 50%;
114
+ right: 0.5rem;
115
+ margin-top: 0;
116
+ flex-direction: column;
117
+ transform: translateY(-50%);
118
+ }
119
+
120
+ .carousel__indicator {
121
+ width: 0.5rem;
122
+ height: 0.5rem;
123
+ padding: 0;
124
+ background: var(--border);
125
+ border: none;
126
+ border-radius: 999px;
127
+ cursor: pointer;
128
+ transition:
129
+ background var(--duration-fast) ease,
130
+ transform var(--duration-fast) ease;
131
+ }
132
+
133
+ .carousel__indicator:hover {
134
+ background: var(--border-strong);
135
+ }
136
+
137
+ .carousel__indicator[data-active] {
138
+ background: var(--foreground);
139
+ transform: scale(1.2);
140
+ }
141
+
142
+ .carousel__indicator:focus-visible {
143
+ outline: var(--border-width-strong) solid var(--foreground);
144
+ outline-offset: 2px;
145
+ }
146
+
147
+ @media (prefers-reduced-motion: reduce) {
148
+ .carousel__content {
149
+ scroll-behavior: auto;
150
+ }
151
+ .carousel__nav,
152
+ .carousel__indicator {
153
+ transition: none;
154
+ }
155
+ }
@@ -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 styles from "./styles.module.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(styles.checkbox, className)}
26
+ {...props}
27
+ >
28
+ <BaseCheckbox.Indicator className={styles.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(styles["checkbox-group"], 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,75 @@
1
+ /* ───────────── Checkbox ───────────── */
2
+
3
+ .checkbox {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 1.125rem;
8
+ height: 1.125rem;
9
+ border: 1px solid var(--border-strong);
10
+ border-radius: calc(var(--radius) - 2px);
11
+ background: var(--background);
12
+ color: var(--primary-foreground);
13
+ cursor: pointer;
14
+ flex-shrink: 0;
15
+ transition: background-color var(--duration-fast), border-color var(--duration-fast);
16
+ -webkit-tap-highlight-color: transparent;
17
+ }
18
+
19
+ .checkbox:hover:not([data-disabled]) {
20
+ border-color: var(--foreground);
21
+ }
22
+
23
+ .checkbox:focus-visible {
24
+ outline: var(--border-width-strong) solid var(--foreground);
25
+ outline-offset: 2px;
26
+ }
27
+
28
+ .checkbox[data-checked],
29
+ .checkbox[data-indeterminate] {
30
+ background: var(--primary);
31
+ border-color: var(--primary);
32
+ }
33
+
34
+ .checkbox[data-disabled] {
35
+ opacity: var(--opacity-disabled);
36
+ cursor: not-allowed;
37
+ }
38
+
39
+ /* 모바일/터치: 최소 탭 영역 */
40
+ @media (hover: none) and (pointer: coarse) {
41
+ .checkbox {
42
+ width: 1.25rem;
43
+ height: 1.25rem;
44
+ }
45
+ }
46
+
47
+ /* ───────────── Indicator ───────────── */
48
+
49
+ .checkbox__indicator {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ }
54
+
55
+ /* ───────────── CheckboxGroup ───────────── */
56
+
57
+ .checkbox-group {
58
+ display: flex;
59
+ gap: 0.625rem;
60
+ }
61
+
62
+ .checkbox-group[data-orientation="vertical"] {
63
+ flex-direction: column;
64
+ }
65
+
66
+ .checkbox-group[data-orientation="horizontal"] {
67
+ flex-direction: row;
68
+ flex-wrap: wrap;
69
+ }
70
+
71
+ @media (prefers-reduced-motion: reduce) {
72
+ .checkbox {
73
+ transition: none;
74
+ }
75
+ }
@@ -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 styles from "./styles.module.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(styles["code-editor"], 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,76 @@
1
+ .code-editor {
2
+ position: relative;
3
+ border: 1px solid var(--border);
4
+ border-radius: var(--radius);
5
+ background: var(--background);
6
+ font-size: 0.8125rem;
7
+ line-height: 1.6;
8
+ overflow: hidden;
9
+ transition: border-color var(--duration-fast);
10
+ }
11
+ .code-editor:focus-within {
12
+ border-color: var(--foreground);
13
+ outline: var(--border-width-strong) solid var(--foreground);
14
+ outline-offset: 2px;
15
+ }
16
+ .code-editor[data-readonly] {
17
+ background: var(--background-subtle);
18
+ }
19
+
20
+ .code-editor .cm-editor {
21
+ background: transparent;
22
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
+ }
24
+ .code-editor .cm-editor.cm-focused {
25
+ outline: none;
26
+ }
27
+ .code-editor .cm-scroller {
28
+ font-family: inherit;
29
+ min-height: var(--sh-ui-code-editor-min-height, 7.5rem);
30
+ max-height: var(--sh-ui-code-editor-max-height, 25rem);
31
+ }
32
+ .code-editor .cm-content {
33
+ caret-color: var(--foreground);
34
+ color: var(--foreground);
35
+ padding: var(--space-3) 0;
36
+ }
37
+ .code-editor .cm-line {
38
+ padding: 0 var(--space-3);
39
+ }
40
+ .code-editor .cm-gutters {
41
+ background: var(--background-subtle);
42
+ color: var(--foreground-muted);
43
+ border-right: 1px solid var(--border);
44
+ }
45
+ .code-editor .cm-activeLineGutter,
46
+ .code-editor .cm-activeLine {
47
+ background: var(--background-muted);
48
+ }
49
+ .code-editor .cm-cursor,
50
+ .code-editor .cm-dropCursor {
51
+ border-left-color: var(--foreground);
52
+ }
53
+ .code-editor .cm-selectionBackground,
54
+ .code-editor .cm-editor .cm-selectionBackground,
55
+ .code-editor .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
56
+ .code-editor ::selection {
57
+ background: var(--background-muted) !important;
58
+ }
59
+ .code-editor .cm-placeholder {
60
+ color: var(--foreground-muted);
61
+ }
62
+ .code-editor .cm-tooltip {
63
+ background: var(--background);
64
+ border: 1px solid var(--border);
65
+ color: var(--foreground);
66
+ border-radius: calc(var(--radius) - 2px);
67
+ }
68
+ .code-editor .cm-tooltip-autocomplete > ul > li[aria-selected] {
69
+ background: var(--background-muted);
70
+ color: var(--foreground);
71
+ }
72
+ .code-editor .cm-matchingBracket,
73
+ .code-editor .cm-nonmatchingBracket {
74
+ background: var(--background-muted);
75
+ color: var(--foreground);
76
+ }