diff --git a/frontend/css/base.css b/frontend/css/base.css index 647148d..db54002 100644 --- a/frontend/css/base.css +++ b/frontend/css/base.css @@ -1,18 +1,33 @@ :root { - --bg-app: #0d1117; - --bg-panel: #161b22; - --bg-panel-alt: #21262d; - --bg-hover: #30363d; - --border: #30363d; - --text-main: #e6edf3; - --text-muted: #848d97; - --accent: #2f81f7; + --bg-app: #1c2128; + --bg-panel: #22272e; + --bg-panel-alt: #2a3038; + --bg-hover: #2d3540; + --border: #1e242b; + --text-main: #cdd9e5; + --text-muted: #768390; + --accent: #58a6ff; --accent-strong: #1f6feb; --success: #3fb950; --danger: #f85149; --radius-md: 6px; --radius-lg: 8px; - --shadow: 0 8px 24px rgba(1, 4, 9, 0.75); + --shadow: 0 8px 24px rgba(1, 4, 9, 0.7); +} + +:root[data-theme="light"] { + --bg-app: #f6f8fa; + --bg-panel: #ffffff; + --bg-panel-alt: #f0f3f6; + --bg-hover: #eaeef2; + --border: #d0d7de; + --text-main: #24292f; + --text-muted: #57606a; + --accent: #0969da; + --accent-strong: #0969da; + --success: #1a7f37; + --danger: #cf222e; + --shadow: 0 8px 24px rgba(31, 35, 40, 0.12); } * { @@ -29,6 +44,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; + -webkit-font-smoothing: antialiased; } button, @@ -45,40 +61,51 @@ button { color: var(--text-main); padding: 5px 12px; cursor: pointer; - transition: background 0.1s ease, border-color 0.1s ease; - font-size: 14px; + transition: background 0.1s ease, border-color 0.1s ease, opacity 0.1s ease; + font-size: 13px; } button:hover { background: var(--bg-hover); - border-color: #8b949e; + border-color: #6e7681; +} + +button:disabled { + opacity: 0.5; + cursor: default; +} + +button:disabled:hover { + background: var(--bg-panel-alt); + border-color: var(--border); } button.primary { background: #238636; - border-color: rgba(240, 246, 252, 0.1); + border-color: transparent; color: #ffffff; } button.primary:hover { background: #2ea043; - border-color: rgba(240, 246, 252, 0.1); + border-color: transparent; } button.primary-blue { background: var(--accent-strong); - border-color: rgba(240, 246, 252, 0.1); + border-color: transparent; color: #ffffff; } button.primary-blue:hover { - background: var(--accent); - border-color: rgba(240, 246, 252, 0.1); + background: #388bfd; + border-color: transparent; } button.danger { - border-color: rgba(248, 81, 73, 0.4); + border-color: rgba(248, 81, 73, 0.35); color: #f85149; + background: transparent; } button.danger:hover { @@ -90,12 +117,12 @@ input, select, textarea { width: 100%; - padding: 5px 12px; + padding: 5px 10px; border-radius: var(--radius-md); border: 1px solid var(--border); - background: var(--bg-app); + background: rgba(0, 0, 0, 0.25); color: var(--text-main); - font-size: 14px; + font-size: 13px; transition: border-color 0.1s ease, box-shadow 0.1s ease; } @@ -104,10 +131,10 @@ select:focus, textarea:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(47, 129, 247, 0.15); + box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.12); } textarea { - min-height: 96px; + min-height: 80px; resize: vertical; } diff --git a/frontend/css/components.css b/frontend/css/components.css index d51ded1..44b8835 100644 --- a/frontend/css/components.css +++ b/frontend/css/components.css @@ -1,3 +1,5 @@ +/* ── App shell ────────────────────────────────────────────────────────────── */ + #app { width: 100%; height: 100%; @@ -5,36 +7,33 @@ .layout { display: grid; - grid-template-columns: 260px 1fr 320px; + grid-template-rows: 48px 1fr; + grid-template-columns: 272px minmax(0, 1fr); width: 100%; height: 100%; - gap: 0; } -.sidebar, -.rightbar { +/* ── Sidebar ──────────────────────────────────────────────────────────────── */ + +.gd-left { + display: flex; + flex-direction: column; background: var(--bg-panel); - border-right: 1px solid var(--border); + border-right: 1px solid rgba(0, 0, 0, 0.35); + overflow: hidden; + min-height: 0; + grid-row: 2; } -.sidebar { - padding: 16px 12px; - overflow-y: auto; -} - -.rightbar { - border-right: 0; - border-left: 1px solid var(--border); - padding: 0 12px 12px; - overflow-y: auto; -} - -.main { - padding: 16px; +.gd-main { overflow: auto; background: var(--bg-app); + min-width: 0; + grid-row: 2; } +/* ── Layout utilities ─────────────────────────────────────────────────────── */ + .panel { border: 1px solid var(--border); border-radius: var(--radius-lg); @@ -62,73 +61,7 @@ justify-content: flex-end; } -.server-chip, -.repo-card { - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 12px; - background: var(--bg-panel); - transition: border-color 0.1s ease; -} - -.repo-card:hover { - border-color: #8b949e; -} - -.repo-grid { - display: grid; - gap: 10px; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); -} - -.repo-filter-pills { - display: flex; - gap: 6px; - flex-wrap: wrap; -} - -.pill-btn { - padding: 4px 14px; - border-radius: 999px; - border: 1px solid var(--border); - background: transparent; - color: var(--text-muted); - font-size: 13px; - cursor: pointer; - transition: background 0.1s, color 0.1s, border-color 0.1s; -} - -.pill-btn:hover { - background: var(--bg-hover); - color: var(--text-main); - border-color: #8b949e; -} - -.pill-btn.active { - background: var(--accent-strong); - border-color: rgba(240, 246, 252, 0.1); - color: #fff; -} - -.org-group { - gap: 8px; -} - -.org-group-header { - display: flex; - align-items: center; - gap: 10px; -} - -.org-badge { - font-size: 12px; - font-weight: 600; - color: var(--text-muted); - background: var(--bg-panel-alt); - border: 1px solid var(--border); - border-radius: 999px; - padding: 1px 10px; -} +/* ── Typography ───────────────────────────────────────────────────────────── */ .muted { color: var(--text-muted); @@ -143,14 +76,14 @@ .title { margin: 0; - font-size: 18px; + font-size: 17px; font-weight: 600; color: var(--text-main); } .subtitle { margin: 0; - font-size: 14px; + font-size: 13px; color: var(--text-muted); } @@ -158,23 +91,18 @@ display: none !important; } -.app-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 14px; +.accent { + color: var(--accent); } .section-header { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; + gap: 12px; } -.list { - margin: 0; - padding-left: 16px; -} +/* ── Welcome view ─────────────────────────────────────────────────────────── */ .welcome-wrap { min-height: 100%; @@ -196,228 +124,65 @@ } .welcome-logo-img { - height: 36px; + height: 34px; width: auto; } .welcome-logo-desktop { - font-size: 20px; + font-size: 19px; font-weight: 300; color: var(--text-muted); - letter-spacing: 0; } -.status-ok { - color: var(--success); - font-weight: 500; -} +/* ── Toolbar ──────────────────────────────────────────────────────────────── */ -.status-error { - color: var(--danger); - font-weight: 500; -} - -/* ── Tabs ─────────────────────────────────────────── */ -.tabs { - display: flex; - align-items: center; - gap: 0; - border-bottom: 1px solid var(--border); - padding: 0; - flex-shrink: 0; -} - -.tab-btn { - background: transparent; - border: none; - border-bottom: 2px solid transparent; - border-radius: 0; - padding: 10px 16px; - color: var(--text-muted); - font-size: 14px; - cursor: pointer; - transition: color 0.1s ease, border-color 0.1s ease; - margin-bottom: -1px; -} - -.tab-btn:hover { - background: transparent; - color: var(--text-main); - border-color: transparent; -} - -.tab-btn.active { - color: var(--text-main); - border-bottom-color: #f78166; - font-weight: 600; -} - -.tab-spacer { - flex: 1; -} - -.tab-search { - width: 200px !important; - padding: 5px 10px !important; - font-size: 13px; - margin: 4px 8px 4px 0; -} - -.main-tabs { - margin-bottom: 0; -} - -.right-tabs { - position: sticky; - top: 0; - margin: 0 -12px; - padding: 0 12px; - background: var(--bg-panel); - z-index: 1; -} - -/* ── Sidebar ──────────────────────────────────────── */ -.sidebar-brand { - padding: 4px 0 12px; - border-bottom: 1px solid var(--border); - margin-bottom: 4px; -} - -.sidebar-brand .title { - font-size: 15px; - font-weight: 600; -} - -.sidebar-logo { - display: flex; - align-items: center; - gap: 8px; -} - -.sidebar-logo-img { - height: 22px; - width: auto; -} - -.sidebar-logo-desktop { - font-size: 13px; - font-weight: 400; - color: var(--text-muted); - letter-spacing: 0; - margin-top: 2px; -} - -.sidebar .server-chip { - overflow: hidden; -} - -.sidebar .server-chip > div { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar-nav { - display: flex; - gap: 6px; -} - -.sidebar-btn { - flex: 1; - font-size: 13px; - padding: 5px 8px; - text-align: center; -} - -.sidebar-recents { - display: flex; - flex-direction: column; - gap: 6px; - flex: 1; - min-height: 0; -} - -.recent-item { - display: block; - width: 100%; - border: 0; - border-radius: var(--radius-md); - background: transparent; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: pointer; - color: var(--text-muted); - padding: 4px 6px; - font-size: 13px; - text-align: left; -} - -.recent-item:hover { - background: var(--bg-hover); - color: var(--accent); - border: 0; -} - -/* ── Git output ───────────────────────────────────── */ -.git-output { - background: #010409; - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 12px 14px; - font-size: 12px; - font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; - color: var(--success); - overflow-x: auto; - white-space: pre-wrap; - word-break: break-word; - margin: 0; - line-height: 1.6; -} - -.git-output-placeholder { - font-size: 13px; - font-style: italic; - color: var(--text-muted); - margin: 0; -} - -.commit-note { - font-size: 11px; - margin-top: 4px; - color: var(--text-muted); -} - -/* ── GitHub Desktop style toolbar ─────────────────── */ .gd-toolbar { - display: grid; - grid-template-columns: minmax(0, 2fr) minmax(0, 1.5fr) minmax(0, 1.5fr); + display: flex; align-items: stretch; - width: 100%; - height: 56px; - margin: -16px -16px 16px; - border-bottom: 1px solid var(--border); + grid-column: 1 / -1; + grid-row: 1; background: var(--bg-panel); + border-bottom: 1px solid rgba(0, 0, 0, 0.35); flex-shrink: 0; } -.main > .gd-toolbar { - margin: 0 0 12px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - overflow: hidden; +/* Left section – matches sidebar width */ +.gd-toolbar-left { + display: grid; + grid-template-columns: 1fr 1fr; + width: 272px; + flex-shrink: 0; + border-right: 1px solid rgba(255, 255, 255, 0.05); } +/* Center section – grows */ +.gd-toolbar-center { + flex: 1; + display: flex; + align-items: center; + padding: 0 18px; +} + +/* Right section – view toggle + utility menu */ +.gd-toolbar-right { + display: flex; + align-items: center; + gap: 10px; + padding: 0 14px; + border-left: 1px solid rgba(255, 255, 255, 0.05); + flex-shrink: 0; +} + +/* Shared toolbar cell (repo + branch buttons) */ .gd-toolbar-cell { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - gap: 10px; + gap: 9px; width: 100%; height: 100%; - padding: 0 14px; + padding: 0 14px 0 12px; border: 0; - border-right: 1px solid var(--border); border-radius: 0; background: transparent; color: var(--text-main); @@ -426,27 +191,25 @@ transition: background 0.1s ease; } -.gd-toolbar-cell:last-child { - border-right: 0; +.gd-repo-cell { + border-right: 1px solid rgba(255, 255, 255, 0.05); } -.gd-toolbar-cell:hover:not(:disabled):not(.gd-toolbar-cell-empty) { - background: rgba(177, 186, 196, 0.08); - border-color: var(--border); +.gd-toolbar-cell:hover:not(:disabled) { + background: rgba(177, 186, 196, 0.07); } -.gd-toolbar-cell:disabled, -.gd-toolbar-cell-empty { +.gd-toolbar-cell:disabled { cursor: default; - opacity: 0.7; + opacity: 0.65; } .gd-cell-icon { display: flex; align-items: center; justify-content: center; - width: 22px; - height: 22px; + width: 20px; + height: 20px; color: var(--text-muted); flex-shrink: 0; } @@ -455,7 +218,7 @@ min-width: 0; display: flex; flex-direction: column; - line-height: 1.25; + line-height: 1.2; } .gd-cell-label { @@ -463,8 +226,9 @@ text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); - font-size: 11px; + font-size: 10px; font-weight: 400; + letter-spacing: 0.01em; } .gd-cell-value { @@ -472,119 +236,1002 @@ text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--text-main); } -.gd-cell-value-muted { - color: var(--text-muted); - font-weight: 400; - font-style: italic; -} - .gd-cell-caret { color: var(--text-muted); flex-shrink: 0; } -.selected-repo-path-line { - margin-top: -6px; - font-size: 12px; - font-family: ui-monospace, "Cascadia Code", "Fira Code", Consolas, monospace; - word-break: break-all; +/* Branch dropdown wrap */ +.gd-branch-wrap { + position: relative; } -.local-scan-panel { - padding: 12px; +.gd-branch-toolbar { + grid-template-columns: auto minmax(0, 1fr) 10px; +} + +/* ── Contextual sync button ───────────────────────────────────────────────── */ + +.gd-sync-btn { + display: inline-flex; + align-items: center; + gap: 10px; + height: 32px; + padding: 0 14px; border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: rgba(33, 38, 45, 0.35); + border-radius: var(--radius-md); + background: var(--bg-panel-alt); + color: var(--text-main); + cursor: pointer; + transition: background 0.16s ease, border-color 0.16s ease, opacity 0.16s ease, transform 0.16s ease; + white-space: nowrap; + min-width: 160px; } -.local-scan-title { - font-size: 15px; +.gd-sync-btn:hover:not(:disabled) { + background: var(--bg-hover); + border-color: #6e7681; } -.local-repo-list { - display: grid; - gap: 8px; - max-height: 320px; +.gd-sync-btn:disabled { + opacity: 0.62; + cursor: default; +} + +.gd-sync-btn:not(:disabled):active { + transform: translateY(1px); +} + +.gd-sync-btn .gd-cell-copy { + line-height: 1.25; + transition: opacity 0.16s ease, transform 0.16s ease; +} + +.gd-sync-btn .gd-cell-value { + font-size: 13px; + font-weight: 600; + margin-top: 1px; + transition: color 0.16s ease; +} + +.gd-sync-btn .gd-cell-label { + font-size: 10px; +} + +.gd-sync-icon { + color: var(--text-muted); + flex-shrink: 0; + display: flex; + align-items: center; + transition: color 0.16s ease, transform 0.16s ease; +} + +.gd-sync-btn:hover:not(:disabled) .gd-sync-icon { + color: var(--accent); +} + +.gd-sync-btn.is-loading .gd-sync-icon { + animation: gd-sync-spin 1s linear infinite; +} + +.gd-sync-btn.is-loading .gd-cell-copy { + opacity: 0.82; +} + +@keyframes gd-sync-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* ── View toggle (Changes / History) ─────────────────────────────────────── */ + +.gd-view-toggle { + display: flex; + background: var(--bg-app); + border: 1px solid var(--border); + border-radius: 6px; + padding: 2px; + gap: 1px; +} + +.gd-view-toggle button { + border: none; + background: transparent; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + padding: 4px 12px; + color: var(--text-muted); + cursor: pointer; + transition: background 0.1s, color 0.1s; + height: 26px; +} + +.gd-view-toggle button:hover { + background: var(--bg-hover); + color: var(--text-main); + border-color: transparent; +} + +.gd-view-toggle button.active { + background: var(--bg-panel); + color: var(--text-main); + font-weight: 600; +} + +/* ── Utility menu ─────────────────────────────────────────────────────────── */ + +.gd-utility-wrap { + position: relative; + flex-shrink: 0; +} + +.gd-utility-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid transparent; + border-radius: var(--radius-md); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: background 0.1s, color 0.1s, border-color 0.1s; +} + +.gd-utility-btn:hover { + background: var(--bg-hover); + border-color: var(--border); + color: var(--text-main); +} + +.gd-utility-menu { + position: absolute; + z-index: 30; + top: calc(100% + 6px); + right: 0; + width: 220px; + padding: 4px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-panel); + box-shadow: var(--shadow); +} + +.gd-utility-menu-item { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: 7px 10px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text-main); + font-size: 13px; + text-align: left; + cursor: pointer; + transition: background 0.08s; +} + +.gd-utility-menu-item:hover { + background: var(--bg-hover); + border-color: transparent; +} + +.gd-utility-menu-item svg { + color: var(--text-muted); + flex-shrink: 0; +} + +.gd-utility-separator { + height: 1px; + background: var(--border); + margin: 3px 4px; +} + +/* ── Branch menu dropdown ─────────────────────────────────────────────────── */ + +.branch-menu { + position: absolute; + z-index: 20; + top: calc(100% + 4px); + left: 4px; + width: 290px; + max-height: min(520px, calc(100vh - 72px)); overflow: auto; + padding: 6px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-panel); + box-shadow: var(--shadow); } -.local-repo-card { +.branch-menu-section + .branch-menu-section { + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid var(--border); +} + +.branch-menu-label { + padding: 4px 8px 5px; + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.branch-menu-item, +.branch-menu-action { display: flex; justify-content: space-between; align-items: center; - gap: 12px; - padding: 10px 12px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-panel); + width: 100%; + padding: 6px 8px; + border: 0; + border-radius: 5px; + background: transparent; + text-align: left; + font-size: 13px; } -.local-repo-info { +.branch-menu-item.active { + background: var(--bg-hover); +} + +.branch-menu-item:hover, +.branch-menu-action:hover { + background: var(--bg-hover); + border-color: transparent; +} + +.branch-current-mark { + color: var(--text-muted); + font-size: 11px; +} + +.danger-subtle { + color: var(--danger); +} + +/* ── Left panel content ───────────────────────────────────────────────────── */ + +.gd-left-content { + flex: 1; + overflow-y: hidden; + overflow-x: hidden; + display: flex; + flex-direction: column; + min-height: 0; +} + +.gd-left-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px 16px; + text-align: center; + gap: 4px; +} + +.gd-left-empty-minimal { + padding: 16px 12px; + text-align: center; + font-size: 12px; +} + +/* ── Changes list ─────────────────────────────────────────────────────────── */ + +.gd-changes-search-row { + display: grid; + grid-template-columns: 32px 1fr; + gap: 5px; + padding: 7px 8px 6px; + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + flex-shrink: 0; +} + +.gd-history-search-row { + grid-template-columns: 1fr; +} + +.gd-filter-menu-btn { + display: flex; + align-items: center; + justify-content: center; + height: 26px; + padding: 0; + color: var(--text-muted); + background: transparent; + border: 1px solid transparent; + border-radius: 5px; +} + +.gd-filter-menu-btn:hover { + background: var(--bg-hover); + border-color: var(--border); + color: var(--text-main); +} + +.gd-filter-input { + height: 26px; + padding: 3px 8px; + font-size: 12px; + border-color: rgba(110, 118, 129, 0.4); + border-radius: 5px; + background: rgba(0, 0, 0, 0.2); +} + +.gd-filter-input:focus { + border-color: var(--accent); +} + +.gd-changes-filter-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 28px; + padding: 4px 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + flex-shrink: 0; +} + +.gd-check-all { + display: flex; + align-items: center; + gap: 6px; + cursor: default; + font-size: 11px; + color: var(--text-muted); +} + +.gd-check-all input[type="checkbox"] { + width: 13px; + padding: 0; + accent-color: #6e7681; +} + +.gd-changes-list { + flex: 1; + overflow: auto; + min-height: 0; +} + +.change-group-title { + padding: 7px 12px 3px; + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.change-file-row { + display: grid; + grid-template-columns: 16px 20px minmax(0, 1fr); + align-items: center; + gap: 7px; + width: 100%; + padding: 6px 12px; + border: 0; + border-radius: 0; + background: transparent; + text-align: left; +} + +.change-file-row:hover, +.change-file-row.active { + background: var(--bg-hover); +} + +.change-file-checkbox { + width: 13px; + padding: 0; + accent-color: #6e7681; +} + +.change-status { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + background: var(--bg-panel-alt); + color: var(--text-muted); +} + +.change-status-added, +.change-status-untracked { + color: var(--success); +} + +.change-status-deleted, +.change-status-conflicted { + color: var(--danger); +} + +.change-status-renamed { + color: var(--accent); +} + +.change-file-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} + +/* ── History list in sidebar ──────────────────────────────────────────────── */ + +.gd-history-list { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.gd-history-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 12px; + border: 0; + border-radius: 0; + border-bottom: 1px solid rgba(48, 54, 61, 0.4); + background: transparent; + text-align: left; + cursor: pointer; + color: var(--text-main); + transition: background 0.08s; +} + +.gd-history-item:hover, +.gd-history-item.active { + background: var(--bg-hover); +} + +.gd-history-info { min-width: 0; display: flex; flex-direction: column; gap: 2px; } -.local-repo-info span { +.gd-history-name { + font-size: 12px; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-main); +} + +.gd-history-path { + font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.local-scan-empty { - padding: 24px 12px; +/* ── Commit panel ─────────────────────────────────────────────────────────── */ + +.gd-commit-area { + flex-shrink: 0; + padding: 10px; + border-top: 1px solid rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 6px; + background: var(--bg-panel); } -/* ── Empty state ──────────────────────────────────── */ -.empty-state { - grid-column: 1 / -1; +.gd-commit-summary { + height: 28px; + padding: 4px 10px; + font-size: 12px; +} + +.commit-desc-input { + min-height: 54px; + font-size: 12px; + resize: none; +} + +.gd-commit-btn { + width: 100%; + height: 30px; + font-size: 12px; + background: #0969da !important; + border-color: #0969da !important; +} + +.gd-commit-btn:disabled { + opacity: 0.5; +} + +/* ── Diff view ────────────────────────────────────────────────────────────── */ + +.diff-preview { + max-height: calc(100vh - 164px); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: auto; + background: #010409; +} + +.diff-preview-inline { + font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; + font-size: 12px; + line-height: 1.55; +} + +.diff-line { + display: grid; + grid-template-columns: 32px minmax(0, 1fr); + min-height: 20px; + color: #c9d1d9; +} + +.diff-line code { + padding: 2px 12px 2px 0; + white-space: pre; +} + +.diff-gutter { + padding: 2px 8px; + color: #6e7681; text-align: center; - padding: 48px 16px; + user-select: none; + border-right: 1px solid rgba(110, 118, 129, 0.18); } -.empty-state > div:first-child { - font-size: 15px; +.diff-line-added { background: rgba(46, 160, 67, 0.14); } +.diff-line-added .diff-gutter { color: var(--success); background: rgba(46, 160, 67, 0.18); } +.diff-line-removed { background: rgba(248, 81, 73, 0.13); } +.diff-line-removed .diff-gutter { color: var(--danger); background: rgba(248, 81, 73, 0.18); } +.diff-line-hunk { color: #a5d6ff; background: rgba(56, 139, 253, 0.14); } +.diff-line-meta { color: var(--text-muted); background: rgba(110, 118, 129, 0.07); } + +/* ── Workflow empty state ─────────────────────────────────────────────────── */ + +.workflow-empty-state { + min-height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 48px 24px; + text-align: center; +} + +.workflow-empty-icon { + width: 68px; + height: 68px; + display: grid; + place-items: center; + border-radius: 20px; + background: var(--bg-panel-alt); + color: var(--text-muted); +} + +.workflow-empty-icon svg { + width: 44px; + height: 44px; + fill: none; + stroke: currentColor; + stroke-width: 4; + stroke-linecap: round; + stroke-linejoin: round; +} + +.workflow-empty-state h2 { + margin: 0; color: var(--text-main); - margin-bottom: 6px; - font-weight: 500; + font-size: 19px; + font-weight: 650; } -/* ── Repo card details ────────────────────────────── */ -.repo-card strong { - color: var(--accent); - font-size: 14px; +.workflow-empty-state p { + max-width: 400px; + margin: 0; + color: var(--text-muted); + font-size: 13px; + line-height: 1.6; } -.repo-card strong:hover { - text-decoration: underline; +.workflow-empty-actions { + display: flex; + gap: 8px; + margin-top: 4px; +} + +/* ── Main area ────────────────────────────────────────────────────────────── */ + +.gd-main-pad { + padding: 18px 20px; +} + +/* ── History detail in main area ──────────────────────────────────────────── */ + +.commit-message-view { + margin: 0; + padding: 12px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-panel-alt); + color: var(--text-main); + white-space: pre-wrap; + font-family: inherit; + font-size: 13px; +} + +.changed-files-chip-row { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.changed-file-chip { + padding: 2px 8px; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--text-muted); + font-size: 11px; + background: var(--bg-panel-alt); +} + +/* ── Modal system ─────────────────────────────────────────────────────────── */ + +.gd-modal-backdrop { + position: fixed; + inset: 0; + z-index: 50; + display: grid; + place-items: center; + padding: 20px; + background: rgba(1, 4, 9, 0.62); +} + +.gd-modal { + display: flex; + flex-direction: column; + width: min(540px, 100%); + max-height: calc(100vh - 40px); + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-panel); + box-shadow: var(--shadow); + overflow: hidden; +} + +.gd-modal-wide { + width: min(860px, calc(100vw - 40px)); +} + +.gd-modal-full-height { + height: calc(100vh - 40px); +} + +.gd-modal-full-height .gd-modal-body { + overflow: hidden; +} + +.gd-modal-narrow-dialog { + width: min(480px, 100%); +} + +.gd-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.gd-modal-title { + margin: 0; + font-size: 15px; + font-weight: 600; +} + +.gd-modal-close { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text-muted); cursor: pointer; } -/* ── Repository viewer ────────────────────────────── */ -.viewer-panel { - gap: 0; +.gd-modal-close:hover { + background: var(--bg-hover); + color: var(--text-main); + border-color: transparent; } -/* Top bar: breadcrumb + branch controls */ -.viewer-topbar { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - flex-wrap: wrap; - padding: 0 0 14px; +.gd-modal-body { + flex: 1; + overflow: auto; + min-height: 0; } +.gd-modal-two-col { + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 100%; +} + +.gd-modal-section { + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; +} + +.gd-modal-section-alt { + background: var(--bg-app); + border-left: 1px solid var(--border); +} + +.gd-modal-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.gd-modal-section-title { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + margin: 0 0 4px; +} + +.gd-modal-divider { + height: 1px; + background: var(--border); + margin: 4px 0; +} + +.gd-modal-empty { + color: var(--text-muted); + font-size: 13px; + padding: 8px 0; + line-height: 1.5; +} + +.gd-modal-list-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid rgba(48, 54, 61, 0.5); +} + +.gd-modal-list-item:last-child { + border-bottom: 0; +} + +.gd-modal-item-info { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.gd-modal-item-info strong { + font-size: 13px; + font-weight: 600; + color: var(--text-main); +} + +.gd-modal-item-info strong.accent { + color: var(--accent); +} + +.gd-modal-scroll-list { + flex: 1; + overflow-y: auto; + min-height: 0; + max-height: 300px; +} + +.gd-modal-scan-list { + display: flex; + flex-direction: column; + max-height: 200px; + overflow-y: auto; +} + +.gd-modal-narrow { + max-width: 440px; + margin: 0 auto; +} + +.gd-path-text { + font-size: 11px; + font-family: ui-monospace, "Cascadia Code", "Fira Code", Consolas, monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Viewer modal layout */ +.gd-modal-viewer { + display: grid; + grid-template-columns: 240px 1fr; + height: 100%; + min-height: 0; +} + +.gd-modal-viewer-sidebar { + display: flex; + flex-direction: column; + border-right: 1px solid var(--border); + overflow: hidden; + background: var(--bg-panel); +} + +.gd-modal-viewer-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + flex-wrap: wrap; +} + +.gd-modal-viewer-main { + overflow: auto; + min-width: 0; + background: var(--bg-app); + display: flex; + flex-direction: column; +} + +.gd-viewer-tree { + flex: 1; + overflow-y: auto; +} + +/* ── Servers UI ───────────────────────────────────────────────────────────── */ + +.gd-server-item { + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-panel-alt); +} + +.gd-server-active { + border-color: var(--accent); + background: rgba(47, 129, 247, 0.06); +} + +.gd-server-name { + font-size: 13px; + font-weight: 600; +} + +.gd-server-url { + font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Repo & org display ───────────────────────────────────────────────────── */ + +.repo-filter-pills { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + +.pill-btn { + padding: 3px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + font-size: 12px; + cursor: pointer; + transition: background 0.1s, color 0.1s, border-color 0.1s; +} + +.pill-btn:hover { + background: var(--bg-hover); + color: var(--text-main); + border-color: #8b949e; +} + +.pill-btn.active { + background: var(--accent-strong); + border-color: transparent; + color: #fff; +} + +/* ── Branch dialog ────────────────────────────────────────────────────────── */ + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 60; + display: grid; + place-items: center; + padding: 24px; + background: rgba(1, 4, 9, 0.58); +} + +.modal-card { + width: min(420px, 100%); + display: flex; + flex-direction: column; + gap: 12px; + padding: 18px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-panel); + box-shadow: var(--shadow); +} + +.modal-card h3, +.modal-card p { + margin: 0; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.danger-note { + padding: 10px 12px; + border: 1px solid rgba(248, 81, 73, 0.35); + border-radius: var(--radius-md); + color: var(--danger); + background: rgba(248, 81, 73, 0.08); + font-size: 13px; +} + +/* ── Repository viewer ────────────────────────────────────────────────────── */ + .viewer-crumb-row { display: flex; align-items: center; - gap: 4px; + gap: 2px; flex-wrap: wrap; min-width: 0; } @@ -594,7 +1241,7 @@ border: 0; background: transparent; color: var(--accent); - font-size: 16px; + font-size: 14px; font-weight: 600; } @@ -607,65 +1254,26 @@ .viewer-crumb-sep { color: var(--text-muted); - font-size: 16px; + font-size: 14px; font-weight: 300; padding: 0 2px; user-select: none; } -.viewer-source-badge { - font-size: 12px; - font-weight: 500; - padding: 2px 8px; - border: 1px solid var(--border); - border-radius: 999px; - color: var(--text-muted); - background: var(--bg-panel-alt); - white-space: nowrap; -} - -.viewer-controls { - gap: 6px; - align-items: center; -} - -/* File table */ -.viewer-table { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; - margin-bottom: 16px; -} - -.viewer-table-header { - display: grid; - grid-template-columns: 20px 1fr auto; - align-items: center; - gap: 8px; - padding: 8px 16px; - background: var(--bg-panel-alt); - border-bottom: 1px solid var(--border); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-muted); -} - .viewer-row { display: grid; grid-template-columns: 20px 1fr auto; align-items: center; width: 100%; - gap: 8px; - padding: 9px 16px; + gap: 7px; + padding: 7px 12px; border: 0; border-radius: 0; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid rgba(48, 54, 61, 0.4); background: var(--bg-panel); text-align: left; color: var(--text-main); - font-size: 14px; + font-size: 13px; cursor: pointer; transition: background 0.08s ease; } @@ -676,12 +1284,10 @@ .viewer-row:hover { background: var(--bg-hover); - border-color: var(--border); } .viewer-row.active { background: rgba(47, 129, 247, 0.07); - border-color: var(--border); } .viewer-row-icon { @@ -702,19 +1308,12 @@ } .viewer-row-size { - font-size: 12px; + font-size: 11px; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; } -.viewer-empty-row { - padding: 32px 16px; - text-align: center; - font-size: 13px; -} - -/* File preview panel (below the table) */ .viewer-file-panel { border: 1px solid var(--border); border-radius: var(--radius-lg); @@ -729,7 +1328,7 @@ justify-content: space-between; align-items: center; gap: 12px; - padding: 10px 16px; + padding: 9px 14px; background: var(--bg-panel-alt); border-bottom: 1px solid var(--border); } @@ -743,7 +1342,6 @@ color: var(--text-main); } -/* README panel */ .viewer-readme-panel { border: 1px solid var(--border); border-radius: var(--radius-lg); @@ -755,148 +1353,24 @@ display: flex; align-items: center; gap: 8px; - padding: 10px 16px; + padding: 9px 14px; background: var(--bg-panel-alt); border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 600; - color: var(--text-main); } .viewer-readme-body { background: var(--bg-panel); } -.markdown-body { - padding: 24px; - font-family: inherit; - font-size: 14px; - line-height: 1.75; - word-break: break-word; - color: var(--text-main); - background: var(--bg-panel); -} - -.markdown-body > :first-child { - margin-top: 0; -} - -.markdown-body > :last-child { - margin-bottom: 0; -} - -.markdown-body h1, -.markdown-body h2 { - padding-bottom: 8px; - border-bottom: 1px solid var(--border); -} - -.markdown-body h1 { - margin: 0 0 16px; - font-size: 28px; -} - -.markdown-body h2 { - margin: 24px 0 12px; - font-size: 22px; -} - -.markdown-body h3 { - margin: 20px 0 10px; - font-size: 18px; -} - -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin: 16px 0 8px; -} - -.markdown-body p { - margin: 0 0 12px; -} - -.markdown-body ul, -.markdown-body ol { - margin: 0 0 12px; - padding-left: 24px; -} - -.markdown-body blockquote { - margin: 0 0 12px; - padding: 0 12px; - border-left: 3px solid var(--border); - color: var(--text-muted); -} - -.markdown-body hr { - height: 1px; - margin: 24px 0; - border: 0; - background: var(--border); -} - -.markdown-body a { - color: var(--accent); - text-decoration: none; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body code { - padding: 2px 5px; - border-radius: var(--radius-md); - background: var(--bg-panel-alt); - font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; - font-size: 85%; -} - -.markdown-body .markdown-code { - margin: 0 0 12px; - padding: 12px; - overflow: auto; - border-radius: var(--radius-md); - background: #010409; -} - -.markdown-body .markdown-code code { - padding: 0; - background: transparent; - font-size: 12px; - line-height: 1.6; -} - -/* Code preview (shared by file panel) */ -.code-preview { - flex: 1; - margin: 0; - padding: 16px; - overflow: auto; - background: #010409; - color: var(--text-main); - font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; - font-size: 12px; - line-height: 1.7; - white-space: pre; -} - -.syntax-keyword { - color: #ff7b72; -} - -.syntax-key { - color: #79c0ff; -} - .viewer-error, .viewer-loading { border: 1px solid var(--border); border-radius: var(--radius-md); - padding: 8px 12px; - font-size: 13px; - margin-bottom: 12px; + padding: 7px 12px; + font-size: 12px; + margin-bottom: 10px; } .viewer-error { @@ -910,17 +1384,162 @@ background: var(--bg-app); } -.viewer-empty { - align-self: stretch; +/* ── Markdown renderer ────────────────────────────────────────────────────── */ + +.markdown-body { + padding: 20px 24px; + font-family: inherit; + font-size: 14px; + line-height: 1.75; + word-break: break-word; + color: var(--text-main); + background: var(--bg-panel); } -/* ── Responsive ───────────────────────────────────── */ -@media (max-width: 1180px) { +.markdown-body > :first-child { margin-top: 0; } +.markdown-body > :last-child { margin-bottom: 0; } + +.markdown-body h1, +.markdown-body h2 { + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +.markdown-body h1 { margin: 0 0 16px; font-size: 26px; } +.markdown-body h2 { margin: 24px 0 12px; font-size: 20px; } +.markdown-body h3 { margin: 20px 0 10px; font-size: 17px; } +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { margin: 16px 0 8px; } +.markdown-body p { margin: 0 0 12px; } +.markdown-body ul, +.markdown-body ol { margin: 0 0 12px; padding-left: 24px; } +.markdown-body blockquote { + margin: 0 0 12px; + padding: 0 12px; + border-left: 3px solid var(--border); + color: var(--text-muted); +} +.markdown-body hr { + height: 1px; + margin: 24px 0; + border: 0; + background: var(--border); +} +.markdown-body a { color: var(--accent); text-decoration: none; } +.markdown-body a:hover { text-decoration: underline; } +.markdown-body code { + padding: 2px 5px; + border-radius: var(--radius-md); + background: var(--bg-panel-alt); + font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; + font-size: 85%; +} +.markdown-body .markdown-code { + margin: 0 0 12px; + padding: 12px; + overflow: auto; + border-radius: var(--radius-md); + background: #010409; +} +.markdown-body .markdown-code code { + padding: 0; + background: transparent; + font-size: 12px; + line-height: 1.6; +} + +/* ── Code preview ─────────────────────────────────────────────────────────── */ + +.code-preview { + flex: 1; + margin: 0; + padding: 14px 16px; + overflow: auto; + background: #010409; + color: var(--text-main); + font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; + font-size: 12px; + line-height: 1.7; + white-space: pre; +} + +.syntax-keyword { color: #ff7b72; } +.syntax-key { color: #79c0ff; } + +/* ── Git output ───────────────────────────────────────────────────────────── */ + +.git-output { + background: #010409; + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 10px 12px; + font-size: 11px; + font-family: ui-monospace, "Cascadia Code", "Fira Code", "Consolas", monospace; + color: var(--success); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + margin: 0; + line-height: 1.6; +} + +.gd-git-out { + margin: 8px; + font-size: 10px; +} + +/* ── Shared empty state ───────────────────────────────────────────────────── */ + +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 40px 16px; +} + +.empty-state > div:first-child { + font-size: 14px; + color: var(--text-main); + margin-bottom: 5px; + font-weight: 500; +} + +/* ── Responsive ───────────────────────────────────────────────────────────── */ + +@media (max-width: 860px) { .layout { - grid-template-columns: 220px 1fr; + grid-template-columns: 240px minmax(0, 1fr); } - .rightbar { + .gd-toolbar-left { + width: 240px; + } + + .gd-modal-two-col { + grid-template-columns: 1fr; + } + + .gd-modal-section-alt { + border-left: 0; + border-top: 1px solid var(--border); + } + + .gd-modal-viewer { + grid-template-columns: 200px 1fr; + } +} + +@media (max-width: 640px) { + .layout { + grid-template-columns: 220px minmax(0, 1fr); + } + + .gd-toolbar-left { + width: 220px; + grid-template-columns: 1fr; + } + + .gd-branch-wrap { display: none; } } diff --git a/frontend/js/app.js b/frontend/js/app.js index 675c8f0..ca0b63f 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,13 +1,28 @@ import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js"; import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js"; import { + checkoutBranch, + commitChanges, + createBranch, + deleteBranch, + getCommitDetail, + getCommitHistory, + getFileDiff, + getRepositorySyncStatus, + getWorkingTreeStatus, listLocalRepoBranches, listLocalRepoTree, + openInExternalEditor, + openInFileExplorer, readLocalRepoFile, + renameBranch, runGitBranch, runGitClone, + runGitFetch, runGitPull, + runGitPublishBranch, runGitPush, + runGitSync, runGitStatus, scanLocalRepos, testGiteaConnection, @@ -24,20 +39,26 @@ const mockRepos = [ ]; let repositories = [...mockRepos]; -let currentUserLogin = ""; // authenticated user's login name +let currentUserLogin = ""; let serverTestResult = ""; let settingsNotice = ""; let gitOutput = ""; -let activeRightTab = "clone"; // "clone" | "settings" | "servers" -let activeMainTab = "repos"; // "repos" | "local" | "viewer" -let repoOwnerFilter = "all"; // "all" | "personal" | "orgs" +let activeView = "changes"; // "changes" | "history" +let activeModal = ""; // "" | "repos" | "clone" | "servers" | "settings" | "viewer" +let utilityMenuOpen = false; +let repoOwnerFilter = "all"; const maxPreviewBytes = 256 * 1024; +const defaultRepositoryName = "Gitpub-Desktop"; +const defaultBranchName = "main"; const FOLDER_ICON = ``; const FILE_ICON = ``; -const LOCAL_REPO_ICON = ``; -const BRANCH_ICON = ``; -const SYNC_ICON = ``; +const LOCAL_REPO_ICON = ``; +const BRANCH_ICON = ``; +const SYNC_ICON = ``; +const PULL_ICON = ``; +const PUSH_ICON = ``; +const PUBLISH_ICON = ``; function uid() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; @@ -68,6 +89,14 @@ function repoNameFromPath(path = "") { return path.split(/[/\\]/).filter(Boolean).pop() || path; } +function currentRepositoryName() { + return getState().selectedRepoName || defaultRepositoryName; +} + +function currentBranchName() { + return getState().workingTree.branch || getState().viewer.branch || defaultBranchName; +} + function normalizeRemoteUrl(value = "") { return value.trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase(); } @@ -82,6 +111,290 @@ function serverRepoRemoteUrls() { return [...new Set(urls.map((url) => normalizeRemoteUrl(url || "")).filter(Boolean))]; } +function applyTheme() { + const theme = getState().settings.theme || "dark"; + const systemPrefersLight = window.matchMedia?.("(prefers-color-scheme: light)")?.matches; + document.documentElement.dataset.theme = theme === "system" && systemPrefersLight ? "light" : theme; +} + +function selectedGitPath() { + return getState().settings.gitExecutablePath; +} + +function statusLabel(status = "") { + const labels = { + modified: "Modified", + added: "Added", + deleted: "Deleted", + renamed: "Renamed", + untracked: "Untracked", + conflicted: "Conflicted", + }; + return labels[status] || "Modified"; +} + +function pluralize(count, singular, plural = `${singular}s`) { + return `${count} ${count === 1 ? singular : plural}`; +} + +function remoteDisplayName(sync = {}) { + return sync.upstreamRemote || sync.defaultRemote || "origin"; +} + +function syncButtonConfig(state) { + if (!state.selectedRepoPath) { + return { + label: "No Repository", + subLabel: "Select a repo to begin", + tooltip: "Select a repository to sync.", + icon: SYNC_ICON, + action: "", + disabled: true, + }; + } + + const sync = state.sync; + const remote = remoteDisplayName(sync); + const operationLabels = { + fetch: "Fetching…", + pull: "Pulling…", + push: "Pushing…", + sync: "Syncing…", + publish: "Publishing…", + }; + + if (sync.operation) { + return { + label: operationLabels[sync.operation] || "Working…", + subLabel: "Git operation in progress", + tooltip: "A Git operation is already running.", + icon: sync.operation === "pull" ? PULL_ICON : sync.operation === "publish" || sync.operation === "push" ? PUSH_ICON : SYNC_ICON, + action: sync.operation, + disabled: true, + loading: true, + }; + } + + if (sync.loading && !sync.lastUpdated) { + return { + label: "Loading…", + subLabel: "Reading repository state", + tooltip: "Reading repository sync state.", + icon: SYNC_ICON, + action: "", + disabled: true, + loading: true, + }; + } + + if (sync.isDetached) { + return { + label: "Detached HEAD", + subLabel: "Checkout a branch to sync", + tooltip: "Detached HEAD cannot be pushed or pulled. Checkout a branch first.", + icon: BRANCH_ICON, + action: "", + disabled: true, + }; + } + + if (!sync.hasRemote && sync.lastUpdated) { + return { + label: "No remote", + subLabel: "Add a remote to sync", + tooltip: "This repository does not have a Git remote configured.", + icon: SYNC_ICON, + action: "", + disabled: true, + }; + } + + if (sync.isUnpublished || (!sync.upstream && sync.hasRemote)) { + return { + label: "Publish branch", + subLabel: `Publish ${sync.branch || "branch"} to ${remote}`, + tooltip: `Publish ${sync.branch || "the current branch"} to ${remote}.`, + icon: PUBLISH_ICON, + action: "publish", + disabled: false, + }; + } + + if (sync.behind > 0 && sync.ahead > 0) { + return { + label: "Sync changes", + subLabel: `${pluralize(sync.behind, "commit")} to pull · ${pluralize(sync.ahead, "commit")} to push`, + tooltip: `${pluralize(sync.behind, "commit")} to pull and ${pluralize(sync.ahead, "commit")} ready to push.`, + icon: SYNC_ICON, + action: "sync", + disabled: false, + }; + } + + if (sync.behind > 0) { + return { + label: `Pull ${remote}`, + subLabel: `${pluralize(sync.behind, "commit")} to pull`, + tooltip: `${pluralize(sync.behind, "commit")} to pull from ${remote}.`, + icon: PULL_ICON, + action: "pull", + disabled: false, + }; + } + + if (sync.ahead > 0) { + return { + label: `Push ${remote}`, + subLabel: `${pluralize(sync.ahead, "commit")} ready to push`, + tooltip: `${pluralize(sync.ahead, "commit")} ready to push to ${remote}.`, + icon: PUSH_ICON, + action: "push", + disabled: false, + }; + } + + return { + label: `Fetch ${remote}`, + subLabel: sync.error || "Your branch is up to date", + tooltip: sync.error ? `Last sync check failed: ${sync.error}` : "Your branch is up to date.", + icon: SYNC_ICON, + action: "fetch", + disabled: false, + }; +} + +function groupedChangedFiles(files = []) { + const order = ["modified", "added", "deleted", "renamed", "untracked", "conflicted"]; + const groups = new Map(order.map((status) => [status, []])); + files.forEach((file) => { + const key = groups.has(file.status) ? file.status : "modified"; + groups.get(key).push(file); + }); + return order.map((status) => ({ status, files: groups.get(status) })).filter((group) => group.files.length); +} + +function diffTemplate(diffResult) { + if (!diffResult) { + return workflowEmptyStateTemplate({ + title: "Select a changed file", + message: "Choose a file from the changes list to inspect its diff before committing.", + icon: "diff", + actions: false, + }); + } + if (diffResult.isBinary) { + return workflowEmptyStateTemplate({ + title: "Binary file", + message: diffResult.diff || "Diff preview is not available for binary files.", + icon: "file", + actions: false, + }); + } + if (diffResult.isDeleted && !diffResult.diff) { + return workflowEmptyStateTemplate({ + title: "Deleted file", + message: "This file was removed from the working tree.", + icon: "file", + actions: false, + }); + } + return `
${renderDiff(diffResult.diff || "")}
`; +} + +function branchOptionTemplate(branch) { + return ``; +} + +function renderDiff(diff = "") { + const lines = diff.split("\n"); + return lines + .map((line) => { + let kind = "context"; + if (line.startsWith("+++") || line.startsWith("---")) kind = "meta"; + else if (line.startsWith("@@")) kind = "hunk"; + else if (line.startsWith("+")) kind = "added"; + else if (line.startsWith("-")) kind = "removed"; + const sign = line.slice(0, 1) || " "; + return `
+ ${escapeHtml(sign)} + ${escapeHtml(line || " ")} +
`; + }) + .join(""); +} + +function emptyStateIcon(type = "repo") { + if (type === "diff") { + return ``; + } + if (type === "file") { + return ``; + } + return ``; +} + +function workflowEmptyStateTemplate({ title, message, icon = "repo", actions = true }) { + return `
+
${emptyStateIcon(icon)}
+

${escapeHtml(title)}

+

${escapeHtml(message)}

+ ${actions ? `
+ + +
` : ""} +
`; +} + +function openBranchDialog(mode, target = "") { + const dialog = getState().branches.dialog; + dialog.mode = mode; + dialog.target = target; + dialog.value = mode === "rename" ? target : ""; + dialog.error = ""; + getState().branches.menuOpen = false; + render(); +} + +function closeBranchDialog() { + Object.assign(getState().branches.dialog, { mode: "", target: "", value: "", error: "" }); + render(); +} + +function branchDialogTemplate(state, displayBranchName) { + const dialog = state.branches.dialog; + if (!dialog.mode) return ""; + const isDelete = dialog.mode === "delete"; + const title = dialog.mode === "create" ? "Create Branch" : dialog.mode === "rename" ? "Rename Branch" : "Delete Branch"; + const description = isDelete + ? "Choose a local branch to delete. This only removes the local branch." + : dialog.mode === "create" + ? `Create a new branch from ${displayBranchName}.` + : `Rename ${dialog.target}.`; + return ``; +} + function languageForPath(path = "") { const extension = path.split(".").pop()?.toLowerCase(); const names = { @@ -280,7 +593,6 @@ function normaliseLocalEntries(entries) { } function serverFormTemplate(server = null) { - // Reused in first-launch setup and in Settings server management. const defaults = { id: "", displayName: "", @@ -292,8 +604,8 @@ function serverFormTemplate(server = null) { }; const config = { ...defaults, ...(server || {}) }; return ` -
-

${server ? "Edit Server" : "Add Server"}

+
+

${server ? "Edit Server" : "Add Server"}

Display name
@@ -315,11 +627,11 @@ function serverFormTemplate(server = null) {
-
+
Username
-
+
Password
@@ -356,8 +668,6 @@ function welcomeView() { function isOrgRepo(repo) { const ownerLogin = repo.owner?.login || repo.full_name.split("/")[0]; - // When we know the authenticated user, compare against owner login. - // Fall back to owner.type for cases where user info wasn't fetched. if (currentUserLogin) { return ownerLogin.toLowerCase() !== currentUserLogin.toLowerCase(); } @@ -391,26 +701,12 @@ function groupedByOrg(repos) { return groups; } -function repoCardTemplate(repo) { - return ` -
-
${escapeHtml(repo.full_name)}
-
${repo.private ? "Private" : "Public"} · ${escapeHtml(repo.updated_at || "recently")}
-
- - - -
-
- `; -} - function localRepoScanTemplate() { const state = getState(); const results = state.localRepoScanResults || []; if (state.localRepoScanLoading) { - return `
Scanning local folders for repositories from the selected Gitea server...
`; + return `
Scanning local folders…
`; } if (state.localRepoScanError) { @@ -418,27 +714,24 @@ function localRepoScanTemplate() { } if (!results.length) { - return `
-
No local repositories scanned yet
-
Only local repos with remotes from the selected Gitea server will be shown.
-
`; + return `
No local repositories scanned yet. Only repos with remotes from the selected server will appear.
`; } - return `
+ return `
${results .map( (repo) => ` -
-
+
+
${escapeHtml(repo.name || repoNameFromPath(repo.path))} - ${escapeHtml(repo.path)} - ${repo.matchedRemoteUrl ? `Remote: ${escapeHtml(repo.matchedRemoteUrl)}` : ""} + ${escapeHtml(repo.path)} + ${repo.matchedRemoteUrl ? `${escapeHtml(repo.matchedRemoteUrl)}` : ""}
-
- - +
+ +
-
+
` ) .join("")} @@ -477,326 +770,560 @@ function filePreviewTemplate(file) { return header + `
${highlightCode(file.content || "", language)}
`; } -function viewerTemplate() { - const { viewer } = getState(); - if (!viewer.source) { - return ` -
-

Repository Viewer

-
-
No repository selected
-
Use View on a repository card, or open a local path and click View files.
+// ── Modal content functions ─────────────────────────────────────────────────── + +function reposModalContent(state) { + const visibleRepos = filteredRepositories(); + const activeServer = getActiveServer(); + return ` +
+
+

Recent Local Repositories

+ ${state.settings.recentRepositories.length + ? state.settings.recentRepositories.map((path) => ` +
+
+ ${escapeHtml(repoNameFromPath(path))} + ${escapeHtml(path)} +
+
+ + +
+
`).join("") + : `
No recent repositories
`} + +
+ +

Open from Path

+
+ +
-
- `; + +
+ +

Find Local Repositories

+
+ + +
+ ${localRepoScanTemplate()} +
+ +
+
+

Server Repositories

+
+ ${escapeHtml(activeServer?.displayName || "No server")} + +
+
+ +
+ + + +
+
+ ${visibleRepos.slice(0, 60).map((repo) => ` +
+
+ ${escapeHtml(repo.full_name)} + ${repo.private ? "Private" : "Public"} · ${escapeHtml(repo.updated_at || "recently")} +
+
+ + +
+
`).join("") || `
No repositories found
`} +
+
+
+ `; +} + +function cloneModalContent(state) { + const visibleRepos = filteredRepositories(); + return ` +
+
+
Server
+ +
+
+
Remote repository
+ +
+
+
Repository URL
+ +
+
+
Destination path
+ +
+ + ${gitOutput + ? `
${escapeHtml(gitOutput)}
` + : `Clone output will appear here.`} +
+ `; +} + +function serversModalContent(state) { + const servers = state.settings.servers; + return ` +
+
+
+

Connected Servers

+ +
+ ${settingsNotice ? `
${escapeHtml(settingsNotice)}
` : ""} + ${servers.length + ? servers.map((server) => ` +
+
${escapeHtml(server.displayName)}
+
${escapeHtml(server.serverUrl)}
+
+ + + +
+
`).join("") + : `
No servers configured
`} +
+
+
Select a server to edit, or add a new one.
+
+
+ `; +} + +function settingsModalContent(state) { + return ` +
+
+

Appearance

+
Theme
+ + +
+ +

Git

+
Git executable path
+ +
Default clone directory
+ +
External editor command
+ + +
+ +
Default server
+ + + + ${settingsNotice ? `
${escapeHtml(settingsNotice)}
` : ""} +
+
+

About

+
+

Gitpub Desktop

+

A focused desktop client for Gitea-compatible Git servers.

+

Built with Tauri 2 and vanilla JavaScript.

+
+
+
+ `; +} + +function viewerModalContent(state) { + const viewer = state.viewer; + if (!viewer.source) { + return `
+

No repository open in viewer

+

Open a repository from the Repositories panel first.

+
`; } const rows = []; if (viewer.path) { - rows.push(` - `); + rows.push(``); } viewer.entries.forEach((entry) => { const active = viewer.selectedFile?.path === entry.path ? " active" : ""; - rows.push(` - `); + rows.push(``); }); - if (!rows.length && !viewer.loading) { - rows.push(`
This folder is empty.
`); - } const showReadme = viewer.readmeFile && !viewer.selectedFile; return ` -
-
-
- ${breadcrumbTemplate(viewer.repoName || "repo", viewer.path)} -
-
- ${viewer.source === "remote" ? "Remote" : "Local"} - - -
-
- - ${viewer.error ? `
${escapeHtml(viewer.error)}
` : ""} - ${viewer.loading ? `
Loading…
` : ""} - -
-
- - Name - Size -
- ${rows.join("")} -
- - ${viewer.selectedFile ? `
${filePreviewTemplate(viewer.selectedFile)}
` : ""} - - ${showReadme ? ` -
-
- ${FILE_ICON} - ${escapeHtml(viewer.readmeFile.path.split("/").pop())} +
+
+
+
+ ${breadcrumbTemplate(viewer.repoName || "repo", viewer.path)}
-
${renderMarkdown(viewer.readmeFile.content || "")}
-
` : ""} -
+
+ + +
+
+
+ ${rows.join("")} + ${!rows.length && !viewer.loading ? `
Empty folder
` : ""} +
+
+
+ ${viewer.error ? `
${escapeHtml(viewer.error)}
` : ""} + ${viewer.loading ? `
Loading…
` : ""} + ${viewer.selectedFile ? `
${filePreviewTemplate(viewer.selectedFile)}
` : ""} + ${showReadme ? ` +
+
${FILE_ICON}${escapeHtml(viewer.readmeFile.path.split("/").pop())}
+
${renderMarkdown(viewer.readmeFile.content || "")}
+
` : ""} + ${!viewer.selectedFile && !viewer.readmeFile && !viewer.loading + ? `
Select a file from the tree to preview
` + : ""} +
+
`; } +function modalTemplate(state) { + if (!activeModal) return ""; + const viewer = state.viewer; + const configs = { + repos: { title: "Repositories", content: reposModalContent(state), wide: true }, + clone: { title: "Clone Repository", content: cloneModalContent(state), wide: false }, + servers: { title: "Gitea Servers", content: serversModalContent(state), wide: true }, + settings: { title: "Settings", content: settingsModalContent(state), wide: true }, + viewer: { title: viewer.repoName ? `File Viewer — ${escapeHtml(viewer.repoName)}` : "File Viewer", content: viewerModalContent(state), wide: true, fullHeight: true }, + }; + const cfg = configs[activeModal]; + if (!cfg) return ""; + + return ``; +} + +// ── Main dashboard view ─────────────────────────────────────────────────────── + function dashboardView() { const state = getState(); - const activeServer = getActiveServer(); - const visibleRepos = filteredRepositories(); + const displayRepoName = currentRepositoryName(); + const displayBranchName = currentBranchName(); + const hasLocalRepo = Boolean(state.selectedRepoPath); + // ── Repository identity ───────────────────────────────────────────────── + const repoIdentity = (() => { + if (!state.selectedRepoPath && !state.selectedRepoName) { + return { primary: "No repository", secondary: "Select or clone a repository" }; + } + const name = state.selectedRepoName || repoNameFromPath(state.selectedRepoPath); + const serverRepo = repositories.find((r) => { + const rName = r.full_name.split("/")[1] || ""; + return rName === name || r.full_name === name; + }); + if (serverRepo) { + return { primary: serverRepo.full_name, secondary: state.selectedRepoPath || "" }; + } + const pathParts = (state.selectedRepoPath || "").split(/[/\\]/).filter(Boolean); + const primary = pathParts.length >= 2 ? pathParts.slice(-2).join("/") : name; + return { primary, secondary: state.selectedRepoPath || "" }; + })(); + + // ── Contextual sync action ────────────────────────────────────────────── + const syncCfg = syncButtonConfig(state); + + // ── Toolbar ───────────────────────────────────────────────────────────── + const toolbarHTML = ` +
+ +
+ + ${state.branches.menuOpen ? `
+
+
Switch Branch
+ ${(state.branches.items.length ? state.branches.items : [{ name: displayBranchName, current: true }]).map(branchOptionTemplate).join("")} +
+
+ + + +
+
` : ""} +
+
+ +
+ +
+ +
+
+ + +
+
+ + ${utilityMenuOpen ? `` : ""} +
+
+ `; + + // ── Sidebar content ───────────────────────────────────────────────────── + const filteredFiles = state.workingTree.files.filter((file) => + file.path.toLowerCase().includes(state.changesFilter.trim().toLowerCase()) + ); + const selectedCount = filteredFiles.filter((file) => state.workingTree.selectedPaths.has(file.path)).length; + const allSelected = filteredFiles.length > 0 && selectedCount === filteredFiles.length; + + let leftContentHTML = ""; + + if (activeView === "history") { + const historyFilter = state.historyFilter.trim().toLowerCase(); + const commits = state.history.commits.filter((commit) => + [commit.title, commit.author, commit.shortHash].some((v) => (v || "").toLowerCase().includes(historyFilter)) + ); + leftContentHTML = ` +
+ +
+
+ ${state.history.loading ? `
Loading…
` : ""} + ${state.history.error ? `
${escapeHtml(state.history.error)}
` : ""} + ${!hasLocalRepo ? `
No repository selected
` : ""} + ${hasLocalRepo && !state.history.loading && commits.length + ? commits.map((commit) => ` + `).join("") + : ""} + ${hasLocalRepo && !state.history.loading && !commits.length + ? `
No commits found
` + : ""} +
+ `; + } else { + leftContentHTML = ` +
+ + +
+
+ + ${selectedCount > 0 ? `${selectedCount} staged` : ""} +
+
+ ${state.workingTree.loading ? `
Loading…
` : ""} + ${state.workingTree.error ? `
${escapeHtml(state.workingTree.error)}
` : ""} + ${!hasLocalRepo ? `
No repository open
` : ""} + ${hasLocalRepo && !state.workingTree.loading && !filteredFiles.length && !state.workingTree.error + ? `
Working tree is clean
` + : ""} + ${groupedChangedFiles(filteredFiles).map((group) => ` +
+
${statusLabel(group.status)}
+ ${group.files.map((file) => ` + + `).join("")} +
+ `).join("")} + ${gitOutput ? `
${escapeHtml(gitOutput)}
` : ""} +
+ `; + } + + // ── Main area ─────────────────────────────────────────────────────────── + let mainHTML = ""; + + if (activeView === "history") { + const commit = state.history.selectedCommit; + if (commit) { + mainHTML = ` +
+
+
+

${escapeHtml(commit.title)}

+

${escapeHtml(commit.author)} · ${escapeHtml(commit.date)} · ${escapeHtml(commit.shortHash)}

+
+ +
+
${escapeHtml(commit.message)}
+
+ ${(commit.files || []).map((file) => `${escapeHtml(file.status)} ${escapeHtml(file.path)}`).join("")} +
+
${renderDiff(commit.diff || "No diff available.")}
+
`; + } else { + mainHTML = workflowEmptyStateTemplate({ + title: hasLocalRepo ? "No commit selected" : "Open a repository to view history", + message: hasLocalRepo ? "Select a commit from the list to review its files and diff." : "History appears after you open or clone a local repository.", + icon: "diff", + actions: !hasLocalRepo, + }); + } + } else { + const selectedChange = state.workingTree.files.find((file) => file.path === state.workingTree.selectedPath); + mainHTML = ` +
+
+
+

${selectedChange ? escapeHtml(selectedChange.path) : hasLocalRepo ? "No local changes" : "No repository selected"}

+

${selectedChange ? `${statusLabel(selectedChange.status)} file` : hasLocalRepo ? "The working tree is clean." : "Open or clone a repository to begin."}

+
+ +
+ ${state.workingTree.diffLoading ? `
Loading diff…
` : ""} + ${state.workingTree.diffError ? `
${escapeHtml(state.workingTree.diffError)}
` : ""} + ${selectedChange + ? diffTemplate(state.workingTree.selectedDiff) + : workflowEmptyStateTemplate({ + title: hasLocalRepo ? "You're all caught up" : "Start with a repository", + message: hasLocalRepo + ? "There are no uncommitted changes in this repository." + : "Open a local repository or clone one from a Gitea-compatible server.", + icon: hasLocalRepo ? "diff" : "repo", + actions: !hasLocalRepo, + })} +
`; + } + + // ── Commit panel ──────────────────────────────────────────────────────── + const hasCommitSummary = Boolean((state.commitSummary || state.commitMessage || "").trim()); + const hasSelectedFiles = state.workingTree.selectedPaths.size > 0; + const canCommit = hasLocalRepo && hasSelectedFiles && hasCommitSummary; + + const commitAreaHTML = ` +
+ + + +
`; + + // ── Assemble ───────────────────────────────────────────────────────────── appRoot.innerHTML = `
-