UI Updates, and fix UTF8 issues

This commit is contained in:
Andy Dixon
2026-04-21 08:59:49 +01:00
parent c5debd9c77
commit 7146063da2
8 changed files with 3629 additions and 2795 deletions
+163 -160
View File
@@ -4,298 +4,301 @@
========================================================================= */ ========================================================================= */
.github-viewer { .github-viewer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
font-family: var(--font-family); font-family: var(--font-family);
} }
/* ── Header bar ────────────────────────────────────────────────────────── */ /* ── Header bar ────────────────────────────────────────────────────────── */
.github-viewer-header { .github-viewer-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5); padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5);
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.github-viewer-title-area { .github-viewer-title-area {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
flex-shrink: 0; flex-shrink: 0;
} }
.github-viewer-icon { .github-viewer-icon {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--protocol-github); color: var(--protocol-github);
opacity: 0.9; opacity: 0.9;
} }
.github-viewer-title { .github-viewer-title {
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
} }
.github-viewer-search { .github-viewer-search {
flex: 1; flex: 1;
max-width: 360px; max-width: 360px;
} }
.github-viewer-search-input { .github-viewer-search-input {
width: 100%; width: 100%;
padding: var(--space-1) var(--space-3); padding: var(--space-1) var(--space-3);
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-primary); color: var(--text-primary);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
outline: none; outline: none;
transition: border-color 0.15s; transition: border-color 0.15s;
} }
.github-viewer-search-input:focus { .github-viewer-search-input:focus {
border-color: var(--protocol-github); border-color: var(--protocol-github);
} }
.github-viewer-search-input::placeholder { .github-viewer-search-input::placeholder {
color: var(--text-muted); color: var(--text-muted);
} }
.github-viewer-refresh { .github-viewer-refresh {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-secondary); color: var(--text-secondary);
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
flex-shrink: 0; flex-shrink: 0;
} }
.github-viewer-refresh:hover { .github-viewer-refresh:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--protocol-github); color: var(--protocol-github);
border-color: var(--protocol-github); border-color: var(--protocol-github);
} }
/* ── Status bar ────────────────────────────────────────────────────────── */ /* ── Status bar ────────────────────────────────────────────────────────── */
.github-viewer-status { .github-viewer-status {
padding: var(--space-1) var(--space-4) var(--space-1) var(--space-5); padding: var(--space-1) var(--space-4) var(--space-1) var(--space-5);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-muted); color: var(--text-muted);
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.github-viewer-status-error { .github-viewer-status-error {
color: var(--red); color: var(--red);
} }
/* ── Repo list ─────────────────────────────────────────────────────────── */ /* ── Repo list ─────────────────────────────────────────────────────────── */
.github-viewer-list { .github-viewer-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5); padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5);
} }
.github-viewer-empty { .github-viewer-empty {
padding: var(--space-8) var(--space-4); padding: var(--space-8) var(--space-4);
text-align: center; text-align: center;
color: var(--text-muted); color: var(--text-muted);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
/* ── Group ─────────────────────────────────────────────────────────────── */ /* ── Group ─────────────────────────────────────────────────────────────── */
.github-group { .github-group {
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.github-group-header { .github-group-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-1) 0; padding: var(--space-1) 0;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
} }
.github-group-name { .github-group-name {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 600; font-weight: 600;
color: var(--text-secondary); color: var(--text-secondary);
text-transform: none; text-transform: none;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
.github-group-count { .github-group-count {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-muted); color: var(--text-muted);
background: var(--bg-tertiary); background: var(--bg-tertiary);
padding: 1px 6px; padding: 1px 6px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
} }
/* ── Repo card ─────────────────────────────────────────────────────────── */ /* ── Repo card ─────────────────────────────────────────────────────────── */
.github-repo-card { .github-repo-card {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
margin-bottom: 2px; margin-bottom: 2px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
transition: background 0.12s; transition: background 0.12s;
} }
.github-repo-card:hover { .github-repo-card:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.github-repo-top { .github-repo-top {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
margin-bottom: 2px; margin-bottom: 2px;
} }
.github-repo-commit { .github-repo-commit {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-muted); color: var(--text-muted);
margin-bottom: 4px; margin-bottom: 4px;
} }
.github-repo-name { .github-repo-name {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 600; font-weight: 600;
color: var(--protocol-github); color: var(--protocol-github);
} }
.github-repo-badge { .github-repo-badge {
font-size: 10px; font-size: 10px;
padding: 0 5px; padding: 0 5px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
line-height: 16px; line-height: 16px;
font-weight: 500; font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
.github-repo-actions { .github-repo-actions {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 4px; gap: 4px;
} }
.github-repo-download, .github-repo-download,
.github-repo-visit { .github-repo-visit {
padding: 0 6px; padding: 0 6px;
height: 18px; height: 18px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-secondary); color: var(--text-secondary);
font-size: 10px; font-size: 10px;
letter-spacing: 0.04em; letter-spacing: 0.04em;
cursor: pointer; cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s; transition:
background 0.15s,
color 0.15s,
border-color 0.15s;
} }
.github-repo-download:hover, .github-repo-download:hover,
.github-repo-visit:hover { .github-repo-visit:hover {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--protocol-github); color: var(--protocol-github);
border-color: var(--protocol-github); border-color: var(--protocol-github);
} }
.github-repo-private { .github-repo-private {
background: rgba(255, 167, 38, 0.12); background: rgba(255, 167, 38, 0.12);
color: #ffa726; color: #ffa726;
} }
.github-repo-fork { .github-repo-fork {
background: rgba(78, 205, 196, 0.12); background: rgba(78, 205, 196, 0.12);
color: #4ecdc4; color: #4ecdc4;
} }
.github-repo-archived { .github-repo-archived {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.06);
color: var(--text-muted); color: var(--text-muted);
} }
.github-repo-desc { .github-repo-desc {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.4; line-height: 1.4;
margin-bottom: 4px; margin-bottom: 4px;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.github-repo-meta { .github-repo-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
font-size: 11px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
} }
.github-repo-lang { .github-repo-lang {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
} }
.github-lang-dot { .github-lang-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
flex-shrink: 0; flex-shrink: 0;
} }
.github-repo-stars { .github-repo-stars {
/* Star colour is inherent in the text */ /* Star colour is inherent in the text */
} }
.github-repo-pushed { .github-repo-pushed {
/* Relative time text */ /* Relative time text */
} }
/* ── Scrollbar styling ─────────────────────────────────────────────────── */ /* ── Scrollbar styling ─────────────────────────────────────────────────── */
.github-viewer-list::-webkit-scrollbar { .github-viewer-list::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.github-viewer-list::-webkit-scrollbar-track { .github-viewer-list::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.github-viewer-list::-webkit-scrollbar-thumb { .github-viewer-list::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb); background: var(--scrollbar-thumb);
border-radius: 3px; border-radius: 3px;
} }
.github-viewer-list::-webkit-scrollbar-thumb:hover { .github-viewer-list::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover); background: var(--scrollbar-thumb-hover);
} }
+236 -214
View File
@@ -7,387 +7,409 @@
Sidebar header (logo / title / drag region) Sidebar header (logo / title / drag region)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-header { .sidebar-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
min-height: var(--tab-bar-height); min-height: var(--tab-bar-height);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-logo { .sidebar-logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
} }
.sidebar-logo svg { .sidebar-logo svg {
width: 18px; width: 18px;
height: 18px; height: 18px;
color: var(--accent); color: var(--accent);
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-title { .sidebar-title {
font-size: var(--text-sm); font-size: var(--text-sm);
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
color: var(--text-primary); color: var(--text-primary);
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar search Sidebar search
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-search { .sidebar-search {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
flex-shrink: 0; flex-shrink: 0;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar navigation (scrollable area) Sidebar navigation (scrollable area)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-nav { .sidebar-nav {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-bottom: var(--space-2); padding-bottom: var(--space-2);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Bulk actions bar (multi-select) Bulk actions bar (multi-select)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-bulk-actions { .sidebar-bulk-actions {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 2; z-index: 2;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
} }
.sidebar-bulk-count { .sidebar-bulk-count {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-secondary); color: var(--text-secondary);
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
} }
.sidebar-bulk-buttons { .sidebar-bulk-buttons {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-1); gap: var(--space-1);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sections (Favourites, Recent, All Connections) Sections (Favourites, Recent, All Connections)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-section { .sidebar-section {
/* Each section can be collapsed */ /* Each section can be collapsed */
} }
.sidebar-section + .sidebar-section { .sidebar-section + .sidebar-section {
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
} }
.sidebar-section-header { .sidebar-section-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
width: 100%; width: 100%;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
font-size: var(--text-xs); font-size: var(--text-xs);
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
color: var(--text-tertiary); color: var(--text-tertiary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.06em;
cursor: pointer; cursor: pointer;
transition: color var(--transition-fast), background var(--transition-fast); transition:
-webkit-user-select: none; color var(--transition-fast),
user-select: none; background var(--transition-fast);
text-align: left; -webkit-user-select: none;
background: none; user-select: none;
border: none; text-align: left;
background: none;
border: none;
} }
.sidebar-section-header.sidebar-section-header-group .section-title { .sidebar-section-header.sidebar-section-header-group .section-title {
color: var(--sidebar-group-title); color: var(--sidebar-group-title);
} }
.sidebar-section-header:hover { .sidebar-section-header:hover {
color: var(--text-secondary); color: var(--text-secondary);
background: var(--bg-hover); background: var(--bg-hover);
} }
.sidebar-section-header.sidebar-section-header-group:hover .section-title { .sidebar-section-header.sidebar-section-header-group:hover .section-title {
color: var(--sidebar-group-title); color: var(--sidebar-group-title);
} }
.sidebar-section-header .section-icon { .sidebar-section-header .section-icon {
width: 14px; width: 14px;
text-align: center; text-align: center;
flex-shrink: 0; flex-shrink: 0;
font-size: var(--text-sm); font-size: var(--text-sm);
} }
.sidebar-section-header .section-chevron { .sidebar-section-header .section-chevron {
width: 12px; width: 12px;
height: 12px; height: 12px;
margin-right: var(--space-1); margin-right: var(--space-1);
flex-shrink: 0; flex-shrink: 0;
transition: transform var(--transition-normal); transition: transform var(--transition-normal);
color: var(--text-disabled); color: var(--text-disabled);
} }
.sidebar-section.is-collapsed .section-chevron { .sidebar-section.is-collapsed .section-chevron {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
.section-count { .section-count {
margin-left: auto; margin-left: auto;
font-size: 10px; font-size: 10px;
font-weight: var(--weight-normal); font-weight: var(--weight-normal);
color: var(--text-disabled); color: var(--text-disabled);
background: var(--bg-overlay); background: var(--bg-overlay);
padding: 0 var(--space-1); padding: 0 var(--space-1);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
min-width: 18px; min-width: 18px;
text-align: center; text-align: center;
line-height: 16px; line-height: 16px;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar list (container for items) Sidebar list (container for items)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-list { .sidebar-list {
padding: 0 var(--space-2); padding: 0 var(--space-2);
} }
.sidebar-section.is-collapsed .sidebar-list { .sidebar-section.is-collapsed .sidebar-list {
display: none; display: none;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar item (individual connection profile) Sidebar item (individual connection profile)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-item { .sidebar-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-2) var(--space-2); padding: var(--space-2) var(--space-2);
margin-bottom: 1px; margin-bottom: 1px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
transition: transition:
background var(--transition-fast), background var(--transition-fast),
box-shadow var(--transition-fast); box-shadow var(--transition-fast);
user-select: none; user-select: none;
position: relative; position: relative;
} }
.sidebar-item:hover { .sidebar-item:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.sidebar-item:active { .sidebar-item:active {
background: var(--bg-active); background: var(--bg-active);
} }
.sidebar-item-selected { .sidebar-item-selected {
background: var(--accent-muted); background: var(--accent-muted);
box-shadow: inset 0 0 0 1px var(--accent); box-shadow: inset 0 0 0 1px var(--accent);
} }
.sidebar-item-selected .sidebar-item-name { .sidebar-item-selected .sidebar-item-name {
color: var(--text-primary); color: var(--text-primary);
} }
/* Active / selected state */ /* Active / selected state */
.sidebar-item.active { .sidebar-item.active {
background: var(--accent-muted); background: var(--accent-muted);
} }
.sidebar-item.active::before { .sidebar-item.active::before {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 4px; top: 4px;
bottom: 4px; bottom: 4px;
width: 2px; width: 2px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
background: var(--accent); background: var(--accent);
} }
/* Protocol indicator icon */ /* Protocol indicator icon */
.sidebar-item .protocol-indicator { .sidebar-item .protocol-indicator {
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
flex-shrink: 0; flex-shrink: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 8px; font-size: 8px;
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
line-height: 1; line-height: 1;
background: var(--bg-overlay); background: var(--bg-overlay);
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.sidebar-item .protocol-indicator::before { .sidebar-item .protocol-indicator::before {
content: '?'; content: "?";
} }
.sidebar-item .protocol-indicator.proto-ssh::before { content: '⌨'; } .sidebar-item .protocol-indicator.proto-ssh::before {
.sidebar-item .protocol-indicator.proto-telnet::before { content: '⇄'; } content: "⌨";
.sidebar-item .protocol-indicator.proto-raw::before { content: '≋'; } }
.sidebar-item .protocol-indicator.proto-github::before { content: 'GH'; } .sidebar-item .protocol-indicator.proto-telnet::before {
content: "⇄";
}
.sidebar-item .protocol-indicator.proto-raw::before {
content: "≋";
}
.sidebar-item .protocol-indicator.proto-github::before {
content: "GH";
}
.sidebar-item .protocol-indicator.proto-ssh { .sidebar-item .protocol-indicator.proto-ssh {
background: rgba(78, 205, 196, 0.18); background: rgba(78, 205, 196, 0.18);
color: var(--protocol-ssh); color: var(--protocol-ssh);
} }
.sidebar-item .protocol-indicator.proto-telnet { .sidebar-item .protocol-indicator.proto-telnet {
background: rgba(255, 167, 38, 0.18); background: rgba(255, 167, 38, 0.18);
color: var(--protocol-telnet); color: var(--protocol-telnet);
} }
.sidebar-item .protocol-indicator.proto-raw { .sidebar-item .protocol-indicator.proto-raw {
background: rgba(171, 71, 188, 0.18); background: rgba(171, 71, 188, 0.18);
color: var(--protocol-raw); color: var(--protocol-raw);
} }
.sidebar-item .protocol-indicator.proto-github { .sidebar-item .protocol-indicator.proto-github {
background: rgba(124, 138, 255, 0.18); background: rgba(124, 138, 255, 0.18);
color: var(--protocol-github); color: var(--protocol-github);
} }
/* Text block (name + host) */ /* Text block (name + host) */
.sidebar-item-text { .sidebar-item-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.sidebar-item-name { .sidebar-item-name {
font-size: var(--text-sm); font-size: var(--text-sm);
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
color: var(--text-primary); color: var(--text-primary);
line-height: var(--leading-tight); line-height: var(--leading-tight);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.sidebar-item-host { .sidebar-item-host {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-tertiary); color: var(--text-tertiary);
font-family: var(--font-mono); font-family: var(--font-mono);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
line-height: var(--leading-tight); line-height: var(--leading-tight);
margin-top: 1px; margin-top: 1px;
} }
/* Edit button (appears on hover, beside name) */ /* Edit button (appears on hover, beside name) */
.sidebar-item-edit { .sidebar-item-edit {
flex-shrink: 0; flex-shrink: 0;
width: 22px; width: 22px;
height: 22px; height: 22px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 12px; font-size: 12px;
color: var(--text-disabled); color: var(--text-disabled);
background: none; background: none;
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0;
transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast); transition:
opacity var(--transition-fast),
color var(--transition-fast),
background var(--transition-fast);
} }
.sidebar-item:hover .sidebar-item-edit { .sidebar-item:hover .sidebar-item-edit {
opacity: 1; opacity: 1;
} }
.sidebar-item-edit:hover { .sidebar-item-edit:hover {
color: var(--accent); color: var(--accent);
background: var(--bg-hover); background: var(--bg-hover);
} }
/* Favourite star */ /* Favourite star */
.sidebar-item-star { .sidebar-item-star {
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 16px;
height: 16px; height: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 11px; font-size: 11px;
color: var(--text-disabled); color: var(--text-disabled);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
opacity: 0; opacity: 0;
transition: opacity var(--transition-fast), color var(--transition-fast); transition:
opacity var(--transition-fast),
color var(--transition-fast);
} }
.sidebar-item:hover .sidebar-item-star { .sidebar-item:hover .sidebar-item-star {
opacity: 1; opacity: 1;
} }
.sidebar-item-star.is-favourite { .sidebar-item-star.is-favourite {
opacity: 1; opacity: 1;
color: var(--warning); color: var(--warning);
} }
.sidebar-item-star:hover { .sidebar-item-star:hover {
color: var(--warning); color: var(--warning);
} }
/* Inline status dot (right side, tiny) */ /* Inline status dot (right side, tiny) */
.sidebar-item-status { .sidebar-item-status {
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
margin-right: var(--space-1); margin-right: var(--space-1);
}
.sidebar-item-status.is-connected {
background: var(--success);
}
.sidebar-item-status.is-connecting {
background: var(--warning);
animation: pulse-dot 1.2s ease infinite;
}
.sidebar-item-status.is-error {
background: var(--error);
} }
.sidebar-item-status.is-connected { background: var(--success); }
.sidebar-item-status.is-connecting { background: var(--warning); animation: pulse-dot 1.2s ease infinite; }
.sidebar-item-status.is-error { background: var(--error); }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar empty list placeholder Sidebar empty list placeholder
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-list-empty { .sidebar-list-empty {
padding: var(--space-3) var(--space-2); padding: var(--space-3) var(--space-2);
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-disabled); color: var(--text-disabled);
text-align: center; text-align: center;
font-style: italic; font-style: italic;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar footer (actions) Sidebar footer (actions)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-footer { .sidebar-footer {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-1); gap: var(--space-1);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-footer .btn:first-child { .sidebar-footer .btn:first-child {
flex: 1; flex: 1;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Sidebar collapsed state Sidebar collapsed state
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.sidebar-collapsed .sidebar-header { .sidebar-collapsed .sidebar-header {
justify-content: center; justify-content: center;
padding: var(--space-3) var(--space-2); padding: var(--space-3) var(--space-2);
} }
.sidebar-collapsed .sidebar-title, .sidebar-collapsed .sidebar-title,
.sidebar-collapsed .sidebar-search, .sidebar-collapsed .sidebar-search,
@@ -396,19 +418,19 @@
.sidebar-collapsed .sidebar-item-star, .sidebar-collapsed .sidebar-item-star,
.sidebar-collapsed .section-count, .sidebar-collapsed .section-count,
.sidebar-collapsed .sidebar-footer .btn:first-child span { .sidebar-collapsed .sidebar-footer .btn:first-child span {
display: none; display: none;
} }
.sidebar-collapsed .sidebar-item { .sidebar-collapsed .sidebar-item {
justify-content: center; justify-content: center;
padding: var(--space-2); padding: var(--space-2);
} }
.sidebar-collapsed .sidebar-section-header { .sidebar-collapsed .sidebar-section-header {
justify-content: center; justify-content: center;
padding: var(--space-2); padding: var(--space-2);
} }
.sidebar-collapsed .sidebar-footer { .sidebar-collapsed .sidebar-footer {
flex-direction: column; flex-direction: column;
} }
+30 -20
View File
@@ -918,10 +918,7 @@
"permission denied", "permission denied",
"no supported methods", "no supported methods",
]; ];
const hostKeyPatterns = [ const hostKeyPatterns = ["host key", "fingerprint"];
"host key",
"fingerprint",
];
if (authPatterns.some((p) => msg.includes(p))) { if (authPatterns.some((p) => msg.includes(p))) {
return { return {
@@ -959,17 +956,23 @@
const session = this.state.getSession(sessionId); const session = this.state.getSession(sessionId);
const label = session?.tabName || session?.host || "Session"; const label = session?.tabName || session?.host || "Session";
const body = `${info.message}\n\n${message}`; const body = `${info.message}\n\n${message}`;
this._showGuidedErrorModal(info.title, label, body, (action) => { this._showGuidedErrorModal(
if (action === "edit" && session?.profileId) { info.title,
this.profileEditor.openEdit(session.profileId); label,
} body,
if (action === "retry") { (action) => {
this.tabs._reconnect(sessionId); if (action === "edit" && session?.profileId) {
} this.profileEditor.openEdit(session.profileId);
if (action === "knownhosts") { }
this.settings.openTab("knownhosts"); if (action === "retry") {
} this.tabs._reconnect(sessionId);
}, info.actions); }
if (action === "knownhosts") {
this.settings.openTab("knownhosts");
}
},
info.actions,
);
} }
/** /**
@@ -1005,15 +1008,20 @@
const footer = modal.querySelector(".modal-footer"); const footer = modal.querySelector(".modal-footer");
const addActionBtn = (label, action, primary) => { const addActionBtn = (label, action, primary) => {
const btn = createElement("button", `btn btn-sm${primary ? " btn-primary" : ""}`, { const btn = createElement(
textContent: label, "button",
}); `btn btn-sm${primary ? " btn-primary" : ""}`,
{
textContent: label,
},
);
btn.addEventListener("click", () => close(action)); btn.addEventListener("click", () => close(action));
footer.appendChild(btn); footer.appendChild(btn);
}; };
addActionBtn("Close", null, false); addActionBtn("Close", null, false);
if (actions.includes("knownhosts")) addActionBtn("Known Hosts", "knownhosts", false); if (actions.includes("knownhosts"))
addActionBtn("Known Hosts", "knownhosts", false);
if (actions.includes("edit")) addActionBtn("Edit Profile", "edit", false); if (actions.includes("edit")) addActionBtn("Edit Profile", "edit", false);
if (actions.includes("retry")) addActionBtn("Retry", "retry", true); if (actions.includes("retry")) addActionBtn("Retry", "retry", true);
@@ -1024,7 +1032,9 @@
if (action) onAction(action); if (action) onAction(action);
}; };
modal.querySelector(".modal-close").addEventListener("click", () => close(null)); modal
.querySelector(".modal-close")
.addEventListener("click", () => close(null));
backdrop.addEventListener("click", (e) => { backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close(null); if (e.target === backdrop) close(null);
}); });
+331 -299
View File
@@ -1,17 +1,17 @@
(function() { (function () {
/** /**
* Nexus Terminal — GitHub repository viewer. * Nexus Terminal — GitHub repository viewer.
* Renders a scrollable list of repositories grouped by owner/organisation, * Renders a scrollable list of repositories grouped by owner/organisation,
* sorted by last commit date (newest first). Automatically refreshes via * sorted by last commit date (newest first). Automatically refreshes via
* Wails events emitted by the Go backend. * Wails events emitted by the Go backend.
*/ */
const { createElement, escapeHtml, relativeTime, $ } = window.NexusTerminal; const { createElement, escapeHtml, relativeTime, $ } = window.NexusTerminal;
class GitHubViewer { class GitHubViewer {
constructor() { constructor() {
/** @type {Map<string, GitHubViewerInstance>} sessionID → viewer */ /** @type {Map<string, GitHubViewerInstance>} sessionID → viewer */
this._viewers = new Map(); this._viewers = new Map();
} }
/** /**
@@ -21,11 +21,15 @@ class GitHubViewer {
* @param {string} profileName * @param {string} profileName
*/ */
createViewer(sessionID, container, profileName) { createViewer(sessionID, container, profileName) {
if (this._viewers.has(sessionID)) return; if (this._viewers.has(sessionID)) return;
const viewer = new GitHubViewerInstance(sessionID, container, profileName); const viewer = new GitHubViewerInstance(
this._viewers.set(sessionID, viewer); sessionID,
viewer.render(); container,
profileName,
);
this._viewers.set(sessionID, viewer);
viewer.render();
} }
/** /**
@@ -33,11 +37,11 @@ class GitHubViewer {
* @param {string} sessionID * @param {string} sessionID
*/ */
destroyViewer(sessionID) { destroyViewer(sessionID) {
const viewer = this._viewers.get(sessionID); const viewer = this._viewers.get(sessionID);
if (viewer) { if (viewer) {
viewer.destroy(); viewer.destroy();
this._viewers.delete(sessionID); this._viewers.delete(sessionID);
} }
} }
/** /**
@@ -47,10 +51,10 @@ class GitHubViewer {
* @param {Array} repos * @param {Array} repos
*/ */
updateRepos(sessionID, login, repos) { updateRepos(sessionID, login, repos) {
const viewer = this._viewers.get(sessionID); const viewer = this._viewers.get(sessionID);
if (viewer) { if (viewer) {
viewer.setData(login, repos); viewer.setData(login, repos);
} }
} }
/** /**
@@ -59,10 +63,10 @@ class GitHubViewer {
* @param {string} message * @param {string} message
*/ */
setStatus(sessionID, message) { setStatus(sessionID, message) {
const viewer = this._viewers.get(sessionID); const viewer = this._viewers.get(sessionID);
if (viewer) { if (viewer) {
viewer.setStatus(message); viewer.setStatus(message);
} }
} }
/** /**
@@ -71,98 +75,98 @@ class GitHubViewer {
* @param {string} message * @param {string} message
*/ */
showError(sessionID, message) { showError(sessionID, message) {
const viewer = this._viewers.get(sessionID); const viewer = this._viewers.get(sessionID);
if (viewer) { if (viewer) {
viewer.showError(message); viewer.showError(message);
} }
} }
} }
class GitHubViewerInstance { class GitHubViewerInstance {
/** /**
* @param {string} sessionID * @param {string} sessionID
* @param {HTMLElement} container * @param {HTMLElement} container
* @param {string} profileName * @param {string} profileName
*/ */
constructor(sessionID, container, profileName) { constructor(sessionID, container, profileName) {
this.sessionID = sessionID; this.sessionID = sessionID;
this.container = container; this.container = container;
this.profileName = profileName; this.profileName = profileName;
/** @type {string} */ /** @type {string} */
this._login = ''; this._login = "";
/** @type {Array} */ /** @type {Array} */
this._repos = []; this._repos = [];
/** @type {string} */ /** @type {string} */
this._filter = ''; this._filter = "";
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
this._el = null; this._el = null;
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
this._listEl = null; this._listEl = null;
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
this._statusEl = null; this._statusEl = null;
/** @type {number|null} */ /** @type {number|null} */
this._refreshTimer = null; this._refreshTimer = null;
} }
render() { render() {
this.container.innerHTML = ''; this.container.innerHTML = "";
const wrapper = createElement('div', 'github-viewer'); const wrapper = createElement("div", "github-viewer");
// ── Header bar ────────────────────────────────────────── // ── Header bar ──────────────────────────────────────────
const header = createElement('div', 'github-viewer-header'); const header = createElement("div", "github-viewer-header");
const titleArea = createElement('div', 'github-viewer-title-area'); const titleArea = createElement("div", "github-viewer-title-area");
const icon = createElement('span', 'github-viewer-icon'); const icon = createElement("span", "github-viewer-icon");
icon.innerHTML = this._githubSVG(); icon.innerHTML = this._githubSVG();
titleArea.appendChild(icon); titleArea.appendChild(icon);
const titleText = createElement('span', 'github-viewer-title'); const titleText = createElement("span", "github-viewer-title");
titleText.textContent = this.profileName || 'GitHub'; titleText.textContent = this.profileName || "GitHub";
titleArea.appendChild(titleText); titleArea.appendChild(titleText);
header.appendChild(titleArea); header.appendChild(titleArea);
// Search / filter // Search / filter
const searchWrap = createElement('div', 'github-viewer-search'); const searchWrap = createElement("div", "github-viewer-search");
const searchInput = createElement('input', 'github-viewer-search-input', { const searchInput = createElement("input", "github-viewer-search-input", {
type: 'text', type: "text",
placeholder: 'Filter repositories…', placeholder: "Filter repositories…",
}); });
searchInput.addEventListener('input', (e) => { searchInput.addEventListener("input", (e) => {
this._filter = e.target.value.toLowerCase(); this._filter = e.target.value.toLowerCase();
this._renderList(); this._renderList();
}); });
searchWrap.appendChild(searchInput); searchWrap.appendChild(searchInput);
header.appendChild(searchWrap); header.appendChild(searchWrap);
// Refresh button // Refresh button
const refreshBtn = createElement('button', 'github-viewer-refresh', { const refreshBtn = createElement("button", "github-viewer-refresh", {
title: 'Refresh now', title: "Refresh now",
}); });
refreshBtn.innerHTML = '↻'; refreshBtn.innerHTML = "↻";
refreshBtn.addEventListener('click', () => this._manualRefresh()); refreshBtn.addEventListener("click", () => this._manualRefresh());
header.appendChild(refreshBtn); header.appendChild(refreshBtn);
wrapper.appendChild(header); wrapper.appendChild(header);
// ── Status bar ────────────────────────────────────────── // ── Status bar ──────────────────────────────────────────
this._statusEl = createElement('div', 'github-viewer-status'); this._statusEl = createElement("div", "github-viewer-status");
this._statusEl.textContent = 'Connecting…'; this._statusEl.textContent = "Connecting…";
wrapper.appendChild(this._statusEl); wrapper.appendChild(this._statusEl);
// ── Repo list ─────────────────────────────────────────── // ── Repo list ───────────────────────────────────────────
this._listEl = createElement('div', 'github-viewer-list'); this._listEl = createElement("div", "github-viewer-list");
wrapper.appendChild(this._listEl); wrapper.appendChild(this._listEl);
this.container.appendChild(wrapper); this.container.appendChild(wrapper);
this._el = wrapper; this._el = wrapper;
} }
/** /**
@@ -171,10 +175,10 @@ class GitHubViewerInstance {
* @param {Array} repos * @param {Array} repos
*/ */
setData(login, repos) { setData(login, repos) {
this._login = login; this._login = login;
this._repos = repos || []; this._repos = repos || [];
this._updateStatus(); this._updateStatus();
this._renderList(); this._renderList();
} }
/** /**
@@ -182,9 +186,9 @@ class GitHubViewerInstance {
* @param {string} message * @param {string} message
*/ */
setStatus(message) { setStatus(message) {
if (!this._statusEl) return; if (!this._statusEl) return;
this._statusEl.classList.remove('github-viewer-status-error'); this._statusEl.classList.remove("github-viewer-status-error");
this._statusEl.textContent = message || ''; this._statusEl.textContent = message || "";
} }
/** /**
@@ -192,89 +196,108 @@ class GitHubViewerInstance {
* @param {string} message * @param {string} message
*/ */
showError(message) { showError(message) {
if (this._statusEl) { if (this._statusEl) {
this._statusEl.textContent = `⚠ ${message}`; this._statusEl.textContent = `⚠ ${message}`;
this._statusEl.classList.add('github-viewer-status-error'); this._statusEl.classList.add("github-viewer-status-error");
} }
} }
destroy() { destroy() {
if (this._refreshTimer) { if (this._refreshTimer) {
clearInterval(this._refreshTimer); clearInterval(this._refreshTimer);
this._refreshTimer = null; this._refreshTimer = null;
} }
if (this._el) { if (this._el) {
this._el.remove(); this._el.remove();
this._el = null; this._el = null;
} }
} }
// ── Private ────────────────────────────────────────────────── // ── Private ──────────────────────────────────────────────────
_updateStatus() { _updateStatus() {
if (!this._statusEl) return; if (!this._statusEl) return;
this._statusEl.classList.remove('github-viewer-status-error'); this._statusEl.classList.remove("github-viewer-status-error");
const count = this._repos.length; const count = this._repos.length;
const now = new Date().toLocaleTimeString(); const now = new Date().toLocaleTimeString();
this._statusEl.textContent = `${this._login} · ${count} repositor${count === 1 ? 'y' : 'ies'} · updated ${now}`; this._statusEl.textContent = `${this._login} · ${count} repositor${count === 1 ? "y" : "ies"} · updated ${now}`;
} }
_renderList() { _renderList() {
if (!this._listEl) return; if (!this._listEl) return;
this._listEl.innerHTML = ''; this._listEl.innerHTML = "";
// Filter // Filter
let filtered = this._repos; let filtered = this._repos;
if (this._filter) { if (this._filter) {
filtered = this._repos.filter((r) => filtered = this._repos.filter(
r.fullName.toLowerCase().includes(this._filter) || (r) =>
(r.description || '').toLowerCase().includes(this._filter) || r.fullName.toLowerCase().includes(this._filter) ||
(r.language || '').toLowerCase().includes(this._filter) (r.description || "").toLowerCase().includes(this._filter) ||
); (r.language || "").toLowerCase().includes(this._filter),
} );
}
if (filtered.length === 0) { if (filtered.length === 0) {
const empty = createElement('div', 'github-viewer-empty'); const empty = createElement("div", "github-viewer-empty");
empty.textContent = this._filter ? 'No matching repositories.' : 'No repositories found.'; empty.textContent = this._filter
this._listEl.appendChild(empty); ? "No matching repositories."
return; : "No repositories found.";
} this._listEl.appendChild(empty);
return;
}
// Group by owner const commitTimestamp = (repo) => {
const groups = new Map(); const raw = repo.latestCommitAt || repo.pushedAt || repo.updatedAt || "";
for (const repo of filtered) { const ts = new Date(raw).getTime();
const owner = repo.owner || 'unknown'; return Number.isNaN(ts) ? 0 : ts;
if (!groups.has(owner)) groups.set(owner, []); };
groups.get(owner).push(repo);
}
// Sort groups alphabetically (user's own repos first) // Group by owner
const sortedKeys = [...groups.keys()].sort((a, b) => { const groups = new Map();
if (a === this._login) return -1; for (const repo of filtered) {
if (b === this._login) return 1; const owner = repo.owner || "unknown";
return a.localeCompare(b); if (!groups.has(owner)) groups.set(owner, []);
}); groups.get(owner).push(repo);
}
for (const owner of sortedKeys) { // Sort repos within each group by latest commit (newest first)
const repos = groups.get(owner); for (const repos of groups.values()) {
repos.sort((a, b) => commitTimestamp(b) - commitTimestamp(a));
}
// Group header // Sort groups by most recent commit in each group (login first on ties)
const groupEl = createElement('div', 'github-group'); const sortedKeys = [...groups.keys()].sort((a, b) => {
const groupHeader = createElement('div', 'github-group-header'); const aRepos = groups.get(a) || [];
groupHeader.innerHTML = ` const bRepos = groups.get(b) || [];
const aMax = aRepos.length > 0 ? commitTimestamp(aRepos[0]) : 0;
const bMax = bRepos.length > 0 ? commitTimestamp(bRepos[0]) : 0;
if (aMax !== bMax) return bMax - aMax;
if (a === this._login) return -1;
if (b === this._login) return 1;
return a.localeCompare(b);
});
for (const owner of sortedKeys) {
const repos = groups.get(owner);
// Group header
const groupEl = createElement("div", "github-group");
const groupHeader = createElement("div", "github-group-header");
groupHeader.innerHTML = `
<span class="github-group-name">${escapeHtml(owner)}</span> <span class="github-group-name">${escapeHtml(owner)}</span>
<span class="github-group-count">${repos.length}</span> <span class="github-group-count">${repos.length}</span>
`; `;
groupEl.appendChild(groupHeader); groupEl.appendChild(groupHeader);
// Repo cards // Repo cards
for (const repo of repos) { for (const repo of repos) {
groupEl.appendChild(this._renderRepoCard(repo)); groupEl.appendChild(this._renderRepoCard(repo));
}
this._listEl.appendChild(groupEl);
} }
this._listEl.appendChild(groupEl);
}
} }
/** /**
@@ -283,135 +306,145 @@ class GitHubViewerInstance {
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
_renderRepoCard(repo) { _renderRepoCard(repo) {
const card = createElement('div', 'github-repo-card'); const card = createElement("div", "github-repo-card");
card.addEventListener('click', () => { card.addEventListener("click", () => {
if (repo.htmlURL) window.open(repo.htmlURL, '_blank'); if (repo.htmlURL) window.open(repo.htmlURL, "_blank");
}); });
// Top row: name + badges // Top row: name + badges
const topRow = createElement('div', 'github-repo-top'); const topRow = createElement("div", "github-repo-top");
const nameEl = createElement('span', 'github-repo-name'); const nameEl = createElement("span", "github-repo-name");
nameEl.textContent = repo.name; nameEl.textContent = repo.name;
topRow.appendChild(nameEl); topRow.appendChild(nameEl);
if (repo.private) {
const badge = createElement(
"span",
"github-repo-badge github-repo-private",
);
badge.textContent = "Private";
topRow.appendChild(badge);
}
if (repo.fork) {
const badge = createElement(
"span",
"github-repo-badge github-repo-fork",
);
badge.textContent = "Fork";
topRow.appendChild(badge);
}
if (repo.archived) {
const badge = createElement(
"span",
"github-repo-badge github-repo-archived",
);
badge.textContent = "Archived";
topRow.appendChild(badge);
}
if (repo.private) { const actions = createElement("div", "github-repo-actions");
const badge = createElement('span', 'github-repo-badge github-repo-private');
badge.textContent = 'Private'; const downloadBtn = createElement("button", "github-repo-download", {
topRow.appendChild(badge); textContent: "ZIP",
} title: "Download repository as zip",
if (repo.fork) { });
const badge = createElement('span', 'github-repo-badge github-repo-fork'); downloadBtn.addEventListener("click", (e) => {
badge.textContent = 'Fork'; e.stopPropagation();
topRow.appendChild(badge); this._downloadRepo(repo);
} });
if (repo.archived) { actions.appendChild(downloadBtn);
const badge = createElement('span', 'github-repo-badge github-repo-archived');
badge.textContent = 'Archived'; const visitLink = createElement("button", "github-repo-visit", {
topRow.appendChild(badge); textContent: "Visit",
title: "Open repository in browser",
});
visitLink.addEventListener("click", (e) => {
e.stopPropagation();
if (repo.htmlURL) {
if (window.runtime && window.runtime.BrowserOpenURL) {
window.runtime.BrowserOpenURL(repo.htmlURL);
} else {
window.open(repo.htmlURL, "_blank");
}
} }
});
actions.appendChild(visitLink);
const actions = createElement('div', 'github-repo-actions'); topRow.appendChild(actions);
const downloadBtn = createElement('button', 'github-repo-download', { card.appendChild(topRow);
textContent: 'ZIP',
title: 'Download repository as zip',
});
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
this._downloadRepo(repo);
});
actions.appendChild(downloadBtn);
const visitLink = createElement('button', 'github-repo-visit', { if (repo.latestCommitter && repo.latestCommitAt) {
textContent: 'Visit', const commitLine = createElement("div", "github-repo-commit");
title: 'Open repository in browser', const when = new Date(repo.latestCommitAt);
}); const whenText = isNaN(when.getTime())
visitLink.addEventListener('click', (e) => { ? repo.latestCommitAt
e.stopPropagation(); : when.toLocaleString();
if (repo.htmlURL) { commitLine.textContent = `Last commit: ${whenText} by ${repo.latestCommitter}`;
if (window.runtime && window.runtime.BrowserOpenURL) { card.appendChild(commitLine);
window.runtime.BrowserOpenURL(repo.htmlURL); }
} else {
window.open(repo.htmlURL, '_blank');
}
}
});
actions.appendChild(visitLink);
topRow.appendChild(actions); // Description
if (repo.description) {
const desc = createElement("div", "github-repo-desc");
desc.textContent = repo.description;
card.appendChild(desc);
}
card.appendChild(topRow); // Meta row: language, stars, pushed
const meta = createElement("div", "github-repo-meta");
if (repo.latestCommitter && repo.latestCommitAt) { if (repo.language) {
const commitLine = createElement('div', 'github-repo-commit'); const lang = createElement("span", "github-repo-lang");
const when = new Date(repo.latestCommitAt); lang.innerHTML = `<span class="github-lang-dot" style="background:${this._langColour(repo.language)}"></span> ${escapeHtml(repo.language)}`;
const whenText = isNaN(when.getTime()) ? repo.latestCommitAt : when.toLocaleString(); meta.appendChild(lang);
commitLine.textContent = `Last commit: ${whenText} by ${repo.latestCommitter}`; }
card.appendChild(commitLine);
}
// Description if (repo.stars > 0) {
if (repo.description) { const stars = createElement("span", "github-repo-stars");
const desc = createElement('div', 'github-repo-desc'); stars.textContent = `★ ${repo.stars.toLocaleString()}`;
desc.textContent = repo.description; meta.appendChild(stars);
card.appendChild(desc); }
}
// Meta row: language, stars, pushed if (repo.pushedAt) {
const meta = createElement('div', 'github-repo-meta'); const pushed = createElement("span", "github-repo-pushed");
pushed.textContent = relativeTime(repo.pushedAt);
pushed.title = new Date(repo.pushedAt).toLocaleString();
meta.appendChild(pushed);
}
if (repo.language) { card.appendChild(meta);
const lang = createElement('span', 'github-repo-lang');
lang.innerHTML = `<span class="github-lang-dot" style="background:${this._langColour(repo.language)}"></span> ${escapeHtml(repo.language)}`;
meta.appendChild(lang);
}
if (repo.stars > 0) { return card;
const stars = createElement('span', 'github-repo-stars');
stars.textContent = `★ ${repo.stars.toLocaleString()}`;
meta.appendChild(stars);
}
if (repo.pushedAt) {
const pushed = createElement('span', 'github-repo-pushed');
pushed.textContent = relativeTime(repo.pushedAt);
pushed.title = new Date(repo.pushedAt).toLocaleString();
meta.appendChild(pushed);
}
card.appendChild(meta);
return card;
} }
async _downloadRepo(repo) { async _downloadRepo(repo) {
if (!this._statusEl) return; if (!this._statusEl) return;
const ref = repo.defaultBranch || ''; const ref = repo.defaultBranch || "";
this._statusEl.classList.remove('github-viewer-status-error'); this._statusEl.classList.remove("github-viewer-status-error");
this._statusEl.textContent = `Downloading ${repo.fullName}...`; this._statusEl.textContent = `Downloading ${repo.fullName}...`;
try { try {
await window.go.api.Service.DownloadGitHubRepoArchive( await window.go.api.Service.DownloadGitHubRepoArchive(
this.sessionID, this.sessionID,
repo.fullName, repo.fullName,
ref, ref,
); );
this._updateStatus(); this._updateStatus();
} catch (err) { } catch (err) {
this._statusEl.textContent = `⚠ Download failed: ${err}`; this._statusEl.textContent = `⚠ Download failed: ${err}`;
this._statusEl.classList.add('github-viewer-status-error'); this._statusEl.classList.add("github-viewer-status-error");
} }
} }
async _manualRefresh() { async _manualRefresh() {
if (!this._statusEl) return; if (!this._statusEl) return;
this._statusEl.textContent = 'Refreshing…'; this._statusEl.textContent = "Refreshing…";
try { try {
await window.go.api.Service.RefreshGitHubRepos(this.sessionID); await window.go.api.Service.RefreshGitHubRepos(this.sessionID);
} catch (err) { } catch (err) {
this.showError(err.message || 'Refresh failed'); this.showError(err.message || "Refresh failed");
} }
} }
/** /**
@@ -421,33 +454,33 @@ class GitHubViewerInstance {
* @returns {string} CSS colour value. * @returns {string} CSS colour value.
*/ */
_langColour(lang) { _langColour(lang) {
const colours = { const colours = {
'JavaScript': '#f1e05a', JavaScript: "#f1e05a",
'TypeScript': '#3178c6', TypeScript: "#3178c6",
'Python': '#3572A5', Python: "#3572A5",
'Go': '#00ADD8', Go: "#00ADD8",
'Rust': '#dea584', Rust: "#dea584",
'Java': '#b07219', Java: "#b07219",
'C': '#555555', C: "#555555",
'C++': '#f34b7d', "C++": "#f34b7d",
'C#': '#178600', "C#": "#178600",
'Ruby': '#701516', Ruby: "#701516",
'PHP': '#4F5D95', PHP: "#4F5D95",
'Swift': '#F05138', Swift: "#F05138",
'Kotlin': '#A97BFF', Kotlin: "#A97BFF",
'Shell': '#89e051', Shell: "#89e051",
'HTML': '#e34c26', HTML: "#e34c26",
'CSS': '#563d7c', CSS: "#563d7c",
'Dart': '#00B4AB', Dart: "#00B4AB",
'Lua': '#000080', Lua: "#000080",
'Scala': '#c22d40', Scala: "#c22d40",
'Elixir': '#6e4a7e', Elixir: "#6e4a7e",
'Haskell': '#5e5086', Haskell: "#5e5086",
'Zig': '#ec915c', Zig: "#ec915c",
'Vue': '#41b883', Vue: "#41b883",
'SCSS': '#c6538c', SCSS: "#c6538c",
}; };
return colours[lang] || '#8b949e'; return colours[lang] || "#8b949e";
} }
/** /**
@@ -455,11 +488,10 @@ class GitHubViewerInstance {
* @returns {string} * @returns {string}
*/ */
_githubSVG() { _githubSVG() {
return '<svg viewBox="0 0 16 16" width="20" height="20" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>'; return '<svg viewBox="0 0 16 16" width="20" height="20" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>';
} }
} }
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.GitHubViewer = new GitHubViewer();
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.GitHubViewer = new GitHubViewer();
})(); })();
+1697 -1214
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+553 -483
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -214,6 +214,10 @@
searchAddon, searchAddon,
webLinksAddon, webLinksAddon,
observer, observer,
decoder:
typeof TextDecoder !== "undefined"
? new TextDecoder("utf-8")
: null,
}); });
return term; return term;
@@ -230,7 +234,16 @@
try { try {
const raw = atob(base64Data); const raw = atob(base64Data);
entry.term.write(raw); if (entry.decoder) {
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) {
bytes[i] = raw.charCodeAt(i);
}
const text = entry.decoder.decode(bytes, { stream: true });
entry.term.write(text);
} else {
entry.term.write(raw);
}
} catch (err) { } catch (err) {
console.error("Failed to decode session data:", err); console.error("Failed to decode session data:", err);
} }