Private
Public Access
1
0

Add tenant branding & theming system with admin UI

Tenant admins can now customize their workspace appearance: page/nav
backgrounds (solid, gradient, or image), column/card colors, borders,
text, opacity with glass blur, card corner radius, and accent colors.
Light and dark mode are configured separately. Theme CSS is injected
server-side via transformPageChunk to prevent FOUC.

Includes avatar components, account page updates, and pre-existing
type error fixes (Buffer, async onMount, InputJsonValue).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-26 21:56:39 -05:00
parent 029b467eae
commit 3a11ed01e9
30 changed files with 1709 additions and 60 deletions
+8 -15
View File
@@ -10,6 +10,7 @@
import CardModal from '$lib/components/CardModal.svelte';
import ActivityFeed from '$lib/components/ActivityFeed.svelte';
import BoardFilters from '$lib/components/BoardFilters.svelte';
import Avatar from '$lib/components/Avatar.svelte';
import type { FilterState } from '$lib/components/BoardFilters.svelte';
let { data } = $props();
@@ -402,12 +403,7 @@
<span class="text-xs text-gray-400 dark:text-gray-500">Also here:</span>
<div class="flex -space-x-1.5">
{#each viewingUsers.filter((u) => u.id !== data.user?.id) as viewer}
<div
class="w-6 h-6 rounded-full bg-emerald-500 text-white text-[11px] flex items-center justify-center border-2 border-white dark:border-gray-900"
title={viewer.name}
>
{viewer.name[0].toUpperCase()}
</div>
<Avatar name={viewer.name} avatarUrl={viewer.avatarUrl} size="sm" class="border-2 border-white dark:border-gray-900" title={viewer.name} />
{/each}
</div>
</div>
@@ -468,7 +464,8 @@
{#each columns as column (column.id)}
{@const visibleCards = filterCards(column.cards)}
<div
class="flex-shrink-0 w-64 sm:w-72 bg-[var(--color-column-bg)] rounded-xl flex flex-col max-h-full"
class="flex-shrink-0 w-64 sm:w-72 rounded-xl flex flex-col max-h-full"
style="background: var(--color-column-bg); border: 1px solid var(--color-column-border); color: var(--color-column-text); backdrop-filter: var(--column-backdrop-blur, none);"
animate:flip={{ duration: flipDurationMs }}
>
<!-- Column header -->
@@ -536,7 +533,8 @@
{@const progress = getChecklistProgress(card)}
{@const dueUrgency = getDueUrgency(card.dueDate)}
<button
class="w-full text-left mb-2 rounded-lg bg-[var(--color-card-bg)] p-3 shadow-sm hover:shadow-md border border-gray-200 hover:border-gray-300 dark:border-gray-600 dark:hover:border-gray-500 transition cursor-pointer"
class="w-full text-left mb-2 p-3 shadow-sm hover:shadow-md transition cursor-pointer"
style="background: var(--color-card-bg); border: 1px solid var(--color-card-border); color: var(--color-card-text); border-radius: var(--color-card-radius); backdrop-filter: var(--card-backdrop-blur, none);"
animate:flip={{ duration: flipDurationMs }}
onclick={() => (selectedCard = card)}
>
@@ -589,12 +587,7 @@
{#if card.assignees && card.assignees.length > 0}
<div class="flex -space-x-1 mt-2">
{#each card.assignees.slice(0, 3) as assignee}
<div
class="w-5 h-5 rounded-full bg-[var(--color-primary)] text-white text-[10px] flex items-center justify-center border border-white"
title={assignee.user.name}
>
{assignee.user.name[0].toUpperCase()}
</div>
<Avatar name={assignee.user.name} avatarUrl={assignee.user.avatarUrl} size="xs" class="border border-white" title={assignee.user.name} />
{/each}
{#if card.assignees.length > 3}
<div
@@ -681,7 +674,7 @@
{#if data.canEdit}
<div class="flex-shrink-0 w-72">
{#if showAddColumn}
<div class="bg-[var(--color-column-bg)] rounded-xl p-3">
<div class="rounded-xl p-3" style="background: var(--color-column-bg);">
<form onsubmit={(e) => { e.preventDefault(); addColumn(); }}>
<input
bind:value={addingColumnTitle}