Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
489
resources/js/modules/spa-router/SPARouter.js
Normal file
489
resources/js/modules/spa-router/SPARouter.js
Normal file
@@ -0,0 +1,489 @@
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
export class SPARouter {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
containerSelector: 'main',
|
||||
linkSelector: 'a[href^="/"]', // All internal links
|
||||
loadingClass: 'spa-loading',
|
||||
excludeSelector: '[data-spa="false"], [download], [target="_blank"], [href^="mailto:"], [href^="tel:"], [href^="#"]',
|
||||
enableTransitions: true,
|
||||
transitionDuration: 100, // Beschleunigt von 300ms auf 100ms
|
||||
skeletonTemplate: this.createSkeletonTemplate(),
|
||||
...options
|
||||
};
|
||||
|
||||
this.container = null;
|
||||
this.isLoading = false;
|
||||
this.currentUrl = window.location.href;
|
||||
this.abortController = null;
|
||||
|
||||
// Bind event handlers to preserve context for removal
|
||||
this.handleLinkClick = this.handleLinkClick.bind(this);
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
static create(options = {}) {
|
||||
return new SPARouter(options);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = document.querySelector(this.options.containerSelector);
|
||||
|
||||
if (!this.container) {
|
||||
Logger.error(`[SPARouter] Container "${this.options.containerSelector}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
this.setupStyles();
|
||||
|
||||
// Handle initial page load for history
|
||||
this.updateHistoryState(window.location.href, document.title);
|
||||
|
||||
Logger.info('[SPARouter] Initialized');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Intercept link clicks
|
||||
document.addEventListener('click', this.handleLinkClick);
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', this.handlePopState);
|
||||
|
||||
// Handle form submissions that might redirect
|
||||
document.addEventListener('submit', this.handleFormSubmit);
|
||||
}
|
||||
|
||||
handleLinkClick(event) {
|
||||
const link = event.target.closest(this.options.linkSelector);
|
||||
|
||||
if (!link) return;
|
||||
|
||||
// Check for exclusions
|
||||
if (link.matches(this.options.excludeSelector)) return;
|
||||
|
||||
// Check for modifier keys (Ctrl, Cmd, etc.)
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const href = link.href;
|
||||
const title = link.title || link.textContent.trim();
|
||||
|
||||
this.navigate(href, title);
|
||||
}
|
||||
|
||||
handlePopState(event) {
|
||||
const url = window.location.href;
|
||||
|
||||
if (url !== this.currentUrl) {
|
||||
this.loadContent(url, false); // Don't update history on popstate
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit(event) {
|
||||
const form = event.target;
|
||||
|
||||
// Only handle forms that might redirect (non-AJAX forms)
|
||||
if (form.hasAttribute('data-spa') && form.getAttribute('data-spa') === 'false') return;
|
||||
if (form._moduleInstance) return; // Skip forms with form-handling module
|
||||
|
||||
// For now, let forms submit normally
|
||||
// Could be enhanced later to handle form submissions via SPA
|
||||
}
|
||||
|
||||
async navigate(url, title = '') {
|
||||
if (this.isLoading) {
|
||||
Logger.warn(`[SPARouter] Already loading, aborting previous request`);
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
if (url === this.currentUrl) {
|
||||
Logger.info(`[SPARouter] Already at ${url}, skipping navigation`);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info(`[SPARouter] Navigating to: ${url}`);
|
||||
|
||||
try {
|
||||
await this.loadContent(url, true, title);
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
Logger.error('[SPARouter] Navigation failed:', error);
|
||||
// Fallback to normal navigation
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadContent(url, updateHistory = true, title = '') {
|
||||
// Prevent loading the same URL that's already current
|
||||
if (url === this.currentUrl && !updateHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isLoading) {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.abortController = new AbortController();
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
this.showLoadingState();
|
||||
|
||||
// Request only the main content
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-SPA-Request': 'true', // Signal to backend this is an SPA request
|
||||
'Accept': 'application/json, text/html'
|
||||
},
|
||||
signal: this.abortController.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Check content type to determine how to parse response
|
||||
const contentType = response.headers.get('content-type');
|
||||
let newContent, newTitle;
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
// Backend sent JSON SPA response
|
||||
const jsonData = await response.json();
|
||||
newContent = jsonData.html;
|
||||
newTitle = jsonData.title || title;
|
||||
|
||||
// Update meta tags if provided
|
||||
if (jsonData.meta) {
|
||||
this.updateMetaTags(jsonData.meta);
|
||||
}
|
||||
} else {
|
||||
// Backend sent full HTML response - extract content
|
||||
const html = await response.text();
|
||||
newContent = this.extractMainContent(html);
|
||||
newTitle = this.extractTitle(html) || title;
|
||||
}
|
||||
|
||||
// Update content
|
||||
await this.updateContent(newContent, newTitle);
|
||||
|
||||
// Update browser history
|
||||
if (updateHistory) {
|
||||
this.updateHistoryState(url, newTitle);
|
||||
}
|
||||
|
||||
this.currentUrl = url;
|
||||
|
||||
Logger.info(`[SPARouter] Successfully loaded: ${url}`);
|
||||
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
this.hideLoadingState();
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
extractMainContent(html) {
|
||||
// Create a temporary DOM to parse the response
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const mainElement = doc.querySelector('main');
|
||||
|
||||
if (mainElement) {
|
||||
return mainElement.innerHTML;
|
||||
}
|
||||
|
||||
// Fallback: try to find content in common containers
|
||||
const fallbackSelectors = ['[role="main"]', '.main-content', '#main', '.content'];
|
||||
|
||||
for (const selector of fallbackSelectors) {
|
||||
const element = doc.querySelector(selector);
|
||||
if (element) {
|
||||
Logger.warn(`[SPARouter] Using fallback selector: ${selector}`);
|
||||
return element.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: use the entire body
|
||||
Logger.warn('[SPARouter] No main element found, using entire body');
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
extractTitle(html) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const titleElement = doc.querySelector('title');
|
||||
return titleElement ? titleElement.textContent.trim() : '';
|
||||
}
|
||||
|
||||
async updateContent(newContent, newTitle) {
|
||||
// Update page title
|
||||
if (newTitle) {
|
||||
document.title = newTitle;
|
||||
}
|
||||
|
||||
// Smooth transition
|
||||
if (this.options.enableTransitions) {
|
||||
await this.transitionOut();
|
||||
}
|
||||
|
||||
// Update content
|
||||
this.container.innerHTML = newContent;
|
||||
|
||||
// Re-initialize modules for new content
|
||||
this.reinitializeModules();
|
||||
|
||||
// Smooth transition in
|
||||
if (this.options.enableTransitions) {
|
||||
await this.transitionIn();
|
||||
}
|
||||
|
||||
this.hideLoadingState();
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
// Trigger custom event
|
||||
this.triggerNavigationEvent();
|
||||
}
|
||||
|
||||
showLoadingState() {
|
||||
document.body.classList.add(this.options.loadingClass);
|
||||
|
||||
// Show skeleton loader
|
||||
if (this.options.enableTransitions) {
|
||||
this.container.classList.add('spa-transitioning-out');
|
||||
}
|
||||
}
|
||||
|
||||
hideLoadingState() {
|
||||
document.body.classList.remove(this.options.loadingClass);
|
||||
}
|
||||
|
||||
async transitionOut() {
|
||||
return new Promise(resolve => {
|
||||
// Verwende schnellere cubic-bezier für snappigere Animation
|
||||
this.container.style.transition = `opacity ${this.options.transitionDuration}ms cubic-bezier(0.4, 0, 1, 1)`;
|
||||
this.container.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, this.options.transitionDuration);
|
||||
});
|
||||
}
|
||||
|
||||
async transitionIn() {
|
||||
return new Promise(resolve => {
|
||||
this.container.style.opacity = '0';
|
||||
|
||||
// Reduziere Verzögerung von 50ms auf 10ms
|
||||
setTimeout(() => {
|
||||
// Verwende schnellere cubic-bezier für snappigere Animation
|
||||
this.container.style.transition = `opacity ${this.options.transitionDuration}ms cubic-bezier(0, 0, 0.2, 1)`;
|
||||
this.container.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
this.container.style.transition = '';
|
||||
this.container.classList.remove('spa-transitioning-out');
|
||||
resolve();
|
||||
}, this.options.transitionDuration);
|
||||
}, 10); // Von 50ms auf 10ms reduziert
|
||||
});
|
||||
}
|
||||
|
||||
updateHistoryState(url, title) {
|
||||
const state = { url, title, timestamp: Date.now() };
|
||||
|
||||
if (url !== window.location.href) {
|
||||
history.pushState(state, title, url);
|
||||
} else {
|
||||
history.replaceState(state, title, url);
|
||||
}
|
||||
}
|
||||
|
||||
reinitializeModules() {
|
||||
// Re-run form auto-enhancement for new content
|
||||
if (window.initAutoFormHandling) {
|
||||
window.initAutoFormHandling();
|
||||
}
|
||||
|
||||
// Re-initialize any data-module elements in new content
|
||||
const moduleElements = this.container.querySelectorAll('[data-module]');
|
||||
|
||||
moduleElements.forEach(element => {
|
||||
const moduleName = element.dataset.module;
|
||||
Logger.info(`[SPARouter] Re-initializing module "${moduleName}" on new content`);
|
||||
|
||||
// Trigger module initialization (would need access to module system)
|
||||
const event = new CustomEvent('spa:reinit-module', {
|
||||
detail: { element, moduleName },
|
||||
bubbles: true
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
createSkeletonTemplate() {
|
||||
return `
|
||||
<div class="spa-skeleton">
|
||||
<div class="spa-skeleton-header"></div>
|
||||
<div class="spa-skeleton-content">
|
||||
<div class="spa-skeleton-line"></div>
|
||||
<div class="spa-skeleton-line"></div>
|
||||
<div class="spa-skeleton-line short"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupStyles() {
|
||||
if (document.getElementById('spa-router-styles')) return;
|
||||
|
||||
const styles = document.createElement('style');
|
||||
styles.id = 'spa-router-styles';
|
||||
styles.textContent = `
|
||||
/* SPA Router Transitions */
|
||||
.spa-loading {
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.spa-transitioning-out {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Skeleton Loading Styles */
|
||||
.spa-skeleton {
|
||||
animation: spa-pulse 1.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.spa-skeleton-header {
|
||||
height: 2rem;
|
||||
background: #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.spa-skeleton-content {
|
||||
space-y: 0.75rem;
|
||||
}
|
||||
|
||||
.spa-skeleton-line {
|
||||
height: 1rem;
|
||||
background: #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.spa-skeleton-line.short {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@keyframes spa-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.spa-skeleton-header,
|
||||
.spa-skeleton-line {
|
||||
background: #374151;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
|
||||
updateMetaTags(metaData) {
|
||||
// Update meta description
|
||||
if (metaData.description) {
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (metaDesc) {
|
||||
metaDesc.content = metaData.description;
|
||||
} else {
|
||||
metaDesc = document.createElement('meta');
|
||||
metaDesc.name = 'description';
|
||||
metaDesc.content = metaData.description;
|
||||
document.head.appendChild(metaDesc);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other meta tag updates as needed
|
||||
Object.entries(metaData).forEach(([key, value]) => {
|
||||
if (key !== 'description' && value) {
|
||||
let metaTag = document.querySelector(`meta[name="${key}"]`);
|
||||
if (metaTag) {
|
||||
metaTag.content = value;
|
||||
} else {
|
||||
metaTag = document.createElement('meta');
|
||||
metaTag.name = key;
|
||||
metaTag.content = value;
|
||||
document.head.appendChild(metaTag);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
triggerNavigationEvent() {
|
||||
const event = new CustomEvent('spa:navigated', {
|
||||
detail: {
|
||||
url: this.currentUrl,
|
||||
container: this.container,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Public API
|
||||
navigateTo(url, title) {
|
||||
return this.navigate(url, title);
|
||||
}
|
||||
|
||||
getCurrentUrl() {
|
||||
return this.currentUrl;
|
||||
}
|
||||
|
||||
isNavigating() {
|
||||
return this.isLoading;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.abortController?.abort();
|
||||
|
||||
// Remove event listeners
|
||||
document.removeEventListener('click', this.handleLinkClick);
|
||||
window.removeEventListener('popstate', this.handlePopState);
|
||||
document.removeEventListener('submit', this.handleFormSubmit);
|
||||
|
||||
// Remove styles
|
||||
const styles = document.getElementById('spa-router-styles');
|
||||
if (styles) {
|
||||
styles.remove();
|
||||
}
|
||||
|
||||
Logger.info('[SPARouter] Destroyed');
|
||||
}
|
||||
}
|
||||
159
resources/js/modules/spa-router/index.js
Normal file
159
resources/js/modules/spa-router/index.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Logger } from '../../core/logger.js';
|
||||
import { SPARouter } from './SPARouter.js';
|
||||
import { getAdaptiveTransition, fastTransition } from './transition-config.js';
|
||||
|
||||
/**
|
||||
* SPA Router Module
|
||||
*
|
||||
* Provides Single Page Application navigation:
|
||||
* - Intercepts internal links and loads content via AJAX
|
||||
* - Updates only the <main> element, keeping header/footer unchanged
|
||||
* - Manages browser history (back/forward buttons work)
|
||||
* - Shows skeleton loading states during navigation
|
||||
* - Progressive enhancement (falls back to normal navigation on errors)
|
||||
*
|
||||
* Features:
|
||||
* - Automatic link interception for all internal links (href="/...")
|
||||
* - Opt-out via data-spa="false"
|
||||
* - Browser history management with pushState
|
||||
* - Loading states and smooth transitions
|
||||
* - Module re-initialization after content changes
|
||||
* - Skeleton loading animation
|
||||
*
|
||||
* Backend Integration:
|
||||
* - Sends X-SPA-Request header to signal SPA navigation
|
||||
* - Backend can return only <main> content for SPA requests
|
||||
* - Falls back to full page load if SPA request fails
|
||||
*/
|
||||
|
||||
const SPARouterModule = {
|
||||
name: 'spa-router',
|
||||
router: null,
|
||||
initialized: false,
|
||||
|
||||
init(config = {}) {
|
||||
// Prevent multiple initialization
|
||||
if (this.initialized && this.router) {
|
||||
Logger.warn('[SPARouterModule] SPA Router already initialized, returning existing instance');
|
||||
return this.router;
|
||||
}
|
||||
|
||||
Logger.info('[SPARouterModule] Initializing SPA Router');
|
||||
|
||||
// Default configuration mit schnelleren Transitions
|
||||
const defaultConfig = {
|
||||
containerSelector: 'main',
|
||||
linkSelector: 'a[href^="/"]',
|
||||
excludeSelector: '[data-spa="false"], [download], [target="_blank"], [href^="mailto:"], [href^="tel:"], [href^="#"]',
|
||||
enableSkeletonLoading: true,
|
||||
...fastTransition, // Verwende schnelle Transitions als Standard
|
||||
...getAdaptiveTransition() // Überschreibe mit adaptiven Einstellungen
|
||||
};
|
||||
|
||||
const options = { ...defaultConfig, ...config };
|
||||
|
||||
// Initialize router
|
||||
this.router = SPARouter.create(options);
|
||||
this.initialized = true;
|
||||
|
||||
// Set up global access
|
||||
if (typeof window !== 'undefined') {
|
||||
window.spaRouter = this.router;
|
||||
}
|
||||
|
||||
// Listen for module re-initialization events
|
||||
document.addEventListener('spa:reinit-module', this.handleModuleReinit.bind(this));
|
||||
|
||||
// Listen for navigation events for debugging
|
||||
document.addEventListener('spa:navigated', this.handleNavigation.bind(this));
|
||||
|
||||
Logger.info('[SPARouterModule] SPA Router initialized successfully');
|
||||
|
||||
return this.router;
|
||||
},
|
||||
|
||||
handleModuleReinit(event) {
|
||||
const { element, moduleName } = event.detail;
|
||||
|
||||
Logger.info(`[SPARouterModule] Re-initializing module: ${moduleName}`, element);
|
||||
|
||||
// This would need access to the main module system
|
||||
// For now, we'll just log and let the main init system handle it
|
||||
|
||||
// Trigger a custom event that the main module system can listen to
|
||||
const reinitEvent = new CustomEvent('module:reinit-needed', {
|
||||
detail: { element, moduleName },
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
document.dispatchEvent(reinitEvent);
|
||||
},
|
||||
|
||||
handleNavigation(event) {
|
||||
const { url, timestamp } = event.detail;
|
||||
|
||||
Logger.info(`[SPARouterModule] Navigation completed to: ${url}`);
|
||||
|
||||
// Re-run auto form enhancement for new content
|
||||
if (typeof window.initAutoFormHandling === 'function') {
|
||||
// Only re-initialize forms that are not already enhanced
|
||||
setTimeout(() => {
|
||||
window.initAutoFormHandling();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Trigger analytics or other tracking if needed
|
||||
if (typeof window.gtag === 'function') {
|
||||
window.gtag('config', 'GA_TRACKING_ID', {
|
||||
page_path: new URL(url).pathname
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Public API methods
|
||||
navigateTo(url, title) {
|
||||
if (this.router) {
|
||||
return this.router.navigateTo(url, title);
|
||||
}
|
||||
Logger.warn('[SPARouterModule] Router not initialized');
|
||||
},
|
||||
|
||||
getCurrentUrl() {
|
||||
return this.router?.getCurrentUrl() || window.location.href;
|
||||
},
|
||||
|
||||
isNavigating() {
|
||||
return this.router?.isNavigating() || false;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this.router) {
|
||||
this.router.destroy();
|
||||
this.router = null;
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
|
||||
// Remove global reference
|
||||
if (typeof window !== 'undefined' && window.spaRouter) {
|
||||
delete window.spaRouter;
|
||||
}
|
||||
|
||||
document.removeEventListener('spa:reinit-module', this.handleModuleReinit);
|
||||
document.removeEventListener('spa:navigated', this.handleNavigation);
|
||||
|
||||
Logger.info('[SPARouterModule] SPA Router destroyed');
|
||||
}
|
||||
};
|
||||
|
||||
// Export the router class for direct usage
|
||||
export { SPARouter };
|
||||
|
||||
// Export as default for module system
|
||||
export default SPARouterModule;
|
||||
|
||||
// Export init function directly for compatibility with module system
|
||||
export const init = SPARouterModule.init.bind(SPARouterModule);
|
||||
|
||||
// Also export named for direct usage
|
||||
export { SPARouterModule };
|
||||
67
resources/js/modules/spa-router/transition-config.js
Normal file
67
resources/js/modules/spa-router/transition-config.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* SPA Router Transition Configuration
|
||||
*
|
||||
* Verschiedene Transition-Presets für unterschiedliche Use-Cases
|
||||
*/
|
||||
|
||||
// Ultra-schnelle Transitions (fast keine sichtbare Animation)
|
||||
export const instantTransition = {
|
||||
enableTransitions: true,
|
||||
transitionDuration: 50 // 50ms - kaum wahrnehmbar
|
||||
};
|
||||
|
||||
// Schnelle Transitions (snappy, aber noch sichtbar)
|
||||
export const fastTransition = {
|
||||
enableTransitions: true,
|
||||
transitionDuration: 100 // 100ms - schnell und responsiv
|
||||
};
|
||||
|
||||
// Standard Transitions (balanced)
|
||||
export const standardTransition = {
|
||||
enableTransitions: true,
|
||||
transitionDuration: 200 // 200ms - ausgewogen
|
||||
};
|
||||
|
||||
// Langsame Transitions (smooth, aber träge)
|
||||
export const slowTransition = {
|
||||
enableTransitions: true,
|
||||
transitionDuration: 300 // 300ms - original Wert
|
||||
};
|
||||
|
||||
// Keine Transitions (instant switch)
|
||||
export const noTransition = {
|
||||
enableTransitions: false,
|
||||
transitionDuration: 0
|
||||
};
|
||||
|
||||
// Performance-basierte Konfiguration
|
||||
export function getAdaptiveTransition() {
|
||||
// Check for reduced motion preference
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
return noTransition;
|
||||
}
|
||||
|
||||
// Check device performance
|
||||
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
|
||||
if (connection) {
|
||||
// Schnelle Verbindung = schnellere Transitions
|
||||
if (connection.effectiveType === '4g') {
|
||||
return instantTransition;
|
||||
} else if (connection.effectiveType === '3g') {
|
||||
return fastTransition;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if device is likely mobile (rough detection)
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
return fastTransition; // Mobile devices: schnellere Transitions
|
||||
}
|
||||
|
||||
return fastTransition; // Default zu schnell
|
||||
}
|
||||
|
||||
// Export default configuration
|
||||
export default fastTransition;
|
||||
Reference in New Issue
Block a user