sh-ui-cli 0.61.3 → 0.62.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,31 @@
|
|
|
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.62.0",
|
|
7
|
+
"date": "2026-05-08",
|
|
8
|
+
"title": "minor — `theme` 컴포넌트가 next-themes 어댑터로 통합 (RootLayout 자동 wiring)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`sh-ui add theme` 의 `useTheme` / `ThemeProvider` 가 next-themes 위에서 동작** — 템플릿이 RootLayout 에 이미 깔아둔 next-themes ThemeProvider 와 동일한 컨텍스트를 공유. 사용자가 add 후 어디서든 `useTheme` 호출 시 throw 없이 즉시 동작. 이전엔 templates 의 next-themes 와 sh-ui 자체 cookie ThemeProvider 두 시스템이 분리돼 있어 혼선.",
|
|
12
|
+
"**시그니처는 보존, 내부 영속화는 cookie → next-themes localStorage 로 이전** — `{ theme, setTheme, toggleTheme }` 와 `light`/`dark` 두 값만 노출하는 정책 그대로. 사용자 코드는 변경 불필요. 다만 기존 `sh-ui-theme` cookie 값은 새 키로 자동 마이그레이션되지 않음(첫 방문 시 defaultTheme 으로 초기화).",
|
|
13
|
+
"**registry.json `theme.dependencies` 에 `next-themes` 추가** — `sh-ui add theme` 시 외부 패키지 자동 설치 안내에 포함. peer-versions.json 에 `^0.4.6` 핀."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.62.0"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.61.4",
|
|
19
|
+
"date": "2026-05-08",
|
|
20
|
+
"title": "fix — MCP 안내가 v0.61.2 의 `create` one-shot 흐름과 일치하도록 갱신",
|
|
21
|
+
"type": "patch",
|
|
22
|
+
"highlights": [
|
|
23
|
+
"**MCP 서버 instructions 의 '테마 커스터마이징' 섹션 정정** — 이전엔 '스캐폴드 후 `tokens.css` 직접 편집 + base64 백업' 을 권장했지만, v0.61.2 부터 `create` 가 단일 진입점으로 동작하므로 'encode-first, then create with theme arg' 흐름으로 flip. 사후 `tokens.css` 손편집은 다음 재스캐폴드 시 유실되는 함정.",
|
|
24
|
+
"**`sh_ui_create_project` description** 에 paths.styles · theme.base 자동 처리 명시 — AI 가 사후 패치 시도 안 하도록 가드.",
|
|
25
|
+
"**`sh_ui_add_component` description** 에 `theme.base: 'custom'` 일 때 `tokens` 는 no-op (보존) 라는 v0.61.3 회귀 가드 동작 명시.",
|
|
26
|
+
"**`sh_ui_encode_theme` description** 에서 'tokens.css 손편집 백업용' 표현 제거 — 이제는 'create 호출 전에 base64 만들어 theme 인자로 넘기는 정석 흐름' 으로 표현."
|
|
27
|
+
],
|
|
28
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.61.4"
|
|
29
|
+
},
|
|
5
30
|
{
|
|
6
31
|
"version": "0.61.3",
|
|
7
32
|
"date": "2026-05-08",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
useTheme as useNextTheme,
|
|
7
|
+
} from "next-themes";
|
|
4
8
|
|
|
5
9
|
type Theme = "light" | "dark";
|
|
6
10
|
|
|
7
|
-
const THEME_COOKIE_NAME = "sh-ui-theme";
|
|
8
|
-
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
9
|
-
|
|
10
11
|
/* ───────────── Context ───────────── */
|
|
11
12
|
|
|
12
13
|
type ThemeContextValue = {
|
|
@@ -15,94 +16,94 @@ type ThemeContextValue = {
|
|
|
15
16
|
toggleTheme: () => void;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 현재 테마와 setter 를 반환한다. ThemeProvider (또는 next-themes 의
|
|
21
|
+
* ThemeProvider) 안에서만 호출 가능.
|
|
22
|
+
*
|
|
23
|
+
* 내부적으로 next-themes 의 useTheme 를 어댑팅 — `resolvedTheme` 을
|
|
24
|
+
* `light`/`dark` 로 좁혀 노출하고, system 모드는 감추는 형태.
|
|
25
|
+
*/
|
|
26
|
+
export function useTheme(): ThemeContextValue {
|
|
27
|
+
const { resolvedTheme, setTheme: setNextTheme } = useNextTheme();
|
|
28
|
+
const theme: Theme = resolvedTheme === "dark" ? "dark" : "light";
|
|
29
|
+
|
|
30
|
+
const setTheme = React.useCallback(
|
|
31
|
+
(next: Theme) => setNextTheme(next),
|
|
32
|
+
[setNextTheme],
|
|
33
|
+
);
|
|
34
|
+
const toggleTheme = React.useCallback(
|
|
35
|
+
() => setNextTheme(theme === "dark" ? "light" : "dark"),
|
|
36
|
+
[setNextTheme, theme],
|
|
37
|
+
);
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return ctx;
|
|
39
|
+
return React.useMemo(
|
|
40
|
+
() => ({ theme, setTheme, toggleTheme }),
|
|
41
|
+
[theme, setTheme, toggleTheme],
|
|
42
|
+
);
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
/* ───────────── Provider ───────────── */
|
|
28
46
|
|
|
29
47
|
export interface ThemeProviderProps {
|
|
30
48
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
49
|
+
* 비제어 모드의 초기 테마. next-themes 가 storage(localStorage) 에 저장된
|
|
50
|
+
* 값을 우선하므로, 사용자가 한 번 선택한 후에는 이 값이 무시된다.
|
|
33
51
|
*
|
|
34
52
|
* @default "light"
|
|
35
|
-
* @example
|
|
36
|
-
* // Next.js App Router
|
|
37
|
-
* const t = (await cookies()).get("sh-ui-theme")?.value;
|
|
38
|
-
* <ThemeProvider defaultTheme={t === "dark" ? "dark" : "light"}>
|
|
39
53
|
*/
|
|
40
54
|
defaultTheme?: Theme;
|
|
41
55
|
/**
|
|
42
|
-
*
|
|
43
|
-
* 보통 `defaultTheme`
|
|
56
|
+
* 제어 모드 — 지정 시 강제 테마로 고정 (next-themes `forcedTheme`).
|
|
57
|
+
* 보통 `defaultTheme` 비제어로 충분.
|
|
44
58
|
*/
|
|
45
59
|
theme?: Theme;
|
|
46
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* 테마 변경 콜백. next-themes 자체는 setter 호출 시 콜백을 노출하지 않으므로
|
|
62
|
+
* 내부 effect 로 변화를 감지해 호출한다.
|
|
63
|
+
*/
|
|
47
64
|
onThemeChange?: (theme: Theme) => void;
|
|
48
65
|
children: React.ReactNode;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
/**
|
|
52
|
-
* 다크/라이트
|
|
53
|
-
*
|
|
69
|
+
* 다크/라이트 테마와 `<html class="dark">` 토글을 담당하는 Provider 어댑터.
|
|
70
|
+
*
|
|
71
|
+
* 내부 구현은 next-themes — `attribute='class'`, `enableSystem={false}`,
|
|
72
|
+
* `disableTransitionOnChange` 로 고정. SSR/hydration mismatch 방지를 위해
|
|
73
|
+
* `<html suppressHydrationWarning>` 을 RootLayout 에 함께 둘 것.
|
|
54
74
|
*/
|
|
55
75
|
export function ThemeProvider({
|
|
56
76
|
defaultTheme = "light",
|
|
57
|
-
theme
|
|
77
|
+
theme,
|
|
58
78
|
onThemeChange,
|
|
59
79
|
children,
|
|
60
80
|
}: ThemeProviderProps) {
|
|
61
|
-
const [_theme, _setTheme] = React.useState<Theme>(defaultTheme);
|
|
62
|
-
const theme = themeProp ?? _theme;
|
|
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
|
-
|
|
79
|
-
const setTheme = React.useCallback(
|
|
80
|
-
(next: Theme) => {
|
|
81
|
-
if (onThemeChange) onThemeChange(next);
|
|
82
|
-
else _setTheme(next);
|
|
83
|
-
|
|
84
|
-
if (typeof document !== "undefined") {
|
|
85
|
-
const root = document.documentElement.classList;
|
|
86
|
-
root.toggle("dark", next === "dark");
|
|
87
|
-
root.toggle("light", next === "light");
|
|
88
|
-
document.cookie = `${THEME_COOKIE_NAME}=${next}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
[onThemeChange],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const toggleTheme = React.useCallback(() => {
|
|
95
|
-
setTheme(theme === "dark" ? "light" : "dark");
|
|
96
|
-
}, [theme, setTheme]);
|
|
97
|
-
|
|
98
|
-
const value = React.useMemo<ThemeContextValue>(
|
|
99
|
-
() => ({ theme, setTheme, toggleTheme }),
|
|
100
|
-
[theme, setTheme, toggleTheme],
|
|
101
|
-
);
|
|
102
|
-
|
|
103
81
|
return (
|
|
104
|
-
<
|
|
82
|
+
<NextThemesProvider
|
|
83
|
+
attribute="class"
|
|
84
|
+
defaultTheme={defaultTheme}
|
|
85
|
+
enableSystem={false}
|
|
86
|
+
disableTransitionOnChange
|
|
87
|
+
forcedTheme={theme}
|
|
88
|
+
themes={["light", "dark"]}
|
|
89
|
+
>
|
|
90
|
+
{onThemeChange ? <ThemeChangeBridge onThemeChange={onThemeChange} /> : null}
|
|
105
91
|
{children}
|
|
106
|
-
</
|
|
92
|
+
</NextThemesProvider>
|
|
107
93
|
);
|
|
108
94
|
}
|
|
95
|
+
|
|
96
|
+
function ThemeChangeBridge({
|
|
97
|
+
onThemeChange,
|
|
98
|
+
}: {
|
|
99
|
+
onThemeChange: (theme: Theme) => void;
|
|
100
|
+
}) {
|
|
101
|
+
const { theme } = useTheme();
|
|
102
|
+
const last = React.useRef<Theme | null>(null);
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (last.current === theme) return;
|
|
105
|
+
last.current = theme;
|
|
106
|
+
onThemeChange(theme);
|
|
107
|
+
}, [theme, onThemeChange]);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
package/package.json
CHANGED
package/src/mcp.mjs
CHANGED
|
@@ -179,17 +179,23 @@ function buildServerInstructions(cliName) {
|
|
|
179
179
|
|
|
180
180
|
사용자가 "apps/web 을 apps/dashboard 로 바꿔줘" 같이 모노레포 앱 이름 변경을 요청하면 \`sh_ui_rename_app\` 사용 — 손으로 6~10 군데 (디렉토리, package.json name, tsconfig paths, Dockerfile WORKDIR, next.config transpilePackages, sh-ui.config aliases, README, .github/workflows) 갈아엎지 않도록 자동화. \`dryRun: true\` 로 먼저 변경 매트릭스 보여주고 사용자 확인 후 실행 권장.
|
|
181
181
|
|
|
182
|
-
## 테마
|
|
182
|
+
## 테마 — encode-first, edit-later 금지
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
\`sh_ui_create_project\` 는 **단일 진입점**이다. 톤은 create **호출 전에** 정하고 \`theme\` 인자로 넘긴다 — 스캐폴드 후 \`tokens.css\` 를 손으로 고치는 흐름은 사용하지 않는다 (변경분이 다음 재스캐폴드 때 유실됨).
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
2. \`sh_ui_encode_theme\` 으로 \`{ light, dark, radius }\` 객체를 base64 로 인코딩.
|
|
188
|
-
- 옵셔널 색 토큰(\`success\`/\`warning\`/\`info\` + \`-foreground\`)도 같이 넣을 수 있음.
|
|
189
|
-
3. 그 base64 를 사용자에게 보여주고 (또는 메모리에 저장), **다음 \`sh_ui_create_project\` 호출 시 \`theme\` 인자에 그대로** 넣어 영구 보관.
|
|
190
|
-
4. 기존 base64 를 일부만 고치고 싶으면 \`sh_ui_decode_theme\` → 객체 수정 → \`sh_ui_encode_theme\` round-trip.
|
|
186
|
+
### 정석 흐름
|
|
191
187
|
|
|
192
|
-
|
|
188
|
+
1. **톤 정한다** — 프리셋(\`neutral\`/\`slate\`/\`rose\`/\`emerald\`/\`violet\`) 또는 base64.
|
|
189
|
+
2. **커스텀이면 \`sh_ui_encode_theme\` 으로 base64 만든다** — \`{ light, dark, radius }\` (+ 옵셔널 \`success\`/\`warning\`/\`info\`). 기존 base64 일부 수정은 \`sh_ui_decode_theme\` → 객체 수정 → 재인코딩 round-trip.
|
|
190
|
+
3. **\`sh_ui_create_project\` 의 \`theme\` 인자에 그대로 넘긴다** — 길이로 프리셋/base64 자동 판별. 결과:
|
|
191
|
+
- \`tokens.css\` 의 색 블록이 정확히 주입됨 (수동 수정 불필요).
|
|
192
|
+
- \`sh-ui.config.json\` 의 \`theme.base\` 가 프리셋이면 그 이름, base64 면 \`'custom'\` 으로 기록.
|
|
193
|
+
- \`paths.styles\` 도 자동 박힘 — \`sh_ui_add_component\` 가 \`base\` 같은 스타일 산출물을 깨짐 없이 처리.
|
|
194
|
+
4. **base64 는 메모리에 저장**해 둔다 (사용자별 프로젝트 메모리). 같은 프로젝트 재스캐폴드/이주 시 같은 톤 보존용.
|
|
195
|
+
|
|
196
|
+
### 기존 프로젝트 톤만 바꾸고 싶을 때
|
|
197
|
+
|
|
198
|
+
> v0.61.2 부터 \`theme.base: "custom"\` 인 프로젝트에서 \`sh_ui_add_component\` 의 \`tokens\` 는 no-op (보존). 색을 바꾸려면 새 base64 를 만들고 새 디렉토리로 \`force: true\` 재스캐폴드하는 게 정석. 부분 편집을 원해도 \`tokens.css\` 직접 수정 후 \`sh_ui_encode_theme\` 로 새 base64 백업까지 같이 — 그 base64 를 메모리에 갱신해야 다음 재스캐폴드와 일관됨.
|
|
193
199
|
`;
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -219,7 +225,9 @@ export async function startMcpServer() {
|
|
|
219
225
|
{
|
|
220
226
|
description:
|
|
221
227
|
"빈 폴더에 sh-ui 프로젝트 스캐폴드 — Next.js (standalone/monorepo) 또는 Flutter. " +
|
|
222
|
-
`FSD 폴더 구조 + 토큰 + sh-ui.config.json 일괄 생성. 사용자가 '새 프로젝트' / '빈 폴더' / '스캐폴드부터' 류 요청을 하면 이 툴 사용 (Bash 로 npx ${cliName} create 직접 호출보다 우선)
|
|
228
|
+
`FSD 폴더 구조 + 토큰 + sh-ui.config.json 일괄 생성. 사용자가 '새 프로젝트' / '빈 폴더' / '스캐폴드부터' 류 요청을 하면 이 툴 사용 (Bash 로 npx ${cliName} create 직접 호출보다 우선). ` +
|
|
229
|
+
"**단일 진입점** — theme/plugins/cssFramework/structure 모두 호출 시점에 정해서 한 번에 박는다. 호출 후 sh-ui.config.json/tokens.css 를 손으로 패치하지 말 것 (다음 재스캐폴드 시 유실). " +
|
|
230
|
+
"산출물: theme 인자가 프리셋이면 sh-ui.config.json 의 theme.base 가 그 이름, base64 면 'custom'. paths.styles · paths.tokens 도 자동 박혀서 sh_ui_add_component 가 사후 패치 없이 동작.",
|
|
223
231
|
inputSchema: {
|
|
224
232
|
name: z.string().min(1)
|
|
225
233
|
.describe("프로젝트 디렉토리 이름. 예: my-app"),
|
|
@@ -412,7 +420,8 @@ export async function startMcpServer() {
|
|
|
412
420
|
{
|
|
413
421
|
description:
|
|
414
422
|
"컴포넌트 한 개 이상을 프로젝트에 설치. 외부 npm 패키지 deps 도 자동 설치(pnpm/npm/yarn/bun 자동 감지). " +
|
|
415
|
-
"특수값 'tokens' 는 sh-ui.config.json 기반 토큰 파일
|
|
423
|
+
"특수값 'tokens' 는 sh-ui.config.json 기반 토큰 파일 생성 — 단, theme.base 가 'custom' (= base64 로 만든 프로젝트) 이면 tokens 는 no-op (create 시점에 정확히 주입됐으므로 보존). " +
|
|
424
|
+
"특수값 'base' 는 sh-ui:reset 스타일 — paths.styles 가 sh-ui.config.json 에 있어야 동작 (v0.61.2+ 의 create 산출물은 자동 포함).",
|
|
416
425
|
inputSchema: {
|
|
417
426
|
names: z.array(z.string()).min(1)
|
|
418
427
|
.describe("설치할 컴포넌트 이름들. 예: ['tokens', 'button', 'dialog']"),
|
|
@@ -487,9 +496,9 @@ export async function startMcpServer() {
|
|
|
487
496
|
"sh_ui_encode_theme",
|
|
488
497
|
{
|
|
489
498
|
description:
|
|
490
|
-
"
|
|
491
|
-
"
|
|
492
|
-
"
|
|
499
|
+
"색 토큰 객체({ light, dark, radius }) 를 sh-ui base64 테마 코드로 인코딩. " +
|
|
500
|
+
"정석 흐름: 톤을 정해 (사용자와 합의 또는 프리셋 변형) 이 툴로 base64 만든 뒤 sh_ui_create_project 의 theme 인자에 그대로 넘긴다 — create 한 번에 끝, 스캐폴드 후 tokens.css 손편집 불필요. " +
|
|
501
|
+
"이미 만든 프로젝트의 톤을 다음 재스캐폴드까지 영구 보관할 때도 사용 (메모리 등에 base64 저장).",
|
|
493
502
|
inputSchema: {
|
|
494
503
|
light: tokenMapSchema.describe("라이트 모드 토큰"),
|
|
495
504
|
dark: tokenMapSchema.describe("다크 모드 토큰"),
|