Files
Mortdecai b8a7810bfd chore: archive design handoff bundle for toolbar refresh
Stores SethMux_4-24-26.zip + extracted design_handoff_sethmux_toolbar/
so the spec, mockups, and reference jsx components stay in-repo for
future reference. Not served in production — the only file that ships
is static/toolbar.js, which already matches the design's toolbar.js
byte-for-byte.
2026-04-24 19:51:48 -04:00

306 lines
11 KiB
JavaScript

(function(){
if(window._toolbar) return;
window._toolbar=true;
// ── Sethmux toolbar — Google Workspace dark vibe, sethmux orange accent ──
// Tokens (kept local, not :root, so we don't pollute the host page)
// bg #202124 toolbar surface
// surface #303134 button face
// border #3c4043 hairlines
// text #e8eaed primary
// text-2 #9aa0a6 secondary / icons at rest
// accent #D35400 sethmux orange (replaces Google blue)
// accent-bg #3a2a1a tinted hover/selected wash
// ok #81c995 save success
var css=document.createElement('style');
css.textContent = [
"#mb{",
"display:none;position:fixed;bottom:0;left:0;right:0;",
"background:#202124;",
"border-top:1px solid #3c4043;",
"padding:6px 8px 7px;",
"gap:0;flex-direction:column;align-items:stretch;",
"z-index:99999;",
"font-family:'Roboto','Helvetica Neue',Arial,sans-serif;",
"-webkit-font-smoothing:antialiased;",
"box-shadow:0 -1px 0 rgba(0,0,0,.4),0 -8px 24px rgba(0,0,0,.35);",
"}",
"#mb .row{",
"display:flex;gap:4px;justify-content:center;align-items:center;",
"width:100%;",
"}",
"#mb .row + .row{margin-top:4px}",
"#mb button{",
"background:#303134;",
"color:#e8eaed;",
"border:1px solid #3c4043;",
"border-radius:4px;",
"padding:0 10px;height:32px;min-width:40px;",
"font:500 12px/1 'Roboto','Helvetica Neue',Arial,sans-serif;",
"letter-spacing:.1px;",
"cursor:pointer;",
"touch-action:manipulation;",
"-webkit-tap-highlight-color:transparent;",
"user-select:none;",
"display:inline-flex;align-items:center;justify-content:center;",
"transition:background .15s ease,border-color .15s ease,color .15s ease;",
"}",
"#mb button:hover{background:#3a2a1a;border-color:#3c4043;color:#fff}",
"#mb button:active{background:#D35400;border-color:#D35400;color:#0a0a0a}",
// Mono labels for chord/arrow keys so they read as terminal input
"#mb button.mono{",
"font-family:'Roboto Mono','SF Mono',ui-monospace,Menlo,Consolas,monospace;",
"font-weight:400;color:#9aa0a6;",
"}",
"#mb button.mono:hover{color:#e8eaed}",
// Accent (orange) — used for primary/important actions at rest
"#mb button.hi{",
"color:#f0a36b;border-color:#5a3a22;background:#2a1f15;",
"}",
"#mb button.hi:hover{background:#3a2a1a;color:#ffb37a;border-color:#7a4a2a}",
// Toggled-on state (Sel active, etc.) — filled accent
"#mb button.on{",
"background:#D35400;border-color:#D35400;color:#0a0a0a;",
"}",
"#mb button.on:hover{background:#e26416;border-color:#e26416;color:#0a0a0a}",
// Success (Save) — Google green at rest, fills on confirm
"#mb button.grn{color:#81c995;border-color:#3c4043;background:#303134}",
"#mb button.grn:hover{background:#1f2a22;color:#a8e0b8;border-color:#3a5a44}",
"#mb button.grn.on{background:#1e8e3e;border-color:#1e8e3e;color:#0a0a0a}",
// Vertical hairline divider between groups
"#mb .sep{",
"width:1px;height:20px;background:#3c4043;margin:0 4px;flex-shrink:0;",
"}",
// ── Compose bar (mobile autocorrect workaround) ──
"#mb .compose{",
"display:none;width:100%;gap:4px;align-items:center;margin-top:4px;",
"}",
"#mb.composing .compose{display:flex}",
"#mb.composing #typebtn{",
"background:#D35400;border-color:#D35400;color:#0a0a0a;",
"}",
"#mb-compose{",
"flex:1;min-width:0;height:36px;",
"padding:0 10px;",
"background:#303134;color:#e8eaed;",
"border:1px solid #3c4043;border-radius:4px;",
"font:400 14px/1 'Roboto Mono','SF Mono',ui-monospace,Menlo,Consolas,monospace;",
"outline:none;",
"-webkit-appearance:none;appearance:none;",
"caret-color:#D35400;",
"}",
"#mb-compose:focus{border-color:#D35400}",
"#mb-compose::placeholder{color:#5f6368}",
"#mb .send{",
"height:36px;min-width:54px;padding:0 12px;",
"background:#D35400;border:1px solid #D35400;color:#0a0a0a;",
"border-radius:4px;font:500 12px/1 'Roboto',sans-serif;cursor:pointer;",
"}",
"#mb .send:disabled{background:#303134;border-color:#3c4043;color:#5f6368;cursor:default}",
"#mb .send.nl{background:#303134;border-color:#3c4043;color:#9aa0a6;min-width:38px;padding:0 8px}",
"#mb .send.nl:hover{color:#e8eaed;background:#3a2a1a}",
// Selection mode visual — dim the terminal slightly so the text-select layer reads
"body.selmode .xterm-screen{",
"pointer-events:none!important;",
"user-select:text!important;-webkit-user-select:text!important;",
"}",
"body.selmode .xterm{filter:brightness(.92)}",
"@media(max-width:900px){#mb{display:flex}}",
// Tighter on very narrow phones
"@media(max-width:380px){",
"#mb button{padding:0 7px;min-width:34px;height:30px;font-size:11.5px}",
"#mb .sep{margin:0 2px}",
"}",
].join("");
document.head.appendChild(css);
var bar=document.createElement('div');
bar.id='mb';
bar.innerHTML =
'<div class="row">' +
'<button class="hi" data-k="\x01c">+ Tab</button>' +
'<button data-k="\x01n">Next</button>' +
'<button data-k="\x01p">Prev</button>' +
'<div class="sep"></div>' +
'<button class="mono" data-k="\x03">^C</button>' +
'<button class="mono" data-k="\x04">^D</button>' +
'<button data-k="\x0c">Clr</button>' +
'<div class="sep"></div>' +
'<button class="mono" data-k="\x1b">Esc</button>' +
'<button class="mono" data-k="\t">Tab</button>' +
'<button class="mono" data-k="\x1bOA">\u25B2</button>' +
'<button class="mono" data-k="\x1bOB">\u25BC</button>' +
'</div>' +
'<div class="row">' +
'<button class="hi" id="selbtn" data-sel="1">Sel</button>' +
'<button class="hi" id="pastebtn" data-paste="1">Paste</button>' +
'<button data-k="\x01z">Zoom</button>' +
'<button class="grn" data-save="1">Save</button>' +
'<div class="sep"></div>' +
'<button data-k="\x01v">V.Spl</button>' +
'<button data-k="\x01s">H.Spl</button>' +
'<button data-k="\x01o">Pane</button>' +
'<button data-k="\x01x">Kill</button>' +
'<div class="sep"></div>' +
'<button class="hi" id="typebtn" data-type="1">Type</button>' +
'</div>' +
'<div class="compose row">' +
'<input id="mb-compose" type="text" ' +
'placeholder="type here \u2014 autocorrect on, \u21B5 sends" ' +
'autocomplete="on" autocorrect="on" autocapitalize="sentences" ' +
'spellcheck="true" enterkeyhint="send" inputmode="text" />' +
'<button class="send nl" data-nl="1" title="Send newline">\u21B5</button>' +
'<button class="send" data-send="1">Send</button>' +
'</div>';
document.body.appendChild(bar);
// ── terminal I/O ─────────────────────────────────────────
function send(k){
if(document.body.classList.contains('selmode')) toggleSel();
var t=window.term;
if(!t) return;
if(t._core && t._core.coreService && t._core.coreService.triggerDataEvent){
t._core.coreService.triggerDataEvent(k);
} else if(t._core && t._core._onData){
t._core._onData.fire(k);
} else if(t.input){
t.input(k);
}
t.focus();
}
function toggleSel(){
var b=document.getElementById('selbtn');
document.body.classList.toggle('selmode');
if(document.body.classList.contains('selmode')){
b.classList.add('on');b.textContent='Done';
} else {
b.classList.remove('on');b.textContent='Sel';
window.getSelection().removeAllRanges();
if(window.term) window.term.focus();
}
}
function doPaste(){
if(!navigator.clipboard||!navigator.clipboard.readText){
alert('Clipboard access not available (needs HTTPS)');
return;
}
navigator.clipboard.readText().then(function(text){
if(text) send(text);
}).catch(function(e){
alert('Clipboard read failed: '+e.message);
});
}
function doSave(){
send('\x01S');
var btn=document.querySelector('[data-save]');
btn.classList.add('on');
btn.textContent='\u2713 Saved';
setTimeout(function(){btn.classList.remove('on');btn.textContent='Save';},1500);
}
// ── Compose bar (mobile autocorrect workaround) ──
// xterm.js reads keys from a hidden textarea via per-keystroke events;
// Gboard autocorrect/swipe replaces .value in bulk so those chars never
// reach stdin. The compose bar gives autocorrect a real <input> to chew
// on, then sends the assembled string to the terminal in one shot.
var ci=document.getElementById('mb-compose');
function toggleType(){
var on=bar.classList.toggle('composing');
if(on){
// Defer focus so the keyboard opens reliably on iOS/Android
setTimeout(function(){ ci.focus(); },0);
relayout();
} else {
ci.blur();
if(window.term) window.term.focus();
relayout();
}
}
function flushCompose(appendNewline){
var v=ci.value;
ci.value='';
if(v) send(v);
if(appendNewline) send('\r');
ci.focus();
}
ci.addEventListener('keydown',function(e){
if(e.key==='Enter'){
e.preventDefault();
flushCompose(true);
}
});
bar.addEventListener('click',function(e){
var btn=e.target.closest('button');
if(!btn) return;
if(btn.dataset.sel) return toggleSel();
if(btn.dataset.paste) return doPaste();
if(btn.dataset.save) return doSave();
if(btn.dataset.type) return toggleType();
if(btn.dataset.send) return flushCompose(true);
if(btn.dataset.nl) return flushCompose(true); // explicit newline button
if(btn.dataset.k) {
// Key pressed while composing: flush typed text first so order is preserved,
// but DON'T close the compose bar — user is mid-thought.
if(bar.classList.contains('composing') && ci.value){
var v=ci.value; ci.value=''; send(v);
}
send(btn.dataset.k);
if(bar.classList.contains('composing')) ci.focus();
}
});
// Keep terminal focused when tapping outside the toolbar
document.addEventListener('click', function(e){
if(!e.target.closest('#mb') && window.term) window.term.focus();
});
// Tab key shouldn't escape into toolbar focus
bar.querySelectorAll('button').forEach(function(b){ b.tabIndex = -1; });
// Disable mobile autocomplete on xterm's hidden textarea
function disableMobileAutocomplete(){
var ta=document.querySelector('.xterm-helper-textarea');
if(!ta || ta.dataset.acOff) return;
ta.setAttribute('autocomplete','off');
ta.setAttribute('autocorrect','off');
ta.setAttribute('autocapitalize','off');
ta.setAttribute('spellcheck','false');
ta.dataset.acOff='1';
}
// Resize terminal to leave room for the toolbar on mobile.
// Height is dynamic: 2 rows when collapsed, 3 when compose bar is open.
function relayout(){
var el=document.querySelector('.xterm');
if(!el || window.innerWidth>900) return;
var h=bar.offsetHeight || 92;
el.style.height='calc(100vh - '+(h+4)+'px)';
if(window.term && window.term.fit) window.term.fit();
}
var obs=new MutationObserver(function(){
disableMobileAutocomplete();
relayout();
});
obs.observe(document.body,{childList:true,subtree:true});
window.addEventListener('resize',relayout);
})();