sh-ui-cli 0.85.0 → 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.
Files changed (53) hide show
  1. package/data/changelog/versions.json +25 -0
  2. package/package.json +1 -1
  3. package/src/constants.js +1 -1
  4. package/src/create/architectures/archSchema.js +1 -1
  5. package/src/create/architectures/flat.js +1 -1
  6. package/src/create/architectures/fsd.js +1 -1
  7. package/src/create/describeTemplate.js +21 -2
  8. package/src/create/generator.js +86 -3
  9. package/src/create/templateManifest.js +49 -0
  10. package/src/mcp.mjs +4 -4
  11. package/templates/nextjs-standalone/_arch/flat/app/globals.css +6 -0
  12. package/templates/nextjs-standalone/_arch/mes/app/globals.css +6 -0
  13. package/templates/nextjs-standalone/app/globals.css +6 -0
  14. package/templates/ui-app-template/src/styles/globals.css +7 -0
  15. package/templates/vite-standalone/CLAUDE.md +7 -0
  16. package/templates/vite-standalone/README.md +23 -0
  17. package/templates/vite-standalone/_arch/flat/sh-ui.config.json +22 -0
  18. package/templates/vite-standalone/_arch/flat/src/App.tsx +13 -0
  19. package/templates/vite-standalone/_arch/flat/src/components/layouts/RootLayout.tsx +5 -0
  20. package/templates/vite-standalone/_arch/flat/src/components/providers/GlobalProvider/index.tsx +13 -0
  21. package/templates/vite-standalone/_arch/flat/src/components/providers/index.tsx +1 -0
  22. package/templates/vite-standalone/_arch/flat/src/components/providers/theme/ThemeProvider.tsx +59 -0
  23. package/templates/vite-standalone/_arch/flat/src/lib/api/queryClient.ts +12 -0
  24. package/templates/vite-standalone/_arch/flat/src/lib/hooks/useTheme.ts +8 -0
  25. package/templates/vite-standalone/_arch/flat/src/lib/styles/globals.css +35 -0
  26. package/templates/vite-standalone/_arch/flat/src/lib/styles/tokens.css +203 -0
  27. package/templates/vite-standalone/_arch/flat/src/lib/utils/utils.ts +6 -0
  28. package/templates/vite-standalone/_arch/flat/src/main.tsx +10 -0
  29. package/templates/vite-standalone/_arch/flat/tsconfig.app.json +23 -0
  30. package/templates/vite-standalone/_arch/fsd/sh-ui.config.json +22 -0
  31. package/templates/vite-standalone/_arch/fsd/src/App.tsx +13 -0
  32. package/templates/vite-standalone/_arch/fsd/src/app/layouts/RootLayout.tsx +5 -0
  33. package/templates/vite-standalone/_arch/fsd/src/app/providers/GlobalProvider/index.tsx +13 -0
  34. package/templates/vite-standalone/_arch/fsd/src/app/providers/theme/ThemeProvider.tsx +59 -0
  35. package/templates/vite-standalone/_arch/fsd/src/main.tsx +10 -0
  36. package/templates/vite-standalone/_arch/fsd/src/shared/api/queryClient.ts +12 -0
  37. package/templates/vite-standalone/_arch/fsd/src/shared/hooks/useTheme.ts +8 -0
  38. package/templates/vite-standalone/_arch/fsd/src/shared/lib/utils.ts +6 -0
  39. package/templates/vite-standalone/_arch/fsd/src/shared/styles/globals.css +35 -0
  40. package/templates/vite-standalone/_arch/fsd/src/shared/styles/tokens.css +203 -0
  41. package/templates/vite-standalone/_arch/fsd/tsconfig.app.json +22 -0
  42. package/templates/vite-standalone/eslint.config.js +34 -0
  43. package/templates/vite-standalone/gitignore +8 -0
  44. package/templates/vite-standalone/index.html +22 -0
  45. package/templates/vite-standalone/package.json +63 -0
  46. package/templates/vite-standalone/src/App.tsx +5 -0
  47. package/templates/vite-standalone/src/Home.tsx +7 -0
  48. package/templates/vite-standalone/src/main.tsx +9 -0
  49. package/templates/vite-standalone/tsconfig.json +7 -0
  50. package/templates/vite-standalone/tsconfig.node.json +12 -0
  51. package/templates/vite-standalone/vite.config.ts +11 -0
  52. package/templates/vite-standalone/vitest.config.ts +13 -0
  53. package/templates/vite-standalone/vitest.setup.ts +1 -0
