feat: major update with network graphs, themes, todo extensions, and more

## New Features

### Network Graph Visualization (Contacts, Calendar, Todo)
- D3.js force simulation for physics-based layout
- Zoom & pan with mouse/touchpad
- Keyboard shortcuts: +/- zoom, 0 reset, Esc deselect, / search, F focus
- Filtering by tags, company/location/project, connection strength
- Shared components in @manacore/shared-ui

### Central Tags API (mana-core-auth)
- CRUD endpoints for tags
- Schema: tags table with userId, name, color, app
- Shared tag components in @manacore/shared-ui

### Custom Themes System
- Theme editor with live preview and color picker
- Community theme gallery
- Theme sharing (public, unlisted, private)
- Backend API in mana-core-auth

### Todo App Extensions
- Glass-pill design for task input and items
- Settings page with 20+ preferences
- Task edit modal with inline editing
- Statistics page with visualizations
- PWA support with offline capabilities
- Multiple kanban boards

### Contacts App Features
- Duplicate detection
- Photo upload
- Batch operations
- Enhanced favorites page with multiple view modes
- Alphabet view improvements
- Search modal

### Help System
- @manacore/shared-help-content
- @manacore/shared-help-ui
- @manacore/shared-help-types

### Other Features
- Themes page for all apps
- Referral system frontend
- CommandBar (global search)
- Skeleton loaders
- Settings page improvements

## Bug Fixes
- Network graph simulation initialization
- Database schema TEXT for user_id columns (Better Auth compatibility)
- Various styling fixes

## Documentation
- Daily report for 2025-12-10
- CI/CD deployment guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 02:37:46 +01:00
parent e84371aa94
commit ee42b6cc76
381 changed files with 39284 additions and 6275 deletions

View file

