sh-ui-cli 0.24.0 → 0.31.1

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 (28) hide show
  1. package/README.md +8 -9
  2. package/data/changelog/versions.json +117 -1
  3. package/data/registry/react/components/code-editor/index.tsx +232 -0
  4. package/data/registry/react/components/code-editor/styles.css +76 -0
  5. package/data/registry/react/components/code-tabs/index.tsx +49 -0
  6. package/data/registry/react/components/header/index.tsx +632 -82
  7. package/data/registry/react/components/header/styles.css +169 -9
  8. package/data/registry/react/components/markdown-editor/index.tsx +121 -0
  9. package/data/registry/react/components/markdown-editor/styles.css +160 -0
  10. package/data/registry/react/components/page-toc/index.tsx +175 -0
  11. package/data/registry/react/components/page-toc/styles.css +82 -0
  12. package/data/registry/react/components/rich-text-editor/index.tsx +350 -0
  13. package/data/registry/react/components/rich-text-editor/styles.css +196 -0
  14. package/data/registry/react/registry.json +100 -0
  15. package/data/summaries/react.json +6 -1
  16. package/package.json +1 -1
  17. package/src/mcp.mjs +0 -1
  18. package/templates/flutter-standalone/README.md +2 -2
  19. package/templates/monorepo/README.md +4 -4
  20. package/templates/nextjs-app/app/api/proxy/[...path]/route.ts +85 -0
  21. package/templates/nextjs-app/src/shared/api/apiTypes.ts +21 -0
  22. package/templates/nextjs-app/src/shared/api/error.ts +12 -0
  23. package/templates/nextjs-app/src/shared/api/http.ts +56 -0
  24. package/templates/nextjs-standalone/README.md +3 -3
  25. package/templates/nextjs-standalone/app/api/proxy/[...path]/route.ts +85 -0
  26. package/templates/nextjs-standalone/src/shared/api/apiTypes.ts +21 -0
  27. package/templates/nextjs-standalone/src/shared/api/error.ts +12 -0
  28. package/templates/nextjs-standalone/src/shared/api/http.ts +56 -0
package/README.md CHANGED
@@ -18,7 +18,6 @@ npx sh-ui-cli <command>
18
18
 
19
19
  ```bash
20
20
  # 대화형
21
- npm create sh-ui my-app
22
21
  npx sh-ui-cli create
23
22
 
24
23
  # 비대화형 (에이전트 / CI)
@@ -32,7 +31,7 @@ npx sh-ui-cli create my-app --platform flutter --yes
32
31
  ### init — 설정 파일 생성
33
32
 
34
33
  ```bash
35
- npx sh-ui init
34
+ npx sh-ui-cli init
36
35
  # 대화형 프롬프트:
37
36
  # platform: react | flutter
38
37
  # base: neutral | zinc | slate
@@ -43,27 +42,27 @@ npx sh-ui init
43
42
  비대화형 예:
44
43
 
45
44
  ```bash
46
- npx sh-ui init --platform react --base neutral --radius md --mode light-dark --yes
45
+ npx sh-ui-cli init --platform react --base neutral --radius md --mode light-dark --yes
47
46
  ```
48
47
 
49
48
  ### add — 컴포넌트 추가
50
49
 
51
50
  ```bash
52
- npx sh-ui add button
53
- npx sh-ui add card input
54
- npx sh-ui add button --diff # 파일 변경 미리보기(실제 쓰지 않음)
51
+ npx sh-ui-cli add button
52
+ npx sh-ui-cli add card input
53
+ npx sh-ui-cli add button --diff # 파일 변경 미리보기(실제 쓰지 않음)
55
54
  ```
56
55
 
57
56
  ### list — 설치된 컴포넌트 목록
58
57
 
59
58
  ```bash
60
- npx sh-ui list
59
+ npx sh-ui-cli list
61
60
  ```
62
61
 
63
62
  ### remove — 컴포넌트 제거
64
63
 
