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');
|
||||
requireTenantAdmin(locals.user);
|
||||
|
||||
const [theme, tenantData] = await withTenant(tenant.id, async (tx) => {
|
||||
const t = await 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;
|
||||
const theme = await withTenant(tenant.id, async (tx) => {
|
||||
return tx.tenantTheme.findUnique({ where: { tenantId: tenant.id } });
|
||||
});
|
||||
|
||||
return {
|
||||
theme: theme ?? null,
|
||||
tenantName: tenant.name,
|
||||
logoUrl: tenantData?.logoUrl ?? null,
|
||||
logoText: tenantData?.logoText ?? null
|
||||
logoUrl: tenant.logoUrl ?? null,
|
||||
logoText: tenant.logoText ?? null
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { toast } from '$lib/stores/toasts.svelte.js';
|
||||
import { getTheme } from '$lib/stores/theme.svelte.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 ImageUploadField from '$lib/components/ImageUploadField.svelte';
|
||||
import type { TenantThemeData } from '../../../app.d.js';
|
||||
@@ -55,6 +56,7 @@
|
||||
body: JSON.stringify({ ...draft, logoText: logoText || null })
|
||||
});
|
||||
if (ok) {
|
||||
persistThemeCSS(draft);
|
||||
toast.success('Theme saved');
|
||||
await invalidateAll();
|
||||
}
|
||||
@@ -84,6 +86,7 @@
|
||||
if (ok) {
|
||||
draft = {};
|
||||
clearThemePreview();
|
||||
persistThemeCSS({});
|
||||
toast.success('Theme reset to defaults');
|
||||
await invalidateAll();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user