diff --git a/README.md b/README.md index 7dba755..e151ece 100644 --- a/README.md +++ b/README.md @@ -31,20 +31,20 @@ Prefer a one-off session? Use **Cmd+Shift+Q** / **Ctrl+Shift+Q** to quick connec All shortcuts use **Cmd** on macOS and **Ctrl** on Windows/Linux. -| macOS Shortcut | Windows/Linux Shortcut | Action | -| ------------- | ----------------------- | --------------------------------- | -| Cmd + K | Ctrl + K | Open command palette | -| Cmd + N | Ctrl + N | New connection | -| Cmd + W | Ctrl + W | Close current tab | -| Cmd + Tab | Ctrl + Tab | Next tab | -| Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab | -| Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number | -| Cmd + , | Ctrl + , | Open settings | -| Cmd + F | Ctrl + F | Find in terminal | -| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | -| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | -| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | -| Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode | -| Cmd + Shift + S | Ctrl + Shift + S | Open snippet library | -| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | -| F11 | F11 | Toggle fullscreen | +| macOS Shortcut | Windows/Linux Shortcut | Action | +| ----------------- | ---------------------- | --------------------------- | +| Cmd + K | Ctrl + K | Open command palette | +| Cmd + N | Ctrl + N | New connection | +| Cmd + W | Ctrl + W | Close current tab | +| Cmd + Tab | Ctrl + Tab | Next tab | +| Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab | +| Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number | +| Cmd + , | Ctrl + , | Open settings | +| Cmd + F | Ctrl + F | Find in terminal | +| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | +| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | +| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | +| Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode | +| Cmd + Shift + S | Ctrl + Shift + S | Open snippet library | +| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | +| F11 | F11 | Toggle fullscreen | diff --git a/docs/quick-start.md b/docs/quick-start.md index 55502e6..2a60e3e 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -42,18 +42,18 @@ The upload picker lists folders only and marks permissions: ## Common shortcuts -| macOS Shortcut | Windows/Linux Shortcut | Action | -| ------------- | ----------------------- | ------------------------ | -| Cmd + K | Ctrl + K | Command palette | -| Cmd + N | Ctrl + N | New connection | -| Cmd + W | Ctrl + W | Close tab | -| Cmd + F | Ctrl + F | Find in terminal | -| Cmd + Shift + S | Ctrl + Shift + S | Snippet library | -| Cmd + Shift + B | Ctrl + Shift + B | Broadcast input | -| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | -| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | -| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | -| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | +| macOS Shortcut | Windows/Linux Shortcut | Action | +| --------------- | ---------------------- | ------------------------ | +| Cmd + K | Ctrl + K | Command palette | +| Cmd + N | Ctrl + N | New connection | +| Cmd + W | Ctrl + W | Close tab | +| Cmd + F | Ctrl + F | Find in terminal | +| Cmd + Shift + S | Ctrl + Shift + S | Snippet library | +| Cmd + Shift + B | Ctrl + Shift + B | Broadcast input | +| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | +| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | +| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | +| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | ## Need more detail? diff --git a/frontend/src/css/forms.css b/frontend/src/css/forms.css index 1aa68a3..f971625 100644 --- a/frontend/src/css/forms.css +++ b/frontend/src/css/forms.css @@ -8,132 +8,132 @@ Form panel (profile editor container) ----------------------------------------------------------------------- */ .form-panel { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } .form-panel-header { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; } .form-panel-title { - font-size: var(--text-md); - font-weight: var(--weight-semibold); - color: var(--text-primary); - flex: 1; + font-size: var(--text-md); + font-weight: var(--weight-semibold); + color: var(--text-primary); + flex: 1; } .form-panel-body { - flex: 1; - overflow-y: auto; - padding: var(--space-4); + flex: 1; + overflow-y: auto; + padding: var(--space-4); } .form-panel-footer { - display: flex; - align-items: center; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; } /* ----------------------------------------------------------------------- Form sections (disclosure-based grouping) ----------------------------------------------------------------------- */ .form-section { - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - margin-bottom: var(--space-4); - overflow: hidden; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + margin-bottom: var(--space-4); + overflow: hidden; } .form-section-header { - display: flex; - align-items: center; - gap: var(--space-2); - width: 100%; - padding: var(--space-3) var(--space-4); - font-size: var(--text-sm); - font-weight: var(--weight-semibold); - color: var(--text-primary); - cursor: pointer; - user-select: none; - -webkit-user-select: none; - background: var(--bg-elevated); - border: none; - cursor: pointer; - user-select: none; - text-align: left; - transition: background var(--transition-fast); + display: flex; + align-items: center; + gap: var(--space-2); + width: 100%; + padding: var(--space-3) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + color: var(--text-primary); + cursor: pointer; + user-select: none; + -webkit-user-select: none; + background: var(--bg-elevated); + border: none; + cursor: pointer; + user-select: none; + text-align: left; + transition: background var(--transition-fast); } .form-section-header:hover { - background: var(--bg-hover); + background: var(--bg-hover); } .form-section-chevron { - width: 14px; - height: 14px; - color: var(--text-tertiary); - transition: transform var(--transition-normal); - flex-shrink: 0; + width: 14px; + height: 14px; + color: var(--text-tertiary); + transition: transform var(--transition-normal); + flex-shrink: 0; } .form-section.is-open .form-section-chevron { - transform: rotate(90deg); + transform: rotate(90deg); } .form-section-icon { - width: 16px; - text-align: center; - flex-shrink: 0; - font-size: var(--text-sm); - color: var(--text-tertiary); + width: 16px; + text-align: center; + flex-shrink: 0; + font-size: var(--text-sm); + color: var(--text-tertiary); } .form-section-body, .form-section-content { - padding: var(--space-4); - display: none; + padding: var(--space-4); + display: none; } .form-section.is-open .form-section-body, .form-section.is-open .form-section-content { - display: block; + display: block; } /* ----------------------------------------------------------------------- Field grid (two-column layout for compact forms) ----------------------------------------------------------------------- */ .form-fields { - display: flex; - flex-direction: column; - gap: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-3); } .form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-3); + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); } /* Single-column override */ .form-row.full-width { - grid-template-columns: 1fr; + grid-template-columns: 1fr; } /* Three-column for port + protocol combos */ .form-row.triple { - grid-template-columns: 1fr 100px 120px; + grid-template-columns: 1fr 100px 120px; } /* ----------------------------------------------------------------------- @@ -143,14 +143,14 @@ .proto-only-ssh, .proto-only-telnet, .proto-only-raw { - display: none; + display: none; } .protocol-ssh .proto-only-ssh, .protocol-telnet .proto-only-telnet, .protocol-raw .proto-only-raw, .protocol-github .proto-only-github { - display: flex; + display: flex; } /* For block-level protocol fields */ @@ -158,415 +158,423 @@ .protocol-telnet .proto-only-telnet.form-section, .protocol-raw .proto-only-raw.form-section, .protocol-github .proto-only-github.form-section { - display: block; + display: block; } /* ----------------------------------------------------------------------- Tag input ----------------------------------------------------------------------- */ .tag-input-wrapper { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-1); - min-height: 30px; - padding: var(--space-1) var(--space-2); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - cursor: text; - transition: border-color var(--transition-fast); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-1); + min-height: 30px; + padding: var(--space-1) var(--space-2); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + cursor: text; + transition: border-color var(--transition-fast); } .tag-input-wrapper:hover { - border-color: var(--border-strong); + border-color: var(--border-strong); } .tag-input-wrapper:focus-within { - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-muted); + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-muted); } .tag-item { - display: inline-flex; - align-items: center; - gap: 3px; - height: 20px; - padding: 0 var(--space-2); - font-size: var(--text-xs); - color: var(--accent); - background: var(--accent-muted); - border-radius: var(--radius-sm); - white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 3px; + height: 20px; + padding: 0 var(--space-2); + font-size: var(--text-xs); + color: var(--accent); + background: var(--accent-muted); + border-radius: var(--radius-sm); + white-space: nowrap; } .tag-item-remove { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - font-size: 10px; - cursor: pointer; - border-radius: 50%; - color: var(--accent); - transition: background var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + font-size: 10px; + cursor: pointer; + border-radius: 50%; + color: var(--accent); + transition: background var(--transition-fast); } .tag-item-remove:hover { - background: var(--accent-strong); + background: var(--accent-strong); } .tag-input { - flex: 1; - min-width: 60px; - border: none; - background: transparent; - font-size: var(--text-sm); - color: var(--text-primary); - outline: none; - height: 22px; + flex: 1; + min-width: 60px; + border: none; + background: transparent; + font-size: var(--text-sm); + color: var(--text-primary); + outline: none; + height: 22px; } .tag-input::placeholder { - color: var(--text-disabled); + color: var(--text-disabled); } /* ----------------------------------------------------------------------- Jump host / proxy list ----------------------------------------------------------------------- */ .jump-host-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .jump-host-item { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - font-size: var(--text-sm); + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + font-size: var(--text-sm); } .jump-host-item .jump-host-order { - font-size: var(--text-xs); - color: var(--text-disabled); - min-width: 16px; - text-align: center; + font-size: var(--text-xs); + color: var(--text-disabled); + min-width: 16px; + text-align: center; } .jump-host-item .jump-host-value { - flex: 1; - font-family: var(--font-mono); - color: var(--text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + flex: 1; + font-family: var(--font-mono); + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .jump-host-item .jump-host-actions { - display: flex; - gap: var(--space-1); - opacity: 0; - transition: opacity var(--transition-fast); + display: flex; + gap: var(--space-1); + opacity: 0; + transition: opacity var(--transition-fast); } .jump-host-item:hover .jump-host-actions { - opacity: 1; + opacity: 1; } .jump-host-add { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2); - font-size: var(--text-sm); - color: var(--text-tertiary); - border: 1px dashed var(--border-default); - border-radius: var(--radius-md); - cursor: pointer; - transition: border-color var(--transition-fast), color var(--transition-fast); + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2); + font-size: var(--text-sm); + color: var(--text-tertiary); + border: 1px dashed var(--border-default); + border-radius: var(--radius-md); + cursor: pointer; + transition: + border-color var(--transition-fast), + color var(--transition-fast); } .jump-host-add:hover { - border-color: var(--accent); - color: var(--accent); + border-color: var(--accent); + color: var(--accent); } /* ----------------------------------------------------------------------- Key file selector row ----------------------------------------------------------------------- */ .file-selector { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } .file-selector .form-input { - flex: 1; + flex: 1; } .file-selector .btn { - flex-shrink: 0; + flex-shrink: 0; } /* File path display */ .file-path { - font-family: var(--font-mono); - font-size: var(--text-xs); - color: var(--text-tertiary); - background: var(--bg-input); - padding: var(--space-1) var(--space-2); - border-radius: var(--radius-sm); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-input); + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* ----------------------------------------------------------------------- Inline validation ----------------------------------------------------------------------- */ .form-group.is-valid .form-input { - border-color: var(--success); + border-color: var(--success); } .form-group.is-valid .form-input:focus { - box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.20); + box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.2); } .form-group.is-invalid .form-input { - border-color: var(--error); + border-color: var(--error); } .form-group.is-invalid .form-input:focus { - box-shadow: 0 0 0 2px rgba(239, 83, 80, 0.20); + box-shadow: 0 0 0 2px rgba(239, 83, 80, 0.2); } .form-validation-icon { - position: absolute; - right: var(--space-3); - top: 50%; - transform: translateY(-50%); - font-size: var(--text-sm); + position: absolute; + right: var(--space-3); + top: 50%; + transform: translateY(-50%); + font-size: var(--text-sm); } -.form-group.is-valid .form-validation-icon { color: var(--success); } -.form-group.is-invalid .form-validation-icon { color: var(--error); } +.form-group.is-valid .form-validation-icon { + color: var(--success); +} +.form-group.is-invalid .form-validation-icon { + color: var(--error); +} /* ----------------------------------------------------------------------- Help text under sections ----------------------------------------------------------------------- */ .form-section-help { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-4) var(--space-3); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-4) var(--space-3); + line-height: var(--leading-relaxed); } /* ----------------------------------------------------------------------- Colour preview (for terminal colour settings) ----------------------------------------------------------------------- */ .colour-preview { - display: inline-block; - width: 18px; - height: 18px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-default); - vertical-align: middle; - cursor: pointer; + display: inline-block; + width: 18px; + height: 18px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-default); + vertical-align: middle; + cursor: pointer; } /* ----------------------------------------------------------------------- Number input with stepper ----------------------------------------------------------------------- */ .form-number { - display: flex; - align-items: center; + display: flex; + align-items: center; } .form-number .form-input { - border-radius: var(--radius-md) 0 0 var(--radius-md); - text-align: center; - width: 60px; - /* Hide browser spinners */ - -moz-appearance: textfield; + border-radius: var(--radius-md) 0 0 var(--radius-md); + text-align: center; + width: 60px; + /* Hide browser spinners */ + -moz-appearance: textfield; } .form-number .form-input::-webkit-inner-spin-button, .form-number .form-input::-webkit-outer-spin-button { - appearance: none; + appearance: none; } .form-number-btn { - width: 24px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-sm); - color: var(--text-secondary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-left: none; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); + width: 24px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-sm); + color: var(--text-secondary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-left: none; + cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); } .form-number-btn:last-child { - border-radius: 0 var(--radius-md) var(--radius-md) 0; + border-radius: 0 var(--radius-md) var(--radius-md) 0; } .form-number-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); + background: var(--bg-hover); + color: var(--text-primary); } /* ----------------------------------------------------------------------- Form action row (test connection, etc.) ----------------------------------------------------------------------- */ .form-actions-inline { - display: flex; - align-items: center; - gap: var(--space-2); - padding-top: var(--space-3); - border-top: 1px solid var(--border-subtle); - margin-top: var(--space-3); + display: flex; + align-items: center; + gap: var(--space-2); + padding-top: var(--space-3); + border-top: 1px solid var(--border-subtle); + margin-top: var(--space-3); } .form-actions-inline .btn { - flex-shrink: 0; + flex-shrink: 0; } .form-actions-inline .form-actions-status { - font-size: var(--text-xs); - color: var(--text-tertiary); - flex: 1; + font-size: var(--text-xs); + color: var(--text-tertiary); + flex: 1; } /* ----------------------------------------------------------------------- Form hint — inline help text below fields ----------------------------------------------------------------------- */ .form-hint { - font-size: var(--text-xs); - color: var(--text-tertiary); - margin-top: var(--space-1); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--text-tertiary); + margin-top: var(--space-1); + line-height: var(--leading-relaxed); } .form-hint.secret-error { - color: var(--danger); + color: var(--danger); } .form-success { - font-size: var(--text-xs); - color: var(--success); - margin-top: var(--space-1); - line-height: var(--leading-relaxed); + font-size: var(--text-xs); + color: var(--success); + margin-top: var(--space-1); + line-height: var(--leading-relaxed); } /* ----------------------------------------------------------------------- Toggle wrapper — label + toggle switch in a row ----------------------------------------------------------------------- */ .form-toggle-wrapper { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - padding: var(--space-1) 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-1) 0; } .form-toggle-label { - font-size: var(--text-sm); - color: var(--text-secondary); - flex: 1; + font-size: var(--text-sm); + color: var(--text-secondary); + flex: 1; } /* ----------------------------------------------------------------------- Jump host rows ----------------------------------------------------------------------- */ .jump-hosts-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .jump-host-row { - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-2); - background: var(--surface-2); - border-radius: var(--radius-sm); - border: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-2); + background: var(--surface-2); + border-radius: var(--radius-sm); + border: 1px solid var(--border); } .jump-host-fields { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } /* ----------------------------------------------------------------------- Port-forwarding rows ----------------------------------------------------------------------- */ .port-forwards-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .port-forward-row { - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-2); - background: var(--surface-2); - border-radius: var(--radius-sm); - border: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-2); + background: var(--surface-2); + border-radius: var(--radius-sm); + border: 1px solid var(--border); } .port-forward-fields { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .port-forward-arrow { - color: var(--text-muted); - font-size: var(--text-sm); - flex-shrink: 0; + color: var(--text-muted); + font-size: var(--text-sm); + flex-shrink: 0; } .port-forward-enabled-label { - display: flex; - gap: var(--space-1); - align-items: center; - font-size: var(--text-xs); - color: var(--text-secondary); - white-space: nowrap; - cursor: pointer; + display: flex; + gap: var(--space-1); + align-items: center; + font-size: var(--text-xs); + color: var(--text-secondary); + white-space: nowrap; + cursor: pointer; } .port-forward-enabled-label input[type="checkbox"] { - accent-color: var(--accent); + accent-color: var(--accent); } /* ----------------------------------------------------------------------- Payload preset rows ----------------------------------------------------------------------- */ .payload-presets-list { - display: flex; - flex-direction: column; - gap: var(--space-2); + display: flex; + flex-direction: column; + gap: var(--space-2); } .preset-row { - display: grid; - grid-template-columns: 120px 1fr 100px auto; - gap: var(--space-2); - align-items: center; + display: grid; + grid-template-columns: 120px 1fr 100px auto; + gap: var(--space-2); + align-items: center; } /* ----------------------------------------------------------------------- @@ -574,152 +582,154 @@ ----------------------------------------------------------------------- */ .form-input-sm, .form-select-sm { - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } /* ----------------------------------------------------------------------- Login Script Editor — expect-style send/wait steps ----------------------------------------------------------------------- */ .form-help { - font-size: var(--text-xs); - color: var(--text-muted); - margin-bottom: var(--space-3); - line-height: 1.5; + font-size: var(--text-xs); + color: var(--text-muted); + margin-bottom: var(--space-3); + line-height: 1.5; } .login-script-list { - display: flex; - flex-direction: column; - gap: var(--space-2); - margin-bottom: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-2); + margin-bottom: var(--space-3); } .login-script-empty { - font-size: var(--text-xs); - color: var(--text-muted); - font-style: italic; - padding: var(--space-2) 0; + font-size: var(--text-xs); + color: var(--text-muted); + font-style: italic; + padding: var(--space-2) 0; } .login-script-row { - display: flex; - align-items: center; - gap: var(--space-2); - background: var(--bg-base); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: var(--space-2); + display: flex; + align-items: center; + gap: var(--space-2); + background: var(--bg-base); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: var(--space-2); } .login-script-type { - width: 90px; - min-width: 90px; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + width: 90px; + min-width: 90px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-value { - flex: 1; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + flex: 1; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-timeout { - width: 60px; - min-width: 60px; - font-size: var(--text-xs); - padding: var(--space-1) var(--space-2); - height: 30px; + width: 60px; + min-width: 60px; + font-size: var(--text-xs); + padding: var(--space-1) var(--space-2); + height: 30px; } .login-script-timeout-label { - font-size: var(--text-xs); - color: var(--text-muted); - min-width: 10px; + font-size: var(--text-xs); + color: var(--text-muted); + min-width: 10px; } .login-script-actions { - display: flex; - gap: 2px; - flex-shrink: 0; + display: flex; + gap: 2px; + flex-shrink: 0; } .login-script-btn { - background: var(--bg-elevated); - color: var(--text-secondary); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-xs); - width: 24px; - height: 24px; - font-size: 10px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s, color 0.15s; + background: var(--bg-elevated); + color: var(--text-secondary); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-xs); + width: 24px; + height: 24px; + font-size: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: + background 0.15s, + color 0.15s; } .login-script-btn:hover { - background: var(--bg-surface); - color: var(--text-primary); + background: var(--bg-surface); + color: var(--text-primary); } .login-script-btn-remove:hover { - background: var(--danger); - color: #fff; + background: var(--danger); + color: #fff; } .login-script-add-row { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } /* ----------------------------------------------------------------------- TOTP / 2FA — secret input row with action buttons ----------------------------------------------------------------------- */ .totp-input-row { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .totp-input-row .form-input { - flex: 1; + flex: 1; } /* SSH key path row */ .key-path-row { - display: flex; - gap: var(--space-2); - align-items: center; + display: flex; + gap: var(--space-2); + align-items: center; } .key-path-row .form-input { - flex: 1; + flex: 1; } .jump-hosts-list .form-error { - margin-bottom: var(--space-2); + margin-bottom: var(--space-2); } .totp-toggle, .totp-test, .totp-save, .totp-delete { - flex-shrink: 0; - white-space: nowrap; + flex-shrink: 0; + white-space: nowrap; } .btn-danger { - background: var(--error); - color: #fff; - border: 1px solid transparent; + background: var(--error); + color: #fff; + border: 1px solid transparent; } .btn-danger:hover { - background: #d32f2f; + background: #d32f2f; } diff --git a/frontend/src/css/terminal.css b/frontend/src/css/terminal.css index 51cb3cd..437afad 100644 --- a/frontend/src/css/terminal.css +++ b/frontend/src/css/terminal.css @@ -7,10 +7,10 @@ Terminal wrapper — fills the workspace panel ----------------------------------------------------------------------- */ .terminal-wrapper { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; } /* ----------------------------------------------------------------------- @@ -18,223 +18,227 @@ Shows connection info above the terminal. ----------------------------------------------------------------------- */ .terminal-header { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-1) var(--space-3); - font-size: var(--text-xs); - color: var(--text-tertiary); - background: var(--bg-surface); - border-bottom: 1px solid var(--border-subtle); - flex-shrink: 0; - min-height: 24px; + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-1) var(--space-3); + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-surface); + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; + min-height: 24px; } .terminal-header-label { - font-weight: var(--weight-medium); - color: var(--text-secondary); + font-weight: var(--weight-medium); + color: var(--text-secondary); } .terminal-header-value { - font-family: var(--font-mono); - color: var(--text-tertiary); + font-family: var(--font-mono); + color: var(--text-tertiary); } .terminal-header-separator { - width: 1px; - height: 10px; - background: var(--border-subtle); + width: 1px; + height: 10px; + background: var(--border-subtle); } .terminal-header-spacer { - flex: 1; + flex: 1; } .terminal-header-actions { - display: flex; - gap: var(--space-1); + display: flex; + gap: var(--space-1); } /* ----------------------------------------------------------------------- xterm.js container ----------------------------------------------------------------------- */ .terminal-container { - flex: 1; - position: relative; - overflow: hidden; - padding: 0; + flex: 1; + position: relative; + overflow: hidden; + padding: 0; } /* ----------------------------------------------------------------------- Disconnected overlay ----------------------------------------------------------------------- */ .session-panel.is-disconnected > *:not(.terminal-disconnect-overlay) { - filter: blur(2px); - pointer-events: none; + filter: blur(2px); + pointer-events: none; } .terminal-disconnect-overlay { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(10, 14, 18, 0.68); - z-index: 6; - opacity: 0; - visibility: hidden; - transition: opacity var(--transition-fast), visibility var(--transition-fast); + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(10, 14, 18, 0.68); + z-index: 6; + opacity: 0; + visibility: hidden; + transition: + opacity var(--transition-fast), + visibility var(--transition-fast); } .terminal-disconnect-overlay.is-visible { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } .terminal-disconnect-content { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-3); - text-align: center; - padding: var(--space-4) var(--space-5); - background: var(--bg-elevated); - border: 1px solid var(--border-default); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + text-align: center; + padding: var(--space-4) var(--space-5); + background: var(--bg-elevated); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); } .terminal-disconnect-title { - font-size: var(--text-lg); - letter-spacing: 0.45em; - text-indent: 0.45em; - text-transform: uppercase; - color: var(--text-primary); - font-weight: var(--weight-semibold); + font-size: var(--text-lg); + letter-spacing: 0.45em; + text-indent: 0.45em; + text-transform: uppercase; + color: var(--text-primary); + font-weight: var(--weight-semibold); } .terminal-disconnect-message { - font-size: var(--text-sm); - color: var(--text-secondary); - max-width: 360px; + font-size: var(--text-sm); + color: var(--text-secondary); + max-width: 360px; } /* xterm.js overrides for consistent look */ .terminal-container .xterm, .session-panel .xterm { - flex: 1; - height: 100%; - min-height: 0; - padding: 3px 0 0 3px; + flex: 1; + height: 100%; + min-height: 0; + padding: 3px 0 0 3px; } .terminal-container .xterm-screen, .session-panel .xterm-screen { - height: 100%; + height: 100%; } .terminal-container .xterm-viewport, .session-panel .xterm-viewport { - /* Allow xterm's own scrollbar to work */ - overflow-y: auto !important; + /* Allow xterm's own scrollbar to work */ + overflow-y: auto !important; } .terminal-container .xterm-viewport::-webkit-scrollbar, .session-panel .xterm-viewport::-webkit-scrollbar { - width: 6px; + width: 6px; } .terminal-container .xterm-viewport::-webkit-scrollbar-track, .session-panel .xterm-viewport::-webkit-scrollbar-track { - background: transparent; + background: transparent; } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb, .session-panel .xterm-viewport::-webkit-scrollbar-thumb { - background: var(--border-strong); - border-radius: var(--radius-full); + background: var(--border-strong); + border-radius: var(--radius-full); } .terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover, .session-panel .xterm-viewport::-webkit-scrollbar-thumb:hover { - background: var(--text-tertiary); + background: var(--text-tertiary); } /* Ensure cursor blink is visible */ .terminal-container .xterm-cursor-layer, .session-panel .xterm-cursor-layer { - z-index: 1; + z-index: 1; } /* Selection styling within terminal */ .terminal-container .xterm .xterm-selection div, .session-panel .xterm .xterm-selection div { - background: var(--accent-strong) !important; + background: var(--accent-strong) !important; } /* ----------------------------------------------------------------------- Terminal search overlay ----------------------------------------------------------------------- */ .terminal-search { - position: absolute; - top: var(--space-2); - right: var(--space-4); - display: flex; - align-items: center; - gap: var(--space-1); - padding: var(--space-1); - background: var(--bg-overlay); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - box-shadow: var(--shadow-md); - z-index: 10; - opacity: 0; - visibility: hidden; - transform: translateY(-8px); - transition: - opacity var(--transition-fast), - visibility var(--transition-fast), - transform var(--transition-fast); + position: absolute; + top: var(--space-2); + right: var(--space-4); + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1); + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + z-index: 10; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: + opacity var(--transition-fast), + visibility var(--transition-fast), + transform var(--transition-fast); } .terminal-search.is-visible { - opacity: 1; - visibility: visible; - transform: translateY(0); + opacity: 1; + visibility: visible; + transform: translateY(0); } .terminal-search input { - width: 200px; - height: 24px; - padding: 0 var(--space-2); - font-size: var(--text-sm); - font-family: var(--font-mono); - color: var(--text-primary); - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + width: 200px; + height: 24px; + padding: 0 var(--space-2); + font-size: var(--text-sm); + font-family: var(--font-mono); + color: var(--text-primary); + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); } .terminal-search input:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } .terminal-search-count { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-1); - white-space: nowrap; - min-width: 70px; + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-1); + white-space: nowrap; + min-width: 70px; } .terminal-search-toggle { - font-family: var(--font-mono); - font-size: var(--text-xs); - opacity: 0.5; - transition: opacity var(--transition-fast), color var(--transition-fast); + font-family: var(--font-mono); + font-size: var(--text-xs); + opacity: 0.5; + transition: + opacity var(--transition-fast), + color var(--transition-fast); } .terminal-search-toggle.is-active { - opacity: 1; - color: var(--accent); + opacity: 1; + color: var(--accent); } /* ----------------------------------------------------------------------- @@ -243,231 +247,233 @@ /* RAW text view — scrollable, monospace output */ .raw-text-view { - flex: 1; - overflow-y: auto; - padding: var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-sm); - line-height: var(--leading-relaxed); - color: var(--text-primary); - background: var(--bg-base); - white-space: pre-wrap; - word-break: break-all; + flex: 1; + overflow-y: auto; + padding: var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: var(--leading-relaxed); + color: var(--text-primary); + background: var(--bg-base); + white-space: pre-wrap; + word-break: break-all; } /* RAW hex view */ .raw-hex-view { - flex: 1; - overflow-y: auto; - padding: var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-xs); - line-height: var(--leading-relaxed); - color: var(--text-primary); - background: var(--bg-base); + flex: 1; + overflow-y: auto; + padding: var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: var(--leading-relaxed); + color: var(--text-primary); + background: var(--bg-base); } .raw-hex-row { - display: flex; - gap: var(--space-4); + display: flex; + gap: var(--space-4); } .raw-hex-offset { - color: var(--text-tertiary); - min-width: 60px; + color: var(--text-tertiary); + min-width: 60px; } .raw-hex-bytes { - min-width: 350px; - letter-spacing: 0.08em; + min-width: 350px; + letter-spacing: 0.08em; } .raw-hex-bytes .byte-highlight { - color: var(--accent); + color: var(--accent); } .raw-hex-ascii { - color: var(--text-secondary); + color: var(--text-secondary); } .raw-hex-ascii .non-printable { - color: var(--text-disabled); + color: var(--text-disabled); } /* RAW split view (text on left, hex on right) */ .raw-split-view { - flex: 1; - display: flex; - overflow: hidden; + flex: 1; + display: flex; + overflow: hidden; } .raw-split-view .raw-text-view, .raw-split-view .raw-hex-view { - flex: 1; + flex: 1; } .raw-split-divider { - width: 1px; - background: var(--border-subtle); - flex-shrink: 0; + width: 1px; + background: var(--border-subtle); + flex-shrink: 0; } /* ----------------------------------------------------------------------- RAW mode view toggle buttons ----------------------------------------------------------------------- */ .raw-view-toggle { - display: flex; - gap: 1px; - background: var(--border-subtle); - border-radius: var(--radius-md); - overflow: hidden; + display: flex; + gap: 1px; + background: var(--border-subtle); + border-radius: var(--radius-md); + overflow: hidden; } .raw-view-toggle-btn { - padding: 0 var(--space-2); - height: 22px; - font-size: var(--text-xs); - font-weight: var(--weight-medium); - color: var(--text-tertiary); - background: var(--bg-surface); - border: none; - cursor: pointer; - transition: background var(--transition-fast), color var(--transition-fast); + padding: 0 var(--space-2); + height: 22px; + font-size: var(--text-xs); + font-weight: var(--weight-medium); + color: var(--text-tertiary); + background: var(--bg-surface); + border: none; + cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); } .raw-view-toggle-btn:hover { - color: var(--text-secondary); + color: var(--text-secondary); } .raw-view-toggle-btn.active { - background: var(--bg-active); - color: var(--text-primary); + background: var(--bg-active); + color: var(--text-primary); } /* ----------------------------------------------------------------------- Send bar (RAW TCP mode — manual data entry) ----------------------------------------------------------------------- */ .send-bar { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - background: var(--bg-surface); - border-top: 1px solid var(--border-subtle); - flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--bg-surface); + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; } .send-bar-input { - flex: 1; - height: 28px; - padding: 0 var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--text-primary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - transition: border-color var(--transition-fast); + flex: 1; + height: 28px; + padding: 0 var(--space-3); + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--text-primary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + transition: border-color var(--transition-fast); } .send-bar-input:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } .send-bar-input::placeholder { - color: var(--text-disabled); + color: var(--text-disabled); } /* Line ending selector */ .send-bar-eol { - height: 28px; - padding: 0 var(--space-2); - font-size: var(--text-xs); - color: var(--text-secondary); - background: var(--bg-input); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - appearance: none; - cursor: pointer; - min-width: 60px; + height: 28px; + padding: 0 var(--space-2); + font-size: var(--text-xs); + color: var(--text-secondary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + appearance: none; + cursor: pointer; + min-width: 60px; } .send-bar-eol:focus { - border-color: var(--accent); - outline: none; + border-color: var(--accent); + outline: none; } /* Encoding selector */ .send-bar-encoding { - font-size: var(--text-xs); - color: var(--text-tertiary); - padding: 0 var(--space-1); + font-size: var(--text-xs); + color: var(--text-tertiary); + padding: 0 var(--space-1); } /* ----------------------------------------------------------------------- Session overlay (connecting / error states) ----------------------------------------------------------------------- */ .session-overlay { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: rgba(28, 28, 28, 0.85); - z-index: 5; + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(28, 28, 28, 0.85); + z-index: 5; } .session-overlay-connecting { - text-align: center; + text-align: center; } .session-overlay-connecting .spinner { - width: 24px; - height: 24px; - margin: 0 auto var(--space-4); + width: 24px; + height: 24px; + margin: 0 auto var(--space-4); } .session-overlay-connecting p { - font-size: var(--text-sm); - color: var(--text-secondary); + font-size: var(--text-sm); + color: var(--text-secondary); } .session-overlay-error { - text-align: center; - max-width: 360px; + text-align: center; + max-width: 360px; } .session-overlay-error h3 { - font-size: var(--text-md); - font-weight: var(--weight-semibold); - color: var(--error); - margin-bottom: var(--space-2); + font-size: var(--text-md); + font-weight: var(--weight-semibold); + color: var(--error); + margin-bottom: var(--space-2); } .session-overlay-error p { - font-size: var(--text-sm); - color: var(--text-tertiary); - line-height: var(--leading-relaxed); - margin-bottom: var(--space-4); + font-size: var(--text-sm); + color: var(--text-tertiary); + line-height: var(--leading-relaxed); + margin-bottom: var(--space-4); } .session-overlay-error .session-error-detail { - font-family: var(--font-mono); - font-size: var(--text-xs); - color: var(--text-disabled); - background: var(--bg-input); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-md); - margin-bottom: var(--space-4); - text-align: left; - max-height: 120px; - overflow-y: auto; - word-break: break-all; + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-disabled); + background: var(--bg-input); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + margin-bottom: var(--space-4); + text-align: left; + max-height: 120px; + overflow-y: auto; + word-break: break-all; } .session-overlay-actions { - display: flex; - gap: var(--space-2); + display: flex; + gap: var(--space-2); } diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js index c84db8d..b9d54f5 100644 --- a/frontend/src/js/profiles.js +++ b/frontend/src/js/profiles.js @@ -1659,7 +1659,9 @@ `; - const keyInput = row.querySelector(`input[name="jumpHost-${i}-privateKeyPath"]`); + const keyInput = row.querySelector( + `input[name="jumpHost-${i}-privateKeyPath"]`, + ); const browseBtn = row.querySelector(".jump-host-browse"); const error = createElement("span", "form-error"); error.style.display = "none"; @@ -1712,7 +1714,9 @@ _attachKeyPathValidation(input, errorEl, successEl) { if (!input || !errorEl) return; - input.addEventListener("blur", () => this._validateKeyPath(input, errorEl, successEl)); + input.addEventListener("blur", () => + this._validateKeyPath(input, errorEl, successEl), + ); input.addEventListener("input", () => { errorEl.style.display = "none"; errorEl.textContent = ""; @@ -1757,24 +1761,23 @@ } return; } - return window.go.api.Service.FileIsPrivateKey(value) - .then((isKey) => { - if (!isKey) { - errorEl.textContent = "File is not a recognised private key."; - errorEl.style.display = "block"; - if (successEl) { - successEl.style.display = "none"; - successEl.textContent = ""; - } - return; - } - errorEl.style.display = "none"; - errorEl.textContent = ""; + return window.go.api.Service.FileIsPrivateKey(value).then((isKey) => { + if (!isKey) { + errorEl.textContent = "File is not a recognised private key."; + errorEl.style.display = "block"; if (successEl) { - successEl.textContent = "Key verified."; - successEl.style.display = "block"; + successEl.style.display = "none"; + successEl.textContent = ""; } - }); + return; + } + errorEl.style.display = "none"; + errorEl.textContent = ""; + if (successEl) { + successEl.textContent = "Key verified."; + successEl.style.display = "block"; + } + }); }) .catch(() => { errorEl.textContent = "Could not verify this path."; diff --git a/frontend/src/js/tabs.js b/frontend/src/js/tabs.js index cf27a3a..04bae0d 100644 --- a/frontend/src/js/tabs.js +++ b/frontend/src/js/tabs.js @@ -1,32 +1,38 @@ -(function() { -/** - * Nexus Terminal — tab management. - * Handles the tab bar: adding, removing, reordering, renaming, - * pinning tabs, and context menus. - */ +(function () { + /** + * Nexus Terminal — tab management. + * Handles the tab bar: adding, removing, reordering, renaming, + * pinning tabs, and context menus. + */ -const { createElement, escapeHtml, protocolColour, stateLabel, dismissContextMenus } = window.NexusTerminal; + const { + createElement, + escapeHtml, + protocolColour, + stateLabel, + dismissContextMenus, + } = window.NexusTerminal; -class TabManager { + class TabManager { /** * @param {App} app */ constructor(app) { - this.app = app; + this.app = app; - /** @type {HTMLElement} The inner tab list where tab elements are inserted. */ - this.tabList = document.getElementById('tab-list'); + /** @type {HTMLElement} The inner tab list where tab elements are inserted. */ + this.tabList = document.getElementById("tab-list"); - /** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */ - this.tabBar = document.getElementById('tab-bar'); + /** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */ + this.tabBar = document.getElementById("tab-bar"); - /** @type {HTMLElement} The container for terminal/viewer panels. */ - this.panelContainer = document.getElementById('panel-container'); + /** @type {HTMLElement} The container for terminal/viewer panels. */ + this.panelContainer = document.getElementById("panel-container"); - /** @type {string[]} Ordered list of session IDs. */ - this.order = []; + /** @type {string[]} Ordered list of session IDs. */ + this.order = []; - this._setupDragAndDrop(); + this._setupDragAndDrop(); } // ── Public API ─────────────────────────────────────────────────── @@ -36,21 +42,21 @@ class TabManager { * @param {Object} sessionInfo */ addTab(sessionInfo) { - this.order.push(sessionInfo.id); - this._renderTab(sessionInfo); + this.order.push(sessionInfo.id); + this._renderTab(sessionInfo); - // Create panel container for this session's terminal/viewer - const panel = createElement('div', 'session-panel', { - id: `panel-${sessionInfo.id}`, - dataset: { sessionId: sessionInfo.id }, - }); - panel.style.display = 'none'; - this.panelContainer.appendChild(panel); + // Create panel container for this session's terminal/viewer + const panel = createElement("div", "session-panel", { + id: `panel-${sessionInfo.id}`, + dataset: { sessionId: sessionInfo.id }, + }); + panel.style.display = "none"; + this.panelContainer.appendChild(panel); - // Hide the empty-state BEFORE activating the panel so the - // panel-container gets the full workspace height immediately. - this._updateEmptyState(); - this.setActive(sessionInfo.id); + // Hide the empty-state BEFORE activating the panel so the + // panel-container gets the full workspace height immediately. + this._updateEmptyState(); + this.setActive(sessionInfo.id); } /** @@ -58,30 +64,33 @@ class TabManager { * @param {string} sessionId */ removeTab(sessionId) { - const idx = this.order.indexOf(sessionId); - if (idx !== -1) this.order.splice(idx, 1); + const idx = this.order.indexOf(sessionId); + if (idx !== -1) this.order.splice(idx, 1); - // Remove tab element - const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); - if (tabEl) tabEl.remove(); + // Remove tab element + const tabEl = this.tabList.querySelector( + `[data-session-id="${sessionId}"]`, + ); + if (tabEl) tabEl.remove(); - // Remove panel - const panel = document.getElementById(`panel-${sessionId}`); - if (panel) panel.remove(); + // Remove panel + const panel = document.getElementById(`panel-${sessionId}`); + if (panel) panel.remove(); - // If this was the active tab, activate the nearest neighbour - if (this.app.state.activeSessionId === sessionId) { - const next = this.order.length > 0 - ? this.order[Math.min(idx, this.order.length - 1)] - : null; - if (next) { - this.setActive(next); - } else { - this.app.state.setActiveSession(null); - } + // If this was the active tab, activate the nearest neighbour + if (this.app.state.activeSessionId === sessionId) { + const next = + this.order.length > 0 + ? this.order[Math.min(idx, this.order.length - 1)] + : null; + if (next) { + this.setActive(next); + } else { + this.app.state.setActiveSession(null); } + } - this._updateEmptyState(); + this._updateEmptyState(); } /** @@ -89,35 +98,35 @@ class TabManager { * @param {string} sessionId */ setActive(sessionId) { - this.app.state.setActiveSession(sessionId); + this.app.state.setActiveSession(sessionId); - // Highlight active tab - this.tabList.querySelectorAll('.tab').forEach((t) => { - t.classList.toggle('tab-active', t.dataset.sessionId === sessionId); - }); + // Highlight active tab + this.tabList.querySelectorAll(".tab").forEach((t) => { + t.classList.toggle("tab-active", t.dataset.sessionId === sessionId); + }); - // Show corresponding panel, hide others - this.panelContainer.querySelectorAll('.session-panel').forEach((p) => { - p.style.display = p.dataset.sessionId === sessionId ? 'flex' : 'none'; - }); + // Show corresponding panel, hide others + this.panelContainer.querySelectorAll(".session-panel").forEach((p) => { + p.style.display = p.dataset.sessionId === sessionId ? "flex" : "none"; + }); - // Notify sidebar - const session = this.app.state.getSession(sessionId); - if (session) { - this.app.sidebar.highlightActive(session.profileId); - } + // Notify sidebar + const session = this.app.state.getSession(sessionId); + if (session) { + this.app.sidebar.highlightActive(session.profileId); + } - // Fit the terminal for the newly active session - this.app.terminals.fitActive(); + // Fit the terminal for the newly active session + this.app.terminals.fitActive(); } /** * Closes the currently active tab. */ closeActive() { - if (this.app.state.activeSessionId) { - this.closeTab(this.app.state.activeSessionId); - } + if (this.app.state.activeSessionId) { + this.closeTab(this.app.state.activeSessionId); + } } /** @@ -125,7 +134,7 @@ class TabManager { * @param {string} sessionId */ reconnectSession(sessionId) { - return this._reconnect(sessionId); + return this._reconnect(sessionId); } /** @@ -133,25 +142,29 @@ class TabManager { * @param {string} sessionId */ async closeTab(sessionId) { - const session = this.app.state.getSession(sessionId); + const session = this.app.state.getSession(sessionId); - // Ask for confirmation if connected and the preference is set - if (session && session.state === 'connected' && this.app.state.preferences.confirmOnDisconnect) { - const ok = await this._confirmClose(sessionId); - if (!ok) return; - } + // Ask for confirmation if connected and the preference is set + if ( + session && + session.state === "connected" && + this.app.state.preferences.confirmOnDisconnect + ) { + const ok = await this._confirmClose(sessionId); + if (!ok) return; + } - try { - await window.go.api.Service.DisconnectSession(sessionId); - } catch (_) { - // Session may already be closed — carry on - } + try { + await window.go.api.Service.DisconnectSession(sessionId); + } catch (_) { + // Session may already be closed — carry on + } - this.app.terminals.destroyTerminal(sessionId); - this.app.rawViewer.destroyViewer(sessionId); - window.NexusTerminal.GitHubViewer.destroyViewer(sessionId); - this.app.state.removeSession(sessionId); - this.removeTab(sessionId); + this.app.terminals.destroyTerminal(sessionId); + this.app.rawViewer.destroyViewer(sessionId); + window.NexusTerminal.GitHubViewer.destroyViewer(sessionId); + this.app.state.removeSession(sessionId); + this.removeTab(sessionId); } /** @@ -161,12 +174,12 @@ class TabManager { * @private */ _confirmClose(sessionId) { - const createElement = window.NexusTerminal.createElement; - const label = this._tabLabel(sessionId); - return new Promise((resolve) => { - const backdrop = createElement('div', 'modal-backdrop'); - const modal = createElement('div', 'modal modal-sm'); - modal.innerHTML = ` + const createElement = window.NexusTerminal.createElement; + const label = this._tabLabel(sessionId); + return new Promise((resolve) => { + const backdrop = createElement("div", "modal-backdrop"); + const modal = createElement("div", "modal modal-sm"); + modal.innerHTML = `