menu's: gegroepeerde bovenbalk-dropdowns + icoon-tabbalk (inklapbaar)
This commit is contained in:
parent
170a036c4e
commit
168c71d2fb
6 changed files with 83 additions and 14 deletions
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" && (
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue