sh-ui-cli 0.61.4 → 0.62.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.
|
@@ -2,6 +2,18 @@
|
|
|
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.62.0",
|
|
7
|
+
"date": "2026-05-08",
|
|
8
|
+
"title": "minor — `theme` 컴포넌트가 next-themes 어댑터로 통합 (RootLayout 자동 wiring)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**`sh-ui add theme` 의 `useTheme` / `ThemeProvider` 가 next-themes 위에서 동작** — 템플릿이 RootLayout 에 이미 깔아둔 next-themes ThemeProvider 와 동일한 컨텍스트를 공유. 사용자가 add 후 어디서든 `useTheme` 호출 시 throw 없이 즉시 동작. 이전엔 templates 의 next-themes 와 sh-ui 자체 cookie ThemeProvider 두 시스템이 분리돼 있어 혼선.",
|
|
12
|
+
"**시그니처는 보존, 내부 영속화는 cookie → next-themes localStorage 로 이전** — `{ theme, setTheme, toggleTheme }` 와 `light`/`dark` 두 값만 노출하는 정책 그대로. 사용자 코드는 변경 불필요. 다만 기존 `sh-ui-theme` cookie 값은 새 키로 자동 마이그레이션되지 않음(첫 방문 시 defaultTheme 으로 초기화).",
|
|
13
|
+
"**registry.json `theme.dependencies` 에 `next-themes` 추가** — `sh-ui add theme` 시 외부 패키지 자동 설치 안내에 포함. peer-versions.json 에 `^0.4.6` 핀."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.62.0"
|
|
16
|
+
},
|
|
5
17
|
{
|
|
6
18
|
"version": "0.61.4",
|
|
7
19
|
"date": "2026-05-08",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
useTheme as useNextTheme,
|
|
7
|
+
} from "next-themes";
|
|
4
8
|
|
|
5
9
|
type Theme = "light" | "dark";
|
|
6
10
|
|
|
7
|
-
const THEME_COOKIE_NAME = "sh-ui-theme";
|
|
8
|
-
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
9
|
-
|
|
10
11
|
/* ───────────── Context ───────────── */
|
|
11
12
|
|
|
12
13
|
type ThemeContextValue = {
|
|
@@ -15,94 +16,94 @@ type ThemeContextValue = {
|
|
|
15
16
|
toggleTheme: () => void;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 현재 테마와 setter 를 반환한다. ThemeProvider (또는 next-themes 의
|
|
21
|
+
* ThemeProvider) 안에서만 호출 가능.
|
|
22
|
+
*
|
|
23
|
+
* 내부적으로 next-themes 의 useTheme 를 어댑팅 — `resolvedTheme` 을
|
|
24
|
+
* `light`/`dark` 로 좁혀 노출하고, system 모드는 감추는 형태.
|
|
25
|
+
*/
|
|
26
|
+
export function useTheme(): ThemeContextValue {
|
|
27
|
+
const { resolvedTheme, setTheme: setNextTheme } = useNextTheme();
|
|
28
|
+
const theme: Theme = resolvedTheme === "dark" ? "dark" : "light";
|
|
29
|
+
|
|
30
|
+
const setTheme = React.useCallback(
|
|
31
|
+
(next: Theme) => setNextTheme(next),
|
|
32
|
+
[setNextTheme],
|
|
33
|
+
);
|
|
34
|
+
const toggleTheme = React.useCallback(
|
|
35
|
+
() => setNextTheme(theme === "dark" ? "light" : "dark"),
|
|
36
|
+
[setNextTheme, theme],
|
|
37
|
+
);
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return ctx;
|
|
39
|
+
return React.useMemo(
|
|
40
|
+
() => ({ theme, setTheme, toggleTheme }),
|
|
41
|
+
[theme, setTheme, toggleTheme],
|
|
42
|
+
);
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
/* ───────────── Provider ───────────── */
|
|
28
46
|
|
|
29
47
|
export interface ThemeProviderProps {
|
|
30
48
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
49
|
+
* 비제어 모드의 초기 테마. next-themes 가 storage(localStorage) 에 저장된
|
|
50
|
+
* 값을 우선하므로, 사용자가 한 번 선택한 후에는 이 값이 무시된다.
|
|
33
51
|
*
|
|
34
52
|
* @default "light"
|
|
35
|
-
* @example
|
|
36
|
-
* // Next.js App Router
|
|
37
|
-
* const t = (await cookies()).get("sh-ui-theme")?.value;
|
|
38
|
-
* <ThemeProvider defaultTheme={t === "dark" ? "dark" : "light"}>
|
|
39
53
|
*/
|
|
40
54
|
defaultTheme?: Theme;
|
|
41
55
|
/**
|
|
42
|
-
*
|
|
43
|
-
* 보통 `defaultTheme`
|
|
56
|
+
* 제어 모드 — 지정 시 강제 테마로 고정 (next-themes `forcedTheme`).
|
|
57
|
+
* 보통 `defaultTheme` 비제어로 충분.
|
|
44
58
|
*/
|
|
45
59
|
theme?: Theme;
|
|
46
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* 테마 변경 콜백. next-themes 자체는 setter 호출 시 콜백을 노출하지 않으므로
|
|
62
|
+
* 내부 effect 로 변화를 감지해 호출한다.
|
|
63
|
+
*/
|
|
47
64
|
onThemeChange?: (theme: Theme) => void;
|
|
48
65
|
children: React.ReactNode;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
/**
|
|
52
|
-
* 다크/라이트
|
|
53
|
-
*
|
|
69
|
+
* 다크/라이트 테마와 `<html class="dark">` 토글을 담당하는 Provider 어댑터.
|
|
70
|
+
*
|
|
71
|
+
* 내부 구현은 next-themes — `attribute='class'`, `enableSystem={false}`,
|
|
72
|
+
* `disableTransitionOnChange` 로 고정. SSR/hydration mismatch 방지를 위해
|
|
73
|
+
* `<html suppressHydrationWarning>` 을 RootLayout 에 함께 둘 것.
|
|
54
74
|
*/
|
|
55
75
|
export function ThemeProvider({
|
|
56
76
|
defaultTheme = "light",
|
|
57
|
-
theme
|
|
77
|
+
theme,
|
|
58
78
|
onThemeChange,
|
|
59
79
|
children,
|
|
60
80
|
}: ThemeProviderProps) {
|
|
61
|
-
const [_theme, _setTheme] = React.useState<Theme>(defaultTheme);
|
|
62
|
-
const theme = themeProp ?? _theme;
|
|
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
|
-
|
|
79
|
-
const setTheme = React.useCallback(
|
|
80
|
-
(next: Theme) => {
|
|
81
|
-
if (onThemeChange) onThemeChange(next);
|
|
82
|
-
else _setTheme(next);
|
|
83
|
-
|
|
84
|
-
if (typeof document !== "undefined") {
|
|
85
|
-
const root = document.documentElement.classList;
|
|
86
|
-
root.toggle("dark", next === "dark");
|
|
87
|
-
root.toggle("light", next === "light");
|
|
88
|
-
document.cookie = `${THEME_COOKIE_NAME}=${next}; path=/; max-age=${THEME_COOKIE_MAX_AGE}`;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
[onThemeChange],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const toggleTheme = React.useCallback(() => {
|
|
95
|
-
setTheme(theme === "dark" ? "light" : "dark");
|
|
96
|
-
}, [theme, setTheme]);
|
|
97
|
-
|
|
98
|
-
const value = React.useMemo<ThemeContextValue>(
|
|
99
|
-
() => ({ theme, setTheme, toggleTheme }),
|
|
100
|
-
[theme, setTheme, toggleTheme],
|
|
101
|
-
);
|
|
102
|
-
|
|
103
81
|
return (
|
|
104
|
-
<
|
|
82
|
+
<NextThemesProvider
|
|
83
|
+
attribute="class"
|
|
84
|
+
defaultTheme={defaultTheme}
|
|
85
|
+
enableSystem={false}
|
|
86
|
+
disableTransitionOnChange
|
|
87
|
+
forcedTheme={theme}
|
|
88
|
+
themes={["light", "dark"]}
|
|
89
|
+
>
|
|
90
|
+
{onThemeChange ? <ThemeChangeBridge onThemeChange={onThemeChange} /> : null}
|
|
105
91
|
{children}
|
|
106
|
-
</
|
|
92
|
+
</NextThemesProvider>
|
|
107
93
|
);
|
|
108
94
|
}
|
|
95
|
+
|
|
96
|
+
function ThemeChangeBridge({
|
|
97
|
+
onThemeChange,
|
|
98
|
+
}: {
|
|
99
|
+
onThemeChange: (theme: Theme) => void;
|
|
100
|
+
}) {
|
|
101
|
+
const { theme } = useTheme();
|
|
102
|
+
const last = React.useRef<Theme | null>(null);
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (last.current === theme) return;
|
|
105
|
+
last.current = theme;
|
|
106
|
+
onThemeChange(theme);
|
|
107
|
+
}, [theme, onThemeChange]);
|
|
108
|
+
return null;
|
|
109
|
+
}
|