@ -8,6 +8,10 @@ export const THEME_VARIANTS: readonly ThemeVariant[] = [
'nature',
'stone',
'ocean',
'sunset',
'midnight',
'rose',
'lavender',
] as const;
/**
@ -194,6 +198,178 @@ const oceanDark: ThemeColors = {
ring: '199 98% 48%',
};
// ============================================================================
// Extended Themes: Sunset, Midnight, Rose, Lavender
// ============================================================================
const sunsetLight: ThemeColors = {
primary: '15 90% 55%', // Coral/Orange
primaryForeground: '0 0% 100%',
secondary: '25 100% 60%', // Warm orange
secondaryForeground: '0 0% 0%',
background: '30 50% 97%', // Warm cream
foreground: '15 50% 20%', // Dark warm brown
surface: '0 0% 100%',
surfaceHover: '30 40% 95%',
surfaceElevated: '0 0% 100%',
muted: '30 30% 93%',
mutedForeground: '15 20% 45%',
border: '30 25% 88%',
borderStrong: '30 30% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '15 90% 55%',
};
const sunsetDark: ThemeColors = {
primary: '15 85% 58%', // Brighter coral in dark
primaryForeground: '0 0% 0%',
secondary: '25 60% 35%',
secondaryForeground: '0 0% 100%',
background: '15 20% 8%', // Dark with warm tint
foreground: '0 0% 100%',
surface: '15 15% 12%',
surfaceHover: '15 15% 16%',
surfaceElevated: '15 15% 14%',
muted: '15 12% 20%',
mutedForeground: '15 10% 60%',
border: '15 12% 25%',
borderStrong: '15 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '15 20% 14%',
ring: '15 85% 58%',
};
const midnightLight: ThemeColors = {
primary: '260 70% 55%', // Deep purple/violet
primaryForeground: '0 0% 100%',
secondary: '270 60% 70%', // Lighter purple
secondaryForeground: '0 0% 0%',
background: '260 30% 97%', // Very light purple tint
foreground: '260 50% 20%', // Dark purple text
surface: '0 0% 100%',
surfaceHover: '260 25% 95%',
surfaceElevated: '0 0% 100%',
muted: '260 20% 93%',
mutedForeground: '260 20% 45%',
border: '260 20% 88%',
borderStrong: '260 25% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '260 70% 55%',
};
const midnightDark: ThemeColors = {
primary: '260 65% 60%', // Brighter purple in dark
primaryForeground: '0 0% 100%',
secondary: '270 40% 35%',
secondaryForeground: '0 0% 100%',
background: '260 25% 7%', // Deep dark purple
foreground: '0 0% 100%',
surface: '260 20% 11%',
surfaceHover: '260 20% 15%',
surfaceElevated: '260 20% 13%',
muted: '260 15% 19%',
mutedForeground: '260 12% 60%',
border: '260 15% 24%',
borderStrong: '260 15% 34%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '260 25% 14%',
ring: '260 65% 60%',
};
const roseLight: ThemeColors = {
primary: '340 80% 55%', // Pink/Magenta
primaryForeground: '0 0% 100%',
secondary: '350 70% 70%', // Lighter pink
secondaryForeground: '0 0% 0%',
background: '340 40% 97%', // Very light pink tint
foreground: '340 50% 20%', // Dark rose text
surface: '0 0% 100%',
surfaceHover: '340 30% 95%',
surfaceElevated: '0 0% 100%',
muted: '340 25% 93%',
mutedForeground: '340 20% 45%',
border: '340 25% 88%',
borderStrong: '340 30% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '340 80% 55%',
};
const roseDark: ThemeColors = {
primary: '340 75% 60%', // Brighter pink in dark
primaryForeground: '0 0% 100%',
secondary: '350 45% 35%',
secondaryForeground: '0 0% 100%',
background: '340 20% 8%', // Dark with pink tint
foreground: '0 0% 100%',
surface: '340 15% 12%',
surfaceHover: '340 15% 16%',
surfaceElevated: '340 15% 14%',
muted: '340 12% 20%',
mutedForeground: '340 10% 60%',
border: '340 12% 25%',
borderStrong: '340 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '340 20% 14%',
ring: '340 75% 60%',
};
const lavenderLight: ThemeColors = {
primary: '270 60% 60%', // Lavender/Light purple
primaryForeground: '0 0% 100%',
secondary: '280 50% 75%', // Softer purple
secondaryForeground: '0 0% 0%',
background: '270 35% 97%', // Very light lavender
foreground: '270 40% 22%', // Dark lavender text
surface: '0 0% 100%',
surfaceHover: '270 25% 95%',
surfaceElevated: '0 0% 100%',
muted: '270 20% 93%',
mutedForeground: '270 18% 45%',
border: '270 20% 88%',
borderStrong: '270 25% 78%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '270 60% 60%',
};
const lavenderDark: ThemeColors = {
primary: '270 55% 65%', // Brighter lavender in dark
primaryForeground: '0 0% 0%',
secondary: '280 35% 38%',
secondaryForeground: '0 0% 100%',
background: '270 20% 8%', // Dark with lavender tint
foreground: '0 0% 100%',
surface: '270 15% 12%',
surfaceHover: '270 15% 16%',
surfaceElevated: '270 15% 14%',
muted: '270 12% 20%',
mutedForeground: '270 10% 60%',
border: '270 12% 25%',
borderStrong: '270 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '270 20% 14%',
ring: '270 55% 65%',
};
/**
* Complete theme variant definitions
*/
@ -234,6 +410,43 @@ export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
light: oceanLight,
dark: oceanDark,
},
// Extended themes (not in PillNav by default, can be pinned)
sunset: {
name: 'sunset',
label: 'Sunset',
emoji: '🌅',
icon: 'sun',
hue: 15,
light: sunsetLight,
dark: sunsetDark,
},
midnight: {
name: 'midnight',
label: 'Midnight',
emoji: '🌙',
icon: 'moon',
hue: 260,
light: midnightLight,
dark: midnightDark,
},
rose: {
name: 'rose',
label: 'Rose',
emoji: '🌹',
icon: 'flower',
hue: 340,
light: roseLight,
dark: roseDark,
},
lavender: {
name: 'lavender',
label: 'Lavender',
emoji: '💜',
icon: 'sparkle',
hue: 270,
light: lavenderLight,
dark: lavenderDark,
},
};
/**

View file

@ -0,0 +1,506 @@
import type {
CustomTheme,
CommunityTheme,
CreateCustomThemeInput,
UpdateCustomThemeInput,
PublishThemeInput,
CommunityThemeQuery,
PaginatedCommunityThemes,
CustomThemesStore,
CustomThemesStoreConfig,
ThemeColors,
EffectiveMode,
} from './types';
import { isBrowser } from './utils';
/**
* Apply a custom theme's colors to the document as CSS variables
*/
function applyCustomThemeToDocument(
colors: ThemeColors,
effectiveMode: EffectiveMode = 'light'
): void {
if (!isBrowser()) return;
const root = document.documentElement;
// Apply all color variables
Object.entries(colors).forEach(([key, value]) => {
// Convert camelCase to kebab-case
const cssVar = key.replace(/([A-Z])/g, '-$1').toLowerCase();
root.style.setProperty(`--${cssVar}`, value);
});
// Set mode class
root.classList.remove('light', 'dark');
root.classList.add(effectiveMode);
// Mark as custom theme
root.setAttribute('data-custom-theme', 'true');
}
/**
* Clear custom theme and revert to standard theme
*/
function clearCustomThemeFromDocument(): void {
if (!isBrowser()) return;
const root = document.documentElement;
// Remove custom theme marker
root.removeAttribute('data-custom-theme');
// Clear inline styles (CSS vars will fall back to theme variant)
root.style.cssText = '';
}
/**
* Create a custom themes store for managing user's custom themes and community themes
*
* @example
* ```typescript
* import { createCustomThemesStore } from '@manacore/shared-theme';
* import { authStore } from '$lib/stores/auth.svelte';
*
* export const customThemesStore = createCustomThemesStore({
* authUrl: import.meta.env.PUBLIC_AUTH_URL,
* getAccessToken: () => authStore.getAccessToken(),
* });
* ```
*/
export function createCustomThemesStore(config: CustomThemesStoreConfig): CustomThemesStore {
const { authUrl, getAccessToken } = config;
// State
let customThemes = $state<CustomTheme[]>([]);
let communityThemes = $state<CommunityTheme[]>([]);
let favorites = $state<CommunityTheme[]>([]);
let downloaded = $state<CommunityTheme[]>([]);
let pagination = $state({ page: 1, totalPages: 1, total: 0 });
let loading = $state(false);
let error = $state<string | null>(null);
// Track currently applied custom theme
let appliedThemeId = $state<string | null>(null);
/**
* Make an authenticated API request
*/
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = await getAccessToken();
if (!token) {
throw new Error('Not authenticated');
}
const url = `${authUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Request failed: ${response.status}`);
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T;
}
return response.json();
}
/**
* Make a public API request (no auth required)
*/
async function publicApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${authUrl}${endpoint}`;
const token = await getAccessToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Add auth if available (for user-specific data like favorites)
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Request failed: ${response.status}`);
}
return response.json();
}
// ==================== Custom Theme Operations ====================
/**
* Load user's custom themes
*/
async function loadCustomThemes(): Promise<void> {
loading = true;
error = null;
try {
customThemes = await apiRequest<CustomTheme[]>('/api/v1/themes');
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load themes';
throw err;
} finally {
loading = false;
}
}
/**
* Create a new custom theme
*/
async function createTheme(input: CreateCustomThemeInput): Promise<CustomTheme> {
loading = true;
error = null;
try {
const theme = await apiRequest<CustomTheme>('/api/v1/themes', {
method: 'POST',
body: JSON.stringify(input),
});
customThemes = [...customThemes, theme];
return theme;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to create theme';
throw err;
} finally {
loading = false;
}
}
/**
* Update an existing custom theme
*/
async function updateTheme(id: string, input: UpdateCustomThemeInput): Promise<CustomTheme> {
loading = true;
error = null;
try {
const theme = await apiRequest<CustomTheme>(`/api/v1/themes/${id}`, {
method: 'PATCH',
body: JSON.stringify(input),
});
customThemes = customThemes.map((t) => (t.id === id ? theme : t));
return theme;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to update theme';
throw err;
} finally {
loading = false;
}
}
/**
* Delete a custom theme
*/
async function deleteTheme(id: string): Promise<void> {
loading = true;
error = null;
try {
await apiRequest(`/api/v1/themes/${id}`, {
method: 'DELETE',
});
customThemes = customThemes.filter((t) => t.id !== id);
// Clear applied theme if it was the deleted one
if (appliedThemeId === id) {
clearCustomTheme();
}
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to delete theme';
throw err;
} finally {
loading = false;
}
}
/**
* Publish a custom theme to the community
*/
async function publishTheme(id: string, input?: PublishThemeInput): Promise<CommunityTheme> {
loading = true;
error = null;
try {
const communityTheme = await apiRequest<CommunityTheme>(`/api/v1/themes/${id}/publish`, {
method: 'POST',
body: JSON.stringify(input || {}),
});
// Update the custom theme's isPublished status
customThemes = customThemes.map((t) => (t.id === id ? { ...t, isPublished: true } : t));
return communityTheme;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to publish theme';
throw err;
} finally {
loading = false;
}
}
// ==================== Community Theme Operations ====================
/**
* Browse community themes with filtering/sorting
*/
async function browseCommunity(query?: CommunityThemeQuery): Promise<void> {
loading = true;
error = null;
try {
const params = new URLSearchParams();
if (query?.page) params.set('page', String(query.page));
if (query?.limit) params.set('limit', String(query.limit));
if (query?.sort) params.set('sort', query.sort);
if (query?.search) params.set('search', query.search);
if (query?.authorId) params.set('authorId', query.authorId);
if (query?.featuredOnly) params.set('featuredOnly', 'true');
if (query?.tags?.length) {
query.tags.forEach((tag) => params.append('tags', tag));
}
const queryString = params.toString();
const endpoint = `/api/v1/community-themes${queryString ? `?${queryString}` : ''}`;
const result = await publicApiRequest<PaginatedCommunityThemes>(endpoint);
communityThemes = result.themes;
pagination = {
page: result.page,
totalPages: result.totalPages,
total: result.total,
};
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to browse community themes';
throw err;
} finally {
loading = false;
}
}
/**
* Download/install a community theme
*/
async function downloadTheme(id: string): Promise<CommunityTheme> {
loading = true;
error = null;
try {
const theme = await apiRequest<CommunityTheme>(`/api/v1/community-themes/${id}/download`, {
method: 'POST',
});
// Update download status in community themes list
communityThemes = communityThemes.map((t) =>
t.id === id ? { ...t, isDownloaded: true, downloadCount: theme.downloadCount } : t
);
// Add to downloaded list if not already there
if (!downloaded.some((t) => t.id === id)) {
downloaded = [...downloaded, theme];
}
return theme;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to download theme';
throw err;
} finally {
loading = false;
}
}
/**
* Rate a community theme
*/
async function rateTheme(
id: string,
rating: number
): Promise<{ averageRating: number; ratingCount: number }> {
error = null;
try {
const result = await apiRequest<{ averageRating: number; ratingCount: number }>(
`/api/v1/community-themes/${id}/rate`,
{
method: 'POST',
body: JSON.stringify({ rating }),
}
);
// Update rating in community themes list
communityThemes = communityThemes.map((t) =>
t.id === id
? {
...t,
averageRating: result.averageRating,
ratingCount: result.ratingCount,
userRating: rating,
}
: t
);
return result;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to rate theme';
throw err;
}
}
/**
* Toggle favorite status for a community theme
*/
async function toggleFavorite(id: string): Promise<{ isFavorited: boolean }> {
error = null;
try {
const result = await apiRequest<{ isFavorited: boolean }>(
`/api/v1/community-themes/${id}/favorite`,
{ method: 'POST' }
);
// Update favorite status in community themes list
communityThemes = communityThemes.map((t) =>
t.id === id ? { ...t, isFavorited: result.isFavorited } : t
);
// Update favorites list
if (result.isFavorited) {
const theme = communityThemes.find((t) => t.id === id);
if (theme && !favorites.some((t) => t.id === id)) {
favorites = [...favorites, { ...theme, isFavorited: true }];
}
} else {
favorites = favorites.filter((t) => t.id !== id);
}
return result;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to toggle favorite';
throw err;
}
}
/**
* Load user's favorite themes
*/
async function loadFavorites(): Promise<void> {
loading = true;
error = null;
try {
favorites = await apiRequest<CommunityTheme[]>('/api/v1/community-themes/favorites');
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load favorites';
throw err;
} finally {
loading = false;
}
}
/**
* Load user's downloaded themes
*/
async function loadDownloaded(): Promise<void> {
loading = true;
error = null;
try {
downloaded = await apiRequest<CommunityTheme[]>('/api/v1/community-themes/downloaded');
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load downloaded themes';
throw err;
} finally {
loading = false;
}
}
// ==================== Apply Theme ====================
/**
* Apply a custom or community theme to the document
*/
function applyCustomTheme(theme: CustomTheme | CommunityTheme): void {
// Determine effective mode from system or stored preference
const effectiveMode: EffectiveMode = isBrowser()
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
: 'light';
const colors = effectiveMode === 'dark' ? theme.darkColors : theme.lightColors;
applyCustomThemeToDocument(colors as ThemeColors, effectiveMode);
appliedThemeId = theme.id;
}
/**
* Clear the applied custom theme and revert to standard theme
*/
function clearCustomTheme(): void {
clearCustomThemeFromDocument();
appliedThemeId = null;
}
return {
get customThemes() {
return customThemes;
},
get communityThemes() {
return communityThemes;
},
get favorites() {
return favorites;
},
get downloaded() {
return downloaded;
},
get pagination() {
return pagination;
},
get loading() {
return loading;
},
get error() {
return error;
},
// Custom theme operations
loadCustomThemes,
createTheme,
updateTheme,
deleteTheme,
publishTheme,
// Community theme operations
browseCommunity,
downloadTheme,
rateTheme,
toggleFavorite,
loadFavorites,
loadDownloaded,
// Apply theme
applyCustomTheme,
clearCustomTheme,
};
}

