feat(dross): global floating bubble; retire the right rail
Adds dross_bubble.js — a fixed FAB orb that opens a draggable, anchored panel wired to wireAgentChat. Mic button rendered but disabled (Phase 2). Swaps renderRightrail call in app.js; removes dead <aside id="rightrail"> from index.html. rightrail.js kept in place (unused). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { api } from './api.js';
|
|||||||
import { route, current, navigate } from './router.js';
|
import { route, current, navigate } from './router.js';
|
||||||
import { renderSidebar } from './components/sidebar.js';
|
import { renderSidebar } from './components/sidebar.js';
|
||||||
import { renderTopbar } from './components/topbar.js';
|
import { renderTopbar } from './components/topbar.js';
|
||||||
import { renderRightrail } from './components/rightrail.js';
|
import { renderDrossBubble } from './components/dross_bubble.js';
|
||||||
import { emit, state } from './state.js';
|
import { emit, state } from './state.js';
|
||||||
import { el, mount } from './dom.js';
|
import { el, mount } from './dom.js';
|
||||||
import { attachDropzone } from './components/dropzone.js';
|
import { attachDropzone } from './components/dropzone.js';
|
||||||
@@ -84,7 +84,7 @@ async function init() {
|
|||||||
await loadTheme(); // apply saved palette overrides before rendering chrome
|
await loadTheme(); // apply saved palette overrides before rendering chrome
|
||||||
renderTopbar(document.getElementById('topbar'));
|
renderTopbar(document.getElementById('topbar'));
|
||||||
renderSidebar(document.getElementById('sidebar'));
|
renderSidebar(document.getElementById('sidebar'));
|
||||||
renderRightrail(document.getElementById('rightrail'));
|
renderDrossBubble();
|
||||||
initChrome();
|
initChrome();
|
||||||
attachDropzone(document.getElementById('main'));
|
attachDropzone(document.getElementById('main'));
|
||||||
route(renderView);
|
route(renderView);
|
||||||
|
|||||||
88
public/components/dross_bubble.js
Normal file
88
public/components/dross_bubble.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// public/components/dross_bubble.js
|
||||||
|
// Global floating Dross companion. Replaces the per-Space right rail.
|
||||||
|
import { el, mount } from '../dom.js';
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { wireAgentChat } from './agent_chat.js';
|
||||||
|
import { drossAvatar } from './dross_avatar.js';
|
||||||
|
|
||||||
|
const TOOL_LABELS = { search: '🔍 searching', read: '📄 reading', context: '🧭 looking at this view', propose_change: '📝 drafting a change' };
|
||||||
|
let cfg = { avatar: 'soft-eye', accent: '#a86adf', persona: '', voiceMode: 'review' };
|
||||||
|
|
||||||
|
function applyAccent(node, hex) {
|
||||||
|
node.style.setProperty('--dross', hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderDrossBubble() {
|
||||||
|
try { cfg = { ...cfg, ...(await api.get('/api/dross/settings')) }; } catch { /* defaults */ }
|
||||||
|
|
||||||
|
const host = el('div', { class: 'dross-host' });
|
||||||
|
document.getElementById('shell').appendChild(host);
|
||||||
|
|
||||||
|
const fab = el('div', { class: 'dross-fab', title: 'Dross' },
|
||||||
|
el('div', { class: 'dross-ping', style: { display: 'none' } }, ''), drossAvatar(cfg.avatar, 60));
|
||||||
|
const log = el('div', { class: 'dross-log' });
|
||||||
|
const input = el('textarea', { rows: 1, placeholder: 'Ask Dross…' });
|
||||||
|
const sendBtn = el('button', { class: 'dross-send', title: 'Send' },
|
||||||
|
el('span', { html: '<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>' }));
|
||||||
|
const mic = el('button', { class: 'dross-mic', disabled: true, title: 'Voice arrives in Phase 2' },
|
||||||
|
el('span', { html: '<svg 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>' }), 'Hold to talk');
|
||||||
|
const closeBtn = el('button', { class: 'dross-x', title: 'Close' }, '⤬');
|
||||||
|
const header = el('div', { class: 'dross-hd' }, drossAvatar(cfg.avatar, 30),
|
||||||
|
el('div', { class: 'dross-who' }, 'Dross', el('small', {}, 'always here, regrettably')), closeBtn);
|
||||||
|
const collapse = el('div', { class: 'dross-collapse', title: 'Collapse' },
|
||||||
|
el('span', { class: 'grip' }), el('span', {}, '⌄ collapse'), el('span', { class: 'grip' }));
|
||||||
|
const panel = el('div', { class: 'dross-panel' }, header, log,
|
||||||
|
el('div', { class: 'dross-inwrap' }, input, el('div', { class: 'dross-btnrow' }, mic, sendBtn)), collapse);
|
||||||
|
|
||||||
|
host.append(fab, panel);
|
||||||
|
applyAccent(fab, cfg.accent); applyAccent(panel, cfg.accent);
|
||||||
|
|
||||||
|
const chat = wireAgentChat({
|
||||||
|
logEl: log, inputEl: input, sendBtnEl: sendBtn,
|
||||||
|
historyUrl: '/api/dross', turnUrl: '/api/dross/turn',
|
||||||
|
agentName: 'Dross', showDrafts: true, toolLabels: TOOL_LABELS,
|
||||||
|
turnBody: (text) => ({ text, view: state.view || null })
|
||||||
|
});
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
function openPanel() {
|
||||||
|
const r = fab.getBoundingClientRect();
|
||||||
|
panel.classList.add('open'); fab.style.display = 'none';
|
||||||
|
const pr = panel.getBoundingClientRect();
|
||||||
|
const left = Math.max(8, Math.min(r.right - pr.width, innerWidth - pr.width - 8));
|
||||||
|
const 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';
|
||||||
|
if (!loaded) { loaded = true; chat.load(); }
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
function closePanel() { panel.classList.remove('open'); fab.style.display = 'block'; }
|
||||||
|
fab.addEventListener('click', () => { if (fab._moved) { fab._moved = false; return; } openPanel(); });
|
||||||
|
closeBtn.addEventListener('click', closePanel);
|
||||||
|
collapse.addEventListener('click', closePanel);
|
||||||
|
|
||||||
|
drag(fab, fab, true); drag(header, panel, false);
|
||||||
|
|
||||||
|
window.addEventListener('dross-settings-changed', async () => {
|
||||||
|
try { cfg = { ...cfg, ...(await api.get('/api/dross/settings')) }; } catch { return; }
|
||||||
|
applyAccent(fab, cfg.accent); applyAccent(panel, cfg.accent);
|
||||||
|
mount(fab, el('div', { class: 'dross-ping', style: { display: 'none' } }), drossAvatar(cfg.avatar, 60));
|
||||||
|
header.replaceChild(drossAvatar(cfg.avatar, 30), header.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(handle, target, isFab) {
|
||||||
|
handle.addEventListener('pointerdown', (e) => {
|
||||||
|
if (e.target.closest('.dross-x') || e.target.closest('.dross-mic') || e.target.closest('.dross-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 (isFab) target._moved = moved; };
|
||||||
|
document.addEventListener('pointermove', mv); document.addEventListener('pointerup', up);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
<header id="topbar"></header>
|
<header id="topbar"></header>
|
||||||
<aside id="sidebar"></aside>
|
<aside id="sidebar"></aside>
|
||||||
<main id="main"></main>
|
<main id="main"></main>
|
||||||
<aside id="rightrail"></aside>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-root"></div>
|
<div id="modal-root"></div>
|
||||||
<script type="module" src="/app.js"></script>
|
<script type="module" src="/app.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user