Skip to content

Gemeinsame Komponenten

Audience: Dev
You will learn: - Geteilte Bibliotheken und Module zwischen Frontend/Backend - Icon-Schema und Datenstrukturen - Wiederverwendbare Utilities und Helper-Funktionen - Design-Tokens und CSS-Patterns

Pre-requisites: - Backend-Übersicht verstanden - Frontend Web-Interface Grundlagen - JavaScript und Python Entwicklungserfahrung

Datenstrukturen & Schemas

Icon-Datenmodell

// Shared icon data schema (TypeScript representation)
interface Icon {
    name: string;         // 'home' (ohne .svg extension)
    filename: string;     // 'home.svg'
    path: string;         // '/static/icons/home.svg'
    display_name: string; // 'Home' (human-readable)
    category: string;     // 'Navigation & Direction'
}

interface IconCategory {
    [categoryName: string]: string[]; // category -> array of filenames
}

interface IconResponse {
    icons: Icon[];
    categories: IconCategory;
    metadata?: {
        total_icons: number;
        total_categories: number;
        timestamp: number;
    };
}

Evidenz: app.py:30-42 (Icon object structure)

Category-Schema

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "title": "Icon Categories",
  "type": "object",
  "patternProperties": {
    "^.+$": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[a-z0-9-]+\\.svg$"
      },
      "uniqueItems": true
    }
  },
  "additionalProperties": false
}

Validation Rules: - Category Names: Keine Leerstrings, Unicode-Support - Filenames: Lowercase, Bindestriche, .svg Extension - Uniqueness: Jede Datei nur in einer Kategorie

Evidenz: icons.json:1-206 Struktur

Icon-Utility-Bibliothek

JavaScript Icon-Utilities

// shared/icon-utils.js
class IconUtils {
    /**
     * Generate display name from filename
     * 'arrow-left.svg' → 'Arrow Left'
     */
    static generateDisplayName(filename) {
        return filename
            .replace('.svg', '')
            .split('-')
            .map(word => word.charAt(0).toUpperCase() + word.slice(1))
            .join(' ');
    }

    /**
     * Validate SVG filename format
     */
    static isValidSVGFilename(filename) {
        return /^[a-z0-9-]+\.svg$/.test(filename);
    }

    /**
     * Extract icon name from filename
     * 'home.svg' → 'home'
     */
    static extractIconName(filename) {
        return filename.replace('.svg', '');
    }

    /**
     * Generate CSS class name from icon name
     * 'arrow-left' → 'icon-arrow-left'
     */
    static generateCSSClass(iconName) {
        return `icon-${iconName}`;
    }

    /**
     * Create SVG path for static serving
     */
    static generateIconPath(filename) {
        return `/static/icons/${filename}`;
    }

    /**
     * Filter icons by search term
     */
    static filterIcons(icons, searchTerm) {
        const term = searchTerm.toLowerCase();
        return icons.filter(icon => 
            icon.name.toLowerCase().includes(term) ||
            icon.display_name.toLowerCase().includes(term) ||
            icon.category.toLowerCase().includes(term)
        );
    }

    /**
     * Group icons by category
     */
    static groupByCategory(icons) {
        return icons.reduce((groups, icon) => {
            const category = icon.category;
            if (!groups[category]) {
                groups[category] = [];
            }
            groups[category].push(icon);
            return groups;
        }, {});
    }
}

// Export for Node.js and Browser
if (typeof module !== 'undefined' && module.exports) {
    module.exports = IconUtils;
} else {
    window.IconUtils = IconUtils;
}

Evidenz: app.py:39 display name generation, frontend patterns

Python Icon-Utilities

# shared/icon_utils.py
import re
import json
from pathlib import Path
from typing import Dict, List, Optional