@@ -2,6 +2,31 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
4
4
  "versions": [
5
+ {
6
+ "version": "0.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
+ },
18
+ {
19
+ "version": "0.85.1",
20
+ "date": "2026-05-13",
21
+ "title": "scaffold globals.css 에 external-imports sentinel — 폰트/디자인시스템 URL @import footgun 차단",
22
+ "type": "patch",
23
+ "highlights": [
24
+ "**4개 globals.css 템플릿에 `sh-ui:external-imports-start/-end` sentinel 블록 추가** — `@import 'tailwindcss'` 위에 명시적으로 외부 URL `@import` 자리 표시. 기존엔 사용자가 `@import 'tailwindcss'` 바로 아래에 폰트 URL `@import` 추가하면 (자연 직관) CSS spec (\"모든 `@import` 는 다른 rule 보다 앞에 와야 함\") 위반으로 Turbopack/lightningcss 가 `@import rules must precede all rules aside from @charset and @layer statements` 로 dev server 즉사. 라인 번호가 번들 후 (2874:8 류) 라 디버깅 동선이 길어 첫 인상에서 30분 헤매던 footgun.",
25
+ "**적용 범위**: `ui-app-template/src/styles/globals.css` (monorepo) · `nextjs-standalone/app/globals.css` (standalone fsd 의 베이스) · `nextjs-standalone/_arch/flat/app/globals.css` · `nextjs-standalone/_arch/mes/app/globals.css`. sentinel 안에 `@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css');` 류 예시도 주석으로 포함 — 새 사용자가 어디에 둘지 한눈에 발견.",
26
+ "**기존 프로젝트 마이그레이션은 별도 codemod 로 후속** — 현재는 신규 스캐폴드에만 sentinel emit. 기존 globals.css 자동 수술은 `sh_ui_migrate` 명령에 추후 추가."
27
+ ],
28
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.85.1"
29
+ },
5
30
  {
6
31
  "version": "0.85.0",
7
32
  "date": "2026-05-13",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/constants.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  // ─── 프로젝트 생성 (sh-ui-cli create) ───
9
9
 
10
- export const CREATE_PLATFORMS = ['next', 'flutter'];
10
+ export const CREATE_PLATFORMS = ['next', 'flutter', 'vite'];
11
11
 
12
12
  export const CREATE_STRUCTURES = ['standalone', 'monorepo'];
13
13
 
@@ -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,
@@ -18,7 +18,7 @@ export const flatArch = {
18
18
  label: 'Flat',
19
19
  description:
20
20
  '슬라이스 없는 관용적 Next.js 구조 (lib/components/app). 작은 프로젝트, 데모, 일회성 도구에 적합.',
21
- platforms: ['next'],
21
+ platforms: ['next', 'vite'],
22
22
 
23
23
  paths: {
24
24
  layouts: 'components/layouts',
@@ -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);
@@ -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', // flat standalone
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) 또는 Flutter. " +
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: "platform=next 일 때 structure ('standalone' | 'monorepo') 가 필요합니다.",
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()
@@ -1,3 +1,9 @@
1
+ /* sh-ui:external-imports-start
2
+ * 외부 폰트 / 아이콘셋 / 디자인시스템 URL @import 는 반드시 이 블록 안에 둘 것.
3
+ * `@import 'tailwindcss'` 아래에 추가하면 CSS spec 위반으로 Turbopack 이 즉사.
4
+ * 예: @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css'); */
5
+ /* sh-ui:external-imports-end */
6
+
1
7
  @import 'tailwindcss';
2
8
  @import '../lib/styles/tokens.css';
3
9
 
@@ -1,3 +1,9 @@
1
+ /* sh-ui:external-imports-start
2
+ * 외부 폰트 / 아이콘셋 / 디자인시스템 URL @import 는 반드시 이 블록 안에 둘 것.
3
+ * `@import 'tailwindcss'` 아래에 추가하면 CSS spec 위반으로 Turbopack 이 즉사.
4
+ * 예: @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css'); */
5
+ /* sh-ui:external-imports-end */
6
+
1
7
  @import 'tailwindcss';
2
8
  @import '../src/lib/styles/tokens.css';
3
9
 
@@ -1,3 +1,9 @@
1
+ /* sh-ui:external-imports-start
2
+ * 외부 폰트 / 아이콘셋 / 디자인시스템 URL @import 는 반드시 이 블록 안에 둘 것.
3
+ * `@import 'tailwindcss'` 아래에 추가하면 CSS spec 위반으로 Turbopack 이 즉사.
4
+ * 예: @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css'); */
5
+ /* sh-ui:external-imports-end */
6
+
1
7
  @import 'tailwindcss';
2
8
  @import '../src/shared/styles/tokens.css';
3
9
 
@@ -1,3 +1,10 @@
1
+ /* sh-ui:external-imports-start
2
+ * 외부 폰트 / 아이콘셋 / 디자인시스템 URL @import 는 반드시 이 블록 안에 둘 것.
3
+ * `@import 'tailwindcss'` 아래에 추가하면 CSS spec ("모든 @import 는 다른 rule 보다
4
+ * 앞에 와야 함") 위반으로 Turbopack/lightningcss 가 parsing 거부 → dev server 즉사.
5
+ * 예: @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css'); */
6
+ /* sh-ui:external-imports-end */
7
+
1
8
  @import 'tailwindcss';
2
9
 
3
10
  @source "../../../../../../apps/**/*.{ts,tsx}";
@@ -0,0 +1,7 @@
1
+ # sh-ui app
2
+
3
+ sh-ui 컴포넌트는 `npx sh-ui-cli add <name>` 으로 추가합니다. 토큰은
4
+ `src/lib/styles/tokens.css` (flat) 또는 `src/shared/styles/tokens.css` (fsd) 입니다.
5
+
6
+ `vite.config.ts` 의 `@tailwindcss/vite` 플러그인이 Tailwind v4 를 처리하므로 PostCSS
7
+ 설정 파일이 없습니다.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export function RootLayout({ children }: { children: ReactNode }) {
4
+ return <div className="min-h-screen bg-background text-foreground">{children}</div>;
5
+ }
@@ -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,12 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+
3
+ export function createQueryClient() {
4
+ return new QueryClient({
5
+ defaultOptions: {
6
+ queries: {
7
+ staleTime: 60 * 1000,
8
+ refetchOnWindowFocus: false,
9
+ },
10
+ },
11
+ });
12
+ }
@@ -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
+ }