sh-ui-cli 0.98.1 → 0.110.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 (40) hide show
  1. package/data/changelog/versions.json +169 -0
  2. package/data/registry/react/components/scroll-area/index.module.tsx +71 -0
  3. package/data/registry/react/components/scroll-area/index.tailwind.tsx +54 -0
  4. package/data/registry/react/components/scroll-area/index.tsx +67 -0
  5. package/data/registry/react/components/scroll-area/styles.css +64 -0
  6. package/data/registry/react/components/scroll-area/styles.module.css +64 -0
  7. package/data/registry/react/components/sheet/index.module.tsx +93 -0
  8. package/data/registry/react/components/sheet/index.tailwind.tsx +120 -0
  9. package/data/registry/react/components/sheet/index.tsx +121 -0
  10. package/data/registry/react/components/sheet/styles.css +183 -0
  11. package/data/registry/react/components/sheet/styles.module.css +171 -0
  12. package/data/registry/react/registry.json +94 -0
  13. package/data/registry/react/tokens-used.json +86 -1
  14. package/data/summaries/react.json +3 -1
  15. package/data/tokens/src/primitives.json +8 -0
  16. package/data/tokens/src/semantic.json +36 -10
  17. package/package.json +1 -1
  18. package/src/create/cli-args.js +20 -5
  19. package/src/create/describeTemplate.js +9 -7
  20. package/src/create/generator.js +168 -21
  21. package/src/create/index.mjs +9 -0
  22. package/src/create/plugins/nextIntl.js +3 -0
  23. package/src/create/templateManifest.js +1 -1
  24. package/src/create/theme/decode.js +3 -0
  25. package/src/create/theme/presets.js +45 -8
  26. package/src/mcp.mjs +68 -6
  27. package/src/theme-extract.mjs +1 -0
  28. package/templates/monorepo/npmrc +5 -0
  29. package/templates/nextjs-standalone/_arch/flat/app/globals.css +16 -4
  30. package/templates/nextjs-standalone/_arch/flat/lib/styles/tokens.css +35 -4
  31. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +35 -4
  32. package/templates/nextjs-standalone/_arch/mes/app/globals.css +16 -4
  33. package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +33 -2
  34. package/templates/nextjs-standalone/app/globals.css +16 -4
  35. package/templates/ui-app-template/src/styles/globals.css +16 -4
  36. package/templates/ui-app-template/src/styles/tokens.css +35 -4
  37. package/templates/vite-standalone/_arch/flat/src/lib/styles/globals.css +16 -0
  38. package/templates/vite-standalone/_arch/flat/src/lib/styles/tokens.css +35 -4
  39. package/templates/vite-standalone/_arch/fsd/src/shared/styles/globals.css +16 -0
  40. package/templates/vite-standalone/_arch/fsd/src/shared/styles/tokens.css +35 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$description": "컴포넌트별 토큰 의존성 (var(--*) 추출). build-registry-tokens.mjs 가 자동 생성.",
