menu's: gegroepeerde bovenbalk-dropdowns + icoon-tabbalk (inklapbaar)

This commit is contained in:
bes-r 2026-06-18 22:21:46 +02:00
parent 170a036c4e
commit 168c71d2fb
6 changed files with 83 additions and 14 deletions

View file

@ -28,10 +28,10 @@ class CoreConfig(AppConfig):
core=True, core=True,
menu_items=( menu_items=(
# Hoofdniveau: Rooster = weergave, Roostermaker = aanpasser. # Hoofdniveau: Rooster = weergave, Roostermaker = aanpasser.
MenuItem("Rooster", "/rooster", icon="calendar", order=10), MenuItem("Rooster", "/rooster", icon="calendar", order=10, group="Rooster"),
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12), MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12, group="Rooster"),
MenuItem("Leerlingrooster", "/leerlingrooster", icon="book", order=14), MenuItem("Leerlingrooster", "/leerlingrooster", icon="book", order=14, group="Rooster"),
MenuItem("Ouderrooster", "/ouderrooster", icon="badge", order=16), MenuItem("Ouderrooster", "/ouderrooster", icon="badge", order=16, group="Rooster"),
# Submenu Organisatie: wie hoort waar. # Submenu Organisatie: wie hoort waar.
MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"), MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"),
MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"), MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"),

View file

@ -23,7 +23,7 @@ class StageConfig(AppConfig):
core=False, core=False,
default_enabled=False, default_enabled=False,
menu_items=( menu_items=(
MenuItem("Stage", "/stage", icon="pin", order=60, group="Groepen"), MenuItem("Stage", "/stage", icon="pin", order=60, group="Organisatie"),
), ),
) )
) )

View file

@ -24,7 +24,7 @@ class UrenConfig(AppConfig):
core=False, core=False,
default_enabled=False, default_enabled=False,
menu_items=( menu_items=(
MenuItem("Urenoverzicht", "/uren", icon="clock", order=30), MenuItem("Urenoverzicht", "/uren", icon="clock", order=30, group="Overzicht"),
), ),
) )
) )

View file

