sh-ui-cli 0.98.1 → 0.109.1

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 (37) hide show
  1. package/data/changelog/versions.json +157 -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 +18 -5
  19. package/src/create/generator.js +116 -4
  20. package/src/create/index.mjs +3 -0
  21. package/src/create/plugins/nextIntl.js +3 -0
  22. package/src/create/theme/decode.js +3 -0
  23. package/src/create/theme/presets.js +45 -8
  24. package/src/mcp.mjs +47 -3
  25. package/src/theme-extract.mjs +1 -0
  26. package/templates/nextjs-standalone/_arch/flat/app/globals.css +16 -4
  27. package/templates/nextjs-standalone/_arch/flat/lib/styles/tokens.css +35 -4
  28. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +35 -4
  29. package/templates/nextjs-standalone/_arch/mes/app/globals.css +16 -4
  30. package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +33 -2
  31. package/templates/nextjs-standalone/app/globals.css +16 -4
  32. package/templates/ui-app-template/src/styles/globals.css +16 -4
  33. package/templates/ui-app-template/src/styles/tokens.css +35 -4
  34. package/templates/vite-standalone/_arch/flat/src/lib/styles/globals.css +16 -0
  35. package/templates/vite-standalone/_arch/flat/src/lib/styles/tokens.css +35 -4
  36. package/templates/vite-standalone/_arch/fsd/src/shared/styles/globals.css +16 -0
  37. 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.109.1",
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'];
18
22
 
19
23
  const SUBCOMMANDS = ['add-app', 'add-component'];
20
24
 
@@ -40,9 +44,11 @@ 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
+ if (name === 'dry-run') flags.dryRun = true;
49
+ else if (name === 'no-git-init') flags.gitInit = false;
50
+ else if (name === 'git-init') flags.gitInit = true;
51
+ else flags[name] = true;
46
52
  continue;
47
53
  }