3
- "$generated": "2026-05-13T03:22:30.348Z",
3
+ "$generated": "2026-05-21T07:02:56.611Z",
4
4
  "components": {
5
5
  "button": {
6
6
  "plain": [
@@ -1977,6 +1977,91 @@
1977
1977
  "css-modules": [],
1978
1978
  "vanilla-extract": []
1979
1979
  },
1980
+ "scroll-area": {
1981
+ "plain": [
1982
+ "--border-strong",
1983
+ "--border-width-strong",
1984
+ "--foreground-muted",
1985
+ "--ring"
1986
+ ],
1987
+ "tailwind": [
1988
+ "--border-width-strong"
1989
+ ],
1990
+ "css-modules": [
1991
+ "--border-strong",
1992
+ "--border-width-strong",
1993
+ "--foreground-muted",
1994
+ "--ring"
1995
+ ],
1996
+ "vanilla-extract": []
1997
+ },
1998
+ "sheet": {
1999
+ "plain": [
2000
+ "--background",
2001
+ "--background-subtle",
2002
+ "--border",
2003
+ "--border-width-strong",
2004
+ "--control-sm",
2005
+ "--duration-fast",
2006
+ "--duration-slow",
2007
+ "--foreground",
2008
+ "--foreground-muted",
2009
+ "--radius",
2010
+ "--ring",
2011
+ "--shadow-xl",
2012
+ "--space-1",
2013
+ "--space-2",
2014
+ "--space-3",
2015
+ "--space-4",
2016
+ "--space-6",
2017
+ "--text-lg",
2018
+ "--text-sm",
2019
+ "--weight-semibold",
2020
+ "--z-modal",
2021
+ "--z-overlay"
2022
+ ],
2023
+ "tailwind": [
2024
+ "--border-width-strong",
2025
+ "--control-sm",
2026
+ "--duration-fast",
2027
+ "--duration-slow",
2028
+ "--radius",
2029
+ "--shadow-xl",
2030
+ "--space-1",
2031
+ "--space-2",
2032
+ "--space-4",
2033
+ "--space-6",
2034
+ "--text-lg",
2035
+ "--text-sm",
2036
+ "--z-modal",
2037
+ "--z-overlay"
2038
+ ],
2039
+ "css-modules": [
2040
+ "--background",
2041
+ "--background-subtle",
2042
+ "--border",
2043
+ "--border-width-strong",
2044
+ "--control-sm",
2045
+ "--duration-fast",
2046
+ "--duration-slow",
2047
+ "--foreground",
2048
+ "--foreground-muted",
2049
+ "--radius",
2050
+ "--ring",
2051
+ "--shadow-xl",
2052
+ "--space-1",
2053
+ "--space-2",
2054
+ "--space-3",
2055
+ "--space-4",
2056
+ "--space-6",
2057
+ "--text-lg",
2058
+ "--text-sm",
2059
+ "--weight-semibold",
2060
+ "--z-modal",
2061
+ "--z-overlay"
2062
+ ],
2063
+ "vanilla-extract": []
2064
+ },
1980
2065
  "utils": {
1981
2066
  "plain": [],
1982
2067
  "tailwind": [],
@@ -55,6 +55,8 @@
55
55
  "form-yup": "yup 스키마 → sh-ui Form 의 Standard Schema 어댑터. `yupSchema(yupObject)` 로 감싸 Form 의 `validation` prop 에 전달. yup 자체는 peerDependency.",
56
56
  "form-rhf": "React Hook Form 인스턴스 → sh-ui FormStore 어댑터. `adaptReactHookForm(rhfInstance, config)` — RHF 가 state owner, sh-ui 가 메타(steps/sections) 관리. react-hook-form 은 peerDependency.",
57
57
  "form-tanstack": "TanStack Form 인스턴스 → sh-ui FormStore 어댑터. `adaptTanstackForm(tanstackInstance, config)` — TanStack Form 이 state owner. @tanstack/react-form 은 peerDependency.",
58
- "calendar": "내부 캘린더 위젯 (DatePicker 가 사용). single / multiple / range 모드. CalendarMessages 로 a11y 텍스트 override. 일반적으로 직접 사용보다 DatePicker / DateRangePicker 권장."
58
+ "calendar": "내부 캘린더 위젯 (DatePicker 가 사용). single / multiple / range 모드. CalendarMessages 로 a11y 텍스트 override. 일반적으로 직접 사용보다 DatePicker / DateRangePicker 권장.",
59
+ "scroll-area": "커스텀 스크롤 컨테이너 — composite export ScrollArea (Base UI). 내부에서 viewport + scrollbar + thumb + corner 를 자동 구성하며 OS-native 스크롤바를 대체한다. orientation: \"vertical\" | \"horizontal\" | \"both\" (기본 vertical). 외부 height/width 가 정해진 컨테이너 안에서 사용. viewportClassName 으로 viewport 의 패딩/레이아웃 분리 적용. 스크롤바는 hover/scrolling 시 fade in, prefers-reduced-motion 존중.",
60
+ "sheet": "화면 가장자리에서 슬라이드 인 하는 side drawer — separate exports: Sheet / SheetTrigger / SheetClose / SheetContent / SheetTitle / SheetDescription / SheetHeader / SheetFooter / SheetCloseX (Base UI Drawer 래핑, 포커스 트랩). 글로벌 알림함 · 작업 큐 · 보조 패널 같은 사이드바 무관 모달 시트용. 사이드바 인근 detail 패널은 SidebarPanel, 중앙 강제 모달은 Dialog 권장. SheetContent 의 side: \"right\" | \"left\" | \"top\" | \"bottom\" (기본 right) 으로 진입 방향 지정. SheetTrigger·SheetClose 는 자체 button — 다른 엘리먼트 슬롯은 `render` prop. ESC/바깥 클릭/포커스 복귀 자동, prefers-reduced-motion 시 transform 트랜지션 제거."
59
61
  }
60
62
  }
@@ -94,6 +94,14 @@
94
94
  "bold": { "$value": 700, "$type": "fontWeight" }
95
95
  },
96
96
 
