sh-ui-cli 0.80.1 → 0.82.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 (44) hide show
  1. package/data/changelog/versions.json +30 -0
  2. package/data/registry/react/components/button/index.tailwind.tsx +1 -1
  3. package/data/registry/react/components/button/styles.css +1 -0
  4. package/data/registry/react/components/button/styles.css.ts +1 -0
  5. package/data/registry/react/components/button/styles.module.css +1 -0
  6. package/data/registry/react/components/code-editor/index.module.tsx +0 -2
  7. package/data/registry/react/components/code-editor/index.tailwind.tsx +1 -2
  8. package/data/registry/react/components/code-editor/index.tsx +0 -2
  9. package/data/registry/react/components/combobox/index.tailwind.tsx +1 -1
  10. package/data/registry/react/components/combobox/styles.css +1 -1
  11. package/data/registry/react/components/combobox/styles.module.css +1 -1
  12. package/data/registry/react/components/context-menu/index.tailwind.tsx +1 -1
  13. package/data/registry/react/components/context-menu/styles.css +1 -1
  14. package/data/registry/react/components/context-menu/styles.module.css +1 -1
  15. package/data/registry/react/components/dropdown-menu/index.module.tsx +22 -7
  16. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +18 -3
  17. package/data/registry/react/components/dropdown-menu/index.tsx +24 -7
  18. package/data/registry/react/components/dropdown-menu/styles.css +1 -1
  19. package/data/registry/react/components/dropdown-menu/styles.module.css +1 -1
  20. package/data/registry/react/components/markdown-editor/index.module.tsx +1 -1
  21. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +1 -1
  22. package/data/registry/react/components/markdown-editor/index.tsx +1 -1
  23. package/data/registry/react/components/select/index.tailwind.tsx +1 -1
  24. package/data/registry/react/components/select/styles.css +1 -1
  25. package/data/registry/react/components/select/styles.module.css +1 -1
  26. package/data/registry/react/components/sidebar/index.module.tsx +18 -9
  27. package/data/registry/react/components/sidebar/index.tailwind.tsx +21 -6
  28. package/data/registry/react/components/sidebar/index.tsx +27 -8
  29. package/data/registry/react/components/sidebar/styles.css +15 -0
  30. package/data/registry/react/components/sidebar/styles.module.css +12 -0
  31. package/data/registry/react/tokens-used.json +13 -13
  32. package/data/tokens/src/primitives.json +3 -3
  33. package/package.json +1 -1
  34. package/src/add.mjs +13 -1
  35. package/src/create/generator.js +46 -1
  36. package/templates/monorepo/packages/typescript-config/base.json +0 -1
  37. package/templates/nextjs-standalone/_arch/flat/lib/styles/tokens.css +2 -2
  38. package/templates/nextjs-standalone/_arch/flat/tsconfig.json +0 -1
  39. package/templates/nextjs-standalone/_arch/fsd/src/shared/styles/tokens.css +2 -2
  40. package/templates/nextjs-standalone/_arch/fsd/tsconfig.json +0 -1
  41. package/templates/nextjs-standalone/_arch/mes/src/lib/styles/tokens.css +2 -2
  42. package/templates/nextjs-standalone/_arch/mes/tsconfig.json +0 -1
  43. package/templates/ui-app-template/src/styles/globals.css +10 -0
  44. package/templates/ui-app-template/src/styles/tokens.css +2 -2