48
54
  if (!VALUE_FLAGS.includes(name)) {
@@ -79,6 +85,13 @@ export const parseArgs = (argv) => {
79
85
  if (name === 'i18n' && !I18N_LIBRARIES.includes(value)) {
80
86
  throw new Error(`--i18n 은 ${I18N_LIBRARIES.join('/')} 중 하나여야 함 (받은 값: ${value})`);
81
87
  }
88
+ if (name === 'locale' && !VALID_LOCALES.includes(value)) {
89
+ throw new Error(
90
+ `--locale 은 ${VALID_LOCALES.join('/')} 중 하나여야 함 (받은 값: ${value}). ` +
91
+ `'ko' 선택 시 Pretendard 폰트가 globals.css 에 자동 적용됩니다. ` +
92
+ `(주의: --locales (복수) 는 i18n 활성화 시 생성할 locale 코드 목록 — 다른 의미).`,
93
+ );
94
+ }
82
95
  if (name === 'css' && !CSS_FRAMEWORKS_SUPPORTED.includes(value)) {
83
96
  // planned 값은 '곧 옵니다' 신호로 분기 — 사용자 의도가 더 명확히 전달.
84
97
  if (CSS_FRAMEWORKS_PLANNED.includes(value)) {
@@ -320,7 +320,7 @@ export async function createProject(options = {}) {
320
320
 
321
321
  if (platform === 'flutter') {
322
322
  await generateFlutter(targetDir, projectName, theme, cssFramework, themeBase);
323
- await finalizeProject(targetDir, { dryRun: options.dryRun });
323
+ await finalizeProject(targetDir, { dryRun: options.dryRun, gitInit: options.gitInit, locale: options.locale });
324
324
  console.log(`\n✅ ${projectName} Flutter 프로젝트가 생성되었습니다!`);
325
325
  console.log(`\n cd ${projectName}`);
326
326
  console.log(' flutter pub get');
@@ -355,7 +355,7 @@ export async function createProject(options = {}) {
355
355
  });
356
356
  }
357
357
 
358
- await finalizeProject(targetDir, { dryRun: options.dryRun });
358
+ await finalizeProject(targetDir, { dryRun: options.dryRun, gitInit: options.gitInit, locale: options.locale });
359
359
 
360
360
  if (options.dryRun) {
361
361
  const files = await listAllFiles(targetDir);
@@ -401,7 +401,7 @@ export async function createProject(options = {}) {
401
401
  });
402
402
  }
403
403
 
404
- await finalizeProject(targetDir, { dryRun: options.dryRun });
404
+ await finalizeProject(targetDir, { dryRun: options.dryRun, gitInit: options.gitInit, locale: options.locale });
405
405
 
406
406
  if (options.dryRun) {
407
407
  const files = await listAllFiles(targetDir);
@@ -1650,15 +1650,38 @@ async function stripTailwindFromPrettier(prettierPath) {
1650
1650
  * strip 한다(없으면 `.npmignore` fallback 으로 사용). 사용자에게 도착하지 않으니
1651
1651
  * 템플릿엔 `gitignore` 로 두고 복사 직후 dot-prefix 를 붙인다.
1652
1652
  *
1653
+ * gitInit 옵션:
1654
+ * - undefined (auto): parent 가 이미 git tree 안이면 스킵, 아니면 init. nested .git
1655
+ * 충돌 방지 — 기존 monorepo / 사용자 작업 트리 안에서 호출 시 안전.
1656
+ * - true: 무조건 init (parent 가 git tree 안이어도). nested 가 의도된 경우.
1657
+ * - false: 무조건 스킵.
1658
+ *
1653
1659
  * git init 은 dry-run 에서는 스킵하고, 실패해도(git 미설치 등) 조용히 넘어간다.
1654
1660
  */
1655
- async function finalizeProject(targetDir, { dryRun = false } = {}) {
1661
+ async function finalizeProject(targetDir, { dryRun = false, gitInit, locale } = {}) {
1656
1662
  // 모노레포 / sub-app 까지 모든 `gitignore` 를 `.gitignore` 로 rename.
1657
1663
  // root 만 처리하면 apps/<name>/gitignore 가 그대로 남아 node_modules/dist 가 staged 된다 (v0.93.0 버그).
1658
1664
  await renameAllGitignoreRecursive(targetDir);
1659
1665
 
1666
+ // locale 후처리 — 한국어면 globals.css 들에 Pretendard 자동 적용 (Aifice 피드백 3.1).
1667
+ // dryRun 이면 skip — globals.css 가 디스크에 안 써졌을 수 있다.
1668
+ if (!dryRun && locale === 'ko') {
1669
+ await injectLocaleFont(targetDir, 'ko');
1670
+ }
1671
+
1660
1672
  if (dryRun) return;
1661
1673
 
1674
+ const decision = resolveGitInit(targetDir, gitInit);
1675
+ if (!decision.init) {
1676
+ if (decision.reason === 'nested') {
1677
+ console.log(
1678
+ `\n ℹ 이미 git tree 안이라 .git 초기화를 스킵했습니다 (parent: ${decision.parentRepo}).\n` +
1679
+ ` nested git repo 가 의도된 경우 --git-init 으로 강제할 수 있습니다.`,
1680
+ );
1681
+ }
1682
+ return;
1683
+ }
1684
+
1662
1685
  try {
1663
1686
  execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
1664
1687
  } catch {
@@ -1666,6 +1689,95 @@ async function finalizeProject(targetDir, { dryRun = false } = {}) {
1666
1689
  }
1667
1690
  }
1668
1691
 
1692
+ /**
1693
+ * `locale` 옵션 후처리 — locale=ko 면 스캐폴드된 모든 globals.css 에 Pretendard 폰트 적용.
1694
+ *
1695
+ * 한국어 사용자가 sh-ui 를 init 한 직후 "Pretendard 로 교체" 가 거의 100% 첫 작업이라
1696
+ * (Aifice 피드백 3.1), 이를 옵션 하나로 자동화. 적용 방식:
1697
+ * - external-imports 마커 안에 Pretendard CDN @import 라인 prepend (Tailwind import 보다 먼저)
1698
+ * - 파일 끝에 `body { font-family: 'Pretendard Variable', ... }` rule 추가
1699
+ *
1700
+ * idempotent: 이미 'Pretendard Variable' 문자열이 있으면 skip.
1701
+ * monorepo 도 안전 — targetDir 하위 모든 globals.css 를 재귀 스캔 (node_modules / .git / _arch 제외).
1702
+ * Flutter 는 globals.css 가 없어 자동 no-op.
1703
+ */
1704
+ async function injectLocaleFont(targetDir, locale) {
1705
+ if (locale !== 'ko') return;
1706
+
1707
+ const PRETENDARD_IMPORT =
1708
+ "@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css');\n";
1709
+ const FONT_FAMILY_RULE =
1710
+ "\n/* sh-ui:locale=ko — Pretendard 기본 적용. 사용자 정의로 override 가능. */\n" +
1711
+ "body { font-family: 'Pretendard Variable', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; }\n";
1712
+ const END_MARKER = '/* sh-ui:external-imports-end */';
1713
+
1714
+ const targets = [];
1715
+ async function walk(dir) {
1716
+ let entries;
1717
+ try {
1718
+ entries = await fs.readdir(dir, { withFileTypes: true });
1719
+ } catch {
1720
+ return;
1721
+ }
1722
+ for (const e of entries) {
1723
+ const full = path.join(dir, e.name);
1724
+ if (e.isDirectory()) {
1725
+ if (['node_modules', '.git', '.next', 'dist', '_arch'].includes(e.name)) continue;
1726
+ await walk(full);
1727
+ } else if (e.isFile() && e.name === 'globals.css') {
1728
+ targets.push(full);
1729
+ }
1730
+ }
1731
+ }
1732
+ await walk(targetDir);
1733
+
1734
+ for (const abs of targets) {
1735
+ let css = await fs.readFile(abs, 'utf-8');
1736
+ if (css.includes("'Pretendard Variable'")) continue;
1737
+ if (!css.includes(END_MARKER)) continue; // 우리 마커 없는 파일은 안전상 건드리지 않음
1738
+ css = css.replace(END_MARKER, `${PRETENDARD_IMPORT}${END_MARKER}`);
1739
+ css += FONT_FAMILY_RULE;
1740
+ await fs.writeFile(abs, css);
1741
+ }
1742
+ }
1743
+
1744
+ /**
1745
+ * git init 실행 여부 결정. auto 모드는 parent 가 git tree 안이면 스킵 — nested .git
1746
+ * 충돌 방지. 명시 override (true/false) 가 있으면 그대로 따른다.
1747
+ *
1748
+ * 감지: `git -C <parentDir> rev-parse --is-inside-work-tree` 출력이 "true" 면 안.
1749
+ * git 미설치 / 권한 문제 등으로 명령이 실패하면 트리 밖으로 간주 (안전 디폴트: init 시도).
1750
+ */
1751
+ function resolveGitInit(targetDir, override) {
1752
+ if (override === false) return { init: false, reason: 'explicit-skip' };
1753
+ if (override === true) return { init: true, reason: 'explicit-force' };
1754
+
1755
+ const parentDir = path.dirname(targetDir);
1756
+ try {
1757
+ const result = execSync('git rev-parse --is-inside-work-tree', {
1758
+ cwd: parentDir,
1759
+ stdio: ['ignore', 'pipe', 'ignore'],
1760
+ })
1761
+ .toString()
1762
+ .trim();
1763
+ if (result === 'true') {
1764
+ let parentRepo = parentDir;
1765
+ try {
1766
+ parentRepo = execSync('git rev-parse --show-toplevel', {
1767
+ cwd: parentDir,
1768
+ stdio: ['ignore', 'pipe', 'ignore'],
1769
+ })
1770
+ .toString()
1771
+ .trim() || parentDir;
1772
+ } catch {}
1773
+ return { init: false, reason: 'nested', parentRepo };
1774
+ }
1775
+ } catch {
1776
+ // parent 가 git tree 밖 (또는 git 미설치) — 안전 디폴트: init 시도.
1777
+ }
1778
+ return { init: true, reason: 'auto' };
1779
+ }
1780
+
1669
1781
  /**
1670
1782
  * CLAUDE.md 의 `{{PLATFORM_APP_DESCRIPTION}}` 치환용 문장 — AI 에이전트에게 어떤 플랫폼인지
1671
1783
  * 정확히 전달해서 잘못된 컨벤션 (예: vite 프로젝트에 App Router 가정) 적용 방지.
@@ -30,8 +30,11 @@ 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 디렉토리 덮어쓰기 + 모노레포 기본값 자동 채택
34
35
  --dry-run 파일을 쓰지 않고 작성될 파일 목록만 출력
36
+ --no-git-init git init 스킵 (기존 git tree 안에서 호출 시 nested .git 충돌 방지). 기본 auto — parent 가 git tree 안이면 자동 스킵
37
+ --git-init git init 무조건 실행 (nested 가 의도된 경우)
35
38
  -h, --help 이 도움말
36
39
 
37
40
  예 (대화형):
@@ -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
 
@@ -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
  /**
@@ -8,8 +8,8 @@
8
8
 
9
9
  const NEUTRAL_LIGHT = {
10
10
  'background': '#FFFFFF',
11
- 'background-subtle': '#FAFAFA',
12
- 'background-muted': '#F5F5F5',
11
+ 'background-subtle': '#F5F5F5',
12
+ 'background-muted': '#E5E5E5',
13
13
  'background-inverse': '#0A0A0A',
14
14
  'foreground': '#0A0A0A',
15
15
  'foreground-muted': '#525252',
@@ -20,6 +20,10 @@ const NEUTRAL_LIGHT = {
20
20
  'primary': '#171717',
21
21
  'primary-foreground': '#FAFAFA',
22
22
  'primary-hover': '#262626',
23
+ // accent — 디폴트는 primary 와 동일. 사용자가 분리하고 싶을 때만 별 값으로 override.
24
+ 'accent': '#171717',
25
+ 'accent-foreground': '#FAFAFA',
26
+ 'accent-hover': '#262626',
23
27
  'danger': '#DC2626',
24
28
  'danger-foreground': '#FFFFFF',
25
29
  'danger-hover': '#B91C1C',
@@ -30,10 +34,10 @@ const NEUTRAL_LIGHT = {
30
34
  'warning-foreground': '#FFFFFF',
31
35
  'info': '#2563EB',
32
36
  'info-foreground': '#FFFFFF',
33
- 'sidebar-bg': '#FAFAFA',
37
+ 'sidebar-bg': '#F5F5F5',
34
38
  'sidebar-fg': '#0A0A0A',
35
39
  'sidebar-border': '#E5E5E5',
36
- 'sidebar-accent': '#F5F5F5',
40
+ 'sidebar-accent': '#E5E5E5',
37
41
  'sidebar-accent-fg': '#0A0A0A',
38
42
  };
39
43
 
@@ -51,6 +55,10 @@ const NEUTRAL_DARK = {
51
55
  'primary': '#FAFAFA',
52
56
  'primary-foreground': '#171717',
53
57
  'primary-hover': '#E5E5E5',
58
+ // accent — 디폴트는 primary 와 동일. 사용자가 분리하고 싶을 때만 별 값으로 override.
59
+ 'accent': '#FAFAFA',
60
+ 'accent-foreground': '#171717',
61
+ 'accent-hover': '#E5E5E5',
54
62
  'danger': '#DC2626',
55
63
  'danger-foreground': '#FFFFFF',
56
64
  'danger-hover': '#EF4444',
@@ -79,8 +87,8 @@ export const THEME_PRESETS = {
79
87
  label: '슬레이트 — 차분한 슬레이트 + 인디고 (정보 밀도)',
80
88
  light: {
81
89
  'background': '#FFFFFF',
82
- 'background-subtle': '#F8FAFC',
83
- 'background-muted': '#F1F5F9',
90
+ 'background-subtle': '#F1F5F9',
91
+ 'background-muted': '#E2E8F0',
84
92
  'background-inverse': '#0F172A',
85
93
  'foreground': '#0F172A',
86
94
  'foreground-muted': '#475569',
@@ -91,6 +99,9 @@ export const THEME_PRESETS = {
91
99
  'primary': '#4F46E5',
92
100
  'primary-foreground': '#FFFFFF',
93
101
  'primary-hover': '#4338CA',
102
+ 'accent': '#4F46E5',
103
+ 'accent-foreground': '#FFFFFF',
104
+ 'accent-hover': '#4338CA',
94
105
  'danger': '#DC2626',
95
106
  'danger-foreground': '#FFFFFF',
96
107
  'danger-hover': '#B91C1C',
@@ -101,10 +112,10 @@ export const THEME_PRESETS = {
101
112
  'warning-foreground': '#FFFFFF',
102
113
  'info': '#2563EB',
103
114
  'info-foreground': '#FFFFFF',
104
- 'sidebar-bg': '#F8FAFC',
115
+ 'sidebar-bg': '#F1F5F9',
105
116
  'sidebar-fg': '#0F172A',
106
117
  'sidebar-border': '#E2E8F0',
107
- 'sidebar-accent': '#F1F5F9',
118
+ 'sidebar-accent': '#E2E8F0',
108
119
  'sidebar-accent-fg': '#0F172A',
109
120
  },
110
121
  dark: {
@@ -121,6 +132,11 @@ export const THEME_PRESETS = {
121
132
  'primary': '#818CF8',
122
133
  'primary-foreground': '#1E1B4B',
123
134
  'primary-hover': '#A5B4FC',
135
+ // accent chroma 상향 — primary 보다 한 단계 lighter+saturated 한 indigo-300.
136
+ // 어두운 배경 위에서 highlight (링크/선택 상태) 가독성 ↑ (v0.109.0+).
137
+ 'accent': '#A5B4FC',
138
+ 'accent-foreground': '#1E1B4B',
139
+ 'accent-hover': '#C7D2FE',
124
140
  'danger': '#F87171',
125
141
  'danger-foreground': '#450A0A',
126
142
  'danger-hover': '#FCA5A5',
@@ -149,12 +165,19 @@ export const THEME_PRESETS = {
149
165
  'primary': '#E11D48',
150
166
  'primary-foreground': '#FFF1F2',
151
167
  'primary-hover': '#BE123C',
168
+ 'accent': '#E11D48',
169
+ 'accent-foreground': '#FFF1F2',
170
+ 'accent-hover': '#BE123C',
152
171
  },
153
172
  dark: {
154
173
  ...NEUTRAL_DARK,
155
174
  'primary': '#FB7185',
156
175
  'primary-foreground': '#4C0519',
157
176
  'primary-hover': '#FDA4AF',
177
+ // accent chroma 상향 — rose-300 (v0.109.0+).
178
+ 'accent': '#FDA4AF',
179
+ 'accent-foreground': '#4C0519',
180
+ 'accent-hover': '#FECDD3',
158
181
  },
159
182
  radius: 0.75,
160
183
  // 큰 모서리 + 큼직한 컨트롤 — 소비자 앱 / 캐주얼 인상
@@ -167,12 +190,19 @@ export const THEME_PRESETS = {
167
190
  'primary': '#059669',
168
191
  'primary-foreground': '#ECFDF5',
169
192
  'primary-hover': '#047857',
193
+ 'accent': '#059669',
194
+ 'accent-foreground': '#ECFDF5',
195
+ 'accent-hover': '#047857',
170
196
  },
171
197
  dark: {
172
198
  ...NEUTRAL_DARK,
173
199
  'primary': '#34D399',
174
200
  'primary-foreground': '#022C22',
175
201
  'primary-hover': '#6EE7B7',
202
+ // accent chroma 상향 — emerald-300 (v0.109.0+).
203
+ 'accent': '#6EE7B7',
204
+ 'accent-foreground': '#022C22',
205
+ 'accent-hover': '#A7F3D0',
176
206
  },
177
207
  radius: 0.5,
178
208
  },
@@ -183,12 +213,19 @@ export const THEME_PRESETS = {
183
213
  'primary': '#7C3AED',
184
214
  'primary-foreground': '#F5F3FF',
185
215
  'primary-hover': '#6D28D9',
216
+ 'accent': '#7C3AED',
217
+ 'accent-foreground': '#F5F3FF',
218
+ 'accent-hover': '#6D28D9',
186
219
  },
187
220
  dark: {
188
221
  ...NEUTRAL_DARK,
189
222
  'primary': '#A78BFA',
190
223
  'primary-foreground': '#1E1B4B',
191
224
  'primary-hover': '#C4B5FD',
225
+ // accent chroma 상향 — violet-300 (v0.109.0+).
226
+ 'accent': '#C4B5FD',
227
+ 'accent-foreground': '#1E1B4B',
228
+ 'accent-hover': '#DDD6FE',
192
229
  },
193
230
  radius: 0.625,
194
231
  // 살짝 큰 컨트롤 + 진한 강조 보더 (creative/디자인 도구 인상)