sh-ui-cli 0.42.1 → 0.43.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "$description": "React 레지스트리 매니페스트 — CLI가 각 컴포넌트 name으로 조회해 files를 사용자 프로젝트로 복사한다.",
2
+ "$description": "React 레지스트리 매니페스트 — CLI가 각 컴포넌트 name으로 조회해 files를 사용자 프로젝트로 복사한다. 컴포넌트 또는 file 엔트리에 frameworks?: string[] 옵션 — 미지정시 모든 cssFramework 에 적용, 지정시 해당 배열에 포함된 경우만 복사.",
3
3
  "components": {
4
4
  "button": {
5
5
  "name": "button",
@@ -7,14 +7,23 @@
7
7
  "files": [
8
8
  {
9
9
  "src": "components/button/index.tsx",
10
- "dest": "{components}/button/index.tsx"
10
+ "dest": "{components}/button/index.tsx",
11
+ "frameworks": ["plain"]
11
12
  },
12
13
  {
13
14
  "src": "components/button/styles.css",
14
- "dest": "{components}/button/styles.css"
15
+ "dest": "{components}/button/styles.css",
16
+ "frameworks": ["plain"]
17
+ },
18
+ {
19
+ "src": "components/button/index.tailwind.tsx",
20
+ "dest": "{components}/button/index.tsx",
21
+ "frameworks": ["tailwind"]
15
22
  }
16
23
  ],
17
- "dependencies": [],
24
+ "dependencies": [
25
+ { "name": "class-variance-authority", "frameworks": ["tailwind"] }
26
+ ],
18
27
  "registryDependencies": []
19
28
  },
20
29
  "card": {
@@ -23,11 +32,18 @@
23
32
  "files": [
24
33
  {
25
34
  "src": "components/card/index.tsx",
26
- "dest": "{components}/card/index.tsx"
35
+ "dest": "{components}/card/index.tsx",
36
+ "frameworks": ["plain"]
27
37
  },
28
38
  {
29
39
  "src": "components/card/styles.css",
30
- "dest": "{components}/card/styles.css"
40
+ "dest": "{components}/card/styles.css",
41
+ "frameworks": ["plain"]
42
+ },
43
+ {
44
+ "src": "components/card/index.tailwind.tsx",
45
+ "dest": "{components}/card/index.tsx",
46
+ "frameworks": ["tailwind"]
31
47
  }
32
48
  ],
33
49
  "dependencies": [],
@@ -39,11 +55,18 @@
39
55
  "files": [
40
56
  {
41
57
  "src": "components/input/index.tsx",
42
- "dest": "{components}/input/index.tsx"
58
+ "dest": "{components}/input/index.tsx",
59
+ "frameworks": ["plain"]
43
60
  },
44
61
  {
45
62
  "src": "components/input/styles.css",
46
- "dest": "{components}/input/styles.css"
63
+ "dest": "{components}/input/styles.css",
64
+ "frameworks": ["plain"]
65
+ },
66
+ {
67
+ "src": "components/input/index.tailwind.tsx",
68
+ "dest": "{components}/input/index.tsx",
69
+ "frameworks": ["tailwind"]
47
70
  }
48
71
  ],
49
72
  "dependencies": [],
@@ -102,6 +102,29 @@ function mergeThemeIndependent(tokens) {
102
102
  return out;
103
103
  }
104
104
 
105
+ /**
106
+ * Tailwind v4 의 @theme inline 블록에 sh-ui 토큰을 매핑해
107
+ * `bg-primary`, `border-border`, `rounded-md` 같은 Tailwind utility 가
108
+ * 동작하도록 한다. 매핑은 light 토큰 맵에서 파생 — 모든 색 키를
109
+ * Tailwind 의 --color-* 네임스페이스로 옮김.
110
+ *
111
+ * radius 는 단일 토큰 (--radius) 이지만 Tailwind 의 rounded-{sm,md,lg,xl}
112
+ * 4 단계로 expand. 템플릿이 손으로 박아두던 패턴을 단일 소스로 옮긴 것.
113
+ */
114
+ function buildTailwindThemeBlock(lightTokens) {
115
+ const lines = [];
116
+ for (const path of Object.keys(lightTokens)) {
117
+ const cssVar = toCssVar(path);
118
+ const themeKey = cssVar.replace(/^--/, "--color-");
119
+ lines.push(` ${themeKey}: var(${cssVar});`);
120
+ }
121
+ lines.push(` --radius-sm: calc(var(--radius) - 2px);`);
122
+ lines.push(` --radius-md: var(--radius);`);
123
+ lines.push(` --radius-lg: calc(var(--radius) + 2px);`);
124
+ lines.push(` --radius-xl: calc(var(--radius) + 4px);`);
125
+ return `@theme inline {\n${lines.join("\n")}\n}`;
126
+ }
127
+
105
128
  export async function buildTokensCss(config) {
106
129
  const tokens = await resolveTokens(config);
107
130
  const mode = config.theme.mode;
@@ -116,6 +139,7 @@ export async function buildTokensCss(config) {
116
139
  if (mode === "light-dark") {
117
140
  blocks.push(emitCssBlock(".dark", tokens.dark));
118
141
  }
142
+ blocks.push(buildTailwindThemeBlock(tokens.light));
119
143
  const header = `/* Generated by @sh-ui/tokens — do not edit directly */\n/* base=${config.theme.base} radius=${config.theme.radius} mode=${config.theme.mode} */\n`;
120
144
  return header + "\n" + blocks.join("\n\n") + "\n";
121
145
  }
@@ -533,6 +557,48 @@ import 'package:flutter/material.dart';
533
557
  return `${header}\n${classes.join("\n\n")}\n\n${themeClass}\n`;
534
558
  }
535
559
 
560
+ /* ───────── Emitter 디스패처 ─────────
561
+ *
562
+ * (platform × cssFramework) → 토큰 emitter.
563
+ * 향후 Tailwind theme config / CSS Modules / vanilla-extract 추가 시
564
+ * 이 테이블에 한 줄만 등록하면 끝나도록 구조를 미리 잡아 둠.
565
+ *
566
+ * Flutter 는 CSS 프레임워크 개념이 무의미하지만, 일관된 디스패치를 위해
567
+ * "plain" 키로 통일 — 호출부가 platform 별 분기 없이 같은 진입점을 쓸 수 있게.
568
+ */
569
+ const tokenEmitters = {
570
+ react: {
571
+ plain: buildTokensCss,
572
+ // Tailwind 변종은 같은 tokens.css 를 공유. 컴포넌트의 utility class 가
573
+ // var(--primary) 를 @theme inline 매핑을 통해 참조하므로 — 토큰 자체는
574
+ // 동일. 향후 Tailwind v3 theme.config.ts 를 별도 emit 하고 싶으면
575
+ // 여기에 다른 함수를 등록.
576
+ tailwind: buildTokensCss,
577
+ },
578
+ flutter: {
579
+ plain: buildTokensDart,
580
+ },
581
+ };
582
+
583
+ export async function buildTokens(config) {
584
+ const platform = config.platform;
585
+ const fw = config.cssFramework ?? "plain";
586
+ const platformEmitters = tokenEmitters[platform];
587
+ if (!platformEmitters) {
588
+ throw new Error(
589
+ `tokens: 알 수 없는 platform '${platform}'. 지원: ${Object.keys(tokenEmitters).join(", ")}`,
590
+ );
591
+ }
592
+ const emit = platformEmitters[fw];
593
+ if (!emit) {
594
+ const supported = Object.keys(platformEmitters).join(", ");
595
+ throw new Error(
596
+ `tokens emitter 미구현: ${platform}/${fw}. 현재 지원: ${platform}/{${supported}}`,
597
+ );
598
+ }
599
+ return emit(config);
600
+ }
601
+
536
602
  /* ───────── CLI ───────── */
537
603
 
538
604
  if (import.meta.url === `file://${process.argv[1]}`) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
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
@@ -149,11 +149,8 @@ async function addTokens(config, cwd, diffMode, summary, conflictResolver) {
149
149
  if (!destRel) throw new Error("paths.tokens 가 설정에 없습니다.");
150
150
  const dest = resolve(cwd, destRel);
151
151
 
152
- const { buildTokensCss, buildTokensDart } = await loadTokensBuilder();
153
- const content =
154
- config.platform === "react"
155
- ? await buildTokensCss(config)
156
- : await buildTokensDart(config);
152
+ const { buildTokens } = await loadTokensBuilder();
153
+ const content = await buildTokens(config);
157
154
 
158
155
  const result = await writeOrDiff({ dest, content, cwd, diffMode, summary, conflictResolver });
159
156
  if (!diffMode && result !== "unchanged") {
@@ -163,6 +160,31 @@ async function addTokens(config, cwd, diffMode, summary, conflictResolver) {
163
160
  }
164
161
  }
165
162
 
163
+ /**
164
+ * registry 엔트리의 frameworks 필드와 현재 cssFramework 가 호환되는지.
165
+ * 필드가 없으면 "모든 프레임워크에 적용" — 기본 케이스.
166
+ */
167
+ function frameworkMatches(entry, cssFramework) {
168
+ if (!entry.frameworks) return true;
169
+ return entry.frameworks.includes(cssFramework);
170
+ }
171
+
172
+ /**
173
+ * cssFramework="tailwind" 인데 컴포넌트에 tailwind 전용 변종 파일이 없으면
174
+ * plain 으로 fallback. plain CSS 컴포넌트도 @theme inline 브리지 덕분에
175
+ * Tailwind v4 프로젝트에서 그대로 동작하므로 깨지지 않음.
176
+ *
177
+ * 점진적 rollout 전략 — 모든 컴포넌트가 한 번에 tailwind 변종을 갖출 필요 없이
178
+ * 가능한 것부터 utility-class 변종을 제공하고, 나머지는 plain 으로 자연 처리.
179
+ */
180
+ function effectiveFramework(entry, cssFramework) {
181
+ if (cssFramework !== "tailwind") return cssFramework;
182
+ const hasTailwindVariant = (entry.files ?? []).some(
183
+ (f) => f.frameworks && f.frameworks.includes("tailwind"),
184
+ );
185
+ return hasTailwindVariant ? "tailwind" : "plain";
186
+ }
187
+
166
188
  async function addComponent(name, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver) {
167
189
  const registryRoot = getRegistryRoot(config.platform);
168
190
  const registry = JSON.parse(
@@ -175,11 +197,30 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
175
197
  );
176
198
  }
177
199
 
200
+ const requestedFw = config.cssFramework ?? "plain";
201
+ const cssFramework = effectiveFramework(entry, requestedFw);
202
+
203
+ // 사용자가 tailwind 를 골랐는데 이 컴포넌트는 plain 으로 fallback 된 경우 한 줄 알림.
204
+ // 동작에 문제는 없지만 일관성에 대한 기대를 정확히 셋업하기 위함.
205
+ if (requestedFw === "tailwind" && cssFramework === "plain" && !diffMode) {
206
+ console.log(
207
+ `ℹ ${name} — Tailwind 변종 미제공, plain 변종으로 설치 (Tailwind v4 환경에서 그대로 동작)`,
208
+ );
209
+ }
210
+
211
+ if (!frameworkMatches(entry, cssFramework)) {
212
+ console.log(
213
+ `↷ ${name} skipped — cssFramework=${cssFramework} 미지원 (지원: ${entry.frameworks.join(", ")})`,
214
+ );
215
+ return;
216
+ }
217
+
178
218
  for (const dep of entry.registryDependencies ?? []) {
179
219
  await addOne(dep, config, cwd, installed, pendingDeps, diffMode, summary, conflictResolver);
180
220
  }
181
221
 
182
222
  for (const file of entry.files) {
223
+ if (!frameworkMatches(file, cssFramework)) continue;
183
224
  const src = resolve(registryRoot, file.src);
184
225
  const dest = resolve(cwd, resolveDest(file.dest, config));
185
226
  const content = await readFile(src, "utf8");
@@ -192,7 +233,14 @@ async function addComponent(name, config, cwd, installed, pendingDeps, diffMode,
192
233
  }
193
234
 
194
235
  for (const dep of entry.dependencies ?? []) {
195
- pendingDeps.add(dep);
236
+ // dep 은 string ("react-hook-form") 또는 object ({name, frameworks?: string[]}).
237
+ // 후자는 cssFramework 에 따라 의존성을 분기 (예: cva 는 tailwind 변종에만 필요).
238
+ if (typeof dep === "string") {
239
+ pendingDeps.add(dep);
240
+ } else if (dep && typeof dep === "object" && dep.name) {
241
+ if (dep.frameworks && !dep.frameworks.includes(cssFramework)) continue;
242
+ pendingDeps.add(dep.name);
243
+ }
196
244
  }
197
245
  }
198
246
 
package/src/api.d.ts CHANGED
@@ -10,18 +10,32 @@ export type ThemeBase = 'neutral' | 'zinc' | 'slate';
10
10
  export type ThemeRadius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
11
11
  export type ThemeMode = 'light-dark' | 'light' | 'dark';
12
12
 
13
+ /** 현재 실제로 동작하는 CSS 프레임워크.
14
+ * - plain: 모든 컴포넌트가 plain 변종 보유.
15
+ * - tailwind: 일부 컴포넌트가 utility-class 변종 보유 — 미지원 컴포넌트는 add 시 plain 으로 자동 fallback. */
16
+ export type CssFrameworkSupported = 'plain' | 'tailwind';
17
+ /** 향후 추가 예정 — UI 에서 "곧 지원" 으로 노출되지만 CLI 는 거부. */
18
+ export type CssFrameworkPlanned = 'css-modules' | 'vanilla-extract';
19
+ /** 알려진 전체 (validation 메시지용). */
20
+ export type CssFramework = CssFrameworkSupported | CssFrameworkPlanned;
21
+
13
22
  export const CREATE_PLATFORMS: readonly CreatePlatform[];
14
23
  export const CREATE_STRUCTURES: readonly CreateStructure[];
15
24
  export const INIT_PLATFORMS: readonly InitPlatform[];
16
25
  export const THEME_BASES: readonly ThemeBase[];
17
26
  export const THEME_RADII: readonly ThemeRadius[];
18
27
  export const THEME_MODES: readonly ThemeMode[];
28
+ export const CSS_FRAMEWORKS_SUPPORTED: readonly CssFrameworkSupported[];
29
+ export const CSS_FRAMEWORKS_PLANNED: readonly CssFrameworkPlanned[];
30
+ export const CSS_FRAMEWORKS_ALL: readonly CssFramework[];
31
+ export const CSS_FRAMEWORK_DEFAULT: CssFrameworkSupported;
19
32
 
20
33
  export const INIT_DEFAULTS: {
21
34
  platform: InitPlatform;
22
35
  base: ThemeBase;
23
36
  radius: ThemeRadius;
24
37
  mode: ThemeMode;
38
+ cssFramework: CssFrameworkSupported;
25
39
  };
26
40
 
27
41
  export type PluginManifest = {
package/src/api.js CHANGED
@@ -16,6 +16,10 @@ export {
16
16
  THEME_RADII,
17
17
  THEME_MODES,
18
18
  INIT_DEFAULTS,
19
+ CSS_FRAMEWORKS_SUPPORTED,
20
+ CSS_FRAMEWORKS_PLANNED,
21
+ CSS_FRAMEWORKS_ALL,
22
+ CSS_FRAMEWORK_DEFAULT,
19
23
  } from './constants.js';
20
24
 
21
25
  export { allPlugins } from './create/plugins/index.js';
package/src/constants.js CHANGED
@@ -21,6 +21,24 @@ export const THEME_RADII = ['none', 'sm', 'md', 'lg', 'xl', 'full'];
21
21
 
22
22
  export const THEME_MODES = ['light-dark', 'light', 'dark'];
23
23
 
24
+ // ─── CSS 프레임워크 (변종 시스템 — 1단계: 그릇만) ───
25
+
26
+ // 현재 실제로 동작하는 값.
27
+ // - plain: CSS custom properties + 일반 .css 파일 (모든 컴포넌트 변종 보유)
28
+ // - tailwind: utility class TSX 변종 (button/card/input 부터 시작, 점진적 확대 — 변종이 없는 컴포넌트는 add 시 plain 으로 자동 fallback)
29
+ export const CSS_FRAMEWORKS_SUPPORTED = ['plain', 'tailwind'];
30
+
31
+ // 향후 추가 예정. 사용자가 이 값을 주면 친절 에러로 안내.
32
+ export const CSS_FRAMEWORKS_PLANNED = ['css-modules', 'vanilla-extract'];
33
+
34
+ // 알려진 전체 — 검증 시 supported 와 planned 둘 다 인지하기 위함.
35
+ export const CSS_FRAMEWORKS_ALL = [
36
+ ...CSS_FRAMEWORKS_SUPPORTED,
37
+ ...CSS_FRAMEWORKS_PLANNED,
38
+ ];
39
+
40
+ export const CSS_FRAMEWORK_DEFAULT = 'plain';
41
+
24
42
  // ─── 기본값 ───
25
43
 
26
44
  export const INIT_DEFAULTS = {
@@ -28,4 +46,5 @@ export const INIT_DEFAULTS = {
28
46
  base: 'neutral',
29
47
  radius: 'md',
30
48
  mode: 'light-dark',
49
+ cssFramework: CSS_FRAMEWORK_DEFAULT,
31
50
  };
@@ -1,11 +1,16 @@
1
- import { CREATE_PLATFORMS, CREATE_STRUCTURES } from '../constants.js';
1
+ import {
2
+ CREATE_PLATFORMS,
3
+ CREATE_STRUCTURES,
4
+ CSS_FRAMEWORKS_SUPPORTED,
5
+ CSS_FRAMEWORKS_PLANNED,
6
+ } from '../constants.js';
2
7
  import { allPlugins } from './plugins/index.js';
3
8
 
4
9
  const VALID_PLATFORMS = CREATE_PLATFORMS;
5
10
  const VALID_STRUCTURES = CREATE_STRUCTURES;
6
11
  const VALID_PLUGINS = allPlugins.map((p) => p.name);
7
12
 
8
- const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app'];
13
+ const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css'];
9
14
  const BOOL_FLAGS = ['yes', 'help', 'dry-run'];
10
15
 
11
16
  const SUBCOMMANDS = ['add-app', 'add-component'];
@@ -63,6 +68,17 @@ export const parseArgs = (argv) => {
63
68
  if (name === 'structure' && !VALID_STRUCTURES.includes(value)) {
64
69
  throw new Error(`--structure 는 ${VALID_STRUCTURES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
65
70
  }
71
+ if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
72
+ // planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
73
+ if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
74
+ throw new Error(
75
+ `--css='${value}' 는 곧 지원 예정. 현재는 ${CSS_FRAMEWORKS_SUPPORTED.join(', ')} 만 가능.`,
76
+ );
77
+ }
78
+ throw new Error(
79
+ `--css 는 ${CSS_FRAMEWORKS_SUPPORTED.join('/')} 중 하나여야 함 (받은 값: ${value})`,
80
+ );
81
+ }
66
82
  flags[name] = value;
67
83
  }
68
84
 
@@ -32,9 +32,28 @@ import {
32
32
  buildDartGradientsBlock,
33
33
  } from './theme/inject.js';
34
34
  import { getTemplatesRoot } from '../paths.mjs';
35
+ import {
36
+ CSS_FRAMEWORK_DEFAULT,
37
+ CSS_FRAMEWORKS_SUPPORTED,
38
+ CSS_FRAMEWORKS_PLANNED,
39
+ } from '../constants.js';
35
40
 
36
41
  const TEMPLATES_DIR = getTemplatesRoot();
37
42
 
43
+ /**
44
+ * 템플릿 복사 직후 sh-ui.config.json 의 cssFramework 필드를 갱신.
45
+ * 템플릿엔 이미 기본값이 박혀 있지만, 사용자가 --css 로 다른 값을 지정한
46
+ * 경우 그 값으로 덮어쓴다. 1단계는 plain 만 지원하므로 사실상 idempotent
47
+ * 이지만 2단계 emitter 가 추가되면 이 한 호출만으로 곧바로 동작.
48
+ */
49
+ async function patchShUiConfig(configPath, cssFramework) {
50
+ const fw = cssFramework ?? CSS_FRAMEWORK_DEFAULT;
51
+ if (!(await fs.pathExists(configPath))) return;
52
+ const config = await fs.readJson(configPath);
53
+ config.cssFramework = fw;
54
+ await fs.writeJson(configPath, config, { spaces: 2 });
55
+ }
56
+
38
57
  // ─── Create new project ───
39
58
 
40
59
  // 비대화형 환경(TTY 없음 — 에이전트, CI, 파이프) 에서는 prompt 가 멈추므로
@@ -71,6 +90,33 @@ export async function createProject(options = {}) {
71
90
  ],
72
91
  });
73
92
 
93
+ // CSS 프레임워크 — 현재는 plain 만 지원하지만, 곧 추가될 옵션을 disabled 로
94
+ // 미리 노출해 사용자가 변종 시스템의 존재를 인지할 수 있게 한다.
95
+ // Flutter 는 CSS 프레임워크 개념이 무의미하므로 자동 plain.
96
+ let cssFramework = options.css ?? CSS_FRAMEWORK_DEFAULT;
97
+ if (
98
+ options.css == null &&
99
+ platform !== 'flutter' &&
100
+ process.stdin.isTTY &&
101
+ !options.yes
102
+ ) {
103
+ cssFramework = await select({
104
+ message: 'CSS 프레임워크:',
105
+ default: CSS_FRAMEWORK_DEFAULT,
106
+ choices: [
107
+ ...CSS_FRAMEWORKS_SUPPORTED.map((fw) => ({
108
+ name: `${fw} — 플레인 CSS + custom properties`,
109
+ value: fw,
110
+ })),
111
+ ...CSS_FRAMEWORKS_PLANNED.map((fw) => ({
112
+ name: fw,
113
+ value: fw,
114
+ disabled: '곧 지원',
115
+ })),
116
+ ],
117
+ });
118
+ }
119
+
74
120
  let theme = null;
75
121
  if (options.theme) {
76
122
  theme = resolveTheme(options.theme);
@@ -114,7 +160,7 @@ export async function createProject(options = {}) {
114
160
  }
115
161
 
116
162
  if (platform === 'flutter') {
117
- await generateFlutter(targetDir, projectName, theme);
163
+ await generateFlutter(targetDir, projectName, theme, cssFramework);
118
164
  await finalizeProject(targetDir, { dryRun: options.dryRun });
119
165
  console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
120
166
  console.log(`\n cd ${projectName}`);
@@ -140,9 +186,9 @@ export async function createProject(options = {}) {
140
186
  plugins.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
141
187
 
142
188
  if (projectType === 'standalone') {
143
- await generateStandalone(targetDir, projectName, plugins, theme);
189
+ await generateStandalone(targetDir, projectName, plugins, theme, cssFramework);
144
190
  } else {
145
- await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme });
191
+ await generateMonorepo(targetDir, projectName, plugins, { yes: options.yes, theme, css: cssFramework });
146
192
  }
147
193
 
148
194
  await finalizeProject(targetDir, { dryRun: options.dryRun });
@@ -302,13 +348,14 @@ export async function addComponent(componentName, appName) {
302
348
 
303
349
  // ─── Generators ───
304
350
 
305
- async function generateFlutter(targetDir, projectName, theme) {
351
+ async function generateFlutter(targetDir, projectName, theme, css) {
306
352
  await fs.copy(path.join(TEMPLATES_DIR, 'flutter-standalone'), targetDir);
307
353
  await replaceInAllFiles(targetDir, '{{project_name}}', projectName);
308
354
  await injectDartTheme(targetDir, theme);
355
+ await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
309
356
  }
310
357
 
311
- async function generateStandalone(targetDir, projectName, plugins, theme) {
358
+ async function generateStandalone(targetDir, projectName, plugins, theme, css) {
312
359
  await fs.copy(path.join(TEMPLATES_DIR, 'nextjs-standalone'), targetDir);
313
360
 
314
361
  // Update package.json
@@ -331,9 +378,10 @@ async function generateStandalone(targetDir, projectName, plugins, theme) {
331
378
  await composeProviders(targetDir, plugins);
332
379
  await applyTransforms(targetDir, plugins);
333
380
  await injectCssTheme(targetDir, theme);
381
+ await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css);
334
382
  }
335
383
 
336
- async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme } = {}) {
384
+ async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css } = {}) {
337
385
  await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
338
386
 
339
387
  // Update root package.json
@@ -367,6 +415,7 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
367
415
  await generateApp(appsDir, appName, port, plugins);
368
416
  const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
369
417
  await injectCssTheme(uiAppDir, theme);
418
+ await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css);
370
419
  }
371
420
 
372
421
  async function generateApp(targetDir, appName, port, plugins) {
@@ -3,7 +3,7 @@
3
3
  import { parseArgs } from './cli-args.js';
4
4
  import { createProject, addApp, addComponent } from './generator.js';
5
5
  import { allPlugins } from './plugins/index.js';
6
- import { CREATE_PLATFORMS, CREATE_STRUCTURES } from '../constants.js';
6
+ import { CREATE_PLATFORMS, CREATE_STRUCTURES, CSS_FRAMEWORKS_SUPPORTED } from '../constants.js';
7
7
  import { THEME_PRESET_NAMES } from './theme/presets.js';
8
8
 
9
9
  const PLUGIN_NAMES = allPlugins.map((p) => p.name);
@@ -22,6 +22,7 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
22
22
  --structure <${CREATE_STRUCTURES.join('|')}> Next.js 프로젝트 구조 (next 일 때)
23
23
  --plugins <a,b> 플러그인 (${PLUGINS_LIST}). 미지정/"" → 없음
24
24
  --theme <preset|base64> 프리셋 이름(${THEME_PRESETS_LIST}) 또는 playground base64. 선택
25
+ --css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크 (현재 plain만 지원, 향후 tailwind 등 추가 예정)
25
26
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
26
27
  --dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
27
28
  -h, --help 이 도움말
@@ -70,6 +71,7 @@ export async function runCreate(rest) {
70
71
  structure: flags.structure,
71
72
  plugins: flags.plugins,
72
73
  theme: flags.theme,
74
+ css: flags.css,
73
75
  yes: flags.yes,
74
76
  dryRun: flags.dryRun,
75
77
  });
package/src/init.mjs CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  THEME_RADII,
10
10
  THEME_MODES,
11
11
  INIT_DEFAULTS,
12
+ CSS_FRAMEWORKS_SUPPORTED,
13
+ CSS_FRAMEWORKS_PLANNED,
12
14
  } from "./constants.js";
13
15
 
14
16
  const CHOICES = {
@@ -16,6 +18,7 @@ const CHOICES = {
16
18
  base: THEME_BASES,
17
19
  radius: THEME_RADII,
18
20
  mode: THEME_MODES,
21
+ cssFramework: CSS_FRAMEWORKS_SUPPORTED,
19
22
  };
20
23
 
21
24
  const DEFAULTS = INIT_DEFAULTS;
@@ -80,11 +83,20 @@ async function prompt(rl, label, choices, def) {
80
83
  }
81
84
 
82
85
  function validateOrThrow(key, value) {
83
- if (!CHOICES[key].includes(value)) {
86
+ if (CHOICES[key].includes(value)) return;
87
+
88
+ // cssFramework 는 향후 지원 예정 값에 대해 별도 안내. 사용자가 "tailwind"
89
+ // 같은 값을 미리 시도해 보고 싶을 때, 단순한 unknown 메시지보다 "곧 옵니다"
90
+ // 신호가 의도를 더 잘 전달함.
91
+ if (key === "cssFramework" && CSS_FRAMEWORKS_PLANNED.includes(value)) {
84
92
  throw new Error(
85
- `--${key}에 '${value}'는 허용되지 않습니다. 허용: ${CHOICES[key].join(", ")}`,
93
+ `--cssFramework='${value}'는 지원 예정입니다. 현재는 ${CHOICES[key].join(", ")} 만 가능합니다.`,
86
94
  );
87
95
  }
96
+
97
+ throw new Error(
98
+ `--${key}에 '${value}'는 허용되지 않습니다. 허용: ${CHOICES[key].join(", ")}`,
99
+ );
88
100
  }
89
101
 
90
102
  /** 플래그/TTY 상태에 따라 4개 축 값을 결정. 필요한 경우에만 프롬프트. */
@@ -97,7 +109,11 @@ async function resolveAnswers(flags) {
97
109
  }
98
110
  }
99
111
 
100
- const missingKeys = Object.keys(CHOICES).filter((k) => flags[k] == null);
112
+ // 선택지가 1개뿐인 축은 프롬프트 스킵 기본값으로 자동 채움.
113
+ // (예: cssFramework 가 plain 만일 때 의미 없는 "[plain] (plain):" 질문 회피)
114
+ const missingKeys = Object.keys(CHOICES).filter(
115
+ (k) => flags[k] == null && CHOICES[k].length > 1,
116
+ );
101
117
  if (flags.yes || missingKeys.length === 0) return answers;
102
118
 
103
119
  if (!stdin.isTTY) {
@@ -125,14 +141,15 @@ function labelFor(key) {
125
141
  base: "기본 색 스케일",
126
142
  radius: "radius",
127
143
  mode: "모드",
144
+ cssFramework: "CSS 프레임워크",
128
145
  }[key];
129
146
  }
130
147
 
131
- function buildConfig({ platform, base, radius, mode }) {
148
+ function buildConfig({ platform, base, radius, mode, cssFramework }) {
132
149
  return {
133
150
  $schema: "https://your-ds.dev/sh-ui.schema.json",
134
151
  platform,
135
- style: "default",
152
+ cssFramework,
136
153
  theme: { base, radius, mode },
137
154
  paths: PATHS[platform],
138
155
  ...(ALIASES[platform] ? { aliases: ALIASES[platform] } : {}),
@@ -153,7 +170,8 @@ export async function init({ cwd, args }) {
153
170
  await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
154
171
 
155
172
  console.log(`\n✓ sh-ui.config.json 생성 완료`);
156
- console.log(` platform: ${answers.platform}`);
157
- console.log(` theme: base=${answers.base}, radius=${answers.radius}, mode=${answers.mode}`);
173
+ console.log(` platform: ${answers.platform}`);
174
+ console.log(` cssFramework: ${answers.cssFramework}`);
175
+ console.log(` theme: base=${answers.base}, radius=${answers.radius}, mode=${answers.mode}`);
158
176
  console.log(`\n다음 단계: sh-ui add tokens button`);
159
177
  }
package/src/mcp.mjs CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  THEME_BASES,
37
37
  THEME_RADII,
38
38
  THEME_MODES,
39
+ CSS_FRAMEWORKS_SUPPORTED,
39
40
  } from "./constants.js";
40
41
  import { allPlugins } from "./create/plugins/index.js";
41
42
  import { THEME_PRESET_NAMES } from "./create/theme/presets.js";
@@ -44,6 +45,7 @@ const PLATFORMS = INIT_PLATFORMS;
44
45
  const BASES = THEME_BASES;
45
46
  const RADII = THEME_RADII;
46
47
  const MODES = THEME_MODES;
48
+ const CSS_FRAMEWORKS = CSS_FRAMEWORKS_SUPPORTED;
47
49
  const PLUGIN_NAMES = allPlugins.map((p) => p.name);
48
50
  const THEME_PRESETS_LIST = THEME_PRESET_NAMES.join(", ");
49
51
 
@@ -70,6 +72,10 @@ const INIT_DESCRIPTIONS = {
70
72
  light: "라이트 전용",
71
73
  dark: "다크 전용",
72
74
  },
75
+ cssFramework: {
76
+ plain: "플레인 CSS — CSS custom properties + 일반 .css 파일 (모든 컴포넌트 지원)",
77
+ tailwind: "Tailwind v4 utility class — class-variance-authority 기반. 변종 미제공 컴포넌트는 plain 으로 자동 fallback",
78
+ },
73
79
  };
74
80
 
75
81
  /** stdout 으로 출력되는 console.* 호출을 버퍼에 캡처해 텍스트로 반환. */
@@ -146,7 +152,7 @@ const SERVER_INSTRUCTIONS = `sh-ui — Base UI 위에 빌드된 React/Flutter
146
152
 
147
153
  export async function startMcpServer() {
148
154
  const server = new McpServer(
149
- { name: "sh-ui", version: "0.23.1" }, // sh-ui-cli 와 동기화
155
+ { name: "sh-ui", version: "0.43.0" }, // sh-ui-cli 와 동기화
150
156
  {
151
157
  capabilities: { tools: {} },
152
158
  instructions: SERVER_INSTRUCTIONS,
@@ -181,6 +187,8 @@ export async function startMcpServer() {
181
187
  .describe(`Next.js 플러그인 (${PLUGIN_NAMES.join(', ')}). 미지정시 빈 배열`),
182
188
  theme: z.string().optional()
183
189
  .describe(`프리셋 이름 (${THEME_PRESETS_LIST}) 또는 playground 에서 생성한 base64 (선택)`),
190
+ cssFramework: z.enum(CSS_FRAMEWORKS).optional()
191
+ .describe(`CSS 프레임워크. 기본 plain. 현재 ${CSS_FRAMEWORKS.join('/')} 지원 (향후 tailwind 등 추가 예정)`),
184
192
  cwd: z.string().optional()
185
193
  .describe("부모 디렉토리. 기본 process.cwd()"),
186
194
  force: z.boolean().optional()
@@ -222,6 +230,7 @@ export async function startMcpServer() {
222
230
  structure: input.structure,
223
231
  plugins: input.plugins,
224
232
  theme: input.theme,
233
+ css: input.cssFramework,
225
234
  yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
226
235
  }),
227
236
  );
@@ -249,6 +258,8 @@ export async function startMcpServer() {
249
258
  .describe("기본 radius. 기본 md"),
250
259
  mode: z.enum(MODES).optional()
251
260
  .describe("색 모드. 기본 light-dark"),
261
+ cssFramework: z.enum(CSS_FRAMEWORKS).optional()
262
+ .describe(`CSS 프레임워크. 기본 plain. 현재 ${CSS_FRAMEWORKS.join('/')} 지원 (향후 tailwind 등 추가 예정)`),
252
263
  cwd: z.string().optional()
253
264
  .describe("작업 디렉토리. 기본 process.cwd()"),
254
265
  force: z.boolean().optional()
@@ -257,7 +268,7 @@ export async function startMcpServer() {
257
268
  },
258
269
  async (input) => {
259
270
  const args = ["--yes"];
260
- for (const k of ["platform", "base", "radius", "mode"]) {
271
+ for (const k of ["platform", "base", "radius", "mode", "cssFramework"]) {
261
272
  if (input[k]) args.push(`--${k}`, input[k]);
262
273
  }
263
274
  if (input.force) args.push("--force");