aether-framework/radial/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

295 lines
14 KiB
HTML
Raw Permalink 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="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Aether — Radial-Menü</title>
<style>
:root{
--fg:#f2f0fa; /* Standard-Textfarbe */
--muted:#a09bbd; /* gedämpfter Text */
--slice:rgba(18,15,30,0.82); /* Grundfarbe der Segmente */
--hub:rgba(16,13,26,0.92); /* Naben-Hintergrund */
--border:rgba(255,255,255,0.12);/* dezente Rahmen */
--accent:#7c5cf6; /* Aether-Akzent (Violett) */
--accent2:#5b8cff; /* zweiter Akzent (Blau) */
}
*{box-sizing:border-box;}
html,body{margin:0;height:100%;overflow:hidden;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);}
/* ============================================================
STILISIERTE SPIELSZENE (Kulisse hinter dem Menü)
============================================================ */
.scene{position:fixed;inset:0;z-index:0;overflow:hidden;
background:linear-gradient(180deg,#141232 0%,#2a2350 34%,#5a3a66 54%,#b07a52 66%,#2a2236 66%);}
.scene .city{position:absolute;left:0;right:0;bottom:28%;height:30%;
background:linear-gradient(90deg,#201a38 0 8%,transparent 8% 13%,#2a2148 13% 21%,transparent 21% 26%,
#1c1730 26% 34%,transparent 34% 40%,#2a2148 40% 47%,transparent 47% 54%,#241d40 54% 62%,transparent 62% 68%,
#201a38 68% 77%,transparent 77% 83%,#2a2148 83% 91%,transparent 91%);}
.scene .ground{position:absolute;left:0;right:0;bottom:0;height:28%;background:linear-gradient(180deg,#161226,#0a0814);}
/* Abdunkelnde Ebene, wenn das Menü offen ist */
.dim{position:fixed;inset:0;z-index:1;background:rgba(6,5,12,0.55);backdrop-filter:blur(3px);
opacity:0;transition:.25s;pointer-events:none;}
.dim.on{opacity:1;}
/* ============================================================
RADIAL-RAD
============================================================ */
.stage{position:fixed;inset:0;z-index:2;display:grid;place-items:center;}
.wheel{position:relative;width:min(440px,88vw);aspect-ratio:1;opacity:0;transform:scale(0.9);
transition:.2s cubic-bezier(.2,.9,.3,1);pointer-events:none;}
.wheel.open{opacity:1;transform:scale(1);pointer-events:auto;}
.wheel svg{width:100%;height:100%;display:block;filter:drop-shadow(0 24px 50px rgba(0,0,0,0.6));}
/* Einzelnes Kuchen-Segment */
.slice{fill:var(--slice);stroke:rgba(0,0,0,0.35);stroke-width:1.2;cursor:pointer;transition:fill .12s;}
.slice:hover{fill:var(--c,var(--accent));} /* --c = Kategorie-Farbe */
.slice.parent:hover{fill:var(--c,var(--accent));}
.lbl-i{font-size:27px;pointer-events:none;dominant-baseline:central;text-anchor:middle;}
.lbl-t{font-size:12.5px;font-weight:700;fill:var(--fg);pointer-events:none;text-anchor:middle;
paint-order:stroke;stroke:rgba(0,0,0,0.45);stroke-width:2.4px;}
.chev{font-size:11px;fill:rgba(255,255,255,0.55);pointer-events:none;text-anchor:middle;} /* ›-Marker bei Untermenüs */
/* Nabe in der Mitte */
.hub{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:38%;aspect-ratio:1;border-radius:50%;
background:var(--hub);border:1px solid var(--border);display:grid;place-items:center;text-align:center;
backdrop-filter:blur(8px);box-shadow:inset 0 0 30px rgba(0,0,0,0.5);}
.hub.back{cursor:pointer;}
.hub .hi{font-size:32px;line-height:1;}
.hub .ht{font-size:14px;font-weight:800;margin-top:5px;padding:0 8px;}
.hub .hs{font-size:10.5px;color:var(--muted);margin-top:3px;font-weight:600;padding:0 10px;}
.hub.back .hs{color:var(--accent);}
/* Hinweis, wenn das Menü geschlossen ist */
.closed{position:fixed;inset:0;z-index:2;display:grid;place-items:center;}
.closed .card{background:var(--hub);border:1px solid var(--border);border-radius:18px;padding:26px 30px;text-align:center;
backdrop-filter:blur(10px);box-shadow:0 24px 50px -20px rgba(0,0,0,0.7);}
.closed .card .ci{font-size:40px;}
.closed .card h2{margin:10px 0 4px;font-size:20px;font-weight:800;}
.closed .card p{margin:0 0 16px;color:var(--muted);font-size:13.5px;}
.kbd{display:inline-block;background:rgba(255,255,255,0.1);border:1px solid var(--border);border-bottom-width:2px;
border-radius:7px;padding:2px 9px;font-weight:800;font-size:13px;font-family:inherit;}
.btn{border:none;border-radius:11px;padding:11px 20px;font-family:inherit;font-size:14px;font-weight:800;cursor:pointer;
background:var(--accent);color:#160c2e;transition:.12s;}
.btn:hover{filter:brightness(1.08);}
.wheel.open ~ .closed{display:none;}
/* Bestätigungs-Toast nach einer Aktion */
.toast{position:fixed;left:50%;bottom:34px;transform:translateX(-50%) translateY(16px);z-index:5;
background:var(--hub);border:1px solid var(--border);border-radius:12px;padding:12px 22px;font-size:14px;font-weight:700;
backdrop-filter:blur(10px);opacity:0;pointer-events:none;transition:.28s;box-shadow:0 16px 40px -16px rgba(0,0,0,0.7);}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
/* Fußzeilen-Hinweis (nur Prototyp) */
.foot{position:fixed;left:0;right:0;bottom:12px;z-index:4;text-align:center;color:var(--muted);font-size:11.5px;pointer-events:none;}
.foot .kbd{pointer-events:none;}
</style>
</head>
<body>
<!-- Kulisse -->
<div class="scene"><div class="city"></div><div class="ground"></div></div>
<div class="dim" id="dim"></div>
<!-- Rad + geschlossener Hinweis -->
<div class="stage">
<div class="wheel" id="wheel">
<svg id="svg" viewBox="0 0 400 400"></svg>
<div class="hub" id="hub"></div>
</div>
<div class="closed" id="closed">
<div class="card">
<div class="ci">🎯</div>
<h2>Interaktionsmenü</h2>
<p>Halte <span class="kbd">G</span> gedrückt, um das Rad zu öffnen.</p>
<button class="btn" id="openBtn">Menü öffnen</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<div class="foot"><span class="kbd">G</span> Menü öffnen/schließen · Klick auf ein Segment wählt · Mitte = zurück</div>
<script>
"use strict";
/* Kurzer Zugriffshelfer auf Elemente per ID */
const $=id=>document.getElementById(id);
/* =====================================================================
MENÜ-DATEN (Kategorien mit optionalen Untermenüs)
Jede Aktion ist bewusst kosmetisch — in FiveM lösen sie NUI-Callbacks aus.
===================================================================== */
const menue=[
{icon:"🙌", label:"Interaktion", farbe:"#7c5cf6", kinder:[
{icon:"🙌", label:"Hände hoch"},
{icon:"👉", label:"Zeigen"},
{icon:"🧎", label:"Knien"},
{icon:"👋", label:"Winken"},
{icon:"🪪", label:"Ausweis zeigen"}, // greift auf documents/ zurück
]},
{icon:"🚗", label:"Fahrzeug", farbe:"#5b8cff", kinder:[
{icon:"🔒", label:"Ab-/Aufschließen"},
{icon:"⚙️", label:"Motor an/aus"},
{icon:"🧳", label:"Kofferraum"},
{icon:"🚪", label:"Türen"},
{icon:"💺", label:"Sitzplatz"},
]},
{icon:"🎭", label:"Emotes", farbe:"#e06fae", kinder:[
{icon:"🪑", label:"Sitzen"},
{icon:"💃", label:"Tanzen"},
{icon:"🚬", label:"Rauchen"},
{icon:"🥤", label:"Trinken"},
{icon:"🧍", label:"Lehnen"},
{icon:"🛑", label:"Stopp / Reset"},
]},
{icon:"💼", label:"Job", farbe:"#34d39a", kinder:[
{icon:"🟢", label:"Dienst an/aus"}, // greift auf jobs/ zurück
{icon:"🧾", label:"Rechnung stellen"},
{icon:"📦", label:"Auftrag annehmen"},
{icon:"📋", label:"Job-Aktion"},
]},
{icon:"🆘", label:"Notdienst", farbe:"#f0506e", kinder:[
{icon:"🚓", label:"Polizei rufen"}, // greift auf faction/ Dispatch zurück
{icon:"🚑", label:"Rettung rufen"},
{icon:"🚨", label:"Panik / 10-13"},
]},
{icon:"🎒", label:"Inventar", farbe:"#f5a742", aktion:"Inventar geöffnet"}, // greift auf inventory/ zurück
{icon:"📱", label:"Handy", farbe:"#3fc7e0", aktion:"Handy geöffnet"}, // greift auf phone/ zurück
];
/* =====================================================================
ZUSTAND
===================================================================== */
let offen=false; // ist das Rad geöffnet?
let aktuelleKinder=null; // aktives Untermenü (null = Wurzelebene)
let aktuelleFarbe=null; // Kategorie-Farbe des aktiven Untermenüs
let elternLabel=""; // Beschriftung der übergeordneten Kategorie
/* Geometrie des Rads (im 400×400-Koordinatensystem) */
const CX=200, CY=200, R_AUSSEN=188, R_INNEN=84, GAP=2.4;
const R_ICON=(R_AUSSEN+R_INNEN)/2; // Radius für Icon/Beschriftung
/* Punkt auf einem Kreis: Winkel in Grad, von oben im Uhrzeigersinn gemessen */
function polar(r,grad){const t=grad*Math.PI/180;return [CX+r*Math.sin(t), CY-r*Math.cos(t)];}
/* Baut den SVG-Pfad eines Donut-Segments zwischen zwei Winkeln */
function segmentPfad(t0,t1){
const [x1,y1]=polar(R_AUSSEN,t0), [x2,y2]=polar(R_AUSSEN,t1);
const [x3,y3]=polar(R_INNEN,t1), [x4,y4]=polar(R_INNEN,t0);
const gross=(t1-t0)>180?1:0; // große-Bogen-Flagge
return `M${x1.toFixed(2)} ${y1.toFixed(2)} A${R_AUSSEN} ${R_AUSSEN} 0 ${gross} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`
+` L${x3.toFixed(2)} ${y3.toFixed(2)} A${R_INNEN} ${R_INNEN} 0 ${gross} 0 ${x4.toFixed(2)} ${y4.toFixed(2)} Z`;
}
/* =====================================================================
RENDERING
===================================================================== */
/* Liefert die aktuell anzuzeigende Knotenliste (Wurzel oder Untermenü) */
function aktuelleListe(){return aktuelleKinder||menue;}
/* Zeichnet das komplette Rad (Segmente + Nabe) für die aktuelle Ebene */
function zeichneRad(){
const liste=aktuelleListe();
const n=liste.length, seg=360/n;
let svg="";
liste.forEach((node,i)=>{
// Segment 0 mittig oben; Lücke (GAP) zwischen den Segmenten
const basis=i*seg-seg/2;
const t0=basis+GAP/2, t1=basis+seg-GAP/2;
const farbe=aktuelleKinder?aktuelleFarbe:node.farbe;
const [ix,iy]=polar(R_ICON,i*seg); // Mittelwinkel = i*seg
svg+=`<path class="slice ${node.kinder?'parent':''}" style="--c:${farbe}" data-i="${i}" d="${segmentPfad(t0,t1)}"></path>`;
svg+=`<text class="lbl-i" x="${ix.toFixed(1)}" y="${(iy-9).toFixed(1)}">${node.icon}</text>`;
svg+=`<text class="lbl-t" x="${ix.toFixed(1)}" y="${(iy+16).toFixed(1)}">${node.label}</text>`;
// Kleiner ›-Hinweis, dass sich ein Untermenü öffnet
if(node.kinder){const [cx,cy]=polar(R_ICON,i*seg);svg+=`<text class="chev" x="${cx.toFixed(1)}" y="${(cy+29).toFixed(1)}">▸</text>`;}
});
$("svg").innerHTML=svg;
// Segment-Ereignisse verknüpfen (Hover aktualisiert die Nabe, Klick wählt)
$("svg").querySelectorAll(".slice").forEach(p=>{
const node=liste[+p.dataset.i];
p.addEventListener("mouseenter",()=>zeigeNabe(node));
p.addEventListener("mouseleave",standardNabe);
p.addEventListener("click",()=>waehle(node));
});
standardNabe();
}
/* Zeigt in der Nabe das gerade überfahrene Segment */
function zeigeNabe(node){
$("hub").className="hub"+(aktuelleKinder?" back":"");
$("hub").innerHTML=`<div><div class="hi">${node.icon}</div><div class="ht">${node.label}</div>
<div class="hs">${node.kinder?"Untermenü öffnen ▸":(aktuelleKinder?"↩ Klick Mitte = zurück":"Auswählen")}</div></div>`;
}
/* Setzt die Nabe auf ihren Standardtext der aktuellen Ebene zurück */
function standardNabe(){
if(aktuelleKinder){
$("hub").className="hub back";
$("hub").innerHTML=`<div><div class="hi">↩</div><div class="ht">${elternLabel}</div><div class="hs">Zurück</div></div>`;
} else {
$("hub").className="hub";
$("hub").innerHTML=`<div><div class="hi">🎯</div><div class="ht">Aktionen</div><div class="hs">Kategorie wählen</div></div>`;
}
}
/* =====================================================================
NAVIGATION & AKTIONEN
===================================================================== */
/* Reaktion auf die Auswahl eines Segments */
function waehle(node){
if(node.kinder){
// In das Untermenü wechseln
aktuelleKinder=node.kinder; aktuelleFarbe=node.farbe; elternLabel=node.label;
zeichneRad();
} else {
// Blatt-Aktion ausführen, Rückmeldung geben und Rad schließen
fuehreAus(node);
}
}
/* Führt eine Blatt-Aktion aus (hier kosmetisch: Toast + schließen) */
function fuehreAus(node){
const text=node.aktion||node.label;
toast(node.icon+" "+text);
setOffen(false);
}
/* Klick auf die Nabe: aus dem Untermenü zurück zur Wurzel */
$("hub").addEventListener("click",()=>{
if(aktuelleKinder){aktuelleKinder=null;aktuelleFarbe=null;zeichneRad();}
});
/* Öffnet/schließt das Rad; beim Schließen zurück auf die Wurzelebene */
function setOffen(b){
offen=b;
$("wheel").classList.toggle("open",b);
$("dim").classList.toggle("on",b);
$("closed").style.display=b?"none":"grid";
if(b){aktuelleKinder=null;aktuelleFarbe=null;zeichneRad();}
}
/* Kurze Bestätigungs-Einblendung */
let toastTimer=null;
function toast(text){const e=$("toast");e.textContent=text;e.classList.add("show");
clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2200);}
/* =====================================================================
STEUERUNG (Maus + Tastatur)
===================================================================== */
$("openBtn").addEventListener("click",()=>setOffen(true));
// Taste G schaltet das Rad um (verhindert Dauerfeuer beim Halten)
addEventListener("keydown",e=>{
if(e.repeat)return;
if(e.key==="g"||e.key==="G")setOffen(!offen);
if(e.key==="Escape")setOffen(false);
});
/* =====================================================================
START (geschlossen)
===================================================================== */
setOffen(false);
</script>
</body>
</html>