class IconUtils:
    """Shared utilities for icon processing"""

    SVG_FILENAME_PATTERN = re.compile(r'^[a-z0-9-]+\.svg$')

    @staticmethod
    def generate_display_name(filename: str) -> str:
        """Generate human-readable display name from filename"""
        name = filename.replace('.svg', '')
        return name.replace('-', ' ').title()

    @staticmethod
    def is_valid_svg_filename(filename: str) -> bool:
        """Validate SVG filename format"""
        return bool(IconUtils.SVG_FILENAME_PATTERN.match(filename))

    @staticmethod
    def extract_icon_name(filename: str) -> str:
        """Extract icon name without extension"""
        return filename.replace('.svg', '')

    @staticmethod
    def create_icon_object(filename: str, category: str = 'Uncategorized') -> Dict:
        """Create standardized icon object"""
        name = IconUtils.extract_icon_name(filename)
        return {
            'name': name,
            'filename': filename,
            'path': f'/static/icons/{filename}',
            'display_name': IconUtils.generate_display_name(filename),
            'category': category
        }

    @staticmethod
    def validate_categories(categories: Dict[str, List[str]]) -> List[str]:
        """Validate category structure and return errors"""
        errors = []

        if not isinstance(categories, dict):
            errors.append("Categories must be a dictionary")
            return errors

        for category, filenames in categories.items():
            if not category or not category.strip():
                errors.append("Empty category name found")

            if not isinstance(filenames, list):
                errors.append(f"Category '{category}' must contain a list of filenames")
                continue

            for filename in filenames:
                if not IconUtils.is_valid_svg_filename(filename):
                    errors.append(f"Invalid filename in '{category}': {filename}")

        return errors

    @staticmethod
    def find_uncategorized_icons(icons_dir: Path, categories: Dict) -> List[str]:
        """Find SVG files not present in categories"""
        actual_files = {f.name for f in icons_dir.glob('*.svg')}
        categorized_files = set()

        for filenames in categories.values():
            categorized_files.update(filenames)

        return list(actual_files - categorized_files)

    @staticmethod
    def find_missing_files(icons_dir: Path, categories: Dict) -> List[str]:
        """Find categorized icons that don't exist as files"""
        actual_files = {f.name for f in icons_dir.glob('*.svg')}
        categorized_files = set()

        for filenames in categories.values():
            categorized_files.update(filenames)

        return list(categorized_files - actual_files)

# Example usage
if __name__ == '__main__':
    # Validate current categories
    with open('icons.json') as f:
        categories = json.load(f)

    errors = IconUtils.validate_categories(categories)
    if errors:
        print("Validation errors:")
        for error in errors:
            print(f"  - {error}")
    else:
        print("Categories validation passed")

Evidenz: app.py:15-43 logic patterns, validation needs

CSS Design Tokens

Icon-Styling Variablen

/* shared/icon-tokens.css */
:root {
  /* Icon Sizes */
  --icon-size-xs: 12px;
  --icon-size-sm: 16px;
  --icon-size-md: 24px;
  --icon-size-lg: 32px;
  --icon-size-xl: 48px;

  /* Icon Colors */
  --icon-color-primary: #007bff;
  --icon-color-secondary: #6c757d;
  --icon-color-success: #28a745;
  --icon-color-warning: #ffc107;
  --icon-color-danger: #dc3545;
  --icon-color-light: #f8f9fa;
  --icon-color-dark: #343a40;

  /* Icon Spacing */
  --icon-margin-inline: 0.25rem;
  --icon-margin-block: 0.125rem;

  /* Icon Effects */
  --icon-transition: all 0.2s ease;
  --icon-hover-scale: 1.1;
  --icon-hover-opacity: 0.8;

  /* Icon Card Layout */
  --icon-card-padding: 1rem;
  --icon-card-border-radius: 8px;
  --icon-card-border-color: #e0e0e0;
  --icon-card-hover-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);

  /* Grid Layout */
  --icon-grid-gap: 1rem;
  --icon-grid-min-width: 120px;
}

/* Dark theme overrides */
[data-theme="dark"] {
  --icon-color-primary: #4dabf7;
  --icon-color-secondary: #adb5bd;
  --icon-color-light: #495057;
  --icon-color-dark: #f8f9fa;
  --icon-card-border-color: #495057;
}

Standard-Icon-Klassen

/* shared/icon-classes.css */
.icon {
  width: var(--icon-size-md);
  height: var(--icon-size-md);
  fill: currentColor;
  vertical-align: -0.125em;
  transition: var(--icon-transition);
}

