b8a7810bfd
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.
306 lines
11 KiB
JavaScript
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);
|
|
})();
|