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,
|
label,
|
||||||
value = $bindable(''),
|
value = $bindable(''),
|
||||||
gradient = false,
|
gradient = false,
|
||||||
|
defaultValue = '',
|
||||||
onchange
|
onchange
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
gradient?: boolean;
|
gradient?: boolean;
|
||||||
|
defaultValue?: string;
|
||||||
onchange?: (value: string) => void;
|
onchange?: (value: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -49,7 +51,11 @@
|
|||||||
|
|
||||||
function switchMode(newMode: 'color' | 'gradient') {
|
function switchMode(newMode: 'color' | 'gradient') {
|
||||||
mode = newMode;
|
mode = newMode;
|
||||||
// Don't emit — let the user pick values first
|
if (newMode === 'color') {
|
||||||
|
emitColor();
|
||||||
|
} else {
|
||||||
|
emitGradient();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
@@ -63,6 +69,8 @@
|
|||||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400">{label}</label>
|
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400">{label}</label>
|
||||||
{#if value}
|
{#if value}
|
||||||
<button onclick={clear} class="text-[10px] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">Reset</button>
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,5 +142,7 @@
|
|||||||
|
|
||||||
{#if value && mode === 'color'}
|
{#if value && mode === 'color'}
|
||||||
<div class="h-4 w-full rounded" style="background: {value}"></div>
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ export function persistThemeCSS(theme: TenantThemeData) {
|
|||||||
if (theme.darkCardOpacity < 1) darkVars.push(` --card-backdrop-blur: blur(8px);`);
|
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 = '';
|
let css = '';
|
||||||
if (lightVars.length > 0) {
|
if (lightVars.length > 0) {
|
||||||
css += `:root {\n${lightVars.join('\n')}\n}\n`;
|
css += `:root {\n${lightVars.join('\n')}\n}\n`;
|
||||||
@@ -81,6 +96,9 @@ export function persistThemeCSS(theme: TenantThemeData) {
|
|||||||
if (darkVars.length > 0) {
|
if (darkVars.length > 0) {
|
||||||
css += `html.dark {\n${darkVars.join('\n')}\n}\n`;
|
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
|
// Find or create the <style id="tenant-theme"> element
|
||||||
let styleEl = document.getElementById('tenant-theme') as HTMLStyleElement | null;
|
let styleEl = document.getElementById('tenant-theme') as HTMLStyleElement | null;
|
||||||
|
|||||||
@@ -32,6 +32,46 @@
|
|||||||
clearThemePreview();
|
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
|
// Helpers to get/set field values by prefix + name
|
||||||
function getField(name: string): any {
|
function getField(name: string): any {
|
||||||
const key = `${activeTab}${name.charAt(0).toUpperCase()}${name.slice(1)}` as keyof TenantThemeData;
|
const key = `${activeTab}${name.charAt(0).toUpperCase()}${name.slice(1)}` as keyof TenantThemeData;
|
||||||
@@ -56,6 +96,7 @@
|
|||||||
body: JSON.stringify({ ...draft, logoText: logoText || null })
|
body: JSON.stringify({ ...draft, logoText: logoText || null })
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
clearThemePreview();
|
||||||
persistThemeCSS(draft);
|
persistThemeCSS(draft);
|
||||||
toast.success('Theme saved');
|
toast.success('Theme saved');
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
@@ -79,12 +120,26 @@
|
|||||||
for (const f of fields) empty[f] = null;
|
for (const f of fields) empty[f] = null;
|
||||||
|
|
||||||
saving = true;
|
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', {
|
const { ok } = await api('/api/admin/branding', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(empty)
|
body: JSON.stringify({ ...empty, logoText: null })
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
draft = {};
|
draft = {};
|
||||||
|
logoText = '';
|
||||||
clearThemePreview();
|
clearThemePreview();
|
||||||
persistThemeCSS({});
|
persistThemeCSS({});
|
||||||
toast.success('Theme reset to defaults');
|
toast.success('Theme reset to defaults');
|
||||||
@@ -176,6 +231,7 @@
|
|||||||
label="Background color"
|
label="Background color"
|
||||||
value={getField('pageBg') || ''}
|
value={getField('pageBg') || ''}
|
||||||
gradient={true}
|
gradient={true}
|
||||||
|
defaultValue={getDefault('pageBg')}
|
||||||
onchange={(v) => setField('pageBg', v)}
|
onchange={(v) => setField('pageBg', v)}
|
||||||
/>
|
/>
|
||||||
<ImageUploadField
|
<ImageUploadField
|
||||||
@@ -195,6 +251,7 @@
|
|||||||
label="Background color"
|
label="Background color"
|
||||||
value={getField('navBg') || ''}
|
value={getField('navBg') || ''}
|
||||||
gradient={true}
|
gradient={true}
|
||||||
|
defaultValue={getDefault('navBg')}
|
||||||
onchange={(v) => setField('navBg', v)}
|
onchange={(v) => setField('navBg', v)}
|
||||||
/>
|
/>
|
||||||
<ImageUploadField
|
<ImageUploadField
|
||||||
@@ -213,16 +270,19 @@
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Background"
|
label="Background"
|
||||||
value={getField('columnBg') || ''}
|
value={getField('columnBg') || ''}
|
||||||
|
defaultValue={getDefault('columnBg')}
|
||||||
onchange={(v) => setField('columnBg', v)}
|
onchange={(v) => setField('columnBg', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Border"
|
label="Border"
|
||||||
value={getField('columnBorder') || ''}
|
value={getField('columnBorder') || ''}
|
||||||
|
defaultValue={getDefault('columnBorder')}
|
||||||
onchange={(v) => setField('columnBorder', v)}
|
onchange={(v) => setField('columnBorder', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Text"
|
label="Text"
|
||||||
value={getField('columnText') || ''}
|
value={getField('columnText') || ''}
|
||||||
|
defaultValue={getDefault('columnText')}
|
||||||
onchange={(v) => setField('columnText', v)}
|
onchange={(v) => setField('columnText', v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,16 +309,19 @@
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Background"
|
label="Background"
|
||||||
value={getField('cardBg') || ''}
|
value={getField('cardBg') || ''}
|
||||||
|
defaultValue={getDefault('cardBg')}
|
||||||
onchange={(v) => setField('cardBg', v)}
|
onchange={(v) => setField('cardBg', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Border"
|
label="Border"
|
||||||
value={getField('cardBorder') || ''}
|
value={getField('cardBorder') || ''}
|
||||||
|
defaultValue={getDefault('cardBorder')}
|
||||||
onchange={(v) => setField('cardBorder', v)}
|
onchange={(v) => setField('cardBorder', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Text"
|
label="Text"
|
||||||
value={getField('cardText') || ''}
|
value={getField('cardText') || ''}
|
||||||
|
defaultValue={getDefault('cardText')}
|
||||||
onchange={(v) => setField('cardText', v)}
|
onchange={(v) => setField('cardText', v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,31 +362,37 @@
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Primary"
|
label="Primary"
|
||||||
value={getField('primary') || ''}
|
value={getField('primary') || ''}
|
||||||
|
defaultValue={getDefault('primary')}
|
||||||
onchange={(v) => setField('primary', v)}
|
onchange={(v) => setField('primary', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Primary hover"
|
label="Primary hover"
|
||||||
value={getField('primaryHover') || ''}
|
value={getField('primaryHover') || ''}
|
||||||
|
defaultValue={getDefault('primaryHover')}
|
||||||
onchange={(v) => setField('primaryHover', v)}
|
onchange={(v) => setField('primaryHover', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Danger"
|
label="Danger"
|
||||||
value={getField('danger') || ''}
|
value={getField('danger') || ''}
|
||||||
|
defaultValue={getDefault('danger')}
|
||||||
onchange={(v) => setField('danger', v)}
|
onchange={(v) => setField('danger', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Danger hover"
|
label="Danger hover"
|
||||||
value={getField('dangerHover') || ''}
|
value={getField('dangerHover') || ''}
|
||||||
|
defaultValue={getDefault('dangerHover')}
|
||||||
onchange={(v) => setField('dangerHover', v)}
|
onchange={(v) => setField('dangerHover', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Success"
|
label="Success"
|
||||||
value={getField('success') || ''}
|
value={getField('success') || ''}
|
||||||
|
defaultValue={getDefault('success')}
|
||||||
onchange={(v) => setField('success', v)}
|
onchange={(v) => setField('success', v)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Warning"
|
label="Warning"
|
||||||
value={getField('warning') || ''}
|
value={getField('warning') || ''}
|
||||||
|
defaultValue={getDefault('warning')}
|
||||||
onchange={(v) => setField('warning', v)}
|
onchange={(v) => setField('warning', v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user