/* Size variants */
.icon--xs { width: var(--icon-size-xs); height: var(--icon-size-xs); }
.icon--sm { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.icon--lg { width: var(--icon-size-lg); height: var(--icon-size-lg); }
.icon--xl { width: var(--icon-size-xl); height: var(--icon-size-xl); }

/* Color variants */
.icon--primary { color: var(--icon-color-primary); }
.icon--secondary { color: var(--icon-color-secondary); }
.icon--success { color: var(--icon-color-success); }
.icon--warning { color: var(--icon-color-warning); }
.icon--danger { color: var(--icon-color-danger); }

/* Interactive states */
.icon--interactive {
  cursor: pointer;
  transition: var(--icon-transition);
}

.icon--interactive:hover {
  transform: scale(var(--icon-hover-scale));
  opacity: var(--icon-hover-opacity);
}

.icon--spinning {
  animation: icon-spin 1s linear infinite;
}

@keyframes icon-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Icon with text */
.icon-text {
  display: inline-flex;
  align-items: center;
  gap: var(--icon-margin-inline);
}

.icon-text--vertical {
  flex-direction: column;
  text-align: center;
  gap: var(--icon-margin-block);
}

Evidenz: Frontend CSS patterns, design consistency needs

API-Client Bibliothek

JavaScript API-Client

// shared/icon-api.js
class IconAPI {
    constructor(baseUrl = '') {
        this.baseUrl = baseUrl;
        this.cache = new Map();
        this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
    }

    /**
     * Get all icons with caching
     */
    async getAllIcons(useCache = true) {
        const cacheKey = 'all-icons';

        if (useCache && this.cache.has(cacheKey)) {
            const cached = this.cache.get(cacheKey);
            if (Date.now() - cached.timestamp < this.cacheTimeout) {
                return cached.data;
            }
        }

        try {
            const response = await fetch(`${this.baseUrl}/api/icons`);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            const data = await response.json();

            // Cache the response
            if (useCache) {
                this.cache.set(cacheKey, {
                    data,
                    timestamp: Date.now()
                });
            }

            return data;
        } catch (error) {
            console.error('Failed to fetch icons:', error);
            throw error;
        }
    }

    /**
     * Get specific icon
     */
    async getIcon(name) {
        try {
            const response = await fetch(`${this.baseUrl}/api/icon/${name}`);
            if (!response.ok) {
                throw new Error(`Icon '${name}' not found`);
            }
            return await response.json();
        } catch (error) {
            console.error(`Failed to fetch icon '${name}':`, error);
            throw error;
        }
    }

    /**
     * Search icons
     */
    async searchIcons(query, category = null) {
        const { icons } = await this.getAllIcons();

        return IconUtils.filterIcons(icons, query)
            .filter(icon => !category || icon.category === category);
    }

    /**
     * Get icons by category
     */
    async getIconsByCategory(category) {
        const { icons } = await this.getAllIcons();
        return icons.filter(icon => icon.category === category);
    }

    /**
     * Clear cache
     */
    clearCache() {
        this.cache.clear();
    }

    /**
     * Preload icon image
     */
    preloadIcon(iconPath) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => resolve(iconPath);
            img.onerror = reject;
            img.src = iconPath;
        });
    }

    /**
     * Bulk preload icons
     */
    async preloadIcons(iconPaths) {
        const results = await Promise.allSettled(
            iconPaths.map(path => this.preloadIcon(path))
        );

        return {
            loaded: results.filter(r => r.status === 'fulfilled').length,
            failed: results.filter(r => r.status === 'rejected').length
        };
    }
}

// Singleton instance
const iconAPI = new IconAPI();

// Export
if (typeof module !== 'undefined' && module.exports) {
    module.exports = { IconAPI, iconAPI };
} else {
    window.IconAPI = IconAPI;
    window.iconAPI = iconAPI;
}

Python API-Client

# shared/icon_client.py
import requests
import json
from typing import Dict, List, Optional
from dataclasses import dataclass
from urllib.parse import urljoin

@dataclass
class IconData:
    name: str
    filename: str
    path: str
    display_name: str
    category: str

    @classmethod
    def from_dict(cls, data: Dict) -> 'IconData':
        return cls(**data)

