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
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { setActiveTheme, initThemeOnBoot, customThemeOption, getActiveTheme } from '../theme';
|
||||
|
||||
const lightCss = `:root[data-theme="papier"] { --bg-app: #fafafa; --accent-primary: #15803d; --radius-card: 8px; }`;
|
||||
|
||||
describe('custom themes', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
document.getElementById('mindsift-custom-theme')?.remove();
|
||||
});
|
||||
|
||||
it('injects the CSS live and uses its data-theme', () => {
|
||||
setActiveTheme('custom:thm_1', lightCss);
|
||||
const style = document.getElementById('mindsift-custom-theme');
|
||||
expect(style?.textContent).toBe(lightCss);
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('papier');
|
||||
// light background -> no Tailwind dark class
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores data-theme mentioned in comments (like in the template)', () => {
|
||||
setActiveTheme('custom:thm_2', `/* benenne [data-theme="mein-theme"] um */\n:root[data-theme="wald"] { --bg-app: #0b1a10; }`);
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('wald');
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('restores the active custom theme on boot from the cache', () => {
|
||||
setActiveTheme('custom:thm_1', lightCss);
|
||||
document.getElementById('mindsift-custom-theme')?.remove();
|
||||
expect(initThemeOnBoot()).toBe('custom:thm_1');
|
||||
expect(document.getElementById('mindsift-custom-theme')?.textContent).toBe(lightCss);
|
||||
});
|
||||
|
||||
it('removes the custom CSS when switching back to a built-in theme', () => {
|
||||
setActiveTheme('custom:thm_1', lightCss);
|
||||
setActiveTheme('dark');
|
||||
expect(document.getElementById('mindsift-custom-theme')).toBeNull();
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
||||
expect(document.documentElement.classList.contains('dark')).toBe(true);
|
||||
expect(getActiveTheme()).toBe('dark');
|
||||
});
|
||||
|
||||
it('falls back to dark when a custom theme has no CSS', () => {
|
||||
setActiveTheme('custom:gone');
|
||||
expect(getActiveTheme()).toBe('dark');
|
||||
});
|
||||
|
||||
it('builds gallery previews from the CSS variables', () => {
|
||||
const opt = customThemeOption({ id: 'thm_1', name: 'Papier', css: lightCss, createdAt: '', updatedAt: '' });
|
||||
expect(opt.id).toBe('custom:thm_1');
|
||||
expect(opt.previewBg).toBe('#fafafa');
|
||||
expect(opt.previewAccent).toBe('#15803d');
|
||||
expect(opt.isRounded).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user