sh-ui-cli 0.53.0 → 0.56.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.
- package/data/changelog/versions.json +53 -0
- package/data/registry/react/components/theme/index.tsx +18 -1
- package/data/tokens/build.mjs +21 -0
- package/package.json +1 -1
- package/src/create/theme/decode.js +20 -1
- package/src/create/theme/encode.js +24 -0
- package/src/create/theme/inject.js +19 -3
- package/src/mcp.mjs +93 -1
- package/templates/nextjs-standalone/src/shared/styles/tokens.css +19 -0
- package/templates/ui-app-template/src/styles/tokens.css +19 -0
|
@@ -2,6 +2,59 @@
|
|
|
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.56.1",
|
|
7
|
+
"date": "2026-05-04",
|
|
8
|
+
"title": "fix — 테마 토글이 새로고침 후 초기화 + v0.54.0 템플릿/빌더 완성",
|
|
9
|
+
"type": "patch",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**force-static 페이지에서 테마 영속화 복구** — `cookies()` 가 throw 해 SSR 시 항상 `isDark=false` 폴백이라 사용자가 토글한 결과(`sh-ui-theme` 쿠키) 가 무시되던 문제. `<head>` 인라인 스크립트로 hydration 전에 쿠키를 읽어 `.dark`/`.light` 클래스 즉시 부여 — 깜빡임 없음.",
|
|
12
|
+
"**ThemeProvider mount-time 동기화 추가** — React state 도 mount 시 쿠키를 직접 확인해 SSR 폴백(`light`)에 갇히지 않도록 보정. 토글 버튼 UI 가 실제 클래스와 항상 일치. registry 원본 + apps/docs 카피본 둘 다 동일하게 패치.",
|
|
13
|
+
"**v0.54.0 미반영 템플릿/빌더 완성** — `nextjs-standalone` / `ui-app-template` 의 `tokens.css` 에 `prefers-color-scheme` 블록 추가, `packages/tokens/build.mjs` 가 해당 블록을 emit. v0.54.0 릴리즈 노트에 약속됐지만 누락됐던 부분을 같은 패치로 정리.",
|
|
14
|
+
"**docs `tokens.css` 의 `@theme inline` 블록 추가** — Tailwind v4 유틸(`bg-primary` / `text-foreground` 등) 이 토큰 변수와 자동 매핑되도록. 기존엔 컴포넌트가 hex fallback 으로만 동작하던 곳도 토큰 흐름으로 통일."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.56.1"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"version": "0.56.0",
|
|
20
|
+
"date": "2026-05-04",
|
|
21
|
+
"title": "playground — Success / Warning / Info 색 그룹",
|
|
22
|
+
"type": "minor",
|
|
23
|
+
"highlights": [
|
|
24
|
+
"**playground `/create` 에 Success / Warning / Info 색 슬롯 추가** — 고급 모드에서 `success` / `success-foreground` / `warning` / `warning-foreground` / `info` / `info-foreground` 6개를 직접 잡을 수 있다. light/dark 각각 별도 디폴트(라이트는 Tailwind green-600/amber-600/sky-500, 다크는 한 단계 밝은 채도) 제공.",
|
|
25
|
+
"**Badge 컴포넌트 fallback 과 동일한 디폴트** — `var(--success, #16a34a)` 같은 기존 컴포넌트 fallback 값을 디폴트로 사용해, 사용자가 색을 안 만져도 시각적 변화가 0. 기존 base64/스캐폴드 결과와 호환 유지.",
|
|
26
|
+
"**v0.55.0 base64 옵셔널 키와 자동 연결** — playground 가 export 하는 base64 에 자동으로 새 6개 키가 포함되며, `sh_ui_create_project` 의 `theme` 인자로 그대로 흘러가 `tokens.css` 에 emit. v0.55.0 의 옵셔널 색 토큰 스키마와 끝에서 끝까지 정렬.",
|
|
27
|
+
"**localStorage v1 호환** — 기존 저장된 토큰은 새 키만 디폴트로 채워진 상태로 로드. 마이그레이션 없이 호환."
|
|
28
|
+
],
|
|
29
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.56.0"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"version": "0.55.0",
|
|
33
|
+
"date": "2026-05-04",
|
|
34
|
+
"title": "MCP — 테마 round-trip + 옵셔널 상태 컬러",
|
|
35
|
+
"type": "minor",
|
|
36
|
+
"highlights": [
|
|
37
|
+
"**`sh_ui_encode_theme` MCP 툴 신규** — 사용자가 손본 토큰 객체(`{ light, dark, radius }`)를 base64 로 인코딩. 산출물을 `sh_ui_create_project` 의 `theme` 인자에 그대로 넘기면 다음 스캐폴드에서 톤이 그대로 보존된다. round-trip 검증 내장 — 잘못된 입력은 즉시 거부.",
|
|
38
|
+
"**`sh_ui_decode_theme` MCP 툴 신규** — base64 테마 코드를 객체로 복원. 기존 테마 일부만 고치고 싶을 때 decode → 수정 → encode 양방향 흐름.",
|
|
39
|
+
"**옵셔널 색 토큰 — `success`/`warning`/`info` + `-foreground`** — base64 스키마에 6개 옵셔널 키 추가. 누락 OK(기존 base64 호환), 들어 있으면 hex 검증. `inject` 는 light/dark 둘 다 정의된 경우에만 CSS 로 emit (한쪽만 있으면 안전 가드로 skip).",
|
|
40
|
+
"**MCP `instructions` + `sh_ui_create_project` description 보강** — \"테마 커스터마이징 round-trip\" 흐름을 새 세션에서도 AI 가 자연스럽게 떠올릴 수 있게 명시.",
|
|
41
|
+
"**docs MCP 페이지 동기화** — 노출 툴 표에 `sh_ui_create_project`/`sh_ui_encode_theme`/`sh_ui_decode_theme` 3개 추가 + 테마 round-trip 섹션."
|
|
42
|
+
],
|
|
43
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.55.0"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"version": "0.54.0",
|
|
47
|
+
"date": "2026-05-04",
|
|
48
|
+
"title": "다크/라이트 자동 전환 — 시스템 설정 기본 반영",
|
|
49
|
+
"type": "minor",
|
|
50
|
+
"highlights": [
|
|
51
|
+
"**`prefers-color-scheme` 미디어쿼리 기본 emit** — `mode: \"light-dark\"` 토큰 출력에 `@media (prefers-color-scheme: dark) { :root:not(.light):not(.dark) { ... } }` 블록을 자동 추가. 사용자가 토글 컴포넌트를 따로 깔지 않아도 OS 다크모드면 다크, 라이트모드면 라이트로 자동 전환됨.",
|
|
52
|
+
"**`.light` 클래스 명시 override 지원** — 기존 `.dark` 클래스에 더해 `.light` 도 미디어쿼리를 이긴다. 시스템이 다크여도 사용자가 라이트를 명시 선택하면 의도대로 라이트 유지.",
|
|
53
|
+
"**Theme 컴포넌트 동기 업데이트** — `setTheme(\"light\")` 가 `<html>` 에 `.light` 클래스를 명시 부여하도록 변경. 토글 사용 시 시스템 설정과 충돌 없이 동작.",
|
|
54
|
+
"**템플릿 + docs 자동 적용** — `nextjs-standalone` / `ui-app-template` / docs 사이트의 `tokens.css` 가 새 패턴으로 갱신. 기존 컴포넌트 코드 변경 불필요."
|
|
55
|
+
],
|
|
56
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.54.0"
|
|
57
|
+
},
|
|
5
58
|
{
|
|
6
59
|
"version": "0.53.0",
|
|
7
60
|
"date": "2026-05-04",
|
|
@@ -61,13 +61,30 @@ export function ThemeProvider({
|
|
|
61
61
|
const [_theme, _setTheme] = React.useState<Theme>(defaultTheme);
|
|
62
62
|
const theme = themeProp ?? _theme;
|
|
63
63
|
|
|
64
|
+
// SSR 폴백 보정 — force-static 페이지에서는 cookies() 가 throw 해 defaultTheme 이
|
|
65
|
+
// 항상 "light" 로 들어온다. mount 시 cookie 를 직접 읽어 React state 와 documentElement
|
|
66
|
+
// 클래스를 사용자가 마지막에 고른 값으로 동기화. 비제어 모드에서만.
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (themeProp !== undefined) return;
|
|
69
|
+
if (typeof document === "undefined") return;
|
|
70
|
+
const m = document.cookie.match(/(?:^|; )sh-ui-theme=(dark|light)/);
|
|
71
|
+
if (!m) return;
|
|
72
|
+
const fromCookie = m[1] as Theme;
|
|
73
|
+
_setTheme(fromCookie);
|
|
74
|
+
const root = document.documentElement.classList;
|
|
75
|
+
root.toggle("dark", fromCookie === "dark");
|
|
76
|
+
root.toggle("light", fromCookie === "light");
|
|
77
|
+
}, [themeProp]);
|
|
78
|
+
|
|
64
79
|
const setTheme = React.useCallback(
|
|
65
80
|
(next: Theme) => {
|
|
66
81
|
if (onThemeChange) onThemeChange(next);
|
|
67
82
|
else _setTheme(next);
|
|
68
83
|
|
|
69
84
|
if (typeof document !== "undefined") {
|
|
70
|
-
document.documentElement.classList
|
|
85
|
+
const root = document.documentElement.classList;
|
|
86
|
+
root.toggle("dark", next === "dark");
|
|
87
|
+
root.toggle("light", next === "light");
|
|
71
88
|
document.cookie = `${THEME_COOKIE_NAME}=${next}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
|
|
72
89
|
}
|
|
73
90
|
},
|
package/data/tokens/build.mjs
CHANGED
|
@@ -92,6 +92,26 @@ function emitCssBlock(selector, entries) {
|
|
|
92
92
|
return `${selector} {\n${lines}\n}`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `prefers-color-scheme: dark` 자동 적용 블록.
|
|
97
|
+
*
|
|
98
|
+
* 셀렉터를 `:root:not(.light):not(.dark)` 로 좁힌 이유 — 미디어쿼리는
|
|
99
|
+
* 명시 토글(`.light` / `.dark` 클래스)이 없을 때만 동작해야 한다. 그렇지
|
|
100
|
+
* 않으면 사용자가 라이트로 토글해도 OS 가 다크면 다시 다크로 돌아감.
|
|
101
|
+
*
|
|
102
|
+
* 적용 매트릭스:
|
|
103
|
+
* 클래스 없음 + 라이트 OS → :root (라이트) 적용
|
|
104
|
+
* 클래스 없음 + 다크 OS → 이 블록 적용 (다크)
|
|
105
|
+
* .light 클래스 → :root 만 적용 (강제 라이트, OS 무관)
|
|
106
|
+
* .dark 클래스 → .dark 블록이 마지막에 와서 항상 승리 (강제 다크)
|
|
107
|
+
*/
|
|
108
|
+
function emitAutoDarkBlock(darkEntries) {
|
|
109
|
+
const lines = Object.entries(darkEntries)
|
|
110
|
+
.map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`)
|
|
111
|
+
.join("\n");
|
|
112
|
+
return `@media (prefers-color-scheme: dark) {\n :root:not(.light):not(.dark) {\n${lines}\n }\n}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
/** 테마 독립 카테고리(light/dark 제외) 전체를 하나의 맵으로 병합 */
|
|
96
116
|
function mergeThemeIndependent(tokens) {
|
|
97
117
|
const out = {};
|
|
@@ -137,6 +157,7 @@ export async function buildTokensCss(config) {
|
|
|
137
157
|
blocks.push(emitCssBlock(":root", { ...tokens.dark, ...themeIndep }));
|
|
138
158
|
}
|
|
139
159
|
if (mode === "light-dark") {
|
|
160
|
+
blocks.push(emitAutoDarkBlock(tokens.dark));
|
|
140
161
|
blocks.push(emitCssBlock(".dark", tokens.dark));
|
|
141
162
|
}
|
|
142
163
|
blocks.push(buildTailwindThemeBlock(tokens.light));
|
package/package.json
CHANGED
|
@@ -8,6 +8,15 @@ const TOKEN_KEYS = [
|
|
|
8
8
|
'danger', 'danger-foreground',
|
|
9
9
|
];
|
|
10
10
|
|
|
11
|
+
// 옵셔널 색 토큰 — 누락 OK. 입력에 들어 있으면 hex 검증 + inject 시 CSS 변수로 emit.
|
|
12
|
+
// 사용자가 success/warning/info 상태 컬러를 커스텀하고 싶을 때 사용.
|
|
13
|
+
// Dart 측 ShUiColorTokens 는 아직 미반영(웹 한정).
|
|
14
|
+
const OPTIONAL_TOKEN_KEYS = [
|
|
15
|
+
'success', 'success-foreground',
|
|
16
|
+
'warning', 'warning-foreground',
|
|
17
|
+
'info', 'info-foreground',
|
|
18
|
+
];
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* 옵셔널 카테고리 검증 — 모두 Record<string, number>.
|
|
13
22
|
* 카테고리 누락은 OK (스캐폴드 시 해당 블록은 디폴트 유지). 형식만 위배되면 에러.
|
|
@@ -95,6 +104,16 @@ const validateTokenMap = (name, map) => {
|
|
|
95
104
|
);
|
|
96
105
|
}
|
|
97
106
|
}
|
|
107
|
+
// 옵셔널 키는 들어 있을 때만 hex 검증.
|
|
108
|
+
for (const key of OPTIONAL_TOKEN_KEYS) {
|
|
109
|
+
if (!(key in map)) continue;
|
|
110
|
+
const value = map[key];
|
|
111
|
+
if (typeof value !== 'string' || !HEX_REGEX.test(value)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`theme 디코드 실패: ${name}.${key} 가 hex 포맷이 아님 (받은 값: ${JSON.stringify(value)})`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
98
117
|
};
|
|
99
118
|
|
|
100
119
|
export const decodeTheme = (b64) => {
|
|
@@ -166,4 +185,4 @@ export const resolveTheme = (input) => {
|
|
|
166
185
|
return decodeTheme(input);
|
|
167
186
|
};
|
|
168
187
|
|
|
169
|
-
export { TOKEN_KEYS, THEME_PRESET_NAMES };
|
|
188
|
+
export { TOKEN_KEYS, OPTIONAL_TOKEN_KEYS, THEME_PRESET_NAMES };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { decodeTheme } from './decode.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 사용자/AI 가 손본 토큰 객체를 sh-ui base64 테마 문자열로 인코딩.
|
|
5
|
+
* `sh_ui_create_project` 의 `theme` 인자에 그대로 넘겨 영구 보관 가능.
|
|
6
|
+
*
|
|
7
|
+
* 입력 형태: `{ light, dark, radius, ...옵셔널 카테고리 }`
|
|
8
|
+
* - light/dark: 15개 필수 토큰 (background, foreground, primary, danger 계열) + 옵셔널 6개 (success/warning/info)
|
|
9
|
+
* - radius: 0~1.5 (rem 단위)
|
|
10
|
+
* - 옵셔널: spacing/typography/weights/controls/borders/durations/shadows/eases/gradients
|
|
11
|
+
*
|
|
12
|
+
* 인코드 직후 round-trip 검증으로 디코더가 거부할 입력은 즉시 throw.
|
|
13
|
+
*/
|
|
14
|
+
export const encodeTheme = (theme) => {
|
|
15
|
+
if (!theme || typeof theme !== 'object' || Array.isArray(theme)) {
|
|
16
|
+
throw new Error('theme 인코드 실패: 객체가 아님');
|
|
17
|
+
}
|
|
18
|
+
const json = JSON.stringify(theme);
|
|
19
|
+
const b64 = Buffer.from(json, 'utf-8').toString('base64');
|
|
20
|
+
// 같은 검증 로직을 한 곳에 두기 위해 round-trip — 인코드 산출물을 즉시 디코드해 본다.
|
|
21
|
+
// throw 면 입력이 스키마에 안 맞다는 뜻이라 그대로 호출자에게 전파.
|
|
22
|
+
decodeTheme(b64);
|
|
23
|
+
return b64;
|
|
24
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TOKEN_KEYS } from './decode.js';
|
|
1
|
+
import { TOKEN_KEYS, OPTIONAL_TOKEN_KEYS } from './decode.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 파일 내용에서 sh-ui:<section>-start / -end 마커 사이 내용을 교체.
|
|
@@ -30,12 +30,28 @@ export const replaceSection = (content, section, commentOpen, commentClose, repl
|
|
|
30
30
|
const cssColorLine = (key, value) => ` --${key}: ${value};`;
|
|
31
31
|
|
|
32
32
|
export const buildCssColorsBlock = (theme) => {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// 옵셔널 색 토큰 — light/dark 둘 다에 정의되어 있을 때만 emit. 한쪽만 있으면 누락된 쪽은 fallback 으로
|
|
34
|
+
// 디자인이 깨질 수 있어서 양쪽 정의가 일치할 때만 안전하게 내보낸다.
|
|
35
|
+
const optionalKeys = OPTIONAL_TOKEN_KEYS.filter(
|
|
36
|
+
(k) => k in theme.light && k in theme.dark,
|
|
37
|
+
);
|
|
38
|
+
const allKeys = [...TOKEN_KEYS, ...optionalKeys];
|
|
39
|
+
|
|
40
|
+
const lightLines = allKeys.map((k) => cssColorLine(k, theme.light[k])).join('\n');
|
|
41
|
+
const darkLines = allKeys.map((k) => cssColorLine(k, theme.dark[k])).join('\n');
|
|
42
|
+
// 미디어쿼리 안의 다크 라인은 한 단계 더 들여쓰기 (`:root:not(...)` 안쪽).
|
|
43
|
+
const darkLinesIndented = allKeys
|
|
44
|
+
.map((k) => ` ${cssColorLine(k, theme.dark[k])}`)
|
|
45
|
+
.join('\n');
|
|
35
46
|
return [
|
|
36
47
|
':root {',
|
|
37
48
|
lightLines,
|
|
38
49
|
'}',
|
|
50
|
+
'@media (prefers-color-scheme: dark) {',
|
|
51
|
+
' :root:not(.light):not(.dark) {',
|
|
52
|
+
darkLinesIndented,
|
|
53
|
+
' }',
|
|
54
|
+
'}',
|
|
39
55
|
'.dark {',
|
|
40
56
|
darkLines,
|
|
41
57
|
'}',
|
package/src/mcp.mjs
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
//
|
|
7
7
|
// 노출 툴:
|
|
8
8
|
// sh_ui_describe_init - init 4개 축(platform/base/radius/mode) enum + 한글 설명
|
|
9
|
+
// sh_ui_create_project - 빈 폴더에 Next.js/Flutter 프로젝트 스캐폴드
|
|
9
10
|
// sh_ui_init - sh-ui.config.json 생성 (비대화형)
|
|
10
11
|
// sh_ui_list_components - 플랫폼 전체 컴포넌트 + 요약
|
|
11
12
|
// sh_ui_get_component - 단일 컴포넌트의 메타·소스·deps
|
|
12
13
|
// sh_ui_add_component - 컴포넌트 설치 (외부 패키지 자동 설치 포함)
|
|
13
14
|
// sh_ui_remove_component - 컴포넌트 삭제
|
|
15
|
+
// sh_ui_get_changelog - 변경 내역(versions.json) 반환
|
|
16
|
+
// sh_ui_encode_theme - 토큰 객체 → base64 (사용자가 손본 톤을 영구 보관)
|
|
17
|
+
// sh_ui_decode_theme - base64 → 토큰 객체 (기존 테마 일부만 수정 후 재인코딩)
|
|
14
18
|
|
|
15
19
|
import { readFile } from "node:fs/promises";
|
|
16
20
|
import { existsSync } from "node:fs";
|
|
@@ -40,6 +44,8 @@ import {
|
|
|
40
44
|
} from "./constants.js";
|
|
41
45
|
import { allPlugins } from "./create/plugins/index.js";
|
|
42
46
|
import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
|
|
47
|
+
import { decodeTheme } from "./create/theme/decode.js";
|
|
48
|
+
import { encodeTheme } from "./create/theme/encode.js";
|
|
43
49
|
|
|
44
50
|
const PLATFORMS = INIT_PLATFORMS;
|
|
45
51
|
const BASES = THEME_BASES;
|
|
@@ -164,6 +170,18 @@ function buildServerInstructions(cliName) {
|
|
|
164
170
|
- \`sh_ui_get_component\` — props/소스 확인 (코드 작성 전)
|
|
165
171
|
- \`sh_ui_add_component\` / \`sh_ui_remove_component\` — 설치/삭제
|
|
166
172
|
- \`sh_ui_get_changelog\` — 최근 변경 내역
|
|
173
|
+
|
|
174
|
+
## 테마 커스터마이징 (스캐폴드 결과 톤이 마음에 안 들 때)
|
|
175
|
+
|
|
176
|
+
스캐폴드 후 사용자가 "눈 아프다" / "Linear 톤으로" 같이 톤 조정을 요청하면, **\`tokens.css\` 직접 편집** + **편집 결과를 base64 로 백업** 두 단계를 같이 한다 — 그래야 다음에 같은 프로젝트를 재생성해도 톤이 보존된다.
|
|
177
|
+
|
|
178
|
+
1. \`tokens.css\` 의 \`:root\` / \`.dark\` 블록 색만 손봄 (마커는 건드리지 않음).
|
|
179
|
+
2. \`sh_ui_encode_theme\` 으로 \`{ light, dark, radius }\` 객체를 base64 로 인코딩.
|
|
180
|
+
- 옵셔널 색 토큰(\`success\`/\`warning\`/\`info\` + \`-foreground\`)도 같이 넣을 수 있음.
|
|
181
|
+
3. 그 base64 를 사용자에게 보여주고 (또는 메모리에 저장), **다음 \`sh_ui_create_project\` 호출 시 \`theme\` 인자에 그대로** 넣어 영구 보관.
|
|
182
|
+
4. 기존 base64 를 일부만 고치고 싶으면 \`sh_ui_decode_theme\` → 객체 수정 → \`sh_ui_encode_theme\` round-trip.
|
|
183
|
+
|
|
184
|
+
> 프리셋(\`neutral\`/\`slate\`/...) 이름과 base64 둘 다 \`theme\` 인자에 넣을 수 있다 — 길이로 자동 판별.
|
|
167
185
|
`;
|
|
168
186
|
}
|
|
169
187
|
|
|
@@ -204,7 +222,7 @@ export async function startMcpServer() {
|
|
|
204
222
|
plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
|
|
205
223
|
.describe(`Next.js 플러그인 (${PLUGIN_NAMES.join(', ')}). 미지정시 빈 배열`),
|
|
206
224
|
theme: z.string().optional()
|
|
207
|
-
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는
|
|
225
|
+
.describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 base64 테마 코드. 사용자가 톤을 직접 손본 결과를 영구 보관하려면 sh_ui_encode_theme 으로 base64 를 만들어 여기에 넘긴다.`),
|
|
208
226
|
cssFramework: z.enum(CSS_FRAMEWORKS).optional()
|
|
209
227
|
.describe(`CSS 프레임워크. 기본 plain. 현재 ${CSS_FRAMEWORKS.join('/')} 지원 — 변종 미보유 컴포넌트는 add 시 plain 으로 자동 fallback`),
|
|
210
228
|
cwd: z.string().optional()
|
|
@@ -432,6 +450,80 @@ export async function startMcpServer() {
|
|
|
432
450
|
},
|
|
433
451
|
);
|
|
434
452
|
|
|
453
|
+
// 테마 round-trip — 사용자가 손본 토큰을 다음 스캐폴드까지 보존.
|
|
454
|
+
// 입력 hex 토큰은 z.object 로 명시 — 누락/오타가 즉시 잡히고 클라이언트(IDE-내 AI)가 schema 만 봐도
|
|
455
|
+
// 어떤 키가 필요한지 자동으로 알 수 있다. round-trip 검증은 encodeTheme 내부의 decodeTheme 호출에 위임.
|
|
456
|
+
const HEX = z.string().regex(/^#[0-9A-Fa-f]{6}$/, "hex 컬러 (#RRGGBB)");
|
|
457
|
+
const tokenMapSchema = z
|
|
458
|
+
.object({
|
|
459
|
+
background: HEX, "background-subtle": HEX, "background-muted": HEX, "background-inverse": HEX,
|
|
460
|
+
foreground: HEX, "foreground-muted": HEX, "foreground-subtle": HEX, "foreground-inverse": HEX,
|
|
461
|
+
border: HEX, "border-strong": HEX,
|
|
462
|
+
primary: HEX, "primary-foreground": HEX, "primary-hover": HEX,
|
|
463
|
+
danger: HEX, "danger-foreground": HEX,
|
|
464
|
+
success: HEX.optional(), "success-foreground": HEX.optional(),
|
|
465
|
+
warning: HEX.optional(), "warning-foreground": HEX.optional(),
|
|
466
|
+
info: HEX.optional(), "info-foreground": HEX.optional(),
|
|
467
|
+
})
|
|
468
|
+
.describe("15개 필수 색 토큰 + 옵셔널 6개(success/warning/info × -foreground). 각 값은 #RRGGBB hex");
|
|
469
|
+
|
|
470
|
+
server.registerTool(
|
|
471
|
+
"sh_ui_encode_theme",
|
|
472
|
+
{
|
|
473
|
+
description:
|
|
474
|
+
"사용자가 손본 색 토큰을 sh-ui base64 테마 코드로 인코딩. " +
|
|
475
|
+
"산출물을 sh_ui_create_project 의 theme 인자에 그대로 넣으면 다음 스캐폴드에서 톤이 보존된다. " +
|
|
476
|
+
"스캐폴드 후 tokens.css 를 직접 편집한 케이스에서 그 결과를 영구 보관할 때 사용.",
|
|
477
|
+
inputSchema: {
|
|
478
|
+
light: tokenMapSchema.describe("라이트 모드 토큰"),
|
|
479
|
+
dark: tokenMapSchema.describe("다크 모드 토큰"),
|
|
480
|
+
radius: z.number().min(0).max(1.5)
|
|
481
|
+
.describe("기본 radius (rem 단위, 0~1.5). 일반 권장값 0.5~0.75"),
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
async (input) => {
|
|
485
|
+
try {
|
|
486
|
+
const b64 = encodeTheme({
|
|
487
|
+
light: input.light,
|
|
488
|
+
dark: input.dark,
|
|
489
|
+
radius: input.radius,
|
|
490
|
+
});
|
|
491
|
+
return jsonResult({
|
|
492
|
+
theme: b64,
|
|
493
|
+
length: b64.length,
|
|
494
|
+
hint: "sh_ui_create_project 의 theme 인자에 위 문자열을 그대로 넣으면 됩니다.",
|
|
495
|
+
});
|
|
496
|
+
} catch (e) {
|
|
497
|
+
return {
|
|
498
|
+
isError: true,
|
|
499
|
+
content: [{ type: "text", text: e.message }],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
server.registerTool(
|
|
506
|
+
"sh_ui_decode_theme",
|
|
507
|
+
{
|
|
508
|
+
description:
|
|
509
|
+
"sh-ui base64 테마 코드를 토큰 객체로 복원. " +
|
|
510
|
+
"기존 base64 테마의 일부만 수정해 다시 인코딩하고 싶을 때 사용 (decode → 수정 → encode).",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
theme: z.string().describe("sh-ui base64 테마 코드"),
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
async (input) => {
|
|
516
|
+
try {
|
|
517
|
+
return jsonResult(decodeTheme(input.theme));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
return {
|
|
520
|
+
isError: true,
|
|
521
|
+
content: [{ type: "text", text: e.message }],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
435
527
|
// 변경 내역 조회 — 보너스: 사용자가 "최근 변경 알려줘" 류 요청 시
|
|
436
528
|
server.registerTool(
|
|
437
529
|
"sh_ui_get_changelog",
|
|
@@ -19,6 +19,25 @@
|
|
|
19
19
|
--danger: #DC2626;
|
|
20
20
|
--danger-foreground: #FFFFFF;
|
|
21
21
|
}
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not(.light):not(.dark) {
|
|
24
|
+
--background: #0A0A0A;
|
|
25
|
+
--background-subtle: #171717;
|
|
26
|
+
--background-muted: #262626;
|
|
27
|
+
--background-inverse: #FFFFFF;
|
|
28
|
+
--foreground: #FAFAFA;
|
|
29
|
+
--foreground-muted: #A3A3A3;
|
|
30
|
+
--foreground-subtle: #737373;
|
|
31
|
+
--foreground-inverse: #0A0A0A;
|
|
32
|
+
--border: #262626;
|
|
33
|
+
--border-strong: #404040;
|
|
34
|
+
--primary: #FAFAFA;
|
|
35
|
+
--primary-foreground: #171717;
|
|
36
|
+
--primary-hover: #E5E5E5;
|
|
37
|
+
--danger: #DC2626;
|
|
38
|
+
--danger-foreground: #FFFFFF;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
.dark {
|
|
23
42
|
--background: #0A0A0A;
|
|
24
43
|
--background-subtle: #171717;
|
|
@@ -19,6 +19,25 @@
|
|
|
19
19
|
--danger: #DC2626;
|
|
20
20
|
--danger-foreground: #FFFFFF;
|
|
21
21
|
}
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not(.light):not(.dark) {
|
|
24
|
+
--background: #0A0A0A;
|
|
25
|
+
--background-subtle: #171717;
|
|
26
|
+
--background-muted: #262626;
|
|
27
|
+
--background-inverse: #FFFFFF;
|
|
28
|
+
--foreground: #FAFAFA;
|
|
29
|
+
--foreground-muted: #A3A3A3;
|
|
30
|
+
--foreground-subtle: #737373;
|
|
31
|
+
--foreground-inverse: #0A0A0A;
|
|
32
|
+
--border: #262626;
|
|
33
|
+
--border-strong: #404040;
|
|
34
|
+
--primary: #FAFAFA;
|
|
35
|
+
--primary-foreground: #171717;
|
|
36
|
+
--primary-hover: #E5E5E5;
|
|
37
|
+
--danger: #DC2626;
|
|
38
|
+
--danger-foreground: #FFFFFF;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
.dark {
|
|
23
42
|
--background: #0A0A0A;
|
|
24
43
|
--background-subtle: #171717;
|