@moontra/moonui 2.4.13 → 2.5.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.
- package/dist/chunk-X5ULQZNC.global.js +33 -0
- package/dist/chunk-X5ULQZNC.global.js.map +1 -0
- package/dist/index.global.js +16 -14
- package/dist/index.global.js.map +1 -1
- package/dist/lib/theme.d.mts +90 -0
- package/dist/lib/theme.d.ts +90 -0
- package/dist/lib/theme.global.js +41 -0
- package/dist/lib/theme.global.js.map +1 -0
- package/dist/lib/theme.js +596 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/theme.mjs +571 -0
- package/dist/lib/theme.mjs.map +1 -0
- package/package.json +23 -10
- package/src/lib/theme-presets.ts +480 -0
- package/src/lib/theme.tsx +227 -363
- package/src/styles/tokens.css +88 -14
- package/tailwind-preset.js +175 -0
package/src/lib/theme.tsx
CHANGED
|
@@ -1,398 +1,262 @@
|
|
|
1
|
-
|
|
2
|
-
import { createContext, useContext, useState, useMemo, ReactNode } from 'react';
|
|
1
|
+
"use client";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* MoonUI ThemeProvider — CALISIR tema sistemi.
|
|
5
|
+
*
|
|
6
|
+
* ONCEKI HALI BOZUKTU: eski ThemeProvider yalnizca bir React context tasiyordu;
|
|
7
|
+
* DOM'a hicbir sey yazmiyordu (setProperty/classList/useEffect yoktu) ve
|
|
8
|
+
* createCssVariables() export edilmis ama HIC CAGRILMAMISTI. Yani <ThemeProvider>
|
|
9
|
+
* gorsel olarak no-op'tu. Ustelik token isimleri (--color-primary) ve deger formati
|
|
10
|
+
* (tam `hsl(...)` string'i) component'lerin okudugu sozlesmeyle uyusmuyordu:
|
|
11
|
+
* Tailwind preset'i renkleri `hsl(var(--primary) / <alpha-value>)` diye bagliyor,
|
|
12
|
+
* dolayisiyla tam hsl(...) yazilsaydi `hsl(hsl(...))` olusur ve renk kirilirdi.
|
|
13
|
+
*
|
|
14
|
+
* BU SURUM:
|
|
15
|
+
* - tokens.css ile AYNI sozlesmeyi kullanir: --primary, --card-foreground, --radius
|
|
16
|
+
* ve ham HSL ucusu ("217.2 91.2% 59.8%").
|
|
17
|
+
* - Token'lari gercekten <html> uzerine inline custom property olarak YAZAR.
|
|
18
|
+
* - 'default' preset'te ve ozel token yokken HICBIR SEY YAZMAZ; statik tokens.css
|
|
19
|
+
* gecerli kalir. Bu, "provider'i mount etmek gorunumu degistirmez" garantisini
|
|
20
|
+
* YAPISAL olarak verir.
|
|
21
|
+
* - Karanlik mod next-themes ile uyumlu: <html>'deki `dark` sinifini MutationObserver
|
|
22
|
+
* ile IZLER ve preset'in dark varyantini uygular. `dark` sinifini kendisi YAZMAZ —
|
|
23
|
+
* o isi tema kutuphanenize birakir, cakisma olmaz.
|
|
6
24
|
*/
|
|
7
|
-
export type ThemeColors = {
|
|
8
|
-
primary: string;
|
|
9
|
-
secondary: string;
|
|
10
|
-
success: string;
|
|
11
|
-
warning: string;
|
|
12
|
-
error: string;
|
|
13
|
-
background: string;
|
|
14
|
-
foreground: string;
|
|
15
|
-
card: string;
|
|
16
|
-
cardForeground: string;
|
|
17
|
-
border: string;
|
|
18
|
-
muted: string;
|
|
19
|
-
mutedForeground: string;
|
|
20
|
-
accent: string;
|
|
21
|
-
accentForeground: string;
|
|
22
|
-
};
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
md: string;
|
|
32
|
-
lg: string;
|
|
33
|
-
full: string;
|
|
34
|
-
};
|
|
35
|
-
fontSizes: {
|
|
36
|
-
xs: string;
|
|
37
|
-
sm: string;
|
|
38
|
-
base: string;
|
|
39
|
-
lg: string;
|
|
40
|
-
xl: string;
|
|
41
|
-
'2xl': string;
|
|
42
|
-
'3xl': string;
|
|
43
|
-
};
|
|
44
|
-
spacing: {
|
|
45
|
-
1: string;
|
|
46
|
-
2: string;
|
|
47
|
-
4: string;
|
|
48
|
-
6: string;
|
|
49
|
-
8: string;
|
|
50
|
-
};
|
|
51
|
-
// Kullanıcıların kendi özel değişkenlerini ekleyebilmesi için
|
|
52
|
-
extensions?: Record<string, Record<string, string> | string>;
|
|
53
|
-
};
|
|
26
|
+
import * as React from "react";
|
|
27
|
+
import {
|
|
28
|
+
themePresets,
|
|
29
|
+
type ThemePresetName,
|
|
30
|
+
type ThemePresetDefinition,
|
|
31
|
+
type ThemeTokens,
|
|
32
|
+
} from "./theme-presets";
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
export type ThemePreset = 'default' | 'modern' | 'rounded' | 'minimal';
|
|
34
|
+
export { themePresets };
|
|
35
|
+
export type { ThemePresetName, ThemePresetDefinition, ThemeTokens };
|
|
59
36
|
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
lg: '1rem',
|
|
69
|
-
full: '9999px',
|
|
70
|
-
},
|
|
71
|
-
colors: {
|
|
72
|
-
primary: 'hsl(250, 95%, 64%)',
|
|
73
|
-
secondary: 'hsl(250, 70%, 80%)',
|
|
74
|
-
success: 'hsl(142, 71%, 45%)',
|
|
75
|
-
warning: 'hsl(38, 92%, 50%)',
|
|
76
|
-
error: 'hsl(0, 84%, 60%)',
|
|
77
|
-
background: 'hsl(0, 0%, 100%)',
|
|
78
|
-
foreground: 'hsl(221, 39%, 11%)',
|
|
79
|
-
card: 'hsl(0, 0%, 100%)',
|
|
80
|
-
cardForeground: 'hsl(221, 39%, 11%)',
|
|
81
|
-
border: 'hsl(210, 20%, 90%)',
|
|
82
|
-
muted: 'hsl(210, 20%, 96%)',
|
|
83
|
-
mutedForeground: 'hsl(215, 16%, 47%)',
|
|
84
|
-
accent: 'hsl(328, 85%, 70%)',
|
|
85
|
-
accentForeground: 'hsl(210, 20%, 98%)',
|
|
86
|
-
}
|
|
87
|
-
};
|
|
37
|
+
/** camelCase token adi -> CSS custom property. cardForeground -> --card-foreground,
|
|
38
|
+
* chart1 -> --chart-1 */
|
|
39
|
+
const toCssVar = (key: string): string =>
|
|
40
|
+
"--" +
|
|
41
|
+
key
|
|
42
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
43
|
+
.replace(/([a-zA-Z])(\d)/g, "$1-$2")
|
|
44
|
+
.toLowerCase();
|
|
88
45
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
sm: '1rem',
|
|
93
|
-
md: '1.25rem',
|
|
94
|
-
lg: '1.5rem',
|
|
95
|
-
full: '9999px',
|
|
96
|
-
}
|
|
97
|
-
};
|
|
46
|
+
/** Bu provider'in <html>'e yazdigi property'lerin listesi burada tutulur; temizlik
|
|
47
|
+
* yalnizca kendi yazdiklarimizi siler, sayfanin diger inline degiskenlerine dokunmaz. */
|
|
48
|
+
const OWNED_ATTR = "data-moonui-theme-vars";
|
|
98
49
|
|
|
99
|
-
|
|
100
|
-
const minimalTheme: Partial<ThemeConfig> = {
|
|
101
|
-
borderRadius: {
|
|
102
|
-
sm: '0.125rem',
|
|
103
|
-
md: '0.25rem',
|
|
104
|
-
lg: '0.375rem',
|
|
105
|
-
full: '9999px',
|
|
106
|
-
},
|
|
107
|
-
colors: {
|
|
108
|
-
primary: 'hsl(220, 14%, 40%)',
|
|
109
|
-
secondary: 'hsl(220, 9%, 60%)',
|
|
110
|
-
success: 'hsl(142, 71%, 45%)',
|
|
111
|
-
warning: 'hsl(38, 92%, 50%)',
|
|
112
|
-
error: 'hsl(0, 84%, 60%)',
|
|
113
|
-
background: 'hsl(0, 0%, 100%)',
|
|
114
|
-
foreground: 'hsl(221, 39%, 11%)',
|
|
115
|
-
card: 'hsl(0, 0%, 100%)',
|
|
116
|
-
cardForeground: 'hsl(221, 39%, 11%)',
|
|
117
|
-
border: 'hsl(220, 13%, 91%)',
|
|
118
|
-
muted: 'hsl(220, 14%, 96%)',
|
|
119
|
-
mutedForeground: 'hsl(215, 16%, 47%)',
|
|
120
|
-
accent: 'hsl(210, 20%, 96%)',
|
|
121
|
-
accentForeground: 'hsl(221, 39%, 11%)',
|
|
122
|
-
}
|
|
123
|
-
};
|
|
50
|
+
export type ColorMode = "light" | "dark";
|
|
124
51
|
|
|
125
|
-
export
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
muted: 'hsl(210, 20%, 96%)',
|
|
138
|
-
mutedForeground: 'hsl(215, 16%, 47%)',
|
|
139
|
-
accent: 'hsl(210, 20%, 96%)',
|
|
140
|
-
accentForeground: 'hsl(221, 39%, 11%)',
|
|
141
|
-
},
|
|
142
|
-
borderRadius: {
|
|
143
|
-
sm: '0.25rem',
|
|
144
|
-
md: '0.375rem',
|
|
145
|
-
lg: '0.5rem',
|
|
146
|
-
full: '9999px',
|
|
147
|
-
},
|
|
148
|
-
fontSizes: {
|
|
149
|
-
xs: '0.75rem',
|
|
150
|
-
sm: '0.875rem',
|
|
151
|
-
base: '1rem',
|
|
152
|
-
lg: '1.125rem',
|
|
153
|
-
xl: '1.25rem',
|
|
154
|
-
'2xl': '1.5rem',
|
|
155
|
-
'3xl': '1.875rem',
|
|
156
|
-
},
|
|
157
|
-
spacing: {
|
|
158
|
-
1: '0.25rem',
|
|
159
|
-
2: '0.5rem',
|
|
160
|
-
4: '1rem',
|
|
161
|
-
6: '1.5rem',
|
|
162
|
-
8: '2rem',
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Varsayılan koyu tema
|
|
168
|
-
*/
|
|
169
|
-
export const defaultDarkTheme: ThemeConfig = {
|
|
170
|
-
...defaultLightTheme,
|
|
171
|
-
colors: {
|
|
172
|
-
primary: 'hsl(221, 83%, 53%)',
|
|
173
|
-
secondary: 'hsl(215, 20%, 65%)',
|
|
174
|
-
success: 'hsl(142, 71%, 45%)',
|
|
175
|
-
warning: 'hsl(38, 92%, 50%)',
|
|
176
|
-
error: 'hsl(0, 84%, 60%)',
|
|
177
|
-
background: 'hsl(221, 39%, 11%)',
|
|
178
|
-
foreground: 'hsl(210, 20%, 98%)',
|
|
179
|
-
card: 'hsl(220, 26%, 14%)',
|
|
180
|
-
cardForeground: 'hsl(210, 20%, 98%)',
|
|
181
|
-
border: 'hsl(215, 28%, 17%)',
|
|
182
|
-
muted: 'hsl(215, 27%, 16%)',
|
|
183
|
-
mutedForeground: 'hsl(215, 16%, 57%)',
|
|
184
|
-
accent: 'hsl(215, 27%, 16%)',
|
|
185
|
-
accentForeground: 'hsl(210, 20%, 98%)',
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Tema bağlamı için tip
|
|
191
|
-
*/
|
|
192
|
-
export interface ThemeContextType {
|
|
193
|
-
theme: ThemeConfig;
|
|
194
|
-
colorMode: 'light' | 'dark';
|
|
195
|
-
setColorMode: (mode: 'light' | 'dark') => void;
|
|
196
|
-
updateTheme: (config: Partial<ThemeConfig>) => void;
|
|
52
|
+
export interface ThemeContextValue {
|
|
53
|
+
preset: ThemePresetName;
|
|
54
|
+
setPreset: (preset: ThemePresetName) => void;
|
|
55
|
+
/** Tum preset tanimlari — tema secici UI'lari icin (isim, aciklama, kategori) */
|
|
56
|
+
presets: Record<ThemePresetName, ThemePresetDefinition>;
|
|
57
|
+
/** Gecerli renk modu (<html>.dark sinifindan okunur) */
|
|
58
|
+
colorMode: ColorMode;
|
|
59
|
+
/** Preset uzerine binen serbest override'lar, ornek: { primary: "270 90% 60%" } */
|
|
60
|
+
overrides: Partial<ThemeTokens> & Record<string, string>;
|
|
61
|
+
setOverrides: (o: Partial<ThemeTokens> & Record<string, string>) => void;
|
|
62
|
+
/** Su an DOM'a yazilmis efektif token'lar; hicbir sey yazilmadiysa null */
|
|
63
|
+
appliedTokens: Record<string, string> | null;
|
|
197
64
|
}
|
|
198
65
|
|
|
199
|
-
|
|
200
|
-
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
66
|
+
const ThemeContext = React.createContext<ThemeContextValue | null>(null);
|
|
201
67
|
|
|
202
|
-
// Tema sağlayıcı props
|
|
203
68
|
export interface ThemeProviderProps {
|
|
204
|
-
children: ReactNode;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
69
|
+
children: React.ReactNode;
|
|
70
|
+
/** Baslangic preset'i. Varsayilan "default" = statik tokens.css, hicbir sey yazilmaz */
|
|
71
|
+
defaultPreset?: ThemePresetName;
|
|
72
|
+
/** Kontrollu kullanim: verilirse ic state yerine bu gecerlidir */
|
|
73
|
+
preset?: ThemePresetName;
|
|
74
|
+
overrides?: Partial<ThemeTokens> & Record<string, string>;
|
|
75
|
+
/** Secimi localStorage'a yaz/oku. Varsayilan false (opt-in) */
|
|
76
|
+
persist?: boolean;
|
|
77
|
+
storageKey?: string;
|
|
210
78
|
}
|
|
211
79
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
case 'modern':
|
|
218
|
-
return modernTheme;
|
|
219
|
-
case 'rounded':
|
|
220
|
-
return roundedTheme;
|
|
221
|
-
case 'minimal':
|
|
222
|
-
return minimalTheme;
|
|
223
|
-
default:
|
|
224
|
-
return {};
|
|
225
|
-
}
|
|
226
|
-
};
|
|
80
|
+
const readColorMode = (): ColorMode =>
|
|
81
|
+
typeof document !== "undefined" &&
|
|
82
|
+
document.documentElement.classList.contains("dark")
|
|
83
|
+
? "dark"
|
|
84
|
+
: "light";
|
|
227
85
|
|
|
228
|
-
/**
|
|
229
|
-
* MoonUI tema sağlayıcısı
|
|
230
|
-
*/
|
|
231
86
|
export const ThemeProvider = ({
|
|
232
87
|
children,
|
|
233
|
-
|
|
234
|
-
preset
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
88
|
+
defaultPreset = "default",
|
|
89
|
+
preset: controlledPreset,
|
|
90
|
+
overrides: controlledOverrides,
|
|
91
|
+
persist = false,
|
|
92
|
+
storageKey = "moonui-theme-preset",
|
|
238
93
|
}: ThemeProviderProps) => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
94
|
+
const [uncontrolledPreset, setUncontrolledPreset] =
|
|
95
|
+
React.useState<ThemePresetName>(defaultPreset);
|
|
96
|
+
const [uncontrolledOverrides, setUncontrolledOverrides] = React.useState<
|
|
97
|
+
Partial<ThemeTokens> & Record<string, string>
|
|
98
|
+
>({});
|
|
99
|
+
// SSR ile ayni ilk render icin daima "light" ile basla; gercegi effect'te oku.
|
|
100
|
+
const [colorMode, setColorMode] = React.useState<ColorMode>("light");
|
|
101
|
+
const [appliedTokens, setAppliedTokens] = React.useState<Record<
|
|
102
|
+
string,
|
|
103
|
+
string
|
|
104
|
+
> | null>(null);
|
|
244
105
|
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
...defaultLightTheme,
|
|
248
|
-
...presetThemeValues,
|
|
249
|
-
...lightTheme,
|
|
250
|
-
colors: {
|
|
251
|
-
...defaultLightTheme.colors,
|
|
252
|
-
...(presetThemeValues.colors || {}),
|
|
253
|
-
...(lightTheme.colors || {}),
|
|
254
|
-
},
|
|
255
|
-
extensions: extensions,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const [customDarkTheme, setCustomDarkTheme] = useState<ThemeConfig>({
|
|
259
|
-
...defaultDarkTheme,
|
|
260
|
-
...presetThemeValues,
|
|
261
|
-
...darkTheme,
|
|
262
|
-
colors: {
|
|
263
|
-
...defaultDarkTheme.colors,
|
|
264
|
-
...(presetThemeValues.colors || {}),
|
|
265
|
-
...(darkTheme.colors || {}),
|
|
266
|
-
},
|
|
267
|
-
extensions: extensions,
|
|
268
|
-
});
|
|
106
|
+
const preset = controlledPreset ?? uncontrolledPreset;
|
|
107
|
+
const overrides = controlledOverrides ?? uncontrolledOverrides;
|
|
269
108
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
109
|
+
// Kaydedilmis preset + ozel token'lari geri yukle. Ozel token'lar da saklanir;
|
|
110
|
+
// aksi halde kullanicinin olusturdugu tema sayfa yenilenince kaybolurdu.
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
if (!persist) return;
|
|
113
|
+
try {
|
|
114
|
+
if (!controlledPreset) {
|
|
115
|
+
const saved = window.localStorage.getItem(storageKey);
|
|
116
|
+
if (saved && saved in themePresets) {
|
|
117
|
+
setUncontrolledPreset(saved as ThemePresetName);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!controlledOverrides) {
|
|
121
|
+
const savedOverrides = window.localStorage.getItem(
|
|
122
|
+
`${storageKey}-overrides`
|
|
123
|
+
);
|
|
124
|
+
if (savedOverrides) {
|
|
125
|
+
const parsed = JSON.parse(savedOverrides);
|
|
126
|
+
if (parsed && typeof parsed === "object") {
|
|
127
|
+
setUncontrolledOverrides(parsed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
/* private mode / bozuk JSON — sessizce gec */
|
|
133
|
+
}
|
|
134
|
+
}, [persist, storageKey, controlledPreset, controlledOverrides]);
|
|
274
135
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
...(config.extensions || {}),
|
|
288
|
-
},
|
|
289
|
-
}));
|
|
290
|
-
} else {
|
|
291
|
-
setCustomDarkTheme(prev => ({
|
|
292
|
-
...prev,
|
|
293
|
-
...config,
|
|
294
|
-
colors: {
|
|
295
|
-
...prev.colors,
|
|
296
|
-
...(config.colors || {}),
|
|
297
|
-
},
|
|
298
|
-
extensions: {
|
|
299
|
-
...(prev.extensions || {}),
|
|
300
|
-
...(config.extensions || {}),
|
|
301
|
-
},
|
|
302
|
-
}));
|
|
136
|
+
// Ozel token'lari sakla (kontrolsuz kullanimda)
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
if (!persist || controlledOverrides) return;
|
|
139
|
+
try {
|
|
140
|
+
const key = `${storageKey}-overrides`;
|
|
141
|
+
if (Object.keys(uncontrolledOverrides).length === 0) {
|
|
142
|
+
window.localStorage.removeItem(key);
|
|
143
|
+
} else {
|
|
144
|
+
window.localStorage.setItem(key, JSON.stringify(uncontrolledOverrides));
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
/* yok say */
|
|
303
148
|
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Tema değiştirme fonksiyonu
|
|
307
|
-
const setThemePreset = (preset: ThemePreset) => {
|
|
308
|
-
const presetValues = getPresetTheme(preset);
|
|
309
|
-
|
|
310
|
-
updateTheme({
|
|
311
|
-
...presetValues,
|
|
312
|
-
colors: presetValues.colors as ThemeColors,
|
|
313
|
-
});
|
|
314
|
-
};
|
|
149
|
+
}, [persist, storageKey, uncontrolledOverrides, controlledOverrides]);
|
|
315
150
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
151
|
+
// next-themes gibi kutuphaneler <html>'deki `dark` sinifini degistirir; izle.
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
setColorMode(readColorMode());
|
|
154
|
+
const el = document.documentElement;
|
|
155
|
+
const observer = new MutationObserver(() => setColorMode(readColorMode()));
|
|
156
|
+
observer.observe(el, { attributes: true, attributeFilter: ["class"] });
|
|
157
|
+
return () => observer.disconnect();
|
|
158
|
+
}, []);
|
|
323
159
|
|
|
324
|
-
|
|
325
|
-
|
|
160
|
+
// Asil is: token'lari DOM'a yaz.
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
const root = document.documentElement;
|
|
326
163
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
colorMode: 'light' | 'dark';
|
|
333
|
-
setColorMode: (mode: 'light' | 'dark') => void;
|
|
334
|
-
updateTheme: (config: Partial<ThemeConfig>) => void;
|
|
335
|
-
setThemePreset: (preset: ThemePreset) => void;
|
|
336
|
-
}
|
|
164
|
+
const previous = root.getAttribute(OWNED_ATTR);
|
|
165
|
+
if (previous) {
|
|
166
|
+
for (const name of previous.split(",")) root.style.removeProperty(name);
|
|
167
|
+
root.removeAttribute(OWNED_ATTR);
|
|
168
|
+
}
|
|
337
169
|
|
|
338
|
-
|
|
339
|
-
* Tema kullanma kancası
|
|
340
|
-
*/
|
|
341
|
-
export const useTheme = (): ThemeContextType => {
|
|
342
|
-
const context = useContext(ThemeContext);
|
|
343
|
-
|
|
344
|
-
if (context === undefined) {
|
|
345
|
-
throw new Error('useTheme must be used within a ThemeProvider');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return context;
|
|
349
|
-
};
|
|
170
|
+
const hasOverrides = Object.keys(overrides).length > 0;
|
|
350
171
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
172
|
+
// SIFIR-DELTA: varsayilan preset + override yok => hicbir sey yazma.
|
|
173
|
+
if (preset === "default" && !hasOverrides) {
|
|
174
|
+
setAppliedTokens(null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const def = themePresets[preset] ?? themePresets.default;
|
|
179
|
+
const effective: Record<string, string> = {
|
|
180
|
+
...(colorMode === "dark" ? def.dark : def.light),
|
|
181
|
+
radius: `${def.radius}rem`,
|
|
182
|
+
...overrides,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const written: string[] = [];
|
|
186
|
+
for (const [key, value] of Object.entries(effective)) {
|
|
187
|
+
if (value == null || value === "") continue;
|
|
188
|
+
// "--" ile baslayan anahtarlar oldugu gibi gecer (ornek: "--brand-600")
|
|
189
|
+
const cssVar = key.startsWith("--") ? key : toCssVar(key);
|
|
190
|
+
root.style.setProperty(cssVar, value);
|
|
191
|
+
written.push(cssVar);
|
|
192
|
+
}
|
|
193
|
+
root.setAttribute(OWNED_ATTR, written.join(","));
|
|
194
|
+
setAppliedTokens(effective);
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
for (const name of written) root.style.removeProperty(name);
|
|
198
|
+
root.removeAttribute(OWNED_ATTR);
|
|
199
|
+
};
|
|
200
|
+
}, [preset, overrides, colorMode]);
|
|
201
|
+
|
|
202
|
+
const setPreset = React.useCallback(
|
|
203
|
+
(next: ThemePresetName) => {
|
|
204
|
+
setUncontrolledPreset(next);
|
|
205
|
+
if (persist) {
|
|
206
|
+
try {
|
|
207
|
+
window.localStorage.setItem(storageKey, next);
|
|
208
|
+
} catch {
|
|
209
|
+
/* yok say */
|
|
210
|
+
}
|
|
386
211
|
}
|
|
387
|
-
}
|
|
212
|
+
},
|
|
213
|
+
[persist, storageKey]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const value = React.useMemo<ThemeContextValue>(
|
|
217
|
+
() => ({
|
|
218
|
+
preset,
|
|
219
|
+
setPreset,
|
|
220
|
+
presets: themePresets,
|
|
221
|
+
colorMode,
|
|
222
|
+
overrides,
|
|
223
|
+
setOverrides: setUncontrolledOverrides,
|
|
224
|
+
appliedTokens,
|
|
225
|
+
}),
|
|
226
|
+
[preset, setPreset, colorMode, overrides, appliedTokens]
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const useTheme = (): ThemeContextValue => {
|
|
235
|
+
const ctx = React.useContext(ThemeContext);
|
|
236
|
+
if (!ctx) {
|
|
237
|
+
throw new Error("useTheme, <ThemeProvider> icinde cagrilmalidir.");
|
|
388
238
|
}
|
|
389
|
-
|
|
390
|
-
return variables;
|
|
239
|
+
return ctx;
|
|
391
240
|
};
|
|
392
241
|
|
|
393
242
|
/**
|
|
394
|
-
* CSS
|
|
243
|
+
* Bir preset'i CSS metnine cevirir — tema secicilerdeki "disa aktar" icin.
|
|
244
|
+
* Uretilen blok dogrudan globals.css'e yapistirilabilir.
|
|
395
245
|
*/
|
|
396
|
-
export const
|
|
397
|
-
|
|
246
|
+
export const themeToCss = (
|
|
247
|
+
preset: ThemePresetName,
|
|
248
|
+
{ selector = ":root", darkSelector = ".dark" } = {}
|
|
249
|
+
): string => {
|
|
250
|
+
const def = themePresets[preset] ?? themePresets.default;
|
|
251
|
+
const block = (tokens: ThemeTokens, radius?: string) =>
|
|
252
|
+
Object.entries(tokens)
|
|
253
|
+
.filter(([, v]) => v)
|
|
254
|
+
.map(([k, v]) => ` ${toCssVar(k)}: ${v};`)
|
|
255
|
+
.concat(radius ? [` --radius: ${radius};`] : [])
|
|
256
|
+
.join("\n");
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
`${selector} {\n${block(def.light, `${def.radius}rem`)}\n}\n\n` +
|
|
260
|
+
`${darkSelector} {\n${block(def.dark)}\n}\n`
|
|
261
|
+
);
|
|
398
262
|
};
|