97
+ "letterSpacing": {
98
+ "tighter": { "$value": "-0.04em", "$type": "dimension" },
99
+ "tight": { "$value": "-0.02em", "$type": "dimension" },
100
+ "normal": { "$value": "0", "$type": "dimension" },
101
+ "wide": { "$value": "0.02em", "$type": "dimension" },
102
+ "wider": { "$value": "0.04em", "$type": "dimension" }
103
+ },
104
+
97
105
  "shadow": {
98
106
  "sm": { "$value": "0 1px 2px rgba(0, 0, 0, 0.08)", "$type": "shadow" },
99
107
  "md": { "$value": "0 4px 12px rgba(0, 0, 0, 0.12)", "$type": "shadow" },
@@ -4,8 +4,8 @@
4
4
  "light": {
5
5
  "background": {
6
6
  "default": { "$value": "{color.white}", "$type": "color" },
7
- "subtle": { "$value": "{color.{base}.50}", "$type": "color" },
8
- "muted": { "$value": "{color.{base}.100}", "$type": "color" },
7
+ "subtle": { "$value": "{color.{base}.100}", "$type": "color" },
8
+ "muted": { "$value": "{color.{base}.200}", "$type": "color" },
9
9
  "inverse": { "$value": "{color.{base}.950}", "$type": "color" }
10
10
  },
11
11
  "foreground": {
@@ -23,6 +23,11 @@
23
23
  "foreground": { "$value": "{color.{base}.50}", "$type": "color" },
24
24
  "hover": { "$value": "{color.{base}.800}", "$type": "color" }
25
25
  },
26
+ "accent": {
27
+ "default": { "$value": "{color.{base}.900}", "$type": "color" },
28
+ "foreground": { "$value": "{color.{base}.50}", "$type": "color" },
29
+ "hover": { "$value": "{color.{base}.800}", "$type": "color" }
30
+ },
26
31
  "danger": {
27
32
  "default": { "$value": "{color.red.600}", "$type": "color" },
28
33
  "foreground": { "$value": "{color.white}", "$type": "color" },
@@ -30,10 +35,10 @@
30
35
  },
31
36
  "ring": { "$value": "{color.{base}.400}", "$type": "color" },
32
37
  "sidebar": {
33
- "bg": { "$value": "{color.{base}.50}", "$type": "color" },
38
+ "bg": { "$value": "{color.{base}.100}", "$type": "color" },
34
39
  "fg": { "$value": "{color.{base}.950}", "$type": "color" },
35
40
  "border": { "$value": "{color.{base}.200}", "$type": "color" },
36
- "accent": { "$value": "{color.{base}.100}", "$type": "color" },
41
+ "accent": { "$value": "{color.{base}.200}", "$type": "color" },
37
42
  "accent-fg": { "$value": "{color.{base}.950}", "$type": "color" }
38
43
  }
39
44
  },
@@ -60,6 +65,11 @@
60
65
  "foreground": { "$value": "{color.{base}.900}", "$type": "color" },
61
66
  "hover": { "$value": "{color.{base}.200}", "$type": "color" }
62
67
  },
68
+ "accent": {
69
+ "default": { "$value": "{color.{base}.50}", "$type": "color" },
70
+ "foreground": { "$value": "{color.{base}.900}", "$type": "color" },
71
+ "hover": { "$value": "{color.{base}.200}", "$type": "color" }
72
+ },
63
73
  "danger": {
64
74
  "default": { "$value": "{color.red.600}", "$type": "color" },
65
75
  "foreground": { "$value": "{color.white}", "$type": "color" },
@@ -76,7 +86,12 @@
76
86
  },
77
87
 
78
88
  "radius": {
79
- "default": { "$value": "{radius.{radius}}", "$type": "dimension" }
89
+ "default": { "$value": "{radius.{radius}}", "$type": "dimension" },
90
+ "sm": { "$value": "{radius.sm}", "$type": "dimension" },
91
+ "md": { "$value": "{radius.md}", "$type": "dimension" },
92
+ "lg": { "$value": "{radius.lg}", "$type": "dimension" },
93
+ "xl": { "$value": "{radius.xl}", "$type": "dimension" },
94
+ "full": { "$value": "{radius.full}", "$type": "dimension" }
80
95
  },
81
96
 
82
97
  "space": {
@@ -112,11 +127,22 @@
112
127
  },
113
128
 
114
129
  "shadow": {
115
- "sm": { "$value": "{shadow.sm}", "$type": "shadow" },
116
- "md": { "$value": "{shadow.md}", "$type": "shadow" },
117
- "lg": { "$value": "{shadow.lg}", "$type": "shadow" },
118
- "xl": { "$value": "{shadow.xl}", "$type": "shadow" },
119
- "menu": { "$value": "{shadow.lg}", "$type": "shadow" }
130
+ "sm": { "$value": "{shadow.sm}", "$type": "shadow" },
131
+ "md": { "$value": "{shadow.md}", "$type": "shadow" },
132
+ "lg": { "$value": "{shadow.lg}", "$type": "shadow" },
133
+ "xl": { "$value": "{shadow.xl}", "$type": "shadow" },
134
+ "menu": { "$value": "{shadow.lg}", "$type": "shadow" },
135
+ "popover": { "$value": "{shadow.lg}", "$type": "shadow" },
136
+ "modal": { "$value": "{shadow.xl}", "$type": "shadow" },
137
+ "toast": { "$value": "{shadow.md}", "$type": "shadow" }
138
+ },
139
+
140
+ "tracking": {
141
+ "tighter": { "$value": "{letterSpacing.tighter}", "$type": "dimension" },
142
+ "tight": { "$value": "{letterSpacing.tight}", "$type": "dimension" },
143
+ "normal": { "$value": "{letterSpacing.normal}", "$type": "dimension" },
144
+ "wide": { "$value": "{letterSpacing.wide}", "$type": "dimension" },
145
+ "wider": { "$value": "{letterSpacing.wider}", "$type": "dimension" }
120
146
  },
