sh-ui-cli 0.42.1 → 0.44.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/README.md +6 -1
- package/data/changelog/versions.json +25 -0
- package/data/registry/flutter/registry.json +1 -1
- package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
- package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
- package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
- package/data/registry/react/components/button/index.tailwind.tsx +70 -0
- package/data/registry/react/components/card/index.tailwind.tsx +111 -0
- package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
- package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
- package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
- package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
- package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
- package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
- package/data/registry/react/components/input/index.tailwind.tsx +405 -0
- package/data/registry/react/components/label/index.tailwind.tsx +78 -0
- package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
- package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
- package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
- package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
- package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
- package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
- package/data/registry/react/components/select/index.tailwind.tsx +199 -0
- package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
- package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
- package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
- package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
- package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
- package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
- package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
- package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
- package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
- package/data/registry/react/peer-versions.json +1 -0
- package/data/registry/react/registry.json +530 -72
- package/data/tokens/build.mjs +66 -0
- package/package.json +1 -1
- package/src/add.mjs +54 -6
- package/src/api.d.ts +14 -0
- package/src/api.js +4 -0
- package/src/constants.js +19 -0
- package/src/create/cli-args.js +18 -2
- package/src/create/generator.js +55 -6
- package/src/create/index.mjs +3 -1
- package/src/init.mjs +25 -7
- package/src/mcp.mjs +13 -2
- package/templates/flutter-standalone/sh-ui.config.json +1 -1
- package/templates/nextjs-standalone/app/globals.css +1 -21
- package/templates/nextjs-standalone/sh-ui.config.json +1 -1
- package/templates/ui-app-template/sh-ui.config.json +1 -1
- package/templates/ui-app-template/src/styles/globals.css +1 -21
package/data/tokens/build.mjs
CHANGED
|
@@ -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
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 {
|
|
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
|
-
|
|
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
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
|
};
|
package/src/create/cli-args.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
|
package/src/create/generator.js
CHANGED
|
@@ -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) {
|
package/src/create/index.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
157
|
-
console.log(`
|
|
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.
|
|
155
|
+
{ name: "sh-ui", version: "0.44.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");
|
|
@@ -3,27 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
@custom-variant dark (&:is(.dark *));
|
|
5
5
|
|
|
6
|
-
@theme inline
|
|
7
|
-
--color-background: var(--background);
|
|
8
|
-
--color-background-subtle: var(--background-subtle);
|
|
9
|
-
--color-background-muted: var(--background-muted);
|
|
10
|
-
--color-background-inverse: var(--background-inverse);
|
|
11
|
-
--color-foreground: var(--foreground);
|
|
12
|
-
--color-foreground-muted: var(--foreground-muted);
|
|
13
|
-
--color-foreground-subtle: var(--foreground-subtle);
|
|
14
|
-
--color-foreground-inverse: var(--foreground-inverse);
|
|
15
|
-
--color-border: var(--border);
|
|
16
|
-
--color-border-strong: var(--border-strong);
|
|
17
|
-
--color-primary: var(--primary);
|
|
18
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
19
|
-
--color-primary-hover: var(--primary-hover);
|
|
20
|
-
--color-danger: var(--danger);
|
|
21
|
-
--color-danger-foreground: var(--danger-foreground);
|
|
22
|
-
--radius-sm: calc(var(--radius) - 2px);
|
|
23
|
-
--radius-md: var(--radius);
|
|
24
|
-
--radius-lg: calc(var(--radius) + 2px);
|
|
25
|
-
--radius-xl: calc(var(--radius) + 4px);
|
|
26
|
-
}
|
|
6
|
+
/* Tailwind v4 @theme inline 블록은 tokens.css 가 자동으로 emit. */
|
|
27
7
|
|
|
28
8
|
@layer base {
|
|
29
9
|
body {
|