Files
familienarchiv/docs/specs/admin-tag-overhaul.html
2026-05-05 12:39:20 +02:00

1208 lines
71 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — Tag Page Complete Overhaul</title>
<link href="https://fonts.googleapis.com/css2?family=Tinos:wght@400;700&family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Montserrat',system-ui,sans-serif;background:#E8E6DF;color:#1A1A24;line-height:1.5;font-size:13px}
.page{max-width:1400px;margin:0 auto;padding:48px 32px 120px}
/* ── Masthead ── */
.mh{padding-bottom:24px;border-bottom:3px solid #012851;margin-bottom:56px}
.mh h1{font-size:24px;font-weight:900;color:#012851;letter-spacing:-.4px}
.mh p{font-size:12.5px;color:#555;max-width:720px;line-height:1.75;margin-top:8px}
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:10px}
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.tag{background:#012851;color:#a1dcd8;padding:2px 8px;border-radius:2px;font-size:8px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}
.tag.amber{background:#92400e;color:#fef3c7}
.tag.green{background:#3a6e42;color:#d1fae5}
.tag.gray{background:#4b5563;color:#e5e7eb}
/* ── Section headers ── */
.sh{margin:64px 0 28px;padding-bottom:14px;border-bottom:2px solid #D8D5CE}
.sh h2{font-size:16px;font-weight:900;color:#012851}
.sh p{font-size:12px;color:#666;margin-top:4px;max-width:760px;line-height:1.65}
/* ── Grid layouts ── */
.grid{display:flex;gap:24px;flex-wrap:wrap;margin-bottom:32px;align-items:flex-start}
.col{display:flex;flex-direction:column;gap:8px}
.lbl{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;display:flex;align-items:center;gap:5px}
.lbl .badge{background:#E0DDD6;color:#666;padding:1px 5px;border-radius:2px;font-size:7px;font-weight:700}
.lbl .badge.new{background:#012851;color:#a1dcd8}
.cap{font-size:10px;color:#888;font-style:italic;line-height:1.6;max-width:480px;margin-top:4px}
/* ── Browser chrome ── */
.chrome{background:#F5F4EE;border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.1)}
.bar{height:18px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 7px;gap:3px;flex-shrink:0}
.dot{width:5px;height:5px;border-radius:50%;background:#BDB8B1}
.url{flex:1;height:7px;background:#CCC8C2;border-radius:5px;margin-left:4px}
/* ── Global app header (65px real → 28px spec) ── */
.app-header{height:28px;background:#012851;display:flex;align-items:center;padding:0 10px;gap:8px;flex-shrink:0}
.app-logo{font-size:6.5px;font-weight:900;color:#fff;letter-spacing:.8px;border-bottom:2px solid #a1dcd8;padding-bottom:1px;font-family:'Montserrat',sans-serif;white-space:nowrap}
.app-link{font-size:5px;color:rgba(255,255,255,.4);font-weight:700;text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
.app-link.act{color:rgba(255,255,255,.85)}
.app-nav-r{margin-left:auto;display:flex;gap:4px;align-items:center}
.app-av{width:14px;height:14px;background:rgba(255,255,255,.12);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:rgba(255,255,255,.5)}
/* ── Admin body shell — everything below app header ── */
.admin-shell{display:flex;overflow:hidden}
/* ── EntityNav (120px desktop / 48px tablet)
Real: bg-brand-navy, border-l-3 on each item, stacked items ── */
.entity-nav{width:66px;flex-shrink:0;background:#012851;display:flex;flex-direction:column}
.entity-nav.tablet{width:26px}
.en-heading{font-size:4.5px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,.35);padding:5px 6px 2px}
.en-item{display:flex;flex-direction:column;align-items:flex-start;gap:1px;padding:5px 6px 5px 5px;border-left:2.5px solid transparent;cursor:pointer;transition:background .1s}
.en-item:hover:not(.en-active){background:rgba(255,255,255,.05)}
.en-item.en-active{border-left-color:#a1dcd8;background:rgba(255,255,255,.08)}
.en-icon{width:9px;height:9px;border-radius:1px;background:rgba(255,255,255,.25);flex-shrink:0}
.en-icon.mint{background:#a1dcd8}
.en-count{font-size:5.5px;font-weight:900;color:rgba(255,255,255,.4);margin-top:1px}
.en-count.mint{color:rgba(255,255,255,.6)}
.en-label{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:rgba(255,255,255,.45);margin-top:0px}
.en-label.mint{color:rgba(255,255,255,.95)}
.en-spacer{flex:1}
.en-sep{border-top:1px solid rgba(255,255,255,.1);margin-top:auto}
/* Tablet: icon + count only, no label */
.entity-nav.tablet .en-item{align-items:center;padding:6px 0}
.entity-nav.tablet .en-label{display:none}
.entity-nav.tablet .en-heading{display:none}
/* ── Tree panel (240px real → 132px spec) ── */
.tree-panel{width:132px;flex-shrink:0;background:#f5f4ef;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid #e4e2d7}
.tree-panel.collapsed{width:18px}
.tree-header{height:24px;display:flex;align-items:center;justify-content:space-between;padding:0 6px;border-bottom:1px solid #e4e2d7;flex-shrink:0}
.tree-header-lbl{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#6b7280}
.tree-collapse-btn{width:12px;height:12px;display:flex;align-items:center;justify-content:center;font-size:7px;color:#9ca3af;border-radius:2px;cursor:pointer;flex-shrink:0}
.tree-scroll{flex:1;overflow-y:auto}
/* Tree nodes */
.tn{display:flex;align-items:center;height:22px;padding-right:6px;cursor:pointer;border-left:2px solid transparent;user-select:none}
.tn:hover{background:#ece9e2}
.tn.active{background:rgba(1,40,81,.09);border-left-color:#012851}
.tn-chevron{width:12px;height:22px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:6px;color:#9ca3af;transition:transform .15s}
.tn-chevron.open{transform:rotate(90deg)}
.tn-chevron.leaf{color:transparent}
.tn-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;margin-right:3px}
.tn-name{font-size:6px;font-weight:600;color:#012851;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tn-count{font-size:5px;color:#9ca3af;font-weight:500;margin-left:2px;flex-shrink:0}
/* Depth indents (12px/level real → 7px/level spec) */
.d0{padding-left:2px}
.d1{padding-left:9px}
.d2{padding-left:16px}
.d3{padding-left:23px}
.d4{padding-left:30px}
/* Collapsed handle */
.tree-collapsed-handle{width:18px;display:flex;flex-direction:column;align-items:center;padding-top:5px;gap:3px;background:#f5f4ef;border-right:1px solid #e4e2d7;cursor:pointer}
.tree-collapsed-lbl{font-size:4.5px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;writing-mode:vertical-rl;transform:rotate(180deg)}
/* ── Edit panel ── */
.edit-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:#fff}
.ep-header{height:30px;display:flex;align-items:center;padding:0 10px;border-bottom:1px solid #e4e2d7;flex-shrink:0;gap:5px}
.ep-back{font-size:5px;color:#9ca3af;cursor:pointer;display:flex;align-items:center;gap:1px;flex-shrink:0}
.ep-title{font-size:7px;font-weight:700;color:#012851;font-family:'Montserrat',sans-serif;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ep-scroll{flex:1;overflow-y:auto;padding:8px 10px}
/* Breadcrumb */
.breadcrumb{display:flex;align-items:center;gap:3px;margin-bottom:7px;padding:4px 6px;background:#f5f4ef;border-radius:2px;border:1px solid #e4e2d7;flex-wrap:wrap}
.bc-link{font-size:5.5px;font-weight:600;color:#012851;text-decoration:underline;text-underline-offset:1px;cursor:pointer}
.bc-sep{font-size:5.5px;color:#9ca3af}
.bc-cur{font-size:5.5px;font-weight:700;color:#012851}
/* Form cards */
.form-card{border:1px solid #e4e2d7;border-radius:2px;background:#fff;padding:8px 9px;margin-bottom:6px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.fct{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#6b7280;margin-bottom:5px}
.form-input{width:100%;border:1px solid #e4e2d7;border-radius:2px;background:#f5f4ef;padding:4px 6px;font-size:6px;color:#012851}
.form-warning{font-size:5px;color:#b45309;background:#fffbeb;border:1px solid #fde68a;border-radius:2px;padding:3px 5px;margin-bottom:5px;line-height:1.5}
/* Parent picker */
.picker-input{width:100%;border:1px solid #e4e2d7;border-radius:2px;background:#f5f4ef;padding:4px 20px 4px 6px;font-size:6px;color:#012851;position:relative;cursor:pointer}
.picker-placeholder{color:#9ca3af}
.picker-val{font-weight:600;color:#012851}
.picker-path{font-size:4.5px;color:#6b7280;margin-top:1px}
.picker-clear{position:absolute;right:5px;top:50%;transform:translateY(-50%);font-size:7px;color:#9ca3af;cursor:pointer}
.picker-dropdown{background:#fff;border:1px solid #e4e2d7;border-radius:2px;box-shadow:0 4px 12px rgba(0,0,0,.1);overflow:hidden;margin-top:2px}
.picker-opt{padding:4px 6px;cursor:pointer;display:flex;align-items:flex-start;gap:3px}
.picker-opt:hover,.picker-opt.sel{background:#f5f4ef}
.picker-opt-dot{width:5px;height:5px;border-radius:50%;margin-top:2px;flex-shrink:0}
.picker-opt-name{font-size:6px;font-weight:600;color:#012851}
.picker-opt-path{font-size:4.5px;color:#6b7280;margin-top:1px}
/* Color picker */
.color-grid{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
.swatch{width:14px;height:14px;border-radius:50%;cursor:pointer;flex-shrink:0}
.swatch.sel{box-shadow:0 0 0 2px #fff,0 0 0 3.5px currentColor}
.color-reset{width:14px;height:14px;border-radius:50%;border:1px solid #e4e2d7;background:#f5f4ef;display:flex;align-items:center;justify-content:center;font-size:7px;color:#9ca3af;cursor:pointer}
.inherited-color{display:flex;align-items:center;gap:4px;padding:4px 6px;background:#f5f4ef;border:1px solid #e4e2d7;border-radius:2px;font-size:5px;color:#6b7280;margin-top:3px}
.inh-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
/* Children chips */
.children-chips{display:flex;flex-wrap:wrap;gap:3px;margin-top:4px}
.child-chip{display:inline-flex;align-items:center;gap:2px;padding:2px 6px;background:#f5f4ef;border:1px solid #e4e2d7;border-radius:9px;font-size:5px;color:#4b5563;cursor:pointer;font-weight:500}
.child-chip:hover{background:#ece9e2}
.chip-count{font-size:4px;color:#9ca3af;margin-left:1px}
.child-more{font-size:5px;color:#9ca3af;align-self:center;padding:2px 0}
/* Merge zone */
.merge-zone{border:1px solid #fde68a;background:#fffbeb;border-radius:2px;padding:8px 9px;margin-bottom:6px}
.mz-title{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#92400e;margin-bottom:4px}
.mz-desc{font-size:5px;color:#78350f;line-height:1.55;margin-bottom:5px}
.mz-btn{display:inline-flex;align-items:center;gap:2px;padding:3px 8px;background:#92400e;color:#fef3c7;border-radius:2px;font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;cursor:pointer;min-height:12px}
/* Danger zone */
.danger-zone{border:1px solid #fecaca;background:#fef2f2;border-radius:2px;padding:8px 9px;margin-bottom:6px}
.dz-title{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#991b1b;margin-bottom:4px}
.dz-impact{background:#fff0f0;border:1px solid #fecaca;border-radius:2px;padding:4px 6px;margin-bottom:5px;font-size:5px;color:#991b1b;line-height:1.5}
.dz-radios{display:flex;flex-direction:column;gap:3px;margin-bottom:5px}
.dz-radio{display:flex;align-items:flex-start;gap:4px;padding:4px 6px;border:1px solid #fecaca;background:#fff;border-radius:2px;cursor:pointer}
.dz-radio.sel{border-color:#991b1b;background:#fef2f2}
.dz-radio-circle{width:6px;height:6px;border-radius:50%;border:1.5px solid #fca5a5;flex-shrink:0;margin-top:1px}
.dz-radio.sel .dz-radio-circle{border-color:#991b1b;background:#991b1b}
.dz-radio-label{font-size:5px;font-weight:600;color:#7f1d1d;line-height:1.4}
.dz-radio-sub{font-size:4.5px;color:#9ca3af;margin-top:1px}
.dz-radio-sub.warn{color:#b91c1c}
.dz-confirm-lbl{font-size:5px;color:#7f1d1d;margin-bottom:4px}
.dz-confirm-lbl code{font-family:monospace;background:#fef2f2;padding:0 2px;border-radius:1px}
.dz-input{width:100%;border:1px solid #fca5a5;border-radius:2px;background:#fff;padding:3px 5px;font-size:5.5px;color:#012851;margin-bottom:4px}
.dz-btn{display:inline-flex;align-items:center;padding:3px 8px;background:#c0392b;color:#fff;border-radius:2px;font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;cursor:pointer;min-height:12px}
.dz-btn.disabled{opacity:.4;cursor:not-allowed}
/* Save bar */
.save-bar{height:28px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border-top:1px solid #e4e2d7;background:#fff;flex-shrink:0}
.save-cancel{font-size:5.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#6b7280;cursor:pointer}
.save-btn{padding:0 10px;height:17px;background:#012851;color:#fff;border-radius:2px;font-size:5.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;display:flex;align-items:center}
/* Banners */
.banner-ok{border:1px solid #bbf7d0;background:#f0fdf4;border-radius:2px;padding:4px 6px;font-size:5.5px;color:#14532d;margin-bottom:6px}
.banner-err{border:1px solid #fecaca;background:#fef2f2;border-radius:2px;padding:4px 6px;font-size:5.5px;color:#991b1b;margin-bottom:6px}
/* ── Modal ── */
.modal-wrap{position:absolute;inset:0;background:rgba(1,18,40,.45);display:flex;align-items:center;justify-content:center;z-index:50}
.modal{background:#fff;border-radius:5px;box-shadow:0 12px 32px rgba(0,0,0,.2);width:240px;overflow:hidden}
.modal-hd{padding:8px 10px;border-bottom:1px solid #e4e2d7}
.modal-step-lbl{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#6b7280;margin-bottom:2px}
.modal-title{font-size:7.5px;font-weight:700;color:#012851}
.modal-body{padding:8px 10px}
.modal-ft{padding:6px 10px;border-top:1px solid #e4e2d7;display:flex;justify-content:space-between;align-items:center}
.modal-cancel{font-size:5.5px;font-weight:700;text-transform:uppercase;color:#6b7280}
.modal-next{padding:0 8px;height:16px;background:#012851;color:#fff;border-radius:2px;font-size:5.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;display:flex;align-items:center}
.modal-next.amber{background:#92400e;color:#fef3c7}
.modal-dots{display:flex;gap:3px;justify-content:center;margin-bottom:8px}
.modal-dot{width:18px;height:2.5px;border-radius:2px;background:#e4e2d7}
.modal-dot.done{background:#012851}
.modal-dot.active{background:#a1dcd8}
/* Merge preview */
.mp{background:#f5f4ef;border:1px solid #e4e2d7;border-radius:2px;padding:6px 8px;margin-top:6px}
.mp-title{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#6b7280;margin-bottom:4px}
.mp-row{display:flex;align-items:baseline;gap:3px;font-size:5.5px;margin-bottom:2.5px}
.mp-num{font-weight:800;color:#012851;font-size:7px}
.mp-desc{color:#4b5563}
.mp-sep{border-top:1px solid #e4e2d7;margin:4px 0}
.mp-warn{font-size:5px;color:#991b1b;font-weight:700}
/* Merge summary step 2 */
.ms-from{padding:7px;background:#fef2f2;border:1px solid #fecaca;border-radius:2px 2px 0 0;text-align:center}
.ms-to{padding:7px;background:#f0fdf4;border:1px solid #bbf7d0;border-top:0;border-radius:0 0 2px 2px;text-align:center}
.ms-lbl{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;margin-bottom:2.5px}
.ms-lbl.del{color:#991b1b}.ms-lbl.surv{color:#14532d}
.ms-tag{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:9px;font-size:6px;font-weight:700}
.ms-tag.del{background:#fecaca;color:#991b1b}
.ms-tag.surv{background:#bbf7d0;color:#14532d}
.ms-stats{display:flex;gap:10px;justify-content:center;margin-top:6px;padding:6px;background:#f5f4ef;border:1px solid #e4e2d7;border-radius:2px}
.ms-stat{text-align:center}
.ms-stat .n{font-size:8px;font-weight:800;color:#012851}
.ms-stat .d{font-size:4.5px;color:#6b7280;margin-top:1px}
/* ── Phone frame ── */
.phone{width:210px;background:#F5F4EE;border-radius:22px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.18);border:4px solid #1C1C24;display:flex;flex-direction:column}
.phone-status{height:14px;background:#F5F4EE;display:flex;align-items:center;justify-content:space-between;padding:0 12px;font-size:6px;font-weight:600;color:#6b7280;flex-shrink:0}
.phone-body{display:flex;flex-direction:column;overflow:hidden;flex:1}
/* ── Annotations ── */
.annotation{display:flex;gap:8px;margin-top:8px;align-items:flex-start}
.ann-bullet{width:16px;height:16px;border-radius:50%;background:#012851;color:#a1dcd8;font-size:7px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
.ann-text{font-size:11px;color:#555;line-height:1.65;flex:1}
.ann-text strong{color:#1A1A24;font-weight:700}
.ann-text code{font-family:monospace;background:#E0DDD6;padding:1px 4px;border-radius:2px;font-size:10px;color:#012851}
/* ── Impl-ref table ── */
.impl-ref table{width:100%;border-collapse:collapse;font-size:11px;margin-top:12px}
.impl-ref th{background:#012851;color:#a1dcd8;font-weight:700;text-transform:uppercase;letter-spacing:.5px;font-size:9px;padding:7px 10px;text-align:left}
.impl-ref td{padding:7px 10px;border-bottom:1px solid #E0DDD6;vertical-align:top;line-height:1.6}
.impl-ref tr:hover td{background:#F0EFE9}
.impl-ref code{font-family:monospace;background:#F0EFE9;padding:1px 4px;border-radius:2px;font-size:10px;color:#012851}
.impl-ref .new{color:#3a6e42;font-weight:600}
.impl-ref .warn{color:#92400e;font-weight:600}
/* Divider line inside admin shell */
.v-line{width:1px;background:#e4e2d7;flex-shrink:0}
</style>
</head>
<body>
<div class="page">
<!-- ══════════════════════════════════════════════════════════════ MASTHEAD -->
<div class="mh">
<h1>Admin — Schlagwörter: Complete Page Overhaul</h1>
<p>Full redesign of the tag admin two-panel page to support infinite hierarchy depth.
The admin area already has a three-column structure:
<strong>EntityNav</strong> (navy sidebar) → <strong>Tags list panel</strong><strong>Edit panel</strong>.
This spec redesigns panels 2 and 3 to handle a real tree, a tree-aware parent picker, ancestry breadcrumb,
children preview, tag merge flow, and a destructive delete guard.</p>
<div class="tag-row">
<span class="tag">Route: /admin/tags</span>
<span class="tag">Route: /admin/tags/[id]</span>
<span class="tag amber">New endpoint: POST /api/tags/{id}/merge</span>
<span class="tag green">Breakpoints: 320 / 768 / 1024 / 1440</span>
<span class="tag gray">Changes: TagsListPanel.svelte · [id]/+page.svelte</span>
</div>
<p class="byline">Leonie Voss · UI/UX Design Lead · Familienarchiv · 2026-04-16</p>
</div>
<!-- ══════════════════════════════════════════ S1: DESKTOP FULL OVERVIEW -->
<div class="sh">
<h2>1 — Desktop: Full Three-Column Layout (lg, ≥1024px)</h2>
<p>The admin shell already has three columns: EntityNav (navy, 120px at lg+) → entity list panel → edit panel.
For tags, the list panel becomes a collapsible tree browser. The edit panel gains breadcrumb, children preview,
merge zone, and a guarded delete zone. Nothing about the EntityNav changes.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">Desktop lg (1280px) — child tag "Deutschland" selected (depth 2)</div>
<div class="chrome" style="width:960px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<!-- Global app header -->
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<span class="app-link">Dokumente</span>
<span class="app-link">Personen</span>
<span class="app-link act">Admin</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<!-- Admin shell -->
<div class="admin-shell" style="height:500px">
<!-- 1: EntityNav (unchanged, 120px real → 66px spec) -->
<div class="entity-nav">
<div class="en-heading">Admin</div>
<!-- Users -->
<div class="en-item">
<div class="en-icon"></div>
<div class="en-count">5</div>
<div class="en-label">Benutzer</div>
</div>
<!-- Groups -->
<div class="en-item">
<div class="en-icon"></div>
<div class="en-count">3</div>
<div class="en-label">Gruppen</div>
</div>
<!-- Tags (active) -->
<div class="en-item en-active">
<div class="en-icon mint"></div>
<div class="en-count mint">142</div>
<div class="en-label mint">Schlagwörter</div>
</div>
<div class="en-spacer"></div>
<div class="en-sep"></div>
<!-- System -->
<div class="en-item" style="border-top:0">
<div class="en-icon"></div>
<div class="en-label">System</div>
</div>
</div>
<!-- 2: Tree panel (NEW — replaces flat TagsListPanel) -->
<div class="tree-panel">
<div class="tree-header">
<span class="tree-header-lbl">Schlagwörter</span>
<span class="tree-collapse-btn"></span>
</div>
<div class="tree-scroll">
<!-- Root: Orte (expanded) -->
<div class="tn d0">
<span class="tn-chevron open"></span>
<span class="tn-dot" style="background:#c17a00"></span>
<span class="tn-name">Orte</span>
<span class="tn-count">(38)</span>
</div>
<!-- d1: Europa (expanded) -->
<div class="tn d1">
<span class="tn-chevron open"></span>
<span class="tn-name">Europa</span>
<span class="tn-count">(22)</span>
</div>
<!-- d2: Deutschland (active) -->
<div class="tn d2 active">
<span class="tn-chevron leaf"></span>
<span class="tn-name">Deutschland</span>
<span class="tn-count">(11)</span>
</div>
<!-- d2: Frankreich -->
<div class="tn d2">
<span class="tn-chevron leaf"></span>
<span class="tn-name">Frankreich</span>
<span class="tn-count">(5)</span>
</div>
<!-- d2: Österreich (collapsed, has children) -->
<div class="tn d2">
<span class="tn-chevron"></span>
<span class="tn-name">Österreich</span>
<span class="tn-count">(6)</span>
</div>
<!-- d1: Asien (collapsed) -->
<div class="tn d1">
<span class="tn-chevron"></span>
<span class="tn-name">Asien</span>
<span class="tn-count">(8)</span>
</div>
<!-- Root: Personen (collapsed) -->
<div class="tn d0">
<span class="tn-chevron"></span>
<span class="tn-dot" style="background:#7a4f9a"></span>
<span class="tn-name">Personen</span>
<span class="tn-count">(54)</span>
</div>
<!-- Root: Ereignisse (expanded) -->
<div class="tn d0">
<span class="tn-chevron open"></span>
<span class="tn-dot" style="background:#3060b0"></span>
<span class="tn-name">Ereignisse</span>
<span class="tn-count">(19)</span>
</div>
<div class="tn d1"><span class="tn-chevron leaf"></span><span class="tn-name">Hochzeiten</span><span class="tn-count">(7)</span></div>
<div class="tn d1"><span class="tn-chevron leaf"></span><span class="tn-name">Geburten</span><span class="tn-count">(5)</span></div>
<div class="tn d1"><span class="tn-chevron leaf"></span><span class="tn-name">Reisen</span><span class="tn-count">(7)</span></div>
<!-- Root: Urkunden (no color, leaf) -->
<div class="tn d0"><span class="tn-chevron leaf"></span><span class="tn-name" style="color:#4b5563">Urkunden</span><span class="tn-count">(12)</span></div>
</div>
</div>
<!-- 3: Edit panel -->
<div class="edit-panel">
<div class="ep-header">
<span class="ep-back" style="display:flex;align-items:center;gap:1px;font-size:5px;color:#9ca3af"></span>
<span class="ep-title">Schlagwort bearbeiten — Deutschland</span>
</div>
<div class="ep-scroll">
<!-- Breadcrumb -->
<div class="breadcrumb">
<a class="bc-link">Orte</a>
<span class="bc-sep"></span>
<a class="bc-link">Europa</a>
<span class="bc-sep"></span>
<span class="bc-cur">Deutschland</span>
</div>
<!-- Name -->
<div class="form-card">
<div class="fct">Name</div>
<div class="form-warning">⚠ Namensänderungen wirken sich auf alle verknüpften Dokumente aus.</div>
<div class="form-input">Deutschland</div>
</div>
<!-- Parent picker -->
<div class="form-card" style="position:relative">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative">
<div class="picker-input">
<div class="picker-val">Europa</div>
<div class="picker-path">Orte Europa</div>
</div>
<span class="picker-clear">×</span>
</div>
</div>
<!-- Inherited color (child tag — no color picker) -->
<div class="form-card">
<div class="fct">Farbe</div>
<div class="inherited-color">
<span class="inh-dot" style="background:#c17a00"></span>
Farbe von Orte (amber) — wird vererbt
</div>
</div>
<!-- Children preview -->
<div class="form-card">
<div class="fct">Untergeordnete Schlagwörter</div>
<div class="children-chips">
<span class="child-chip">Berlin<span class="chip-count">(3)</span></span>
<span class="child-chip">München<span class="chip-count">(4)</span></span>
<span class="child-chip">Hamburg<span class="chip-count">(2)</span></span>
<span class="child-chip">Bayern<span class="chip-count">(2)</span></span>
<span class="child-more">… und 2 weitere →</span>
</div>
</div>
<!-- Merge -->
<div class="merge-zone">
<div class="mz-title">Zusammenführen</div>
<div class="mz-desc">Alle Dokumente und untergeordnete Schlagwörter auf ein anderes Schlagwort übertragen und dieses danach löschen.</div>
<div class="mz-btn">⇒ Mit anderem Schlagwort zusammenführen …</div>
</div>
<!-- Danger zone -->
<div class="danger-zone">
<div class="dz-title">Löschen</div>
<div class="dz-impact"><strong>11 Dokumente</strong> verknüpft · <strong>6 Untergeordnete Schlagwörter</strong></div>
<div class="dz-radios">
<div class="dz-radio sel">
<div class="dz-radio-circle"></div>
<div>
<div class="dz-radio-label">Nur dieses Schlagwort löschen</div>
<div class="dz-radio-sub">Untergeordnete werden zu Europa verschoben</div>
</div>
</div>
<div class="dz-radio">
<div class="dz-radio-circle"></div>
<div>
<div class="dz-radio-label">Gesamten Teilbaum löschen</div>
<div class="dz-radio-sub warn">Löscht auch 6 untergeordnete Schlagwörter</div>
</div>
</div>
</div>
<div class="dz-confirm-lbl">Gib <code>Deutschland</code> zur Bestätigung ein:</div>
<div class="dz-input" style="color:#9ca3af">Deutschland</div>
<div class="dz-btn disabled">Löschen</div>
</div>
</div><!-- /ep-scroll -->
<div class="save-bar">
<span class="save-cancel">Abbrechen</span>
<span class="save-btn">Speichern</span>
</div>
</div><!-- /edit-panel -->
</div><!-- /admin-shell -->
</div><!-- /chrome -->
<div class="cap">Three columns left-to-right: <strong>EntityNav</strong> (unchanged, 120px navy sidebar) →
<strong>Tree panel</strong> (240px, replaces flat list) → <strong>Edit panel</strong> (flex-1, ~860px at 1280px).
The EntityNav and its flyout behaviour on tablet are not touched by this spec.</div>
</div>
</div>
<!-- ══════════════════════════════════ S2: TABLET (md, 768px) -->
<div class="sh">
<h2>2 — Tablet: md breakpoint (768px)</h2>
<p>At tablet width, EntityNav shrinks to 48px icon-only strip (existing behaviour, unchanged).
The tree panel and edit panel share the remaining 720px.
The tree panel collapses to 32px when not needed, giving the edit panel more room.</p>
</div>
<div class="grid">
<!-- Tablet, tree visible -->
<div class="col">
<div class="lbl">768px — tree + edit side by side</div>
<div class="chrome" style="width:640px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="admin-shell" style="height:380px">
<!-- EntityNav — tablet: icon-only, 48px real → 26px spec -->
<div class="entity-nav tablet">
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-item en-active"><div class="en-icon mint" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-spacer"></div>
<div class="en-sep"></div>
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
</div>
<!-- Tree panel (same as desktop) -->
<div class="tree-panel">
<div class="tree-header">
<span class="tree-header-lbl">Schlagwörter</span>
<span class="tree-collapse-btn"></span>
</div>
<div class="tree-scroll">
<div class="tn d0"><span class="tn-chevron open"></span><span class="tn-dot" style="background:#c17a00"></span><span class="tn-name">Orte</span><span class="tn-count">(38)</span></div>
<div class="tn d1"><span class="tn-chevron open"></span><span class="tn-name">Europa</span><span class="tn-count">(22)</span></div>
<div class="tn d2 active"><span class="tn-chevron leaf"></span><span class="tn-name">Deutschland</span><span class="tn-count">(11)</span></div>
<div class="tn d2"><span class="tn-chevron leaf"></span><span class="tn-name">Frankreich</span><span class="tn-count">(5)</span></div>
<div class="tn d1"><span class="tn-chevron"></span><span class="tn-name">Asien</span><span class="tn-count">(8)</span></div>
<div class="tn d0"><span class="tn-chevron"></span><span class="tn-dot" style="background:#7a4f9a"></span><span class="tn-name">Personen</span><span class="tn-count">(54)</span></div>
</div>
</div>
<!-- Edit panel (narrower at tablet) -->
<div class="edit-panel">
<div class="ep-header">
<span class="ep-title">Schlagwort bearbeiten — Deutschland</span>
</div>
<div class="ep-scroll">
<div class="breadcrumb"><a class="bc-link">Orte</a><span class="bc-sep"></span><a class="bc-link">Europa</a><span class="bc-sep"></span><span class="bc-cur">Deutschland</span></div>
<div class="form-card"><div class="fct">Name</div><div class="form-input">Deutschland</div></div>
<div class="form-card">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative"><div class="picker-input"><div class="picker-val">Europa</div><div class="picker-path">Orte Europa</div></div><span class="picker-clear">×</span></div>
</div>
</div>
<div class="save-bar"><span class="save-cancel">Abbrechen</span><span class="save-btn">Speichern</span></div>
</div>
</div>
</div>
<div class="cap">Tablet: EntityNav shrinks to icon-only strip (26px spec / 48px real). The tree panel and edit panel
still sit side by side. Tree panel can be collapsed to give the edit panel more space.</div>
</div>
<!-- Tablet, tree collapsed -->
<div class="col">
<div class="lbl">768px — tree collapsed (more edit space)</div>
<div class="chrome" style="width:640px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header"><span class="app-logo">FAMILIENARCHIV</span><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div class="admin-shell" style="height:380px">
<div class="entity-nav tablet">
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-item en-active"><div class="en-icon mint" style="width:8px;height:8px;margin:auto"></div></div>
<div class="en-spacer"></div><div class="en-sep"></div>
<div class="en-item"><div class="en-icon" style="width:8px;height:8px;margin:auto"></div></div>
</div>
<!-- Tree panel — collapsed -->
<div class="tree-collapsed-handle">
<span style="font-size:8px;color:#9ca3af;margin-top:2px"></span>
<span class="tree-collapsed-lbl">Schlagwörter</span>
</div>
<!-- Edit panel gains the freed space -->
<div class="edit-panel">
<div class="ep-header">
<span class="ep-title">Schlagwort bearbeiten — Deutschland</span>
</div>
<div class="ep-scroll">
<div class="breadcrumb"><a class="bc-link">Orte</a><span class="bc-sep"></span><a class="bc-link">Europa</a><span class="bc-sep"></span><span class="bc-cur">Deutschland</span></div>
<div class="form-card"><div class="fct">Name</div><div class="form-warning">⚠ Namensänderungen wirken sich auf alle verknüpften Dokumente aus.</div><div class="form-input">Deutschland</div></div>
<div class="form-card">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative"><div class="picker-input"><div class="picker-val">Europa</div><div class="picker-path">Orte Europa</div></div><span class="picker-clear">×</span></div>
</div>
<div class="form-card"><div class="fct">Farbe</div><div class="inherited-color"><span class="inh-dot" style="background:#c17a00"></span>Farbe von Orte (amber) — wird vererbt</div></div>
<div class="form-card"><div class="fct">Untergeordnete (6)</div><div class="children-chips"><span class="child-chip">Berlin<span class="chip-count">(3)</span></span><span class="child-chip">München<span class="chip-count">(4)</span></span><span class="child-more">… und 4 weitere →</span></div></div>
</div>
<div class="save-bar"><span class="save-cancel">Abbrechen</span><span class="save-btn">Speichern</span></div>
</div>
</div>
</div>
<div class="cap">Collapsed tree: 32px real wide. Click the arrow to re-expand.
Edit panel gets the freed space (~690px at 768px with EntityNav 48px + collapsed tree 32px).</div>
</div>
</div>
<!-- ══════════════════════════════════ S3: TREE PANEL STATES -->
<div class="sh">
<h2>3 — Tree Panel: Node States &amp; Depth</h2>
<p>Detailed view of each node type. True depth indentation: 12px per level real (7px at spec scale).
The panel width increases from 200px → 240px to accommodate depth-3+ labels without truncation.
On tablet this still fits: 48px EntityNav + 240px tree + remaining edit = ✓ at 768px.</p>
</div>
<div class="grid">
<!-- Expanded root with children -->
<div class="col">
<div class="lbl">Root — expanded, active child</div>
<div class="chrome" style="width:140px">
<div style="background:#f5f4ef">
<div class="tn d0" style="background:rgba(1,40,81,.04)"><span class="tn-chevron open"></span><span class="tn-dot" style="background:#c17a00"></span><span class="tn-name">Orte</span><span class="tn-count">(38)</span></div>
<div class="tn d1"><span class="tn-chevron open"></span><span class="tn-name">Europa</span><span class="tn-count">(22)</span></div>
<div class="tn d2 active"><span class="tn-chevron leaf"></span><span class="tn-name">Deutschland</span><span class="tn-count">(11)</span></div>
<div class="tn d2"><span class="tn-chevron leaf"></span><span class="tn-name">Frankreich</span><span class="tn-count">(5)</span></div>
<div class="tn d1"><span class="tn-chevron"></span><span class="tn-name">Asien</span><span class="tn-count">(8)</span></div>
</div>
</div>
<div class="cap">Active: <code>border-l-2 border-primary bg-primary/[0.09]</code>. Depth 2 indent: <code>pl-[28px]</code> real.</div>
</div>
<!-- Deep tree (depth 3-4) -->
<div class="col">
<div class="lbl">Deep hierarchy — depth 34</div>
<div class="chrome" style="width:140px">
<div style="background:#f5f4ef">
<div class="tn d0"><span class="tn-chevron open"></span><span class="tn-dot" style="background:#c17a00"></span><span class="tn-name">Orte</span><span class="tn-count">(38)</span></div>
<div class="tn d1"><span class="tn-chevron open"></span><span class="tn-name">Europa</span><span class="tn-count">(22)</span></div>
<div class="tn d2"><span class="tn-chevron open"></span><span class="tn-name">Deutschland</span><span class="tn-count">(11)</span></div>
<div class="tn d3 active"><span class="tn-chevron leaf"></span><span class="tn-name">Bayern</span><span class="tn-count">(4)</span></div>
<div class="tn d3"><span class="tn-chevron open"></span><span class="tn-name">Sachsen</span><span class="tn-count">(2)</span></div>
<div class="tn d4"><span class="tn-chevron leaf"></span><span class="tn-name">Leipzig</span><span class="tn-count">(1)</span></div>
</div>
</div>
<div class="cap">Depth-4 indent: <code>pl-[52px]</code> real. Label always <code>truncate</code>. 240px panel provides ~134px for label at depth 4.</div>
</div>
<!-- Color states -->
<div class="col">
<div class="lbl">Color dots — root only</div>
<div class="chrome" style="width:140px">
<div style="background:#f5f4ef">
<div class="tn d0"><span class="tn-chevron leaf"></span><span class="tn-dot" style="background:#5a8a6a"></span><span class="tn-name">Natur</span><span class="tn-count">(6)</span></div>
<div class="tn d0"><span class="tn-chevron"></span><span class="tn-dot" style="background:#3060b0"></span><span class="tn-name">Ereignisse</span><span class="tn-count">(19)</span></div>
<div class="tn d0"><span class="tn-chevron leaf"></span><span class="tn-name" style="color:#4b5563">Sonstiges</span><span class="tn-count">(3)</span></div>
<div class="tn d0" style="background:#ece9e2"><span class="tn-chevron leaf"></span><span class="tn-dot" style="background:#c0446e"></span><span class="tn-name">Familie</span><span class="tn-count">(9)</span></div>
</div>
</div>
<div class="cap">Color dot only on root tags with a color set. Root without color: no dot, muted label color. Hover: <code>bg-muted</code>.</div>
</div>
<!-- Hover on chevron -->
<div class="col">
<div class="lbl">Collapsed panel handle (32px real)</div>
<div class="chrome" style="width:60px;min-height:120px;display:flex">
<div class="tree-collapsed-handle" style="flex:1;min-height:120px">
<span style="font-size:8px;color:#9ca3af"></span>
<span class="tree-collapsed-lbl">Schlagwörter</span>
</div>
</div>
<div class="cap">32px wide. Click expands panel. State saved per tag in <code>localStorage</code>.</div>
</div>
</div>
<div class="annotation">
<div class="ann-bullet">A</div>
<div class="ann-text">Each chevron is a real <code>&lt;button aria-expanded&gt;</code> with <code>aria-controls</code> pointing to the child list.
Keyboard navigation: <strong></strong> expands, <strong></strong> collapses, <strong>↑↓</strong> move between visible nodes.
The row link is a separate <code>&lt;a&gt;</code> for navigation — the chevron and the label/name are different interactive targets.</div>
</div>
<!-- ══════════════════════════════════ S4: EDIT PANEL STATES -->
<div class="sh">
<h2>4 — Edit Panel: Root Tag vs. Child Tag</h2>
<p>Root tags show the color picker. Child tags show an inherited-color indicator and a full ancestry breadcrumb.
Both show the children preview section, merge zone, and delete guard.</p>
</div>
<div class="grid">
<!-- Root tag -->
<div class="col">
<div class="lbl">Root tag "Orte" — color picker visible, no breadcrumb trail</div>
<div class="chrome" style="width:340px">
<div class="edit-panel" style="height:520px">
<div class="ep-header"><span class="ep-title">Schlagwort bearbeiten — Orte</span></div>
<div class="ep-scroll" style="flex:1;overflow-y:auto;padding:8px 10px">
<div class="breadcrumb"><span class="bc-cur">Orte</span></div>
<div class="form-card">
<div class="fct">Name</div>
<div class="form-warning">⚠ Namensänderungen wirken sich auf alle verknüpften Dokumente aus.</div>
<div class="form-input">Orte</div>
</div>
<div class="form-card">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative">
<div class="picker-input"><span class="picker-placeholder">Kein übergeordnetes Schlagwort</span></div>
<span class="picker-clear" style="opacity:.3">×</span>
</div>
</div>
<!-- Color picker — root only -->
<div class="form-card">
<div class="fct">Farbe</div>
<div class="color-grid">
<div class="swatch" style="background:#5a8a6a"></div>
<div class="swatch sel" style="background:#c17a00;color:#c17a00"></div>
<div class="swatch" style="background:#607080"></div>
<div class="swatch" style="background:#7a4f9a"></div>
<div class="swatch" style="background:#c0446e"></div>
<div class="swatch" style="background:#3060b0"></div>
<div class="swatch" style="background:#4a7a3a"></div>
<div class="swatch" style="background:#9a8040"></div>
<div class="swatch" style="background:#c05540"></div>
<div class="swatch" style="background:#a0522d"></div>
<div class="color-reset">×</div>
</div>
</div>
<div class="form-card">
<div class="fct">Untergeordnete Schlagwörter (5)</div>
<div class="children-chips">
<span class="child-chip">Europa<span class="chip-count">(22)</span></span>
<span class="child-chip">Asien<span class="chip-count">(8)</span></span>
<span class="child-chip">Amerika<span class="chip-count">(5)</span></span>
<span class="child-chip">Afrika<span class="chip-count">(2)</span></span>
<span class="child-chip">Australien<span class="chip-count">(1)</span></span>
</div>
</div>
<div class="merge-zone"><div class="mz-title">Zusammenführen</div><div class="mz-desc">Alle Dokumente und untergeordnete Schlagwörter übertragen.</div><div class="mz-btn">⇒ Zusammenführen …</div></div>
<div class="danger-zone"><div class="dz-title">Löschen</div><div class="dz-impact"><strong>38 Dok.</strong> · <strong>5 Untergeordnete</strong></div><div class="dz-btn disabled">Löschen</div></div>
</div>
<div class="save-bar"><span class="save-cancel">Abbrechen</span><span class="save-btn">Speichern</span></div>
</div>
</div>
<div class="cap">Root: single breadcrumb segment (no links). Color picker visible (no parent).
When a parent is selected, the color picker slides out and the inherited-color indicator slides in.</div>
</div>
<!-- Child tag full form -->
<div class="col">
<div class="lbl">Child tag "Deutschland" (depth 2) — full delete guard expanded</div>
<div class="chrome" style="width:340px">
<div class="edit-panel" style="height:560px">
<div class="ep-header"><span class="ep-title">Schlagwort bearbeiten — Deutschland</span></div>
<div class="ep-scroll" style="flex:1;overflow-y:auto;padding:8px 10px">
<div class="breadcrumb"><a class="bc-link">Orte</a><span class="bc-sep"></span><a class="bc-link">Europa</a><span class="bc-sep"></span><span class="bc-cur">Deutschland</span></div>
<div class="form-card"><div class="fct">Name</div><div class="form-warning">⚠ Namensänderungen wirken sich auf alle verknüpften Dokumente aus.</div><div class="form-input">Deutschland</div></div>
<div class="form-card">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative"><div class="picker-input"><div class="picker-val">Europa</div><div class="picker-path">Orte Europa</div></div><span class="picker-clear">×</span></div>
</div>
<div class="form-card"><div class="fct">Farbe</div><div class="inherited-color"><span class="inh-dot" style="background:#c17a00"></span>Farbe von Orte (amber) — wird vererbt</div></div>
<div class="form-card"><div class="fct">Untergeordnete Schlagwörter</div><div class="children-chips"><span class="child-chip">Berlin<span class="chip-count">(3)</span></span><span class="child-chip">München<span class="chip-count">(4)</span></span><span class="child-chip">Hamburg<span class="chip-count">(2)</span></span><span class="child-chip">Bayern<span class="chip-count">(2)</span></span><span class="child-more">… und 2 weitere →</span></div></div>
<div class="merge-zone"><div class="mz-title">Zusammenführen</div><div class="mz-desc">Alle Dokumente und untergeordnete Schlagwörter auf ein anderes Schlagwort übertragen.</div><div class="mz-btn">⇒ Zusammenführen …</div></div>
<!-- Delete zone — fully expanded with radio selection -->
<div class="danger-zone">
<div class="dz-title">Löschen</div>
<div class="dz-impact"><strong>11 Dokumente</strong> verknüpft · <strong>6 Untergeordnete Schlagwörter</strong></div>
<div class="dz-radios">
<div class="dz-radio sel">
<div class="dz-radio-circle"></div>
<div><div class="dz-radio-label">Nur dieses Schlagwort löschen</div><div class="dz-radio-sub">Untergeordnete werden zu Europa verschoben</div></div>
</div>
<div class="dz-radio">
<div class="dz-radio-circle"></div>
<div><div class="dz-radio-label">Gesamten Teilbaum löschen</div><div class="dz-radio-sub warn">Löscht auch 6 untergeordnete Schlagwörter unwiderruflich</div></div>
</div>
</div>
<div class="dz-confirm-lbl">Gib <code>Deutschland</code> zur Bestätigung ein:</div>
<div class="dz-input">Deutschland</div>
<div class="dz-btn">Löschen</div>
</div>
</div>
<div class="save-bar"><span class="save-cancel">Abbrechen</span><span class="save-btn">Speichern</span></div>
</div>
</div>
<div class="cap">Delete guard: radio option selected + name confirmed → delete button enabled.
The name input only appears after a radio is chosen. This prevents someone from typing before reading.</div>
</div>
</div>
<!-- ══════════════════════════════════ S5: PARENT PICKER -->
<div class="sh">
<h2>5 — Tree-Aware Parent Picker</h2>
<p>Replaces the flat <code style="font-family:monospace;font-size:11px">&lt;select&gt;</code> at <code style="font-family:monospace;font-size:11px">[id]/+page.svelte:123</code>.
Each result shows the tag name plus its full ancestry path as a subtitle.
Tags that are self or descendants are excluded server-side — not shown as disabled, simply not in results.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">Picker open — query "eur"</div>
<div class="chrome" style="width:280px">
<div style="background:#fff;padding:10px;position:relative">
<div class="fct" style="margin-bottom:5px">Übergeordnetes Schlagwort</div>
<div style="position:relative">
<div class="picker-input" style="border-color:#012851;box-shadow:0 0 0 2px rgba(1,40,81,.14)">
<div class="picker-val">eur</div>
</div>
<span class="picker-clear">×</span>
</div>
<div class="picker-dropdown">
<div class="picker-opt sel">
<span class="picker-opt-dot" style="background:#c17a00"></span>
<div><div class="picker-opt-name">Europa</div><div class="picker-opt-path">Orte Europa</div></div>
</div>
<div class="picker-opt">
<span class="picker-opt-dot" style="background:#c17a00"></span>
<div><div class="picker-opt-name">Mitteleuropa</div><div class="picker-opt-path">Orte Europa Mitteleuropa</div></div>
</div>
<div class="picker-opt">
<span class="picker-opt-dot" style="background:#c17a00"></span>
<div><div class="picker-opt-name">Südeuropa</div><div class="picker-opt-path">Orte Europa Südeuropa</div></div>
</div>
</div>
</div>
</div>
<div class="cap">Path resolved client-side from the <code>tags</code> array already in layout server state — no extra API call.
Color dot matches the root ancestor's color. Keyboard: ↑↓ navigate, Enter selects, Escape closes.</div>
</div>
<div class="col">
<div class="lbl">Value selected — clear affordance + color inheritance note</div>
<div class="chrome" style="width:280px">
<div style="background:#fff;padding:10px">
<div class="fct" style="margin-bottom:5px">Übergeordnetes Schlagwort</div>
<div style="position:relative">
<div class="picker-input"><div class="picker-val">Europa</div><div class="picker-path">Orte Europa</div></div>
<span class="picker-clear">×</span>
</div>
<div style="margin-top:5px;font-size:5px;color:#6b7280;display:flex;align-items:center;gap:3px">
<span class="inh-dot" style="background:#c17a00;width:6px;height:6px;border-radius:50%;flex-shrink:0"></span>
Farbe amber von Orte wird automatisch vererbt.
</div>
</div>
</div>
<div class="cap">When a parent with a color is selected, a helper text explains color inheritance.
The color picker card hides. Clicking × restores the empty state and re-shows the color picker.</div>
</div>
<div class="col" style="max-width:260px;padding-top:28px">
<div class="annotation">
<div class="ann-bullet">B</div>
<div class="ann-text"><strong>Accessibility:</strong> <code>role="combobox"</code>, <code>aria-expanded</code>,
<code>aria-autocomplete="list"</code>, <code>aria-activedescendant</code>.
On open, focus stays on input. Backspace clears selected value before triggering search.</div>
</div>
<div class="annotation" style="margin-top:10px">
<div class="ann-bullet">C</div>
<div class="ann-text">Self and descendants excluded by the server.
The server already has cycle detection in <code>validateNoAncestorCycle()</code>
the picker simply calls <code>GET /api/tags?query=…</code> and server omits invalid parents.
No client-side exclusion list needed.</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════ S6: MERGE MODAL -->
<div class="sh">
<h2>6 — Tag Merge: Two-Step Modal</h2>
<p>Triggered from the amber merge zone in the edit panel. Step 1: pick target and see a live preview of what moves.
Step 2: confirm with a visual from/to summary. After merge, redirect to the surviving tag.</p>
</div>
<div class="grid">
<!-- Step 1 -->
<div class="col">
<div class="lbl">Step 1 of 2 — Pick target, live preview</div>
<div class="chrome" style="width:320px">
<div style="position:relative;height:400px;overflow:hidden">
<!-- Blurred page behind -->
<div class="app-header"><span class="app-logo">FAMILIENARCHIV</span></div>
<div style="flex:1;background:#f0efe9;opacity:.3;height:30px"></div>
<!-- Modal -->
<div class="modal-wrap">
<div class="modal">
<div class="modal-hd">
<div class="modal-step-lbl">Schlagwort zusammenführen · Schritt 1 von 2</div>
<div class="modal-title">Ziel-Schlagwort wählen</div>
</div>
<div class="modal-body">
<div class="modal-dots"><div class="modal-dot done"></div><div class="modal-dot active"></div></div>
<div style="font-size:5px;color:#6b7280;margin-bottom:6px;line-height:1.55"><strong style="color:#012851">Deutschland</strong> wird auf das Ziel-Schlagwort übertragen und anschließend gelöscht.</div>
<div class="fct" style="margin-bottom:4px">Ziel-Schlagwort</div>
<div style="position:relative;margin-bottom:6px">
<div class="picker-input" style="border-color:#012851;box-shadow:0 0 0 2px rgba(1,40,81,.14)"><div class="picker-val">BRD</div></div>
<span class="picker-clear">×</span>
</div>
<div class="mp">
<div class="mp-title">Vorschau der Änderungen</div>
<div class="mp-row"><span class="mp-num">11</span><span class="mp-desc">Dokumente werden zu <strong>BRD</strong> verschoben</span></div>
<div class="mp-row"><span class="mp-num">6</span><span class="mp-desc">Untergeordnete zu <strong>BRD</strong> umgehängt</span></div>
<div class="mp-sep"></div>
<div class="mp-warn">Deutschland wird danach gelöscht.</div>
</div>
</div>
<div class="modal-ft">
<span class="modal-cancel">Abbrechen</span>
<span class="modal-next">Weiter →</span>
</div>
</div>
</div>
</div>
</div>
<div class="cap">Preview card updates live as target changes. "Weiter →" disabled until a valid target is selected.
Target cannot be self, descendant, or a tag that would create a cycle.</div>
</div>
<!-- Step 2 -->
<div class="col">
<div class="lbl">Step 2 of 2 — Confirm summary</div>
<div class="chrome" style="width:320px">
<div style="position:relative;height:400px;overflow:hidden">
<div class="app-header"><span class="app-logo">FAMILIENARCHIV</span></div>
<div style="height:30px;background:#f0efe9;opacity:.3"></div>
<div class="modal-wrap">
<div class="modal">
<div class="modal-hd">
<div class="modal-step-lbl">Schlagwort zusammenführen · Schritt 2 von 2</div>
<div class="modal-title">Zusammenführen bestätigen</div>
</div>
<div class="modal-body">
<div class="modal-dots"><div class="modal-dot done"></div><div class="modal-dot done"></div></div>
<div class="ms-from"><div class="ms-lbl del">Wird gelöscht</div><span class="ms-tag del">Deutschland</span></div>
<div style="text-align:center;padding:3px 0;font-size:8px;color:#a1dcd8;background:#f5f4ef;border-left:1px solid #e4e2d7;border-right:1px solid #e4e2d7"></div>
<div class="ms-to"><div class="ms-lbl surv">Überlebt</div><span class="ms-tag surv">BRD</span></div>
<div class="ms-stats">
<div class="ms-stat"><div class="n">11</div><div class="d">Dokumente</div></div>
<div class="ms-stat"><div class="n">6</div><div class="d">Untergeordnete</div></div>
<div class="ms-stat"><div class="n">1</div><div class="d">Gelöscht</div></div>
</div>
<div style="margin-top:6px;font-size:5px;color:#6b7280;line-height:1.6;text-align:center">Diese Aktion kann nicht rückgängig gemacht werden.</div>
</div>
<div class="modal-ft">
<span class="modal-cancel">← Zurück</span>
<span class="modal-next amber">Jetzt zusammenführen</span>
</div>
</div>
</div>
</div>
</div>
<div class="cap">Confirm button amber — not red. Data is not lost, it moves to the target.
After merge: redirect to surviving tag ("BRD") with a <code>banner-ok</code> success banner in the edit panel.</div>
</div>
<div class="col" style="max-width:260px;padding-top:28px">
<div class="annotation">
<div class="ann-bullet">D</div>
<div class="ann-text"><strong>New backend endpoint:</strong> <code>POST /api/tags/{id}/merge</code> body: <code>{"targetId": "uuid"}</code>.
Atomically: (1) reassign all <code>document_tags</code> rows, (2) reparent children to targetId,
(3) delete source. Requires <code>ADMIN_TAG</code> permission. Returns surviving tag.
<code>DomainException</code> on self-merge or target-is-descendant.</div>
</div>
<div class="annotation" style="margin-top:10px">
<div class="ann-bullet">E</div>
<div class="ann-text">Modal: <code>role="dialog"</code> <code>aria-modal="true"</code> <code>aria-labelledby</code> focus trap.
<code>Escape</code> cancels and returns focus to the merge button. On error (e.g. duplicate name),
modal stays open and shows <code>banner-err</code> above the summary — no silent redirect.</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════ S7: MOBILE (375px) -->
<div class="sh">
<h2>7 — Mobile (375px): EntityNav hidden, full-screen tree → edit</h2>
<p>On mobile the EntityNav is <code style="font-family:monospace;font-size:11px">hidden md:flex</code> — completely gone (existing behaviour).
The tag section shows either the tree list or the edit form full-screen, same as today.
The tree list gets the same depth-aware nodes; the edit form gets breadcrumb and the new zones.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">375px — Tree list (full screen)</div>
<div class="phone">
<div class="phone-status">9:41<span>●●●</span></div>
<div class="phone-body">
<div class="app-header"><span class="app-logo">FAMILIENARCHIV</span><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div style="height:24px;background:#f5f4ef;border-bottom:1px solid #e4e2d7;display:flex;align-items:center;padding:0 8px">
<span style="font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#6b7280">Alle Schlagwörter</span>
</div>
<div style="flex:1;overflow-y:auto;background:#f5f4ef">
<div class="tn d0"><span class="tn-chevron open"></span><span class="tn-dot" style="background:#c17a00"></span><span class="tn-name">Orte</span><span class="tn-count">(38)</span></div>
<div class="tn d1"><span class="tn-chevron open"></span><span class="tn-name">Europa</span><span class="tn-count">(22)</span></div>
<div class="tn d2"><span class="tn-chevron leaf"></span><span class="tn-name">Deutschland</span><span class="tn-count">(11)</span></div>
<div class="tn d2"><span class="tn-chevron leaf"></span><span class="tn-name">Frankreich</span><span class="tn-count">(5)</span></div>
<div class="tn d1"><span class="tn-chevron"></span><span class="tn-name">Asien</span><span class="tn-count">(8)</span></div>
<div class="tn d0"><span class="tn-chevron"></span><span class="tn-dot" style="background:#7a4f9a"></span><span class="tn-name">Personen</span><span class="tn-count">(54)</span></div>
<div class="tn d0"><span class="tn-chevron open"></span><span class="tn-dot" style="background:#3060b0"></span><span class="tn-name">Ereignisse</span><span class="tn-count">(19)</span></div>
<div class="tn d1"><span class="tn-chevron leaf"></span><span class="tn-name">Hochzeiten</span><span class="tn-count">(7)</span></div>
</div>
</div>
</div>
<div class="cap">Mobile: EntityNav hidden. Tree list is full-width. No collapse button on mobile. Tapping a node navigates to the full-screen edit.</div>
</div>
<div class="col">
<div class="lbl">375px — Edit form (full screen)</div>
<div class="phone">
<div class="phone-status">9:41<span>●●●</span></div>
<div class="phone-body">
<div class="app-header"><span class="app-logo">FAMILIENARCHIV</span></div>
<div class="ep-header" style="height:30px">
<span class="ep-back"> Schlagwörter</span>
<span class="ep-title">Deutschland</span>
</div>
<div style="flex:1;overflow-y:auto;padding:7px 9px">
<div class="breadcrumb"><a class="bc-link">Orte</a><span class="bc-sep"></span><a class="bc-link">Europa</a><span class="bc-sep"></span><span class="bc-cur">Deutschland</span></div>
<div class="form-card"><div class="fct">Name</div><div class="form-input">Deutschland</div></div>
<div class="form-card">
<div class="fct">Übergeordnetes Schlagwort</div>
<div style="position:relative"><div class="picker-input"><div class="picker-val">Europa</div><div class="picker-path">Orte Europa</div></div><span class="picker-clear">×</span></div>
</div>
<div class="form-card"><div class="fct">Untergeordnete (6)</div><div class="children-chips"><span class="child-chip">Berlin<span class="chip-count">(3)</span></span><span class="child-chip">München<span class="chip-count">(4)</span></span><span class="child-more">… und 4 weitere →</span></div></div>
<div class="merge-zone"><div class="mz-title">Zusammenführen</div><div class="mz-btn">⇒ Zusammenführen …</div></div>
<div class="danger-zone"><div class="dz-title">Löschen</div><div class="dz-impact"><strong>11 Dok.</strong> · <strong>6 Untergeordnete</strong></div><div class="dz-btn disabled" style="margin-top:5px">Löschen</div></div>
</div>
<div class="save-bar"><span class="save-cancel">Abbrechen</span><span class="save-btn">Speichern</span></div>
</div>
</div>
<div class="cap">Mobile edit: back link " Schlagwörter" returns to tree list. All touch targets min 44×44px real. Merge modal opens full-screen on mobile.</div>
</div>
</div>
<!-- ══════════════════════════════════ IMPL-REF -->
<div class="sh">
<h2>Implementation Reference</h2>
<p>Exact Tailwind classes, real pixel values, and file locations for every changed element.
Green rows are new. Rows without colour are updates to existing elements.</p>
</div>
<div class="impl-ref">
<table>
<tr>
<th style="width:22%">Element</th>
<th style="width:38%">Tailwind Classes</th>
<th style="width:15%">Real px / value</th>
<th>Notes / file</th>
</tr>
<!-- EntityNav — unchanged -->
<tr>
<td><strong>EntityNav</strong> (unchanged)</td>
<td><code>md:w-12 lg:w-30 flex-shrink-0 bg-brand-navy</code></td>
<td>48px tablet / 120px desktop</td>
<td>No changes. <code>EntityNav.svelte</code></td>
</tr>
<!-- Tree panel -->
<tr>
<td><strong>Tree panel — expanded</strong></td>
<td><code>w-60 flex-shrink-0 bg-surface border-r border-line flex flex-col overflow-hidden</code></td>
<td>240px (was 200px)</td>
<td>Widen +40px to fit depth-3+ labels. <code>TagsListPanel.svelte</code></td>
</tr>
<tr>
<td>Tree panel — collapsed</td>
<td><code>w-8 flex-shrink-0 bg-surface border-r border-line flex flex-col items-center cursor-pointer</code></td>
<td>32px</td>
<td>Click handle to re-expand. Per-tag state in <code>localStorage</code>.</td>
</tr>
<tr>
<td>Tree node row</td>
<td><code>flex items-center h-9 pr-2 cursor-pointer border-l-2 border-transparent hover:bg-muted transition-colors</code></td>
<td>height: 36px</td>
<td>Row is split: chevron button + link are siblings, not nested</td>
</tr>
<tr>
<td>Tree node — active</td>
<td><code>border-l-primary bg-primary/[0.09]</code></td>
<td>2px left border, 9% navy bg</td>
<td><code>aria-current="page"</code> on the <code>&lt;a&gt;</code></td>
</tr>
<tr>
<td>Depth indent (per level)</td>
<td>d0: <code>pl-[4px]</code> d1: <code>pl-[16px]</code> d2: <code>pl-[28px]</code> d3: <code>pl-[40px]</code> d4+: <code>pl-[52px]</code></td>
<td>12px per level</td>
<td>Computed from <code>TagTreeNodeDTO</code> depth. Labels: <code>truncate</code></td>
</tr>
<tr>
<td>Chevron button</td>
<td><code>w-4 h-9 flex items-center justify-center text-ink-3 hover:text-ink flex-shrink-0 transition-transform</code></td>
<td>16×36px (full row height)</td>
<td><code>aria-expanded</code> + <code>aria-controls="{id}-children"</code>. SVG icon, not text.</td>
</tr>
<tr>
<td>Color dot (root tags)</td>
<td><code>w-2 h-2 rounded-full flex-shrink-0 mr-1.5</code> + inline <code>style="background-color: var(--c-tag-{color})"</code></td>
<td>8×8px</td>
<td>Root tags with a color only. Child tags: no dot in tree.</td>
</tr>
<tr>
<td>Doc count suffix</td>
<td><code>text-[11px] font-normal text-ink-3 ml-1 flex-shrink-0</code></td>
<td>11px</td>
<td>From <code>TagTreeNodeDTO.documentCount</code>. Format: <code>(12)</code></td>
</tr>
<!-- Breadcrumb -->
<tr>
<td><strong>Ancestry breadcrumb</strong></td>
<td><code>flex items-center flex-wrap gap-1.5 mb-3 px-3 py-2 bg-muted rounded-sm border border-line text-xs</code></td>
<td>12px, 8px/12px padding</td>
<td>Root: single non-linked span. <code>aria-label="Schlagwort-Pfad"</code></td>
</tr>
<tr>
<td>Breadcrumb link</td>
<td><code>text-primary font-semibold hover:underline underline-offset-2 focus-visible:ring-1 focus-visible:ring-focus-ring rounded-sm</code></td>
<td></td>
<td>Real <code>&lt;a href="/admin/tags/{id}"&gt;</code></td>
</tr>
<!-- Parent picker -->
<tr>
<td><strong>Parent picker</strong> (replaces <code>&lt;select&gt;</code>)</td>
<td><code>w-full border border-line rounded-sm bg-muted px-3 py-2.5 text-sm text-ink cursor-pointer focus-visible:ring-2 focus-visible:ring-focus-ring outline-none</code></td>
<td>height: ~40px; 14px font</td>
<td>Custom combobox. <code>role="combobox"</code> <code>aria-expanded</code> <code>aria-autocomplete="list"</code></td>
</tr>
<tr>
<td>Picker dropdown option</td>
<td><code>flex items-start gap-2 px-3 py-2.5 hover:bg-muted cursor-pointer</code></td>
<td>min-height: 44px (two-line)</td>
<td>Path subtitle: <code>text-[11px] text-ink-3 mt-0.5</code></td>
</tr>
<!-- Inherited color -->
<tr>
<td><strong>Inherited color indicator</strong></td>
<td><code>flex items-center gap-2 mt-2 px-3 py-2 bg-muted border border-line rounded-sm text-xs text-ink-2</code></td>
<td>12px font</td>
<td>Only on child tags. Replaces color picker when parent is set.</td>
</tr>
<!-- Children chips -->
<tr>
<td><strong>Children chip</strong></td>
<td><code>inline-flex items-center gap-1.5 px-2.5 py-1 bg-muted border border-line rounded-full text-[11px] font-medium text-ink-2 hover:bg-canvas transition-colors</code></td>
<td>11px, pill shape</td>
<td>Wrapped in <code>&lt;a href="/admin/tags/{id}"&gt;</code></td>
</tr>
<!-- Merge zone -->
<tr>
<td><strong>Merge zone</strong></td>
<td><code>border border-amber-200 bg-amber-50 rounded-sm p-4 mb-4</code></td>
<td>16px padding</td>
<td>Amber = recoverable. Data moves to target, nothing is lost.</td>
</tr>
<tr>
<td>Merge button</td>
<td><code>inline-flex items-center gap-2 px-4 py-2.5 bg-amber-800 text-amber-50 rounded-sm text-xs font-bold uppercase tracking-widest hover:opacity-80 min-h-[44px]</code></td>
<td>min 44px height</td>
<td></td>
</tr>
<!-- Merge modal -->
<tr>
<td><strong>Merge modal backdrop</strong></td>
<td><code>fixed inset-0 bg-ink/40 flex items-center justify-center z-50</code></td>
<td>40% overlay</td>
<td><code>role="dialog"</code> <code>aria-modal="true"</code>. Focus trap. Escape cancels.</td>
</tr>
<tr>
<td>Modal confirm button (Step 2)</td>
<td><code>px-5 py-2.5 bg-amber-800 text-amber-50 rounded-sm text-xs font-bold uppercase tracking-widest min-h-[44px]</code></td>
<td>Amber</td>
<td>On success: redirect to surviving tag + <code>banner-ok</code></td>
</tr>
<!-- Delete guard -->
<tr>
<td><strong>Delete impact summary</strong></td>
<td><code>bg-red-50 border border-red-200 rounded-sm px-3 py-2 mb-3 text-xs text-red-800</code></td>
<td>12px</td>
<td>Counts from tree data already in page state — no extra API fetch needed</td>
</tr>
<tr>
<td>Delete radio option</td>
<td><code>flex items-start gap-3 p-3 border border-red-200 bg-white rounded-sm cursor-pointer hover:bg-red-50 mb-2 min-h-[44px]</code></td>
<td>min 44px</td>
<td>Custom radio. Real <code>&lt;input type="radio"&gt;</code> visually hidden + styled indicator.</td>
</tr>
<tr>
<td>Delete name input</td>
<td><code>hidden</code> until radio selected, then <code>w-full border border-red-200 rounded-sm bg-white px-3 py-2 text-sm focus:ring-1 focus:ring-red-400 outline-none</code></td>
<td></td>
<td>Input appears only after a radio is chosen. Prevents pre-filling before reading the warning.</td>
</tr>
<tr>
<td>Delete confirm button</td>
<td><code>bg-danger text-danger-fg rounded-sm px-4 py-2.5 text-xs font-bold uppercase tracking-widest hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed min-h-[44px]</code></td>
<td>min 44px. <code>--c-danger: #c0392b</code></td>
<td>Enabled only when radio is selected AND name input matches exactly</td>
</tr>
<!-- New backend endpoint -->
<tr class="new">
<td><strong class="new">NEW: POST /api/tags/{id}/merge</strong></td>
<td>Body: <code>{"targetId": "uuid"}</code></td>
<td>Returns: surviving <code>Tag</code></td>
<td>Permission: <code>ADMIN_TAG</code>. Atomic transaction. <code>DomainException</code> on self-merge or descendant target.</td>
</tr>
</table>
</div>
</div><!-- /page -->
</body>
</html>