sh-ui-cli 0.55.0 → 0.56.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.
@@ -2,6 +2,32 @@
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.56.1",
7
+ "date": "2026-05-04",
8
+ "title": "fix — 테마 토글이 새로고침 후 초기화 + v0.54.0 템플릿/빌더 완성",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "**force-static 페이지에서 테마 영속화 복구** — `cookies()` 가 throw 해 SSR 시 항상 `isDark=false` 폴백이라 사용자가 토글한 결과(`sh-ui-theme` 쿠키) 가 무시되던 문제. `<head>` 인라인 스크립트로 hydration 전에 쿠키를 읽어 `.dark`/`.light` 클래스 즉시 부여 — 깜빡임 없음.",
12
+ "**ThemeProvider mount-time 동기화 추가** — React state 도 mount 시 쿠키를 직접 확인해 SSR 폴백(`light`)에 갇히지 않도록 보정. 토글 버튼 UI 가 실제 클래스와 항상 일치. registry 원본 + apps/docs 카피본 둘 다 동일하게 패치.",
13
+ "**v0.54.0 미반영 템플릿/빌더 완성** — `nextjs-standalone` / `ui-app-template` 의 `tokens.css` 에 `prefers-color-scheme` 블록 추가, `packages/tokens/build.mjs` 가 해당 블록을 emit. v0.54.0 릴리즈 노트에 약속됐지만 누락됐던 부분을 같은 패치로 정리.",
14
+ "**docs `tokens.css` 의 `@theme inline` 블록 추가** — Tailwind v4 유틸(`bg-primary` / `text-foreground` 등) 이 토큰 변수와 자동 매핑되도록. 기존엔 컴포넌트가 hex fallback 으로만 동작하던 곳도 토큰 흐름으로 통일."
15
+ ],
16
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.56.1"
17
+ },
18
+ {
19
+ "version": "0.56.0",
20
+ "date": "2026-05-04",
21
+ "title": "playground — Success / Warning / Info 색 그룹",
22
+ "type": "minor",
23
+ "highlights": [
24
+ "**playground `/create` 에 Success / Warning / Info 색 슬롯 추가** — 고급 모드에서 `success` / `success-foreground` / `warning` / `warning-foreground` / `info` / `info-foreground` 6개를 직접 잡을 수 있다. light/dark 각각 별도 디폴트(라이트는 Tailwind green-600/amber-600/sky-500, 다크는 한 단계 밝은 채도) 제공.",
25
+ "**Badge 컴포넌트 fallback 과 동일한 디폴트** — `var(--success, #16a34a)` 같은 기존 컴포넌트 fallback 값을 디폴트로 사용해, 사용자가 색을 안 만져도 시각적 변화가 0. 기존 base64/스캐폴드 결과와 호환 유지.",
26
+ "**v0.55.0 base64 옵셔널 키와 자동 연결** — playground 가 export 하는 base64 에 자동으로 새 6개 키가 포함되며, `sh_ui_create_project` 의 `theme` 인자로 그대로 흘러가 `tokens.css` 에 emit. v0.55.0 의 옵셔널 색 토큰 스키마와 끝에서 끝까지 정렬.",
27
+ "**localStorage v1 호환** — 기존 저장된 토큰은 새 키만 디폴트로 채워진 상태로 로드. 마이그레이션 없이 호환."
28
+ ],
29
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.56.0"
30
+ },
5
31
  {
6
32
  "version": "0.55.0",
7
33
  "date": "2026-05-04",
@@ -61,13 +61,30 @@ export function ThemeProvider({
61
61
  const [_theme, _setTheme] = React.useState<Theme>(defaultTheme);
62
62
  const theme = themeProp ?? _theme;
63
63
 
64
+ // SSR 폴백 보정 — force-static 페이지에서는 cookies() 가 throw 해 defaultTheme 이
65
+ // 항상 "light" 로 들어온다. mount 시 cookie 를 직접 읽어 React state 와 documentElement
66
+ // 클래스를 사용자가 마지막에 고른 값으로 동기화. 비제어 모드에서만.
67
+ React.useEffect(() => {
68
+ if (themeProp !== undefined) return;
69
+ if (typeof document === "undefined") return;
70
+ const m = document.cookie.match(/(?:^|; )sh-ui-theme=(dark|light)/);
71
+ if (!m) return;
72
+ const fromCookie = m[1] as Theme;
73
+ _setTheme(fromCookie);
74
+ const root = document.documentElement.classList;
75
+ root.toggle("dark", fromCookie === "dark");
76
+ root.toggle("light", fromCookie === "light");
77
+ }, [themeProp]);
78
+
64
79
  const setTheme = React.useCallback(
65
80
  (next: Theme) => {
66
81
  if (onThemeChange) onThemeChange(next);
67
82
  else _setTheme(next);
68
83
 
69
84
  if (typeof document !== "undefined") {
70
- document.documentElement.classList.toggle("dark", next === "dark");
85
+ const root = document.documentElement.classList;
86
+ root.toggle("dark", next === "dark");
87
+ root.toggle("light", next === "light");
71
88
  document.cookie = `${THEME_COOKIE_NAME}=${next}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
72
89
  }
73
90
  },
@@ -92,6 +92,26 @@ function emitCssBlock(selector, entries) {
92
92
  return `${selector} {\n${lines}\n}`;
93
93
  }
94
94
 
95
+ /**
96
+ * `prefers-color-scheme: dark` 자동 적용 블록.
97
+ *
98
+ * 셀렉터를 `:root:not(.light):not(.dark)` 로 좁힌 이유 — 미디어쿼리는
99
+ * 명시 토글(`.light` / `.dark` 클래스)이 없을 때만 동작해야 한다. 그렇지
100
+ * 않으면 사용자가 라이트로 토글해도 OS 가 다크면 다시 다크로 돌아감.
101
+ *
102
+ * 적용 매트릭스:
103
+ * 클래스 없음 + 라이트 OS → :root (라이트) 적용
104
+ * 클래스 없음 + 다크 OS → 이 블록 적용 (다크)
105
+ * .light 클래스 → :root 만 적용 (강제 라이트, OS 무관)
106
+ * .dark 클래스 → .dark 블록이 마지막에 와서 항상 승리 (강제 다크)
107
+ */
108
+ function emitAutoDarkBlock(darkEntries) {
109
+ const lines = Object.entries(darkEntries)
110
+ .map(([p, entry]) => ` ${toCssVar(p)}: ${toCssValue(entry)};`)
111
+ .join("\n");
112
+ return `@media (prefers-color-scheme: dark) {\n :root:not(.light):not(.dark) {\n${lines}\n }\n}`;
113
+ }
114
+
95
115
  /** 테마 독립 카테고리(light/dark 제외) 전체를 하나의 맵으로 병합 */
96
116
  function mergeThemeIndependent(tokens) {
97
117
  const out = {};
@@ -137,6 +157,7 @@ export async function buildTokensCss(config) {
137
157
  blocks.push(emitCssBlock(":root", { ...tokens.dark, ...themeIndep }));
138
158
  }
139
159
  if (mode === "light-dark") {
160
+ blocks.push(emitAutoDarkBlock(tokens.dark));
140
161
  blocks.push(emitCssBlock(".dark", tokens.dark));
141
162
  }
142
163
  blocks.push(buildTailwindThemeBlock(tokens.light));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.55.0",
3
+ "version": "0.56.1",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,6 +19,25 @@
19
19
  --danger: #DC2626;
20
20
  --danger-foreground: #FFFFFF;
21
21
  }
22
+ @media (prefers-color-scheme: dark) {
23
+ :root:not(.light):not(.dark) {
24
+ --background: #0A0A0A;
25
+ --background-subtle: #171717;
26
+ --background-muted: #262626;
27
+ --background-inverse: #FFFFFF;
28
+ --foreground: #FAFAFA;
29
+ --foreground-muted: #A3A3A3;
30
+ --foreground-subtle: #737373;
31
+ --foreground-inverse: #0A0A0A;
32
+ --border: #262626;
33
+ --border-strong: #404040;
34
+ --primary: #FAFAFA;
35
+ --primary-foreground: #171717;
36
+ --primary-hover: #E5E5E5;
37
+ --danger: #DC2626;
38
+ --danger-foreground: #FFFFFF;
39
+ }
40
+ }
22
41
  .dark {
23
42
  --background: #0A0A0A;
24
43
  --background-subtle: #171717;
@@ -19,6 +19,25 @@
19
19
  --danger: #DC2626;
20
20
  --danger-foreground: #FFFFFF;
21
21
  }
22
+ @media (prefers-color-scheme: dark) {
23
+ :root:not(.light):not(.dark) {
24
+ --background: #0A0A0A;
25
+ --background-subtle: #171717;
26
+ --background-muted: #262626;
27
+ --background-inverse: #FFFFFF;
28
+ --foreground: #FAFAFA;
29
+ --foreground-muted: #A3A3A3;
30
+ --foreground-subtle: #737373;
31
+ --foreground-inverse: #0A0A0A;
32
+ --border: #262626;
33
+ --border-strong: #404040;
34
+ --primary: #FAFAFA;
35
+ --primary-foreground: #171717;
36
+ --primary-hover: #E5E5E5;
37
+ --danger: #DC2626;
38
+ --danger-foreground: #FFFFFF;
39
+ }
40
+ }
22
41
  .dark {
23
42
  --background: #0A0A0A;
24
43
  --background-subtle: #171717;