Fix branding page 500 error and make theme changes instant
1. Fix 500 on hard refresh: branding page load was querying tenant for logoUrl/logoText fields that may not exist in DB yet. Now uses locals.tenant data (from resolver) instead of a separate query. 2. Instant theme application: after saving, persistThemeCSS() generates CSS from the draft and writes it into the <style id="tenant-theme"> element in the DOM. This means navigating to any page after saving immediately shows the new theme without a hard refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
import type { TenantThemeData } from '../../app.d.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side CSS variable mapping — mirrors server-side theme-css.ts
|
||||||
|
*/
|
||||||
|
const FIELD_MAP: Record<string, { cssVar: string; mode: 'light' | 'dark' }> = {};
|
||||||
|
|
||||||
|
const vars = [
|
||||||
|
['PageBg', '--color-page-bg'],
|
||||||
|
['NavBg', '--color-nav-bg'],
|
||||||
|
['ColumnBg', '--color-column-bg'],
|
||||||
|
['ColumnBorder', '--color-column-border'],
|
||||||
|
['ColumnText', '--color-column-text'],
|
||||||
|
['CardBg', '--color-card-bg'],
|
||||||
|
['CardBorder', '--color-card-border'],
|
||||||
|
['CardText', '--color-card-text'],
|
||||||
|
['CardRadius', '--color-card-radius'],
|
||||||
|
['Primary', '--color-primary'],
|
||||||
|
['PrimaryHover', '--color-primary-hover'],
|
||||||
|
['Danger', '--color-danger'],
|
||||||
|
['DangerHover', '--color-danger-hover'],
|
||||||
|
['Success', '--color-success'],
|
||||||
|
['Warning', '--color-warning']
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const [suffix, cssVar] of vars) {
|
||||||
|
FIELD_MAP[`light${suffix}`] = { cssVar, mode: 'light' };
|
||||||
|
FIELD_MAP[`dark${suffix}`] = { cssVar, mode: 'dark' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgba(hex: string, opacity: number): string {
|
||||||
|
const h = hex.replace('#', '');
|
||||||
|
const r = parseInt(h.substring(0, 2), 16);
|
||||||
|
const g = parseInt(h.substring(2, 4), 16);
|
||||||
|
const b = parseInt(h.substring(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates CSS from theme data and writes it into the <style id="tenant-theme"> element.
|
||||||
|
* This mirrors what generateTenantCSS does server-side, but runs in the browser
|
||||||
|
* so theme changes take effect immediately without a full page reload.
|
||||||
|
*/
|
||||||
|
export function persistThemeCSS(theme: TenantThemeData) {
|
||||||
|
const lightVars: string[] = [];
|
||||||
|
const darkVars: string[] = [];
|
||||||
|
|
||||||
|
for (const [field, { cssVar, mode }] of Object.entries(FIELD_MAP)) {
|
||||||
|
const value = (theme as any)[field];
|
||||||
|
if (value == null) continue;
|
||||||
|
const target = mode === 'dark' ? darkVars : lightVars;
|
||||||
|
target.push(` ${cssVar}: ${value};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle opacity for columns
|
||||||
|
if (theme.lightColumnOpacity != null && theme.lightColumnBg?.startsWith('#')) {
|
||||||
|
const idx = lightVars.findIndex((v) => v.includes('--color-column-bg:'));
|
||||||
|
if (idx !== -1) lightVars[idx] = ` --color-column-bg: ${hexToRgba(theme.lightColumnBg, theme.lightColumnOpacity)};`;
|
||||||
|
if (theme.lightColumnOpacity < 1) lightVars.push(` --column-backdrop-blur: blur(8px);`);
|
||||||
|
}
|
||||||
|
if (theme.lightCardOpacity != null && theme.lightCardBg?.startsWith('#')) {
|
||||||
|
const idx = lightVars.findIndex((v) => v.includes('--color-card-bg:'));
|
||||||
|
if (idx !== -1) lightVars[idx] = ` --color-card-bg: ${hexToRgba(theme.lightCardBg, theme.lightCardOpacity)};`;
|
||||||
|
if (theme.lightCardOpacity < 1) lightVars.push(` --card-backdrop-blur: blur(8px);`);
|
||||||
|
}
|
||||||
|
if (theme.darkColumnOpacity != null && theme.darkColumnBg?.startsWith('#')) {
|
||||||
|
const idx = darkVars.findIndex((v) => v.includes('--color-column-bg:'));
|
||||||
|
if (idx !== -1) darkVars[idx] = ` --color-column-bg: ${hexToRgba(theme.darkColumnBg, theme.darkColumnOpacity)};`;
|
||||||
|
if (theme.darkColumnOpacity < 1) darkVars.push(` --column-backdrop-blur: blur(8px);`);
|
||||||
|
}
|
||||||
|
if (theme.darkCardOpacity != null && theme.darkCardBg?.startsWith('#')) {
|
||||||
|
const idx = darkVars.findIndex((v) => v.includes('--color-card-bg:'));
|
||||||
|
if (idx !== -1) darkVars[idx] = ` --color-card-bg: ${hexToRgba(theme.darkCardBg, theme.darkCardOpacity)};`;
|
||||||
|
if (theme.darkCardOpacity < 1) darkVars.push(` --card-backdrop-blur: blur(8px);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let css = '';
|
||||||
|
if (lightVars.length > 0) {
|
||||||
|
css += `:root {\n${lightVars.join('\n')}\n}\n`;
|
||||||
|
}
|
||||||
|
if (darkVars.length > 0) {
|
||||||
|
css += `html.dark {\n${darkVars.join('\n')}\n}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create the <style id="tenant-theme"> element
|
||||||
|
let styleEl = document.getElementById('tenant-theme') as HTMLStyleElement | null;
|
||||||
|
if (!styleEl) {
|
||||||
|
styleEl = document.createElement('style');
|
||||||
|
styleEl.id = 'tenant-theme';
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
}
|
||||||
|
styleEl.textContent = css;
|
||||||
|
}
|
||||||
@@ -8,19 +8,14 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
if (!tenant) throw error(400, 'Unknown tenant');
|
if (!tenant) throw error(400, 'Unknown tenant');
|
||||||
requireTenantAdmin(locals.user);
|
requireTenantAdmin(locals.user);
|
||||||
|
|
||||||
const [theme, tenantData] = await withTenant(tenant.id, async (tx) => {
|
const theme = await withTenant(tenant.id, async (tx) => {
|
||||||
const t = await tx.tenantTheme.findUnique({ where: { tenantId: tenant.id } });
|
return tx.tenantTheme.findUnique({ where: { tenantId: tenant.id } });
|
||||||
const td = await tx.tenant.findUnique({
|
|
||||||
where: { id: tenant.id },
|
|
||||||
select: { logoUrl: true, logoText: true }
|
|
||||||
});
|
|
||||||
return [t, td] as const;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: theme ?? null,
|
theme: theme ?? null,
|
||||||
tenantName: tenant.name,
|
tenantName: tenant.name,
|
||||||
logoUrl: tenantData?.logoUrl ?? null,
|
logoUrl: tenant.logoUrl ?? null,
|
||||||
logoText: tenantData?.logoText ?? null
|
logoText: tenant.logoText ?? null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { toast } from '$lib/stores/toasts.svelte.js';
|
import { toast } from '$lib/stores/toasts.svelte.js';
|
||||||
import { getTheme } from '$lib/stores/theme.svelte.js';
|
import { getTheme } from '$lib/stores/theme.svelte.js';
|
||||||
import { applyThemePreview, clearThemePreview } from '$lib/utils/theme-preview.js';
|
import { applyThemePreview, clearThemePreview } from '$lib/utils/theme-preview.js';
|
||||||
|
import { persistThemeCSS } from '$lib/utils/theme-persist.js';
|
||||||
import ColorPicker from '$lib/components/ColorPicker.svelte';
|
import ColorPicker from '$lib/components/ColorPicker.svelte';
|
||||||
import ImageUploadField from '$lib/components/ImageUploadField.svelte';
|
import ImageUploadField from '$lib/components/ImageUploadField.svelte';
|
||||||
import type { TenantThemeData } from '../../../app.d.js';
|
import type { TenantThemeData } from '../../../app.d.js';
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
body: JSON.stringify({ ...draft, logoText: logoText || null })
|
body: JSON.stringify({ ...draft, logoText: logoText || null })
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
persistThemeCSS(draft);
|
||||||
toast.success('Theme saved');
|
toast.success('Theme saved');
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,7 @@
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
draft = {};
|
draft = {};
|
||||||
clearThemePreview();
|
clearThemePreview();
|
||||||
|
persistThemeCSS({});
|
||||||
toast.success('Theme reset to defaults');
|
toast.success('Theme reset to defaults');
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user