sh-ui-cli 0.61.1 → 0.61.3

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,28 @@
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.61.3",
7
+ "date": "2026-05-08",
8
+ "title": "fix — `sh-ui add tokens` 가 base=custom 테마에서 깨지던 회귀",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "**`sh-ui add tokens` 가 `theme.base: \"custom\"` 일 때 `해석 실패: {color.custom.50}` 로 깨지던 회귀 수정** — v0.61.2 에서 base64 테마 입력 시 `theme.base` 를 `'custom'` 으로 기록하기 시작했는데, 토큰 빌더가 `{color.<base>.<shade>}` placeholder 를 풀려다 'custom' 스케일을 못 찾아 throw 했음. base64 는 사후 색상 재해석이 불가능한 원본이므로 add tokens 를 no-op 으로 처리하고 안내 메시지 출력 (tokens.css 는 create 시점에 이미 정확히 주입됨)."
12
+ ],
13
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.61.3"
14
+ },
15
+ {
16
+ "version": "0.61.2",
17
+ "date": "2026-05-08",
18
+ "title": "fix — create 산출물의 sh-ui.config.json 누수 두 건 (paths.styles · theme.base)",
19
+ "type": "patch",
20
+ "highlights": [
21
+ "**템플릿의 `sh-ui.config.json` 에 `paths.styles` 추가** — `base.css` 같은 스타일 산출물이 `{styles}` placeholder 를 참조하는데 기존 템플릿(ui-app-template, nextjs-standalone fsd/flat) 이 `paths.styles` 를 안 박아 줘서 `sh-ui add base` 가 'paths.styles 가 sh-ui.config.json에 없습니다' 로 실패하던 문제. 이제 `create` 직후 추가 패치 없이 `add` 가 동작.",
22
+ "**`create --theme <preset|base64>` 결과가 `sh-ui.config.json` 의 `theme.base` 에 반영** — 기존엔 어떤 테마를 골라도 `theme.base` 가 템플릿 기본값(`neutral`)으로 남아 있어 의미 불일치. 이제 프리셋 이름이면 그 이름(`violet`/`rose`/...), base64 입력이면 `custom` 으로 기록.",
23
+ "**`init.mjs` 의 `PATHS.react` 에도 `styles` 추가** — `sh-ui init` 으로 만든 config 도 같은 시드 가짐."
24
+ ],
25
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.61.2"
26
+ },
5
27
  {
6
28
  "version": "0.61.1",
7
29
  "date": "2026-05-07",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.61.1",
3
+ "version": "0.61.3",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/add.mjs CHANGED
@@ -172,6 +172,19 @@ async function addTokens(config, cwd, diffMode, summary, conflictResolver) {
172
172
  if (!destRel) throw new Error("paths.tokens 가 설정에 없습니다.");
173
173
  const dest = resolve(cwd, destRel);
174
174
 
175
+ // theme.base === 'custom' 이면 토큰 빌더가 color.custom.X 스케일을 못 찾아 throw 한다 —
176
+ // base64 테마는 사후 색상 재생성 자체가 불가능 (원본 base64 가 단일 진실, 재해석 X).
177
+ // create 시점에 tokens.css 가 이미 정확히 주입돼 있으므로 보존하고 사용자에게 안내한다.
178
+ if (config.theme?.base === 'custom') {
179
+ if (!diffMode) {
180
+ console.log(
181
+ `↷ tokens → ${relative(cwd, dest)} (custom 테마 — tokens.css 보존, ` +
182
+ `색 조정은 파일 직접 편집 또는 sh_ui_encode_theme 으로 새 base64 생성 후 재스캐폴드)`
183
+ );
184
+ }
185
+ return;
186
+ }
187
+
175
188
  const { buildTokens } = await loadTokensBuilder();
176
189
  const content = await buildTokens(config);
177
190
 
@@ -78,7 +78,7 @@ import {
78
78
  DEFAULT_ARCH,
79
79
  } from './architectures/index.js';
80
80
  import { resolveTheme } from './theme/decode.js';
81
- import { THEME_PRESETS, getThemePreset } from './theme/presets.js';
81
+ import { THEME_PRESETS, THEME_PRESET_NAMES, getThemePreset } from './theme/presets.js';
82
82
  import {
83
83
  replaceSection,
84
84
  buildCssColorsBlock,
@@ -114,12 +114,15 @@ import {
114
114
  const TEMPLATES_DIR = getTemplatesRoot();
115
115
 
116
116
  /**
117
- * 템플릿 복사 직후 sh-ui.config.json 의 cssFramework 필드를 갱신.
117
+ * 템플릿 복사 직후 sh-ui.config.json 의 cssFramework + theme.base 필드를 갱신.
118
118
  *
119
119
  * Flutter 는 cssFramework 가 의미 없으므로 platform=flutter 면 필드 자체를 안 쓴다.
120
120
  * Next.js 는 plain/tailwind/css-modules 따라 컴포넌트 변종 결정 + base 파일도 분기 emit.
121
+ *
122
+ * themeBase 는 사용자가 고른 프리셋 이름(neutral/slate/rose/emerald/violet) 또는
123
+ * 커스텀 base64 였을 때 'custom'. null 이면 템플릿 기본값 유지.
121
124
  */
122
- async function patchShUiConfig(configPath, cssFramework) {
125
+ async function patchShUiConfig(configPath, cssFramework, themeBase) {
123
126
  if (!(await fs.pathExists(configPath))) return;
124
127
  const config = await fs.readJson(configPath);
125
128
  // Flutter 는 cssFramework 무관 — 필드 자체를 두지 않는다.
@@ -128,6 +131,10 @@ async function patchShUiConfig(configPath, cssFramework) {
128
131
  } else {
129
132
  config.cssFramework = cssFramework ?? CSS_FRAMEWORK_DEFAULT;
130
133
  }
134
+ if (themeBase != null) {
135
+ config.theme = config.theme ?? {};
136
+ config.theme.base = themeBase;
137
+ }
131
138
  await fs.writeJson(configPath, config, { spaces: 2 });
132
139
  }
133
140
 
@@ -208,8 +215,12 @@ export async function createProject(options = {}) {
208
215
  }
209
216
 
210
217
  let theme = null;
218
+ // themeBase: 'neutral'|'slate'|'rose'|'emerald'|'violet'|'custom'|null.
219
+ // sh-ui.config.json 의 theme.base 가 실제 사용된 팔레트를 반영하게 한다 (이전엔 항상 템플릿 기본값으로 남아 있던 문제).
220
+ let themeBase = null;
211
221
  if (options.theme) {
212
222
  theme = resolveTheme(options.theme);
223
+ themeBase = THEME_PRESET_NAMES.includes(options.theme) ? options.theme : 'custom';
213
224
  } else if (process.stdin.isTTY && !options.yes) {
214
225
  // --yes 는 "선택 옵션은 기본값으로" 의미. 테마는 옵션이므로 prompt 우회.
215
226
  const NONE = '__none__';
@@ -224,7 +235,10 @@ export async function createProject(options = {}) {
224
235
  })),
225
236
  ],
226
237
  });
227
- if (choice !== NONE) theme = getThemePreset(choice);
238
+ if (choice !== NONE) {
239
+ theme = getThemePreset(choice);
240
+ themeBase = choice;
241
+ }
228
242
  }
