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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user