feat: integrate Matrix chat (Option 2 - credentials stored in Supabase)

- Add matrix-js-sdk, marked, highlight.js, twemoji, @tanstack/svelte-virtual deps
- Copy Matrix core layer: /matrix/, /stores/matrix.ts, /cache/, /services/
- Copy Matrix components: matrix/, message/, chat-layout/, chat-settings/
- Copy UI components: EmojiPicker, Twemoji, ImagePreviewModal, VirtualList
- Copy utils: emojiData, twemoji, twemojiGlobal
- Replace lucide-svelte with Material Symbols in SyncRecoveryBanner
- Extend Avatar with xs size and status indicator prop
- Fix ui.ts store conflict: re-export toasts from toast.svelte.ts
- Add migration 020_matrix_credentials for storing Matrix tokens per user/org
- Add /api/matrix-credentials endpoint (GET/POST/DELETE)
- Create [orgSlug]/chat page with Matrix login form + full chat UI
- Add Chat to sidebar navigation
This commit is contained in:
AlacrisDevs
2026-02-07 01:44:06 +02:00
parent e55881b38b
commit d1ce5d0951
62 changed files with 11432 additions and 41 deletions

196
src/lib/stores/theme.ts Normal file
View File

@@ -0,0 +1,196 @@
/**
* Theme Store - Manages app theme (dark/light mode and accent colors)
*/
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
export type ThemeMode = 'dark' | 'light';
export interface ThemeColors {
primary: string;
name: string;
}
export const PRESET_COLORS: ThemeColors[] = [
{ name: 'Cyan', primary: '#00A3E0' },
{ name: 'Purple', primary: '#8B5CF6' },
{ name: 'Pink', primary: '#EC4899' },
{ name: 'Green', primary: '#10B981' },
{ name: 'Orange', primary: '#F97316' },
{ name: 'Red', primary: '#EF4444' },
];
const THEME_STORAGE_KEY = 'app_theme';
interface ThemeState {
mode: ThemeMode;
primaryColor: string;
}
const defaultTheme: ThemeState = {
mode: 'dark',
primaryColor: '#00A3E0',
};
// Convert hex to HSL
function hexToHSL(hex: string): { h: number; s: number; l: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return { h: 0, s: 0, l: 0 };
let r = parseInt(result[1], 16) / 255;
let g = parseInt(result[2], 16) / 255;
let b = parseInt(result[3], 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
// Convert HSL to hex
function hslToHex(h: number, s: number, l: number): string {
s /= 100;
l /= 100;
const a = s * Math.min(l, 1 - l);
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
}
// Generate derived colors from primary
function generateDerivedColors(primary: string, mode: ThemeMode) {
const { h, s } = hexToHSL(primary);
if (mode === 'dark') {
return {
night: hslToHex(h, Math.min(s, 40), 6), // 6% lightness - panels
dark: hslToHex(h, Math.min(s, 35), 10), // 10% lightness - elevated panels
background: hslToHex(h, Math.min(s, 30), 3), // 3% lightness - page background
light: '#e5e6f0', // Light color for text/icons
text: '#ffffff', // White text
textMuted: 'rgba(229, 230, 240, 0.5)',
};
} else {
// Light mode: use lower saturation to avoid too colorful backgrounds
const lightSat = Math.min(s, 30);
return {
night: hslToHex(h, lightSat, 92),
dark: hslToHex(h, lightSat, 85),
background: hslToHex(h, lightSat, 98),
light: '#1a1a2e',
text: '#0a121f',
textMuted: 'rgba(10, 18, 31, 0.6)',
};
}
}
function loadTheme(): ThemeState {
if (!browser) return defaultTheme;
try {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
if (stored) {
return { ...defaultTheme, ...JSON.parse(stored) };
}
} catch (e) {
console.warn('Failed to load theme:', e);
}
return defaultTheme;
}
function saveTheme(theme: ThemeState): void {
if (!browser) return;
localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(theme));
}
function createThemeStore() {
const { subscribe, set, update } = writable<ThemeState>(loadTheme());
return {
subscribe,
setMode: (mode: ThemeMode) => {
update(state => {
const newState = { ...state, mode };
saveTheme(newState);
applyTheme(newState);
return newState;
});
},
setPrimaryColor: (color: string) => {
update(state => {
const newState = { ...state, primaryColor: color };
saveTheme(newState);
applyTheme(newState);
return newState;
});
},
toggleMode: () => {
update(state => {
const newMode: ThemeMode = state.mode === 'dark' ? 'light' : 'dark';
const newState: ThemeState = { ...state, mode: newMode };
saveTheme(newState);
applyTheme(newState);
return newState;
});
},
reset: () => {
set(defaultTheme);
saveTheme(defaultTheme);
applyTheme(defaultTheme);
},
};
}
export const theme = createThemeStore();
// Derived stores for convenience
export const isDarkMode = derived(theme, $t => $t.mode === 'dark');
export const primaryColor = derived(theme, $t => $t.primaryColor);
// Apply theme to document
export function applyTheme(state: ThemeState): void {
if (!browser) return;
const root = document.documentElement;
// Set mode class
root.classList.remove('dark', 'light');
root.classList.add(state.mode);
// Set CSS custom property for primary color
root.style.setProperty('--color-primary', state.primaryColor);
// Calculate hover variant
const { h, s, l } = hexToHSL(state.primaryColor);
root.style.setProperty('--color-primary-hover', hslToHex(h, s, Math.min(100, l + 10)));
// Generate and apply derived colors
const derived = generateDerivedColors(state.primaryColor, state.mode);
root.style.setProperty('--color-night', derived.night);
root.style.setProperty('--color-dark', derived.dark);
root.style.setProperty('--color-background', derived.background);
root.style.setProperty('--color-light', derived.light);
root.style.setProperty('--color-text', derived.text);
root.style.setProperty('--color-text-muted', derived.textMuted);
}
// Initialize theme on load
if (browser) {
applyTheme(loadTheme());
}