class IconAPIClient:
    """Python client for Icon Management API"""

    def __init__(self, base_url: str = 'http://localhost:5000'):
        self.base_url = base_url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'IconAPIClient/1.0'
        })

    def get_all_icons(self) -> Dict:
        """Get all icons and categories"""
        response = self.session.get(f'{self.base_url}/api/icons')
        response.raise_for_status()
        return response.json()

    def get_icon(self, name: str) -> Optional[IconData]:
        """Get specific icon by name"""
        try:
            response = self.session.get(f'{self.base_url}/api/icon/{name}')
            response.raise_for_status()
            return IconData.from_dict(response.json())
        except requests.HTTPError:
            return None

    def search_icons(self, query: str, category: Optional[str] = None) -> List[IconData]:
        """Search icons by name or category"""
        data = self.get_all_icons()
        icons = [IconData.from_dict(icon) for icon in data['icons']]

        # Filter by search term
        query_lower = query.lower()
        filtered = [
            icon for icon in icons
            if query_lower in icon.name.lower() 
            or query_lower in icon.display_name.lower()
            or query_lower in icon.category.lower()
        ]

        # Filter by category if specified
        if category:
            filtered = [icon for icon in filtered if icon.category == category]

        return filtered

    def get_categories(self) -> Dict[str, List[str]]:
        """Get all categories with their icons"""
        data = self.get_all_icons()
        return data['categories']

    def download_icon(self, icon_name: str, output_path: str) -> bool:
        """Download SVG file for specific icon"""
        icon = self.get_icon(icon_name)
        if not icon:
            return False

        try:
            response = self.session.get(f'{self.base_url}{icon.path}')
            response.raise_for_status()

            with open(output_path, 'wb') as f:
                f.write(response.content)
            return True
        except requests.RequestException:
            return False

# Example usage
if __name__ == '__main__':
    client = IconAPIClient()

    # Get all icons
    data = client.get_all_icons()
    print(f"Total icons: {len(data['icons'])}")

    # Search for navigation icons
    nav_icons = client.search_icons('arrow', 'Navigation & Direction')
    print(f"Navigation arrows: {len(nav_icons)}")

    # Download specific icon
    success = client.download_icon('home', 'downloaded-home.svg')
    print(f"Download success: {success}")

Evidenz: app.py:51-64 API endpoints

Validation & Testing Utilities

Schema Validation

# shared/validation.py
import json
import jsonschema
from pathlib import Path

ICON_CATEGORIES_SCHEMA = {
    "$schema": "https://json-schema.org/draft-07/schema#",
    "title": "Icon Categories",
    "type": "object",
    "patternProperties": {
        "^.+$": {
            "type": "array",
            "items": {
                "type": "string",
                "pattern": "^[a-z0-9-]+\\.svg$"
            },
            "uniqueItems": True
        }
    },
    "additionalProperties": False
}

def validate_categories_file(file_path: str) -> List[str]:
    """Validate icons.json against schema"""
    try:
        with open(file_path) as f:
            data = json.load(f)

        jsonschema.validate(data, ICON_CATEGORIES_SCHEMA)
        return []  # No errors
    except json.JSONDecodeError as e:
        return [f"Invalid JSON: {e}"]
    except jsonschema.ValidationError as e:
        return [f"Schema validation error: {e.message}"]
    except FileNotFoundError:
        return [f"File not found: {file_path}"]

def validate_svg_content(svg_content: str) -> bool:
    """Basic SVG content validation"""
    return (
        svg_content.strip().startswith('<svg') and
        svg_content.strip().endswith('</svg>') and
        'fill="currentColor"' in svg_content
    )

Test Utilities

# shared/test_utils.py
import tempfile
import shutil
from pathlib import Path
from typing import Dict, List

