feat: workspace lives in the terminal view and keeps its sessions across pages; terminal panes split out

This commit is contained in:
redtea committed 2026-10-15 20:10:28 +03:00
1 parent 113c63fd1b
commit 012c0af7f9
9 files changed
+791 -736

No files matched your search

+160
View File
@@ -262,3 +262,163 @@ input.sw:disabled { opacity: .45; cursor: not-allowed; }
/* The one-page shell: the header's current view. */
header.topbar nav a.on { color: var(--text); }
/* ---- The workspace (Терминал view) ---- */
#status.st-ok { color:#37c871; }
#status.st-err { color:#ff5c5c; }
#status { display:inline-block; vertical-align:middle; margin-left:18px; min-width:0; max-width:32vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* SSH: the badge on a pane, and the dialog to connect. */
.badge-ssh { flex:none; font-size:10px; font-weight:600; letter-spacing:.04em; padding:1px 5px; border-radius:4px;
background:#12202f; color:var(--accent); border:1px solid #1f3a57; }
dialog.sshdlg { max-width:460px; }
.sshdlg .dlg-h .ico { color:var(--accent); display:inline-flex; }
.sshdlg .dlg-b { display:grid; gap:12px; }
.sshdlg .f { display:grid; gap:5px; margin:0; }
.sshdlg .f > span { font-size:12px; color:var(--muted); }
.sshdlg .frow { display:flex; gap:10px; align-items:end; }
.sshdlg .frow .grow { flex:1; min-width:0; }
.sshdlg .frow .port { width:92px; flex:none; }
.sshdlg input, .sshdlg select, .sshdlg textarea { width:100%; box-sizing:border-box; }
.sshdlg textarea { font:12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; resize:vertical; min-height:90px;
background:var(--bg, #0c0f13); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:8px 10px; }
.sshdlg textarea:focus { border-color:var(--accent); outline:none; }
.sshdlg .auth { display:grid; gap:8px; animation:auth-in .18s ease; }
.sshdlg .auth[hidden] { display:none; }
@keyframes auth-in { from { opacity:0; transform:translateY(-4px); } }
.sshdlg p.auth { margin:0; font-size:13px; }
.sshdlg .keyname { font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; align-self:center; }
.sshdlg .seg { display:flex; padding:3px; gap:3px; background:#0c0f13; border:1px solid var(--border); border-radius:9px; }
.sshdlg .seg button { flex:1; padding:6px 8px; font-size:13px; background:transparent; color:var(--muted); border:0; border-radius:6px;
transition:background .18s, color .18s; }
.sshdlg .seg button:hover { color:var(--text); }
.sshdlg .seg button.on { background:#12202f; color:var(--text); box-shadow:inset 0 0 0 1px #1f3a57; }
.sshdlg .sshmsg { margin:0; min-height:18px; font-size:13px; color:var(--danger); }
.sshdlg .row.end { display:flex; justify-content:flex-end; gap:8px; }
@media (max-width:520px) { .sshdlg .seg button { font-size:12px; padding:6px 4px; } }
/* The page is the terminal: bar, tabs, then the terminals to the bottom. */
/* The shell: the header, then the view to the bottom. Машины and
Настройки scroll inside it; the terminal view fills it. */
body.app { height:100vh; display:flex; flex-direction:column; overflow:hidden; }
body.app main#app { flex:1; min-height:0; overflow:auto; }
.view-term { height:100%; display:flex; flex-direction:column; overflow:hidden; }
header.topbar { flex:none; }
/* Tabs: folded away while there is one, sliding in with the second. */
.tabs { flex:none; display:flex; align-items:flex-end; gap:4px; padding:0 16px; max-height:0; opacity:0; overflow:hidden;
transition:max-height .28s ease, opacity .22s ease, padding .28s ease; }
body.multi .tabs, body.multi-hint .tabs { max-height:48px; opacity:1; padding-top:10px; }
.tablist { position:relative; display:flex; gap:4px; min-width:0; overflow-x:auto; scrollbar-width:none; }
.tablist::-webkit-scrollbar { display:none; }
.tab { display:flex; align-items:center; gap:8px; flex:none; min-width:0; height:34px; padding:0 6px 0 12px; box-sizing:border-box;
border:1px solid var(--border); border-bottom:0; border-radius:9px 9px 0 0; background:#11151b; color:var(--muted);
font-size:13px; user-select:none; cursor:default; overflow:hidden; white-space:nowrap;
transition:background .18s, color .18s, width .22s ease, max-width .22s ease, opacity .2s, padding .22s ease; }
.tab:hover { color:var(--text); background:#151a21; }
.tab.on { background:var(--panel); color:var(--text); }
.tab.enter, .tab.leave { max-width:0; opacity:0; padding:0; }
.tab.dragging { opacity:.3; }
.tab-title { flex:1; overflow:hidden; text-overflow:ellipsis; min-width:0; }
.tab-edit { width:auto; min-width:60px; max-width:150px; height:24px; padding:2px 6px; font-size:13px; }
.tab-x, .pane-x { flex:none; width:20px; height:20px; padding:0; display:inline-flex; align-items:center; justify-content:center;
background:transparent; border:0; border-radius:5px; color:var(--muted); opacity:.6; transition:opacity .15s, background .15s, color .15s; }
.tab-x:hover, .pane-x:hover { opacity:1; background:#3a1d1d; color:var(--danger); }
.tab-add { flex:none; width:34px; height:34px; padding:0; display:inline-flex; align-items:center; justify-content:center;
border:1px solid var(--border); border-bottom:0; border-radius:9px 9px 0 0; background:transparent; color:var(--muted);
font-size:18px; line-height:1; transition:background .18s, color .18s; }
.tab-add:hover { background:#151a21; color:var(--text); }
.tabmark { position:absolute; top:6px; bottom:0; width:3px; margin-left:-1px; border-radius:2px; background:var(--accent);
pointer-events:none; transition:left .12s ease; }
.tabmark[hidden] { display:none; }
.dot { width:7px; height:7px; border-radius:50%; background:#555d6a; flex:none; transition:background .3s; }
.dot.ok { background:var(--ok); } .dot.err { background:var(--danger); padding:0; border:0; margin:0; } .dot.wait { background:#e6a23c; }
/* The terminals' area. */
.area { position:relative; flex:1; min-height:0; margin:12px 16px 16px; border:1px solid var(--border); border-radius:10px; background:#0c0f13; overflow:hidden; }
body.multi .area { margin-top:0; border-top-left-radius:0; }
.float-add { position:absolute; top:8px; right:10px; z-index:6; width:28px; height:28px; padding:0; display:inline-flex;
align-items:center; justify-content:center; font-size:17px; line-height:1; color:var(--text);
background:transparent; border:1px solid var(--border); opacity:.28; transition:opacity .25s ease, background .25s ease, top .25s ease, right .25s ease; }
/* One tab split in panes: below the panes' title bars, clear of their close buttons. */
body.split1 .float-add { top:42px; right:16px; }
.float-add:hover, .float-add:focus-visible { opacity:1; background:var(--panel); }
body.multi .float-add { opacity:0; pointer-events:none; }
.tabview { position:absolute; inset:8px; opacity:0; visibility:hidden; transform:translateY(4px) scale(.995);
transition:opacity .22s ease, transform .22s ease, visibility 0s .22s; }
.tabview.on { opacity:1; visibility:visible; transform:none; transition:opacity .22s ease, transform .22s ease, visibility 0s; }
.pane { position:absolute; display:flex; flex-direction:column; border-radius:7px; overflow:hidden;
transition:left .26s ease, top .26s ease, width .26s ease, height .26s ease, border-color .2s; }
.tabview.instant .pane, .pane.instant { transition:none; }
.pane-head { display:none; }
.tabview.split .pane { border:1px solid var(--border); }
.tabview.split .pane.active { border-color:rgba(79,157,255,.7); }
.tabview.split .pane-head { display:flex; align-items:center; gap:7px; flex:none; height:26px; padding:0 4px 0 9px;
background:#11151b; border-bottom:1px solid var(--border); font-size:12px; color:var(--muted); cursor:grab; user-select:none; }
.tabview.split .pane.active .pane-head { color:var(--text); }
.pane-title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pane-body { flex:1; min-height:0; padding:4px 0 0 4px; }
/* A lone terminal's scrollbar sits in the margin by the frame, right of
the corner "+", not over the text. */
body:not(.touch) .tabview:not(.split) .pane { overflow:visible; }
body:not(.touch) .tabview:not(.split) .xterm .xterm-viewport { right:-7px; }
.pane.dragging { opacity:.45; }
.gutter { position:absolute; z-index:3; }
.gutter.c { cursor:col-resize; } .gutter.r { cursor:row-resize; }
.gutter::after { content:""; position:absolute; inset:0; border-radius:3px; background:var(--accent); opacity:0; transition:opacity .15s; }
.gutter:hover::after, .gutter.drag::after { opacity:.55; }
.hint { position:absolute; z-index:5; display:flex; align-items:center; justify-content:center; text-align:center; padding:8px;
border:2px solid var(--accent); border-radius:9px; background:rgba(79,157,255,.14); color:var(--text); font-size:13px;
pointer-events:none; transition:left .14s ease, top .14s ease, width .14s ease, height .14s ease; }
.hint.bad { border-color:var(--danger); background:rgba(255,92,92,.12); }
.hint[hidden] { display:none; }
.drag-ghost { position:fixed; left:0; top:0; z-index:60; pointer-events:none; padding:7px 12px; border:1px solid var(--accent);
border-radius:8px; background:var(--panel); color:var(--text); font-size:13px; white-space:nowrap;
box-shadow:0 12px 30px rgba(0,0,0,.55); transition:transform .04s linear; }
.drag-ghost[data-hint]:not([data-hint=""])::after { content:attr(data-hint); display:block; margin-top:3px; font-size:11px; color:var(--muted); }
body.dnd, body.dnd * { cursor:grabbing !important; }
.empty { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; color:var(--muted); }
.empty[hidden] { display:none; }
/* The "+" menu: a terminal here, or on another machine. */
.addmenu { position:fixed; z-index:50; min-width:270px; max-width:340px; max-height:70vh; overflow:auto; padding:6px;
background:var(--panel); border:1px solid var(--border); border-radius:10px; box-shadow:0 14px 36px rgba(0,0,0,.55);
animation:menu-in .14s ease; }
.addmenu[hidden] { display:none; }
@keyframes menu-in { from { opacity:0; transform:translateY(-4px); } }
.addmenu .item { display:flex; align-items:center; gap:10px; width:100%; padding:8px 10px; border-radius:7px; text-align:left;
background:transparent; color:var(--text); font-weight:400; }
.addmenu .item:hover:not(:disabled) { background:#12202f; }
.addmenu .item:disabled { opacity:.45; cursor:not-allowed; }
.addmenu .item b { display:block; font-weight:500; font-size:14px; }
.addmenu .item small { display:block; color:var(--muted); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:240px; }
.addmenu .ico { width:22px; text-align:center; color:var(--accent); font-size:16px; flex:none; }
.addmenu .chev { margin-left:auto; color:var(--muted); }
.addmenu .mlist { border-top:1px solid var(--border); margin-top:4px; padding-top:4px; }
/* Touch screens: the page is as tall as what the on-screen keyboard leaves,
with a row of extra keys at the bottom, the way Termux does it. Tabs are
smaller, and there is no splitting. */
.keys { display:none; }
body.touch { position:fixed; left:0; right:0; top:0; height:100%; }
body.touch .area { margin:6px; }
body.touch.multi .area { margin-top:0; }
body.touch .tabs { padding-left:6px; padding-right:6px; }
body.touch.multi .tabs { padding-top:6px; max-height:38px; }
body.touch .tab { height:28px; font-size:12px; padding:0 2px 0 8px; gap:6px; }
body.touch .tab-add { width:30px; height:28px; }
body.touch .tabview { inset:4px; }
body.touch .keys {
display:grid; grid-template-columns:repeat(7, 1fr); gap:4px; flex:none;
padding:4px 4px calc(4px + env(safe-area-inset-bottom)); background:var(--panel); border-top:1px solid var(--border);
}
.keys button {
height:38px; padding:0; border:1px solid var(--border); border-radius:6px;
background:#0c0f13; color:var(--text); font:500 13px ui-monospace, SFMono-Regular, Menlo, monospace;
touch-action:manipulation; user-select:none; -webkit-user-select:none; -webkit-tap-highlight-color:transparent;
}
.keys button:active { background:var(--border); }
.keys button.on { background:var(--accent); border-color:var(--accent); color:#fff; }
.keys button.lock { background:var(--accent); border-color:#fff; color:#fff; }
/* Copy and paste on touch screens: a long press selects, this menu acts. */
.selmenu { position:fixed; left:50%; transform:translateX(-50%); z-index:20; display:flex; gap:7px; padding:5px;
background:var(--panel); border:1px solid var(--border); border-radius:8px; box-shadow:0 6px 24px rgba(0,0,0,.5); }
.selmenu[hidden] { display:none; }
.selmenu button { width:42px; height:42px; padding:0; display:inline-flex; align-items:center; justify-content:center;
touch-action:manipulation; -webkit-tap-highlight-color:transparent; }
body.touch .area { -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
#status:empty { display:none; }