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 (
-
-
-
-
- {items.map((item) => {
- const Icon = item.icon
- const active = value === item.id
- return (
- onChange(item.id)}
- >
- {Icon ? (
-
- ) : null}
- {item.label}
-
- )
- })}
-
-
-
{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 (
+ {
+ if (!disabled) onChange(item.id)
+ }}
+ >
+ {Icon ? (
+
+ ) : null}
+ {item.label}
+
+ )
+ })}
+
+ )
+}
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) => (
- setActiveTab(tab.id)}
- className={
- 'admin-page-subtabs__btn' +
- (activeTab === tab.id ? ' admin-page-subtabs__btn--active' : '')
- }
- >
- {tab.label}
-
- ))}
-
+
{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() {
-
- setTab('catalog')}
- >
- Katalog und Hierarchie
-
- setTab('models')}
- >
- Reifegradmodelle
-
- setTab('bindings')}
- >
- Kontext-Zuordnung
-
- setTab('matrixviz')}
- >
- Matrix-Ansicht und Export
-
-
+
{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) => (
- setActiveTab(tab)}
- >
- {tab === 'clubs' && 'Vereine'}
- {tab === 'divisions' && 'Sparten'}
- {tab === 'groups' && 'Trainingsgruppen'}
- {tab === 'members' && 'Mitglieder'}
-
- ))}
-
+
- {/* 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() {
)}
-
- setPageTab('list')}
- >
- Liste
-
- setPageTab('progression')}
- >
- Progressionsgraphen
-
-
+
{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 => (
- setActiveTab(tab)}
- style={{
- padding: '12px 24px',
- background: activeTab === tab ? 'var(--accent)' : 'transparent',
- color: activeTab === tab ? 'white' : 'var(--text1)',
- border: 'none',
- borderBottom: activeTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
- cursor: 'pointer',
- fontSize: '16px',
- fontWeight: activeTab === tab ? 'bold' : 'normal',
- transition: 'all 0.2s'
- }}
- >
- {tab === 'preview' && '👁️ Vorschau'}
- {tab === 'execute' && '▶️ Ausführen'}
- {tab === 'history' && '📜 Historie'}
-
- ))}
-
-
+
{/* 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
-
- setActiveTab('skills')}
- >
- Fähigkeiten
-
- setActiveTab('methods')}
- >
- Trainingsmethoden
-
-
+
{/* 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() {
-
-
- {[
+
+
(
- setFrameworkTab(t.id)}
- >
- {t.label}
-
- ))}
-
+ ]}
+ className="page-section-nav--embedded framework-edit__section-nav"
+ />
Ansicht
-
-
setPlanView('list')}
- className={
- 'planning-segment-group__btn' +
- (planView === 'list' ? ' planning-segment-group__btn--active' : '')
- }
- >
- Liste
-
-
{
+ {
+ if (id === 'calendar') {
setPlanView('calendar')
setCalendarMonthStr((prev) => {
const fromList = (startDate || '').slice(0, 7)
if (/^\d{4}-\d{2}$/.test(fromList)) return fromList
return prev || new Date().toISOString().slice(0, 7)
})
- }}
- className={
- 'planning-segment-group__btn' +
- (planView === 'calendar' ? ' planning-segment-group__btn--active' : '')
+ } else {
+ setPlanView('list')
}
- >
- Kalender
-
-
+ }}
+ 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
-
- setPlanScope('group')}
- >
- Nur diese Gruppe
-
- setPlanScope('club')}
- >
- Ganzer Verein
-
-
+