121
147
 
122
148
  "duration": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.98.1",
3
+ "version": "0.110.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,8 +13,12 @@ const VALID_STRUCTURES = CREATE_STRUCTURES;
13
13
  const VALID_PLUGINS = allPlugins.map((p) => p.name);
14
14
  const VALID_ARCHES = allArchitectures.map((a) => a.name);
15
15
 
16
- const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port', 'i18n', 'locales'];
17
- const BOOL_FLAGS = ['yes', 'help', 'dry-run'];
16
+ const VALUE_FLAGS = ['platform', 'structure', 'plugins', 'theme', 'app', 'css', 'arch', 'port', 'i18n', 'locales', 'locale'];
17
+
18
+ // `locale` (단수) 은 한국어/일본어 같은 사용자 지역 디폴트 가정 (폰트 등) 을 활성화하는
19
+ // 옵션. `locales` (복수) 와 다르다: locales 는 i18n 활성화 시 생성할 locale 코드 목록.
20
+ const VALID_LOCALES = ['default', 'ko'];
21
+ const BOOL_FLAGS = ['yes', 'help', 'dry-run', 'no-git-init', 'git-init', 'in-place'];
18
22
 
19
23
  const SUBCOMMANDS = ['add-app', 'add-component'];
20
24
 
@@ -40,9 +44,13 @@ export const parseArgs = (argv) => {
40
44
  }
41
45
  const name = arg.slice(2);
42
46
  if (BOOL_FLAGS.includes(name)) {
43
- // dry-run dryRun 으로 캐멀 케이스
44
- const key = name === 'dry-run' ? 'dryRun' : name;
45
- flags[key] = true;
47
+ // dry-run dryRun. --no-git-init gitInit:false. --git-init → gitInit:true.
48
+ // --in-place inPlace:true (기존 디렉토리 비파괴 머지).
49
+ if (name === 'dry-run') flags.dryRun = true;
50
+ else if (name === 'no-git-init') flags.gitInit = false;
51
+ else if (name === 'git-init') flags.gitInit = true;
52
+ else if (name === 'in-place') flags.inPlace = true;
53
+ else flags[name] = true;
46
54
  continue;
47
55
  }
