Migrate from tailwind-sidebar to syncfusion sidebar component

This commit is contained in:
2025-09-12 16:09:19 +00:00
parent 75c5622efe
commit f3b72da9fe
9 changed files with 1820 additions and 7183 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"@syncfusion/ej2-navigations": "^30.2.7", "@syncfusion/ej2-navigations": "^30.2.7",
"@syncfusion/ej2-notifications": "^30.2.4", "@syncfusion/ej2-notifications": "^30.2.4",
"@syncfusion/ej2-popups": "^30.2.4", "@syncfusion/ej2-popups": "^30.2.4",
"@syncfusion/ej2-react-buttons": "^30.1.37", "@syncfusion/ej2-react-buttons": "^30.2.4",
"@syncfusion/ej2-react-calendars": "^30.1.37", "@syncfusion/ej2-react-calendars": "^30.1.37",
"@syncfusion/ej2-react-dropdowns": "^30.1.37", "@syncfusion/ej2-react-dropdowns": "^30.1.37",
"@syncfusion/ej2-react-filemanager": "^30.1.38", "@syncfusion/ej2-react-filemanager": "^30.1.38",
@@ -31,7 +31,7 @@
"@syncfusion/ej2-react-inputs": "^30.1.38", "@syncfusion/ej2-react-inputs": "^30.1.38",
"@syncfusion/ej2-react-kanban": "^30.1.37", "@syncfusion/ej2-react-kanban": "^30.1.37",
"@syncfusion/ej2-react-layouts": "^30.1.40", "@syncfusion/ej2-react-layouts": "^30.1.40",
"@syncfusion/ej2-react-navigations": "^30.1.39", "@syncfusion/ej2-react-navigations": "^30.2.7",
"@syncfusion/ej2-react-notifications": "^30.1.37", "@syncfusion/ej2-react-notifications": "^30.1.37",
"@syncfusion/ej2-react-popups": "^30.1.37", "@syncfusion/ej2-react-popups": "^30.1.37",
"@syncfusion/ej2-react-schedule": "^30.1.37", "@syncfusion/ej2-react-schedule": "^30.1.37",

5067
dashboard/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"appName": "Infoscreen-Management", "appName": "Infoscreen-Management",
"version": "2025.1.0-alpha.3", "version": "2025.1.0-alpha.4",
"copyright": "© 2025 Third-Age-Applications", "copyright": "© 2025 Third-Age-Applications",
"supportContact": "support@third-age-applications.com", "supportContact": "support@third-age-applications.com",
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.", "description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",

View File

