sh-ui-cli 0.52.3 → 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.
- package/data/changelog/versions.json +41 -0
- package/data/registry/react/components/calendar/index.module.tsx +124 -25
- package/data/registry/react/components/calendar/index.tailwind.tsx +94 -20
- package/data/registry/react/components/calendar/index.tsx +124 -25
- package/data/registry/react/components/date-picker/index.module.tsx +41 -10
- package/data/registry/react/components/date-picker/index.tailwind.tsx +28 -8
- package/data/registry/react/components/date-picker/index.tsx +50 -10
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
5
|
-
import { Calendar, type DateRange } from "../calendar";
|
|
5
|
+
import { Calendar, DEFAULT_LOCALE, type CalendarMessages, type DateRange } from "../calendar";
|
|
6
6
|
import "./styles.css";
|
|
7
7
|
|
|
8
8
|
import { cn } from "@SH_UI_UTILS@";
|
|
@@ -17,6 +17,18 @@ const formatDefault = (d: Date) =>
|
|
|
17
17
|
const startOfMonth = (d: Date) =>
|
|
18
18
|
new Date(d.getFullYear(), d.getMonth(), 1);
|
|
19
19
|
|
|
20
|
+
/** locale 기반 단일 날짜 placeholder. 한국어 외에는 영어 fallback. */
|
|
21
|
+
function defaultDatePlaceholder(locale: string): string {
|
|
22
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
23
|
+
return lang === "ko" ? "날짜 선택" : "Select date";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** locale 기반 범위 placeholder. */
|
|
27
|
+
function defaultRangePlaceholder(locale: string): string {
|
|
28
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
29
|
+
return lang === "ko" ? "시작일 ~ 종료일" : "Start date – end date";
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/* ───────── Icons ───────── */
|
|
21
33
|
|
|
22
34
|
function CalendarIcon() {
|
|
@@ -39,6 +51,8 @@ interface DatePickerContextValue {
|
|
|
39
51
|
setFocusedDate: (date: Date) => void;
|
|
40
52
|
formatDate: (date: Date) => string;
|
|
41
53
|
placeholder: string;
|
|
54
|
+
locale: string;
|
|
55
|
+
messages?: CalendarMessages;
|
|
42
56
|
min?: Date;
|
|
43
57
|
max?: Date;
|
|
44
58
|
disabled?: boolean;
|
|
@@ -76,10 +90,16 @@ export interface DatePickerProps {
|
|
|
76
90
|
/** 선택 가능 최대 날짜 (포함). 이후 날짜는 비활성. */
|
|
77
91
|
max?: Date;
|
|
78
92
|
/**
|
|
79
|
-
* 미선택 상태의 트리거 텍스트.
|
|
80
|
-
* @default "날짜 선택"
|
|
93
|
+
* 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 자동 생성("날짜 선택" / "Select date").
|
|
81
94
|
*/
|
|
82
95
|
placeholder?: string;
|
|
96
|
+
/**
|
|
97
|
+
* BCP47 로케일. 내부 Calendar 와 placeholder 기본값에 모두 적용된다.
|
|
98
|
+
* @default "ko-KR"
|
|
99
|
+
*/
|
|
100
|
+
locale?: string;
|
|
101
|
+
/** 내부 Calendar 의 nav/select aria-label override. */
|
|
102
|
+
messages?: CalendarMessages;
|
|
83
103
|
/** 비활성. 트리거 클릭·키보드 모두 차단. */
|
|
84
104
|
disabled?: boolean;
|
|
85
105
|
/** 읽기 전용. 트리거 표시는 유지하되 popover가 열리지 않는다. */
|
|
@@ -119,7 +139,9 @@ export function DatePicker({
|
|
|
119
139
|
formatDate = formatDefault,
|
|
120
140
|
min,
|
|
121
141
|
max,
|
|
122
|
-
placeholder
|
|
142
|
+
placeholder,
|
|
143
|
+
locale = DEFAULT_LOCALE,
|
|
144
|
+
messages,
|
|
123
145
|
disabled,
|
|
124
146
|
readOnly,
|
|
125
147
|
"aria-invalid": ariaInvalid,
|
|
@@ -128,6 +150,7 @@ export function DatePicker({
|
|
|
128
150
|
container,
|
|
129
151
|
children,
|
|
130
152
|
}: DatePickerProps) {
|
|
153
|
+
const resolvedPlaceholder = placeholder ?? defaultDatePlaceholder(locale);
|
|
131
154
|
const isControlled = value !== undefined;
|
|
132
155
|
const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
|
|
133
156
|
const selected = isControlled ? value : internal;
|
|
@@ -160,7 +183,9 @@ export function DatePicker({
|
|
|
160
183
|
focusedDate,
|
|
161
184
|
setFocusedDate,
|
|
162
185
|
formatDate,
|
|
163
|
-
placeholder,
|
|
186
|
+
placeholder: resolvedPlaceholder,
|
|
187
|
+
locale,
|
|
188
|
+
messages,
|
|
164
189
|
min,
|
|
165
190
|
max,
|
|
166
191
|
disabled,
|
|
@@ -174,7 +199,9 @@ export function DatePicker({
|
|
|
174
199
|
open,
|
|
175
200
|
focusedDate,
|
|
176
201
|
formatDate,
|
|
177
|
-
|
|
202
|
+
resolvedPlaceholder,
|
|
203
|
+
locale,
|
|
204
|
+
messages,
|
|
178
205
|
min,
|
|
179
206
|
max,
|
|
180
207
|
disabled,
|
|
@@ -355,6 +382,8 @@ export function DatePickerCalendar() {
|
|
|
355
382
|
onMonthChange={ctx.setFocusedDate}
|
|
356
383
|
min={ctx.min}
|
|
357
384
|
max={ctx.max}
|
|
385
|
+
locale={ctx.locale}
|
|
386
|
+
messages={ctx.messages}
|
|
358
387
|
/>
|
|
359
388
|
);
|
|
360
389
|
}
|
|
@@ -407,10 +436,16 @@ export interface DateRangePickerProps {
|
|
|
407
436
|
/** 선택 가능 최대 날짜. */
|
|
408
437
|
max?: Date;
|
|
409
438
|
/**
|
|
410
|
-
* 미선택 상태의 트리거 텍스트.
|
|
411
|
-
* @default "시작일 ~ 종료일"
|
|
439
|
+
* 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 자동 생성.
|
|
412
440
|
*/
|
|
413
441
|
placeholder?: string;
|
|
442
|
+
/**
|
|
443
|
+
* BCP47 로케일. 내부 Calendar 와 placeholder 기본값에 모두 적용.
|
|
444
|
+
* @default "ko-KR"
|
|
445
|
+
*/
|
|
446
|
+
locale?: string;
|
|
447
|
+
/** 내부 Calendar 의 nav/select aria-label override. */
|
|
448
|
+
messages?: CalendarMessages;
|
|
414
449
|
/** 비활성. */
|
|
415
450
|
disabled?: boolean;
|
|
416
451
|
/** 읽기 전용. popover가 열리지 않는다. */
|
|
@@ -438,7 +473,9 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
438
473
|
formatDate = formatDefault,
|
|
439
474
|
min,
|
|
440
475
|
max,
|
|
441
|
-
placeholder
|
|
476
|
+
placeholder,
|
|
477
|
+
locale = DEFAULT_LOCALE,
|
|
478
|
+
messages,
|
|
442
479
|
disabled,
|
|
443
480
|
readOnly,
|
|
444
481
|
"aria-invalid": ariaInvalid,
|
|
@@ -447,6 +484,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
447
484
|
},
|
|
448
485
|
ref,
|
|
449
486
|
) {
|
|
487
|
+
const resolvedPlaceholder = placeholder ?? defaultRangePlaceholder(locale);
|
|
450
488
|
const isControlled = value !== undefined;
|
|
451
489
|
const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
|
|
452
490
|
const selected = isControlled ? value : internal;
|
|
@@ -485,7 +523,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
485
523
|
}}
|
|
486
524
|
>
|
|
487
525
|
<span className={cn("sh-ui-date-picker__value", !displayText && "sh-ui-date-picker__placeholder")}>
|
|
488
|
-
{displayText ??
|
|
526
|
+
{displayText ?? resolvedPlaceholder}
|
|
489
527
|
</span>
|
|
490
528
|
<span className="sh-ui-date-picker__icon" aria-hidden>
|
|
491
529
|
<CalendarIcon />
|
|
@@ -509,6 +547,8 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
509
547
|
onMonthChange={setCalendarMonth}
|
|
510
548
|
min={min}
|
|
511
549
|
max={max}
|
|
550
|
+
locale={locale}
|
|
551
|
+
messages={messages}
|
|
512
552
|
/>
|
|
513
553
|
</BasePopover.Popup>
|
|
514
554
|
</BasePopover.Positioner>
|
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",
|