65
64
  ```bash
66
- npx sh-ui remove button
65
+ npx sh-ui-cli remove button
67
66
  ```
68
67
 
69
68
  ### mcp — AI 에게 sh-ui 를 알려주기 (v0.21.0+)
@@ -85,7 +84,7 @@ npx -y sh-ui-cli mcp init --client claude-desktop # → 사용자 전역 (재
85
84
  npx -y sh-ui-cli mcp init --client claude-code --scope user
86
85
  ```
87
86
 
88
- > 참고: 위 다른 명령들의 `npx sh-ui ...` 표기는 `sh-ui-cli` 가 dev 의존성으로 설치된 상태를 가정한다. 빈 폴더에서 한 번에 쓰려면 동일하게 `npx -y sh-ui-cli ...` 로 호출.
87
+ > 참고: 위 다른 명령들의 `npx sh-ui-cli ...` 표기는 `sh-ui-cli` 가 dev 의존성으로 설치된 상태를 가정한다. 빈 폴더에서 한 번에 쓰려면 동일하게 `npx -y sh-ui-cli ...` 로 호출.
89
88
 
90
89
  기존 설정 파일이 있으면 다른 MCP 서버 엔트리를 보존하며 `sh-ui` 만 머지·갱신.
91
90
 
@@ -2,6 +2,122 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
4
4
  "versions": [
5
+ {
6
+ "version": "0.31.1",
7
+ "date": "2026-04-28",
8
+ "title": "Header drawer 그룹 라벨을 사이드바와 동일한 캡션 스타일로 정렬",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "HeaderNavGroup 의 drawer 라벨이 uppercase + letter-spacing 으로 두드러져 같은 모바일 메뉴 안의 HeaderMenu 트리거(서브메뉴 헤더)와 시각적으로 헷갈리던 문제 해결",
12
+ "스타일을 Sidebar 의 group-label 과 동일하게 통일 — text-xs / weight-medium / foreground-muted, height 2rem, padding 0 var(--space-2) (uppercase·letter-spacing 제거)",
13
+ "그룹 라벨은 \"섹션 캡션\", 메뉴 트리거는 \"클릭 가능한 nav 항목\" 으로 역할이 명확히 갈라짐"
14
+ ],
15
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.31.1"
16
+ },
17
+ {
18
+ "version": "0.31.0",
19
+ "date": "2026-04-28",
20
+ "title": "Editor 3종(Code/Markdown/RichText) uncontrolled 모드 추가",
21
+ "type": "minor",
22
+ "highlights": [
23
+ "CodeEditor / MarkdownEditor / RichTextEditor 모두 value 를 옵셔널로 만들고 defaultValue 를 추가 — 라우터·외부 상태와 동기화할 게 없는 폼이면 useState 없이 한 줄로 끝 (Tabs/RadioGroup/Header NavMatch 와 동일 패턴)",
24
+ "controlled (value 명시) 모드는 종전과 동일하게 동작 — 외부 동기화 effect 가 isControlled 일 때만 작동하도록 가드. uncontrolled 모드에서는 에디터(CodeMirror/Tiptap)가 자체 source-of-truth",
25
+ "onChange 는 controlled / uncontrolled 양 모드 모두에서 변경마다 호출 — 외부 관찰만 필요할 땐 onChange 만 받아도 OK",
26
+ "MarkdownEditor 는 미리보기 패널이 현재 값을 필요로 하므로 uncontrolled 모드에서도 자체 React state 로 트래킹"
27
+ ],
28
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.31.0"
29
+ },
30
+ {
31
+ "version": "0.30.0",
32
+ "date": "2026-04-28",
33
+ "title": "PageTOC + CodeTabs registry 승격 — docs 인프라 컴포넌트 공개",
34
+ "type": "minor",
35
+ "highlights": [
36
+ "PageTOC 신규 — 페이지 안의 헤딩(기본 h2/h3)을 자동 스캔해 우측 레일에 목차(On this page)를 렌더. 자동 slugify·id 부여, IntersectionObserver 로 현재 보이는 섹션 active 표시(aria-current 자동), 클릭 시 smooth scroll. 라우터 비종속 — routeKey 로 라우트 변경 신호를 받아 재스캔하므로 Next.js / React Router / no-router 모두 지원. levels·containerSelector·excludeSelector·label·headerOffsetRem 으로 커스터마이즈",
37
+ "CodeTabs 신규 — 같은 예제의 여러 코드 뷰(예: React/Flutter, 또는 \"강조 / 전체 코드\")를 탭으로 전환. 각 탭은 그대로 CodePanel 로 렌더되어 shiki 하이라이팅·복사 버튼·파일명 헤더 등을 그대로 사용. registryDependencies = tabs · code-panel",
38
+ "두 컴포넌트 모두 그동안 docs 내부 전용으로 쓰이던 것을 일반화 — 이제 사용자 프로젝트에서 `npx sh-ui-cli add page-toc` / `npx sh-ui-cli add code-tabs` 로 설치 가능. /components 인덱스에도 등록"
39
+ ],
40
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.30.0"
41
+ },
42
+ {
43
+ "version": "0.29.0",
44
+ "date": "2026-04-28",
45
+ "title": "Header — 서브메뉴·그룹·a11y·active 자동 매칭 보강 + variant/stickyHide 정식 도입",
46
+ "type": "minor",
47
+ "highlights": [
48
+ "a11y — drawer 가 열릴 때 focus trap · ESC 닫기 · 닫힐 때 트리거로 포커스 복원 자동화. drawer 패널에 role=dialog + aria-modal=true 부여",
49
+ "HeaderMenu / HeaderMenuTrigger / HeaderMenuContent — 데스크탑은 document.body 로 portal 된 dropdown(부모 overflow 클리핑 회피, 트리거 위치를 scroll/resize 마다 추종, 클릭 외부·ESC 닫기), 모바일 drawer 안에서는 collapsible 로 자동 전환. NavLocationContext 가 자식 위치를 전파해 한 자식 트리로 두 모드 처리",
50
+ "HeaderNavGroup(label) — drawer 안에서 nav 항목을 섹션으로 묶음(라벨 + 들여쓰기). inline 모드는 display:contents 로 자식만 평면 렌더, 데스크탑 레이아웃 무영향",
51
+ "HeaderDesktopOnly / HeaderMobileOnly — drawer 로 옮기지 않고 단순히 가시성만 토글하는 슬롯. display:contents 라 부모 flex 흐름 그대로 유지. 데스크탑 검색/로그인/마이페이지 같은 항목을 모바일에서 자체 drawer 트리거로 대체할 때 사용",
52
+ "active 자동 매칭 — HeaderNav 의 value(controlled) 또는 defaultValue+onValueChange(uncontrolled) 로 자식 HeaderItem 의 active 가 일괄 결정 (Tabs/RadioGroup 패턴). 라우터 앱은 value=pathname, 라우터 없는 위젯은 defaultValue 만으로 클릭 자동 추적. 기본 매칭은 exact 또는 prefix(root 제외), match 함수로 커스터마이즈, 개별 active prop 으로 override. 자동/수동 어느 쪽이든 data-active 와 aria-current=\"page\" 부여",
53
+ "variant=\"solid\"|\"transparent\"|\"blur\" 로 헤더 배경 표현 전환. blur 는 표준 글래스 헤더 수준의 85% opacity + saturate(180%) blur(16px), --sh-ui-header-blur-opacity / --sh-ui-header-blur-radius CSS 변수로 instance 별 조정. backdrop-filter 미지원 브라우저는 @supports 폴백으로 불투명 배경 자동 적용",
54
+ "stickyHide + stickyHideThreshold — 스크롤 다운 시 자동 숨김. 가장 가까운 스크롤 가능 조상을 자동 감지해 페이지 스크롤뿐 아니라 컨테이너 내부 스크롤에도 반응. prefers-reduced-motion: reduce 환경에서는 슬라이드 트랜지션 비활성화"
55
+ ],
56
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.29.0"
57
+ },
58
+ {
59
+ "version": "0.28.1",
60
+ "date": "2026-04-28",
61
+ "title": "RichTextEditor — Link 버튼이 빈 선택에서 호출돼도 anchor 가 생기도록 수정",
62
+ "type": "patch",
63
+ "highlights": [
64
+ "Link 버튼: 텍스트 선택이 없고 기존 링크 위도 아닐 때 URL 자체를 anchor 텍스트로 삽입 — 빈 link 마크만 걸려 사용자가 추가로 타이핑해야 했던 어색한 상태 제거",
65
+ "선택이 있거나 기존 링크 위에서 호출하면 종전과 동일하게 selection·기존 링크에 setLink"
66
+ ],
67
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.28.1"
68
+ },
69
+ {
70
+ "version": "0.28.0",
71
+ "date": "2026-04-28",
72
+ "title": "RichTextEditor 컴포넌트 — Tiptap 3 + sh-ui 토큰 테마 + 기본 toolbar",
73
+ "type": "minor",
74
+ "highlights": [
75
+ "RichTextEditor 신규 — Tiptap 3 (StarterKit + Placeholder + Link) 위에 sh-ui 토큰 테마와 기본 toolbar 를 얹은 controlled WYSIWYG. value 는 HTML 문자열로 입출력",
76
+ "툴바: 헤딩(H1~H3) · 리스트(불릿/숫자) · 인용 · 코드(인라인/블록) · 링크 · 강조(B/I/S) · 구분선 · undo/redo. 본문 포커스를 잃지 않도록 mousedown 가로채기",
77
+ "SSR 안전 — `immediatelyRender: false` 로 Next 15+ stricter SSR 환경에서 hydration mismatch 없음. readOnly·hideToolbar·minHeight/maxHeight prop 지원",
78
+ "추가 확장(이미지·테이블·멘션 등)은 registry 카피본을 직접 수정해 `extensions` 배열에 끼워 넣는 방식으로 확장"
79
+ ],
80
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.28.0"
81
+ },
82
+ {
83
+ "version": "0.27.0",
84
+ "date": "2026-04-28",
85
+ "title": "MarkdownEditor 컴포넌트 — CodeEditor + react-markdown 라이브 프리뷰",
86
+ "type": "minor",
87
+ "highlights": [
88
+ "MarkdownEditor 신규 — CodeEditor(소스) + react-markdown(미리보기) 합성, GFM(테이블·체크리스트·strikethrough) 지원",
89
+ "raw HTML 은 react-markdown 기본 동작으로 차단 — 사용자 입력 마크다운으로부터의 XSS 자동 방어",
90
+ "previewPosition: right(기본) / bottom — 좁은 화면(<768px)에서는 자동으로 위·아래로 쌓임. preview={false} 로 미리보기를 끄면 단순 마크다운 입력기로 사용",
91
+ "registry dependency = code-editor — `npx sh-ui-cli add markdown-editor` 시 code-editor 도 함께 설치"
92
+ ],
93
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.27.0"
94
+ },
95
+ {
96
+ "version": "0.26.0",
97
+ "date": "2026-04-28",
98
+ "title": "CodeEditor 컴포넌트 — CodeMirror 6 + sh-ui 토큰 테마",
99
+ "type": "minor",
100
+ "highlights": [
101
+ "CodeEditor 신규 — CodeMirror 6 basicSetup 기반 controlled 인라인 코드 에디터 (value/onChange · placeholder · readOnly · showLineNumbers · minHeight/maxHeight)",
102
+ "지원 언어: text · javascript · typescript · jsx · tsx · json · css · html · markdown — 런타임 변경은 Compartment 로 hot-swap",
103
+ "컬러·여백을 sh-ui 토큰(--background/--foreground/--border/--background-muted 등)으로 매핑 — `.dark` 스코프에서 다크 테마 자동 추종",
104
+ "CLI 레지스트리에 `npx sh-ui-cli add code-editor` 등록, docs 사이드바 Components 섹션에 진입점 추가"
105
+ ],
106
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.26.0"
107
+ },
108
+ {
109
+ "version": "0.25.0",
110
+ "date": "2026-04-28",
111
+ "title": "Next.js 템플릿에 http 골격 + BFF proxy 추가 — IS_SERVER 분기 제거",
112
+ "type": "minor",
113
+ "highlights": [
114
+ "nextjs-app · nextjs-standalone 템플릿에 axios 단일 인스턴스(src/shared/api/http.ts)와 BFF 라우트 핸들러(app/api/proxy/[...path]/route.ts) 추가 — 클라/서버 모두 /api/proxy 경유, IS_SERVER 분기 없음",
115
+ "인증 · 토큰 refresh · Sentry · 로그인 쿠키 특례 처리는 의도적으로 제외해 인증 비종속 골격으로 유지 — 프로젝트별 추가는 apps/docs/recipes 가이드로 분리",
116
+ "레시피 9종 추가 (apps/docs/app/recipes/): api-layer, auth, file-upload, tanstack-query, async-boundary, i18n, sentry, testing, deployment",
117
+ "사이드바에 '레시피' 진입점 추가"
118
+ ],
119
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.25.0"
120
+ },
5
121
  {
6
122
  "version": "0.24.0",
7
123
  "date": "2026-04-28",
@@ -74,7 +190,7 @@
74
190
  "sh-ui mcp init --client <claude-code|cursor|claude-desktop> 추가 — IDE 별 MCP 설정 파일을 자동으로 찾아 sh-ui 엔트리 머지",
75
191
  "스코프 분기 — 기본 project(.mcp.json / .cursor/mcp.json), --scope user 로 전역(~/.claude/mcp.json 등) 선택 가능. claude-desktop 은 user 전용(OS 별 경로 자동 분기)",
76
192
  "기존 JSON 보존 — 다른 MCP 서버 엔트리·기타 키를 건드리지 않고 sh-ui 만 머지·갱신",
77
- "수동 JSON 편집 없이 npx sh-ui mcp init --client cursor 한 줄로 끝"
193
+ "수동 JSON 편집 없이 npx sh-ui-cli mcp init --client cursor 한 줄로 끝"
78
194
  ],
79
195
  "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.22.0"
80
196
  },
@@ -0,0 +1,232 @@
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.css";
13
+
14
+ export type CodeEditorLanguage =
15
+ | "text"
16
+ | "javascript"
17
+ | "typescript"
18
+ | "jsx"
19
+ | "tsx"
20
+ | "json"
21
+ | "css"
22
+ | "html"
23
+ | "markdown";
24
+
25
+ export interface CodeEditorProps {
26
+ /**
27
+ * Controlled — 현재 코드. 명시 시 value 가 진실원천이 되고 onChange 로 외부에서 갱신해야 한다.
28
+ * 미지정이면 uncontrolled — 에디터가 자체 내부 문서로 동작.
29
+ */
30
+ value?: string;
31
+ /**
32
+ * Uncontrolled 초기값. value 미지정 시에만 사용된다.
33
+ * @default ""
34
+ */
35
+ defaultValue?: string;
36
+ /** 코드가 바뀔 때마다 호출 (controlled · uncontrolled 모두). */
37
+ onChange?: (value: string) => void;
38
+ /**
39
+ * 신택스 하이라이팅 언어.
40
+ * @default "text"
41
+ */
42
+ language?: CodeEditorLanguage;
43
+ /** 비어 있을 때 표시할 placeholder. */
44
+ placeholder?: string;
45
+ /** 읽기 전용. 키 입력은 막지만 선택·복사는 가능. */
46
+ readOnly?: boolean;
47
+ /**
48
+ * 좌측 줄 번호 표시 여부.
49
+ * @default true
50
+ */
51
+ showLineNumbers?: boolean;
52
+ /** 에디터 최소 높이 (CSS 길이 단위). */
53
+ minHeight?: string;
54
+ /** 에디터 최대 높이 (CSS 길이 단위). 초과 시 내부 스크롤. */
55
+ maxHeight?: string;
56
+ className?: string;
57
+ id?: string;
58
+ "aria-label"?: string;
59
+ "aria-labelledby"?: string;
60
+ }
61
+
62
+ function cx(...args: (string | undefined | false | null)[]) {
63
+ return args.filter(Boolean).join(" ");
64
+ }
65
+
66
+ function languageExtension(language: CodeEditorLanguage): Extension {
67
+ switch (language) {
68
+ case "javascript":
69
+ return javascript();
70
+ case "typescript":
71
+ return javascript({ typescript: true });
72
+ case "jsx":
73
+ return javascript({ jsx: true });
74
+ case "tsx":
75
+ return javascript({ jsx: true, typescript: true });
76
+ case "json":
77
+ return json();
78
+ case "css":
79
+ return cssLang();
80
+ case "html":
81
+ return html();
82
+ case "markdown":
83
+ return markdown();
84
+ case "text":
85
+ default:
86
+ return [];
87
+ }
88
+ }
89
+
90
+ /**
91
+ * CodeMirror 6 기반 인라인 코드 에디터.
92
+ *
93
+ * Controlled (value/onChange) · Uncontrolled (defaultValue + 선택 onChange) 모두 지원.
94
+ * 라우터·외부 상태와 동기화할 게 없는 경우 defaultValue 한 줄로 끝 — useState 불필요.
95
+ *
96
+ * 신택스 하이라이팅·자동 들여쓰기·괄호 매칭 등은 CodeMirror `basicSetup` 을 그대로 사용,
97
+ * 컬러·여백은 sh-ui 토큰(`--background`, `--foreground`, `--border` 등)으로 매핑돼 테마에 자동 추종.
98
+ */
99
+ export function CodeEditor({
100
+ value: valueProp,
101
+ defaultValue,
102
+ onChange,
103
+ language = "text",
104
+ placeholder,
105
+ readOnly = false,
106
+ showLineNumbers = true,
107
+ minHeight,
108
+ maxHeight,
109
+ className,
110
+ id,
111
+ "aria-label": ariaLabel,
112
+ "aria-labelledby": ariaLabelledBy,
113
+ }: CodeEditorProps) {
114
+ const isControlled = valueProp !== undefined;
115
+ const hostRef = useRef<HTMLDivElement>(null);
116
+ const viewRef = useRef<EditorView | null>(null);
117
+ const onChangeRef = useRef(onChange);
118
+ onChangeRef.current = onChange;
119
+ const initialDocRef = useRef(valueProp ?? defaultValue ?? "");
120
+
121
+ const compartments = useMemo(
122
+ () => ({
123
+ language: new Compartment(),
124
+ readOnly: new Compartment(),
125
+ lineNumbers: new Compartment(),
126
+ placeholder: new Compartment(),
127
+ }),
128
+ [],
129
+ );
130
+
131
+ useEffect(() => {
132
+ if (!hostRef.current) return;
133
+
134
+ const extensions: Extension[] = [
135
+ basicSetup,
136
+ EditorView.lineWrapping,
137
+ EditorView.updateListener.of((update) => {
138
+ if (update.docChanged) {
139
+ onChangeRef.current?.(update.state.doc.toString());
140
+ }
141
+ }),
142
+ compartments.language.of(languageExtension(language)),
143
+ compartments.readOnly.of(EditorState.readOnly.of(readOnly)),
144
+ compartments.lineNumbers.of(
145
+ showLineNumbers
146
+ ? []
147
+ : EditorView.theme({ ".cm-gutters": { display: "none" } }),
148
+ ),
149
+ compartments.placeholder.of(placeholder ? placeholderExt(placeholder) : []),
150
+ ];
151
+
152
+ const view = new EditorView({
153
+ state: EditorState.create({ doc: initialDocRef.current, extensions }),
154
+ parent: hostRef.current,
155
+ });
156
+ viewRef.current = view;
157
+
158
+ return () => {
159
+ view.destroy();
160
+ viewRef.current = null;
161
+ };
162
+ // 초기 마운트 1회만 — 후속 동기화는 별도 이펙트가 처리
163
+ // eslint-disable-next-line react-hooks/exhaustive-deps
164
+ }, []);
165
+
166
+ // controlled 모드에서만 외부 value 를 에디터 doc 에 동기화. uncontrolled 면 에디터가 자체 source-of-truth.
167
+ useEffect(() => {
168
+ if (!isControlled) return;
169
+ const view = viewRef.current;
170
+ if (!view) return;
171
+ const current = view.state.doc.toString();
172
+ if (current === valueProp) return;
173
+ view.dispatch({
174
+ changes: { from: 0, to: current.length, insert: valueProp ?? "" },
175
+ });
176
+ }, [isControlled, valueProp]);
177
+
178
+ useEffect(() => {
179
+ viewRef.current?.dispatch({
180
+ effects: compartments.language.reconfigure(languageExtension(language)),
181
+ });
182
+ }, [language, compartments.language]);
183
+
184
+ useEffect(() => {
185
+ viewRef.current?.dispatch({
186
+ effects: compartments.readOnly.reconfigure(EditorState.readOnly.of(readOnly)),
187
+ });
188
+ }, [readOnly, compartments.readOnly]);
189
+
190
+ useEffect(() => {
191
+ viewRef.current?.dispatch({
192
+ effects: compartments.lineNumbers.reconfigure(
193
+ showLineNumbers
194
+ ? []
195
+ : EditorView.theme({ ".cm-gutters": { display: "none" } }),
196
+ ),
197
+ });
198
+ }, [showLineNumbers, compartments.lineNumbers]);
199
+
200
+ useEffect(() => {
201
+ viewRef.current?.dispatch({
202
+ effects: compartments.placeholder.reconfigure(
203
+ placeholder ? placeholderExt(placeholder) : [],
204
+ ),
205
+ });
206
+ }, [placeholder, compartments.placeholder]);
207
+
208
+ useEffect(() => {
209
+ const view = viewRef.current;
210
+ if (!view) return;
211
+ const node = view.contentDOM;
212
+ if (id) node.id = id;
213
+ if (ariaLabel) node.setAttribute("aria-label", ariaLabel);
214
+ else node.removeAttribute("aria-label");
215
+ if (ariaLabelledBy) node.setAttribute("aria-labelledby", ariaLabelledBy);
216
+ else node.removeAttribute("aria-labelledby");
217
+ }, [id, ariaLabel, ariaLabelledBy]);
218
+
219
+ return (
220
+ <div
221
+ ref={hostRef}
222
+ className={cx("sh-ui-code-editor", className)}
223
+ data-readonly={readOnly || undefined}
224
+ style={
225
+ {
226
+ "--sh-ui-code-editor-min-height": minHeight,
227
+ "--sh-ui-code-editor-max-height": maxHeight,
228
+ } as React.CSSProperties
229
+ }
230
+ />
231
+ );
232
+ }
@@ -0,0 +1,76 @@
1
+ .sh-ui-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
+ .sh-ui-code-editor:focus-within {
12
+ border-color: var(--foreground);
13
+ outline: var(--border-width-strong) solid var(--foreground);
14
+ outline-offset: 2px;
15
+ }
16
+ .sh-ui-code-editor[data-readonly] {
17
+ background: var(--background-subtle);
18
+ }
19
+
20
+ .sh-ui-code-editor .cm-editor {
21
+ background: transparent;
22
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
+ }
24
+ .sh-ui-code-editor .cm-editor.cm-focused {
25
+ outline: none;
26
+ }
27
+ .sh-ui-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
+ .sh-ui-code-editor .cm-content {
33
+ caret-color: var(--foreground);
34
+ color: var(--foreground);
35
+ padding: var(--space-3) 0;
36
+ }
37
+ .sh-ui-code-editor .cm-line {
38
+ padding: 0 var(--space-3);
39
+ }
40
+ .sh-ui-code-editor .cm-gutters {
41
+ background: var(--background-subtle);
42
+ color: var(--foreground-muted);
43
+ border-right: 1px solid var(--border);
44
+ }
45
+ .sh-ui-code-editor .cm-activeLineGutter,
46
+ .sh-ui-code-editor .cm-activeLine {
47
+ background: var(--background-muted);
48
+ }
49
+ .sh-ui-code-editor .cm-cursor,
50
+ .sh-ui-code-editor .cm-dropCursor {
51
+ border-left-color: var(--foreground);
52
+ }
53
+ .sh-ui-code-editor .cm-selectionBackground,
54
+ .sh-ui-code-editor .cm-editor .cm-selectionBackground,
55
+ .sh-ui-code-editor .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
56
+ .sh-ui-code-editor ::selection {
57
+ background: var(--background-muted) !important;
58
+ }
59
+ .sh-ui-code-editor .cm-placeholder {
60
+ color: var(--foreground-muted);
61
+ }
62
+ .sh-ui-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
+ .sh-ui-code-editor .cm-tooltip-autocomplete > ul > li[aria-selected] {
69
+ background: var(--background-muted);
70
+ color: var(--foreground);
71
+ }
72
+ .sh-ui-code-editor .cm-matchingBracket,
73
+ .sh-ui-code-editor .cm-nonmatchingBracket {
74
+ background: var(--background-muted);
75
+ color: var(--foreground);
76
+ }
@@ -0,0 +1,49 @@
1
+ import { CodePanel, type CodePanelProps } from "../code-panel";
2
+ import {
3
+ Tabs,
4
+ TabsContent,
5
+ TabsIndicator,
6
+ TabsList,
7
+ TabsTrigger,
8
+ } from "../tabs";
9
+
10
+ export interface CodeTabsItem extends Omit<CodePanelProps, "code"> {
11
+ /** 탭 식별자 (Tabs 의 value 와 동일). */
12
+ value: string;
13
+ /** 탭 트리거에 표시될 라벨. */
14
+ label: string;
15
+ /** 표시할 코드. */
16
+ code: string;
17
+ }
18
+
19
+ export interface CodeTabsProps {
20
+ items: CodeTabsItem[];
21
+ /** 초기 활성 탭 value. 미지정 시 첫 번째 항목. */
22
+ defaultValue?: string;
23
+ }
24
+
25
+ /**
26
+ * 같은 예제의 여러 코드 뷰(예: React / Flutter, 또는 "강조 부분 / 전체 코드") 를 탭으로 전환.
27
+ * 각 탭의 내용은 그대로 `CodePanel` 로 렌더되므로 shiki 하이라이팅 · 복사 버튼 · 파일명 헤더
28
+ * 등을 그대로 사용할 수 있다.
29
+ */
30
+ export function CodeTabs({ items, defaultValue }: CodeTabsProps) {
31
+ const initial = defaultValue ?? items[0]?.value;
32
+ return (
33
+ <Tabs defaultValue={initial}>
34
+ <TabsList>
35
+ <TabsIndicator />
36
+ {items.map((item) => (
37
+ <TabsTrigger key={item.value} value={item.value}>
38
+ {item.label}
39
+ </TabsTrigger>
40
+ ))}
41
+ </TabsList>
42
+ {items.map(({ value, label: _label, ...panel }) => (
43
+ <TabsContent key={value} value={value}>
44
+ <CodePanel {...panel} />
45
+ </TabsContent>
46
+ ))}
47
+ </Tabs>
48
+ );
49
+ }