🚚 feat(context): integrate context app into monorepo

Restructure the context app (formerly basetext) to follow the monorepo
pattern with proper workspace configuration.

Changes:
- Move app files to apps/context/apps/mobile/
- Rename package to @context/mobile
- Update bundle ID to com.manacore.context
- Create pnpm-workspace.yaml for project workspace
- Add dev scripts to root package.json
- Update CLAUDE.md with project documentation

The app structure is prepared for future web/backend additions.

Note: Existing TypeScript errors in the original codebase are preserved.
These should be fixed in a follow-up PR.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-05 15:09:04 +01:00
parent 34c879929b
commit bb0e0cf5cb
303 changed files with 31904 additions and 475 deletions

View file

@ -0,0 +1,37 @@
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
options?: { leading?: boolean; trailing?: boolean }
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
let result: any;
const leading = options?.leading ?? false;
const trailing = options?.trailing ?? true;
const debounced = function (this: any, ...args: Parameters<T>) {
const context = this;
const later = () => {
timeout = null;
if (trailing) result = func.apply(context, args);
};
const callNow = leading && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(context, args);
return result;
};
debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
return debounced;
}

View file

@ -0,0 +1,44 @@
import { ViewStyle } from 'react-native';
/**
* Fügt Debug-Borders zu einem Style-Objekt hinzu
* @param baseStyle Das Basis-Style-Objekt
* @param color Die Farbe des Debug-Rahmens (optional)
* @param showBorder Flag, ob der Debug-Rahmen angezeigt werden soll
* @returns Das Style-Objekt mit Debug-Rahmen (wenn aktiviert)
*/
export const addDebugBorder = (
baseStyle: ViewStyle,
color: string = '#ff0000',
showBorder: boolean = false
): ViewStyle => {
if (!showBorder) return baseStyle;
return {
...baseStyle,
borderWidth: 1,
borderColor: color,
};
};
/**
* Generiert zufällige Debug-Farben für verschiedene UI-Elemente
*/
export const debugColors = {
container: '#ff0000', // Rot
section: '#00ff00', // Grün
item: '#0000ff', // Blau
header: '#ff00ff', // Magenta
content: '#ffff00', // Gelb
footer: '#00ffff', // Cyan
// Generiert eine zufällige Farbe für andere Elemente
random: () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
},
};

View file

@ -0,0 +1,40 @@
// Einfacher Event-Emitter für die App
type EventCallback = (...args: any[]) => void;
class EventEmitter {
private events: Record<string, EventCallback[]> = {};
// Event registrieren
on(event: string, callback: EventCallback): void {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
// Event deregistrieren
off(event: string, callback: EventCallback): void {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter((cb) => cb !== callback);
}
// Event auslösen
emit(event: string, ...args: any[]): void {
if (!this.events[event]) return;
this.events[event].forEach((callback) => {
try {
callback(...args);
} catch (error) {
console.error(`Fehler beim Ausführen des Event-Handlers für ${event}:`, error);
}
});
}
}
// Singleton-Instanz
export const eventEmitter = new EventEmitter();
// Event-Namen als Konstanten
export const EVENTS = {
TOKEN_BALANCE_UPDATED: 'TOKEN_BALANCE_UPDATED',
};

View file

@ -0,0 +1,101 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
// Import translation files
import en from '~/locales/en.json';
import de from '~/locales/de.json';
// Define supported languages
export const SUPPORTED_LANGUAGES = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
] as const;
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]['code'];
const resources = {
en: {
translation: en,
},
de: {
translation: de,
},
};
// Get device language, fallback to English if not supported
function getDeviceLanguage(): SupportedLanguage {
try {
const locale = Localization.locale;
if (!locale) return 'en';
const languageCode = locale.split('-')[0] as SupportedLanguage;
// Check if the language is supported
const isSupported = SUPPORTED_LANGUAGES.some((lang) => lang.code === languageCode);
return isSupported ? languageCode : 'en';
} catch (error) {
console.warn('Error getting device language:', error);
return 'en';
}
}
// Initialize i18n
i18n.use(initReactI18next).init({
resources,
lng: getDeviceLanguage(),
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes values
},
// Enable plurals
pluralSeparator: '_',
// Namespace configuration
defaultNS: 'translation',
// Debug mode - set to true during development
debug: __DEV__,
// React i18next options
react: {
useSuspense: false,
},
});
export default i18n;
// Helper function to get current language
export const getCurrentLanguage = (): SupportedLanguage => {
return i18n.language as SupportedLanguage;
};
// Helper function to change language
export const changeLanguage = async (language: SupportedLanguage): Promise<void> => {
await i18n.changeLanguage(language);
};
// Helper function to get language name
export const getLanguageName = (code: SupportedLanguage): string => {
const lang = SUPPORTED_LANGUAGES.find((l) => l.code === code);
return lang?.nativeName || code;
};
// Helper function to check if language is supported
export const isLanguageSupported = (code: string): code is SupportedLanguage => {
return SUPPORTED_LANGUAGES.some((lang) => lang.code === code);
};
// Helper function to get device locale info
export const getDeviceLocaleInfo = () => {
return {
locale: Localization.locale,
locales: Localization.locales,
timezone: Localization.timezone,
isRTL: Localization.isRTL,
region: Localization.region,
};
};

