Files
Void-Homelab/docs/mockups/dross-chat.html
root 0a39b1166f docs(dross): floating Dross chat design spec + mockup
Brainstormed design: global floating bubble companion (replaces the
per-Space right rail), draggable orb+panel, bottom collapse + top close,
3 selectable violet avatars, tunable persona, local faster-whisper voice
(CT 102) review-and-send. Phased P1 UI/global → P2 voice → P3 modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:39:47 +10:00

222 lines
15 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, maximum-scale=1">
<title>Dross floating chat — mockup v2</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600&family=Cormorant+Garamond:ital@0;1&family=JetBrains+Mono&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0a0e; --panel:#14141c; --panel-2:#1c1c26; --border:#2a2a36;
--text:#e8e6ed; --muted:#888094; --accent:#ff4f2e;
--dross:#a86adf; --dross-dim:#5a2e8a; --dross-soft:#1e1030; --dross-glow:#c79bff;
--font-display:'Cinzel',serif; --font-body:'Cormorant Garamond',serif; --font-mono:'JetBrains Mono',monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{background:radial-gradient(80% 60% at 50% 0%, #16131b 0%, #0a0a0e 60%),var(--bg);color:var(--text);
font-family:var(--font-mono);min-height:100%;overflow-x:hidden;padding:18px 16px 120px}
h2{font-family:var(--font-display);letter-spacing:.16em;text-transform:uppercase;font-size:13px;color:#cdbbe6;margin:6px 0 4px}
.sub{color:var(--muted);font-size:11px;margin-bottom:14px}
/* ---------- avatar options ---------- */
.avs{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-bottom:26px}
.avcard{border:1px solid var(--border);border-radius:14px;padding:16px 10px 12px;display:flex;flex-direction:column;align-items:center;gap:9px;
background:linear-gradient(160deg,#16131a,#0f0d12)}
.avname{font-family:var(--font-mono);font-size:11px;color:#cdbbe6;letter-spacing:.05em}
.avdesc{font-family:var(--font-body);font-size:13px;color:var(--muted);text-align:center;line-height:1.25}
.orb{width:62px;height:62px;border-radius:50%;position:relative;flex:none;
background:radial-gradient(circle at 38% 30%, #2a1640, #1a0f2a 70%, #120a1e);
box-shadow:0 0 0 1px #ffffff12, 0 6px 22px -6px #000, 0 0 26px -4px var(--dross-dim);
display:grid;place-items:center;overflow:hidden;animation:bob 5s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
/* A — soft eye (friendlier) */
.a-eye{width:34px;height:34px;border-radius:50%;background:radial-gradient(circle at 50% 40%, #2a1c3a, #140b20);
display:grid;place-items:center;box-shadow:inset 0 0 10px #000}
.a-pupil{width:15px;height:15px;border-radius:50%;position:relative;
background:radial-gradient(circle at 38% 32%, #fff, var(--dross-glow) 50%, var(--dross) 100%);
box-shadow:0 0 10px var(--dross-glow);animation:look 7s ease-in-out infinite}
.a-pupil::after{content:"";position:absolute;right:2px;bottom:3px;width:4px;height:4px;border-radius:50%;background:#fff;opacity:.8}
@keyframes look{0%,45%{transform:translate(0,0)}58%{transform:translate(4px,-2px)}72%{transform:translate(-3px,1px)}88%,100%{transform:translate(0,0)}}
/* B — wisp / plasma core */
.b-core{position:absolute;inset:8px;border-radius:50%;
background:conic-gradient(from 0deg, var(--dross-dim), var(--dross-glow), var(--dross), var(--dross-soft), var(--dross-dim));
filter:blur(3px);animation:spin 7s linear infinite}
.b-bright{position:absolute;inset:20px;border-radius:50%;background:radial-gradient(circle,#fff,var(--dross-glow) 60%,transparent 75%);
animation:pulse 3s ease-in-out infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:.55;transform:scale(.9)}50%{opacity:1;transform:scale(1.1)}}
/* C — rune sigil */
.c-sigil{width:30px;height:30px;filter:drop-shadow(0 0 6px var(--dross-glow));animation:sigil 8s ease-in-out infinite}
@keyframes sigil{0%,100%{opacity:.7;transform:rotate(0)}50%{opacity:1;transform:rotate(20deg)}}
/* D — orbiting motes */
.d-core{width:14px;height:14px;border-radius:50%;background:radial-gradient(circle,#fff,var(--dross-glow) 60%,var(--dross));box-shadow:0 0 12px var(--dross-glow)}
.d-ring{position:absolute;inset:0;animation:spin 5s linear infinite}
.d-ring.r2{animation-duration:8s;animation-direction:reverse}
.d-mote{position:absolute;top:7px;left:50%;width:7px;height:7px;margin-left:-3.5px;border-radius:50%;background:var(--dross-glow);box-shadow:0 0 8px var(--dross-glow)}
.d-ring.r2 .d-mote{top:auto;bottom:9px;background:var(--dross);width:5px;height:5px}
/* ---------- live orb (bottom-right, draggable) ---------- */
#live{position:fixed;right:20px;bottom:20px;cursor:grab;z-index:40;touch-action:none}
#live:active{cursor:grabbing}
.ping{position:absolute;right:-2px;top:-2px;width:17px;height:17px;border-radius:50%;background:var(--accent);
color:#0a0a0e;font-size:10px;display:grid;place-items:center;box-shadow:0 0 0 2px var(--bg);z-index:2}
/* ---------- panel ---------- */
.panel{position:fixed;right:20px;bottom:20px;width:340px;max-width:calc(100vw - 24px);height:480px;max-height:calc(100vh - 24px);
display:none;flex-direction:column;z-index:41;border:1px solid var(--dross-dim);border-radius:16px;overflow:hidden;
background:linear-gradient(180deg, rgba(30,16,48,.6), rgba(20,20,28,.96) 22%);
box-shadow:0 24px 70px -18px #000, 0 0 0 1px #00000060, 0 0 40px -16px var(--dross-dim);backdrop-filter:blur(6px)}
.panel.open{display:flex;animation:rise .18s ease}
@keyframes rise{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}
.hd{display:flex;align-items:center;gap:10px;padding:11px 12px;cursor:grab;
background:linear-gradient(180deg, var(--dross-soft), transparent);border-bottom:1px solid var(--border);touch-action:none}
.mini{width:30px;height:30px;border-radius:50%;flex:none;position:relative;overflow:hidden;
background:radial-gradient(circle at 38% 30%, #2a1640, #160d24)}
.mini .b-core{inset:4px;filter:blur(2px)}.mini .b-bright{inset:11px}
.who{font-family:var(--font-display);letter-spacing:.16em;text-transform:uppercase;font-size:13px;color:#efe9f6;flex:1}
.who small{display:block;font-family:var(--font-mono);letter-spacing:0;text-transform:none;font-size:10px;color:var(--dross-glow);opacity:.85}
.xbtn{background:none;border:0;color:var(--muted);font-size:18px;cursor:pointer;padding:4px 6px}
.xbtn:hover{color:var(--text)}
.log{flex:1;overflow:auto;padding:14px 13px;display:flex;flex-direction:column;gap:11px}
.msg{max-width:88%;padding:9px 12px;border-radius:13px;font-family:var(--font-body);font-size:16px;line-height:1.35}
.msg.d{align-self:flex-start;background:var(--dross-soft);border:1px solid var(--dross-dim);border-bottom-left-radius:4px;color:#efe9f6}
.msg.u{align-self:flex-end;background:var(--panel-2);border:1px solid var(--border);border-bottom-right-radius:4px}
.msg .nm{font-family:var(--font-mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--dross-glow);opacity:.7;margin-bottom:2px}
/* input: textarea on top, big mic + send BELOW */
.inwrap{padding:10px;border-top:1px solid var(--border);background:#0d0a12;display:flex;flex-direction:column;gap:9px}
textarea{width:100%;background:#0d0d13;border:1px solid var(--border);border-radius:10px;padding:10px 12px;color:var(--text);
font-family:var(--font-mono);font-size:13px;resize:none;height:46px;max-height:96px}
.btnrow{display:flex;gap:10px}
.mic{flex:1;height:50px;border-radius:12px;border:1px solid var(--dross-dim);background:var(--dross-soft);color:var(--dross-glow);
cursor:pointer;display:flex;align-items:center;justify-content:center;gap:9px;font-family:var(--font-ui),sans-serif;font-size:13px;letter-spacing:.02em}
.mic:hover{border-color:var(--dross)}
.mic.rec{background:#3a1010;border-color:var(--accent);color:#fff;animation:recpulse 1.2s infinite}
@keyframes recpulse{0%,100%{box-shadow:0 0 0 0 rgba(255,79,46,.5)}50%{box-shadow:0 0 0 8px rgba(255,79,46,0)}}
.send{width:64px;height:50px;border-radius:12px;border:1px solid var(--dross-dim);background:linear-gradient(180deg,var(--dross),var(--dross-dim));
color:#fff;cursor:pointer;display:grid;place-items:center}
.send:hover{filter:brightness(1.1)}
/* bottom collapse handle — thumb-friendly minimise-to-orb */
.collapsebar{display:flex;align-items:center;justify-content:center;gap:8px;height:34px;cursor:pointer;
color:var(--muted);font-family:var(--font-ui),sans-serif;font-size:11px;letter-spacing:.12em;text-transform:uppercase;
background:#0b0810;border-top:1px solid var(--border)}
.collapsebar:hover{color:var(--dross-glow)}
.collapsebar .grip{width:42px;height:4px;border-radius:3px;background:var(--border)}
.wave{display:flex;gap:3px;align-items:center;height:16px}
.wave span{width:3px;background:#fff;border-radius:2px;animation:wv .8s ease-in-out infinite}
@keyframes wv{0%,100%{height:4px}50%{height:15px}}
svg{display:block}
</style>
</head>
<body>
<h2>Pick Dross's look</h2>
<div class="sub">Four takes on the orb — all violet, all his. Which feels right?</div>
<div class="avs">
<div class="avcard">
<div class="orb"><div class="a-eye"><div class="a-pupil"></div></div></div>
<div class="avname">A · Soft Eye</div>
<div class="avdesc">The eye, softened — rounder, a glint, a calmer gaze. Still true to character, less stare.</div>
</div>
<div class="avcard">
<div class="orb"><div class="b-core"></div><div class="b-bright"></div></div>
<div class="avname">B · Wisp Core</div>
<div class="avdesc">A swirling violet madra core. No eye — a contained spirit. Abstract & mystical.</div>
</div>
<div class="avcard">
<div class="orb"><svg class="c-sigil" viewBox="0 0 32 32" fill="none" stroke="#c79bff" stroke-width="1.6">
<path d="M16 2 L20 12 L30 16 L20 20 L16 30 L12 20 L2 16 L12 12 Z"/><circle cx="16" cy="16" r="3" fill="#c79bff" stroke="none"/></svg></div>
<div class="avname">C · Rune Sigil</div>
<div class="avdesc">A glowing arcane glyph that slowly turns. Reads as "a power", not a face.</div>
</div>
<div class="avcard">
<div class="orb"><div class="d-ring"><div class="d-mote"></div></div><div class="d-ring r2"><div class="d-mote"></div></div><div class="d-core"></div></div>
<div class="avname">D · Orbiting Motes</div>
<div class="avdesc">A bright core with motes circling it. Lively, restless — feels alive & busy.</div>
</div>
</div>
<h2>The chat (revised)</h2>
<div class="sub">New mic icon · bigger mic + send below the input · opens anchored to the orb. The live orb is bottom-right — drag it, tap to open.</div>
<!-- live orb (Wisp by default) -->
<div id="live"><div class="ping">2</div><div class="orb" style="animation:none"><div class="b-core"></div><div class="b-bright"></div></div></div>
<div class="panel" id="panel">
<div class="hd" id="hd">
<div class="mini"><div class="b-core"></div><div class="b-bright"></div></div>
<div class="who">Dross <small>always here, regrettably</small></div>
<button class="xbtn" id="close"></button>
</div>
<div class="log">
<div class="msg d"><div class="nm">Dross</div>Back already? Your CPU graphs and I were just getting acquainted. Thrilling curves. What do you need?</div>
<div class="msg u">how's the farm backup</div>
<div class="msg d"><div class="nm">Dross</div>Two days ago — 2.5 gigs, landed on Won, didn't fall over. I'd have woken you otherwise. <span style="opacity:.75">Per-guest breakdown, or shall we keep trusting the universe?</span></div>
</div>
<div class="inwrap">
<textarea id="ta" placeholder="Ask Dross…"></textarea>
<div class="btnrow">
<button class="mic" id="mic">
<svg id="micicon" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
<span id="miclabel">Hold to talk</span>
</button>
<button class="send">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
</button>
</div>
</div>
<div class="collapsebar" id="collapse" title="Collapse Dross">
<span class="grip"></span><span>⌄ collapse</span><span class="grip"></span>
</div>
</div>
<script>
const live=document.getElementById('live'), panel=document.getElementById('panel');
function openPanel(){
const r=live.getBoundingClientRect();
panel.classList.add('open'); live.style.display='none';
const pr=panel.getBoundingClientRect();
// anchor the panel's bottom-right to roughly where the orb was
let left=Math.max(8, Math.min(r.right-pr.width, innerWidth-pr.width-8));
let top =Math.max(8, Math.min(r.bottom-pr.height, innerHeight-pr.height-8));
panel.style.right='auto'; panel.style.bottom='auto'; panel.style.left=left+'px'; panel.style.top=top+'px';
}
function closePanel(){ panel.classList.remove('open'); live.style.display='block'; }
live.addEventListener('click',()=>{ if(live._moved){live._moved=false;return;} openPanel(); });
document.getElementById('close').addEventListener('click',closePanel);
document.getElementById('collapse').addEventListener('click',closePanel);
// mic recording sim
const mic=document.getElementById('mic'), label=document.getElementById('miclabel'), ta=document.getElementById('ta'), icon=document.getElementById('micicon');
let rec=false;
mic.addEventListener('click',()=>{
rec=!rec; mic.classList.toggle('rec',rec);
if(rec){ label.innerHTML='<span class="wave">'+Array(16).fill('<span></span>').join('')+'</span> 0:03'; icon.style.display='none'; }
else { icon.style.display='block'; label.textContent='Hold to talk'; ta.value="what's eating the most disk on the media stack right now"; ta.focus(); }
});
// drag helper
function drag(handle,target,isOrb){
handle.addEventListener('pointerdown',e=>{
if(e.target.closest('.xbtn')||e.target.closest('.mic')||e.target.closest('.send')) return;
e.preventDefault();
const r=target.getBoundingClientRect(); const sx=e.clientX,sy=e.clientY; let moved=false;
target.style.right='auto';target.style.bottom='auto';target.style.left=r.left+'px';target.style.top=r.top+'px';
const mv=ev=>{const dx=ev.clientX-sx,dy=ev.clientY-sy; if(Math.abs(dx)+Math.abs(dy)>4)moved=true;
target.style.left=Math.max(4,Math.min(innerWidth-r.width-4,r.left+dx))+'px';
target.style.top=Math.max(4,Math.min(innerHeight-r.height-4,r.top+dy))+'px';};
const up=()=>{document.removeEventListener('pointermove',mv);document.removeEventListener('pointerup',up); if(isOrb)live._moved=moved;};
document.addEventListener('pointermove',mv);document.addEventListener('pointerup',up);
});
}
drag(live,live,true); drag(document.getElementById('hd'),panel,false);
</script>
</body>
</html>