@@ -24,25 +24,51 @@ body {
--sidebar-bg: #e5d8c7; --sidebar-bg: #e5d8c7;
--sidebar-fg: #78591c; --sidebar-fg: #78591c;
--sidebar-border: #d6c3a6; --sidebar-border: #d6c3a6;
--sidebar-text: #000;
--sidebar-hover-bg: #d1b89b;
--sidebar-hover-text: #000;
--sidebar-active-bg: #cda76b;
--sidebar-active-text: #fff;
} }
/* Layout-Container für Sidebar und Content */ /* Layout-Container für Sidebar und Content */
.layout-container { .layout-container {
display: flex; display: flex;
position: relative; /* Wichtig für die absolute Positionierung des Inhalts */
height: 100vh; /* Feste Höhe auf die des Viewports setzen */ height: 100vh; /* Feste Höhe auf die des Viewports setzen */
overflow: hidden; /* Verhindert, dass der Scrollbalken den gesamten Container betrifft */ overflow: hidden; /* Verhindert, dass der Scrollbalken den gesamten Container betrifft */
} }
/* Sidebar fixieren, keine Scrollbalken */ /* Sidebar fixieren, keine Scrollbalken, volle Höhe */
.sidebar-theme { .sidebar-theme {
background-color: var(--sidebar-bg); background-color: var(--sidebar-bg);
color: var(--sidebar-fg); color: var(--sidebar-text);
font-size: 1.15rem; font-size: 1.15rem;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
flex-shrink: 0; flex-shrink: 0;
overflow: hidden;
z-index: 10; /* Stellt sicher, dass die Sidebar über dem Inhalt ist */ z-index: 10; /* Stellt sicher, dass die Sidebar über dem Inhalt ist */
height: 100vh; /* Volle Browser-Höhe */
min-height: 100vh; /* Mindesthöhe für volle Browser-Höhe */
max-height: 100vh; /* Maximale Höhe begrenzen */
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
}
/* Sicherstelle vertikale Anordnung der Navigation und Footer am Ende */
.sidebar-theme nav {
display: flex !important;
flex-direction: column !important;
flex: 1 1 auto !important;
overflow-y: auto !important;
min-height: 0 !important; /* Ermöglicht Flex-Shrinking */
}
/* Footer-Bereich am unteren Ende fixieren */
.sidebar-theme > div:last-child {
margin-top: auto !important;
flex-shrink: 0 !important;
min-height: auto !important;
padding-bottom: 0.5rem !important; /* Zusätzlicher Abstand vom unteren Rand */
} }
.sidebar-theme .sidebar-link { .sidebar-theme .sidebar-link {
@@ -50,6 +76,9 @@ body {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: flex !important;
width: 100% !important;
box-sizing: border-box;
} }
.sidebar-theme .sidebar-logout { .sidebar-theme .sidebar-logout {
@@ -58,38 +87,32 @@ body {
text-align: left; text-align: left;
width: 100%; width: 100%;
font-size: 1.15rem; font-size: 1.15rem;
display: flex !important;
box-sizing: border-box;
} }
.sidebar-theme .sidebar-btn,
.sidebar-theme .sidebar-link,
.sidebar-theme .sidebar-logout { .sidebar-link:hover,
background-color: var(--sidebar-bg); .sidebar-logout:hover {
color: var(--sidebar-fg); background-color: var(--sidebar-hover-bg);
transition: background 0.2s, color 0.2s; color: var(--sidebar-hover-text);
font-weight: 500;
} }
.sidebar-theme .sidebar-btn:hover, .sidebar-link.active {
.sidebar-theme .sidebar-link:hover, background-color: var(--sidebar-active-bg);
.sidebar-theme .sidebar-logout:hover { color: var(--sidebar-active-text);
background-color: var(--sidebar-fg); font-weight: bold;
color: var(--sidebar-bg);
} }
/* === START: ROBUSTES ABSOLUTE-POSITIONING-LAYOUT === */ /* === START: SYNCFUSION-KOMPATIBLES LAYOUT === */
/* Der Inhaltsbereich wird absolut positioniert, um den Rest des Bildschirms auszufüllen */ /* Der Inhaltsbereich arbeitet mit Syncfusion's natürlichem Layout */
.content-area { .content-area {
position: absolute;
inset: 0 0 0 16rem; /* Shorthand für top, right, bottom, left */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: inset-inline-start 0.3s ease-in-out; /* Animiert die 'left' Eigenschaft */ flex: 1;
} min-width: 0; /* Verhindert Flex-Item-Overflow */
/* Anpassung für die eingeklappte Sidebar */
.content-area.collapsed {
left: 5rem; /* Breite der eingeklappten Sidebar (w-20) */
} }
.content-header { .content-header {
@@ -103,7 +126,7 @@ body {
background-color: #f3f4f6; background-color: #f3f4f6;
} }
/* === ENDE: ROBUSTES ABSOLUTE-POSITIONING-LAYOUT === */ /* === ENDE: SYNCFUSION-KOMPATIBLES LAYOUT === */
/* Kanban-Karten im Sidebar-Style */ /* Kanban-Karten im Sidebar-Style */
@@ -156,4 +179,97 @@ body {
padding-bottom: 8px; padding-bottom: 8px;
} }
/* Entfernt - Syncfusion verwaltet das Layout selbst */
/* Grundlegende Sidebar-Styles - Syncfusion-kompatibel */
#sidebar .sidebar-link,
#sidebar .sidebar-logout {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
#sidebar .sidebar-link svg,
#sidebar .sidebar-logout svg {
flex-shrink: 0 !important;
}
/* Text standardmäßig IMMER sichtbar */
#sidebar .sidebar-link .sidebar-text,
#sidebar .sidebar-logout .sidebar-text {
margin-left: 0 !important;
display: inline-block !important;
opacity: 1 !important;
transition: opacity 0.3s, transform 0.3s !important;
}
#sidebar .sidebar-link:hover,
#sidebar .sidebar-logout:hover {
background-color: var(--sidebar-hover-bg) !important;
color: var(--sidebar-hover-text) !important;
}
/* Expanded state - Text sichtbar (Standard) */
#sidebar .sidebar-theme.expanded .sidebar-link,
#sidebar .sidebar-theme.expanded .sidebar-logout {
justify-content: flex-start !important;
padding: 12px 24px !important;
gap: 8px !important;
}
#sidebar .sidebar-theme.expanded .sidebar-text {
display: inline-block !important;
opacity: 1 !important;
}
#sidebar .sidebar-theme.expanded .sidebar-link svg,
#sidebar .sidebar-theme.expanded .sidebar-logout svg {
margin-right: 8px !important;
}
/* Collapsed state - nur Icons */
#sidebar .sidebar-theme.collapsed .sidebar-link,
#sidebar .sidebar-theme.collapsed .sidebar-logout {
justify-content: center !important;
padding: 12px 8px !important;
gap: 0 !important;
position: relative !important;
}
#sidebar .sidebar-theme.collapsed .sidebar-text {
display: none !important;
}
#sidebar .sidebar-theme.collapsed .sidebar-link svg,
#sidebar .sidebar-theme.collapsed .sidebar-logout svg {
margin-right: 0 !important;
}
/* Syncfusion TooltipComponent wird jetzt verwendet - CSS-Tooltips entfernt */
/* Logo und Versionsnummer im collapsed state ausblenden */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-50%) translateX(-5px);
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
}
/* Logo und Versionsnummer im collapsed state ausblenden */
#sidebar .sidebar-theme.collapsed img {
display: none !important;
}
#sidebar .sidebar-theme.collapsed .version-info {
display: none !important;
}