View file

@ -0,0 +1,50 @@
/**
* Utility functions for working with markdown content
*/
/**
* Extracts a title from markdown content
* First tries to find an H1 heading, then falls back to the first line
* Trims the content to a reasonable length for display
*
* @param content The markdown content
* @param maxLength Maximum length for the extracted title
* @returns The extracted title or a default title if none found
*/
export const extractTitleFromMarkdown = (
content: string | null | undefined,
maxLength: number = 50
): string => {
if (!content) return 'Unbenanntes Dokument';
// Try to find an H1 heading (# Title)
const h1Match = content.match(/^#\s+(.+)$/m);
if (h1Match && h1Match[1]) {
return truncateTitle(h1Match[1], maxLength);
}
// Fall back to first line
const firstLine = content.split('\n')[0].trim();
if (firstLine) {
// Remove markdown formatting from the first line
const cleanLine = firstLine
.replace(/^[#*_~`>]+\s*/, '') // Remove heading markers, list markers, etc.
.replace(/[*_~`]+/g, ''); // Remove bold, italic, etc.
return truncateTitle(cleanLine, maxLength);
}
return 'Unbenanntes Dokument';
};
/**
* Truncates a title to a specified maximum length
*
* @param title The title to truncate
* @param maxLength Maximum length for the title
* @returns The truncated title
*/
const truncateTitle = (title: string, maxLength: number): string => {
if (title.length <= maxLength) return title;
return title.substring(0, maxLength - 3) + '...';
};

View file

@ -0,0 +1,24 @@
/**
* Utility functions for processing markdown syntax
*/
/**
* Processes content in markdown
*
* @param content The markdown content to process
* @param isDark Whether dark mode is enabled
* @returns Processed markdown content
*/
export const processAIContentBlocks = (content: string, isDark: boolean): string => {
// Wir entfernen alle speziellen Marker, die früher für die Unterscheidung
// zwischen Benutzer- und KI-Eingaben verwendet wurden
if (!content) return '';
// Entferne alle Marker
let processedContent = content
.replace(/\/\/\/ KI Antwort, .*?, .*?\n\n/g, '')
.replace(/\/\/\/ Nutzer, .*?, .*?\n\n/g, '')
.replace(/\n\n\/\/\//g, '');
return processedContent;
};

View file

@ -0,0 +1,134 @@
/**
* Hilfsfunktionen für die Verarbeitung von Varianten in Markdown-Texten
*/
/**
* Extrahiert Varianten aus einem Markdown-Text
* Format: [Option1, Option2, Option3]
*/
export interface VariantOption {
original: string;
selected: string;
allOptions: string[];
position: {
start: number;
end: number;
};
}
/**
* Extrahiert Varianten aus einem Markdown-Text
* @param text Der Markdown-Text
* @returns Array von gefundenen Varianten
*/
export const extractVariantsFromMarkdown = (text: string): VariantOption[] => {
const variants: VariantOption[] = [];
// Suche nach Varianten in eckigen Klammern mit Kommas als Trennzeichen
// Format: [Option1, Option2, Option3]
const variantRegex = /\[(.*?)\]/g;
let match;
while ((match = variantRegex.exec(text)) !== null) {
const fullMatch = match[0]; // z.B. "[Heilbronn, München, Hamburg]"
const optionsString = match[1]; // z.B. "Heilbronn, München, Hamburg"
const options = optionsString.split(',').map((option) => option.trim());
if (options.length > 0) {
variants.push({
original: fullMatch,
selected: options[0], // Die erste Option ist standardmäßig ausgewählt
allOptions: options,
position: {
start: match.index,
end: match.index + fullMatch.length,
},
});
}
}
return variants;
};
/**
* Ersetzt Varianten in einem Text mit den ausgewählten Optionen
* @param text Der Originaltext mit Varianten
* @param variants Die Varianten mit ausgewählten Optionen
* @returns Der Text mit ersetzten Varianten
*/
export const replaceVariantsInText = (text: string, variants: VariantOption[]): string => {
let updatedContent = text;
// Sortiere die Varianten nach Position (von hinten nach vorne),
// damit die Indizes nicht durcheinander kommen, wenn wir Text ersetzen
const sortedVariants = [...variants].sort((a, b) => b.position.start - a.position.start);
// Ersetze alle Varianten mit der ausgewählten Option
sortedVariants.forEach(({ original, selected, position }) => {
updatedContent =
updatedContent.substring(0, position.start) +
selected +
updatedContent.substring(position.end);
});
return updatedContent;
};
/**
* Berechnet die Gesamtzahl der möglichen Kombinationen von Varianten
* @param variants Die Varianten
* @returns Die Anzahl der möglichen Kombinationen
*/
export const calculateTotalCombinations = (variants: VariantOption[]): number => {
return variants.reduce((total, variant) => total * variant.allOptions.length, 1);
};
/**
* Generiert alle möglichen Kombinationen von Varianten
* @param text Der Originaltext mit Varianten
* @param variants Die Varianten
* @returns Array von Texten mit allen möglichen Kombinationen
*/
export const generateAllCombinations = (text: string, variants: VariantOption[]): string[] => {
if (variants.length === 0) {
return [text];
}
// Rekursive Funktion zum Generieren aller Kombinationen
const generateCombinations = (
currentVariants: VariantOption[],
currentIndex: number,
currentSelections: string[]
): string[] => {
// Basisfall: Alle Varianten wurden verarbeitet
if (currentIndex >= currentVariants.length) {
// Erstelle eine Kopie der Varianten mit den aktuellen Auswahlen
const variantsWithSelections = currentVariants.map((variant, idx) => ({
...variant,
selected: currentSelections[idx],
}));
// Ersetze die Varianten im Text
return [replaceVariantsInText(text, variantsWithSelections)];
}
// Rekursiver Fall: Generiere Kombinationen für jede Option der aktuellen Variante
const results: string[] = [];
const currentVariant = currentVariants[currentIndex];
for (const option of currentVariant.allOptions) {
const newSelections = [...currentSelections];
newSelections[currentIndex] = option;
const combinations = generateCombinations(currentVariants, currentIndex + 1, newSelections);
results.push(...combinations);
}
return results;
};
// Starte die rekursive Generierung mit leeren Auswahlen
const initialSelections = variants.map(() => '');
return generateCombinations(variants, 0, initialSelections);
};

View file

@ -0,0 +1,114 @@
/**
* Utility functions for processing document mentions in markdown content
*/
// Regex to match XML tags with attributes
export const XML_TAG_REGEX = /<mention[^>]*>(.*?)<\/mention>/g;
// Regular expression to match document mentions in the format [[Document Title]]
export const MENTION_REGEX = /\[\[(.*?)\]\]/g;
// Standard Markdown-Link-Format: [Titel](ID)
export const MARKDOWN_LINK_REGEX = /\[(.*?)\]\((.*?)\)/g;
// Legacy format for backward compatibility
export const LEGACY_MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
/**
* Extract all document mentions from markdown content
*/
export const extractMentions = (content: string): Array<{ title: string; id?: string }> => {
const mentions: Array<{ title: string; id?: string }> = [];
let match;
// Neue Format: [[Dokumenttitel]]
const regex = new RegExp(MENTION_REGEX);
while ((match = regex.exec(content)) !== null) {
mentions.push({
title: match[1],
});
}
// Standard Markdown-Link-Format: [Titel](ID)
const markdownRegex = new RegExp(MARKDOWN_LINK_REGEX);
while ((match = markdownRegex.exec(content)) !== null) {
mentions.push({
title: match[1],
id: match[2],
});
}
// Legacy Format: @[Dokumenttitel](dokumentId)
const legacyRegex = new RegExp(LEGACY_MENTION_REGEX);
while ((match = legacyRegex.exec(content)) !== null) {
mentions.push({
title: match[1],
id: match[2],
});
}
return mentions;
};
/**
* Replace document mentions in markdown content with custom components
* This is used by the markdown renderer to render mentions as interactive components
*/
export const processMentionsInMarkdown = (content: string): string => {
let processedContent = content;
// Verarbeite das neue Format [[Dokumenttitel]]
processedContent = processedContent.replace(MENTION_REGEX, (match, title) => {
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
return `<mention documentTitle="${title}">${title}</mention>`;
});
// Verarbeite Standard Markdown-Links [Titel](ID)
processedContent = processedContent.replace(MARKDOWN_LINK_REGEX, (match, title, id) => {
// Prüfe, ob der Link eine Dokument-ID ist (keine URL)
if (!id.startsWith('http') && !id.startsWith('www') && !id.includes('/')) {
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
return `<mention documentId="${id}" documentTitle="${title}">${title}</mention>`;
}
// Wenn es ein normaler Link ist, nicht verändern
return match;
});
// Verarbeite das Legacy-Format @[Dokumenttitel](dokumentId) für Abwärtskompatibilität
processedContent = processedContent.replace(LEGACY_MENTION_REGEX, (match, title, id) => {
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
return `<mention documentId="${id}" documentTitle="${title}">${title}</mention>`;
});
return processedContent;
};
/**
* Entfernt XML-Tags aus dem Markdown-Inhalt und ersetzt sie durch den Inhalt des Tags
* Dies ist nützlich, wenn die Markdown-Komponente die benutzerdefinierten Tags nicht korrekt verarbeitet
*/
export const cleanMentionTags = (content: string): string => {
if (!content) return '';
// Entferne alle <mention>-Tags und ersetze sie durch den Inhalt des documentTitle-Attributs
let cleanedContent = content;
// Ersetze alle Vorkommen von <mention>-Tags mit ihrem documentTitle
// Dieses Pattern ist sehr spezifisch für die Struktur der Tags
cleanedContent = cleanedContent.replace(
/<mention[^>]*documentTitle="([^"]*)"[^>]*>.*?<\/mention>/g,
(match, title) => {
return title;
}
);
return cleanedContent;
};
/**
* Check if text contains a potential mention
*/
export const containsPotentialMention = (text: string): boolean => {
return text.includes('[[') || text.includes('@') || (text.includes('[') && text.includes(']('));
};

View file

@ -0,0 +1,23 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
// Prüfen, ob wir in einer Browser-Umgebung sind
const isBrowser = typeof window !== 'undefined';
// Nur AsyncStorage importieren, wenn wir in einer Browser-/React Native-Umgebung sind
let AsyncStorage;
if (isBrowser) {
// Dynamischer Import, um SSR-Probleme zu vermeiden
AsyncStorage = require('@react-native-async-storage/async-storage').default;
}
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '', {
auth: {
storage: isBrowser ? AsyncStorage : undefined,
autoRefreshToken: true,
persistSession: isBrowser,
detectSessionInUrl: false,
},
});

View file

@ -0,0 +1,118 @@
import { supabase } from './supabase';
/**
* Testet die Verbindung zu Supabase
* @returns Ein Promise mit dem Ergebnis des Tests
*/
export const testSupabaseConnection = async () => {
try {
// Einfache Abfrage, um zu testen, ob die Verbindung funktioniert
// Verwende eine einfache Systemabfrage, die keine RLS-Richtlinien auslöst
const { data, error } = await supabase.auth.getSession();
if (error) {
return {
success: false,
message: `Fehler bei der Verbindung zu Supabase: ${error.message}`,
error,
};
}
return {
success: true,
message: 'Verbindung zu Supabase erfolgreich hergestellt!',
data,
};
} catch (err: any) {
return {
success: false,
message: `Unerwarteter Fehler: ${err.message}`,
error: err,
};
}
};
/**
* Testet die Authentifizierung mit Supabase
* @param email E-Mail-Adresse
* @param password Passwort
* @returns Ein Promise mit dem Ergebnis des Tests
*/
export const testSupabaseAuth = async (email: string, password: string) => {
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return {
success: false,
message: `Fehler bei der Authentifizierung: ${error.message}`,
error,
};
}
return {
success: true,
message: 'Authentifizierung erfolgreich!',
user: data.user,
session: data.session,
};
} catch (err: any) {
return {
success: false,
message: `Unerwarteter Fehler: ${err.message}`,
error: err,
};
}
};
/**
* Ruft alle Spaces aus der Datenbank ab
* @returns Ein Promise mit dem Ergebnis der Abfrage
*/
export const fetchAllSpaces = async () => {
try {
// Verwende eine Abfrage mit einer expliziten Bedingung, um RLS-Probleme zu vermeiden
// Hier verwenden wir eine Abfrage, die nur öffentliche Spaces abruft oder Spaces, bei denen der Benutzer Mitglied ist
const { data: session } = await supabase.auth.getSession();
// Wenn der Benutzer angemeldet ist, rufe seine Spaces ab
if (session?.session?.user) {
const userId = session.session.user.id;
const { data, error } = await supabase
.from('spaces')
.select('id, name, description, created_at')
.or(`created_by.eq.${userId},public.eq.true`);
if (error) {
return {
success: false,
message: `Fehler beim Abrufen der Spaces: ${error.message}`,
error,
};
}
return {
success: true,
message: `${data?.length || 0} Spaces erfolgreich abgerufen`,
spaces: data || [],
};
} else {
// Wenn kein Benutzer angemeldet ist, gib eine leere Liste zurück
return {
success: true,
message: 'Keine Spaces gefunden (nicht angemeldet)',
spaces: [],
};
}
} catch (err: any) {
return {
success: false,
message: `Unerwarteter Fehler: ${err.message}`,
error: err,
};
}
};

View file

@ -0,0 +1,27 @@
/**
* Hilfsfunktionen für Textverarbeitung
*/
/**
* Zählt die Anzahl der Wörter in einem Text
* @param text Der zu zählende Text
* @returns Die Anzahl der Wörter im Text
*/
export const countWords = (text: string): number => {
if (!text) return 0;
return text
.trim()
.split(/\s+/)
.filter((word) => word.length > 0).length;
};
/**
* Berechnet die geschätzte Lesezeit in Minuten
* @param text Der zu lesende Text
* @param wordsPerMinute Wörter pro Minute (Standard: 200)
* @returns Die geschätzte Lesezeit in Minuten
*/
export const calculateReadingTime = (text: string, wordsPerMinute: number = 200): number => {
const words = countWords(text);
return Math.ceil(words / wordsPerMinute);
};

View file

@ -0,0 +1,313 @@
/**
* Zentrale Farbdefinitionen für die Anwendung
* Diese Datei definiert alle Farben und Themes, die in der Anwendung verwendet werden
*/
// Gemeinsame Farben für alle Themes
const commonColors = {
// Button-Styles
button: {
// Varianten
primary: {
background: {
default: { light: '#0ea5e9', dark: '#0284c7' },
hover: { light: '#0284c7', dark: '#0369a1' },
active: { light: '#0369a1', dark: '#075985' },
disabled: { light: '#bae6fd', dark: '#075985' },
},
text: {
default: { light: '#ffffff', dark: '#ffffff' },
disabled: { light: '#e0f2fe', dark: '#7dd3fc' },
},
},
secondary: {
background: {
default: { light: '#e5e7eb', dark: '#374151' },
hover: { light: '#d1d5db', dark: '#4b5563' },
active: { light: '#9ca3af', dark: '#6b7280' },
disabled: { light: '#f3f4f6', dark: '#1f2937' },
},
text: {
default: { light: '#111827', dark: '#f9fafb' },
active: { light: '#ffffff', dark: '#ffffff' },
disabled: { light: '#6b7280', dark: '#9ca3af' },
},
},
outline: {
border: {
default: { light: '#d1d5db', dark: '#4b5563' },
hover: { light: '#9ca3af', dark: '#6b7280' },
active: { light: '#6b7280', dark: '#9ca3af' },
},
text: {
default: { light: '#111827', dark: '#f9fafb' },
hover: { light: '#374151', dark: '#e5e7eb' },
disabled: { light: '#9ca3af', dark: '#4b5563' },
},
},
},
// Graustufen
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
// Statusfarben
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
},
};
// Theme 1: Blau/Indigo (Standard)
const theme1 = {
name: 'blue',
displayName: 'Blau',
// Primärfarben
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
// Akzentfarben
accent: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1', // Indigo-500
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
950: '#1e1b4b',
},
// Hintergrundfarben
background: {
light: '#ffffff',
dark: '#121212',
},
// Textfarben
text: {
light: {
primary: '#1f2937', // gray-800
secondary: '#4b5563', // gray-600
tertiary: '#9ca3af', // gray-400
inverse: '#f9fafb', // gray-50
},
dark: {
primary: '#f9fafb', // gray-50
secondary: '#e5e7eb', // gray-200
tertiary: '#9ca3af', // gray-400
inverse: '#1f2937', // gray-800
},
},
// Randfarben
border: {
light: '#e5e7eb', // gray-200
dark: '#374151', // gray-700
},
};
// Theme 2: Grün/Smaragd
const theme2 = {
name: 'green',
displayName: 'Grün',
// Primärfarben
primary: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
950: '#022c22',
},
// Akzentfarben
accent: {
50: '#f0fdfa',
100: '#ccfbf1',
200: '#99f6e4',
300: '#5eead4',
400: '#2dd4bf',
500: '#14b8a6', // Teal-500
600: '#0d9488',
700: '#0f766e',
800: '#115e59',
900: '#134e4a',
950: '#042f2e',
},
// Hintergrundfarben
background: {
light: '#ffffff',
dark: '#0f1b17',
},
// Textfarben
text: {
light: {
primary: '#064e3b', // green-900
secondary: '#065f46', // green-800
tertiary: '#047857', // green-700
inverse: '#f9fafb', // gray-50
},
dark: {
primary: '#d1fae5', // green-100
secondary: '#a7f3d0', // green-200
tertiary: '#6ee7b7', // green-300
inverse: '#064e3b', // green-900
},
},
// Randfarben
border: {
light: '#d1fae5', // green-100
dark: '#065f46', // green-800
},
};
// Theme 3: Violett/Fuchsia
const theme3 = {
name: 'purple',
displayName: 'Violett',
// Primärfarben
primary: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
// Akzentfarben
accent: {
50: '#fdf4ff',
100: '#fae8ff',
200: '#f5d0fe',
300: '#f0abfc',
400: '#e879f9',
500: '#d946ef', // Fuchsia-500
600: '#c026d3',
700: '#a21caf',
800: '#86198f',
900: '#701a75',
950: '#4a044e',
},
// Hintergrundfarben
background: {
light: '#ffffff',
dark: '#1a1025',
},
// Textfarben
text: {
light: {
primary: '#581c87', // purple-900
secondary: '#6b21a8', // purple-800
tertiary: '#7e22ce', // purple-700
inverse: '#f9fafb', // gray-50
},
dark: {
primary: '#f3e8ff', // purple-100
secondary: '#e9d5ff', // purple-200
tertiary: '#d8b4fe', // purple-300
inverse: '#581c87', // purple-900
},
},
// Randfarben
border: {
light: '#f3e8ff', // purple-100
dark: '#6b21a8', // purple-800
},
};
// Alle verfügbaren Themes
export const themes = {
blue: theme1,
green: theme2,
purple: theme3,
};
// Exportiere die Farben des Standard-Themes (Blau)
export const colors = {
...commonColors,
...theme1,
};
// Typdefinitionen für die Themes
export type ThemeNames = keyof typeof themes;
export type ThemeColors = typeof theme1;
export type ColorTheme = typeof colors;

View file

@ -0,0 +1,3 @@
export * from './colors';
export * from './theme';
export { default as theme } from './theme';

View file

@ -0,0 +1,117 @@
import { colors, ColorTheme, themes, ThemeNames } from './colors';
import { useAppTheme, ThemeMode } from '~/components/theme/ThemeProvider';
/**
* Theme-Utility für die Anwendung
* Bietet Funktionen und Hooks für die einfache Verwendung des Themes
*/
/**
* Hook zum Abrufen des aktuellen Themes
* @returns Das aktuelle Theme-Objekt mit Farben und Funktionen
*/
export function useTheme() {
const appTheme = useAppTheme();
const currentThemeColors = themes[appTheme.themeName] || themes.blue;
return {
...appTheme,
colors: {
...colors,
...currentThemeColors,
},
};
}
/**
* Hilfsfunktion zum Abrufen einer Farbe basierend auf dem aktuellen Farbschema
* @param lightColor Die Farbe im Light-Modus
* @param darkColor Die Farbe im Dark-Modus
* @returns Die entsprechende Farbe basierend auf dem aktuellen Farbschema
*/
export function useColorModeValue(lightColor: string, darkColor: string): string {
const { isDark } = useTheme();
return isDark ? darkColor : lightColor;
}
/**
* Hilfsfunktion zum Generieren von Tailwind-Klassen basierend auf dem Farbschema
* @param lightClasses Die Tailwind-Klassen im Light-Modus
* @param darkClasses Die Tailwind-Klassen im Dark-Modus
* @returns Die kombinierten Tailwind-Klassen
*/
export function tw(lightClasses: string, darkClasses: string): string {
const { isDark } = useTheme();
return isDark ? darkClasses : lightClasses;
}
/**
* Hilfsfunktion zum Generieren von Tailwind-Klassen mit automatischer Dark-Mode-Unterstützung
* Beispiel: twMerge('bg-white dark:bg-gray-900', 'text-gray-800 dark:text-gray-200')
* @param classes Die Tailwind-Klassen
* @returns Die kombinierten Tailwind-Klassen
*/
export function twMerge(...classes: string[]): string {
return classes.filter(Boolean).join(' ');
}
/**
* Generiere Theme-Klassen basierend auf dem aktuellen Theme
* @param themeName Der Name des Themes
* @returns Ein Objekt mit vordefinierten Tailwind-Klassen für das angegebene Theme
*/
export function getThemeClasses(themeName: ThemeNames = 'blue') {
const theme = themes[themeName] || themes.blue;
return {
// Hintergrundfarben
background: 'bg-white dark:bg-gray-900',
backgroundSecondary: 'bg-gray-50 dark:bg-gray-800',
backgroundTertiary: 'bg-gray-100 dark:bg-gray-700',
// Textfarben
text: 'text-gray-800 dark:text-gray-200',
textSecondary: 'text-gray-600 dark:text-gray-400',
textTertiary: 'text-gray-500 dark:text-gray-500',
textAccent: `text-${theme.name}-600 dark:text-${theme.name}-400`,
// Randfarben
border: 'border-gray-200 dark:border-gray-700',
borderAccent: `border-${theme.name}-500 dark:border-${theme.name}-400`,
// Interaktive Elemente
button: `bg-${theme.name}-500 hover:bg-${theme.name}-600 text-white`,
buttonSecondary:
'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200',
// Karten und Container
card: 'bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm',
// Formulare
input: `bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-${theme.name}-500 focus:border-${theme.name}-500`,
};
}
/**
* Hook zum Abrufen der Theme-Klassen basierend auf dem aktuellen Theme
* @returns Ein Objekt mit vordefinierten Tailwind-Klassen für das aktuelle Theme
*/
export function useThemeClasses() {
const { themeName } = useTheme();
return getThemeClasses(themeName);
}
// Vordefinierte Theme-Klassen für häufig verwendete UI-Elemente (für Abwärtskompatibilität)
export const themeClasses = getThemeClasses();
// Export aller Theme-Funktionen und -Konstanten
export default {
colors,
themes,
useTheme,
useColorModeValue,
tw,
twMerge,
getThemeClasses,
useThemeClasses,
};