sh-ui-cli 0.53.0 → 0.55.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.
@@ -2,6 +2,33 @@
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.55.0",
7
+ "date": "2026-05-04",
8
+ "title": "MCP — 테마 round-trip + 옵셔널 상태 컬러",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**`sh_ui_encode_theme` MCP 툴 신규** — 사용자가 손본 토큰 객체(`{ light, dark, radius }`)를 base64 로 인코딩. 산출물을 `sh_ui_create_project` 의 `theme` 인자에 그대로 넘기면 다음 스캐폴드에서 톤이 그대로 보존된다. round-trip 검증 내장 — 잘못된 입력은 즉시 거부.",
12
+ "**`sh_ui_decode_theme` MCP 툴 신규** — base64 테마 코드를 객체로 복원. 기존 테마 일부만 고치고 싶을 때 decode → 수정 → encode 양방향 흐름.",
13
+ "**옵셔널 색 토큰 — `success`/`warning`/`info` + `-foreground`** — base64 스키마에 6개 옵셔널 키 추가. 누락 OK(기존 base64 호환), 들어 있으면 hex 검증. `inject` 는 light/dark 둘 다 정의된 경우에만 CSS 로 emit (한쪽만 있으면 안전 가드로 skip).",
14
+ "**MCP `instructions` + `sh_ui_create_project` description 보강** — \"테마 커스터마이징 round-trip\" 흐름을 새 세션에서도 AI 가 자연스럽게 떠올릴 수 있게 명시.",
15
+ "**docs MCP 페이지 동기화** — 노출 툴 표에 `sh_ui_create_project`/`sh_ui_encode_theme`/`sh_ui_decode_theme` 3개 추가 + 테마 round-trip 섹션."
16
+ ],
17
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.55.0"
18
+ },
19
+ {
20
+ "version": "0.54.0",
21
+ "date": "2026-05-04",
22
+ "title": "다크/라이트 자동 전환 — 시스템 설정 기본 반영",
23
+ "type": "minor",
24
+ "highlights": [
25
+ "**`prefers-color-scheme` 미디어쿼리 기본 emit** — `mode: \"light-dark\"` 토큰 출력에 `@media (prefers-color-scheme: dark) { :root:not(.light):not(.dark) { ... } }` 블록을 자동 추가. 사용자가 토글 컴포넌트를 따로 깔지 않아도 OS 다크모드면 다크, 라이트모드면 라이트로 자동 전환됨.",
26
+ "**`.light` 클래스 명시 override 지원** — 기존 `.dark` 클래스에 더해 `.light` 도 미디어쿼리를 이긴다. 시스템이 다크여도 사용자가 라이트를 명시 선택하면 의도대로 라이트 유지.",
27
+ "**Theme 컴포넌트 동기 업데이트** — `setTheme(\"light\")` 가 `<html>` 에 `.light` 클래스를 명시 부여하도록 변경. 토글 사용 시 시스템 설정과 충돌 없이 동작.",
28
+ "**템플릿 + docs 자동 적용** — `nextjs-standalone` / `ui-app-template` / docs 사이트의 `tokens.css` 가 새 패턴으로 갱신. 기존 컴포넌트 코드 변경 불필요."
29
+ ],
30
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.54.0"
31
+ },
5
32
  {
6
33
  "version": "0.53.0",
7
34
  "date": "2026-05-04",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- const lightLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.light[k])).join('\n');
34
- const darkLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.dark[k])).join('\n');
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}) 또는 playground 에서 생성한 base64 (선택)`),
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",