@ -16,6 +16,9 @@ const ICONS = {
away: "🌴", gear: "⚙️", door: "🚪", away: "🌴", gear: "⚙️", door: "🚪",
}; };
// Iconen voor de groep-dropdowns in de bovenbalk.
const GROEP_ICON = { Rooster: "calendar", Organisatie: "users", Overzicht: "clock", Hulpmiddelen: "puzzle", Toegang: "key" };
const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]); const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]);
const GROEPSINDELING_ITEM = { label: "Groepsindeling", path: "/groepsindeling", icon: "layers", order: 24, group: "Organisatie" }; const GROEPSINDELING_ITEM = { label: "Groepsindeling", path: "/groepsindeling", icon: "layers", order: 24, group: "Organisatie" };
const normaliseerPad = (path) => (GROEPSINDELING_PADEN.has(path) ? "/groepsindeling" : path); const normaliseerPad = (path) => (GROEPSINDELING_PADEN.has(path) ? "/groepsindeling" : path);
@ -174,6 +177,7 @@ function Shell({ user, onLogout }) {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [cogOpen, setCogOpen] = useState(false); const [cogOpen, setCogOpen] = useState(false);
const [openMenu, setOpenMenu] = useState(null);
async function load() { async function load() {
setLoading(true); setLoading(true);
@ -228,6 +232,24 @@ function Shell({ user, onLogout }) {
const activePad = normaliseerPad(active); const activePad = normaliseerPad(active);
const Page = PAGES[activePad]; const Page = PAGES[activePad];
// Groepeer de hoofdtabs: groepen met meerdere items worden een dropdown,
// groepen met één item en losse items blijven directe tabs.
const navEntries = (() => {
const directe = [];
const groepMap = new Map();
for (const it of tabs) {
if (it.group) { if (!groepMap.has(it.group)) groepMap.set(it.group, []); groepMap.get(it.group).push(it); }
else directe.push(it);
}
const entries = directe.map((it) => ({ type: "tab", order: it.order, it }));
for (const [groep, items] of groepMap) {
if (items.length === 1) entries.push({ type: "tab", order: items[0].order, it: items[0] });
else entries.push({ type: "group", order: Math.min(...items.map((i) => i.order)), groep, items });
}
entries.sort((a, b) => a.order - b.order);
return entries;
})();
function TabKnop({ it, compact }) { function TabKnop({ it, compact }) {
const pad = normaliseerPad(it.path); const pad = normaliseerPad(it.path);
return ( return (
@ -243,6 +265,37 @@ function Shell({ user, onLogout }) {
); );
} }
function NavDropdown({ groep, items }) {
const open = openMenu === groep;
const actief = items.some((i) => normaliseerPad(i.path) === activePad);
const icon = GROEP_ICON[groep] || items[0].icon;
return (
<div className={open ? "nav-menu open" : "nav-menu"} onMouseLeave={() => setOpenMenu(null)}>
<button
type="button"
className={actief ? "top-tab nav-trigger active" : "top-tab nav-trigger"}
onClick={() => setOpenMenu((m) => (m === groep ? null : groep))}
aria-haspopup="true"
aria-expanded={open}
>
<span className="ico">{ICONS[icon] || "•"}</span>
<span>{groep}</span>
<span className="caret"></span>
</button>
<div className="nav-dropdown" role="menu">
{items.map((it) => {
const pad = normaliseerPad(it.path);
return (
<button key={it.path} className={activePad === pad ? "cog-item active" : "cog-item"} role="menuitem" onClick={() => { setActive(pad); setOpenMenu(null); }}>
<span className="ico">{ICONS[it.icon] || "•"}</span><span>{it.label}</span>
</button>
);
})}
</div>
</div>
);
}
// Instellingen-cog: staat in de bovenbalk (buiten de scrollende tabbalk, // Instellingen-cog: staat in de bovenbalk (buiten de scrollende tabbalk,
// anders knipt overflow het uitklapmenu weg). // anders knipt overflow het uitklapmenu weg).
const cogMenu = instellingenTabs.length > 0 ? ( const cogMenu = instellingenTabs.length > 0 ? (
@ -290,7 +343,9 @@ function Shell({ user, onLogout }) {
</div> </div>
</div> </div>
<nav className="top-tabs" aria-label="Hoofdnavigatie"> <nav className="top-tabs" aria-label="Hoofdnavigatie">
{tabs.map((it) => <TabKnop key={it.path} it={it} />)} {navEntries.map((e) => e.type === "group"
? <NavDropdown key={"groep:" + e.groep} groep={e.groep} items={e.items} />
: <TabKnop key={e.it.path} it={e.it} />)}
</nav> </nav>
</header> </header>

View file

@ -2429,12 +2429,12 @@ export function RoosterScherm({ mode }) {
{edit && ( {edit && (
<div className="tabbar no-print"> <div className="tabbar no-print">
<button className={tab === "week" ? "tab on" : "tab"} title="Weekrooster" onClick={() => setTab("week")}> Week</button> <button className={tab === "week" ? "tab on" : "tab"} title="Weekrooster" onClick={() => setTab("week")}><span className="ico"></span><span className="tab-tekst">Week</span></button>
<button className={tab === "conflicten" ? "tab on" : "tab"} title="Conflicten" onClick={() => setTab("conflicten")}> Conflicten</button> <button className={tab === "conflicten" ? "tab on" : "tab"} title="Conflicten" onClick={() => setTab("conflicten")}><span className="ico"></span><span className="tab-tekst">Conflicten</span></button>
<button className={tab === "schooljaar" ? "tab on" : "tab"} title="Schooljaar" onClick={() => setTab("schooljaar")}> Jaar</button> <button className={tab === "schooljaar" ? "tab on" : "tab"} title="Schooljaar" onClick={() => setTab("schooljaar")}><span className="ico"></span><span className="tab-tekst">Jaar</span></button>
{blokwizardAan && <button className={tab === "wizard" ? "tab on" : "tab"} title="Vak-wizard" onClick={() => setTab("wizard")}> Wizard</button>} {blokwizardAan && <button className={tab === "wizard" ? "tab on" : "tab"} title="Vak-wizard" onClick={() => setTab("wizard")}><span className="ico"></span><span className="tab-tekst">Wizard</span></button>}
{autoroosterAan && <button className={tab === "auto" ? "tab on" : "tab"} title="Automatisch rooster" onClick={() => setTab("auto")}>🪄 Auto-rooster</button>} {autoroosterAan && <button className={tab === "auto" ? "tab on" : "tab"} title="Automatisch rooster" onClick={() => setTab("auto")}><span className="ico">🪄</span><span className="tab-tekst">Auto-rooster</span></button>}
<button className={tab === "instellingen" ? "tab on" : "tab"} title="Instellingen" onClick={() => setTab("instellingen")}> Instellingen</button> <button className={tab === "instellingen" ? "tab on" : "tab"} title="Instellingen" onClick={() => setTab("instellingen")}><span className="ico"></span><span className="tab-tekst">Instellingen</span></button>
</div> </div>
)} )}
{edit && tab === "week" && ( {edit && tab === "week" && (

View file

@ -22,7 +22,7 @@ body { margin: 0; font-family: system-ui, "Segoe UI", Roboto, sans-serif; color:
} }
.topbar-main { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 8px; } .topbar-main { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
.topbar-user { display: flex; align-items: center; gap: 10px; white-space: nowrap; } .topbar-user { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.top-tabs { display: flex; align-items: center; gap: 6px; overflow-x: auto; scrollbar-width: thin; } .top-tabs { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.top-tabs-settings { display: flex; align-items: center; gap: 6px; margin-left: auto; padding-left: 10px; border-left: 1px solid var(--line); } .top-tabs-settings { display: flex; align-items: center; gap: 6px; margin-left: auto; padding-left: 10px; border-left: 1px solid var(--line); }
/* Instellingen als cog-menu (popover via hover of klik) */ /* Instellingen als cog-menu (popover via hover of klik) */
@ -492,3 +492,17 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.voorstel-klas { margin-bottom: 18px; } .voorstel-klas { margin-bottom: 18px; }
.voorstel-klas h3 { margin: 8px 0 6px; } .voorstel-klas h3 { margin: 8px 0 6px; }
/* Groep-dropdowns in de bovenbalk */
.nav-menu { position: relative; }
.nav-trigger .caret { font-size: 10px; opacity: .65; margin-left: 1px; }
.nav-dropdown { display: none; position: absolute; left: 0; top: 100%; margin-top: 4px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.15); min-width: 210px; padding: 6px; z-index: 60; }
.nav-menu:hover .nav-dropdown, .nav-menu.open .nav-dropdown { display: block; }
/* In-pagina tabbalk: icoon altijd, label inklapbaar op smalle schermen */
.tabbar .tab .ico { font-size: 15px; }
.tabbar .tab .tab-tekst { margin-left: 6px; }
@media (max-width: 720px) {
.tabbar .tab .tab-tekst { display: none; }
.tabbar .tab { padding-left: 12px; padding-right: 12px; }
}