sh-ui-cli 0.22.1 → 0.23.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 (111) hide show
  1. package/README.md +19 -2
  2. package/bin/sh-ui.mjs +7 -0
  3. package/data/changelog/versions.json +27 -0
  4. package/package.json +13 -2
  5. package/src/create/cli-args.js +63 -0
  6. package/src/create/generator.js +542 -0
  7. package/src/create/index.mjs +68 -0
  8. package/src/create/plugins/index.js +17 -0
  9. package/src/create/plugins/nextIntl.js +197 -0
  10. package/src/create/plugins/sentry.js +689 -0
  11. package/src/create/theme/decode.js +66 -0
  12. package/src/create/theme/inject.js +111 -0
  13. package/src/mcp.mjs +108 -2
  14. package/src/paths.mjs +5 -0
  15. package/templates/flutter-standalone/README.md +34 -0
  16. package/templates/flutter-standalone/analysis_options.yaml +1 -0
  17. package/templates/flutter-standalone/lib/main.dart +103 -0
  18. package/templates/flutter-standalone/lib/sh_ui/foundation/sh_ui_tokens.dart +389 -0
  19. package/templates/flutter-standalone/pubspec.yaml +20 -0
  20. package/templates/flutter-standalone/sh-ui.config.json +15 -0
  21. package/templates/monorepo/.dockerignore +7 -0
  22. package/templates/monorepo/.eslintrc.js +8 -0
  23. package/templates/monorepo/.prettierrc +17 -0
  24. package/templates/monorepo/README.md +103 -0
  25. package/templates/monorepo/package.json +24 -0
  26. package/templates/monorepo/packages/eslint-config/base.js +31 -0
  27. package/templates/monorepo/packages/eslint-config/fsd.js +119 -0
  28. package/templates/monorepo/packages/eslint-config/next.js +65 -0
  29. package/templates/monorepo/packages/eslint-config/package.json +31 -0
  30. package/templates/monorepo/packages/eslint-config/react-internal.js +36 -0
  31. package/templates/monorepo/packages/typescript-config/base.json +20 -0
  32. package/templates/monorepo/packages/typescript-config/nextjs.json +13 -0
  33. package/templates/monorepo/packages/typescript-config/package.json +5 -0
  34. package/templates/monorepo/packages/typescript-config/react-library.json +8 -0
  35. package/templates/monorepo/packages/ui/ui-apps/.gitkeep +0 -0
  36. package/templates/monorepo/packages/ui/ui-core/eslint.config.js +3 -0
  37. package/templates/monorepo/packages/ui/ui-core/package.json +23 -0
  38. package/templates/monorepo/packages/ui/ui-core/src/lib/utils.ts +6 -0
  39. package/templates/monorepo/packages/ui/ui-core/tsconfig.json +11 -0
  40. package/templates/monorepo/pnpm-workspace.yaml +5 -0
  41. package/templates/monorepo/tsconfig.json +3 -0
  42. package/templates/monorepo/turbo.json +26 -0
  43. package/templates/nextjs-app/.env.example +2 -0
  44. package/templates/nextjs-app/Dockerfile +11 -0
  45. package/templates/nextjs-app/README.md +64 -0
  46. package/templates/nextjs-app/app/layout.tsx +22 -0
  47. package/templates/nextjs-app/app/page.tsx +7 -0
  48. package/templates/nextjs-app/eslint.config.js +10 -0
  49. package/templates/nextjs-app/next.config.ts +12 -0
  50. package/templates/nextjs-app/package.json +45 -0
  51. package/templates/nextjs-app/postcss.config.mjs +1 -0
  52. package/templates/nextjs-app/src/app/layouts/.gitkeep +0 -0
  53. package/templates/nextjs-app/src/app/providers/GlobalProvider/index.tsx +23 -0
  54. package/templates/nextjs-app/src/app/providers/index.tsx +1 -0
  55. package/templates/nextjs-app/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
  56. package/templates/nextjs-app/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  57. package/templates/nextjs-app/src/app/providers/theme/ThemeProviders.tsx +12 -0
  58. package/templates/nextjs-app/src/entities/.gitkeep +0 -0
  59. package/templates/nextjs-app/src/features/.gitkeep +0 -0
  60. package/templates/nextjs-app/src/shared/api/.gitkeep +0 -0
  61. package/templates/nextjs-app/src/shared/config/.gitkeep +0 -0
  62. package/templates/nextjs-app/src/shared/hooks/.gitkeep +0 -0
  63. package/templates/nextjs-app/src/shared/lib/.gitkeep +0 -0
  64. package/templates/nextjs-app/src/shared/model/.gitkeep +0 -0
  65. package/templates/nextjs-app/src/shared/ui/.gitkeep +0 -0
  66. package/templates/nextjs-app/src/views/.gitkeep +0 -0
  67. package/templates/nextjs-app/src/widgets/.gitkeep +0 -0
  68. package/templates/nextjs-app/tsconfig.json +23 -0
  69. package/templates/nextjs-app/vitest.config.ts +15 -0
  70. package/templates/nextjs-app/vitest.setup.ts +1 -0
  71. package/templates/nextjs-standalone/.env.example +2 -0
  72. package/templates/nextjs-standalone/.prettierrc +17 -0
  73. package/templates/nextjs-standalone/README.md +77 -0
  74. package/templates/nextjs-standalone/app/globals.css +33 -0
  75. package/templates/nextjs-standalone/app/layout.tsx +22 -0
  76. package/templates/nextjs-standalone/app/page.tsx +7 -0
  77. package/templates/nextjs-standalone/eslint.config.js +162 -0
  78. package/templates/nextjs-standalone/next.config.ts +10 -0
  79. package/templates/nextjs-standalone/package.json +66 -0
  80. package/templates/nextjs-standalone/postcss.config.mjs +5 -0
  81. package/templates/nextjs-standalone/sh-ui.config.json +19 -0
  82. package/templates/nextjs-standalone/src/app/layouts/.gitkeep +0 -0
  83. package/templates/nextjs-standalone/src/app/providers/GlobalProvider/index.tsx +23 -0
  84. package/templates/nextjs-standalone/src/app/providers/index.tsx +1 -0
  85. package/templates/nextjs-standalone/src/app/providers/tanstack/QueryClientProvider.tsx +27 -0
  86. package/templates/nextjs-standalone/src/app/providers/tanstack/TanstackDevtoolsProvider.tsx +13 -0
  87. package/templates/nextjs-standalone/src/app/providers/theme/ThemeProviders.tsx +12 -0
  88. package/templates/nextjs-standalone/src/entities/.gitkeep +0 -0
  89. package/templates/nextjs-standalone/src/features/.gitkeep +0 -0
  90. package/templates/nextjs-standalone/src/shared/api/.gitkeep +0 -0
  91. package/templates/nextjs-standalone/src/shared/config/.gitkeep +0 -0
  92. package/templates/nextjs-standalone/src/shared/hooks/.gitkeep +0 -0
  93. package/templates/nextjs-standalone/src/shared/lib/utils.ts +6 -0
  94. package/templates/nextjs-standalone/src/shared/model/.gitkeep +0 -0
  95. package/templates/nextjs-standalone/src/shared/styles/tokens.css +95 -0
  96. package/templates/nextjs-standalone/src/shared/ui/.gitkeep +0 -0
  97. package/templates/nextjs-standalone/src/views/.gitkeep +0 -0
  98. package/templates/nextjs-standalone/src/widgets/.gitkeep +0 -0
  99. package/templates/nextjs-standalone/tsconfig.json +39 -0
  100. package/templates/nextjs-standalone/vitest.config.ts +15 -0
  101. package/templates/nextjs-standalone/vitest.setup.ts +1 -0
  102. package/templates/ui-app-template/eslint.config.js +3 -0
  103. package/templates/ui-app-template/package.json +38 -0
  104. package/templates/ui-app-template/postcss.config.mjs +5 -0
  105. package/templates/ui-app-template/sh-ui.config.json +14 -0
  106. package/templates/ui-app-template/src/components/.gitkeep +0 -0
  107. package/templates/ui-app-template/src/hooks/.gitkeep +0 -0
  108. package/templates/ui-app-template/src/lib/.gitkeep +0 -0
  109. package/templates/ui-app-template/src/styles/globals.css +37 -0
  110. package/templates/ui-app-template/src/styles/tokens.css +95 -0
  111. package/templates/ui-app-template/tsconfig.json +11 -0
