Hernoem Leerplein naar Bovenliggende groepen; route /groepsindeling; categorie Groepsindeling

This commit is contained in:
bes-r 2026-06-15 22:52:46 +02:00
parent fa50438d35
commit c49531f692
8 changed files with 61 additions and 49 deletions

View file

@ -72,7 +72,7 @@ CORE_APPS = [
MODULE_APPS = [ MODULE_APPS = [
"modules.printing", # Printen/Exporteren "modules.printing", # Printen/Exporteren
"modules.accounts", # Multi-user & toegang "modules.accounts", # Multi-user & toegang
"modules.leerplein", # Leerplein (groepsstructuur) "modules.leerplein", # Bovenliggende groepen
"modules.stage", # Stage (intern/extern) "modules.stage", # Stage (intern/extern)
] ]

View file

@ -32,7 +32,7 @@ class CoreConfig(AppConfig):
MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12), MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12),
# 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", "/structuur", icon="layers", order=24, group="Organisatie"), MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"),
MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"), MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"),
), ),
) )

View file

@ -0,0 +1,19 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("module_leerplein", "0002_leerplein_medewerkers"),
]
operations = [
migrations.AlterModelOptions(
name="leerplein",
options={
"ordering": ["naam"],
"verbose_name": "bovenliggende groep",
"verbose_name_plural": "bovenliggende groepen",
},
),
]

View file

@ -1,8 +1,8 @@
""" """
Leerplein-module: een overkoepelende groepsstructuur. Module voor bovenliggende groepen.
- Een Leerplein bundelt meerdere groepen en/of subgroepen, en kan daarnaast - Een bovenliggende groep bundelt meerdere groepen en/of subgroepen, en kan
losse (individuele) leerlingen bevatten. daarnaast losse (individuele) leerlingen bevatten.
- Een SubgroepGroep-koppeling verbindt een subgroep met een groep. - Een SubgroepGroep-koppeling verbindt een subgroep met een groep.
De module raakt de kernmodellen niet aan; het zijn losse koppeltabellen die De module raakt de kernmodellen niet aan; het zijn losse koppeltabellen die
@ -33,8 +33,8 @@ class Leerplein(models.Model):
aangemaakt_op = models.DateTimeField(auto_now_add=True) aangemaakt_op = models.DateTimeField(auto_now_add=True)
class Meta: class Meta:
verbose_name = "leerplein" verbose_name = "bovenliggende groep"
verbose_name_plural = "leerpleinen" verbose_name_plural = "bovenliggende groepen"
ordering = ["naam"] ordering = ["naam"]
def __str__(self) -> str: def __str__(self) -> str:

View file