229
243
 
230
244
  // dry-run 은 tmpdir 에 그대로 생성한 뒤 파일 목록 출력 + 정리.
@@ -250,7 +264,7 @@ export async function createProject(options = {}) {
250
264
  }
251
265
 
252
266
  if (platform === 'flutter') {
253
- await generateFlutter(targetDir, projectName, theme, cssFramework);
267
+ await generateFlutter(targetDir, projectName, theme, cssFramework, themeBase);
254
268
  await finalizeProject(targetDir, { dryRun: options.dryRun });
255
269
  console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
256
270
  console.log(`\n cd ${projectName}`);
@@ -276,9 +290,9 @@ export async function createProject(options = {}) {
276
290
  plugins.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
277
291
 
278
292
  if (projectType === 'standalone') {
279
- await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch);
293
+ await generateStandalone(targetDir, projectName, plugins, theme, cssFramework, arch, themeBase);
280
294
  } else {
281
- await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework, arch });
295
+ await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework, arch, themeBase });
282
296
  }
283
297
 
284
298
  await finalizeProject(targetDir, { dryRun: options.dryRun });
@@ -444,14 +458,14 @@ export async function addComponent(componentName, appName) {
444
458
 
445
459
  // ─── Generators ───
446
460
 
447
- async function generateFlutter(targetDir, projectName, theme, css) {
461
+ async function generateFlutter(targetDir, projectName, theme, css, themeBase) {
448
462
  await fs.copy(path.join(TEMPLATES_DIR, 'flutter-standalone'), targetDir);
449
463
  await replaceInAllFiles(targetDir, '{{project_name}}', projectName);
450
464
  await injectDartTheme(targetDir, theme);
451
- await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
465
+ await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
452
466
  }
453
467
 
454
- async function generateStandalone(targetDir, projectName, plugins, theme, css, arch) {
468
+ async function generateStandalone(targetDir, projectName, plugins, theme, css, arch, themeBase) {
455
469
  // 베이스 (arch-neutral) + arch 오버레이 — generateApp 과 같은 패턴.
456
470
  await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir, {
457
471
  filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
@@ -488,10 +502,10 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
488
502
  await applyTransforms(targetDir, plugins, arch);
489
503
  await applyCssFrameworkVariant(targetDir, css, { isMonorepo: false, plugins, arch });
490
504
  await injectCssTheme(targetDir, theme);
491
- await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
505
+ await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
492
506
  }
493
507
 
494
- async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch } = {}) {
508
+ async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase } = {}) {
495
509
  await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
496
510
 
497
511
  // Update root package.json
@@ -526,7 +540,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
526
540
  // generateApp 이 ui-{app} 패키지의 cssFramework 변종까지 처리. 여기선 theme + sh-ui.config.json 만.
527
541
  const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
528
542
  await injectCssTheme(uiAppDir, theme);
529
- await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css);
543
+ await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css, themeBase);
530
544
  }
531
545
 
532
546
  async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailwind') {
package/src/init.mjs CHANGED
@@ -26,6 +26,7 @@ const DEFAULTS = INIT_DEFAULTS;
26
26
  const PATHS = {
27
27
  react: {
28
28
  tokens: "src/styles/tokens.css",
29
+ styles: "src/styles",
29
30
  components: "src/components/ui",
30
31
  utils: "src/lib/utils.ts",
31
32
  },
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "paths": {
10
10
  "tokens": "lib/styles/tokens.css",
11
+ "styles": "lib/styles",
11
12
  "components": "components/common",
12
13
  "utils": "lib/utils/utils.ts"
13
14
  },
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "paths": {
10
10
  "tokens": "src/shared/styles/tokens.css",
11
+ "styles": "src/shared/styles",
11
12
  "components": "src/shared/ui",
12
13
  "utils": "src/shared/lib/utils.ts"
13
14
  },
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "paths": {
10
10
  "tokens": "src/styles/tokens.css",
11
+ "styles": "src/styles",
11
12
  "components": "src/components",
12
13
  "utils": "src/lib/utils.ts"
13
14
  },