@@ -2,6 +2,36 @@
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.82.0",
7
+ "date": "2026-05-13",
8
+ "title": "atlas P2 잔여 — Sidebar topbar 정렬 + 메뉴 사이즈 디폴트 다운 + DX 정리",
9
+ "type": "minor",
10
+ "highlights": [
11
+ "**`SidebarHeader` 에 `align=\"topbar\"` + `divider` prop 신설 (B8)** — admin/dashboard 의 좌 Sidebar 헤더와 우 Topbar 구분선이 4~5px 어긋나던 정렬 hack (`!h-14 !p-0 !flex-row` + SidebarSeparator 제거 + `border-b` 직접 박는 류) 영구 해결. `align=\"topbar\"` 가 56px(`3.5rem`) 가로 행 + 가로 패딩만 적용, `divider` 가 `border-bottom: 1px solid var(--sidebar-border)` 박는다. 두 옵션 조합 시 우 Topbar 의 border-bottom 과 픽셀 정렬.",
12
+ "**`SidebarMenuButton` / `SidebarMenuSubButton` / `SidebarCollapsibleTrigger` 의 size 디폴트 `md` → `sm` (D3)** — 일반 사이드바 패턴(28~32px) 톤에 맞춤. v0.81.0 의 `--control-md` 40→36px 다운과 결합돼 사이드바가 데스크탑 정보 밀집 화면에서 후하게 느껴지던 회귀 차단. atlas 실측에서 size 명시 24건 전부 `sm` 이었던 신호 반영.",
13
+ "**`create_project` 직후 출력에 베이스 컴포넌트 추가 힌트 1줄 (B6)** — 기존엔 `cd / pnpm install / pnpm dev` 3줄만 출력. monorepo 는 `cd .../packages/ui/ui-core` + `npx sh-ui-cli add button card input dialog`, standalone 은 `npx sh-ui-cli add ...` 만. MCP 사용자에게는 `sh_ui_add_component` 한 줄도 같이.",
14
+ "**`CodeEditor` 의 `language` 타입 `\"text\"` / `\"plaintext\"` 중복 단일화 (B3)** — 의미상 동일한 두 값이 공존해 사용자가 어느 쪽을 쓸지 모호하던 결함. `\"plaintext\"` 제거, `\"text\"` 만 유지. `language=\"plaintext\"` 사용처는 `\"text\"` 로 교체."
15
+ ],
16
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.82.0"
17
+ },
18
+ {
19
+ "version": "0.81.0",
20
+ "date": "2026-05-13",
21
+ "title": "atlas P0/P1 일괄 — monorepo create_project · registry · 디자인 톤",
22
+ "type": "minor",
23
+ "highlights": [
24
+ "**monorepo `create_project` 자동완성 흐름** — (1) ui-core `sh-ui.config.json` 의 `cssFramework` 가 항상 `plain` 으로 emit 되던 결함 수정 (호출 시점 인자가 ui-core/ui-app 양쪽에 반영), (2) ui-app `globals.css` 의 `@source` 에 ui-core 경로 누락 수정 (`../../../../ui-core/src/**/*.{ts,tsx}` 자동 박힘), (3) utility CSS 5종 (`base · focus-ring · animations · breakpoints · z-index`) 을 스캐폴드 시점에 ui-core 에 자동 emit + ui-app `globals.css` 에 sentinel-bounded `@import` 자동 박기. `create → dev` 가 매뉴얼 보정 없이 굴러감.",
25
+ "**registry `.tsx` cross-import 잔존 6건 일괄 제거** — v0.80.1 에서 sidebar/plain 한 개만 고쳤던 누락 후속. sidebar `tailwind/module` + markdown-editor `plain/tailwind/module` + docs 듀얼카피 markdown-editor 의 `../code-editor/index.tsx` / `../popover/index.tsx` 를 확장자 없는 형태로 통일. Next.js + TS 디폴트(`allowImportingTsExtensions: false`) 에서 빌드 깨지던 결함 해소.",
26
+ "**DropdownMenu / Select / Combobox / ContextMenu z-index `--z-dropdown` (200) → `--z-popover` (500)** — Sidebar offcanvas drawer 의 `--z-overlay` (300) 아래로 깔려 안 보이던 결함 수정. 드로어 안에 있는 메뉴/콤보가 정상 위로 뜸. 듀얼 카피본까지 일괄.",
27
+ "**DropdownMenuLabel free-standing 자동 self-wrap** — Base UI 의 `Menu.GroupLabel` 이 `Menu.Group` 부재 시 \"MenuGroupRootContext is missing\" 으로 런타임 throw 하던 마이그레이션 함정 해소. 자체 Context 로 부모 Group 감지, 없으면 Label 이 스스로 `Menu.Group` 으로 wrap. 명시적 Group 안에 두면 중첩 없이 그대로 동작 — shadcn 코드 그대로 옮겨와도 OK.",
28
+ "**디폴트 control 사이즈 다운 (40 → 36)** — `--control-md` 가 데스크탑 SaaS/대시보드 톤에 비해 통통하다는 atlas 측정(`size='sm'` 24건, `size='lg'` 0건) 반영. `--control-sm/md/lg` 를 `2rem / 2.25rem / 2.5rem` (32/36/40) 로. 모바일 터치 점프(44) 는 그대로. 기존 프로젝트의 `tokens.css` 는 미변경 — 신규 프로젝트만 새 디폴트.",
29
+ "**Button `white-space: nowrap`** — 좁은 컨테이너에서 한글 라벨이 줄바꿈되며 버튼 높이 60px+ 로 부풀던 결함 수정. 4 변종 (plain/tailwind/module/vanilla-extract) + 듀얼카피 일괄.",
30
+ "**`noUncheckedIndexedAccess` 디폴트 off** — 스캐폴드의 `typescript-config/base.json` + nextjs-standalone 3 arch 의 tsconfig 에서 제거. registry 컴포넌트들이 index access 결과를 narrowing 없이 쓰는 패턴이라 신규 사용자가 첫 `typecheck` 부터 실패하던 결함 해소.",
31
+ "**`add tokens` 가 ui-core 에 대해 던지는 에러 메시지에 가이드 추가** — `paths.tokens 가 설정에 없습니다` 만 던지던 것을 v0.65 layout 설명 + `cwd` 를 `packages/ui/ui-apps/ui-<name>` 로 바꾸라는 한 줄 안내로 보강."
32
+ ],
33
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.81.0"
34
+ },
5
35
  {
6
36
  "version": "0.80.1",
7
37
  "date": "2026-05-12",
@@ -3,7 +3,7 @@ import { cn } from "@SH_UI_UTILS@";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
4
 
5
5
  const buttonVariants = cva(
6
- "inline-flex items-center justify-center gap-[var(--space-2)] border border-transparent rounded-[var(--radius)] font-medium leading-none cursor-pointer select-none transition-[background-color,color,border-color] duration-[var(--duration-fast)] disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 active:scale-[0.97]",
6
+ "inline-flex items-center justify-center gap-[var(--space-2)] border border-transparent rounded-[var(--radius)] font-medium leading-none whitespace-nowrap cursor-pointer select-none transition-[background-color,color,border-color] duration-[var(--duration-fast)] disabled:opacity-[var(--opacity-disabled)] disabled:pointer-events-none focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 active:scale-[0.97]",
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -7,6 +7,7 @@
7
7
  border-radius: var(--radius);
8
8
  font-weight: var(--weight-medium);
9
9
  line-height: 1;
10
+ white-space: nowrap;
10
11
  cursor: pointer;
11
12
  transition: background-color var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast),
12
13
  transform 80ms ease-out, filter 80ms;
@@ -9,6 +9,7 @@ export const button = style({
9
9
  borderRadius: "var(--radius)",
10
10
  fontWeight: "var(--weight-medium)",
11
11
  lineHeight: 1,
12
+ whiteSpace: "nowrap",
12
13
  cursor: "pointer",
13
14
  transition:
14
15
  "background-color var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast), transform 80ms ease-out, filter 80ms",
@@ -7,6 +7,7 @@
7
7
  border-radius: var(--radius);
8
8
  font-weight: var(--weight-medium);
9
9
  line-height: 1;
10
+ white-space: nowrap;
10
11
  cursor: pointer;
11
12
  transition: background-color var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast),
12
13
  transform 80ms ease-out, filter 80ms;
@@ -15,7 +15,6 @@ import styles from "./styles.module.css";
15
15
  import { cn } from "@SH_UI_UTILS@";
16
16
  export type CodeEditorLanguage =
17
17
  | "text"
18
- | "plaintext"
19
18
  | "javascript"
20
19
  | "typescript"
21
20
  | "jsx"
@@ -85,7 +84,6 @@ function languageExtension(language: CodeEditorLanguage): Extension {
85
84
  case "yaml":
86
85
  return yaml();
87
86
  case "text":
88
- case "plaintext":
89
87
  default:
90
88
  return [];
91
89
  }
@@ -13,7 +13,7 @@ import { yaml } from "@codemirror/lang-yaml";
13
13
 
14
14
  import { cn } from "@SH_UI_UTILS@";
15
15
  export type CodeEditorLanguage =
16
- | "text" | "plaintext" | "javascript" | "typescript" | "jsx" | "tsx"
16
+ | "text" | "javascript" | "typescript" | "jsx" | "tsx"
17
17
  | "json" | "css" | "html" | "markdown" | "yaml";
18
18
 
19
19
  export interface CodeEditorProps {
@@ -45,7 +45,6 @@ function languageExtension(language: CodeEditorLanguage): Extension {
45
45
  case "markdown": return markdown();
46
46
  case "yaml": return yaml();
47
47
  case "text":
48
- case "plaintext":
49
48
  default: return [];
50
49
  }
51
50
  }
@@ -15,7 +15,6 @@ import "./styles.css";
15
15
  import { cn } from "@SH_UI_UTILS@";
16
16
  export type CodeEditorLanguage =
17
17
  | "text"
18
- | "plaintext"
19
18
  | "javascript"
20
19
  | "typescript"
21
20
  | "jsx"
@@ -85,7 +84,6 @@ function languageExtension(language: CodeEditorLanguage): Extension {
85
84
  case "yaml":
86
85
  return yaml();
87
86
  case "text":
88
- case "plaintext":
89
87
  default:
90
88
  return [];
91
89
  }
@@ -53,7 +53,7 @@ export const ComboboxContent = React.forwardRef<
53
53
  return (
54
54
  <BaseCombobox.Portal container={container}>
55
55
  <BaseCombobox.Positioner
56
- className="z-[var(--z-dropdown)] outline-none w-[var(--anchor-width)]"
56
+ className="z-[var(--z-popover)] outline-none w-[var(--anchor-width)]"
57
57
  sideOffset={sideOffset}
58
58
  align="start"
59
59
  >
@@ -31,7 +31,7 @@
31
31
 
32
32
  /* ───── Popup ───── */
33
33
  .sh-ui-combobox__positioner {
34
- z-index: var(--z-dropdown);
34
+ z-index: var(--z-popover);
35
35
  outline: none;
36
36
  width: var(--anchor-width);
37
37
  }
@@ -31,7 +31,7 @@
31
31
 
32
32
  /* ───── Popup ───── */
33
33
  .combobox__positioner {
34
- z-index: var(--z-dropdown);
34
+ z-index: var(--z-popover);
35
35
  outline: none;
36
36
  width: var(--anchor-width);
37
37
  }
@@ -29,7 +29,7 @@ export const ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuCo
29
29
  function ContextMenuContent({ className, children, container, ...props }, ref) {
30
30
  return (
31
31
  <BaseContextMenu.Portal container={container}>
32
- <BaseContextMenu.Positioner className="outline-none z-[var(--z-dropdown)]">
32
+ <BaseContextMenu.Positioner className="outline-none z-[var(--z-popover)]">
33
33
  <BaseContextMenu.Popup ref={ref} className={cn(contentClasses, className)} {...props}>
34
34
  {children}
35
35
  </BaseContextMenu.Popup>
@@ -10,7 +10,7 @@
10
10
 
11
11
  .sh-ui-cm__positioner {
12
12
  outline: none;
13
- z-index: var(--z-dropdown);
13
+ z-index: var(--z-popover);
14
14
  }
15
15
 
16
16
  .sh-ui-cm__content {
@@ -10,7 +10,7 @@
10
10
 
11
11
  .cm__positioner {
12
12
  outline: none;
13
- z-index: var(--z-dropdown);
13
+ z-index: var(--z-popover);
14
14
  }
15
15
 
16
16
  .cm__content {
@@ -149,18 +149,26 @@ export const DropdownMenuRadioItem = React.forwardRef<
149
149
  );
150
150
  });
151
151
 
152
- /* ───────── Group / Label ───────── */
152
+ /* ───────── Group / Label ─────────
153
+ *
154
+ * Base UI Menu.GroupLabel 은 Menu.Group 안에서만 동작 — free-standing 시 throw.
155
+ * DropdownMenuLabel 이 부모 Group 부재 시 자체 Group 으로 self-wrap.
156
+ */
157
+
158
+ const InDmGroupContext = React.createContext(false);
153
159
 
154
160
  export const DropdownMenuGroup = React.forwardRef<
155
161
  HTMLDivElement,
156
162
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Group>>
157
163
  >(function DropdownMenuGroup({ className, ...props }, ref) {
158
164
  return (
159
- <BaseMenu.Group
160
- ref={ref}
161
- className={cn(styles.dm__group, className)}
162
- {...props}
163
- />
165
+ <InDmGroupContext.Provider value={true}>
166
+ <BaseMenu.Group
167
+ ref={ref}
168
+ className={cn(styles.dm__group, className)}
169
+ {...props}
170
+ />
171
+ </InDmGroupContext.Provider>
164
172
  );
165
173
  });
166
174
 
@@ -168,13 +176,20 @@ export const DropdownMenuLabel = React.forwardRef<
168
176
  HTMLDivElement,
169
177
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.GroupLabel>>
170
178
  >(function DropdownMenuLabel({ className, ...props }, ref) {
171
- return (
179
+ const inGroup = React.useContext(InDmGroupContext);
180
+ const label = (
172
181
  <BaseMenu.GroupLabel
173
182
  ref={ref}
174
183
  className={cn(styles.dm__label, className)}
175
184
  {...props}
176
185
  />
177
186
  );
187
+ if (inGroup) return label;
188
+ return (
189
+ <InDmGroupContext.Provider value={true}>
190
+ <BaseMenu.Group>{label}</BaseMenu.Group>
191
+ </InDmGroupContext.Provider>
192
+ );
178
193
  });
179
194
 
180
195
  /* ───────── Separator ─────────
@@ -52,7 +52,7 @@ export const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenu
52
52
  return (
53
53
  <BaseMenu.Portal container={container}>
54
54
  <BaseMenu.Positioner
55
- className="outline-none z-[var(--z-dropdown)]"
55
+ className="outline-none z-[var(--z-popover)]"
56
56
  side={side}
57
57
  align={align}
58
58
  sideOffset={sideOffset}
@@ -120,18 +120,27 @@ export const DropdownMenuRadioItem = React.forwardRef<
120
120
  );
121
121
  });
122
122
 
123
+ // Base UI Menu.GroupLabel 은 Menu.Group 안에 있어야만 동작 — free-standing 시 throw.
124
+ // DropdownMenuLabel 이 부모 Group 부재 시 자체 Group 으로 self-wrap 하도록 Context 로 감지.
125
+ const InDmGroupContext = React.createContext(false);
126
+
123
127
  export const DropdownMenuGroup = React.forwardRef<
124
128
  HTMLDivElement,
125
129
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Group>>
126
130
  >(function DropdownMenuGroup({ className, ...props }, ref) {
127
- return <BaseMenu.Group ref={ref} className={cn("p-0", className)} {...props} />;
131
+ return (
132
+ <InDmGroupContext.Provider value={true}>
133
+ <BaseMenu.Group ref={ref} className={cn("p-0", className)} {...props} />
134
+ </InDmGroupContext.Provider>
135
+ );
128
136
  });
129
137
 
130
138
  export const DropdownMenuLabel = React.forwardRef<
131
139
  HTMLDivElement,
132
140
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.GroupLabel>>
133
141
  >(function DropdownMenuLabel({ className, ...props }, ref) {
134
- return (
142
+ const inGroup = React.useContext(InDmGroupContext);
143
+ const label = (
135
144
  <BaseMenu.GroupLabel
136
145
  ref={ref}
137
146
  className={cn(
@@ -141,6 +150,12 @@ export const DropdownMenuLabel = React.forwardRef<
141
150
  {...props}
142
151
  />
143
152
  );
153
+ if (inGroup) return label;
154
+ return (
155
+ <InDmGroupContext.Provider value={true}>
156
+ <BaseMenu.Group>{label}</BaseMenu.Group>
157
+ </InDmGroupContext.Provider>
158
+ );
144
159
  });
145
160
 
146
161
  export const DropdownMenuSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
@@ -149,18 +149,28 @@ export const DropdownMenuRadioItem = React.forwardRef<
149
149
  );
150
150
  });
151
151
 
152
- /* ───────── Group / Label ───────── */
152
+ /* ───────── Group / Label ─────────
153
+ *
154
+ * Base UI Menu.GroupLabel 은 Menu.Group 안에 있어야만 동작 (없으면 런타임 throw —
155
+ * "MenuGroupRootContext is missing"). shadcn 등에선 Label 이 free-standing 으로도
156
+ * 쓰여서 마이그레이션 시 사용자가 잘 찔린다. DropdownMenuLabel 이 부모 Group 부재 시
157
+ * 자체로 Group 으로 self-wrap 하도록 Context 로 감지.
158
+ */
159
+
160
+ const InDmGroupContext = React.createContext(false);
153
161
 
154
162
  export const DropdownMenuGroup = React.forwardRef<
155
163
  HTMLDivElement,
156
164
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Group>>
157
165
  >(function DropdownMenuGroup({ className, ...props }, ref) {
158
166
  return (
159
- <BaseMenu.Group
160
- ref={ref}
161
- className={cn("sh-ui-dm__group", className)}
162
- {...props}
163
- />
167
+ <InDmGroupContext.Provider value={true}>
168
+ <BaseMenu.Group
169
+ ref={ref}
170
+ className={cn("sh-ui-dm__group", className)}
171
+ {...props}
172
+ />
173
+ </InDmGroupContext.Provider>
164
174
  );
165
175
  });
166
176
 
@@ -168,13 +178,20 @@ export const DropdownMenuLabel = React.forwardRef<
168
178
  HTMLDivElement,
169
179
  WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.GroupLabel>>
170
180
  >(function DropdownMenuLabel({ className, ...props }, ref) {
171
- return (
181
+ const inGroup = React.useContext(InDmGroupContext);
182
+ const label = (
172
183
  <BaseMenu.GroupLabel
173
184
  ref={ref}
174
185
  className={cn("sh-ui-dm__label", className)}
175
186
  {...props}
176
187
  />
177
188
  );
189
+ if (inGroup) return label;
190
+ return (
191
+ <InDmGroupContext.Provider value={true}>
192
+ <BaseMenu.Group>{label}</BaseMenu.Group>
193
+ </InDmGroupContext.Provider>
194
+ );
178
195
  });
179
196
 
180
197
  /* ───────── Separator ─────────
@@ -17,7 +17,7 @@
17
17
 
18
18
  .sh-ui-dm__positioner {
19
19
  outline: none;
20
- z-index: var(--z-dropdown);
20
+ z-index: var(--z-popover);
21
21
  }
22
22
 
23
23
  .sh-ui-dm__content {
@@ -17,7 +17,7 @@
17
17
 
18
18
  .dm__positioner {
19
19
  outline: none;
20
- z-index: var(--z-dropdown);
20
+ z-index: var(--z-popover);
21
21
  }
22
22
 
23
23
  .dm__content {
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
- import { CodeEditor } from "../code-editor/index.tsx";
6
+ import { CodeEditor } from "../code-editor";
7
7
  import styles from "./styles.module.css";
8
8
 
9
9
  import { cn } from "@SH_UI_UTILS@";
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
- import { CodeEditor } from "../code-editor/index.tsx";
6
+ import { CodeEditor } from "../code-editor";
7
7
 
8
8
  import { cn } from "@SH_UI_UTILS@";
9
9
  export interface MarkdownEditorProps {
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
- import { CodeEditor } from "../code-editor/index.tsx";
6
+ import { CodeEditor } from "../code-editor";
7
7
  import "./styles.css";
8
8
 
9
9
  import { cn } from "@SH_UI_UTILS@";
@@ -76,7 +76,7 @@ export const SelectContent = React.forwardRef<
76
76
  }
77
77
  >(({ className, children, container, ...props }, ref) => (
78
78
  <BaseSelect.Portal container={container}>
79
- <BaseSelect.Positioner className="outline-none z-[var(--z-dropdown)]" sideOffset={4} align="start">
79
+ <BaseSelect.Positioner className="outline-none z-[var(--z-popover)]" sideOffset={4} align="start">
80
80
  <BaseSelect.Popup
81
81
  ref={ref}
82
82
  className={cn(
@@ -64,7 +64,7 @@
64
64
  /* 팝업 */
65
65
  .sh-ui-select__positioner {
66
66
  outline: none;
67
- z-index: var(--z-dropdown);
67
+ z-index: var(--z-popover);
68
68
  }
69
69
 
70
70
  .sh-ui-select__content {
@@ -64,7 +64,7 @@
64
64
  /* 팝업 */
65
65
  .select__positioner {
66
66
  outline: none;
67
- z-index: var(--z-dropdown);
67
+ z-index: var(--z-popover);
68
68
  }
69
69
 
70
70
  .select__content {
@@ -3,7 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { cn } from "@SH_UI_UTILS@";
5
5
  import { ChevronRightIcon, PanelLeftIcon } from "lucide-react";
6
- import { Popover, PopoverContent, PopoverTrigger } from "../popover/index.tsx";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
7
7
  import styles from "./styles.module.css";
8
8
 
9
9
  const SIDEBAR_COOKIE_NAME = "sidebar_state";
@@ -482,10 +482,19 @@ export function SidebarInset({ className, ...props }: React.HTMLAttributes<HTMLE
482
482
 
483
483
  /* ───────────── Header / Footer / Content / Separator ───────────── */
484
484
 
485
- /** Sidebar 상단 영역. 보통 로고/검색을 둔다. */
486
- export function SidebarHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
485
+ export interface SidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
486
+ /** @default "stack" */
487
+ align?: "stack" | "topbar";
488
+ /** @default false */
489
+ divider?: boolean;
490
+ }
491
+
492
+ /** Sidebar 상단 영역. 로고/검색/topbar-align 모드 지원. */
493
+ export function SidebarHeader({ className, align = "stack", divider = false, ...props }: SidebarHeaderProps) {
487
494
  return (
488
495
  <div
496
+ data-align={align}
497
+ data-divider={divider ? "true" : undefined}
489
498
  className={cn(styles.sidebar__header, className)}
490
499
  {...props}
491
500
  />
@@ -587,7 +596,7 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
587
596
  * - `md` — 일반 (기본)
588
597
  * - `lg` — 강조 메뉴
589
598
  *
590
- * @default "md"
599
+ * @default "sm"
591
600
  */
592
601
  size?: "sm" | "md" | "lg";
593
602
  /**
@@ -617,7 +626,7 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
617
626
  */
618
627
  export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
619
628
  function SidebarMenuButton(
620
- { className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props },
629
+ { className, isActive, size = "sm", render, sectionId, panelId, onClick, children, ...props },
621
630
  ref
622
631
  ) {
623
632
  const tocActive = useTOCActiveId();
@@ -701,7 +710,7 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
701
710
  isActive?: boolean;
702
711
  /**
703
712
  * 크기.
704
- * @default "md"
713
+ * @default "sm"
705
714
  */
706
715
  size?: "sm" | "md";
707
716
  /**
@@ -715,7 +724,7 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
715
724
  /** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
716
725
  export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
717
726
  function SidebarMenuSubButton(
718
- { className, isActive, size = "md", render, sectionId, children, ...props },
727
+ { className, isActive, size = "sm", render, sectionId, children, ...props },
719
728
  ref
720
729
  ) {
721
730
  const tocActive = useTOCActiveId();
@@ -844,7 +853,7 @@ export function SidebarCollapsible({
844
853
  export interface SidebarCollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
845
854
  /**
846
855
  * 크기. 부모 메뉴와 시각 위계를 맞춘다.
847
- * @default "md"
856
+ * @default "sm"
848
857
  */
849
858
  size?: "sm" | "md" | "lg";
850
859
  }
@@ -852,7 +861,7 @@ export interface SidebarCollapsibleTriggerProps extends React.ButtonHTMLAttribut
852
861
  /** Collapsible을 토글하는 메뉴 버튼. flyout 모드면 Popover Trigger로 자동 위임된다. */
853
862
  export function SidebarCollapsibleTrigger({
854
863
  className,
855
- size = "md",
864
+ size = "sm",
856
865
  children,
857
866
  onClick,
858
867
  ...props
@@ -3,7 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { cn } from "@SH_UI_UTILS@";
5
5
  import { ChevronRightIcon, PanelLeftIcon } from "lucide-react";
6
- import { Popover, PopoverContent, PopoverTrigger } from "../popover/index.tsx";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
7
7
 
8
8
  const SIDEBAR_COOKIE_NAME = "sidebar_state";
9
9
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
@@ -305,8 +305,23 @@ export function SidebarInset({ className, ...props }: React.HTMLAttributes<HTMLE
305
305
  return <main className={cn("flex-1 min-w-0 bg-background flex flex-col", className)} {...props} />;
306
306
  }
307
307
 
308
- export function SidebarHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
309
- return <div className={cn("flex flex-col gap-[var(--space-2)] p-[var(--space-2)] overflow-hidden", className)} {...props} />;
308
+ export interface SidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
309
+ /** @default "stack" */
310
+ align?: "stack" | "topbar";
311
+ /** @default false */
312
+ divider?: boolean;
313
+ }
314
+
315
+ export function SidebarHeader({ className, align = "stack", divider = false, ...props }: SidebarHeaderProps) {
316
+ const base = align === "topbar"
317
+ ? "flex flex-row items-center gap-[var(--space-2)] h-14 px-[var(--space-3)] overflow-hidden"
318
+ : "flex flex-col gap-[var(--space-2)] p-[var(--space-2)] overflow-hidden";
319
+ return (
320
+ <div
321
+ className={cn(base, divider && "border-b border-[var(--sidebar-border)]", className)}
322
+ {...props}
323
+ />
324
+ );
310
325
  }
311
326
 
312
327
  export function SidebarFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
@@ -370,7 +385,7 @@ const menuButtonSize = {
370
385
  };
371
386
 
372
387
  export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
373
- function SidebarMenuButton({ className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props }, ref) {
388
+ function SidebarMenuButton({ className, isActive, size = "sm", render, sectionId, panelId, onClick, children, ...props }, ref) {
374
389
  const tocActive = useTOCActiveId();
375
390
  const ctx = React.useContext(SidebarContext);
376
391
  const panelActive = panelId != null && ctx?.activePanel === panelId;
@@ -436,7 +451,7 @@ const menuSubButtonBase =
436
451
  "flex items-center gap-[var(--space-2)] h-7 px-[var(--space-2)] rounded-[calc(var(--radius)-2px)] text-[0.8125rem] text-[var(--sidebar-fg)] no-underline transition-[background-color,color] duration-[var(--duration-fast)] min-w-0 [&>span]:flex-1 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:[text-overflow:ellipsis] [&>span]:whitespace-nowrap hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-accent-fg)] data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:font-semibold data-[active]:hover:bg-primary-hover motion-reduce:transition-none";
437
452
 
438
453
  export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
439
- function SidebarMenuSubButton({ className, isActive, size = "md", render, sectionId, children, ...props }, ref) {
454
+ function SidebarMenuSubButton({ className, isActive, size = "sm", render, sectionId, children, ...props }, ref) {
440
455
  const tocActive = useTOCActiveId();
441
456
  const resolvedIsActive = isActive ?? (sectionId != null ? tocActive === sectionId : undefined);
442
457
  const cls = cn(menuSubButtonBase, size === "sm" && "text-[length:var(--text-xs)]", className);
@@ -510,7 +525,7 @@ export interface SidebarCollapsibleTriggerProps extends React.ButtonHTMLAttribut
510
525
  size?: "sm" | "md" | "lg";
511
526
  }
512
527
 
513
- export function SidebarCollapsibleTrigger({ className, size = "md", children, onClick, ...props }: SidebarCollapsibleTriggerProps) {
528
+ export function SidebarCollapsibleTrigger({ className, size = "sm", children, onClick, ...props }: SidebarCollapsibleTriggerProps) {
514
529
  const { open, toggle, flyoutMode, flyoutOpen } = useCollapsible();
515
530
  const cls = cn(menuButtonBase, menuButtonSize[size], className);
516
531
  const isOpen = flyoutMode ? flyoutOpen : open;
@@ -482,10 +482,29 @@ export function SidebarInset({ className, ...props }: React.HTMLAttributes<HTMLE
482
482
 
483
483
  /* ───────────── Header / Footer / Content / Separator ───────────── */
484
484
 
485
- /** Sidebar 상단 영역. 보통 로고/검색을 둔다. */
486
- export function SidebarHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
485
+ export interface SidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
486
+ /**
487
+ * 헤더 레이아웃 모드.
488
+ * - `stack` (기본) — 세로 쌓기. 로고 + 검색 등을 위아래로.
489
+ * - `topbar` — 우측 Topbar 와 같은 56px 가로 행. admin/dashboard 에서 좌 Sidebar 헤더와
490
+ * 우 Topbar 의 구분선을 한 줄로 정렬할 때 사용.
491
+ * @default "stack"
492
+ */
493
+ align?: "stack" | "topbar";
494
+ /**
495
+ * 헤더 하단에 1px 구분선. 별도 `SidebarSeparator` 없이 헤더-콘텐츠 경계를 그릴 때.
496
+ * `align="topbar"` 와 조합하면 우측 Topbar 의 border-bottom 과 정확히 정렬된다.
497
+ * @default false
498
+ */
499
+ divider?: boolean;
500
+ }
501
+
502
+ /** Sidebar 상단 영역. 로고/검색/topbar-align 모드 지원. */
503
+ export function SidebarHeader({ className, align = "stack", divider = false, ...props }: SidebarHeaderProps) {
487
504
  return (
488
505
  <div
506
+ data-align={align}
507
+ data-divider={divider ? "true" : undefined}
489
508
  className={cn("sh-ui-sidebar__header", className)}
490
509
  {...props}
491
510
  />
@@ -587,7 +606,7 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
587
606
  * - `md` — 일반 (기본)
588
607
  * - `lg` — 강조 메뉴
589
608
  *
590
- * @default "md"
609
+ * @default "sm"
591
610
  */
592
611
  size?: "sm" | "md" | "lg";
593
612
  /**
@@ -618,7 +637,7 @@ export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLB
618
637
  */
619
638
  export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
620
639
  function SidebarMenuButton(
621
- { className, isActive, size = "md", render, sectionId, panelId, onClick, children, ...props },
640
+ { className, isActive, size = "sm", render, sectionId, panelId, onClick, children, ...props },
622
641
  ref
623
642
  ) {
624
643
  const tocActive = useTOCActiveId();
@@ -712,7 +731,7 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
712
731
  isActive?: boolean;
713
732
  /**
714
733
  * 크기.
715
- * @default "md"
734
+ * @default "sm"
716
735
  */
717
736
  size?: "sm" | "md";
718
737
  /**
@@ -729,7 +748,7 @@ export interface SidebarMenuSubButtonProps extends React.AnchorHTMLAttributes<HT
729
748
  /** 서브 메뉴 항목 내부의 링크. `render` prop 으로 다른 엘리먼트 슬롯 가능. `sectionId`로 TOC 활성 연동. */
730
749
  export const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
731
750
  function SidebarMenuSubButton(
732
- { className, isActive, size = "md", render, sectionId, children, ...props },
751
+ { className, isActive, size = "sm", render, sectionId, children, ...props },
733
752
  ref
734
753
  ) {
735
754
  const tocActive = useTOCActiveId();
@@ -866,7 +885,7 @@ export function SidebarCollapsible({
866
885
  export interface SidebarCollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
867
886
  /**
868
887
  * 크기. 부모 메뉴와 시각 위계를 맞춘다.
869
- * @default "md"
888
+ * @default "sm"
870
889
  */
871
890
  size?: "sm" | "md" | "lg";
872
891
  }
@@ -874,7 +893,7 @@ export interface SidebarCollapsibleTriggerProps extends React.ButtonHTMLAttribut
874
893
  /** Collapsible을 토글하는 메뉴 버튼. flyout 모드면 Popover Trigger로 자동 위임된다. */
875
894
  export function SidebarCollapsibleTrigger({
876
895
  className,
877
- size = "md",
896
+ size = "sm",
878
897
  children,
879
898
  onClick,
880
899
  ...props
@@ -273,6 +273,21 @@
273
273
  overflow: hidden;
274
274
  }
275
275
 
276
+ /* align=topbar — Topbar 와 동일한 56px 행. 좌측 Sidebar 헤더 구분선이 우측 Topbar
277
+ * 구분선과 한 줄로 만난다. admin/dashboard 의 가장 흔한 정렬 패턴. */
278
+ .sh-ui-sidebar__header[data-align="topbar"] {
279
+ flex-direction: row;
280
+ align-items: center;
281
+ gap: var(--space-2);
282
+ height: 3.5rem;
283
+ padding: 0 var(--space-3);
284
+ }
285
+
286
+ /* divider — 헤더 하단 1px 라인. topbar 모드와 조합 시 우측 Topbar 의 border-bottom 과 픽셀 정렬. */
287
+ .sh-ui-sidebar__header[data-divider="true"] {
288
+ border-bottom: 1px solid var(--sidebar-border);
289
+ }
290
+
276
291
  .sh-ui-sidebar__content {
277
292
  display: flex;
278
293
  flex-direction: column;
@@ -273,6 +273,18 @@
273
273
  overflow: hidden;
274
274
  }
275
275
 
276
+ .sidebar__header[data-align="topbar"] {
277
+ flex-direction: row;
278
+ align-items: center;
279
+ gap: var(--space-2);
280
+ height: 3.5rem;
281
+ padding: 0 var(--space-3);
282
+ }
283
+
284
+ .sidebar__header[data-divider="true"] {
285
+ border-bottom: 1px solid var(--sidebar-border);
286
+ }
287
+
276
288
  .sidebar__content {
277
289
  display: flex;
278
290
  flex-direction: column;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$description": "컴포넌트별 토큰 의존성 (var(--*) 추출). build-registry-tokens.mjs 가 자동 생성.",
3
- "$generated": "2026-05-11T07:02:18.245Z",
3
+ "$generated": "2026-05-12T23:53:11.079Z",
4
4
  "components": {
5
5
  "button": {
6
6
  "plain": [
@@ -600,7 +600,7 @@
600
600
  "--text-sm",
601
601
  "--text-xs",
602
602
  "--weight-semibold",
603
- "--z-dropdown"
603
+ "--z-popover"
604
604
  ],
605
605
  "tailwind": [
606
606
  "--border-width-strong",
@@ -614,7 +614,7 @@
614
614
  "--space-3",
615
615
  "--text-sm",
616
616
  "--text-xs",
617
- "--z-dropdown"
617
+ "--z-popover"
618
618
  ],
619
619
  "css-modules": [
620
620
  "--background",
@@ -636,7 +636,7 @@
636
636
  "--text-sm",
637
637
  "--text-xs",
638
638
  "--weight-semibold",
639
- "--z-dropdown"
639
+ "--z-popover"
640
640
  ],
641
641
  "vanilla-extract": []
642
642
  },
@@ -1034,7 +1034,7 @@
1034
1034
  "--text-xs",
1035
1035
  "--weight-medium",
1036
1036
  "--weight-semibold",
1037
- "--z-dropdown"
1037
+ "--z-popover"
1038
1038
  ],
1039
1039
  "tailwind": [
1040
1040
  "--border-width-strong",
@@ -1048,7 +1048,7 @@
1048
1048
  "--space-3",
1049
1049
  "--text-sm",
1050
1050
  "--text-xs",
1051
- "--z-dropdown"
1051
+ "--z-popover"
1052
1052
  ],
1053
1053
  "css-modules": [
1054
1054
  "--background",
@@ -1072,7 +1072,7 @@
1072
1072
  "--text-xs",
1073
1073
  "--weight-medium",
1074
1074
  "--weight-semibold",
1075
- "--z-dropdown"
1075
+ "--z-popover"
1076
1076
  ],
1077
1077
  "vanilla-extract": []
1078
1078
  },
@@ -1202,7 +1202,7 @@
1202
1202
  "--text-sm",
1203
1203
  "--text-xs",
1204
1204
  "--weight-semibold",
1205
- "--z-dropdown"
1205
+ "--z-popover"
1206
1206
  ],
1207
1207
  "tailwind": [
1208
1208
  "--border-width-strong",
@@ -1213,7 +1213,7 @@
1213
1213
  "--space-2",
1214
1214
  "--text-sm",
1215
1215
  "--text-xs",
1216
- "--z-dropdown"
1216
+ "--z-popover"
1217
1217
  ],
1218
1218
  "css-modules": [
1219
1219
  "--background",
@@ -1230,7 +1230,7 @@
1230
1230
  "--text-sm",
1231
1231
  "--text-xs",
1232
1232
  "--weight-semibold",
1233
- "--z-dropdown"
1233
+ "--z-popover"
1234
1234
  ],
1235
1235
  "vanilla-extract": []
1236
1236
  },
@@ -1248,7 +1248,7 @@
1248
1248
  "--text-sm",
1249
1249
  "--text-xs",
1250
1250
  "--weight-semibold",
1251
- "--z-dropdown"
1251
+ "--z-popover"
1252
1252
  ],
1253
1253
  "tailwind": [
1254
1254
  "--opacity-disabled",
@@ -1258,7 +1258,7 @@
1258
1258
  "--space-2",
1259
1259
  "--text-sm",
1260
1260
  "--text-xs",
1261
- "--z-dropdown"
1261
+ "--z-popover"
1262
1262
  ],
1263
1263
  "css-modules": [
1264
1264
  "--background",
@@ -1273,7 +1273,7 @@
1273
1273
  "--text-sm",
1274
1274
  "--text-xs",
1275
1275
  "--weight-semibold",
1276
- "--z-dropdown"
1276
+ "--z-popover"
1277
1277
  ],
1278
1278
  "vanilla-extract": []
1279
1279
  },
@@ -113,9 +113,9 @@
113
113
  },
114
114
 
115
115
  "controlHeight": {
116
- "sm": { "$value": "2rem", "$type": "dimension" },
117
- "md": { "$value": "2.5rem", "$type": "dimension" },
118
- "lg": { "$value": "3rem", "$type": "dimension" }
116
+ "sm": { "$value": "2rem", "$type": "dimension" },
117
+ "md": { "$value": "2.25rem", "$type": "dimension" },
118
+ "lg": { "$value": "2.5rem", "$type": "dimension" }
119
119
  },
120
120
 
121
121
  "borderWidth": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.80.1",
3
+ "version": "0.82.0",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/add.mjs CHANGED
@@ -180,7 +180,19 @@ async function writeOrDiff({ dest, content, cwd, diffMode, summary, conflictReso
180
180
  /** 특수 컴포넌트: 설정으로 토큰 파일 생성 */
181
181
  async function addTokens(config, cwd, diffMode, summary, conflictResolver) {
182
182
  const destRel = config.paths?.tokens;
183
- if (!destRel) throw new Error("paths.tokens 가 설정에 없습니다.");
183
+ if (!destRel) {
184
+ // v0.65+ monorepo: ui-core 는 tokens-only 마커가 아니라 컴포넌트-only.
185
+ // tokens 는 ui-apps/ui-<name>/ 의 역할. cwd 가 ui-core 면 안내를 명확히.
186
+ const cwdLower = cwd.replace(/\\/g, '/').toLowerCase();
187
+ if (cwdLower.endsWith('/packages/ui/ui-core')) {
188
+ throw new Error(
189
+ "paths.tokens 가 설정에 없습니다.\n" +
190
+ " ui-core 는 컴포넌트 단일 SoT 라 tokens 를 보관하지 않습니다 (v0.65+ layout).\n" +
191
+ " tokens 는 ui-app 의 역할 — cwd 를 packages/ui/ui-apps/ui-<name>/ 로 바꾸세요.",
192
+ );
193
+ }
194
+ throw new Error("paths.tokens 가 설정에 없습니다.");
195
+ }
184
196
  const dest = resolve(cwd, destRel);
185
197
 
186
198
  // theme.base === 'custom' 이면 토큰 빌더가 color.custom.X 스케일을 못 찾아 throw 한다 —
@@ -104,7 +104,7 @@ import {
104
104
  buildDartEasesBlock,
105
105
  buildDartGradientsBlock,
106
106
  } from './theme/inject.js';
107
- import { getTemplatesRoot } from '../paths.mjs';
107
+ import { getTemplatesRoot, getRegistryRoot } from '../paths.mjs';
108
108
  import {
109
109
  CSS_FRAMEWORK_DEFAULT,
110
110
  CSS_FRAMEWORKS_SUPPORTED,
@@ -356,6 +356,18 @@ export async function createProject(options = {}) {
356
356
  console.log(`\n cd ${projectName}`);
357
357
  console.log(' pnpm install');
358
358
  console.log(' pnpm dev\n');
359
+
360
+ // v0.82.0+ — 다음 액션이 막막한 첫-사용 경험 보강. baseline 컴포넌트를 어디서/어떻게
361
+ // 추가할지 1줄 가이드. monorepo 는 ui-core 가 SoT, standalone 은 cwd 자체.
362
+ if (projectType === 'monorepo') {
363
+ console.log('다음 단계 — 베이스 컴포넌트 추가 (예시):');
364
+ console.log(` cd ${projectName}/packages/ui/ui-core`);
365
+ console.log(' npx sh-ui-cli add button card input dialog\n');
366
+ console.log('또는 MCP: sh_ui_add_component({ names: ["button", "card", "input", "dialog"] })\n');
367
+ } else {
368
+ console.log('다음 단계 — 베이스 컴포넌트 추가 (예시):');
369
+ console.log(' npx sh-ui-cli add button card input dialog\n');
370
+ }
359
371
  }
360
372
 
361
373
  /**
@@ -714,6 +726,39 @@ async function generateMonorepo(targetDir, projectName, plugins, { yes = false,
714
726
  const uiAppDir = path.join(targetDir, 'packages', 'ui', 'ui-apps', `ui-${appName}`);
715
727
  await injectCssTheme(uiAppDir, theme);
716
728
  await patchShUiConfig(path.join(uiAppDir, 'sh-ui.config.json'), css, themeBase);
729
+
730
+ // ui-core sh-ui.config.json 의 cssFramework 도 동일하게 맞춘다 (v0.81.0+).
731
+ // 안 그러면 후속 add_component 호출이 plain 변종으로 fallback.
732
+ const uiCoreConfigPath = path.join(targetDir, 'packages', 'ui', 'ui-core', 'sh-ui.config.json');
733
+ await patchShUiConfig(uiCoreConfigPath, css, themeBase);
734
+
735
+ // ui-core 에 utility CSS 5종 자동 설치 (v0.81.0+).
736
+ // ui-app/globals.css 가 이 5개 파일을 @import 하므로 안 깔리면 빌드 깨짐.
737
+ // create → dev 사이에 사용자가 매뉴얼 `add base focus-ring …` 호출 안 해도 굴러가게 함.
738
+ await prefetchUiCoreUtilityCss(path.join(targetDir, 'packages', 'ui', 'ui-core'));
739
+ }
740
+
741
+ /**
742
+ * v0.81.0+ — monorepo create_project 직후 ui-core 에 베이스 유틸 CSS 를 자동 emit.
743
+ *
744
+ * ui-app-template/src/styles/globals.css 가 `@workspace/ui-core/styles/{base,focus-ring,
745
+ * animations,breakpoints,z-index}.css` 를 sentinel 블록 안에서 @import 하므로 이 파일들이
746
+ * 존재해야 Tailwind v4 빌드가 깨지지 않는다. 외부 의존성 없는 5개 CSS 파일이라 registry
747
+ * 라우팅 거치지 않고 직접 카피 — add.mjs 의 framework 매칭 / placeholder 치환 로직이
748
+ * 필요 없다 (모두 plain CSS, 토큰 변수만 참조).
749
+ */
750
+ async function prefetchUiCoreUtilityCss(uiCoreDir) {
751
+ const stylesDir = path.join(uiCoreDir, 'src', 'styles');
752
+ await fs.ensureDir(stylesDir);
753
+ const registryRoot = getRegistryRoot('react');
754
+ const names = ['base', 'focus-ring', 'animations', 'breakpoints', 'z-index'];
755
+ for (const name of names) {
756
+ const src = path.join(registryRoot, 'components', name, `${name}.css`);
757
+ const dest = path.join(stylesDir, `${name}.css`);
758
+ if (await fs.pathExists(src) && !(await fs.pathExists(dest))) {
759
+ await fs.copy(src, dest);
760
+ }
761
+ }
717
762
  }
718
763
 
719
764
  async function generateApp(targetDir, appName, port, plugins, arch, css = 'tailwind') {
@@ -11,7 +11,6 @@
11
11
  "module": "NodeNext",
12
12
  "moduleDetection": "force",
13
13
  "moduleResolution": "NodeNext",
14
- "noUncheckedIndexedAccess": true,
15
14
  "resolveJsonModule": true,
16
15
  "skipLibCheck": true,
17
16
  "strict": true,
@@ -145,8 +145,8 @@
145
145
  /* sh-ui:theme-ease-end */
146
146
  /* sh-ui:theme-control-start */
147
147
  --control-sm: 2rem;
148
- --control-md: 2.5rem;
149
- --control-lg: 3rem;
148
+ --control-md: 2.25rem;
149
+ --control-lg: 2.5rem;
150
150
  /* sh-ui:theme-control-end */
151
151
  /* sh-ui:theme-border-width-start */
152
152
  --border-width: 1px;
@@ -13,7 +13,6 @@
13
13
  "resolveJsonModule": true,
14
14
  "skipLibCheck": true,
15
15
  "strict": true,
16
- "noUncheckedIndexedAccess": true,
17
16
  "allowJs": true,
18
17
  "jsx": "preserve",
19
18
  "noEmit": true,
@@ -145,8 +145,8 @@
145
145
  /* sh-ui:theme-ease-end */
146
146
  /* sh-ui:theme-control-start */
147
147
  --control-sm: 2rem;
148
- --control-md: 2.5rem;
149
- --control-lg: 3rem;
148
+ --control-md: 2.25rem;
149
+ --control-lg: 2.5rem;
150
150
  /* sh-ui:theme-control-end */
151
151
  /* sh-ui:theme-border-width-start */
152
152
  --border-width: 1px;
@@ -13,7 +13,6 @@
13
13
  "resolveJsonModule": true,
14
14
  "skipLibCheck": true,
15
15
  "strict": true,
16
- "noUncheckedIndexedAccess": true,
17
16
  "allowJs": true,
18
17
  "jsx": "preserve",
19
18
  "noEmit": true,
@@ -112,8 +112,8 @@
112
112
  /* sh-ui:theme-ease-end */
113
113
  /* sh-ui:theme-control-start */
114
114
  --control-sm: 2rem;
115
- --control-md: 2.5rem;
116
- --control-lg: 3rem;
115
+ --control-md: 2.25rem;
116
+ --control-lg: 2.5rem;
117
117
  /* sh-ui:theme-control-end */
118
118
  /* sh-ui:theme-border-width-start */
119
119
  --border-width: 1px;
@@ -13,7 +13,6 @@
13
13
  "resolveJsonModule": true,
14
14
  "skipLibCheck": true,
15
15
  "strict": true,
16
- "noUncheckedIndexedAccess": true,
17
16
  "allowJs": true,
18
17
  "jsx": "preserve",
19
18
  "noEmit": true,
@@ -1,10 +1,20 @@
1
1
  @import 'tailwindcss';
2
2
 
3
3
  @source "../../../../../../apps/**/*.{ts,tsx}";
4
+ @source "../../../../ui-core/src/**/*.{ts,tsx}";
4
5
  @source "../**/*.{ts,tsx}";
5
6
 
6
7
  @import './tokens.css';
7
8
 
9
+ /* sh-ui:utility-imports-start — v0.81+ create_project 가 자동 설치하는 베이스 유틸 CSS.
10
+ * 이 블록 안의 줄들은 add_component 가 멱등하게 관리. 직접 편집은 가능하지만 sentinel 은 유지. */
11
+ @import '@workspace/ui-core/styles/base.css';
12
+ @import '@workspace/ui-core/styles/focus-ring.css';
13
+ @import '@workspace/ui-core/styles/animations.css';
14
+ @import '@workspace/ui-core/styles/breakpoints.css';
15
+ @import '@workspace/ui-core/styles/z-index.css';
16
+ /* sh-ui:utility-imports-end */
17
+
8
18
  @custom-variant dark (&:is(.dark *));
9
19
 
10
20
  /* Tailwind v4 — 토큰 CSS 변수를 utility 클래스로 노출.
@@ -145,8 +145,8 @@
145
145
  /* sh-ui:theme-ease-end */
146
146
  /* sh-ui:theme-control-start */
147
147
  --control-sm: 2rem;
148
- --control-md: 2.5rem;
149
- --control-lg: 3rem;
148
+ --control-md: 2.25rem;
149
+ --control-lg: 2.5rem;
150
150
  /* sh-ui:theme-control-end */
151
151
  /* sh-ui:theme-border-width-start */
152
152
  --border-width: 1px;