48
56
  if (!VALUE_FLAGS.includes(name)) {
@@ -79,6 +87,13 @@ export const parseArgs = (argv) => {
79
87
  if (name === 'i18n' && !I18N_LIBRARIES.includes(value)) {
80
88
  throw new Error(`--i18n 은 ${I18N_LIBRARIES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
81
89
  }
90
+ if (name === 'locale' && !VALID_LOCALES.includes(value)) {
91
+ throw new Error(
92
+ `--locale 은 ${VALID_LOCALES.join('/')} 중 하나여야 함 (받은 값: ${value}). ` +
93
+ `'ko' 선택 시 Pretendard 폰트가 globals.css 에 자동 적용됩니다. ` +
94
+ `(주의: --locales (복수) 는 i18n 활성화 시 생성할 locale 코드 목록 — 다른 의미).`,
95
+ );
96
+ }
82
97
  if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
83
98
  // planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
84
99
  if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
@@ -375,18 +375,20 @@ function finalize(groups) {
375
375
  * generator.js 의 `finalizeProject` 가 실제 fs 단계에서 적용하는 rename 을
376
376
  * file plan 텍스트 레벨에서 mock-apply (v0.96.0+ — 피드백 #3 buglet).
377
377
  *
378
- * 현재 규칙:
379
- * - basename 이 정확히 'gitignore' 인 경로 → '.gitignore' prefix dot 추가.
380
- * (npm publish .gitignore strip 하므로 템플릿엔 점 없이 두고 emit 후 dot-prefix.)
381
- * - 이미 '.gitignore' 경로는 그대로.
378
+ * 현재 규칙 — npm publish 가 strip 하는 dotfile 의 점 없는 템플릿 이름을 복원:
379
+ * - 'gitignore' → '.gitignore' (npm strip, generator 의 STRIPPED_DOTFILES 와 일치)
380
+ * - 'npmrc' → '.npmrc' (npm publish 항상 strip)
381
+ * - 이미 붙은 경로는 그대로.
382
382
  *
383
- * 미래에 다른 fs-level rename 이 추가되면 여기에 같이 등록 (describeTemplate 의
384
- * file-plan ↔ create_project 실제 emit 1:1 정합성 유지).
383
+ * 미래에 다른 fs-level rename 이 추가되면 generator.js STRIPPED_DOTFILES
384
+ * 함께 여기에 등록 (describeTemplate 의 file-plan ↔ create_project 실제 emit 1:1 정합성).
385
385
  */
386
+ const STRIPPED_DOTFILES = { gitignore: '.gitignore', npmrc: '.npmrc' };
387
+
386
388
  function applyFinalizeRenames(p) {
387
389
  const slash = p.lastIndexOf('/');
388
390
  const dir = slash === -1 ? '' : p.slice(0, slash + 1);
389
391
  const base = slash === -1 ? p : p.slice(slash + 1);
390
- if (base === 'gitignore') return dir + '.gitignore';
392
+ if (STRIPPED_DOTFILES[base]) return dir + STRIPPED_DOTFILES[base];
391
393
  return p;
392
394
  }
@@ -290,19 +290,28 @@ export async function createProject(options = {}) {
290
290
  }
291
291
  }
292
292
 
293
- // dry-run tmpdir 그대로 생성한 파일 목록 출력 + 정리.
294
- // 사용자 cwd 건드리지 않으면서 실제 generation 흐름을 그대로 검증한다.
293
+ // inPlace 이미 커스터마이즈된 디렉토리(루트 docs·git 보존)에 비파괴 머지.
294
+ // generation dry-run 동일하게 tmpdir 에서 돌린 뒤, 이미 있는 파일은
295
+ // 건드리지 않고 새 파일만 realTargetDir 로 복사한다.
296
+ const inPlace = options.inPlace === true && !options.dryRun;
297
+ const realTargetDir = path.resolve(process.cwd(), projectName);
298
+
299
+ // dry-run / inPlace 는 tmpdir 에 생성. dry-run 은 목록 출력 후 정리,
300
+ // inPlace 는 realTargetDir 로 비파괴 머지. 일반 모드는 cwd/name 에 직접 생성.
295
301
  const targetDir = options.dryRun
296
302
  ? await fs.mkdtemp(path.join(os.tmpdir(), 'sh-ui-dry-'))
297
- : path.resolve(process.cwd(), projectName);
303
+ : inPlace
304
+ ? await fs.mkdtemp(path.join(os.tmpdir(), 'sh-ui-inplace-'))
305
+ : realTargetDir;
298
306
 
299
- // 방어 가드 — projectName 검증을 이미 통과했어도 `fs.remove` 직전에 한 번 더 확인.
300
- // dry-run tmpdir 이라 parent cwd 아니므로 스킵.
307
+ // 방어 가드 — projectName 검증을 이미 통과했어도 fs 쓰기 직전에 한 번 더 확인.
308
+ // dry-run / inPlace targetDir tmpdir 이므로 실제 목적지를 검증한다.
301
309
  if (!options.dryRun) {
302
- assertWithin(process.cwd(), targetDir);
310
+ assertWithin(process.cwd(), realTargetDir);
303
311
  }
304
312
 
305
- if (!options.dryRun && await fs.pathExists(targetDir)) {
313
+ // 일반 모드만 기존 디렉토리 덮어쓰기 확인. inPlace 는 비파괴라 remove 하지 않는다.
314
+ if (!options.dryRun && !inPlace && await fs.pathExists(targetDir)) {
306
315
  if (options.yes) {
307
316
  await fs.remove(targetDir);
308
317
  } else {
@@ -318,9 +327,29 @@ export async function createProject(options = {}) {
318
327
  }
319
328
  }
320
329
 
330
+ // finalizeProject + (inPlace 면) temp → realTargetDir 비파괴 머지.
331
+ // 세 플랫폼 경로(flutter/vite/next)가 공통으로 호출한다.
332
+ async function finalizeAndCommit() {
333
+ await finalizeProject(targetDir, {
334
+ dryRun: options.dryRun,
335
+ // inPlace 는 temp 에서 git init 해봐야 버려지므로 스킵 — realTargetDir 의
336
+ // 기존 .git 을 그대로 둔다 (gitignore/npmrc 복원은 finalize 안에서 수행됨).
337
+ gitInit: inPlace ? false : options.gitInit,
338
+ locale: options.locale,
339
+ });
340
+ if (inPlace) {
341
+ await fs.ensureDir(realTargetDir);
342
+ await fs.copy(targetDir, realTargetDir, { overwrite: false, errorOnExist: false });
343
+ await fs.remove(targetDir);
344
+ console.log(
345
+ `\n ℹ inPlace — 기존 디렉토리에 비파괴 머지 완료 (이미 있는 파일은 보존).`,
346
+ );
347
+ }
348
+ }
349
+
321
350
  if (platform === 'flutter') {
322
351
  await generateFlutter(targetDir, projectName, theme, cssFramework, themeBase);
323
- await finalizeProject(targetDir, { dryRun: options.dryRun });
352
+ await finalizeAndCommit();
324
353
  console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
325
354
  console.log(`\n cd ${projectName}`);
326
355
  console.log(' flutter pub get');
@@ -355,7 +384,7 @@ export async function createProject(options = {}) {
355
384
  });
356
385
  }
357
386
 
358
- await finalizeProject(targetDir, { dryRun: options.dryRun });
387
+ await finalizeAndCommit();
359
388
 
360
389
  if (options.dryRun) {
361
390
  const files = await listAllFiles(targetDir);
@@ -401,7 +430,7 @@ export async function createProject(options = {}) {
401
430
  });
402
431
  }
403
432
 
404
- await finalizeProject(targetDir, { dryRun: options.dryRun });
433
+ await finalizeAndCommit();
405
434
 
406
435
  if (options.dryRun) {
407
436
  const files = await listAllFiles(targetDir);
@@ -1644,21 +1673,50 @@ async function stripTailwindFromPrettier(prettierPath) {
1644
1673
  }
1645
1674
 
1646
1675
  /**
1647
- * 스캐폴드 마무리 `gitignore` 파일을 `.gitignore` 되돌리고 `git init` 실행.
1676
+ * npm publish 패키지 tarball 에서 strip/변형하는 dotfile 템플릿엔 점 없는
1677
+ * 이름으로 두고 스캐폴드 직후 점을 복원한다.
1678
+ * - `.gitignore` → npm 이 strip (없으면 `.npmignore` fallback 으로 사용)
1679
+ * - `.npmrc` → npm 이 항상 strip (publish 시 레지스트리 토큰 유출 방지)
1680
+ * 둘 다 published CLI 엔 도착하지 않으므로 템플릿엔 `gitignore` / `npmrc` 로 둔다.
1681
+ */
1682
+ const STRIPPED_DOTFILES = { gitignore: '.gitignore', npmrc: '.npmrc' };
1683
+
1684
+ /**
1685
+ * 스캐폴드 마무리 — strip 된 dotfile(`gitignore`/`npmrc`) 을 점 붙은 이름으로
1686
+ * 되돌리고 `git init` 실행.
1648
1687
  *
1649
- * 이름을 우회하는가: npm publish 는 패키지 안의 `.gitignore` 를 자동으로
1650
- * strip 한다(없으면 `.npmignore` fallback 으로 사용). 사용자에게 도착하지 않으니
1651
- * 템플릿엔 `gitignore` 두고 복사 직후 dot-prefix 붙인다.
1688
+ * gitInit 옵션:
1689
+ * - undefined (auto): parent 이미 git tree 안이면 스킵, 아니면 init. nested .git
1690
+ * 충돌 방지 기존 monorepo / 사용자 작업 트리 안에서 호출 시 안전.
1691
+ * - true: 무조건 init (parent 가 git tree 안이어도). nested 가 의도된 경우.
1692
+ * - false: 무조건 스킵.
1652
1693
  *
1653
1694
  * git init 은 dry-run 에서는 스킵하고, 실패해도(git 미설치 등) 조용히 넘어간다.
1654
1695
  */
1655
- async function finalizeProject(targetDir, { dryRun = false } = {}) {
1656
- // 모노레포 / sub-app 까지 모든 `gitignore` `.gitignore` rename.
1696
+ async function finalizeProject(targetDir, { dryRun = false, gitInit, locale } = {}) {
1697
+ // 모노레포 / sub-app 까지 strip 된 dotfile(gitignore/npmrc) 붙은 이름으로 복원.
1657
1698
  // root 만 처리하면 apps/<name>/gitignore 가 그대로 남아 node_modules/dist 가 staged 된다 (v0.93.0 버그).
1658
- await renameAllGitignoreRecursive(targetDir);
1699
+ await restoreStrippedDotfilesRecursive(targetDir);
1700
+
1701
+ // locale 후처리 — 한국어면 globals.css 들에 Pretendard 자동 적용 (Aifice 피드백 3.1).
1702
+ // dryRun 이면 skip — globals.css 가 디스크에 안 써졌을 수 있다.
1703
+ if (!dryRun && locale === 'ko') {
1704
+ await injectLocaleFont(targetDir, 'ko');
1705
+ }
1659
1706
 
1660
1707
  if (dryRun) return;
1661
1708
 
1709
+ const decision = resolveGitInit(targetDir, gitInit);
1710
+ if (!decision.init) {
1711
+ if (decision.reason === 'nested') {
1712
+ console.log(
1713
+ `\n ℹ 이미 git tree 안이라 .git 초기화를 스킵했습니다 (parent: ${decision.parentRepo}).\n` +
1714
+ ` nested git repo 가 의도된 경우 --git-init 으로 강제할 수 있습니다.`,
1715
+ );
1716
+ }
1717
+ return;
1718
+ }
1719
+
1662
1720
  try {
1663
1721
  execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
1664
1722
  } catch {
@@ -1666,6 +1724,95 @@ async function finalizeProject(targetDir, { dryRun = false } = {}) {
1666
1724
  }
1667
1725
  }
1668
1726
 
1727
+ /**
1728
+ * `locale` 옵션 후처리 — locale=ko 면 스캐폴드된 모든 globals.css 에 Pretendard 폰트 적용.
1729
+ *
1730
+ * 한국어 사용자가 sh-ui 를 init 한 직후 "Pretendard 로 교체" 가 거의 100% 첫 작업이라
1731
+ * (Aifice 피드백 3.1), 이를 옵션 하나로 자동화. 적용 방식:
1732
+ * - external-imports 마커 안에 Pretendard CDN @import 라인 prepend (Tailwind import 보다 먼저)
1733
+ * - 파일 끝에 `body { font-family: 'Pretendard Variable', ... }` rule 추가
1734
+ *
1735
+ * idempotent: 이미 'Pretendard Variable' 문자열이 있으면 skip.
1736
+ * monorepo 도 안전 — targetDir 하위 모든 globals.css 를 재귀 스캔 (node_modules / .git / _arch 제외).
1737
+ * Flutter 는 globals.css 가 없어 자동 no-op.
1738
+ */
1739
+ async function injectLocaleFont(targetDir, locale) {
1740
+ if (locale !== 'ko') return;
1741
+
1742
+ const PRETENDARD_IMPORT =
1743
+ "@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css');\n";
1744
+ const FONT_FAMILY_RULE =
1745
+ "\n/* sh-ui:locale=ko — Pretendard 기본 적용. 사용자 정의로 override 가능. */\n" +
1746
+ "body { font-family: 'Pretendard Variable', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; }\n";
1747
+ const END_MARKER = '/* sh-ui:external-imports-end */';
1748
+
1749
+ const targets = [];
1750
+ async function walk(dir) {
1751
+ let entries;
1752
+ try {
1753
+ entries = await fs.readdir(dir, { withFileTypes: true });
1754
+ } catch {
1755
+ return;
1756
+ }
1757
+ for (const e of entries) {
1758
+ const full = path.join(dir, e.name);
1759
+ if (e.isDirectory()) {
1760
+ if (['node_modules', '.git', '.next', 'dist', '_arch'].includes(e.name)) continue;
1761
+ await walk(full);
1762
+ } else if (e.isFile() && e.name === 'globals.css') {
1763
+ targets.push(full);
1764
+ }
1765
+ }
1766
+ }
1767
+ await walk(targetDir);
1768
+
1769
+ for (const abs of targets) {
1770
+ let css = await fs.readFile(abs, 'utf-8');
1771
+ if (css.includes("'Pretendard Variable'")) continue;
1772
+ if (!css.includes(END_MARKER)) continue; // 우리 마커 없는 파일은 안전상 건드리지 않음
1773
+ css = css.replace(END_MARKER, `${PRETENDARD_IMPORT}${END_MARKER}`);
1774
+ css += FONT_FAMILY_RULE;
1775
+ await fs.writeFile(abs, css);
1776
+ }
1777
+ }
1778
+
1779
+ /**
1780
+ * git init 실행 여부 결정. auto 모드는 parent 가 git tree 안이면 스킵 — nested .git
1781
+ * 충돌 방지. 명시 override (true/false) 가 있으면 그대로 따른다.
1782
+ *
1783
+ * 감지: `git -C <parentDir> rev-parse --is-inside-work-tree` 출력이 "true" 면 안.
1784
+ * git 미설치 / 권한 문제 등으로 명령이 실패하면 트리 밖으로 간주 (안전 디폴트: init 시도).
1785
+ */
1786
+ function resolveGitInit(targetDir, override) {
1787
+ if (override === false) return { init: false, reason: 'explicit-skip' };
1788
+ if (override === true) return { init: true, reason: 'explicit-force' };
1789
+
1790
+ const parentDir = path.dirname(targetDir);
1791
+ try {
1792
+ const result = execSync('git rev-parse --is-inside-work-tree', {
1793
+ cwd: parentDir,
1794
+ stdio: ['ignore', 'pipe', 'ignore'],
1795
+ })
1796
+ .toString()
1797
+ .trim();
1798
+ if (result === 'true') {
1799
+ let parentRepo = parentDir;
1800
+ try {
1801
+ parentRepo = execSync('git rev-parse --show-toplevel', {
1802
+ cwd: parentDir,
1803
+ stdio: ['ignore', 'pipe', 'ignore'],
1804
+ })
1805
+ .toString()
1806
+ .trim() || parentDir;
1807
+ } catch {}
1808
+ return { init: false, reason: 'nested', parentRepo };
1809
+ }
1810
+ } catch {
1811
+ // parent 가 git tree 밖 (또는 git 미설치) — 안전 디폴트: init 시도.
1812
+ }
1813
+ return { init: true, reason: 'auto' };
1814
+ }
1815
+
1669
1816
  /**
1670
1817
  * CLAUDE.md 의 `{{PLATFORM_APP_DESCRIPTION}}` 치환용 문장 — AI 에이전트에게 어떤 플랫폼인지
1671
1818
  * 정확히 전달해서 잘못된 컨벤션 (예: vite 프로젝트에 App Router 가정) 적용 방지.
@@ -1678,7 +1825,7 @@ function describeAppPlatform(platform) {
1678
1825
  return 'Next.js 앱 (App Router + Server Components). 라우트 + 비즈니스 로직.';
1679
1826
  }
1680
1827
 
1681
- async function renameAllGitignoreRecursive(dir) {
1828
+ async function restoreStrippedDotfilesRecursive(dir) {
1682
1829
  let entries;
1683
1830
  try {
1684
1831
  entries = await fs.readdir(dir, { withFileTypes: true });
@@ -1690,9 +1837,9 @@ async function renameAllGitignoreRecursive(dir) {
1690
1837
  if (entry.isDirectory()) {
1691
1838
  // 스캐폴드 직후엔 node_modules / .git 가 없지만 방어적으로.
1692
1839
  if (entry.name === 'node_modules' || entry.name === '.git') continue;
1693
- await renameAllGitignoreRecursive(fullPath);
1694
- } else if (entry.name === 'gitignore') {
1695
- await fs.move(fullPath, path.join(dir, '.gitignore'), { overwrite: true });
1840
+ await restoreStrippedDotfilesRecursive(fullPath);
1841
+ } else if (STRIPPED_DOTFILES[entry.name]) {
1842
+ await fs.move(fullPath, path.join(dir, STRIPPED_DOTFILES[entry.name]), { overwrite: true });
1696
1843
  }
1697
1844
  }
1698
1845
  }
@@ -30,8 +30,12 @@ export const HELP_TEXT = `sh-ui create — sh-ui 프로젝트 스캐폴드 (Next
30
30
  --css <${CSS_FRAMEWORKS_SUPPORTED.join('|')}> CSS 프레임워크. base 파일까지 분기 emit (tailwind/plain/css-modules)
31
31
  --i18n <react-i18next|none> vite 전용 — react-i18next 셋업 emit (i18n config + I18nProvider). 기본 none (v0.92.0+)
32
32
  --locales <ko,en> i18n 활성화 시 생성할 locale 코드 (comma-separated). 기본 'ko,en'
33
+ --locale <default|ko> 사용자 지역 디폴트 가정 — 'ko' 선택 시 globals.css 에 Pretendard 자동 적용 (v0.103.0+)
33
34
  --yes 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
35
+ --in-place 기존 디렉토리에 비파괴 머지 — 이미 있는 파일은 보존, 없는 파일만 채움. 커스텀 루트 docs·.git 보존하며 재생성 (디렉토리 삭제 안 함)
34
36
  --dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
37
+ --no-git-init git init 스킵 (기존 git tree 안에서 호출 시 nested .git 충돌 방지). 기본 auto — parent 가 git tree 안이면 자동 스킵
38
+ --git-init git init 무조건 실행 (nested 가 의도된 경우)
35
39
  -h, --help 이 도움말
36
40
 
37
41
  예 (대화형):
@@ -102,6 +106,11 @@ export async function runCreate(rest) {
102
106
  port: flags.port,
103
107
  yes: flags.yes,
104
108
  dryRun: flags.dryRun,
109
+ // gitInit / locale / inPlace — 이전엔 파싱만 하고 createProject 로
110
+ // 전달하지 않아 무시되던 버그. v0.110.0 에서 함께 연결.
111
+ gitInit: flags.gitInit,
112
+ locale: flags.locale,
113
+ inPlace: flags.inPlace,
105
114
  });
106
115
  }
107
116
  }
@@ -231,6 +231,9 @@ export function RootLayout({
231
231
  export const routing = defineRouting({
232
232
  locales: ['ko', 'en'],
233
233
  defaultLocale: 'ko',
234
+ // ko 는 prefix 없이 '/', en 만 '/en/...' — 한국 사용자 베이스 + 영문 보조 페어가 흔한 패턴.
235
+ // 모든 locale 에 prefix 강제하고 싶으면 'always', 절대 prefix 안 붙이면 'never'.
236
+ localePrefix: 'as-needed',
234
237
  });
235
238
  `,
236
239
 
@@ -17,11 +17,11 @@ export const TEMPLATE_MANIFEST = {
17
17
  "monorepo": {
18
18
  "base": [
19
19
  ".dockerignore",
20
- ".npmrc",
21
20
  ".prettierrc",
22
21
  "CLAUDE.md",
23
22
  "README.md",
24
23
  "gitignore",
24
+ "npmrc",
25
25
  "package.json",
26
26
  "packages/eslint-config/base.js",
27
27
  "packages/eslint-config/flat.js",
@@ -24,6 +24,9 @@ const OPTIONAL_TOKEN_KEYS = [
24
24
  // fallback 을 두지만, Tailwind @theme inline 의 --color-sidebar-* 가 :root 에서 해석되도록
25
25
  // tokens.css 에도 끌어올린다.
26
26
  'sidebar-bg', 'sidebar-fg', 'sidebar-border', 'sidebar-accent', 'sidebar-accent-fg',
27
+ // v0.100.0+ — accent 토큰. primary 와 의미 분리: primary = 브랜드 action color,
28
+ // accent = signature highlight (선택 상태·링크·hover bg 등). 디폴트는 primary 와 동일.
29
+ 'accent', 'accent-foreground', 'accent-hover',
27
30
  ];
28
31
 
29
32
  /**