class IconTestFixture:
    """Test fixture for icon-related tests"""

    def __init__(self):
        self.temp_dir = None
        self.icons_dir = None
        self.metadata_file = None

    def setup(self):
        """Create temporary test environment"""
        self.temp_dir = Path(tempfile.mkdtemp())
        self.icons_dir = self.temp_dir / 'static' / 'icons'
        self.icons_dir.mkdir(parents=True)
        self.metadata_file = self.temp_dir / 'icons.json'

        # Create sample icons
        self.create_sample_icons()
        self.create_sample_metadata()

    def teardown(self):
        """Clean up test environment"""
        if self.temp_dir and self.temp_dir.exists():
            shutil.rmtree(self.temp_dir)

    def create_sample_icons(self):
        """Create sample SVG files"""
        sample_svg = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
            <path d="M12 2L2 7v10c0 5.55 3.84 9.95 9 11 5.16-1.05 9-5.45 9-11V7l-10-5z"/>
        </svg>'''

        icons = ['home.svg', 'user.svg', 'settings.svg']
        for icon in icons:
            (self.icons_dir / icon).write_text(sample_svg)

    def create_sample_metadata(self):
        """Create sample metadata file"""
        categories = {
            "Navigation": ["home.svg"],
            "User": ["user.svg"],
            "Settings": ["settings.svg"]
        }
        self.metadata_file.write_text(json.dumps(categories, indent=2))

    def add_icon(self, filename: str, category: str = "Test"):
        """Add icon to test fixture"""
        # Add SVG file
        svg_content = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
            <path d="M12 12m-10 0a10 10 0 1 0 20 0a10 10 0 1 0 -20 0"/>
        </svg>'''
        (self.icons_dir / filename).write_text(svg_content)

        # Update metadata
        with open(self.metadata_file) as f:
            categories = json.load(f)

        if category not in categories:
            categories[category] = []
        categories[category].append(filename)

        with open(self.metadata_file, 'w') as f:
            json.dump(categories, f, indent=2)

# Usage in tests
import unittest

class TestIconUtils(unittest.TestCase):
    def setUp(self):
        self.fixture = IconTestFixture()
        self.fixture.setup()

    def tearDown(self):
        self.fixture.teardown()

    def test_icon_validation(self):
        # Test with fixture data
        errors = validate_categories_file(str(self.fixture.metadata_file))
        self.assertEqual(errors, [])

Evidenz: Testing needs from development workflow

Configuration Management

Shared Configuration

# shared/config.py
import os
from pathlib import Path
from typing import Dict, Any

class IconConfig:
    """Shared configuration for icon management system"""

    # File paths
    ICONS_DIR = Path('static/icons')
    METADATA_FILE = Path('icons.json')
    TEMPLATES_DIR = Path('templates')

    # Icon constraints
    MAX_ICONS = int(os.environ.get('MAX_ICONS', '1000'))
    MAX_CATEGORIES = int(os.environ.get('MAX_CATEGORIES', '50'))
    MAX_ZIP_SIZE_MB = int(os.environ.get('MAX_ZIP_SIZE_MB', '10'))

    # API settings
    API_CACHE_TIMEOUT = int(os.environ.get('API_CACHE_TIMEOUT', '300'))

    # Development settings
    DEBUG = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
    ADMIN_ENABLED = os.environ.get('ADMIN_ENABLED', 'false').lower() == 'true'

    # Performance settings
    ENABLE_CACHING = os.environ.get('ENABLE_CACHING', 'true').lower() == 'true'

    @classmethod
    def validate(cls) -> List[str]:
        """Validate configuration"""
        errors = []

        if not cls.ICONS_DIR.exists():
            errors.append(f"Icons directory does not exist: {cls.ICONS_DIR}")

        if not cls.METADATA_FILE.exists():
            errors.append(f"Metadata file does not exist: {cls.METADATA_FILE}")

        if cls.MAX_ICONS < 1:
            errors.append("MAX_ICONS must be positive")

        return errors

    @classmethod
    def to_dict(cls) -> Dict[str, Any]:
        """Export configuration as dictionary"""
        return {
            'icons_dir': str(cls.ICONS_DIR),
            'metadata_file': str(cls.METADATA_FILE),
            'max_icons': cls.MAX_ICONS,
            'max_categories': cls.MAX_CATEGORIES,
            'debug': cls.DEBUG,
            'admin_enabled': cls.ADMIN_ENABLED,
            'caching_enabled': cls.ENABLE_CACHING
        }

Evidenz: Configuration needs across components


Integration: - Backend-Übersicht nutzt diese Utilities - Frontend implementiert shared CSS/JS - API-Referenz dokumentiert gemeinsame Schemas