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