Loading frontend/SongList.mjs +54 −39 Original line number Diff line number Diff line // SongList.mjs import SongItem from './SongItem.mjs'; import initialSongs from './mockData.mjs'; // Deine Mock-Daten import initialSongs from './mockData.mjs'; const SongList = { songs: [], groupedAndSortedSongs: {}, allSongs: [], // Speichert alle initial geladenen Songs isLoading: true, loadSongs: function() { loadAllSongs: function() { this.isLoading = true; // Simuliere einen API-Call // In echt: m.request({ url: "https://example.com/api/songs" }).then(...) new Promise(resolve => setTimeout(() => resolve(initialSongs), 200)) // Kurze Verzögerung für Lade-Effekt // Simulierter API-Call new Promise(resolve => setTimeout(() => resolve(initialSongs), 200)) .then(data => { this.songs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite })); // Stelle sicher, dass isFavorite existiert this.processSongs(); this.allSongs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite })); this.isLoading = false; m.redraw(); // Wichtig nach asynchronen Operationen außerhalb Mithrils Lebenszyklus m.redraw(); }).catch(error => { console.error("Fehler beim Laden der Songs:", error); this.isLoading = false; // Hier könntest du eine Fehlermeldung anzeigen m.redraw(); }); }, oninit: function() { this.loadSongs(); this.loadAllSongs(); }, processSongs: function() { getProcessedSongsForView: function(viewFilter) { let songsToProcess = []; let viewSpecificMessage = null; if (this.allSongs && this.allSongs.length > 0) { switch (viewFilter) { case 'favorites': songsToProcess = this.allSongs.filter(song => song.isFavorite); if (songsToProcess.length === 0) { viewSpecificMessage = "Du hast noch keine Favoriten markiert."; } break; case 'popular': // Hier könnten später Songs von 'https://example.com/api/songs/popular' geladen werden songsToProcess = []; // Fürs Erste leer lassen viewSpecificMessage = "Die Liste der beliebten Songs ist bald verfügbar!"; break; case 'all': default: songsToProcess = this.allSongs; if (songsToProcess.length === 0) { viewSpecificMessage = "Keine Songs in der Bibliothek."; } break; } } else if (!this.isLoading) { viewSpecificMessage = "Keine Songs geladen oder Bibliothek ist leer."; } const groups = {}; this.songs.forEach(song => { songsToProcess.forEach(song => { groups[song.artist] = groups[song.artist] || []; groups[song.artist].push(song); }); Loading @@ -42,49 +67,39 @@ const SongList = { sortedArtistNames.forEach(artist => { finalGroupedSongs[artist] = groups[artist].sort((a, b) => a.title.localeCompare(b.title)); }); this.groupedAndSortedSongs = finalGroupedSongs; return { groupedSongs: finalGroupedSongs, message: viewSpecificMessage, hasContent: songsToProcess.length > 0 }; }, toggleFavorite: function(songId) { const song = this.songs.find(s => s.id === songId); const song = this.allSongs.find(s => s.id === songId); if (song) { song.isFavorite = !song.isFavorite; // Hier würdest du den API Call machen: // m.request({ // method: "PUT", // url: `https://example.com/api/songs/${song.id}/favorite`, // body: { isFavorite: song.isFavorite } // }).then(() => { // console.log(`Favoritenstatus für ${song.title} geändert.`); // }).catch(e => { // console.error("Fehler beim Ändern des Favoritenstatus:", e); // song.isFavorite = !song.isFavorite; // Rollback bei Fehler // m.redraw(); // UI aktualisieren // }); // Für dieses Frontend-Beispiel reicht das direkte Ändern und Neuzeichnen (Mithril macht das bei Event-Handlern meist automatisch) // Da `groupedAndSortedSongs` von `this.songs` abhängt, muss es nicht neu prozessiert werden, // solange die Reihenfolge/Gruppierung sich nicht ändert, nur das Icon. // Ein m.redraw() wird implizit durch den Klick-Handler ausgelöst. // API Call: m.request({ method: "PUT", url: `https://example.com/api/songs/${song.id}/favorite`, body: { isFavorite: song.isFavorite }}) // Mithril's automatisches Redraw nach dem Event-Handler aktualisiert die Ansicht. // Wenn die 'Favoriten'-Ansicht aktiv ist, wird die Liste korrekt neu gerendert. } }, view: function() { view: function(vnode) { const { currentView } = vnode.attrs; if (this.isLoading) { return m("div.song-list-outer-container", m("div.loading-placeholder", "Lade Songs...")); } const artists = Object.keys(this.groupedAndSortedSongs); const { groupedSongs, message, hasContent } = this.getProcessedSongsForView(currentView); const artists = Object.keys(groupedSongs); if (artists.length === 0) { return m("div.song-list-outer-container", m("p", "Keine Songs verfügbar.")); if (message && !hasContent) { // Zeige Nachricht nur, wenn keine Songs für die aktuelle Ansicht da sind return m("div.song-list-outer-container", m("p.view-message", message)); } return m("div.song-list-outer-container", artists.map(artist => m("div.artist-group", { key: artist }, [ m("div.artist-group", { key: `${currentView}-${artist}` }, [ // Key um Eindeutigkeit bei View-Wechsel zu erhöhen m("h2.artist-name", artist), this.groupedAndSortedSongs[artist].map(song => groupedSongs[artist].map(song => m(SongItem, { key: song.id, song: song, Loading frontend/TabBar.mjs 0 → 100644 +25 −0 Original line number Diff line number Diff line // TabBar.mjs const TabBar = { view: function(vnode) { const { currentView, onViewChange } = vnode.attrs; const tabs = [ { id: 'all', label: 'Alle' }, { id: 'favorites', label: 'Favoriten' }, { id: 'popular', label: 'Beliebt' } // 'Beliebt' als Option ]; return m("div.tab-bar-container", tabs.map(tab => { return m("div.tab-item", { class: currentView === tab.id ? 'active' : '', onclick: () => onViewChange(tab.id), role: "button", // Für Barrierefreiheit tabindex: 0, // Für Tastaturnavigation onkeypress: (e) => { if (e.key === 'Enter' || e.key === ' ') onViewChange(tab.id); } }, tab.label); }) ); } }; export default TabBar; No newline at end of file frontend/app.mjs +13 −1 Original line number Diff line number Diff line // app.mjs import SearchBar from './SearchBar.mjs'; import TabBar from './TabBar.mjs'; // Neu importiert import SongList from './SongList.mjs'; const AppLayout = { currentView: 'all', // Mögliche Werte: 'all', 'favorites', 'popular' onViewChange: function(view) { this.currentView = view; // m.redraw() wird von Mithril automatisch nach Event-Handlern aufgerufen }, view: function() { return m("div.app-layout", [ m(SearchBar), m(SongList) m(TabBar, { currentView: this.currentView, onViewChange: this.onViewChange.bind(this) // .bind(this) ist wichtig }), m(SongList, { currentView: this.currentView }) // Übergabe des aktuellen Views ]); } }; Loading frontend/style.css +78 −26 Original line number Diff line number Diff line /* Globale Resets und Basis-Styling */ /* Globale Resets und Basis-Styling (wie vorher) */ body, html { margin: 0; padding: 0; font-family: 'Roboto', sans-serif; background-color: #f4f4f9; /* Heller Hintergrund für Kontrast */ background-color: #f4f4f9; color: #333; line-height: 1.6; } Loading @@ -13,14 +13,14 @@ body, html { margin: 0 auto; } .loading-placeholder { .loading-placeholder, .view-message { /* Kombiniert für Konsistenz */ text-align: center; padding: 50px; font-size: 1.2em; color: #777; padding: 40px 20px; font-size: 1.1em; color: #757575; } /* Suchleiste */ /* Suchleiste (Höhe ca. 60px) */ .search-bar-container { position: fixed; top: 0; Loading @@ -30,6 +30,8 @@ body, html { padding: 12px 16px; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.2); box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */ height: 60px; /* Feste Höhe für genaue Positionierung */ } .search-input { Loading @@ -47,27 +49,69 @@ body, html { color: #888; } /* Tab Bar (Höhe ca. 48px) */ .tab-bar-container { display: flex; position: fixed; top: 60px; /* Direkt unter der Suchleiste */ left: 0; right: 0; background-color: #3F51B5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */ z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */ box-shadow: 0 2px 4px rgba(0,0,0,0.15); /* Dezenter Schatten */ height: 48px; /* Feste Höhe */ box-sizing: border-box; } .tab-item { flex-grow: 1; display: flex; /* Für vertikales Zentrieren des Texts */ align-items: center; justify-content: center; padding: 0 8px; /* Horizontal Padding */ text-align: center; color: rgba(255,255,255,0.75); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */ cursor: pointer; font-weight: 500; font-size: 0.9em; /* Leicht kleiner als Haupttext */ text-transform: uppercase; border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */ transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; user-select: none; } .tab-item:hover { background-color: rgba(255,255,255,0.1); /* Leichter Hover-Effekt */ } .tab-item.active { color: #FFFFFF; /* Reines Weiß für aktiven Tab */ border-bottom-color: #FF4081; /* Material Accent Pink für den Indikator */ } /* Song-Liste Container */ .song-list-outer-container { padding-top: 60px; /* Höhe der Suchleiste + etwas Puffer */ /* padding-top: calc(60px + 48px + 10px); /* Höhe Suchleiste + Höhe TabBar + kleiner Abstand */ padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */ padding-bottom: 20px; } /* Restliche Styles für Artist-Group, Song-Item etc. bleiben wie zuvor */ .artist-group { margin-bottom: 12px; margin: 0 8px 12px 8px; /* Etwas seitlicher Margin */ background-color: #ffffff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; /* Für abgerundete Ecken bei Kindern */ overflow: hidden; } .artist-name { padding: 12px 16px; background-color: #E8EAF6; /* Indigo Lighten-5 */ color: #303F9F; /* Indigo Darken-2 */ background-color: #E8EAF6; color: #303F9F; font-size: 1.1em; font-weight: 500; border-bottom: 1px solid #C5CAE9; /* Indigo Lighten-4 */ border-bottom: 1px solid #C5CAE9; } .song-item { Loading @@ -89,28 +133,37 @@ body, html { .song-info { flex-grow: 1; margin-right: 8px; /* Abstand zum Icon */ overflow: hidden; /* Verhindert Textüberlauf */ } .song-title { font-size: 1em; font-weight: 400; color: #212121; /* Almost Black */ color: #212121; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* Text abschneiden bei Überlauf */ } .song-artist { font-size: 0.85em; color: #757575; /* Grey */ color: #757575; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .song-actions { margin-left: 16px; margin-left: auto; /* Pusht das Icon nach rechts, falls song-info nicht flex-grow hätte */ flex-shrink: 0; /* Verhindert, dass das Icon schrumpft */ } .heart-icon { cursor: pointer; color: #BDBDBD; /* Grey Lighten-1 (Outline) */ font-size: 24px; /* Standardgröße für Material Icons */ user-select: none; /* Verhindert Textauswahl beim Klicken */ color: #BDBDBD; font-size: 24px; user-select: none; transition: color 0.2s ease-in-out, transform 0.1s ease-out; vertical-align: middle; } Loading @@ -120,15 +173,14 @@ body, html { } .heart-icon.is-favorite { color: #FF4081; /* Pink A200 - Material Accent */ font-variation-settings: 'FILL' 1; /* Füllt das Icon */ color: #FF4081; font-variation-settings: 'FILL' 1; } /* Material Symbols Basis-Styling (optional, oft schon gut) */ .material-symbols-outlined { font-variation-settings: 'FILL' 0, /* 0 für Outline, 1 für Filled */ 'wght' 400, /* Schriftstärke */ 'GRAD' 0, /* Verlauf */ 'opsz' 24 /* Optische Größe */ 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; } No newline at end of file Loading
frontend/SongList.mjs +54 −39 Original line number Diff line number Diff line // SongList.mjs import SongItem from './SongItem.mjs'; import initialSongs from './mockData.mjs'; // Deine Mock-Daten import initialSongs from './mockData.mjs'; const SongList = { songs: [], groupedAndSortedSongs: {}, allSongs: [], // Speichert alle initial geladenen Songs isLoading: true, loadSongs: function() { loadAllSongs: function() { this.isLoading = true; // Simuliere einen API-Call // In echt: m.request({ url: "https://example.com/api/songs" }).then(...) new Promise(resolve => setTimeout(() => resolve(initialSongs), 200)) // Kurze Verzögerung für Lade-Effekt // Simulierter API-Call new Promise(resolve => setTimeout(() => resolve(initialSongs), 200)) .then(data => { this.songs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite })); // Stelle sicher, dass isFavorite existiert this.processSongs(); this.allSongs = data.map(s => ({ ...s, isFavorite: !!s.isFavorite })); this.isLoading = false; m.redraw(); // Wichtig nach asynchronen Operationen außerhalb Mithrils Lebenszyklus m.redraw(); }).catch(error => { console.error("Fehler beim Laden der Songs:", error); this.isLoading = false; // Hier könntest du eine Fehlermeldung anzeigen m.redraw(); }); }, oninit: function() { this.loadSongs(); this.loadAllSongs(); }, processSongs: function() { getProcessedSongsForView: function(viewFilter) { let songsToProcess = []; let viewSpecificMessage = null; if (this.allSongs && this.allSongs.length > 0) { switch (viewFilter) { case 'favorites': songsToProcess = this.allSongs.filter(song => song.isFavorite); if (songsToProcess.length === 0) { viewSpecificMessage = "Du hast noch keine Favoriten markiert."; } break; case 'popular': // Hier könnten später Songs von 'https://example.com/api/songs/popular' geladen werden songsToProcess = []; // Fürs Erste leer lassen viewSpecificMessage = "Die Liste der beliebten Songs ist bald verfügbar!"; break; case 'all': default: songsToProcess = this.allSongs; if (songsToProcess.length === 0) { viewSpecificMessage = "Keine Songs in der Bibliothek."; } break; } } else if (!this.isLoading) { viewSpecificMessage = "Keine Songs geladen oder Bibliothek ist leer."; } const groups = {}; this.songs.forEach(song => { songsToProcess.forEach(song => { groups[song.artist] = groups[song.artist] || []; groups[song.artist].push(song); }); Loading @@ -42,49 +67,39 @@ const SongList = { sortedArtistNames.forEach(artist => { finalGroupedSongs[artist] = groups[artist].sort((a, b) => a.title.localeCompare(b.title)); }); this.groupedAndSortedSongs = finalGroupedSongs; return { groupedSongs: finalGroupedSongs, message: viewSpecificMessage, hasContent: songsToProcess.length > 0 }; }, toggleFavorite: function(songId) { const song = this.songs.find(s => s.id === songId); const song = this.allSongs.find(s => s.id === songId); if (song) { song.isFavorite = !song.isFavorite; // Hier würdest du den API Call machen: // m.request({ // method: "PUT", // url: `https://example.com/api/songs/${song.id}/favorite`, // body: { isFavorite: song.isFavorite } // }).then(() => { // console.log(`Favoritenstatus für ${song.title} geändert.`); // }).catch(e => { // console.error("Fehler beim Ändern des Favoritenstatus:", e); // song.isFavorite = !song.isFavorite; // Rollback bei Fehler // m.redraw(); // UI aktualisieren // }); // Für dieses Frontend-Beispiel reicht das direkte Ändern und Neuzeichnen (Mithril macht das bei Event-Handlern meist automatisch) // Da `groupedAndSortedSongs` von `this.songs` abhängt, muss es nicht neu prozessiert werden, // solange die Reihenfolge/Gruppierung sich nicht ändert, nur das Icon. // Ein m.redraw() wird implizit durch den Klick-Handler ausgelöst. // API Call: m.request({ method: "PUT", url: `https://example.com/api/songs/${song.id}/favorite`, body: { isFavorite: song.isFavorite }}) // Mithril's automatisches Redraw nach dem Event-Handler aktualisiert die Ansicht. // Wenn die 'Favoriten'-Ansicht aktiv ist, wird die Liste korrekt neu gerendert. } }, view: function() { view: function(vnode) { const { currentView } = vnode.attrs; if (this.isLoading) { return m("div.song-list-outer-container", m("div.loading-placeholder", "Lade Songs...")); } const artists = Object.keys(this.groupedAndSortedSongs); const { groupedSongs, message, hasContent } = this.getProcessedSongsForView(currentView); const artists = Object.keys(groupedSongs); if (artists.length === 0) { return m("div.song-list-outer-container", m("p", "Keine Songs verfügbar.")); if (message && !hasContent) { // Zeige Nachricht nur, wenn keine Songs für die aktuelle Ansicht da sind return m("div.song-list-outer-container", m("p.view-message", message)); } return m("div.song-list-outer-container", artists.map(artist => m("div.artist-group", { key: artist }, [ m("div.artist-group", { key: `${currentView}-${artist}` }, [ // Key um Eindeutigkeit bei View-Wechsel zu erhöhen m("h2.artist-name", artist), this.groupedAndSortedSongs[artist].map(song => groupedSongs[artist].map(song => m(SongItem, { key: song.id, song: song, Loading
frontend/TabBar.mjs 0 → 100644 +25 −0 Original line number Diff line number Diff line // TabBar.mjs const TabBar = { view: function(vnode) { const { currentView, onViewChange } = vnode.attrs; const tabs = [ { id: 'all', label: 'Alle' }, { id: 'favorites', label: 'Favoriten' }, { id: 'popular', label: 'Beliebt' } // 'Beliebt' als Option ]; return m("div.tab-bar-container", tabs.map(tab => { return m("div.tab-item", { class: currentView === tab.id ? 'active' : '', onclick: () => onViewChange(tab.id), role: "button", // Für Barrierefreiheit tabindex: 0, // Für Tastaturnavigation onkeypress: (e) => { if (e.key === 'Enter' || e.key === ' ') onViewChange(tab.id); } }, tab.label); }) ); } }; export default TabBar; No newline at end of file
frontend/app.mjs +13 −1 Original line number Diff line number Diff line // app.mjs import SearchBar from './SearchBar.mjs'; import TabBar from './TabBar.mjs'; // Neu importiert import SongList from './SongList.mjs'; const AppLayout = { currentView: 'all', // Mögliche Werte: 'all', 'favorites', 'popular' onViewChange: function(view) { this.currentView = view; // m.redraw() wird von Mithril automatisch nach Event-Handlern aufgerufen }, view: function() { return m("div.app-layout", [ m(SearchBar), m(SongList) m(TabBar, { currentView: this.currentView, onViewChange: this.onViewChange.bind(this) // .bind(this) ist wichtig }), m(SongList, { currentView: this.currentView }) // Übergabe des aktuellen Views ]); } }; Loading
frontend/style.css +78 −26 Original line number Diff line number Diff line /* Globale Resets und Basis-Styling */ /* Globale Resets und Basis-Styling (wie vorher) */ body, html { margin: 0; padding: 0; font-family: 'Roboto', sans-serif; background-color: #f4f4f9; /* Heller Hintergrund für Kontrast */ background-color: #f4f4f9; color: #333; line-height: 1.6; } Loading @@ -13,14 +13,14 @@ body, html { margin: 0 auto; } .loading-placeholder { .loading-placeholder, .view-message { /* Kombiniert für Konsistenz */ text-align: center; padding: 50px; font-size: 1.2em; color: #777; padding: 40px 20px; font-size: 1.1em; color: #757575; } /* Suchleiste */ /* Suchleiste (Höhe ca. 60px) */ .search-bar-container { position: fixed; top: 0; Loading @@ -30,6 +30,8 @@ body, html { padding: 12px 16px; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.2); box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */ height: 60px; /* Feste Höhe für genaue Positionierung */ } .search-input { Loading @@ -47,27 +49,69 @@ body, html { color: #888; } /* Tab Bar (Höhe ca. 48px) */ .tab-bar-container { display: flex; position: fixed; top: 60px; /* Direkt unter der Suchleiste */ left: 0; right: 0; background-color: #3F51B5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */ z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */ box-shadow: 0 2px 4px rgba(0,0,0,0.15); /* Dezenter Schatten */ height: 48px; /* Feste Höhe */ box-sizing: border-box; } .tab-item { flex-grow: 1; display: flex; /* Für vertikales Zentrieren des Texts */ align-items: center; justify-content: center; padding: 0 8px; /* Horizontal Padding */ text-align: center; color: rgba(255,255,255,0.75); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */ cursor: pointer; font-weight: 500; font-size: 0.9em; /* Leicht kleiner als Haupttext */ text-transform: uppercase; border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */ transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; user-select: none; } .tab-item:hover { background-color: rgba(255,255,255,0.1); /* Leichter Hover-Effekt */ } .tab-item.active { color: #FFFFFF; /* Reines Weiß für aktiven Tab */ border-bottom-color: #FF4081; /* Material Accent Pink für den Indikator */ } /* Song-Liste Container */ .song-list-outer-container { padding-top: 60px; /* Höhe der Suchleiste + etwas Puffer */ /* padding-top: calc(60px + 48px + 10px); /* Höhe Suchleiste + Höhe TabBar + kleiner Abstand */ padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */ padding-bottom: 20px; } /* Restliche Styles für Artist-Group, Song-Item etc. bleiben wie zuvor */ .artist-group { margin-bottom: 12px; margin: 0 8px 12px 8px; /* Etwas seitlicher Margin */ background-color: #ffffff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; /* Für abgerundete Ecken bei Kindern */ overflow: hidden; } .artist-name { padding: 12px 16px; background-color: #E8EAF6; /* Indigo Lighten-5 */ color: #303F9F; /* Indigo Darken-2 */ background-color: #E8EAF6; color: #303F9F; font-size: 1.1em; font-weight: 500; border-bottom: 1px solid #C5CAE9; /* Indigo Lighten-4 */ border-bottom: 1px solid #C5CAE9; } .song-item { Loading @@ -89,28 +133,37 @@ body, html { .song-info { flex-grow: 1; margin-right: 8px; /* Abstand zum Icon */ overflow: hidden; /* Verhindert Textüberlauf */ } .song-title { font-size: 1em; font-weight: 400; color: #212121; /* Almost Black */ color: #212121; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* Text abschneiden bei Überlauf */ } .song-artist { font-size: 0.85em; color: #757575; /* Grey */ color: #757575; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .song-actions { margin-left: 16px; margin-left: auto; /* Pusht das Icon nach rechts, falls song-info nicht flex-grow hätte */ flex-shrink: 0; /* Verhindert, dass das Icon schrumpft */ } .heart-icon { cursor: pointer; color: #BDBDBD; /* Grey Lighten-1 (Outline) */ font-size: 24px; /* Standardgröße für Material Icons */ user-select: none; /* Verhindert Textauswahl beim Klicken */ color: #BDBDBD; font-size: 24px; user-select: none; transition: color 0.2s ease-in-out, transform 0.1s ease-out; vertical-align: middle; } Loading @@ -120,15 +173,14 @@ body, html { } .heart-icon.is-favorite { color: #FF4081; /* Pink A200 - Material Accent */ font-variation-settings: 'FILL' 1; /* Füllt das Icon */ color: #FF4081; font-variation-settings: 'FILL' 1; } /* Material Symbols Basis-Styling (optional, oft schon gut) */ .material-symbols-outlined { font-variation-settings: 'FILL' 0, /* 0 für Outline, 1 für Filled */ 'wght' 400, /* Schriftstärke */ 'GRAD' 0, /* Verlauf */ 'opsz' 24 /* Optische Größe */ 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; } No newline at end of file