diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c79731..8c67e6a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Tenant { tags Tag[] categories Category[] boardTemplates BoardTemplate[] + theme TenantTheme? @@map("tenants") } @@ -329,3 +330,59 @@ model BoardTemplate { @@map("board_templates") } + +// ─── Tenant Branding & Theming ─────────────────────────────── + +model TenantTheme { + id String @id @default(uuid()) @db.Uuid + tenantId String @unique @map("tenant_id") @db.Uuid + + // Light mode + lightPageBg String? @map("light_page_bg") + lightPageBgImage String? @map("light_page_bg_image") + lightNavBg String? @map("light_nav_bg") + lightNavBgImage String? @map("light_nav_bg_image") + lightColumnBg String? @map("light_column_bg") + lightColumnBorder String? @map("light_column_border") + lightColumnText String? @map("light_column_text") + lightColumnOpacity Float? @map("light_column_opacity") + lightCardBg String? @map("light_card_bg") + lightCardBorder String? @map("light_card_border") + lightCardText String? @map("light_card_text") + lightCardOpacity Float? @map("light_card_opacity") + lightCardRadius String? @map("light_card_radius") + lightPrimary String? @map("light_primary") + lightPrimaryHover String? @map("light_primary_hover") + lightDanger String? @map("light_danger") + lightDangerHover String? @map("light_danger_hover") + lightSuccess String? @map("light_success") + lightWarning String? @map("light_warning") + + // Dark mode + darkPageBg String? @map("dark_page_bg") + darkPageBgImage String? @map("dark_page_bg_image") + darkNavBg String? @map("dark_nav_bg") + darkNavBgImage String? @map("dark_nav_bg_image") + darkColumnBg String? @map("dark_column_bg") + darkColumnBorder String? @map("dark_column_border") + darkColumnText String? @map("dark_column_text") + darkColumnOpacity Float? @map("dark_column_opacity") + darkCardBg String? @map("dark_card_bg") + darkCardBorder String? @map("dark_card_border") + darkCardText String? @map("dark_card_text") + darkCardOpacity Float? @map("dark_card_opacity") + darkCardRadius String? @map("dark_card_radius") + darkPrimary String? @map("dark_primary") + darkPrimaryHover String? @map("dark_primary_hover") + darkDanger String? @map("dark_danger") + darkDangerHover String? @map("dark_danger_hover") + darkSuccess String? @map("dark_success") + darkWarning String? @map("dark_warning") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + @@map("tenant_themes") +} diff --git a/src/app.css b/src/app.css index faed7cf..6bcd6a9 100644 --- a/src/app.css +++ b/src/app.css @@ -4,8 +4,15 @@ @theme { --color-board-bg: #f1f2f4; + --color-page-bg: #f9fafb; + --color-nav-bg: #0079bf; --color-column-bg: #ebecf0; + --color-column-border: transparent; + --color-column-text: inherit; --color-card-bg: #ffffff; + --color-card-border: #e5e7eb; + --color-card-text: inherit; + --color-card-radius: 0.5rem; --color-primary: #0079bf; --color-primary-hover: #026aa7; --color-danger: #eb5a46; @@ -78,8 +85,15 @@ /* Dark mode overrides */ html.dark { --color-board-bg: #111827; + --color-page-bg: #030712; + --color-nav-bg: #3b82f6; --color-column-bg: #1f2937; + --color-column-border: transparent; + --color-column-text: inherit; --color-card-bg: #374151; + --color-card-border: #4b5563; + --color-card-text: inherit; + --color-card-radius: 0.5rem; --color-primary: #3b82f6; --color-primary-hover: #2563eb; --color-danger: #ef4444; diff --git a/src/app.d.ts b/src/app.d.ts index 4381197..7f13b9e 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,47 @@ // See https://svelte.dev/docs/kit/types#app.d.ts import type { PrismaClient } from '@prisma/client'; +export interface TenantThemeData { + lightPageBg?: string | null; + lightPageBgImage?: string | null; + lightNavBg?: string | null; + lightNavBgImage?: string | null; + lightColumnBg?: string | null; + lightColumnBorder?: string | null; + lightColumnText?: string | null; + lightColumnOpacity?: number | null; + lightCardBg?: string | null; + lightCardBorder?: string | null; + lightCardText?: string | null; + lightCardOpacity?: number | null; + lightCardRadius?: string | null; + lightPrimary?: string | null; + lightPrimaryHover?: string | null; + lightDanger?: string | null; + lightDangerHover?: string | null; + lightSuccess?: string | null; + lightWarning?: string | null; + darkPageBg?: string | null; + darkPageBgImage?: string | null; + darkNavBg?: string | null; + darkNavBgImage?: string | null; + darkColumnBg?: string | null; + darkColumnBorder?: string | null; + darkColumnText?: string | null; + darkColumnOpacity?: number | null; + darkCardBg?: string | null; + darkCardBorder?: string | null; + darkCardText?: string | null; + darkCardOpacity?: number | null; + darkCardRadius?: string | null; + darkPrimary?: string | null; + darkPrimaryHover?: string | null; + darkDanger?: string | null; + darkDangerHover?: string | null; + darkSuccess?: string | null; + darkWarning?: string | null; +} + declare global { namespace App { interface Error { @@ -12,6 +53,7 @@ declare global { id: string; name: string; slug: string; + theme?: TenantThemeData | null; } | null; user: { id: string; @@ -19,6 +61,7 @@ declare global { name: string; globalRole: string; tenantRole: string | null; + avatarUrl: string | null; } | null; session: { id: string; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 186114b..2e0c1de 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,6 +2,7 @@ import type { Handle } from '@sveltejs/kit'; import { resolveTenant, seedSystemTemplates } from '$lib/server/tenant.js'; import { validateSession, sessionCookieName } from '$lib/server/auth.js'; import { logger } from '$lib/server/logger.js'; +import { generateTenantCSS } from '$lib/server/theme-css.js'; // One-time seed on startup seedSystemTemplates().catch((e) => logger.error({ err: e }, 'Failed to seed system templates')); @@ -30,7 +31,8 @@ export const handle: Handle = async ({ event, resolve }) => { email: session.user.email, name: session.user.name, globalRole: session.user.globalRole, - tenantRole: session.user.tenantRole + tenantRole: session.user.tenantRole, + avatarUrl: session.user.avatarUrl }; // Ensure user belongs to current tenant (unless site admin) @@ -56,7 +58,14 @@ export const handle: Handle = async ({ event, resolve }) => { // 3. Read theme preference event.locals.theme = event.cookies.get('theme') || undefined; - const response = await resolve(event); + // 4. Generate tenant theme CSS for injection + const tenantCSS = generateTenantCSS(tenant?.theme); + + const response = await resolve(event, { + transformPageChunk: tenantCSS + ? ({ html }) => html.replace('
', `${tenantCSS}`) + : undefined + }); logger.info({ method: event.request.method, diff --git a/src/lib/components/ActivityFeed.svelte b/src/lib/components/ActivityFeed.svelte index eda39a3..14f33d1 100644 --- a/src/lib/components/ActivityFeed.svelte +++ b/src/lib/components/ActivityFeed.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { onBoardEvent, offBoardEvent } from '$lib/stores/socket.js'; import { api } from '$lib/utils/api.js'; + import Avatar from '$lib/components/Avatar.svelte'; type Props = { boardId: string; @@ -102,13 +103,14 @@ }); } - onMount(async () => { - const data = await fetchActivities(); - if (data) { - activities = data.activities; - nextCursor = data.nextCursor; - } - loading = false; + onMount(() => { + fetchActivities().then((data) => { + if (data) { + activities = data.activities; + nextCursor = data.nextCursor; + } + loading = false; + }); onBoardEvent('activity:new', onActivityNew); return () => offBoardEvent('activity:new', onActivityNew); @@ -127,11 +129,8 @@ {:else} {#each activities as activity (activity.id)}
diff --git a/src/lib/components/AssigneePicker.svelte b/src/lib/components/AssigneePicker.svelte
index 6fee6ae..3a81ca9 100644
--- a/src/lib/components/AssigneePicker.svelte
+++ b/src/lib/components/AssigneePicker.svelte
@@ -1,5 +1,6 @@
+
+{#if showImg}
+ (imgError = true)}
+ />
+{:else}
+