sh-ui-cli 0.85.1 → 0.86.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 +13 -0
- package/package.json +1 -1
- package/src/constants.js +1 -1
- package/src/create/architectures/archSchema.js +1 -1
- package/src/create/architectures/flat.js +1 -1
- package/src/create/architectures/fsd.js +1 -1
- package/src/create/describeTemplate.js +21 -2
- package/src/create/generator.js +86 -3
- package/src/create/templateManifest.js +49 -0
- package/src/mcp.mjs +4 -4
- package/templates/vite-standalone/CLAUDE.md +7 -0
- package/templates/vite-standalone/README.md +23 -0
- package/templates/vite-standalone/_arch/flat/sh-ui.config.json +22 -0
- package/templates/vite-standalone/_arch/flat/src/App.tsx +13 -0
- package/templates/vite-standalone/_arch/flat/src/components/layouts/RootLayout.tsx +5 -0
- package/templates/vite-standalone/_arch/flat/src/components/providers/GlobalProvider/index.tsx +13 -0
- package/templates/vite-standalone/_arch/flat/src/components/providers/index.tsx +1 -0
- package/templates/vite-standalone/_arch/flat/src/components/providers/theme/ThemeProvider.tsx +59 -0
- package/templates/vite-standalone/_arch/flat/src/lib/api/queryClient.ts +12 -0
- package/templates/vite-standalone/_arch/flat/src/lib/hooks/useTheme.ts +8 -0
- package/templates/vite-standalone/_arch/flat/src/lib/styles/globals.css +35 -0
- package/templates/vite-standalone/_arch/flat/src/lib/styles/tokens.css +203 -0
- package/templates/vite-standalone/_arch/flat/src/lib/utils/utils.ts +6 -0
- package/templates/vite-standalone/_arch/flat/src/main.tsx +10 -0
- package/templates/vite-standalone/_arch/flat/tsconfig.app.json +23 -0
- package/templates/vite-standalone/_arch/fsd/sh-ui.config.json +22 -0
- package/templates/vite-standalone/_arch/fsd/src/App.tsx +13 -0
- package/templates/vite-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +5 -0
- package/templates/vite-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +13 -0
- package/templates/vite-standalone/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +59 -0
- package/templates/vite-standalone/_arch/fsd/src/main.tsx +10 -0
- package/templates/vite-standalone/_arch/fsd/src/shared/api/queryClient.ts +12 -0
- package/templates/vite-standalone/_arch/fsd/src/shared/hooks/useTheme.ts +8 -0
- package/templates/vite-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
- package/templates/vite-standalone/_arch/fsd/src/shared/styles/globals.css +35 -0
- package/templates/vite-standalone/_arch/fsd/src/shared/styles/tokens.css +203 -0
- package/templates/vite-standalone/_arch/fsd/tsconfig.app.json +22 -0
- package/templates/vite-standalone/eslint.config.js +34 -0
- package/templates/vite-standalone/gitignore +8 -0
- package/templates/vite-standalone/index.html +22 -0
- package/templates/vite-standalone/package.json +63 -0
- package/templates/vite-standalone/src/App.tsx +5 -0
- package/templates/vite-standalone/src/Home.tsx +7 -0
- package/templates/vite-standalone/src/main.tsx +9 -0
- package/templates/vite-standalone/tsconfig.json +7 -0
- package/templates/vite-standalone/tsconfig.node.json +12 -0
- package/templates/vite-standalone/vite.config.ts +11 -0
- package/templates/vite-standalone/vitest.config.ts +13 -0
- package/templates/vite-standalone/vitest.setup.ts +1 -0
|
@@ -2,6 +2,19 @@
|
|
|
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.86.0",
|
|
7
|
+
"date": "2026-05-14",
|
|
8
|
+
"title": "Vite 플랫폼 프리셋 — sh_ui_create_project platform=vite (standalone)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`sh-ui-cli create --platform vite --structure standalone`** — Vite 5 + React 19 + Tailwind v4 SPA 스캐폴드 신설. ai-org 같은 Tauri 셸 + SPA 조합처럼 Next.js SSR/App Router 가 무용지물이고 HMR 속도·번들 크기에서 vite 가 유리한 워크로드를 위한 1급 진입점. `@tailwindcss/vite` 공식 플러그인 경유 (next 프리셋의 postcss 경로와 다름). flat·fsd arch 모두 호환 (`mes` 는 App Router 의존이라 제외). MCP `sh_ui_create_project` / `sh_ui_describe_template` 도 platform=vite 인식.",
|
|
12
|
+
"**`next-themes` 없이 self-rolled `useTheme` 훅 + `ThemeProvider`** — vite SPA 환경에 맞춰 context + `localStorage` + `document.documentElement.classList` 토글 + `prefers-color-scheme` 변화 구독을 ~70 줄로 직접 구현. `index.html` 의 inline FOUC 스크립트가 첫 paint 전에 dark class 박아 깜빡임 차단. registry 에는 theme 두지 않는 v0.63+ 정책 그대로.",
|
|
13
|
+
"**`vite-tsconfig-paths` 플러그인 포함** — tsconfig 의 `@/*` (fsd) / `@/lib/*` + `@/components/*` (flat) 별칭을 vite/rollup 빌드 타임에 그대로 해석. tsconfig.app.json 이 별칭 단일 소스, `vite.config.ts` 는 plugin 한 줄만 추가. tsconfig 별칭과 빌드 별칭이 어긋날 일 없음.",
|
|
14
|
+
"**monorepo + Tauri 통합은 후속**. v0.86.0 은 vite-standalone 한정. vite-monorepo (apps/{name} + packages/ui/ui-core 공유) 는 v0.87 에서 generateMonorepo platform 분기로 추가 예정. Tauri scaffold 자동화는 별도 PR 후보 (현재도 `tauri init` 으로 vite 앱을 wrap 가능 — `dist/` 표준 위치 그대로)."
|
|
15
|
+
],
|
|
16
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.86.0"
|
|
17
|
+
},
|
|
5
18
|
{
|
|
6
19
|
"version": "0.85.1",
|
|
7
20
|
"date": "2026-05-13",
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -40,7 +40,7 @@ export const ArchSchema = z.object({
|
|
|
40
40
|
// 이 arch 가 적용 가능한 플랫폼들. CLI 에서 platform/arch 조합이 호환되는지 검증.
|
|
41
41
|
// 예: fsd/flat 은 ['next'], 미래에 추가될 flutter-clean 은 ['flutter'].
|
|
42
42
|
// 같은 이름의 arch 가 두 플랫폼 모두 지원하는 케이스도 가능 (예: 'flat' 가 양쪽).
|
|
43
|
-
platforms: z.array(z.enum(['next', 'flutter'])).min(1),
|
|
43
|
+
platforms: z.array(z.enum(['next', 'flutter', 'vite'])).min(1),
|
|
44
44
|
|
|
45
45
|
// 논리 키 → 파일시스템 경로 (앱 루트 기준 상대)
|
|
46
46
|
paths: PathKeys,
|
|
@@ -12,7 +12,7 @@ export const fsdArch = {
|
|
|
12
12
|
label: 'Feature-Sliced Design',
|
|
13
13
|
description:
|
|
14
14
|
'슬라이스 기반 폴더 구조 (entities/features/widgets/views/shared/app). 도메인이 큰 프로젝트에 적합.',
|
|
15
|
-
platforms: ['next'],
|
|
15
|
+
platforms: ['next', 'vite'],
|
|
16
16
|
|
|
17
17
|
paths: {
|
|
18
18
|
layouts: 'src/app/layouts',
|
|
@@ -28,8 +28,8 @@ import { CSS_FRAMEWORK_DEFAULT } from '../constants.js';
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @typedef {Object} DescribeOptions
|
|
31
|
-
* @property {'next'|'flutter'} [platform]
|
|
32
|
-
* @property {'standalone'|'monorepo'} [structure] next 일 때만 의미
|
|
31
|
+
* @property {'next'|'flutter'|'vite'} [platform]
|
|
32
|
+
* @property {'standalone'|'monorepo'} [structure] next/vite 일 때만 의미
|
|
33
33
|
* @property {string} [arch] next 일 때 'fsd'|'flat'|'mes'
|
|
34
34
|
* @property {string[]} [plugins] ['sentry', 'next-intl', 'auth-jwt']
|
|
35
35
|
* @property {'tailwind'|'plain'|'css-modules'} [cssFramework]
|
|
@@ -71,6 +71,25 @@ export function describeTemplate(opts = {}) {
|
|
|
71
71
|
]);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
if (platform === 'vite') {
|
|
75
|
+
// standalone only in Phase 1. monorepo is added in Task 13.
|
|
76
|
+
const tpl = TEMPLATE_MANIFEST['vite-standalone'];
|
|
77
|
+
if (!tpl) {
|
|
78
|
+
throw new Error("Template manifest missing entry for 'vite-standalone'.");
|
|
79
|
+
}
|
|
80
|
+
const safeArchName = isKnownArch(archName) ? archName : DEFAULT_ARCH;
|
|
81
|
+
const archObj = getArchByName(safeArchName);
|
|
82
|
+
if (!archObj.platforms.includes('vite')) {
|
|
83
|
+
throw new Error(`Arch '${safeArchName}' is not compatible with vite.`);
|
|
84
|
+
}
|
|
85
|
+
const baseFiles = tpl.base.slice();
|
|
86
|
+
const archFiles = (tpl.arches?.[safeArchName] ?? []).slice();
|
|
87
|
+
return finalize([
|
|
88
|
+
makeGroup('base', '베이스 (vite-standalone)', baseFiles),
|
|
89
|
+
makeGroup('arch', `Arch (${safeArchName})`, archFiles),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
// platform === 'next'
|
|
75
94
|
const safeArchName = isKnownArch(archName) ? archName : DEFAULT_ARCH;
|
|
76
95
|
const archObj = getArchByName(safeArchName);
|
package/src/create/generator.js
CHANGED
|
@@ -191,7 +191,7 @@ export async function createProject(options = {}) {
|
|
|
191
191
|
if (!process.stdin.isTTY) {
|
|
192
192
|
assertNoTtyFlag(options.name, '<project-name> (positional)');
|
|
193
193
|
assertNoTtyFlag(options.platform, '--platform');
|
|
194
|
-
if (options.platform === 'next') {
|
|
194
|
+
if (options.platform === 'next' || options.platform === 'vite') {
|
|
195
195
|
assertNoTtyFlag(options.structure, '--structure');
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -208,6 +208,7 @@ export async function createProject(options = {}) {
|
|
|
208
208
|
message: '플랫폼:',
|
|
209
209
|
choices: [
|
|
210
210
|
{ name: 'Next.js', value: 'next' },
|
|
211
|
+
{ name: 'Vite (SPA)', value: 'vite' },
|
|
211
212
|
{ name: 'Flutter', value: 'flutter' },
|
|
212
213
|
],
|
|
213
214
|
});
|
|
@@ -221,6 +222,9 @@ export async function createProject(options = {}) {
|
|
|
221
222
|
if (platform === 'next') {
|
|
222
223
|
const archName = options.arch ?? DEFAULT_ARCH;
|
|
223
224
|
arch = assertArchPlatformCompat(archName, 'next');
|
|
225
|
+
} else if (platform === 'vite') {
|
|
226
|
+
const archName = options.arch ?? DEFAULT_ARCH; // 'fsd' default — flat 도 호환
|
|
227
|
+
arch = assertArchPlatformCompat(archName, 'vite');
|
|
224
228
|
} else if (platform === 'flutter' && options.arch) {
|
|
225
229
|
arch = assertArchPlatformCompat(options.arch, 'flutter');
|
|
226
230
|
}
|
|
@@ -317,6 +321,47 @@ export async function createProject(options = {}) {
|
|
|
317
321
|
return;
|
|
318
322
|
}
|
|
319
323
|
|
|
324
|
+
if (platform === 'vite') {
|
|
325
|
+
// vite 는 next 와 동일하게 structure 옵션을 받지만 Phase 1 에서는 standalone 만 처리.
|
|
326
|
+
// monorepo 는 후속 task 에서 generateMonorepo 의 platform 분기로 연결.
|
|
327
|
+
const projectType = options.structure ?? await select({
|
|
328
|
+
message: '프로젝트 구조:',
|
|
329
|
+
choices: [
|
|
330
|
+
{ name: '단독 (Vite standalone)', value: 'standalone' },
|
|
331
|
+
{ name: '모노레포 (Turborepo + pnpm)', value: 'monorepo' },
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (projectType === 'standalone') {
|
|
336
|
+
await generateViteStandalone(targetDir, projectName, theme, cssFramework, arch, themeBase);
|
|
337
|
+
} else {
|
|
338
|
+
// monorepo path is added in Task 12. For now, fail loudly so users know.
|
|
339
|
+
throw new Error(
|
|
340
|
+
'platform=vite + structure=monorepo 는 아직 구현되지 않았습니다 (Phase 2 — v0.87 예정). ' +
|
|
341
|
+
'standalone 을 사용하거나 platform=next 로 monorepo 를 만든 뒤 vite 앱을 수동으로 추가해주세요.',
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await finalizeProject(targetDir, { dryRun: options.dryRun });
|
|
346
|
+
|
|
347
|
+
if (options.dryRun) {
|
|
348
|
+
const files = await listAllFiles(targetDir);
|
|
349
|
+
console.log(`\n[DRY RUN] ${projectName} 스캐폴드 시 작성될 파일 (${files.length}개):\n`);
|
|
350
|
+
for (const f of files.sort()) console.log(` ${f}`);
|
|
351
|
+
await fs.remove(targetDir);
|
|
352
|
+
console.log(`\n실제 스캐폴드: --dry-run 제거 후 같은 명령 실행.`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log(`\n✅ ${projectName} Vite 프로젝트가 생성되었습니다!`);
|
|
357
|
+
console.log(`\n cd ${projectName}`);
|
|
358
|
+
console.log(' pnpm install');
|
|
359
|
+
console.log(' pnpm dev\n');
|
|
360
|
+
console.log('다음 단계 — 베이스 컴포넌트 추가 (예시):');
|
|
361
|
+
console.log(' npx sh-ui-cli add button card input dialog\n');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
320
365
|
// platform === 'next' 경로
|
|
321
366
|
const projectType = options.structure ?? await select({
|
|
322
367
|
message: '프로젝트 구조:',
|
|
@@ -690,6 +735,43 @@ async function generateStandalone(targetDir, projectName, plugins, theme, css, a
|
|
|
690
735
|
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
691
736
|
}
|
|
692
737
|
|
|
738
|
+
async function generateViteStandalone(targetDir, projectName, theme, css, arch, themeBase) {
|
|
739
|
+
// 베이스 (arch-neutral) + arch 오버레이 — generateStandalone 과 같은 패턴.
|
|
740
|
+
await fs.copy(path.join(TEMPLATES_DIR, 'vite-standalone'), targetDir, {
|
|
741
|
+
filter: (src) => !src.includes(`${path.sep}_arch${path.sep}`) && !src.endsWith(`${path.sep}_arch`),
|
|
742
|
+
});
|
|
743
|
+
await ensureArchCleanup(targetDir);
|
|
744
|
+
await fs.copy(
|
|
745
|
+
path.join(TEMPLATES_DIR, 'vite-standalone', '_arch', arch.name),
|
|
746
|
+
targetDir,
|
|
747
|
+
{ overwrite: true },
|
|
748
|
+
);
|
|
749
|
+
// vite 는 모든 소스가 src/ 아래 — arch.paths.layouts(next 관용) 앞에 src/ 를 붙여
|
|
750
|
+
// sentinel 위치를 보정한다. flat 의 components/layouts → src/components/layouts,
|
|
751
|
+
// fsd 의 src/app/layouts → src/app/layouts (변화 없음 — fsd 가 이미 src/ 박혀 있음).
|
|
752
|
+
const layoutsPath = arch.paths.layouts.startsWith('src/')
|
|
753
|
+
? arch.paths.layouts
|
|
754
|
+
: `src/${arch.paths.layouts}`;
|
|
755
|
+
const sentinelPath = path.join(targetDir, `${layoutsPath}/RootLayout.tsx`);
|
|
756
|
+
if (!(await fs.pathExists(sentinelPath))) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`arch 오버레이 누락: vite-standalone + ${arch.name} 의 sentinel 파일(${layoutsPath}/RootLayout.tsx) 이 ${targetDir} 에 없습니다.`,
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// package.json — name + dep sort
|
|
763
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
764
|
+
const pkg = await fs.readJson(pkgPath);
|
|
765
|
+
pkg.name = projectName;
|
|
766
|
+
if (pkg.dependencies) pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
767
|
+
if (pkg.devDependencies) pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
768
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
769
|
+
|
|
770
|
+
await applyCssFrameworkVariant(targetDir, css, { isMonorepo: false, plugins: [], arch });
|
|
771
|
+
await injectCssTheme(targetDir, theme);
|
|
772
|
+
await patchShUiConfig(path.join(targetDir, 'sh-ui.config.json'), css, themeBase);
|
|
773
|
+
}
|
|
774
|
+
|
|
693
775
|
async function generateMonorepo(targetDir, projectName, plugins, { yes = false, theme, css, arch, themeBase } = {}) {
|
|
694
776
|
await fs.copy(path.join(TEMPLATES_DIR, 'monorepo'), targetDir);
|
|
695
777
|
|
|
@@ -1755,9 +1837,10 @@ const OPTIONAL_DART_INJECTORS = [
|
|
|
1755
1837
|
async function injectCssTheme(projectDir, theme) {
|
|
1756
1838
|
if (!theme) return;
|
|
1757
1839
|
const candidates = [
|
|
1758
|
-
'src/shared/styles/tokens.css', // FSD standalone
|
|
1840
|
+
'src/shared/styles/tokens.css', // FSD standalone (next + vite)
|
|
1759
1841
|
'src/styles/tokens.css', // monorepo ui-app-template (arch-neutral)
|
|
1760
|
-
'lib/styles/tokens.css',
|
|
1842
|
+
'src/lib/styles/tokens.css', // flat standalone (vite)
|
|
1843
|
+
'lib/styles/tokens.css', // flat standalone (next)
|
|
1761
1844
|
];
|
|
1762
1845
|
for (const rel of candidates) {
|
|
1763
1846
|
const abs = path.join(projectDir, rel);
|
|
@@ -324,5 +324,54 @@ export const TEMPLATE_MANIFEST = {
|
|
|
324
324
|
"src/styles/tokens.css",
|
|
325
325
|
"tsconfig.json"
|
|
326
326
|
]
|
|
327
|
+
},
|
|
328
|
+
"vite-standalone": {
|
|
329
|
+
"base": [
|
|
330
|
+
"CLAUDE.md",
|
|
331
|
+
"README.md",
|
|
332
|
+
"eslint.config.js",
|
|
333
|
+
"gitignore",
|
|
334
|
+
"index.html",
|
|
335
|
+
"package.json",
|
|
336
|
+
"src/App.tsx",
|
|
337
|
+
"src/Home.tsx",
|
|
338
|
+
"src/main.tsx",
|
|
339
|
+
"tsconfig.json",
|
|
340
|
+
"tsconfig.node.json",
|
|
341
|
+
"vite.config.ts",
|
|
342
|
+
"vitest.config.ts",
|
|
343
|
+
"vitest.setup.ts"
|
|
344
|
+
],
|
|
345
|
+
"arches": {
|
|
346
|
+
"flat": [
|
|
347
|
+
"sh-ui.config.json",
|
|
348
|
+
"src/App.tsx",
|
|
349
|
+
"src/components/layouts/RootLayout.tsx",
|
|
350
|
+
"src/components/providers/GlobalProvider/index.tsx",
|
|
351
|
+
"src/components/providers/index.tsx",
|
|
352
|
+
"src/components/providers/theme/ThemeProvider.tsx",
|
|
353
|
+
"src/lib/api/queryClient.ts",
|
|
354
|
+
"src/lib/hooks/useTheme.ts",
|
|
355
|
+
"src/lib/styles/globals.css",
|
|
356
|
+
"src/lib/styles/tokens.css",
|
|
357
|
+
"src/lib/utils/utils.ts",
|
|
358
|
+
"src/main.tsx",
|
|
359
|
+
"tsconfig.app.json"
|
|
360
|
+
],
|
|
361
|
+
"fsd": [
|
|
362
|
+
"sh-ui.config.json",
|
|
363
|
+
"src/App.tsx",
|
|
364
|
+
"src/app/layouts/RootLayout.tsx",
|
|
365
|
+
"src/app/providers/GlobalProvider/index.tsx",
|
|
366
|
+
"src/app/providers/theme/ThemeProvider.tsx",
|
|
367
|
+
"src/main.tsx",
|
|
368
|
+
"src/shared/api/queryClient.ts",
|
|
369
|
+
"src/shared/hooks/useTheme.ts",
|
|
370
|
+
"src/shared/lib/utils.ts",
|
|
371
|
+
"src/shared/styles/globals.css",
|
|
372
|
+
"src/shared/styles/tokens.css",
|
|
373
|
+
"tsconfig.app.json"
|
|
374
|
+
]
|
|
375
|
+
}
|
|
327
376
|
}
|
|
328
377
|
};
|
package/src/mcp.mjs
CHANGED
|
@@ -366,7 +366,7 @@ export async function startMcpServer() {
|
|
|
366
366
|
"sh_ui_create_project",
|
|
367
367
|
{
|
|
368
368
|
description:
|
|
369
|
-
"빈 폴더에 sh-ui 프로젝트 스캐폴드 — Next.js (standalone/monorepo)
|
|
369
|
+
"빈 폴더에 sh-ui 프로젝트 스캐폴드 — Next.js (standalone/monorepo) | Vite (standalone) | Flutter. " +
|
|
370
370
|
`FSD 폴더 구조 + 토큰 + sh-ui.config.json 일괄 생성. 사용자가 '새 프로젝트' / '빈 폴더' / '스캐폴드부터' 류 요청을 하면 이 툴 사용 (Bash 로 npx ${cliName} create 직접 호출보다 우선). ` +
|
|
371
371
|
"**단일 진입점** — theme/plugins/cssFramework/structure 모두 호출 시점에 정해서 한 번에 박는다. 호출 후 sh-ui.config.json/tokens.css 를 손으로 패치하지 말 것 (다음 재스캐폴드 시 유실). " +
|
|
372
372
|
"산출물: theme 인자가 프리셋이면 sh-ui.config.json 의 theme.base 가 그 이름, base64 면 'custom'. paths.styles · paths.tokens 도 자동 박혀서 sh_ui_add_component 가 사후 패치 없이 동작.",
|
|
@@ -397,13 +397,13 @@ export async function startMcpServer() {
|
|
|
397
397
|
},
|
|
398
398
|
},
|
|
399
399
|
async (input) => {
|
|
400
|
-
if (input.platform === "next" && !input.structure) {
|
|
400
|
+
if ((input.platform === "next" || input.platform === "vite") && !input.structure) {
|
|
401
401
|
return {
|
|
402
402
|
isError: true,
|
|
403
403
|
content: [
|
|
404
404
|
{
|
|
405
405
|
type: "text",
|
|
406
|
-
text:
|
|
406
|
+
text: `platform=${input.platform} 일 때 structure ('standalone' | 'monorepo') 가 필요합니다.`,
|
|
407
407
|
},
|
|
408
408
|
],
|
|
409
409
|
};
|
|
@@ -820,7 +820,7 @@ export async function startMcpServer() {
|
|
|
820
820
|
platform: z.enum(CREATE_PLATFORMS)
|
|
821
821
|
.describe("타겟 플랫폼"),
|
|
822
822
|
structure: z.enum(CREATE_STRUCTURES).optional()
|
|
823
|
-
.describe("Next.js 구조. platform=next 일 때 의미. 기본 standalone"),
|
|
823
|
+
.describe("Next.js 구조. platform=next | vite 일 때 의미. 기본 standalone"),
|
|
824
824
|
arch: z.enum(ARCH_NAMES).optional()
|
|
825
825
|
.describe("아키텍처. platform=next 일 때 의미. 기본 fsd"),
|
|
826
826
|
plugins: z.array(z.enum(PLUGIN_NAMES)).optional()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# my-app
|
|
2
|
+
|
|
3
|
+
Vite + React + sh-ui 스캐폴드.
|
|
4
|
+
|
|
5
|
+
## 개발
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 빌드
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm build
|
|
16
|
+
pnpm preview
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 컴포넌트 추가
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx sh-ui-cli add button card input dialog
|
|
23
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/sanghyeonKim0201/sh-ui/live/packages/cli/sh-ui.schema.json",
|
|
3
|
+
"platform": "react",
|
|
4
|
+
"cssFramework": "tailwind",
|
|
5
|
+
"theme": {
|
|
6
|
+
"base": "neutral",
|
|
7
|
+
"radius": "md",
|
|
8
|
+
"mode": "light-dark"
|
|
9
|
+
},
|
|
10
|
+
"paths": {
|
|
11
|
+
"tokens": "src/lib/styles/tokens.css",
|
|
12
|
+
"cssEntry": "src/lib/styles/globals.css",
|
|
13
|
+
"styles": "src/lib/styles",
|
|
14
|
+
"components": "src/components/common",
|
|
15
|
+
"utils": "src/lib/utils/utils.ts"
|
|
16
|
+
},
|
|
17
|
+
"aliases": {
|
|
18
|
+
"components": "@/components/common",
|
|
19
|
+
"utils": "@/lib/utils/utils",
|
|
20
|
+
"ui": "@/components/common"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RootLayout } from '@/components/layouts/RootLayout';
|
|
2
|
+
import { GlobalProvider } from '@/components/providers';
|
|
3
|
+
import Home from './Home';
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
return (
|
|
7
|
+
<GlobalProvider>
|
|
8
|
+
<RootLayout>
|
|
9
|
+
<Home />
|
|
10
|
+
</RootLayout>
|
|
11
|
+
</GlobalProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
package/templates/vite-standalone/_arch/flat/src/components/providers/GlobalProvider/index.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { type ReactNode, useState } from 'react';
|
|
3
|
+
import { createQueryClient } from '@/lib/api/queryClient';
|
|
4
|
+
import { ThemeProvider } from '../theme/ThemeProvider';
|
|
5
|
+
|
|
6
|
+
export function GlobalProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
const [queryClient] = useState(() => createQueryClient());
|
|
8
|
+
return (
|
|
9
|
+
<ThemeProvider>
|
|
10
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GlobalProvider } from './GlobalProvider';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createContext, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
4
|
+
|
|
5
|
+
type ThemeContextValue = {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
resolvedTheme: 'light' | 'dark';
|
|
8
|
+
setTheme: (theme: Theme) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
const STORAGE_KEY = 'theme';
|
|
14
|
+
|
|
15
|
+
function getSystemTheme(): 'light' | 'dark' {
|
|
16
|
+
if (typeof window === 'undefined') return 'light';
|
|
17
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveTheme(theme: Theme): 'light' | 'dark' {
|
|
21
|
+
return theme === 'system' ? getSystemTheme() : theme;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
25
|
+
const [theme, setThemeState] = useState<Theme>(() => {
|
|
26
|
+
if (typeof window === 'undefined') return 'system';
|
|
27
|
+
return (localStorage.getItem(STORAGE_KEY) as Theme) ?? 'system';
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => resolveTheme(theme));
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const resolved = resolveTheme(theme);
|
|
34
|
+
setResolvedTheme(resolved);
|
|
35
|
+
document.documentElement.classList.toggle('dark', resolved === 'dark');
|
|
36
|
+
}, [theme]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (theme !== 'system') return;
|
|
40
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
41
|
+
const handler = () => {
|
|
42
|
+
const resolved = getSystemTheme();
|
|
43
|
+
setResolvedTheme(resolved);
|
|
44
|
+
document.documentElement.classList.toggle('dark', resolved === 'dark');
|
|
45
|
+
};
|
|
46
|
+
mq.addEventListener('change', handler);
|
|
47
|
+
return () => mq.removeEventListener('change', handler);
|
|
48
|
+
}, [theme]);
|
|
49
|
+
|
|
50
|
+
const setTheme = useCallback((next: Theme) => {
|
|
51
|
+
setThemeState(next);
|
|
52
|
+
if (next === 'system') localStorage.removeItem(STORAGE_KEY);
|
|
53
|
+
else localStorage.setItem(STORAGE_KEY, next);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const value = useMemo(() => ({ theme, resolvedTheme, setTheme }), [theme, resolvedTheme, setTheme]);
|
|
57
|
+
|
|
58
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ThemeContext } from '@/components/providers/theme/ThemeProvider';
|
|
3
|
+
|
|
4
|
+
export function useTheme() {
|
|
5
|
+
const ctx = useContext(ThemeContext);
|
|
6
|
+
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider');
|
|
7
|
+
return ctx;
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* sh-ui:external-imports-start
|
|
2
|
+
* 외부 폰트 / 아이콘셋 / 디자인시스템 URL @import 는 반드시 이 블록 안에 둘 것. */
|
|
3
|
+
/* sh-ui:external-imports-end */
|
|
4
|
+
|
|
5
|
+
@import 'tailwindcss';
|
|
6
|
+
@import './tokens.css';
|
|
7
|
+
|
|
8
|
+
@custom-variant dark (&:is(.dark *));
|
|
9
|
+
|
|
10
|
+
@theme inline {
|
|
11
|
+
--color-background: var(--background);
|
|
12
|
+
--color-background-subtle: var(--background-subtle);
|
|
13
|
+
--color-background-muted: var(--background-muted);
|
|
14
|
+
--color-background-inverse: var(--background-inverse);
|
|
15
|
+
--color-foreground: var(--foreground);
|
|
16
|
+
--color-foreground-muted: var(--foreground-muted);
|
|
17
|
+
--color-foreground-subtle: var(--foreground-subtle);
|
|
18
|
+
--color-foreground-inverse: var(--foreground-inverse);
|
|
19
|
+
--color-border: var(--border);
|
|
20
|
+
--color-border-strong: var(--border-strong);
|
|
21
|
+
--color-primary: var(--primary);
|
|
22
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
23
|
+
--color-primary-hover: var(--primary-hover);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-danger: var(--danger);
|
|
26
|
+
--color-danger-hover: var(--danger-hover);
|
|
27
|
+
--color-danger-foreground: var(--danger-foreground);
|
|
28
|
+
--color-success: var(--success);
|
|
29
|
+
--color-success-foreground: var(--success-foreground);
|
|
30
|
+
--color-warning: var(--warning);
|
|
31
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
32
|
+
--color-info: var(--info);
|
|
33
|
+
--color-info-foreground: var(--info-foreground);
|
|
34
|
+
--color-sidebar-bg: var(--sidebar-bg);
|
|
35
|
+
}
|