View file

@ -28,11 +28,29 @@ export type {
StartPageConfig,
WeekStartDay,
GeneralSettings,
// Custom & Community Themes Types
ThemeColorsInput,
CustomTheme,
CreateCustomThemeInput,
UpdateCustomThemeInput,
CommunityTheme,
CommunityThemeQuery,
PaginatedCommunityThemes,
PublishThemeInput,
ThemeEditorState,
CustomThemesStore,
CustomThemesStoreConfig,
} from './types';
// User Settings Constants
export { DEFAULT_GLOBAL_SETTINGS, DEFAULT_GENERAL_SETTINGS } from './types';
// Theme Variant Categories
export { DEFAULT_THEME_VARIANTS, EXTENDED_THEME_VARIANTS } from './types';
// Custom Theme Constants
export { MAIN_THEME_COLORS, EXTENDED_THEME_COLORS, THEME_COLOR_LABELS } from './types';
// Constants
export {
THEME_VARIANTS,
@ -63,6 +81,9 @@ export { createA11yStore } from './a11y-store.svelte';
// User Settings Store
export { createUserSettingsStore } from './user-settings-store.svelte';
// Custom Themes Store
export { createCustomThemesStore } from './custom-themes-store.svelte';
// Utils
export {
isBrowser,

View file

@ -5,9 +5,28 @@ export type ThemeMode = 'light' | 'dark' | 'system';
/**
* Theme variant - visual style/color scheme
* All apps share the same 4 variants
* Default variants (shown in PillNav): lume, nature, stone, ocean
* Extended variants (only on themes page, can be pinned): sunset, midnight, rose, lavender
*/
export type ThemeVariant = 'lume' | 'nature' | 'stone' | 'ocean';
export type ThemeVariant =
| 'lume'
| 'nature'
| 'stone'
| 'ocean'
| 'sunset'
| 'midnight'
| 'rose'
| 'lavender';
/**
* Default theme variants - always visible in PillNav
*/
export const DEFAULT_THEME_VARIANTS: ThemeVariant[] = ['lume', 'nature', 'stone', 'ocean'];
/**
* Extended theme variants - only on themes page, can be pinned
*/
export const EXTENDED_THEME_VARIANTS: ThemeVariant[] = ['sunset', 'midnight', 'rose', 'lavender'];
/**
* Effective mode - the actual rendered mode (resolved from system preference)
@ -258,6 +277,8 @@ export interface ThemeSettings {
mode: ThemeMode;
/** Color scheme / variant */
colorScheme: string;
/** Pinned themes to show in PillNav (extended themes only) */
pinnedThemes?: ThemeVariant[];
}
/**
@ -303,7 +324,7 @@ export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = {
*/
export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
nav: { desktopPosition: 'top', sidebarCollapsed: false },
theme: { mode: 'system', colorScheme: 'ocean' },
theme: { mode: 'system', colorScheme: 'ocean', pinnedThemes: [] },
locale: 'de',
general: DEFAULT_GENERAL_SETTINGS,
};
@ -356,3 +377,260 @@ export interface UserSettingsStoreConfig {
/** Function to get current access token */
getAccessToken: () => Promise<string | null>;
}
// ============================================================================
// Custom & Community Themes Types
// ============================================================================
/**
* Partial theme colors for API DTOs (some fields optional)
*/
export interface ThemeColorsInput {
primary: HSLValue;
primaryForeground?: HSLValue;
background: HSLValue;
foreground: HSLValue;
surface: HSLValue;
surfaceHover?: HSLValue;
surfaceElevated?: HSLValue;
muted?: HSLValue;
mutedForeground?: HSLValue;
border?: HSLValue;
borderStrong?: HSLValue;
secondary?: HSLValue;
secondaryForeground?: HSLValue;
input?: HSLValue;
ring?: HSLValue;
error: HSLValue;
success: HSLValue;
warning: HSLValue;
}
/**
* User-created custom theme
*/
export interface CustomTheme {
id: string;
userId: string;
name: string;
description?: string;
emoji: string;
icon: string;
lightColors: ThemeColors;
darkColors: ThemeColors;
baseVariant?: ThemeVariant;
isPublished: boolean;
createdAt: Date;
updatedAt: Date;
}
/**
* Input for creating a new custom theme
*/
export interface CreateCustomThemeInput {
name: string;
description?: string;
emoji?: string;
icon?: string;
lightColors: ThemeColorsInput;
darkColors: ThemeColorsInput;
baseVariant?: ThemeVariant;
}
/**
* Input for updating a custom theme
*/
export interface UpdateCustomThemeInput {
name?: string;
description?: string;
emoji?: string;
icon?: string;
lightColors?: ThemeColorsInput;
darkColors?: ThemeColorsInput;
baseVariant?: ThemeVariant;
}
/**
* Community theme shared publicly
*/
export interface CommunityTheme {
id: string;
authorId?: string;
authorName?: string;
name: string;
description?: string;
emoji: string;
icon: string;
lightColors: ThemeColors;
darkColors: ThemeColors;
baseVariant?: ThemeVariant;
downloadCount: number;
averageRating: number;
ratingCount: number;
status: 'pending' | 'approved' | 'rejected' | 'featured';
isFeatured: boolean;
tags: string[];
createdAt: Date;
publishedAt?: Date;
/** User-specific fields (when authenticated) */
isFavorited?: boolean;
isDownloaded?: boolean;
userRating?: number;
}
/**
* Query parameters for browsing community themes
*/
export interface CommunityThemeQuery {
page?: number;
limit?: number;
sort?: 'popular' | 'recent' | 'rating' | 'downloads';
search?: string;
tags?: string[];
authorId?: string;
featuredOnly?: boolean;
}
/**
* Paginated response for community themes
*/
export interface PaginatedCommunityThemes {
themes: CommunityTheme[];
total: number;
page: number;
limit: number;
totalPages: number;
}
/**
* Input for publishing a theme to the community
*/
export interface PublishThemeInput {
tags?: string[];
description?: string;
}
/**
* Theme editor state for UI
*/
export interface ThemeEditorState {
/** Theme being edited */
theme: Partial<CreateCustomThemeInput>;
/** Currently editing light or dark colors */
editingMode: EffectiveMode;
/** Currently selected color key */
selectedColorKey: keyof ThemeColors | null;
/** Is preview mode active */
isPreviewing: boolean;
/** Has unsaved changes */
isDirty: boolean;
}
/**
* Custom themes store interface
*/
export interface CustomThemesStore {
/** User's custom themes */
readonly customThemes: CustomTheme[];
/** Community themes (from current query) */
readonly communityThemes: CommunityTheme[];
/** User's favorited themes */
readonly favorites: CommunityTheme[];
/** User's downloaded themes */
readonly downloaded: CommunityTheme[];
/** Pagination info */
readonly pagination: { page: number; totalPages: number; total: number };
/** Loading state */
readonly loading: boolean;
/** Error state */
readonly error: string | null;
// Custom theme operations
loadCustomThemes: () => Promise<void>;
createTheme: (input: CreateCustomThemeInput) => Promise<CustomTheme>;
updateTheme: (id: string, input: UpdateCustomThemeInput) => Promise<CustomTheme>;
deleteTheme: (id: string) => Promise<void>;
publishTheme: (id: string, input?: PublishThemeInput) => Promise<CommunityTheme>;
// Community theme operations
browseCommunity: (query?: CommunityThemeQuery) => Promise<void>;
downloadTheme: (id: string) => Promise<CommunityTheme>;
rateTheme: (
id: string,
rating: number
) => Promise<{ averageRating: number; ratingCount: number }>;
toggleFavorite: (id: string) => Promise<{ isFavorited: boolean }>;
loadFavorites: () => Promise<void>;
loadDownloaded: () => Promise<void>;
// Apply theme
applyCustomTheme: (theme: CustomTheme | CommunityTheme) => void;
clearCustomTheme: () => void;
}
/**
* Custom themes store configuration
*/
export interface CustomThemesStoreConfig {
/** Auth service base URL */
authUrl: string;
/** Function to get current access token */
getAccessToken: () => Promise<string | null>;
/** Theme store to apply custom themes to */
themeStore?: ThemeStore;
}
/**
* Main colors for the simplified editor view
* These are the 7 most important colors users typically want to customize
*/
export const MAIN_THEME_COLORS: (keyof ThemeColors)[] = [
'primary',
'background',
'surface',
'foreground',
'error',
'success',
'warning',
];
/**
* Extended/advanced colors (collapsed by default in editor)
*/
export const EXTENDED_THEME_COLORS: (keyof ThemeColors)[] = [
'primaryForeground',
'secondary',
'secondaryForeground',
'surfaceHover',
'surfaceElevated',
'muted',
'mutedForeground',
'border',
'borderStrong',
'input',
'ring',
];
/**
* Color labels for the editor UI
*/
export const THEME_COLOR_LABELS: Record<keyof ThemeColors, string> = {
primary: 'Primary',
primaryForeground: 'Primary Text',
secondary: 'Secondary',
secondaryForeground: 'Secondary Text',
background: 'Background',
foreground: 'Text',
surface: 'Surface',
surfaceHover: 'Surface Hover',
surfaceElevated: 'Elevated Surface',
muted: 'Muted',
mutedForeground: 'Muted Text',
border: 'Border',
borderStrong: 'Border Strong',
error: 'Error',
success: 'Success',
warning: 'Warning',
input: 'Input',
ring: 'Focus Ring',
};