@zhang_libo/resource-hub 1.0.2
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/LICENSE +21 -0
- package/README.en.md +80 -0
- package/README.ja.md +80 -0
- package/README.md +79 -0
- package/README.zh-TW.md +80 -0
- package/bin/cli.js +10 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +59 -0
- package/dist/app.js.map +1 -0
- package/dist/db/index.js +12 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +169 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +743 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +88 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/i18n.js +309 -0
- package/dist/i18n.js.map +1 -0
- package/dist/plugins/admin.d.ts +4 -0
- package/dist/plugins/admin.d.ts.map +1 -0
- package/dist/plugins/admin.js +19 -0
- package/dist/plugins/admin.js.map +1 -0
- package/dist/plugins/auth.d.ts +4 -0
- package/dist/plugins/auth.d.ts.map +1 -0
- package/dist/plugins/auth.js +35 -0
- package/dist/plugins/auth.js.map +1 -0
- package/dist/routes/auth.d.ts +4 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +352 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/categories.d.ts +4 -0
- package/dist/routes/categories.d.ts.map +1 -0
- package/dist/routes/categories.js +112 -0
- package/dist/routes/categories.js.map +1 -0
- package/dist/routes/config.d.ts +4 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +227 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/resources.d.ts +4 -0
- package/dist/routes/resources.d.ts.map +1 -0
- package/dist/routes/resources.js +474 -0
- package/dist/routes/resources.js.map +1 -0
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.d.ts.map +1 -0
- package/dist/routes/tags.js +37 -0
- package/dist/routes/tags.js.map +1 -0
- package/dist/routes/users.d.ts +4 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +181 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/services/crypto.js +49 -0
- package/dist/services/crypto.js.map +1 -0
- package/dist/services/mail.d.ts +16 -0
- package/dist/services/mail.d.ts.map +1 -0
- package/dist/services/mail.js +33 -0
- package/dist/services/mail.js.map +1 -0
- package/dist/services/rsa.js +49 -0
- package/dist/services/rsa.js.map +1 -0
- package/dist/services/token.d.ts +9 -0
- package/dist/services/token.d.ts.map +1 -0
- package/dist/services/token.js +29 -0
- package/dist/services/token.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
- package/public/admin/AdminCategories.jsx +310 -0
- package/public/admin/AdminConfig.jsx +254 -0
- package/public/admin/AdminEmail.jsx +279 -0
- package/public/admin/AdminTags.jsx +263 -0
- package/public/admin/AdminUsers.jsx +452 -0
- package/public/app.jsx +186 -0
- package/public/components/ConfirmDialog.jsx +78 -0
- package/public/components/DropdownSelect.jsx +281 -0
- package/public/components/EmailPreviewModal.jsx +104 -0
- package/public/components/EmptyState.jsx +50 -0
- package/public/components/Modal.jsx +127 -0
- package/public/components/PasswordStrength.jsx +45 -0
- package/public/components/Skeleton.jsx +68 -0
- package/public/components/Toast.jsx +80 -0
- package/public/components/TooltipIconButton.jsx +55 -0
- package/public/context/AppContext.jsx +314 -0
- package/public/features/BatchResourceModal.jsx +606 -0
- package/public/features/ChangePasswordModal.jsx +187 -0
- package/public/features/ProfileModal.jsx +170 -0
- package/public/features/ResourceCard.jsx +422 -0
- package/public/features/ResourceFormModal.jsx +915 -0
- package/public/features/ResourceRow.jsx +287 -0
- package/public/features/ResourceTimeline.jsx +472 -0
- package/public/hooks/useApi.jsx +26 -0
- package/public/hooks/useRouter.jsx +35 -0
- package/public/index.html +258 -0
- package/public/layout/AdminLayout.jsx +167 -0
- package/public/layout/AppLayout.jsx +119 -0
- package/public/layout/AuthLayout.jsx +503 -0
- package/public/layout/Header.jsx +543 -0
- package/public/layout/Sidebar.jsx +175 -0
- package/public/pages/AdminPage.jsx +30 -0
- package/public/pages/ForgotPasswordPage.jsx +93 -0
- package/public/pages/HomePage.jsx +2297 -0
- package/public/pages/LoginPage.jsx +191 -0
- package/public/pages/RegisterPage.jsx +137 -0
- package/public/pages/ResetPasswordPage.jsx +169 -0
- package/public/pages/SetupPage.jsx +157 -0
- package/public/utils/helpers.jsx +152 -0
- package/public/utils/i18n.jsx +1374 -0
- package/public/utils/preferences.jsx +220 -0
- package/public/utils/security.jsx +88 -0
- package/public/utils/theme.jsx +24 -0
- package/public/vendor/babel.min.js +2 -0
- package/public/vendor/lucide-react.min.js +9 -0
- package/public/vendor/react-dom.development.js +29869 -0
- package/public/vendor/react.development.js +3342 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const UI_PREFERENCES_KEY = 'rh_ui_preferences_v1';
|
|
2
|
+
const LEGACY_THEME_KEY = 'rh_theme';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_UI_PREFERENCES = Object.freeze({
|
|
5
|
+
locale: 'zh-Hans',
|
|
6
|
+
theme: 'system',
|
|
7
|
+
viewMode: 'card',
|
|
8
|
+
sortBy: 'hot',
|
|
9
|
+
quickAccessFilter: null,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const VALID_LOCALES = new Set(['zh-Hans', 'zh-Hant', 'en', 'ja']);
|
|
13
|
+
const VALID_THEMES = new Set(['light', 'dark', 'system']);
|
|
14
|
+
const VALID_VIEW_MODES = new Set(['card', 'list', 'timeline']);
|
|
15
|
+
const VALID_SORTS = new Set(['hot', 'created', 'updated']);
|
|
16
|
+
const VALID_QUICK_ACCESS = new Set(['favorites', 'history', 'mine']);
|
|
17
|
+
|
|
18
|
+
function getDefaultLocale() {
|
|
19
|
+
return window.i18n?.detectBrowserLocale?.() || DEFAULT_UI_PREFERENCES.locale;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cloneDefaultUiPreferences() {
|
|
23
|
+
return {
|
|
24
|
+
locale: getDefaultLocale(),
|
|
25
|
+
theme: DEFAULT_UI_PREFERENCES.theme,
|
|
26
|
+
viewMode: DEFAULT_UI_PREFERENCES.viewMode,
|
|
27
|
+
sortBy: DEFAULT_UI_PREFERENCES.sortBy,
|
|
28
|
+
quickAccessFilter: DEFAULT_UI_PREFERENCES.quickAccessFilter,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeLocale(locale, fallback = getDefaultLocale()) {
|
|
33
|
+
return VALID_LOCALES.has(locale) ? locale : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sanitizeTheme(theme, fallback = DEFAULT_UI_PREFERENCES.theme) {
|
|
37
|
+
return VALID_THEMES.has(theme) ? theme : fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sanitizeViewMode(viewMode, fallback = DEFAULT_UI_PREFERENCES.viewMode) {
|
|
41
|
+
return VALID_VIEW_MODES.has(viewMode) ? viewMode : fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sanitizeSortBy(sortBy, fallback = DEFAULT_UI_PREFERENCES.sortBy) {
|
|
45
|
+
return VALID_SORTS.has(sortBy) ? sortBy : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizeQuickAccessFilter(quickAccessFilter, allowQuickAccess, fallback = null) {
|
|
49
|
+
if (!allowQuickAccess) return null;
|
|
50
|
+
if (quickAccessFilter === null || quickAccessFilter === undefined || quickAccessFilter === '') return null;
|
|
51
|
+
return VALID_QUICK_ACCESS.has(quickAccessFilter) ? quickAccessFilter : fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizePreferenceRecord(record = {}, options = {}) {
|
|
55
|
+
const fallback = options.fallback || cloneDefaultUiPreferences();
|
|
56
|
+
const allowQuickAccess = Boolean(options.allowQuickAccess);
|
|
57
|
+
return {
|
|
58
|
+
locale: sanitizeLocale(record.locale, fallback.locale),
|
|
59
|
+
theme: sanitizeTheme(record.theme, fallback.theme),
|
|
60
|
+
viewMode: sanitizeViewMode(record.viewMode, fallback.viewMode),
|
|
61
|
+
sortBy: sanitizeSortBy(record.sortBy, fallback.sortBy),
|
|
62
|
+
quickAccessFilter: sanitizeQuickAccessFilter(record.quickAccessFilter, allowQuickAccess, allowQuickAccess ? fallback.quickAccessFilter : null),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildEmptyPreferenceStore() {
|
|
67
|
+
return {
|
|
68
|
+
guest: cloneDefaultUiPreferences(),
|
|
69
|
+
users: {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function safeReadLocalStorage(key) {
|
|
74
|
+
try {
|
|
75
|
+
return localStorage.getItem(key);
|
|
76
|
+
} catch (_error) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeWriteLocalStorage(key, value) {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(key, value);
|
|
84
|
+
} catch (_error) {
|
|
85
|
+
// Ignore quota / privacy mode failures. The app should remain usable.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readLegacyTheme() {
|
|
90
|
+
const legacyTheme = safeReadLocalStorage(LEGACY_THEME_KEY);
|
|
91
|
+
return sanitizeTheme(legacyTheme, null);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readRawPreferenceStore() {
|
|
95
|
+
const raw = safeReadLocalStorage(UI_PREFERENCES_KEY);
|
|
96
|
+
if (!raw) return { raw: null, hasStored: false };
|
|
97
|
+
try {
|
|
98
|
+
return { raw: JSON.parse(raw), hasStored: true };
|
|
99
|
+
} catch (_error) {
|
|
100
|
+
return { raw: null, hasStored: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizePreferenceStore(rawStore, hasStored) {
|
|
105
|
+
const store = buildEmptyPreferenceStore();
|
|
106
|
+
const legacyTheme = readLegacyTheme();
|
|
107
|
+
const source = rawStore && typeof rawStore === 'object' && !Array.isArray(rawStore) ? rawStore : {};
|
|
108
|
+
const guestFallback = cloneDefaultUiPreferences();
|
|
109
|
+
|
|
110
|
+
if (legacyTheme) guestFallback.theme = legacyTheme;
|
|
111
|
+
|
|
112
|
+
store.guest = sanitizePreferenceRecord(source.guest || guestFallback, {
|
|
113
|
+
allowQuickAccess: false,
|
|
114
|
+
fallback: guestFallback,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const users = source.users && typeof source.users === 'object' && !Array.isArray(source.users) ? source.users : {};
|
|
118
|
+
Object.keys(users).forEach((userId) => {
|
|
119
|
+
if (!userId) return;
|
|
120
|
+
store.users[userId] = sanitizePreferenceRecord(users[userId], {
|
|
121
|
+
allowQuickAccess: true,
|
|
122
|
+
fallback: { ...store.guest, quickAccessFilter: null },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const serializedStore = JSON.stringify(store);
|
|
127
|
+
const shouldPersist =
|
|
128
|
+
!hasStored ||
|
|
129
|
+
Boolean(legacyTheme) ||
|
|
130
|
+
serializedStore !== JSON.stringify(source || {});
|
|
131
|
+
|
|
132
|
+
return { store, shouldPersist };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ensurePreferenceStore() {
|
|
136
|
+
const { raw, hasStored } = readRawPreferenceStore();
|
|
137
|
+
const { store, shouldPersist } = normalizePreferenceStore(raw, hasStored);
|
|
138
|
+
if (shouldPersist) safeWriteLocalStorage(UI_PREFERENCES_KEY, JSON.stringify(store));
|
|
139
|
+
return store;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getGuestPreferences() {
|
|
143
|
+
const store = ensurePreferenceStore();
|
|
144
|
+
return { ...store.guest };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getUserPreferences(userId) {
|
|
148
|
+
const store = ensurePreferenceStore();
|
|
149
|
+
const guestPreferences = { ...store.guest, quickAccessFilter: null };
|
|
150
|
+
if (userId === null || userId === undefined || userId === '') return guestPreferences;
|
|
151
|
+
const storedUserPreferences = store.users[String(userId)];
|
|
152
|
+
if (!storedUserPreferences) return guestPreferences;
|
|
153
|
+
return sanitizePreferenceRecord(storedUserPreferences, {
|
|
154
|
+
allowQuickAccess: true,
|
|
155
|
+
fallback: guestPreferences,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getPreferencesForActor(user) {
|
|
160
|
+
if (user && user.id !== null && user.id !== undefined) {
|
|
161
|
+
return getUserPreferences(user.id);
|
|
162
|
+
}
|
|
163
|
+
return getGuestPreferences();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sanitizePreferencesForActor(user, preferences, fallback = null) {
|
|
167
|
+
const actorFallback = fallback || getPreferencesForActor(user);
|
|
168
|
+
return sanitizePreferenceRecord({ ...actorFallback, ...(preferences || {}) }, {
|
|
169
|
+
allowQuickAccess: Boolean(user && user.id !== null && user.id !== undefined),
|
|
170
|
+
fallback: actorFallback,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function savePreferencesForActor(user, preferences) {
|
|
175
|
+
const store = ensurePreferenceStore();
|
|
176
|
+
const isUserScoped = Boolean(user && user.id !== null && user.id !== undefined);
|
|
177
|
+
const nextPreferences = sanitizePreferencesForActor(user, preferences);
|
|
178
|
+
|
|
179
|
+
if (isUserScoped) {
|
|
180
|
+
store.users[String(user.id)] = nextPreferences;
|
|
181
|
+
} else {
|
|
182
|
+
store.guest = sanitizePreferenceRecord(nextPreferences, {
|
|
183
|
+
allowQuickAccess: false,
|
|
184
|
+
fallback: cloneDefaultUiPreferences(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
safeWriteLocalStorage(UI_PREFERENCES_KEY, JSON.stringify(store));
|
|
189
|
+
return isUserScoped ? { ...store.users[String(user.id)] } : { ...store.guest };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractPreferencesFromState(state) {
|
|
193
|
+
return sanitizePreferenceRecord({
|
|
194
|
+
locale: state?.locale,
|
|
195
|
+
theme: state?.theme,
|
|
196
|
+
viewMode: state?.viewMode,
|
|
197
|
+
sortBy: state?.sortBy,
|
|
198
|
+
quickAccessFilter: state?.quickAccessFilter,
|
|
199
|
+
}, {
|
|
200
|
+
allowQuickAccess: Boolean(state?.currentUser && state.currentUser.id !== null && state.currentUser.id !== undefined),
|
|
201
|
+
fallback: getPreferencesForActor(state?.currentUser || null),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getInitialUiState() {
|
|
206
|
+
return getGuestPreferences();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
window.uiPreferences = {
|
|
210
|
+
UI_PREFERENCES_KEY,
|
|
211
|
+
LEGACY_THEME_KEY,
|
|
212
|
+
DEFAULT_UI_PREFERENCES,
|
|
213
|
+
getInitialUiState,
|
|
214
|
+
getGuestPreferences,
|
|
215
|
+
getUserPreferences,
|
|
216
|
+
getPreferencesForActor,
|
|
217
|
+
sanitizePreferencesForActor,
|
|
218
|
+
savePreferencesForActor,
|
|
219
|
+
extractPreferencesFromState,
|
|
220
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// RSA helpers for password encryption
|
|
2
|
+
(function () {
|
|
3
|
+
const cache = {
|
|
4
|
+
publicKeyPem: null,
|
|
5
|
+
cryptoKey: null,
|
|
6
|
+
fetchedAt: 0,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function nowSeconds() {
|
|
10
|
+
return Math.floor(Date.now() / 1000);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pemToArrayBuffer(pem) {
|
|
14
|
+
const b64 = pem
|
|
15
|
+
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
16
|
+
.replace('-----END PUBLIC KEY-----', '')
|
|
17
|
+
.replace(/\s+/g, '');
|
|
18
|
+
const raw = atob(b64);
|
|
19
|
+
const buffer = new ArrayBuffer(raw.length);
|
|
20
|
+
const view = new Uint8Array(buffer);
|
|
21
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
22
|
+
view[i] = raw.charCodeAt(i);
|
|
23
|
+
}
|
|
24
|
+
return buffer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchRsaPublicKey() {
|
|
28
|
+
if (cache.publicKeyPem && cache.cryptoKey) return cache.cryptoKey;
|
|
29
|
+
|
|
30
|
+
const resp = await fetch('/api/auth/rsa-public-key');
|
|
31
|
+
if (!resp.ok) throw new Error('failed_to_fetch_rsa_public_key');
|
|
32
|
+
const json = await resp.json();
|
|
33
|
+
const pem = json?.data?.publicKey;
|
|
34
|
+
if (!pem || typeof pem !== 'string') throw new Error('invalid_rsa_public_key');
|
|
35
|
+
|
|
36
|
+
const keyData = pemToArrayBuffer(pem);
|
|
37
|
+
const cryptoKey = await window.crypto.subtle.importKey(
|
|
38
|
+
'spki',
|
|
39
|
+
keyData,
|
|
40
|
+
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
|
41
|
+
true,
|
|
42
|
+
['encrypt'],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
cache.publicKeyPem = pem;
|
|
46
|
+
cache.cryptoKey = cryptoKey;
|
|
47
|
+
cache.fetchedAt = nowSeconds();
|
|
48
|
+
return cryptoKey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function arrayBufferToBase64(buffer) {
|
|
52
|
+
const bytes = new Uint8Array(buffer);
|
|
53
|
+
let binary = '';
|
|
54
|
+
for (let i = 0; i < bytes.byteLength; i += 1) {
|
|
55
|
+
binary += String.fromCharCode(bytes[i]);
|
|
56
|
+
}
|
|
57
|
+
return btoa(binary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function encryptPasswordWithTs(password) {
|
|
61
|
+
if (!password) throw new Error('password_required');
|
|
62
|
+
const ts = nowSeconds();
|
|
63
|
+
const plain = `${password}:${ts}`;
|
|
64
|
+
const key = await fetchRsaPublicKey();
|
|
65
|
+
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const data = encoder.encode(plain);
|
|
68
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
69
|
+
{ name: 'RSA-OAEP' },
|
|
70
|
+
key,
|
|
71
|
+
data,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const passwordEnc = arrayBufferToBase64(encrypted);
|
|
75
|
+
return { passwordEnc, ts };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function prepareEncryptedPasswordPayload(password) {
|
|
79
|
+
return encryptPasswordWithTs(password);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
window.security = {
|
|
83
|
+
fetchRsaPublicKey,
|
|
84
|
+
encryptPasswordWithTs,
|
|
85
|
+
prepareEncryptedPasswordPayload,
|
|
86
|
+
};
|
|
87
|
+
}());
|
|
88
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Theme utilities
|
|
2
|
+
function applyTheme(theme) {
|
|
3
|
+
const isDark = theme === 'dark' ||
|
|
4
|
+
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
5
|
+
window.__rhActiveThemeSetting = theme;
|
|
6
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getTheme() {
|
|
10
|
+
if (window.__rhActiveThemeSetting) return window.__rhActiveThemeSetting;
|
|
11
|
+
if (window.uiPreferences?.getGuestPreferences) {
|
|
12
|
+
return window.uiPreferences.getGuestPreferences().theme;
|
|
13
|
+
}
|
|
14
|
+
return localStorage.getItem('rh_theme') || 'system';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Listen for system theme changes when in 'system' mode
|
|
18
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
19
|
+
const theme = getTheme();
|
|
20
|
+
if (theme === 'system') applyTheme('system');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
window.applyTheme = applyTheme;
|
|
24
|
+
window.getTheme = getTheme;
|