feat(themes): custom CSS themes per user (upload, paste, live)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m21s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m21s
- Theme manager in settings: upload a .css file or paste CSS, template
download/copy/insert, edit and delete; custom themes show up in the
gallery and the quick switchers
- Themes are injected live as <style id="mindsift-custom-theme">; the
active one is cached so it applies on boot without flashing
- Backend: user_themes table and GET/POST /themes, DELETE /themes/{id}
(per user, max 200 KB)
- Docs: theming, REST, database
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84395f3322
commit
bf0d68e7e1
15 files changed
+653
-51
No files matched your search
+34
-6
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Layers,
|
||||
CheckSquare,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
Mic,
|
||||
MicOff,
|
||||
} from 'lucide-react';
|
||||
import { AuthStatus, DashboardStats, PriorityLevel, SharedList, Thought, User } from './lib/types';
|
||||
import { AuthStatus, DashboardStats, PriorityLevel, SharedList, Thought, User, UserTheme } from './lib/types';
|
||||
import {
|
||||
api,
|
||||
clearToken,
|
||||
@@ -36,7 +36,9 @@ import {
|
||||
} from './lib/api';
|
||||
import {
|
||||
AVAILABLE_THEMES,
|
||||
customThemeOption,
|
||||
initThemeOnBoot,
|
||||
isCustomThemeId,
|
||||
setActiveTheme as applyTheme,
|
||||
ThemeId,
|
||||
} from './lib/theme';
|
||||
@@ -115,6 +117,8 @@ export const App: React.FC = () => {
|
||||
instanceName: 'MindSift',
|
||||
});
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [customThemes, setCustomThemes] = useState<UserTheme[]>([]);
|
||||
const allThemes = useMemo(() => [...AVAILABLE_THEMES, ...customThemes.map(customThemeOption)], [customThemes]);
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
inboxCount: 0,
|
||||
tasksOpenCount: 0,
|
||||
@@ -416,12 +420,33 @@ export const App: React.FC = () => {
|
||||
setShowProfileMenu(false);
|
||||
};
|
||||
|
||||
const activateTheme = (themeId: ThemeId, themes: UserTheme[]) => {
|
||||
const css = themes.find((t) => 'custom:' + t.id === themeId)?.css;
|
||||
applyTheme(themeId, css);
|
||||
setActiveThemeState(isCustomThemeId(themeId) && css === undefined ? 'dark' : themeId);
|
||||
};
|
||||
|
||||
const handleChangeTheme = (themeId: ThemeId) => {
|
||||
triggerHaptic(10);
|
||||
applyTheme(themeId);
|
||||
setActiveThemeState(themeId);
|
||||
activateTheme(themeId, customThemes);
|
||||
};
|
||||
|
||||
// Custom themes live on the server; after login refresh them and the active one (it may have
|
||||
// been edited or deleted on another device)
|
||||
const handleCustomThemesChange = (themes: UserTheme[], activate?: ThemeId) => {
|
||||
setCustomThemes(themes);
|
||||
const next = activate ?? activeTheme;
|
||||
if (activate || isCustomThemeId(next)) activateTheme(next, themes);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
api
|
||||
.getThemes()
|
||||
.then((themes) => handleCustomThemesChange(themes))
|
||||
.catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user?.id]);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
try {
|
||||
const [meRes, deckRes, listsRes] = await Promise.all([
|
||||
@@ -770,7 +795,7 @@ export const App: React.FC = () => {
|
||||
<span>Design-Theme</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{AVAILABLE_THEMES.map((t) => (
|
||||
{allThemes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
@@ -1009,7 +1034,10 @@ export const App: React.FC = () => {
|
||||
<UserSettingsView
|
||||
currentUser={user}
|
||||
activeTheme={activeTheme}
|
||||
themes={allThemes}
|
||||
customThemes={customThemes}
|
||||
onThemeChange={handleChangeTheme}
|
||||
onCustomThemesChange={handleCustomThemesChange}
|
||||
onUserUpdated={(u) => {
|
||||
setUser(u);
|
||||
refreshAll();
|
||||
@@ -1320,7 +1348,7 @@ export const App: React.FC = () => {
|
||||
<span>Design-Theme</span>
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{AVAILABLE_THEMES.map((t) => (
|
||||
{allThemes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user