feat(ui): make Ralph Wiggum tracker panel detachable

Add ability to detach the Ralph Wiggum tracker panel as a floating
window that can be dragged around the screen.

Changes:
- Add detach button to the Ralph panel header
- Restructure HTML to separate clickable content from controls
- Add CSS styles for detached state (floating, draggable, resizable)
- Add toggleRalphDetach() and setupRalphDrag() JavaScript methods
- Panel auto-expands when detached for better visibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 18:41:14 +01:00
co-authored by Claude Opus 4.5
parent 2c38f7924a
commit 6a414a2648
3 changed files with 184 additions and 35 deletions
+89
View File
@@ -1910,6 +1910,95 @@ class ClaudemanApp {
this.renderInnerStatePanel();
}
toggleRalphDetach() {
const panel = this.$('innerStatePanel');
const detachBtn = this.$('ralphDetachBtn');
if (!panel) return;
if (panel.classList.contains('detached')) {
// Re-attach to original position
panel.classList.remove('detached');
panel.style.top = '';
panel.style.left = '';
panel.style.width = '';
panel.style.height = '';
if (detachBtn) {
detachBtn.innerHTML = '&#x2197;'; // Detach icon
detachBtn.title = 'Detach panel';
}
} else {
// Detach as floating window
panel.classList.add('detached');
// Expand when detaching for better visibility
this.innerStatePanelCollapsed = false;
panel.classList.remove('collapsed');
if (detachBtn) {
detachBtn.innerHTML = '&#x2199;'; // Attach icon
detachBtn.title = 'Attach panel';
}
// Setup drag functionality
this.setupRalphDrag();
}
this.renderInnerStatePanel();
}
setupRalphDrag() {
const panel = this.$('innerStatePanel');
const header = this.$('ralphSummary');
if (!panel || !header) return;
let isDragging = false;
let startX, startY, startLeft, startTop;
const onMouseDown = (e) => {
// Only drag from header, not from buttons or toggle
if (e.target.closest('button') || e.target.closest('.ralph-toggle')) return;
if (!panel.classList.contains('detached')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Keep within viewport bounds
const rect = panel.getBoundingClientRect();
newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
// Remove existing listeners before adding new ones
header.removeEventListener('mousedown', header._ralphDragHandler);
header._ralphDragHandler = onMouseDown;
header.addEventListener('mousedown', onMouseDown);
}
renderInnerStatePanel() {
const panel = this.$('innerStatePanel');
const toggle = this.$('ralphToggle');
+25 -20
View File
@@ -75,28 +75,33 @@
<!-- Enhanced Ralph Wiggum Loop Panel -->
<div class="ralph-panel collapsed" id="innerStatePanel" style="display: none;">
<!-- Collapsed Summary Bar -->
<div class="ralph-summary" onclick="app.toggleInnerStatePanel()">
<div class="ralph-ring-mini" id="ralphRingMini">
<svg viewBox="0 0 36 36" class="ralph-ring-svg">
<circle class="ralph-ring-bg" cx="18" cy="18" r="15.9" />
<circle class="ralph-ring-progress" cx="18" cy="18" r="15.9" id="ralphRingMiniProgress" />
</svg>
<span class="ralph-ring-text" id="ralphRingMiniText">0%</span>
<div class="ralph-summary" id="ralphSummary">
<div class="ralph-summary-content" onclick="app.toggleInnerStatePanel()">
<div class="ralph-ring-mini" id="ralphRingMini">
<svg viewBox="0 0 36 36" class="ralph-ring-svg">
<circle class="ralph-ring-bg" cx="18" cy="18" r="15.9" />
<circle class="ralph-ring-progress" cx="18" cy="18" r="15.9" id="ralphRingMiniProgress" />
</svg>
<span class="ralph-ring-text" id="ralphRingMiniText">0%</span>
</div>
<div class="ralph-summary-info">
<span class="ralph-title">Ralph Wiggum Tracker</span>
<span class="ralph-status-badge" id="ralphStatusBadge">
<span class="ralph-status-dot"></span>
<span class="ralph-status-text">Idle</span>
</span>
<span class="ralph-phrase-preview" id="ralphPhrasePreview"></span>
</div>
<div class="ralph-summary-stats">
<span class="ralph-stat"><span class="ralph-stat-icon">&#x23F1;</span><span id="ralphStatTime">0m</span></span>
<span class="ralph-stat"><span class="ralph-stat-icon">&#x21BB;</span><span id="ralphStatCycles">0</span></span>
<span class="ralph-stat"><span class="ralph-stat-icon">&#x2713;</span><span id="ralphStatTasks">0/0</span></span>
</div>
<span class="ralph-toggle" id="ralphToggle">&#x25B6;</span>
</div>
<div class="ralph-summary-info">
<span class="ralph-title">Ralph Wiggum Tracker</span>
<span class="ralph-status-badge" id="ralphStatusBadge">
<span class="ralph-status-dot"></span>
<span class="ralph-status-text">Idle</span>
</span>
<span class="ralph-phrase-preview" id="ralphPhrasePreview"></span>
<div class="ralph-controls">
<button class="btn-icon-sm" onclick="event.stopPropagation(); app.toggleRalphDetach()" title="Detach/Attach panel" id="ralphDetachBtn">&#x2197;</button>
</div>
<div class="ralph-summary-stats">
<span class="ralph-stat"><span class="ralph-stat-icon">&#x23F1;</span><span id="ralphStatTime">0m</span></span>
<span class="ralph-stat"><span class="ralph-stat-icon">&#x21BB;</span><span id="ralphStatCycles">0</span></span>
<span class="ralph-stat"><span class="ralph-stat-icon">&#x2713;</span><span id="ralphStatTasks">0/0</span></span>
</div>
<span class="ralph-toggle" id="ralphToggle">&#x25B6;</span>
</div>
<!-- Expanded Detail View -->
+70 -15
View File
@@ -457,21 +457,6 @@ body {
position: relative;
}
/* Collapsed Summary Bar */
.ralph-summary {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.ralph-summary:hover {
background: var(--bg-hover);
}
/* Mini Progress Ring (in summary) */
.ralph-ring-mini {
position: relative;
@@ -908,6 +893,76 @@ body {
padding: 0.5rem;
}
/* Ralph panel summary layout adjustments */
.ralph-summary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
user-select: none;
transition: background 0.15s;
}
.ralph-summary-content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
cursor: pointer;
}
.ralph-summary-content:hover {
background: var(--bg-hover);
}
.ralph-controls {
display: flex;
gap: 0.25rem;
flex-shrink: 0;
}
/* Detached Ralph Panel */
.ralph-panel.detached {
position: fixed;
top: 100px;
left: 100px;
width: 400px;
min-height: 200px;
max-height: 80vh;
z-index: 1000;
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
background: var(--bg-card);
overflow: auto;
resize: both;
}
.ralph-panel.detached .ralph-summary {
cursor: move;
background: linear-gradient(180deg, var(--bg-hover) 0%, var(--bg-card) 100%);
border-bottom: 1px solid var(--border);
border-radius: 8px 8px 0 0;
}
.ralph-panel.detached .ralph-summary-content {
cursor: move;
}
.ralph-panel.detached .ralph-summary-content:hover {
background: transparent;
}
.ralph-panel.detached .ralph-detail {
max-height: none;
overflow: auto;
}
.ralph-panel.detached.collapsed {
min-height: auto;
height: auto;
}
/* Main - Terminal Area */
.main {
flex: 1;