add theme patterns

This commit is contained in:
JCEEE
2026-09-10 13:23:55 +01:00
parent 27492b1558
commit a270d965fe
13 changed files with 1429 additions and 27 deletions
@@ -98,6 +98,9 @@ export async function load(event) {
userId: session.id,
memberName: session.name,
memberColor: session.color || '',
memberPattern: session.pattern || '',
memberThemeSize: session.themeSize || '',
memberThemeOpacity: session.themeOpacity || '',
username: session.username || '',
role: session.role
}
+21 -1
View File
@@ -9,9 +9,26 @@
import { Sidebar, TopNav, Footer, Chat } from '$lib/components';
import { chatIcon } from '$lib/components/icons';
import { recordShortcut } from '$lib/shortcut';
import { themeShades } from '$lib/theme';
import '$lib/theme-patterns.css';
import { themeDraft } from '$lib/stores/theme.svelte';
import type { Session } from '$lib/types';
let { children, data } = $props();
// Background pattern ('' = none) tinted with the user's colour. A live
// draft (clicked in the picker, not yet saved) previews immediately;
// otherwise the saved value from the session applies.
let bgPattern = $derived(themeDraft.pattern ?? (data.session?.memberPattern || ''));
let bgColor = $derived(themeDraft.color ?? (data.session?.memberColor || '#6366f1'));
let bgOpacity = $derived(
themeDraft.opacity ??
(Number(data.session?.memberThemeOpacity) > 0 ? data.session?.memberThemeOpacity : 1)
);
let bgShades = $derived(themeShades(bgColor, Number(bgOpacity) || 1));
// Pattern size in vw units ('' = untouched → pattern class default).
let bgSize = $derived(themeDraft.size ?? (data.session?.memberThemeSize || ''));
let bgSizeVw = $derived(Number(bgSize) > 0 ? bgSize : '');
let session = $state<Session | null>(data.session);
let isParent = $state(data.isParent);
let role = $state(data.role);
@@ -177,7 +194,10 @@
</script>
<div class="layout-stage" class:chat-open={chatStore.open}>
<div class="app-shell">
<div
class="app-shell {bgPattern ? `pattern-${bgPattern}` : ''}"
style={`${bgSizeVw ? `--s:calc(${bgSizeVw} * 1vw);` : ''}--c1:${bgShades.c1};--c2:${bgShades.c2};--c3:${bgShades.c3};--c4:${bgShades.c4}`}
>
<Sidebar {famName} session={data.session} {isParent} {role} />
<TopNav
role={data.session ? role : ''}
@@ -37,6 +37,9 @@ export async function load(event) {
famId,
memberName: me.name,
memberColor: me.color,
pattern: me.pattern || '',
themeSize: me.themeSize || '',
themeOpacity: me.themeOpacity || '',
email: me.email || '',
session: true
};
@@ -63,6 +66,9 @@ export async function load(event) {
famId,
memberName: session.name || '',
memberColor: session.color || '',
pattern: session.pattern || '',
themeSize: session.themeSize || '',
themeOpacity: session.themeOpacity || '',
email: '',
session: true
};
@@ -77,6 +83,9 @@ export const actions = {
const name = fd.get('name') as string;
const color = fd.get('color') as string;
const email = fd.get('email') as string;
const pattern = (fd.get('pattern') as string) || '';
const themeSize = (fd.get('themeSize') as string) || '';
const themeOpacity = (fd.get('themeOpacity') as string) || '';
const s = servicesFor(event);
const famId = session.famId;
@@ -86,9 +95,20 @@ export const actions = {
const data: Record<string, string> = {};
if (name) data.name = name;
if (color) data.color = color;
data.pattern = pattern;
data.themeSize = themeSize;
data.themeOpacity = themeOpacity;
data.email = email || '';
const me = await s.fam.updateProfile(famId, data);
return { success: true, name: me.name, color: me.color, email: me.email };
return {
success: true,
name: me.name,
color: me.color,
pattern: me.pattern,
themeSize: me.themeSize,
themeOpacity: me.themeOpacity,
email: me.email
};
} catch (e) {
return { error: e instanceof Error ? e.message : 'Update failed' };
}
@@ -99,8 +119,19 @@ export const actions = {
const data: Record<string, string> = {};
if (name) data.name = name;
if (color) data.color = color;
data.pattern = pattern;
data.themeSize = themeSize;
data.themeOpacity = themeOpacity;
const me = await s.fam.updateProfile(famId, data);
return { success: true, name: me.name, color: me.color, email: '' };
return {
success: true,
name: me.name,
color: me.color,
pattern: me.pattern,
themeSize: me.themeSize,
themeOpacity: me.themeOpacity,
email: ''
};
} catch (e) {
return { error: e instanceof Error ? e.message : 'Update failed' };
}
@@ -1,12 +1,20 @@
<script lang="ts">
import { enhance, applyAction } from '$app/forms';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import { invalidateAll } from '$app/navigation';
import { ViewHeader, CardGrid, Card, Button, PatternPicker } from '$lib/components';
import { themeDraft } from '$lib/stores/theme.svelte';
import { THEME_SIZE_DEFAULT, THEME_OPACITY_DEFAULT } from '$lib/theme';
let { data, form } = $props();
let memberName = $state(data.memberName || '');
let memberColor = $state(data.memberColor || '#6366f1');
let memberEmail = $state(data.email || '');
let memberPattern = $state(data.pattern || '');
let memberSize = $state(Number(data.themeSize) > 0 ? Number(data.themeSize) : THEME_SIZE_DEFAULT);
let memberOpacity = $state(
Number(data.themeOpacity) > 0 ? Number(data.themeOpacity) : THEME_OPACITY_DEFAULT
);
const colors = [
'#6366f1',
@@ -32,21 +40,33 @@
</CardGrid>
{:else}
<CardGrid>
<Card cols={2} title="Appearance">
<form
method="POST"
action="?/update"
use:enhance={() => {
return async ({ result }) => {
// Apply the action result (so the success/error message
// shows) WITHOUT resetting the form — the default enhance
// clears name/email inputs on a successful submit.
if (result.type === 'success' || result.type === 'failure') {
await applyAction(result);
}
};
}}
>
<!-- One form for appearance + theme; display:contents keeps the
cards as grid items. -->
<form
style="display:contents"
method="POST"
action="?/update"
use:enhance={() => {
return async ({ result }) => {
// Apply the action result (so the success/error message
// shows) WITHOUT resetting the form — the default enhance
// clears name/email inputs on a successful submit.
if (result.type === 'success' || result.type === 'failure') {
await applyAction(result);
}
// Refresh session.pattern so the app background updates, and drop
// the live draft — the saved value is authoritative from here on.
if (result.type === 'success') {
themeDraft.pattern = null;
themeDraft.color = null;
themeDraft.size = null;
themeDraft.opacity = null;
await invalidateAll();
}
};
}}
>
<Card cols={2} title="Appearance">
<div style="margin-bottom:1rem">
<label style="display:block;font-size:0.85rem;color:#374151;margin-bottom:0.25rem">
Display Name
@@ -71,7 +91,10 @@
style="width:2rem;height:2rem;border-radius:50%;border:{c === memberColor
? '3px solid #111827'
: '2px solid transparent'};background:{c};cursor:pointer"
onclick={() => (memberColor = c)}
onclick={() => {
memberColor = c;
themeDraft.color = c;
}}
></button>
{/each}
</div>
@@ -102,8 +125,40 @@
<div class="form-actions">
<Button type="submit" variant="primary">Update preferences</Button>
</div>
</form>
</Card>
</Card>
<Card cols={1} title="Theme">
<p class="theme-hint">
Pick a background pattern — it's coloured with your chosen colour. None keeps the plain
background.
</p>
<PatternPicker
value={themeDraft.pattern ?? memberPattern}
color={memberColor}
size={themeDraft.size ?? memberSize}
opacity={themeDraft.opacity ?? memberOpacity}
onchange={(v) => {
memberPattern = v;
// Live preview on the app background before saving.
themeDraft.pattern = v;
themeDraft.color = memberColor;
}}
onsize={(v) => {
memberSize = v;
// Live scale preview before saving.
themeDraft.size = v;
}}
onopacity={(v) => {
memberOpacity = v;
// Live fade preview before saving.
themeDraft.opacity = v;
}}
/>
<input type="hidden" name="pattern" value={themeDraft.pattern ?? memberPattern} />
<input type="hidden" name="themeSize" value={themeDraft.size ?? memberSize} />
<input type="hidden" name="themeOpacity" value={themeDraft.opacity ?? memberOpacity} />
</Card>
</form>
<Card cols={1} title="Account">
<p style="font-size:0.85rem;color:#6b7280">
@@ -122,4 +177,9 @@
padding-top: 1.25rem;
border-top: 1px solid #e5e7eb;
}
.theme-hint {
font-size: 0.85rem;
color: #6b7280;
margin: 0 0 0.75rem;
}
</style>