@@ -0,0 +1,66 @@
1
+ const TOKEN_KEYS = [
2
+ 'background', 'background-subtle', 'background-muted',
3
+ 'foreground', 'foreground-muted',
4
+ 'border', 'border-strong',
5
+ 'primary', 'primary-foreground', 'primary-hover',
6
+ 'danger', 'danger-foreground',
7
+ ];
8
+
9
+ const HEX_REGEX = /^#[0-9A-Fa-f]{6}$/;
10
+ const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
11
+ const MAX_THEME_BYTES = 10 * 1024; // 정상 테마 ~860 바이트. 10KB 면 10× 여유.
12
+
13
+ const validateTokenMap = (name, map) => {
14
+ if (!map || typeof map !== 'object') {
15
+ throw new Error(`theme 디코드 실패: ${name} 가 객체가 아님`);
16
+ }
17
+ for (const key of TOKEN_KEYS) {
18
+ if (!(key in map)) {
19
+ throw new Error(`theme 디코드 실패: ${name}.${key} 누락`);
20
+ }
21
+ const value = map[key];
22
+ if (typeof value !== 'string' || !HEX_REGEX.test(value)) {
23
+ throw new Error(
24
+ `theme 디코드 실패: ${name}.${key} 가 hex 포맷이 아님 (받은 값: ${JSON.stringify(value)})`,
25
+ );
26
+ }
27
+ }
28
+ };
29
+
30
+ export const decodeTheme = (b64) => {
31
+ if (typeof b64 !== 'string') {
32
+ throw new Error(`theme 디코드 실패: 문자열이 아님`);
33
+ }
34
+ if (b64.length > MAX_THEME_BYTES) {
35
+ throw new Error(
36
+ `theme 크기가 허용 범위를 초과함 (${b64.length} > ${MAX_THEME_BYTES} 바이트). ` +
37
+ `playground 에서 생성한 값만 사용하세요.`
38
+ );
39
+ }
40
+ if (!BASE64_REGEX.test(b64)) {
41
+ throw new Error(`theme 디코드 실패: base64 포맷이 아님`);
42
+ }
43
+ let json;
44
+ try {
45
+ json = Buffer.from(b64, 'base64').toString('utf-8');
46
+ } catch (e) {
47
+ throw new Error(`theme 디코드 실패: base64 디코드 실패 (${e.message})`);
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(json);
52
+ } catch (e) {
53
+ throw new Error(`theme 디코드 실패: JSON 파싱 실패 (${e.message})`);
54
+ }
55
+ validateTokenMap('light', parsed.light);
56
+ validateTokenMap('dark', parsed.dark);
57
+ if (typeof parsed.radius !== 'number' || Number.isNaN(parsed.radius)) {
58
+ throw new Error(`theme 디코드 실패: radius 가 숫자가 아님`);
59
+ }
60
+ if (parsed.radius < 0 || parsed.radius > 1.5) {
61
+ throw new Error(`theme 디코드 실패: radius 가 허용 범위(0~1.5)를 벗어남 (${parsed.radius})`);
62
+ }
63
+ return parsed;
64
+ };
65
+
66
+ export { TOKEN_KEYS };
@@ -0,0 +1,111 @@
1
+ import { TOKEN_KEYS } from './decode.js';
2
+
3
+ /**
4
+ * 파일 내용에서 sh-ui:<section>-start / -end 마커 사이 내용을 교체.
5
+ * commentOpen / commentClose 는 파일 형식에 따라 주어짐:
6
+ * CSS → '/*', '*' + '/'
7
+ * Dart → '//', ''
8
+ */
9
+ export const replaceSection = (content, section, commentOpen, commentClose, replacement) => {
10
+ const startMarker = commentClose
11
+ ? `${commentOpen} sh-ui:${section}-start ${commentClose}`
12
+ : `${commentOpen} sh-ui:${section}-start`;
13
+ const endMarker = commentClose
14
+ ? `${commentOpen} sh-ui:${section}-end ${commentClose}`
15
+ : `${commentOpen} sh-ui:${section}-end`;
16
+
17
+ const startIdx = content.indexOf(startMarker);
18
+ const endIdx = content.indexOf(endMarker);
19
+ if (startIdx < 0 || endIdx < 0 || endIdx < startIdx) {
20
+ throw new Error(`inject 실패: 섹션 ${section} 마커 없음`);
21
+ }
22
+
23
+ const before = content.slice(0, startIdx + startMarker.length);
24
+ const after = content.slice(endIdx);
25
+ return `${before}\n${replacement}\n${after}`;
26
+ };
27
+
28
+ // ─── CSS 블록 빌더 ───
29
+
30
+ const cssColorLine = (key, value) => ` --${key}: ${value};`;
31
+
32
+ export const buildCssColorsBlock = (theme) => {
33
+ const lightLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.light[k])).join('\n');
34
+ const darkLines = TOKEN_KEYS.map((k) => cssColorLine(k, theme.dark[k])).join('\n');
35
+ return [
36
+ ':root {',
37
+ lightLines,
38
+ '}',
39
+ '.dark {',
40
+ darkLines,
41
+ '}',
42
+ ].join('\n');
43
+ };
44
+
45
+ export const buildCssRadiusBlock = (theme) => {
46
+ return ` --radius: ${theme.radius}rem;`;
47
+ };
48
+
49
+ // ─── Dart 블록 빌더 ───
50
+
51
+ const toDartColor = (hex) => `Color(0xFF${hex.replace('#', '').toUpperCase()})`;
52
+
53
+ /**
54
+ * Dart 의 ShUiColorTokens 필드 순서 + 각 필드가 어떤 소스에서 값을 가져오는지.
55
+ * self — 현재 모드의 편집값
56
+ * inverse — 반대 모드의 편집값
57
+ * default — playground 가 노출하지 않음, 고정 기본값 사용
58
+ */
59
+ const DART_FIELD_SOURCES = [
60
+ { field: 'background', source: { kind: 'self', key: 'background' } },
61
+ { field: 'backgroundSubtle', source: { kind: 'self', key: 'background-subtle' } },
62
+ { field: 'backgroundMuted', source: { kind: 'self', key: 'background-muted' } },
63
+ { field: 'backgroundInverse', source: { kind: 'inverse', key: 'background' } },
64
+ { field: 'foreground', source: { kind: 'self', key: 'foreground' } },
65
+ { field: 'foregroundMuted', source: { kind: 'self', key: 'foreground-muted' } },
66
+ { field: 'foregroundSubtle', source: { kind: 'default' } },
67
+ { field: 'foregroundInverse', source: { kind: 'inverse', key: 'foreground' } },
68
+ { field: 'border', source: { kind: 'self', key: 'border' } },
69
+ { field: 'borderStrong', source: { kind: 'self', key: 'border-strong' } },
70
+ { field: 'primary', source: { kind: 'self', key: 'primary' } },
71
+ { field: 'primaryForeground', source: { kind: 'self', key: 'primary-foreground' } },
72
+ { field: 'primaryHover', source: { kind: 'self', key: 'primary-hover' } },
73
+ { field: 'danger', source: { kind: 'self', key: 'danger' } },
74
+ { field: 'dangerForeground', source: { kind: 'self', key: 'danger-foreground' } },
75
+ ];
76
+
77
+ const DART_DEFAULTS = {
78
+ light: { foregroundSubtle: '0xFFA3A3A3' },
79
+ dark: { foregroundSubtle: '0xFF737373' },
80
+ };
81
+
82
+ const buildDartStaticConst = (mode, self, opposite) => {
83
+ const lines = DART_FIELD_SOURCES.map(({ field, source }) => {
84
+ switch (source.kind) {
85
+ case 'self':
86
+ return ` ${field}: ${toDartColor(self[source.key])},`;
87
+ case 'inverse':
88
+ return ` ${field}: ${toDartColor(opposite[source.key])},`;
89
+ case 'default':
90
+ return ` ${field}: Color(${DART_DEFAULTS[mode][field]}),`;
91
+ }
92
+ }).join('\n');
93
+ return [
94
+ ` static const ${mode} = ShUiColorTokens(`,
95
+ lines,
96
+ ' );',
97
+ ].join('\n');
98
+ };
99
+
100
+ export const buildDartColorsBlock = (theme) => {
101
+ return [
102
+ buildDartStaticConst('light', theme.light, theme.dark),
103
+ '',
104
+ buildDartStaticConst('dark', theme.dark, theme.light),
105
+ ].join('\n');
106
+ };
107
+
108
+ export const buildDartRadiusBlock = (theme) => {
109
+ const px = (theme.radius * 16).toFixed(1);
110
+ return ` defaultRadius: ${px},`;
111
+ };
package/src/mcp.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
  // sh_ui_remove_component - 컴포넌트 삭제