@ -19,7 +19,7 @@ class StageConfig(AppConfig):
"krijgen een begeleider; een interne stage ook een ruimte." "krijgen een begeleider; een interne stage ook een ruimte."
), ),
version="0.1.0", version="0.1.0",
category="Structuur", category="Groepsindeling",
core=False, core=False,
default_enabled=False, default_enabled=False,
menu_items=( menu_items=(

View file

@ -5,7 +5,7 @@ import {
PersonenPage, FunctiesPage, PersonenPage, FunctiesPage,
ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage,
RoosterWeergavePage, RoostermakerPage, RoosterWeergavePage, RoostermakerPage,
AfwezigheidPage, LeerpleinPage, StagePage, GebruikersPage, AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage,
} from "./pages.jsx"; } from "./pages.jsx";
const ICONS = { const ICONS = {
@ -14,16 +14,16 @@ const ICONS = {
away: "🌴", gear: "⚙️", away: "🌴", gear: "⚙️",
}; };
const STRUCTUUR_PADEN = new Set(["/structuur", "/leerplein", "/groepen", "/subgroepen"]); const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]);
const STRUCTUUR_ITEM = { label: "Groepsindeling", path: "/structuur", icon: "layers", order: 24, group: "Organisatie" }; const GROEPSINDELING_ITEM = { label: "Groepsindeling", path: "/groepsindeling", icon: "layers", order: 24, group: "Organisatie" };
const normaliseerPad = (path) => (STRUCTUUR_PADEN.has(path) ? "/structuur" : path); const normaliseerPad = (path) => (GROEPSINDELING_PADEN.has(path) ? "/groepsindeling" : path);
// Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een // Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een
// placeholder (worden in een latere fase ingevuld). // placeholder (worden in een latere fase ingevuld).
const PAGES = { const PAGES = {
"/rooster": RoosterWeergavePage, "/rooster": RoosterWeergavePage,
"/roostermaker": RoostermakerPage, "/roostermaker": RoostermakerPage,
"/structuur": LeerpleinPage, "/groepsindeling": GroepsindelingPage,
"/personen": PersonenPage, "/personen": PersonenPage,
"/functies": FunctiesPage, "/functies": FunctiesPage,
"/activiteiten": ActiviteitenPage, "/activiteiten": ActiviteitenPage,
@ -137,13 +137,13 @@ function Shell({ user, onLogout }) {
} }
// Bouw de navigatie op uit de menu-items die ACTIEVE modules aanbieden. // Bouw de navigatie op uit de menu-items die ACTIEVE modules aanbieden.
// Verouderde structuurpaden blijven werken, maar verschijnen als één tab. // Verouderde groepspaden blijven werken, maar verschijnen als één tab.
const { tabs, instellingenTabs, alleItems } = useMemo(() => { const { tabs, instellingenTabs, alleItems } = useMemo(() => {
const perPad = new Map(); const perPad = new Map();
const voegToe = (raw) => { const voegToe = (raw) => {
const pad = normaliseerPad(raw.path); const pad = normaliseerPad(raw.path);
const item = pad === "/structuur" const item = pad === "/groepsindeling"
? { ...raw, ...STRUCTUUR_ITEM, module: raw.module } ? { ...raw, ...GROEPSINDELING_ITEM, module: raw.module }
: { ...raw, path: pad }; : { ...raw, path: pad };
const huidig = perPad.get(item.path); const huidig = perPad.get(item.path);
if (!huidig || item.order < huidig.order) perPad.set(item.path, item); if (!huidig || item.order < huidig.order) perPad.set(item.path, item);

View file

@ -199,7 +199,7 @@ export function LocatiesPage() {
/* --------------------------------- Groepen -------------------------------- */ /* --------------------------------- Groepen -------------------------------- */
export function GroepenPage() { export function GroepenPage() {
return <LeerpleinPage />; return <GroepsindelingPage />;
} }
/* -------------------------------- Personen -------------------------------- */ /* -------------------------------- Personen -------------------------------- */
@ -241,7 +241,7 @@ export function PersonenPage() {
} }
useEffect(() => { laadHulplijsten(); }, []); useEffect(() => { laadHulplijsten(); }, []);
// Leerplein-koppeling loopt via de leerplein-API (de module bezit die relatie). // De bovenliggende-groep-koppeling loopt via de bestaande API.
async function syncLeerpleinen(persoonId, gekozenIds) { async function syncLeerpleinen(persoonId, gekozenIds) {
const huidige = await listResource("leerpleinen").catch(() => []); const huidige = await listResource("leerpleinen").catch(() => []);
for (const lp of huidige) { for (const lp of huidige) {
@ -446,7 +446,7 @@ export function PersonenPage() {
/* ------------------------------- Subgroepen ------------------------------- */ /* ------------------------------- Subgroepen ------------------------------- */
export function SubgroepenPage() { export function SubgroepenPage() {
return <LeerpleinPage />; return <GroepsindelingPage />;
} }
/* ------------------------------- Tijdsloten ------------------------------- */ /* ------------------------------- Tijdsloten ------------------------------- */
@ -703,7 +703,7 @@ export function AfwezigheidPage() {
/* ----------------------------- Groepsindeling ----------------------------- */ /* ----------------------------- Groepsindeling ----------------------------- */
// Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep. // Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep.
export function LeerpleinPage() { export function GroepsindelingPage() {
const [leerpleinen, setLeerpleinen] = useState([]); const [leerpleinen, setLeerpleinen] = useState([]);
const [groepen, setGroepen] = useState([]); const [groepen, setGroepen] = useState([]);
const [subgroepen, setSubgroepen] = useState([]); const [subgroepen, setSubgroepen] = useState([]);
@ -891,7 +891,7 @@ export function LeerpleinPage() {
<p className="muted">Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.</p> <p className="muted">Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.</p>
<Foutmelding msg={error} /> <Foutmelding msg={error} />
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}> <form className="form-card indeling-form" onSubmit={opslaanLeerplein}>
<h2>{BOVENGROEP_LABEL}</h2> <h2>{BOVENGROEP_LABEL}</h2>
<div className="form-row"> <div className="form-row">
<input placeholder={`Naam ${BOVENGROEP_LABEL_LC}`} value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} /> <input placeholder={`Naam ${BOVENGROEP_LABEL_LC}`} value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
@ -911,7 +911,7 @@ export function LeerpleinPage() {
</div> </div>
</form> </form>
<form className="form-card structuur-form" onSubmit={opslaanGroep}> <form className="form-card indeling-form" onSubmit={opslaanGroep}>
<h2>Groep</h2> <h2>Groep</h2>
<div className="form-row"> <div className="form-row">
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}> <select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
@ -937,7 +937,7 @@ export function LeerpleinPage() {
</div> </div>
</form> </form>
<form className="form-card structuur-form" onSubmit={opslaanSubgroep}> <form className="form-card indeling-form" onSubmit={opslaanSubgroep}>
<h2>Subgroep</h2> <h2>Subgroep</h2>
<div className="form-row"> <div className="form-row">
<select value={subgroepForm.groep} required onChange={(e) => { <select value={subgroepForm.groep} required onChange={(e) => {
@ -980,7 +980,7 @@ export function LeerpleinPage() {
</form> </form>
{loading ? <p>Laden</p> : ( {loading ? <p>Laden</p> : (
<div className="structuur-tree"> <div className="indeling-tree">
{leerpleinen.map((lp) => { {leerpleinen.map((lp) => {
const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id)); const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id));
const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep)); const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep));
@ -1942,13 +1942,13 @@ export function RoosterScherm({ mode }) {
</div> </div>
<span className="split-count">{kolommen.length}</span> <span className="split-count">{kolommen.length}</span>
</div> </div>
<div className="split-kolommen"> <div className="split-kolommen split-vertakking">
{kolommen.map(([gid, groepBlokken]) => { {kolommen.map(([gid, groepBlokken]) => {
const groep = groepen.find((g) => g.id === gid); const groep = groepen.find((g) => g.id === gid);
return ( return (
<div className="split-kolom" key={gid}> <div className="split-kolom" key={gid}>
<div className="split-kop">{groep ? groep.naam : "Groep"}</div> <div className="split-kop split-node-kop">{groep ? groep.naam : "Groep"}</div>
{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))} <div className="split-vakken">{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}</div>
</div> </div>
); );
})} })}
@ -1977,11 +1977,11 @@ export function RoosterScherm({ mode }) {
</div> </div>
<span className="split-count">{kolomAantal}</span> <span className="split-count">{kolomAantal}</span>
</div> </div>
<div className="split-kolommen"> <div className="split-kolommen split-vertakking">
{heleGroep.length > 0 && ( {heleGroep.length > 0 && (
<div className="split-kolom"> <div className="split-kolom">
<div className="split-kop">Hele groep</div> <div className="split-kop split-node-kop">Hele groep</div>
{heleGroep.map((b) => renderCompactBlok(b, iso, kal))} <div className="split-vakken">{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}</div>
</div> </div>
)} )}
{subIds.map((sid) => { {subIds.map((sid) => {
@ -1989,8 +1989,8 @@ export function RoosterScherm({ mode }) {
const blocks = groepBlokken.filter((b) => b.subgroep === sid); const blocks = groepBlokken.filter((b) => b.subgroep === sid);
return ( return (
<div className="split-kolom" key={sid}> <div className="split-kolom" key={sid}>
<div className="split-kop">{sub ? sub.naam : "Subgroep"}</div> <div className="split-kop split-node-kop">{sub ? sub.naam : "Subgroep"}</div>
{blocks.map((b) => renderCompactBlok(b, iso, kal))} <div className="split-vakken">{blocks.map((b) => renderCompactBlok(b, iso, kal))}</div>
</div> </div>
); );
})} })}

View file

@ -190,12 +190,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.split-title span { font-size: 11px; color: var(--muted); } .split-title span { font-size: 11px; color: var(--muted); }
.split-count { min-width: 22px; height: 22px; display: inline-grid; place-items: center; border-radius: 999px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 800; } .split-count { min-width: 22px; height: 22px; display: inline-grid; place-items: center; border-radius: 999px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 800; }
.split-groep { font-size: 12px; font-weight: 800; color: var(--brand); margin: 0 2px 7px; } .split-groep { font-size: 12px; font-weight: 800; color: var(--brand); margin: 0 2px 7px; }
.split-kolommen { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; align-items: stretch; padding-top: 12px; } .split-kolommen { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: stretch; margin-left: 12px; padding: 12px 0 0 16px; border-left: 2px solid #bfdbfe; }
.split-kolommen::before { content: ""; position: absolute; top: 0; left: 10%; right: 10%; border-top: 2px solid #bfdbfe; } .split-kolommen::before { content: ""; position: absolute; top: 0; left: -2px; height: 12px; border-left: 2px solid #bfdbfe; }
.split-kolom { position: relative; min-width: 0; border-left: 1px solid var(--line); padding-left: 7px; height: 100%; } .split-kolom { position: relative; min-width: 0; height: 100%; padding-left: 0; }
.split-kolom::before { content: ""; position: absolute; top: -12px; left: 50%; width: 2px; height: 12px; background: #bfdbfe; } .split-kolom::before { content: ""; position: absolute; top: 14px; left: -16px; width: 16px; border-top: 2px solid #bfdbfe; }
.split-kolom:nth-child(odd) { border-left: 0; padding-left: 0; }
.split-kop { font-size: 11px; font-weight: 700; color: var(--muted); margin: 0 0 4px; } .split-kop { font-size: 11px; font-weight: 700; color: var(--muted); margin: 0 0 4px; }
.split-node-kop { display: inline-flex; max-width: 100%; align-items: center; border: 1px solid #bfdbfe; border-radius: 6px; background: #fff; color: var(--brand); padding: 4px 7px; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split-vakken { display: grid; gap: 6px; }
.split-vak { position: relative; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 6px; background: #fff; padding: 6px 6px 5px; margin-bottom: 6px; cursor: pointer; box-shadow: 0 1px 0 rgba(31,41,55,.03); } .split-vak { position: relative; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 6px; background: #fff; padding: 6px 6px 5px; margin-bottom: 6px; cursor: pointer; box-shadow: 0 1px 0 rgba(31,41,55,.03); }
.split-vak.open { box-shadow: 0 0 0 2px var(--brand-soft); } .split-vak.open { box-shadow: 0 0 0 2px var(--brand-soft); }
.split-vak.vervallen { opacity: .55; } .split-vak.vervallen { opacity: .55; }
@ -222,14 +223,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.split-preview-children::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 2px solid #bfdbfe; } .split-preview-children::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 2px solid #bfdbfe; }
.split-preview-children span { position: relative; padding: 5px 8px; border-radius: 6px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; } .split-preview-children span { position: relative; padding: 5px 8px; border-radius: 6px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; }
.split-preview-children span::before { content: ""; position: absolute; top: -13px; left: 50%; width: 2px; height: 13px; background: #bfdbfe; } .split-preview-children span::before { content: ""; position: absolute; top: -13px; left: 50%; width: 2px; height: 13px; background: #bfdbfe; }
.structuur-form h2 { margin: 0 0 10px; font-size: 17px; } .indeling-form h2 { margin: 0 0 10px; font-size: 17px; }
.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 7px; margin-top: 8px; } .check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 7px; margin-top: 8px; }
.check-item { display: flex; align-items: center; gap: 7px; min-width: 0; border: 1px solid var(--line); border-radius: 7px; background: #fff; padding: 7px 9px; font-size: 13px; } .check-item { display: flex; align-items: center; gap: 7px; min-width: 0; border: 1px solid var(--line); border-radius: 7px; background: #fff; padding: 7px 9px; font-size: 13px; }
.check-item input { min-width: auto; } .check-item input { min-width: auto; }
.check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; } .lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.structuur-overzicht { display: grid; gap: 14px; margin-top: 18px; } .indeling-tree { display: grid; gap: 14px; margin-top: 18px; }
.structuur-tree { display: grid; gap: 14px; margin-top: 18px; }
.tree-root { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; } .tree-root { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
.tree-node { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: 10px 12px; } .tree-node { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: 10px 12px; }
.tree-node-main { display: flex; align-items: flex-start; gap: 10px; min-width: 0; } .tree-node-main { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
@ -247,13 +247,6 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.tree-subchildren { margin-top: 0; border-left-color: #dbeafe; } .tree-subchildren { margin-top: 0; border-left-color: #dbeafe; }
.tree-meta { padding-left: 16px; } .tree-meta { padding-left: 16px; }
.tree-empty { padding: 8px 0 8px 12px; } .tree-empty { padding: 8px 0 8px 12px; }
.structuur-card { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; }
.structuur-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.structuur-head.compact { align-items: center; margin-bottom: 6px; }
.structuur-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.structuur-groep { border: 1px solid var(--line); border-top: 3px solid var(--brand); border-radius: 8px; background: #fff; padding: 10px; }
.structuur-subgroepen { display: grid; gap: 7px; margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--line); }
.structuur-subgroep { border-left: 3px solid #93c5fd; border-radius: 6px; background: #f9fafb; padding: 7px 9px; }
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; z-index: 900; overflow: auto; } .modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; z-index: 900; overflow: auto; }
.modal { background: var(--panel); border-radius: 12px; padding: 18px 20px; width: min(560px, 100%); box-shadow: 0 20px 50px rgba(0,0,0,.3); } .modal { background: var(--panel); border-radius: 12px; padding: 18px 20px; width: min(560px, 100%); box-shadow: 0 20px 50px rgba(0,0,0,.3); }
.modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
@ -321,8 +314,8 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
.topbar-user { flex-direction: column; align-items: flex-end; gap: 2px; } .topbar-user { flex-direction: column; align-items: flex-end; gap: 2px; }
.content, .content-wide { padding: 18px 12px 28px; } .content, .content-wide { padding: 18px 12px 28px; }
.top-tabs-settings { margin-left: 0; border-left: 0; padding-left: 0; } .top-tabs-settings { margin-left: 0; border-left: 0; padding-left: 0; }
.split-kolommen { grid-template-columns: 1fr; } .split-kolommen { grid-template-columns: 1fr; margin-left: 8px; padding-left: 14px; }
.split-kolom { border-left: 0; padding-left: 0; } .split-kolom::before { left: -14px; width: 14px; }
.tree-node { flex-direction: column; align-items: stretch; } .tree-node { flex-direction: column; align-items: stretch; }
.tree-children { margin-left: 10px; padding-left: 12px; } .tree-children { margin-left: 10px; padding-left: 12px; }
.tree-meta { padding-left: 0; } .tree-meta { padding-left: 0; }