add theme patterns
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user