Private
Public Access
1
0

Fix branding: live updates on save, reset clears everything, show defaults

- Clear inline preview styles before persisting CSS on save to avoid
  specificity conflicts that required Ctrl+Shift+R
- Reset to Defaults now deletes uploaded images and clears logoText
- ColorPicker emits value on Solid/Gradient toggle for instant preview
- Show default color swatch with dashed border when field is cleared
- persistThemeCSS now handles background-image rules like server-side

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-27 17:33:34 -05:00
parent 795e7c14ff
commit 8c9a5abc5a
3 changed files with 99 additions and 2 deletions
+11 -1
View File
@@ -3,11 +3,13 @@
label,
value = $bindable(''),
gradient = false,
defaultValue = '',
onchange
}: {
label: string;
value: string;
gradient?: boolean;
defaultValue?: string;
onchange?: (value: string) => void;
} = $props();
@@ -49,7 +51,11 @@
function switchMode(newMode: 'color' | 'gradient') {
mode = newMode;
// Don't emit — let the user pick values first
if (newMode === 'color') {
emitColor();
} else {
emitGradient();
}
}
function clear() {
@@ -63,6 +69,8 @@
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400">{label}</label>
{#if value}
<button onclick={clear} class="text-[10px] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">Reset</button>
{:else if defaultValue}
<span class="text-[10px] text-gray-400 dark:text-gray-500">Default</span>
{/if}
</div>
@@ -134,5 +142,7 @@
{#if value && mode === 'color'}
<div class="h-4 w-full rounded" style="background: {value}"></div>
{:else if !value && defaultValue}
<div class="h-4 w-full rounded border border-dashed border-gray-300 dark:border-gray-600" style="background: {defaultValue}"></div>
{/if}
</div>
+18
View File
@@ -74,6 +74,21 @@ export function persistThemeCSS(theme: TenantThemeData) {
if (theme.darkCardOpacity < 1) darkVars.push(` --card-backdrop-blur: blur(8px);`);
}
// Handle background images
const extraRules: string[] = [];
if (theme.lightPageBgImage) {
extraRules.push(`.layout-page-bg { background-image: url('${theme.lightPageBgImage}'); }`);
}
if (theme.lightNavBgImage) {
extraRules.push(`.layout-nav-bg { background-image: url('${theme.lightNavBgImage}'); }`);
}
if (theme.darkPageBgImage) {
extraRules.push(`html.dark .layout-page-bg { background-image: url('${theme.darkPageBgImage}'); }`);
}
if (theme.darkNavBgImage) {
extraRules.push(`html.dark .layout-nav-bg { background-image: url('${theme.darkNavBgImage}'); }`);
}
let css = '';
if (lightVars.length > 0) {
css += `:root {\n${lightVars.join('\n')}\n}\n`;
@@ -81,6 +96,9 @@ export function persistThemeCSS(theme: TenantThemeData) {
if (darkVars.length > 0) {
css += `html.dark {\n${darkVars.join('\n')}\n}\n`;
}
if (extraRules.length > 0) {
css += extraRules.join('\n') + '\n';
}
// Find or create the <style id="tenant-theme"> element
let styleEl = document.getElementById('tenant-theme') as HTMLStyleElement | null;
+70 -1
View File
@@ -32,6 +32,46 @@
clearThemePreview();
});
// CSS defaults from app.css — shown when a field is cleared
const DEFAULTS: Record<string, Record<string, string>> = {
light: {
pageBg: '#f9fafb',
navBg: '#0079bf',
columnBg: '#ebecf0',
columnBorder: 'transparent',
columnText: 'inherit',
cardBg: '#ffffff',
cardBorder: '#e5e7eb',
cardText: 'inherit',
primary: '#0079bf',
primaryHover: '#026aa7',
danger: '#eb5a46',
dangerHover: '#cf513d',
success: '#61bd4f',
warning: '#f2d600'
},
dark: {
pageBg: '#030712',
navBg: '#3b82f6',
columnBg: '#1f2937',
columnBorder: 'transparent',
columnText: 'inherit',
cardBg: '#374151',
cardBorder: '#4b5563',
cardText: 'inherit',
primary: '#3b82f6',
primaryHover: '#2563eb',
danger: '#ef4444',
dangerHover: '#dc2626',
success: '#22c55e',
warning: '#eab308'
}
};
function getDefault(name: string): string {
return DEFAULTS[activeTab]?.[name] ?? '';
}
// Helpers to get/set field values by prefix + name
function getField(name: string): any {
const key = `${activeTab}${name.charAt(0).toUpperCase()}${name.slice(1)}` as keyof TenantThemeData;
@@ -56,6 +96,7 @@
body: JSON.stringify({ ...draft, logoText: logoText || null })
});
if (ok) {
clearThemePreview();
persistThemeCSS(draft);
toast.success('Theme saved');
await invalidateAll();
@@ -79,12 +120,26 @@
for (const f of fields) empty[f] = null;
saving = true;
// Delete all uploaded images
const imageSlots = ['light-page-bg', 'dark-page-bg', 'light-nav-bg', 'dark-nav-bg', 'logo'];
await Promise.all(
imageSlots.map((slot) =>
api('/api/admin/branding/images', {
method: 'DELETE',
body: JSON.stringify({ slot })
}).catch(() => {})
)
);
// Clear all color/opacity fields + logoText
const { ok } = await api('/api/admin/branding', {
method: 'PUT',
body: JSON.stringify(empty)
body: JSON.stringify({ ...empty, logoText: null })
});
if (ok) {
draft = {};
logoText = '';
clearThemePreview();
persistThemeCSS({});
toast.success('Theme reset to defaults');
@@ -176,6 +231,7 @@
label="Background color"
value={getField('pageBg') || ''}
gradient={true}
defaultValue={getDefault('pageBg')}
onchange={(v) => setField('pageBg', v)}
/>
<ImageUploadField
@@ -195,6 +251,7 @@
label="Background color"
value={getField('navBg') || ''}
gradient={true}
defaultValue={getDefault('navBg')}
onchange={(v) => setField('navBg', v)}
/>
<ImageUploadField
@@ -213,16 +270,19 @@
<ColorPicker
label="Background"
value={getField('columnBg') || ''}
defaultValue={getDefault('columnBg')}
onchange={(v) => setField('columnBg', v)}
/>
<ColorPicker
label="Border"
value={getField('columnBorder') || ''}
defaultValue={getDefault('columnBorder')}
onchange={(v) => setField('columnBorder', v)}
/>
<ColorPicker
label="Text"
value={getField('columnText') || ''}
defaultValue={getDefault('columnText')}
onchange={(v) => setField('columnText', v)}
/>
</div>
@@ -249,16 +309,19 @@
<ColorPicker
label="Background"
value={getField('cardBg') || ''}
defaultValue={getDefault('cardBg')}
onchange={(v) => setField('cardBg', v)}
/>
<ColorPicker
label="Border"
value={getField('cardBorder') || ''}
defaultValue={getDefault('cardBorder')}
onchange={(v) => setField('cardBorder', v)}
/>
<ColorPicker
label="Text"
value={getField('cardText') || ''}
defaultValue={getDefault('cardText')}
onchange={(v) => setField('cardText', v)}
/>
</div>
@@ -299,31 +362,37 @@
<ColorPicker
label="Primary"
value={getField('primary') || ''}
defaultValue={getDefault('primary')}
onchange={(v) => setField('primary', v)}
/>
<ColorPicker
label="Primary hover"
value={getField('primaryHover') || ''}
defaultValue={getDefault('primaryHover')}
onchange={(v) => setField('primaryHover', v)}
/>
<ColorPicker
label="Danger"
value={getField('danger') || ''}
defaultValue={getDefault('danger')}
onchange={(v) => setField('danger', v)}
/>
<ColorPicker
label="Danger hover"
value={getField('dangerHover') || ''}
defaultValue={getDefault('dangerHover')}
onchange={(v) => setField('dangerHover', v)}
/>
<ColorPicker
label="Success"
value={getField('success') || ''}
defaultValue={getDefault('success')}
onchange={(v) => setField('success', v)}
/>
<ColorPicker
label="Warning"
value={getField('warning') || ''}
defaultValue={getDefault('warning')}
onchange={(v) => setField('warning', v)}
/>
</div>