14
14
 
15
15
  import { readFile } from "node:fs/promises";
16
+ import { existsSync } from "node:fs";
16
17
  import { resolve } from "node:path";
17
18
  import { z } from "zod";
18
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -22,6 +23,7 @@ import { init } from "./init.mjs";
22
23
  import { add } from "./add.mjs";
23
24
  import { list } from "./list.mjs";
24
25
  import { remove } from "./remove.mjs";
26
+ import { createProject } from "./create/generator.js";
25
27
  import {
26
28
  getRegistryRoot,
27
29
  getSummariesPath,
@@ -100,10 +102,44 @@ function resolveCwd(input) {
100
102
  return input?.cwd ? resolve(input.cwd) : process.cwd();
101
103
  }
102
104
 
105
+ const SERVER_INSTRUCTIONS = `sh-ui — Base UI 위에 빌드된 React/Flutter 디자인 시스템.
106
+
107
+ ## 새 프로젝트를 만드는 경우
108
+
109
+ 빈 폴더에서 시작하거나 사용자가 "Next.js 앱 만들어줘", "Flutter 프로젝트 새로", "sh-ui 로 시작" 처럼 **스캐폴드부터** 요청하면:
110
+
111
+ **1차 — \`sh_ui_create_project\` MCP 툴** (선호):
112
+ - 인자: name, platform (next|flutter), structure (next 일 때 standalone|monorepo), plugins (선택), force (덮어쓰기)
113
+ - 인터랙티브 프롬프트 없이 한 번에 스캐폴드 + 토큰 + sh-ui.config.json 생성
114
+
115
+ **2차 — Bash** (사용자가 직접 셸에서 돌리고 싶다고 명시할 때만):
116
+ npm create sh-ui my-app
117
+ npx sh-ui-cli create my-app --platform next --structure standalone --yes
118
+
119
+ \`create-next-app\` + \`sh_ui_init\` 조합은 **쓰지 말 것** — 위 두 경로가 더 짧고 sh-ui 관용에 맞다.
120
+
121
+ ## 이미 있는 프로젝트에 sh-ui 를 얹는 경우 (MCP 툴 사용)
122
+
123
+ 기존 Next.js/Vite/Flutter 프로젝트에 sh-ui 컴포넌트만 추가하고 싶을 때:
124
+ 1. \`sh_ui_describe_init\` — 자연어 의도("다크 모던")를 enum 으로 매핑
125
+ 2. \`sh_ui_init\` — \`sh-ui.config.json\` 생성
126
+ 3. \`sh_ui_add_component\` — \`tokens\` 먼저, 그다음 컴포넌트
127
+
128
+ ## 컴포넌트 작업
129
+
130
+ - \`sh_ui_list_components\` — 어떤 게 있는지
131
+ - \`sh_ui_get_component\` — props/소스 확인 (코드 작성 전)
132
+ - \`sh_ui_add_component\` / \`sh_ui_remove_component\` — 설치/삭제
133
+ - \`sh_ui_get_changelog\` — 최근 변경 내역
134
+ `;
135
+
103
136
  export async function startMcpServer() {
104
137
  const server = new McpServer(
105
- { name: "sh-ui", version: "0.22.1" },
106
- { capabilities: { tools: {} } },
138
+ { name: "sh-ui", version: "0.23.0" }, // sh-ui-cli 와 동기화
139
+ {
140
+ capabilities: { tools: {} },
141
+ instructions: SERVER_INSTRUCTIONS,
142
+ },
107
143
  );
108
144
 
109
145
  server.registerTool(
@@ -117,10 +153,80 @@ export async function startMcpServer() {
117
153
  async () => jsonResult(INIT_DESCRIPTIONS),
118
154
  );
119
155
 
156
+ server.registerTool(
157
+ "sh_ui_create_project",
158
+ {
159
+ description:
160
+ "빈 폴더에 sh-ui 프로젝트 스캐폴드 — Next.js (standalone/monorepo) 또는 Flutter. " +
161
+ "FSD 폴더 구조 + 토큰 + sh-ui.config.json 일괄 생성. 사용자가 '새 프로젝트' / '빈 폴더' / '스캐폴드부터' 류 요청을 하면 이 툴 사용 (Bash 로 npx sh-ui-cli create 직접 호출보다 우선).",
162
+ inputSchema: {
163
+ name: z.string().min(1)
164
+ .describe("프로젝트 디렉토리 이름. 예: my-app"),
165
+ platform: z.enum(["next", "flutter"])
166
+ .describe("타겟 플랫폼"),
167
+ structure: z.enum(["standalone", "monorepo"]).optional()
168
+ .describe("Next.js 구조 — platform=next 일 때 필수. standalone(단독) | monorepo(Turborepo)"),
169
+ plugins: z.array(z.enum(["sentry", "next-intl"])).optional()
170
+ .describe("Next.js 플러그인. 미지정시 빈 배열"),
171
+ theme: z.string().optional()
172
+ .describe("base64 인코딩된 테마 JSON (선택)"),
173
+ cwd: z.string().optional()
174
+ .describe("부모 디렉토리. 기본 process.cwd()"),
175
+ force: z.boolean().optional()
176
+ .describe("기존 디렉토리 덮어쓰기. 기본 false (안전)"),
177
+ },
178
+ },
179
+ async (input) => {
180
+ if (input.platform === "next" && !input.structure) {
181
+ return {
182
+ isError: true,
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: "platform=next 일 때 structure ('standalone' | 'monorepo') 가 필요합니다.",
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ const targetParent = resolveCwd(input);
192
+ const targetDir = resolve(targetParent, input.name);
193
+ if (existsSync(targetDir) && !input.force) {
194
+ return {
195
+ isError: true,
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `'${targetDir}' 가 이미 존재합니다. 덮어쓰려면 force: true.`,
200
+ },
201
+ ],
202
+ };
203
+ }
204
+ const origCwd = process.cwd();
205
+ try {
206
+ process.chdir(targetParent);
207
+ const text = await captureConsole(() =>
208
+ createProject({
209
+ name: input.name,
210
+ platform: input.platform,
211
+ structure: input.structure,
212
+ plugins: input.plugins,
213
+ theme: input.theme,
214
+ yes: true, // 사전 검사를 마쳤으니 generator 의 confirm 프롬프트 우회
215
+ }),
216
+ );
217
+ return textResult(text || "✓ 프로젝트 생성 완료");
218
+ } finally {
219
+ process.chdir(origCwd);
220
+ }
221
+ },
222
+ );
223
+
120
224
  server.registerTool(
121
225
  "sh_ui_init",
122
226
  {
123
227
  description:
228
+ "⚠️ 빈 폴더/새 프로젝트면 이 툴 대신 sh_ui_create_project 사용 — 스캐폴드 + 토큰 + config 일괄 처리. " +
229
+ "이 툴은 **이미 있는** Next.js/Vite/Flutter 프로젝트에 sh-ui 만 얹을 때. " +
124
230
  "현재 디렉토리(또는 cwd)에 sh-ui.config.json 을 생성. 비대화형 — 누락된 값은 기본값 사용. " +
125
231
  "선택지 의미가 헷갈리면 먼저 sh_ui_describe_init 호출 권장.",
126
232
  inputSchema: {
package/src/paths.mjs CHANGED
@@ -47,6 +47,11 @@ export function getSummariesPath(platform) {
47
47
  : resolve(MONOREPO_PACKAGES, "llms", "summaries", `${platform}.json`);
48
48
  }
49
49
 
50
+ /** sh-ui create 용 프로젝트 템플릿 루트 — 이 패키지 안에 직접 들어있다 */
51
+ export function getTemplatesRoot() {
52
+ return resolve(CLI_ROOT, "templates");
53
+ }
54
+
50
55
  /** 변경 내역 JSON */
51
56
  export function getVersionsPath() {
52
57
  return isBundled
@@ -0,0 +1,34 @@
1
+ # {{project_name}}
2
+
3
+ sh-ui 기반 Flutter 앱.
4
+
5
+ ## 시작하기
6
+
7
+ ```bash
8
+ flutter pub get
9
+ flutter run
10
+ ```
11
+
12
+ ## sh-ui 위젯 추가
13
+
14
+ ```bash
15
+ npx sh-ui add button
16
+ npx sh-ui add card input
17
+ ```
18
+
19
+ 위젯은 `lib/sh_ui/widgets/` 아래로 복사됩니다. 설정은 `sh-ui.config.json` 을 참조하세요.
20
+
21
+ ## 구조
22
+
23
+ ```
24
+ lib/
25
+ ├── main.dart # 앱 진입점
26
+ └── sh_ui/ # sh-ui 자산 (건드리지 말 것 — sh-ui CLI 가 관리)
27
+ ├── foundation/
28
+ │ └── sh_ui_tokens.dart # 디자인 토큰
29
+ └── widgets/ # sh-ui add 로 추가되는 위젯들
30
+ ```
31
+
32
+ ## 더 알아보기
33
+
34
+ - sh-ui 컴포넌트 목록 및 가이드: https://github.com/sanghyeonKim0201/sh-ui
@@ -0,0 +1 @@
1
+ include: package:flutter_lints/flutter.yaml
@@ -0,0 +1,103 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'sh_ui/foundation/sh_ui_tokens.dart';
3
+
4
+ void main() {
5
+ runApp(const MyApp());
6
+ }
7
+
8
+ class MyApp extends StatefulWidget {
9
+ const MyApp({super.key});
10
+
11
+ @override
12
+ State<MyApp> createState() => _MyAppState();
13
+ }
14
+
15
+ class _MyAppState extends State<MyApp> {
16
+ ThemeMode _themeMode = ThemeMode.light;
17
+
18
+ void _toggleTheme() {
19
+ setState(() {
20
+ _themeMode =
21
+ _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
22
+ });
23
+ }
24
+
25
+ @override
26
+ Widget build(BuildContext context) {
27
+ return MaterialApp(
28
+ title: '{{project_name}}',
29
+ debugShowCheckedModeBanner: false,
30
+ themeMode: _themeMode,
31
+ theme: ThemeData(
32
+ brightness: Brightness.light,
33
+ scaffoldBackgroundColor: ShUiColorTokens.light.background,
34
+ extensions: const [ShUiTheme.light],
35
+ ),
36
+ darkTheme: ThemeData(
37
+ brightness: Brightness.dark,
38
+ scaffoldBackgroundColor: ShUiColorTokens.dark.background,
39
+ extensions: const [ShUiTheme.dark],
40
+ ),
41
+ home: HomePage(
42
+ themeMode: _themeMode,
43
+ onToggleTheme: _toggleTheme,
44
+ ),
45
+ );
46
+ }
47
+ }
48
+
49
+ class HomePage extends StatelessWidget {
50
+ const HomePage({
51
+ super.key,
52
+ required this.themeMode,
53
+ required this.onToggleTheme,
54
+ });
55
+
56
+ final ThemeMode themeMode;
57
+ final VoidCallback onToggleTheme;
58
+
59
+ @override
60
+ Widget build(BuildContext context) {
61
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
62
+ final colors = shUi.colors;
63
+
64
+ return Scaffold(
65
+ appBar: AppBar(
66
+ title: const Text('{{project_name}}'),
67
+ actions: [
68
+ IconButton(
69
+ icon: Icon(
70
+ themeMode == ThemeMode.light
71
+ ? Icons.dark_mode_outlined
72
+ : Icons.light_mode_outlined,
73
+ ),
74
+ onPressed: onToggleTheme,
75
+ ),
76
+ ],
77
+ ),
78
+ body: Center(
79
+ child: Column(
80
+ mainAxisAlignment: MainAxisAlignment.center,
81
+ children: [
82
+ Text(
83
+ 'sh-ui 기반 Flutter 앱',
84
+ style: TextStyle(
85
+ color: colors.foreground,
86
+ fontSize: 20,
87
+ fontWeight: FontWeight.w600,
88
+ ),
89
+ ),
90
+ const SizedBox(height: 12),
91
+ Text(
92
+ 'sh-ui add <widget> 로 위젯을 추가해 보세요',
93
+ style: TextStyle(
94
+ color: colors.foregroundMuted,
95
+ fontSize: 14,
96
+ ),
97
+ ),
98
+ ],
99
+ ),
100
+ ),
101
+ );
102
+ }
103
+ }