1208 lines
71 KiB
HTML
1208 lines
71 KiB
HTML
<!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 & 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 3–4</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><button aria-expanded></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><a></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"><select></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><a></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><a href="/admin/tags/{id}"></code></td>
|
||
</tr>
|
||
<!-- Parent picker -->
|
||
<tr>
|
||
<td><strong>Parent picker</strong> (replaces <code><select></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><a href="/admin/tags/{id}"></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><input type="radio"></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>
|