From db8af53652d0d16a37ac8168ac42aed4ec752b2b Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 6 May 2026 12:49:35 +0200 Subject: [PATCH] refactor: update navigation components and styles for improved consistency - Replaced legacy .capture-shell with .app-subnav-shell and integrated PageSectionNav for a unified navigation experience across multiple pages. - Refactored AdminCatalogsPage, AdminMaturityModelsPage, ClubsPage, ExercisesListPage, MediaWikiImportPage, SkillsPage, and TrainingFrameworkProgramEditPage to utilize the new PageSectionNav component for tab navigation. - Enhanced CSS styles for better responsiveness and visual clarity in navigation elements. - Improved accessibility features with appropriate ARIA roles and attributes for better usability. --- frontend/src/app.css | 215 ++++-------------- frontend/src/components/AppSubnavShell.jsx | 52 ++--- frontend/src/components/PageSectionNav.jsx | 50 ++++ frontend/src/pages/AdminCatalogsPage.jsx | 46 ++-- .../src/pages/AdminMaturityModelsPage.jsx | 60 ++--- frontend/src/pages/ClubsPage.jsx | 47 ++-- frontend/src/pages/ExercisesListPage.jsx | 37 +-- frontend/src/pages/MediaWikiImportPage.jsx | 40 ++-- frontend/src/pages/SkillsPage.jsx | 37 ++- .../TrainingFrameworkProgramEditPage.jsx | 29 +-- frontend/src/pages/TrainingPlanningPage.jsx | 81 +++---- 11 files changed, 243 insertions(+), 451 deletions(-) create mode 100644 frontend/src/components/PageSectionNav.jsx diff --git a/frontend/src/app.css b/frontend/src/app.css index 9946aa7..de4db83 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -980,132 +980,15 @@ a.analysis-split__nav-item { padding: 8px 10px; } -/* Erfassung: Sub-Navigation — oben Chip-Zeile (alle Viewports); wie admin-page-subtabs */ -.capture-shell { +/* Legacy .capture-shell entfällt — Sektionsnav: PageSectionNav → .admin-page-subtabs */ +.app-subnav-shell { width: 100%; -} - -.capture-shell__layout { display: flex; flex-direction: column; gap: 16px; - align-items: stretch; } - -.capture-shell__nav-wrap { - width: 100%; +.app-subnav-shell__main { min-width: 0; - position: sticky; - top: var(--header-h); - z-index: 4; - background: var(--bg); - padding-bottom: 4px; - margin-bottom: 0; - border-bottom: 1px solid var(--border); -} - -.capture-shell__nav { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - gap: 6px; - overflow-x: auto; - padding-bottom: 6px; - -ms-overflow-style: none; - scrollbar-width: none; -} - -.capture-shell__nav::-webkit-scrollbar { - display: none; -} - -.capture-shell__nav-item { - flex-shrink: 0; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 12px; - border-radius: 20px; - border: 1.5px solid var(--border2); - background: var(--surface); - color: var(--text2); - font-family: var(--font); - font-size: 13px; - font-weight: 500; - text-decoration: none; - white-space: nowrap; - cursor: pointer; - box-sizing: border-box; -} - -.capture-shell__nav-item:hover { - border-color: var(--accent); - color: var(--text1); -} - -.capture-shell__nav-item--active { - border-color: var(--accent); - background: var(--accent); - color: white; -} - -.capture-shell__nav-item--active:hover { - color: white; -} - -.capture-shell__nav-item--highlight:not(.capture-shell__nav-item--active) { - border-color: #7f77dd88; - background: #7f77dd14; -} - -.capture-shell__nav-icon { - font-size: 15px; - line-height: 1; -} - -.capture-shell__nav-label { - line-height: 1.2; -} - -.capture-shell__main { - min-width: 0; - flex: 1; -} - -@media (min-width: 1024px) { - .capture-shell__layout { - gap: 20px; - } - - .capture-shell__nav-wrap { - top: var(--header-h); - padding-top: 2px; - padding-bottom: 10px; - } - - .capture-shell__nav { - flex-wrap: wrap; - overflow-x: visible; - padding-bottom: 0; - gap: 8px; - } - - .capture-shell__nav-item { - padding: 9px 14px; - border-radius: 999px; - font-size: 13px; - font-weight: 600; - } -} - -button.capture-shell__nav-item { - font-family: inherit; - text-align: left; - -webkit-tap-highlight-color: transparent; -} -.capture-shell__nav-item svg.capture-shell__nav-icon { - flex-shrink: 0; } /* Einstellungen: gleiche Split-Struktur wie Analyse/Admin */ @@ -1262,46 +1145,6 @@ button.capture-shell__nav-item { border-radius: 7px; } - /* Capture-Shell / AppSubnavShell: kompakte Chips + Scroll-Snap */ - .capture-shell__layout { - gap: 10px; - } - - .capture-shell__nav-wrap { - width: 100%; - max-width: none; - margin-left: calc(-1 * max(12px, env(safe-area-inset-left, 0px))); - margin-right: calc(-1 * max(12px, env(safe-area-inset-right, 0px))); - padding-left: max(12px, env(safe-area-inset-left, 0px)); - padding-right: max(12px, env(safe-area-inset-right, 0px)); - box-sizing: border-box; - } - - .capture-shell__nav { - gap: 5px; - padding-bottom: 4px; - scroll-snap-type: x proximity; - scroll-padding-inline: max(12px, env(safe-area-inset-left, 0px)); - } - - .capture-shell__nav-item { - padding: 6px 11px; - min-height: 36px; - font-size: 12px; - font-weight: 600; - gap: 5px; - border-radius: 999px; - border-width: 1px; - scroll-snap-align: start; - box-sizing: border-box; - } - - .capture-shell__nav-item svg { - width: 15px !important; - height: 15px !important; - } - - /* Admin-Seitenleiste oben: dieselbe Chip-Idee wie Subnav */ .admin-top-nav { flex-wrap: nowrap; overflow-x: auto; @@ -1391,7 +1234,7 @@ button.capture-shell__nav-item { gap: 6px; } - .framework-edit__tabbar .planning-segment-group__btn { + .framework-edit__tabbar .admin-page-subtabs__btn { padding: 6px 8px; font-size: 11px; line-height: 1.25; @@ -1518,12 +1361,11 @@ button.capture-shell__nav-item { /* ---------- Navigations-Ebenen (Kurzreferenz) ---------- * Hauptbereiche: Bottom-Nav / App-Header. - * Sektionsumschalter auf einer Seite (2–n Einträge): horizontale Chip-Zeile oben — - * • AppSubnavShell → .capture-shell (sticky unter dem Header, Wrap auf großen Screens) - * • viele flache Tabs (z. B. Stammdaten) → .admin-page-subtabs (gleiche Chip-Idee, Edge-Scroll mobil) + * Sektionsumschalter auf einer Seite: PageSectionNav → .admin-page-subtabs (Chips, mobil Edge-Scroll). + * AppSubnavShell = PageSectionNav + Inhalt (z. B. Hierarchie-Admin). * Wechsel zwischen Admin-Seiten → .admin-top-nav - * „Sub-Sub“ (dritte Ebene, z. B. Editor-Spalten): bewusst in jeweiligen Feature-Layouts (Seitenleiste / Panel). - * Karten-Raster: .card-grid oder Klassen mit *list-grid* / *slots-board* / .dashboard-training-grid — dort kein .card+.card-Abstand (nur gap). + * „Sub-Sub“ (dritte Ebene): Feature-Layouts (Rahmen-Editor, Slots). + * Karten-Raster: .card-grid / *list-grid* / *slots-board* — nur gap, kein .card+.card. * ---------- */ /* Admin-Kataloge: Seite „Stammdaten“ — viele Unter-Tabs, Chip-Scroll */ @@ -1559,6 +1401,19 @@ button.capture-shell__nav-item { color: var(--text2); transition: background 0.12s, color 0.12s, border-color 0.12s; -webkit-tap-highlight-color: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; +} +.admin-page-subtabs__btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.admin-page-subtabs__btn:disabled:hover { + border-color: var(--border2); + color: var(--text2); + background: var(--surface2); } .admin-page-subtabs__btn:hover { border-color: var(--accent); @@ -1578,6 +1433,31 @@ button.capture-shell__nav-item { font-size: 13px; padding: 9px 15px; } + .page-section-nav--wrap.admin-page-subtabs { + flex-wrap: wrap; + } +} + +.page-section-nav__icon { + flex-shrink: 0; +} +/* Eingebettet in z. B. framework-edit__tabbar — keine zweite Unterlinie */ +.page-section-nav--embedded.admin-page-subtabs { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + flex: 1; + min-width: 0; +} +/* Inline neben Labeln (Planung: Ansicht / Einblenden) */ +.page-section-nav--inline.admin-page-subtabs { + display: inline-flex; + width: auto; + max-width: 100%; + margin-bottom: 0; + flex: 0 1 auto; + border-bottom: none; + padding-bottom: 0; } /* Admin Hierarchy & Catalog Section (Komponenten) */ @@ -4145,7 +4025,8 @@ button.capture-shell__nav-item { .framework-edit__tabbar::-webkit-scrollbar { display: none; } -.framework-edit__tabbar .planning-segment-group { +.framework-edit__tabbar .admin-page-subtabs, +.framework-edit__tabbar .page-section-nav { flex: 1; min-width: 0; } diff --git a/frontend/src/components/AppSubnavShell.jsx b/frontend/src/components/AppSubnavShell.jsx index 637625d..6fef51e 100644 --- a/frontend/src/components/AppSubnavShell.jsx +++ b/frontend/src/components/AppSubnavShell.jsx @@ -1,9 +1,8 @@ +import PageSectionNav from './PageSectionNav' + /** - * Sub-Navigation (einheitlich mit Admin-Sektionsschaltern): - * — Mobil & Desktop: eine horizontale Chip-/Registerzeile oben (scroll auf schmalen Viewports). - * — Eine darunter liegende „Sub-Sub“-Ebene (z. B. Rahmen bearbeiten) bleibt bewusst - * seitlich / in eigenen Layout-Komponenten (z. B. Trainingseinheit, Framework-Editor). - * Nutzt .capture-shell* aus app.css. + * Sub-Navigation mit Icon-Chips: gleiche Darstellung wie Stammdaten / Vereine (PageSectionNav). + * „Sub-Sub“ (z. B. Editor) bleibt in den jeweiligen Feature-Layouts. */ export default function AppSubnavShell({ ariaLabel, @@ -14,39 +13,16 @@ export default function AppSubnavShell({ iconSize = 18, }) { return ( -
-
-
- -
-
{children}
-
+
+ +
{children}
) } diff --git a/frontend/src/components/PageSectionNav.jsx b/frontend/src/components/PageSectionNav.jsx new file mode 100644 index 0000000..8c95513 --- /dev/null +++ b/frontend/src/components/PageSectionNav.jsx @@ -0,0 +1,50 @@ +/** + * Einheitliche Sektions-Navigation: Chip-Zeile wie Admin-Stammdaten (.admin-page-subtabs). + * Für „Tabs“ (role=tablist) oder kompakte Umschalter (aria-pressed, role=group). + */ +export default function PageSectionNav({ + ariaLabel, + value, + onChange, + items, + className = '', + iconSize = 16, + semantics = 'tabs', +}) { + const isToggle = semantics === 'toggle' + return ( +
+ {items.map((item) => { + const Icon = item.icon + const active = value === item.id + const disabled = Boolean(item.disabled) + return ( + + ) + })} +
+ ) +} diff --git a/frontend/src/pages/AdminCatalogsPage.jsx b/frontend/src/pages/AdminCatalogsPage.jsx index 51d70ad..5ee10ef 100644 --- a/frontend/src/pages/AdminCatalogsPage.jsx +++ b/frontend/src/pages/AdminCatalogsPage.jsx @@ -1,6 +1,19 @@ import { useState, useEffect } from 'react' import { api } from '../utils/api' import AdminPageNav from '../components/AdminPageNav' +import PageSectionNav from '../components/PageSectionNav' + +const CATALOG_SUBTABS = [ + { id: 'focus-areas', label: 'Fokusbereiche' }, + { id: 'training-styles', label: 'Stilrichtungen' }, + { id: 'training-types', label: 'Trainingsstil' }, + { id: 'hierarchy', label: 'Hierarchie' }, + { id: 'target-groups', label: 'Zielgruppen' }, + { id: 'target-groups-matrix', label: 'Zuordnungen' }, + { id: 'training-characters', label: 'Trainingscharakter' }, + { id: 'skill-categories', label: 'Fähigkeitskategorien' }, + { id: 'trainer-assignments', label: 'Trainer-Zuordnungen' }, +] export default function AdminCatalogsPage() { const [activeTab, setActiveTab] = useState('focus-areas') @@ -318,33 +331,12 @@ export default function AdminCatalogsPage() {

Stammdaten-Kataloge

-
- {[ - { id: 'focus-areas', label: 'Fokusbereiche' }, - { id: 'training-styles', label: 'Stilrichtungen' }, - { id: 'training-types', label: 'Trainingsstil' }, - { id: 'hierarchy', label: 'Hierarchie' }, - { id: 'target-groups', label: 'Zielgruppen' }, - { id: 'target-groups-matrix', label: 'Zuordnungen' }, - { id: 'training-characters', label: 'Trainingscharakter' }, - { id: 'skill-categories', label: 'Fähigkeitskategorien' }, - { id: 'trainer-assignments', label: 'Trainer-Zuordnungen' } - ].map((tab) => ( - - ))} -
+ {error &&
{error}
} diff --git a/frontend/src/pages/AdminMaturityModelsPage.jsx b/frontend/src/pages/AdminMaturityModelsPage.jsx index 109a29f..357b566 100644 --- a/frontend/src/pages/AdminMaturityModelsPage.jsx +++ b/frontend/src/pages/AdminMaturityModelsPage.jsx @@ -6,6 +6,14 @@ import SkillsCatalogAdmin from '../components/admin/SkillsCatalogAdmin' import MaturityModelsAdminPanel from '../components/admin/MaturityModelsAdminPanel' import MaturityModelBindingsAdmin from '../components/admin/MaturityModelBindingsAdmin' import MaturityMatrixToolsAdmin from '../components/admin/MaturityMatrixToolsAdmin' +import PageSectionNav from '../components/PageSectionNav' + +const MATURITY_SECTION_TABS = [ + { id: 'catalog', label: 'Katalog und Hierarchie' }, + { id: 'models', label: 'Reifegradmodelle' }, + { id: 'bindings', label: 'Kontext-Zuordnung' }, + { id: 'matrixviz', label: 'Matrix-Ansicht und Export' }, +] export default function AdminMaturityModelsPage() { const { user } = useAuth() @@ -27,52 +35,12 @@ export default function AdminMaturityModelsPage() {

-
- - - - -
+
{tab === 'catalog' ? ( diff --git a/frontend/src/pages/ClubsPage.jsx b/frontend/src/pages/ClubsPage.jsx index 153dce7..64347f2 100644 --- a/frontend/src/pages/ClubsPage.jsx +++ b/frontend/src/pages/ClubsPage.jsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import api from '../utils/api' import { useAuth } from '../context/AuthContext' +import PageSectionNav from '../components/PageSectionNav' const CLUB_ROLE_OPTIONS = [ { code: 'club_admin', label: 'Vereinsadmin' }, @@ -285,6 +286,19 @@ function ClubsPage() { setFormData(prev => ({ ...prev, [field]: value })) } + const clubTabItems = useMemo(() => { + const ids = canManageOrgSomewhere + ? ['clubs', 'divisions', 'groups', 'members'] + : ['clubs', 'divisions', 'groups'] + const labels = { + clubs: 'Vereine', + divisions: 'Sparten', + groups: 'Trainingsgruppen', + members: 'Mitglieder', + } + return ids.map((id) => ({ id, label: labels[id] })) + }, [canManageOrgSomewhere]) + if (loading) { return (
@@ -294,10 +308,6 @@ function ClubsPage() { ) } - const clubTabIds = canManageOrgSomewhere - ? ['clubs', 'divisions', 'groups', 'members'] - : ['clubs', 'divisions', 'groups'] - return (

Vereinsverwaltung

@@ -306,27 +316,14 @@ function ClubsPage() { Sparten sind optional — typische Eckdaten einer Gruppe (Wochentag, Zeit, Ort) kannst du schrittweise eintragen.

-
- {clubTabIds.map((tab) => ( - - ))} -
+ - {/* Clubs Tab */} + {/* Clubs Tab */} {activeTab === 'clubs' && ( <>
diff --git a/frontend/src/pages/ExercisesListPage.jsx b/frontend/src/pages/ExercisesListPage.jsx index 807b04d..e092c46 100644 --- a/frontend/src/pages/ExercisesListPage.jsx +++ b/frontend/src/pages/ExercisesListPage.jsx @@ -17,10 +17,15 @@ import { useAuth } from '../context/AuthContext' import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels' import MultiSelectCombo from '../components/MultiSelectCombo' import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel' +import PageSectionNav from '../components/PageSectionNav' import { sanitizeExerciseRichText, coerceApiNameList } from '../utils/sanitizeHtml' const PAGE_SIZE = 100 const BULK_MAX_IDS = 500 +const EXERCISES_PAGE_TABS = [ + { id: 'list', label: 'Liste' }, + { id: 'progression', label: 'Progressionsgraphen' }, +] const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null) const INITIAL_FILTERS = { @@ -646,31 +651,13 @@ function ExercisesListPage() { )}
-
- - -
+ {pageTab === 'progression' ? ( diff --git a/frontend/src/pages/MediaWikiImportPage.jsx b/frontend/src/pages/MediaWikiImportPage.jsx index 2be6dd5..8b91937 100644 --- a/frontend/src/pages/MediaWikiImportPage.jsx +++ b/frontend/src/pages/MediaWikiImportPage.jsx @@ -1,6 +1,14 @@ import React, { useState, useEffect } from 'react' +import { Eye, Play, History } from 'lucide-react' import api from '../utils/api' import AdminPageNav from '../components/AdminPageNav' +import PageSectionNav from '../components/PageSectionNav' + +const WIKI_IMPORT_TABS = [ + { id: 'preview', label: 'Vorschau', icon: Eye }, + { id: 'execute', label: 'Ausführen', icon: Play }, + { id: 'history', label: 'Historie', icon: History }, +] export default function MediaWikiImportPage() { const [activeTab, setActiveTab] = useState('preview') @@ -111,32 +119,12 @@ export default function MediaWikiImportPage() { Importiere Übungen, Fähigkeiten und Methoden aus karatetrainer.net

- {/* Tabs */} -
-
- {['preview', 'execute', 'history'].map(tab => ( - - ))} -
-
+ {/* Error Display */} {error && ( diff --git a/frontend/src/pages/SkillsPage.jsx b/frontend/src/pages/SkillsPage.jsx index 626c5b0..0de662e 100644 --- a/frontend/src/pages/SkillsPage.jsx +++ b/frontend/src/pages/SkillsPage.jsx @@ -1,6 +1,12 @@ import React, { useState, useEffect } from 'react' import api from '../utils/api' import { useAuth } from '../context/AuthContext' +import PageSectionNav from '../components/PageSectionNav' + +const SKILLS_SECTION_TABS = [ + { id: 'skills', label: 'Fähigkeiten' }, + { id: 'methods', label: 'Trainingsmethoden' }, +] function SkillsPage() { const { user } = useAuth() @@ -146,30 +152,13 @@ function SkillsPage() {

Fähigkeiten & Methoden

-
- - -
+ {/* Skills Tab */} {activeTab === 'skills' && ( diff --git a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx index 8d7cb57..05879ff 100644 --- a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx +++ b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx @@ -4,6 +4,7 @@ import api from '../utils/api' import ExercisePickerModal from '../components/ExercisePickerModal' import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' +import PageSectionNav from '../components/PageSectionNav' import { defaultSection, normalizeUnitToForm, @@ -675,27 +676,17 @@ export default function TrainingFrameworkProgramEditPage() {
-
-
- {[ +
+ ( - - ))} -
+ ]} + className="page-section-nav--embedded framework-edit__section-nav" + />
Ansicht -
- - -
+ }} + items={[ + { id: 'list', label: 'Liste' }, + { id: 'calendar', label: 'Kalender' }, + ]} + className="page-section-nav--inline planning-ansicht-nav" + /> {planView === 'list' ? 'Zeitraum unten mit Von/Bis filtern.' @@ -1047,32 +1035,17 @@ function TrainingPlanningPage() { Einblenden -
- - -
+