View File

@@ -1,5 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
import { SidebarComponent } from '@syncfusion/ej2-react-navigations';
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
import { TooltipComponent } from '@syncfusion/ej2-react-popups';
import logo from './assets/logo.png'; import logo from './assets/logo.png';
import './App.css'; import './App.css';
@@ -49,8 +52,9 @@ import Logout from './logout';
// const ENV = import.meta.env.VITE_ENV || 'development'; // const ENV = import.meta.env.VITE_ENV || 'development';
const Layout: React.FC = () => { const Layout: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const [version, setVersion] = useState(''); const [version, setVersion] = useState('');
const [isCollapsed, setIsCollapsed] = useState(false);
let sidebarRef: SidebarComponent | null;
React.useEffect(() => { React.useEffect(() => {
fetch('/program-info.json') fetch('/program-info.json')
@@ -59,84 +63,236 @@ const Layout: React.FC = () => {
.catch(err => console.error('Failed to load version info:', err)); .catch(err => console.error('Failed to load version info:', err));
}, []); }, []);
return ( const toggleSidebar = () => {
<div className="layout-container"> if (sidebarRef) {
{/* Sidebar */} sidebarRef.toggle();
<aside }
className={`sidebar-theme flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`} };
const onSidebarChange = () => {
// Syncfusion unterscheidet zwischen isOpen (true/false) und dem Dock-Modus
// Im Dock-Modus ist isOpen=true, aber die Sidebar ist kollabiert
const sidebar = sidebarRef?.element;
if (sidebar) {
const currentWidth = sidebar.style.width;
const newCollapsedState = currentWidth === '60px' || currentWidth.includes('60');
setIsCollapsed(newCollapsedState);
}
};
const sidebarTemplate = () => (
<div
className={`sidebar-theme ${isCollapsed ? 'collapsed' : 'expanded'}`}
style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
minHeight: '100vh',
overflow: 'hidden',
}}
> >
<div <div
className="h-20 flex items-center justify-center border-b"
style={{ borderColor: 'var(--sidebar-border)' }}
>
<img
src={logo}
alt="Logo"
className="h-12"
style={{ display: collapsed ? 'none' : 'block' }}
/>
</div>
<button
className="sidebar-btn p-2 focus:outline-none transition-colors"
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? 'Sidebar ausklappen' : 'Sidebar einklappen'}
type="button"
>
<span style={{ fontSize: 20 }}>{collapsed ? '▶' : '◀'}</span>
</button>
<nav className="flex-1 mt-4">
{sidebarItems.map(item => {
const Icon = item.icon;
return (
<Link
key={item.path}
to={item.path}
className="sidebar-link flex items-center gap-3 px-6 py-3 transition-colors no-underline"
title={collapsed ? item.name : undefined}
>
<Icon size={22} />
{!collapsed && item.name}
</Link>
);
})}
</nav>
{/* Abmelden-Button und Version immer ganz unten */}
<div className="mt-auto mb-2">
<Link
to="/logout"
className="sidebar-logout flex items-center gap-3 px-6 py-3 w-full transition-colors no-underline"
title={collapsed ? 'Abmelden' : undefined}
>
<LogOut size={22} />
{!collapsed && 'Abmelden'}
</Link>
{!collapsed && version && (
<div className="px-6 pt-2 text-xs text-center opacity-70">Version {version}</div>
)}
</div>
</aside>
{/* Main Content */}
<div className={`content-area ${collapsed ? 'collapsed' : ''}`}>
{/* Header */}
<header
className="content-header flex items-center px-8 shadow"
style={{ style={{
backgroundColor: '#e5d8c7', borderColor: 'var(--sidebar-border)',
color: '#78591c', height: '68px',
height: 'calc(48px + 20px)', flexShrink: 0,
fontSize: '1.15rem', display: 'flex',
fontFamily: alignItems: 'center',
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif', justifyContent: 'center',
borderBottom: '1px solid var(--sidebar-border)',
margin: 0,
padding: 0,
}} }}
> >
<img <img
src={logo} src={logo}
alt="Logo" alt="Logo"
className="h-12 mr-4" style={{
style={{ marginTop: 10, marginBottom: 10 }} height: '64px',
maxHeight: '60px',
display: 'block',
margin: '0 auto',
}}
/> />
<span className="text-2xl font-bold mr-8">Infoscreen-Management</span> </div>
<span className="ml-auto" style={{ color: '#78591c' }}> <nav
style={{
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
marginTop: '1rem',
overflowY: 'auto',
minHeight: 0, // Wichtig für Flex-Shrinking
}}
>
{sidebarItems.map(item => {
const Icon = item.icon;
const linkContent = (
<Link
key={item.path}
to={item.path}
className="sidebar-link no-underline w-full"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '8px',
padding: '12px 24px',
transition: 'background 0.2s, color 0.2s, justify-content 0.3s',
textDecoration: 'none',
color: 'var(--sidebar-fg)',
backgroundColor: 'var(--sidebar-bg)',
}}
>
<Icon size={22} style={{ flexShrink: 0, marginRight: 0 }} />
<span className="sidebar-text" style={{ marginLeft: 0, transition: 'opacity 0.3s' }}>
{item.name}
</span>
</Link>
);
// Syncfusion Tooltip nur im collapsed state
return isCollapsed ? (
<TooltipComponent
key={item.path}
content={item.name}
position="RightCenter"
opensOn="Hover"
showTipPointer={true}
animation={{
open: { effect: 'FadeIn', duration: 200 },
close: { effect: 'FadeOut', duration: 200 },
}}
>
{linkContent}
</TooltipComponent>
) : (
linkContent
);
})}
</nav>
<div
style={{
flexShrink: 0,
marginTop: 'auto',
display: 'flex',
flexDirection: 'column',
minHeight: 'auto',
}}
>
{(() => {
const logoutContent = (
<Link
to="/logout"
className="sidebar-logout no-underline w-full"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '8px',
padding: '12px 24px',
transition: 'background 0.2s, color 0.2s, justify-content 0.3s',
textDecoration: 'none',
color: 'var(--sidebar-fg)',
backgroundColor: 'var(--sidebar-bg)',
border: 'none',
cursor: 'pointer',
fontSize: '1.15rem',
}}
>
<LogOut size={22} style={{ flexShrink: 0, marginRight: 0 }} />
<span className="sidebar-text" style={{ marginLeft: 0, transition: 'opacity 0.3s' }}>
Abmelden
</span>
</Link>
);
// Syncfusion Tooltip nur im collapsed state
return isCollapsed ? (
<TooltipComponent
content="Abmelden"
position="RightCenter"
opensOn="Hover"
showTipPointer={true}
animation={{
open: { effect: 'FadeIn', duration: 200 },
close: { effect: 'FadeOut', duration: 200 },
}}
>
{logoutContent}
</TooltipComponent>
) : (
logoutContent
);
})()}
{version && (
<div
className="version-info px-6 py-2 text-xs text-center opacity-70 border-t"
style={{ borderColor: 'var(--sidebar-border)' }}
>
Version {version}
</div>
)}
</div>
</div>
);
return (
<div className="layout-container">
<SidebarComponent
id="sidebar"
ref={(sidebar: SidebarComponent | null) => {
sidebarRef = sidebar;
}}
width="256px"
target=".layout-container"
isOpen={true}
closeOnDocumentClick={false}
enableGestures={false}
type="Auto"
enableDock={true}
dockSize="60px"
change={onSidebarChange}
>
{sidebarTemplate()}
</SidebarComponent>
<div className="content-area">
<header
className="content-header flex items-center shadow"
style={{
backgroundColor: '#e5d8c7',
color: '#78591c',
height: '68px', // Exakt gleiche Höhe wie Sidebar-Header
fontSize: '1.15rem',
fontFamily:
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
margin: 0,
padding: '0 2rem 0 0', // Nur rechts Padding, links kein Padding
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
}}
>
<ButtonComponent
cssClass="e-inherit"
iconCss="e-icons e-menu"
onClick={toggleSidebar}
isToggle={true}
style={{
margin: '0 1rem 0 0', // Nur rechts Margin für Abstand zum Logo
padding: '8px 12px',
minWidth: '44px',
height: '44px',
flexShrink: 0,
}}
/>
<img src={logo} alt="Logo" className="h-16 mr-4" style={{ maxHeight: '60px' }} />
<span className="text-2xl font-bold mr-8" style={{ color: '#78591c' }}>
Infoscreen-Management
</span>
<span className="ml-auto text-lg font-medium" style={{ color: '#78591c' }}>
[Organisationsname] [Organisationsname]
</span> </span>
</header> </header>

View File

@@ -1,6 +1,7 @@
@tailwind base; /* @tailwind base;
@tailwind components; @tailwind components; */
@tailwind utilities;
/* @tailwind utilities; */
/* :root { /* :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;

View File

@@ -3,9 +3,14 @@ import { createRoot } from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App.tsx'; import App from './App.tsx';
import { registerLicense } from '@syncfusion/ej2-base'; import { registerLicense } from '@syncfusion/ej2-base';
import '@syncfusion/ej2-base/styles/material3.css';
import '@syncfusion/ej2-navigations/styles/material3.css';
import '@syncfusion/ej2-buttons/styles/material3.css';
// Setze hier deinen Lizenzschlüssel ein // Setze hier deinen Lizenzschlüssel ein
registerLicense('ORg4AjUWIQA/Gnt3VVhhQlJDfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5VdENiXX1dcHxUQWNVWkd2'); registerLicense(
'ORg4AjUWIQA/Gnt3VVhhQlJDfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5VdENiXX1dcHxUQWNVWkd2'
);
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>

View File

@@ -4,6 +4,33 @@ import react from '@vitejs/plugin-react';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: {
alias: {
'@syncfusion/ej2-react-navigations': '@syncfusion/ej2-react-navigations/index.js',
'@syncfusion/ej2-react-buttons': '@syncfusion/ej2-react-buttons/index.js',
},
},
optimizeDeps: {
include: [
'@syncfusion/ej2-react-navigations',
'@syncfusion/ej2-react-buttons',
'@syncfusion/ej2-base',
'@syncfusion/ej2-navigations',
'@syncfusion/ej2-buttons',
'@syncfusion/ej2-react-base',
],
force: true,
esbuildOptions: {
target: 'es2020',
},
},
build: {
target: 'es2020',
commonjsOptions: {
include: [/node_modules/],
transformMixedEsModules: true,
},
},
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
watch: { watch: {