refactor: extract CSS modules for terminal and git components (Task 2.2)

- Create TerminalComponent.module.css with terminal-* styles
- Create GitToolbar.module.css with git toolbar styles
- Create CommitDialog.module.css with commit dialog styles
- Create MergeDialog.module.css with merge dialog styles
- Create FileEditor.module.css with file editor styles
- Update all components to import their CSS modules
- Remove extracted rules from styles.css (~441 lines removed)

Quality gates: tsc (pass), eslint (pass), build (pass)
Refs: repo-restructure Task 2.2
This commit is contained in:
Developer
2026-06-02 20:39:32 +00:00
parent 58a9728d5e
commit dd7696b5a4
15 changed files with 1263 additions and 1845 deletions
+15 -14
View File
@@ -1,3 +1,4 @@
import styles from "./features/git/CommitDialog.module.css";
import React, { useState } from "react";
import { Icon } from "./icon";
@@ -71,40 +72,40 @@ export const CommitDialog: React.FC<CommitDialogProps> = ({
const hasChanges = diff.some((d) => d.type !== "same");
return (
<div className="dialog-overlay">
<div className="commit-dialog">
<div className="dialog-header">
<div className={styles.dialogOverlay}>
<div className={styles.commitDialog}>
<div className={styles.dialogHeader}>
<h3>Commit Changes</h3>
<button className="dialog-close" onClick={onCancel} type="button">
<button className={styles.dialogClose} onClick={onCancel} type="button">
×
</button>
</div>
<div className="dialog-body">
<p className="file-info">
<div className={styles.dialogBody}>
<p className={styles.fileInfo}>
Editing: <strong>{filePath}</strong>
</p>
{!hasChanges && (
<div className="warning-message">No changes to commit</div>
<div className={styles.warningMessage}>No changes to commit</div>
)}
{hasChanges && (
<div className="diff-preview">
<div className={styles.diffPreview}>
<h4>Changes</h4>
<div className="diff-content">
<div className={styles.diffContent}>
{diff.map((line, i) => (
<div
key={i}
className={`diff-line diff-${line.type}`}
className={`${styles.diffLine} ${line.type === "added" ? styles.diffAdded : line.type === "removed" ? styles.diffRemoved : styles.diffSame}`}
>
<span className="diff-line-number">{line.lineNum}</span>
<span className="diff-marker">
<span className={styles.diffLineNumber}>{line.lineNum}</span>
<span className={styles.diffMarker}>
{line.type === "added" && "+"}
{line.type === "removed" && "-"}
{line.type === "same" && " "}
</span>
<span className="diff-line-content">{line.line}</span>
<span className={styles.diffLineContent}>{line.line}</span>
</div>
))}
</div>
@@ -125,7 +126,7 @@ export const CommitDialog: React.FC<CommitDialogProps> = ({
{error && <div className="error-message">{error}</div>}
</div>
<div className="dialog-footer">
<div className={styles.dialogFooter}>
<button
className="btn-secondary"
onClick={onCancel}
@@ -0,0 +1,130 @@
.commitDialog {
background: var(--panel);
border-radius: 14px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.dialogHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.dialogHeader h3 {
margin: 0;
font-size: 1.1rem;
}
.dialogClose {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--muted);
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.dialogClose:hover {
background: var(--bg);
color: var(--ink);
}
.dialogBody {
padding: 1.5rem;
}
.fileInfo {
margin: 0 0 1rem;
color: var(--muted);
}
.diffPreview {
margin-bottom: 1.5rem;
}
.diffPreview h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--muted);
}
.diffContent {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: auto;
max-height: 300px;
font-family: 'Fira Code', 'Monaco', 'Courier New', monospace;
font-size: 13px;
}
.diffLine {
display: flex;
padding: 0.15rem 0.5rem;
gap: 0.5rem;
}
.diffLineNumber {
color: var(--muted);
min-width: 2rem;
text-align: right;
user-select: none;
}
.diffMarker {
width: 1rem;
text-align: center;
font-weight: bold;
}
.diffAdded {
background: rgba(16, 185, 129, 0.1);
}
.diffAdded .diffMarker {
color: #059669;
}
.diffRemoved {
background: rgba(239, 68, 68, 0.1);
}
.diffRemoved .diffMarker {
color: #dc2626;
}
.diffSame {
background: transparent;
}
.diffLineContent {
flex: 1;
}
.warningMessage {
padding: 0.75rem;
background: rgba(245, 158, 11, 0.1);
color: #d97706;
border-radius: 8px;
margin-bottom: 1rem;
}
.dialogFooter {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
}
@@ -0,0 +1,28 @@
.fileEditor {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.fileEditorToolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.fileActions {
display: flex;
gap: 0.5rem;
}
.fileEditorContent {
flex: 1;
overflow: hidden;
background: var(--bg);
display: flex;
flex-direction: column;
}
@@ -0,0 +1,170 @@
.gitToolbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 1.5rem;
background: var(--bg);
border-bottom: 1px solid var(--border);
min-height: 48px;
}
.toolbarRow {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.toolbarGroup {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbarButton {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.toolbarButton:hover:not(:disabled) {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.toolbarButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbarButtonPrimary {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
border: 1px solid var(--brand);
border-radius: 6px;
background: var(--brand);
color: white;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.branchSelect {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
cursor: pointer;
min-width: 140px;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 4px;
background: var(--brand);
color: white;
font-size: 0.7rem;
font-weight: 600;
border-radius: 999px;
}
.toolbarError {
color: #ef4444;
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
background: rgba(239, 68, 68, 0.1);
border-radius: 4px;
}
.toolbarInput {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
}
.newBranchForm {
padding: 0.75rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
margin-top: 0.5rem;
}
.statusSummary {
gap: 0.75rem;
}
.statusBadge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.statusBadgeModified {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
background: rgba(245, 158, 11, 0.1);
color: #d97706;
}
.statusBadgeAdded {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
background: rgba(16, 185, 129, 0.1);
color: #059669;
}
.statusBadgeDeleted {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.statusBadgeUntracked {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
background: rgba(107, 114, 128, 0.1);
color: #4b5563;
}
@@ -0,0 +1,45 @@
.mergeForm {
display: flex;
flex-direction: column;
gap: 1rem;
}
.mergeForm .formField {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.mergeForm label {
font-size: 0.875rem;
font-weight: 500;
}
.mergeForm select,
.mergeForm input,
.mergeForm textarea {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--ink);
font-family: inherit;
font-size: 0.875rem;
}
.mergeForm textarea {
resize: vertical;
}
.inputDisabled {
opacity: 0.6;
cursor: not-allowed;
}
.successText {
color: #10b981;
font-size: 0.875rem;
padding: 0.5rem;
background: rgba(16, 185, 129, 0.1);
border-radius: 4px;
}
@@ -0,0 +1,142 @@
.terminalWrapper {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
background: #1e1e1e;
}
.terminalHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
flex-shrink: 0;
gap: 0.5rem;
}
.terminalStatus {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.terminalStatus .statusDot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.terminalStatus .statusText {
font-size: 0.8rem;
color: #d4d4d4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.terminalActions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-shrink: 0;
}
.terminalClose {
padding: 0.25rem 0.6rem;
background: transparent;
border: 1px solid #666;
border-radius: 6px;
color: #d4d4d4;
cursor: pointer;
font-size: 0.8rem;
}
.terminalClose:hover {
background: #3e3e3e;
}
.terminalContainer {
flex: 1;
min-height: 0;
}
.terminalContainer :global(.xterm-viewport) {
background: #1e1e1e !important;
}
.terminalOverlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: grid;
place-content: center;
z-index: 10;
}
.terminalOverlayContent {
background: #2d2d2d;
border: 1px solid #3e3e3e;
border-radius: 10px;
padding: 1.5rem;
text-align: center;
max-width: 400px;
color: #d4d4d4;
}
.terminalOverlayContent h3 {
margin: 0 0 0.5rem;
color: #f14c4c;
}
.terminalOverlayContent p {
margin: 0 0 1rem;
font-size: 0.9rem;
}
.terminalOverlayActions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.terminalReconnectBanner {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: #3e3e3e;
color: #f5f543;
font-size: 0.8rem;
flex-shrink: 0;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 767px) {
.terminalOverlayContent {
margin: 0 1rem;
}
}
+5 -4
View File
@@ -1,3 +1,4 @@
import styles from "./features/git/FileEditor.module.css";
import React, { useCallback, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { apiClient } from "../api/client";
@@ -154,8 +155,8 @@ export const FileEditor: React.FC<FileEditorProps> = ({
if (error) return <p className="error-text">{error}</p>;
return (
<div className="file-editor">
<div className="file-editor-toolbar">
<div className={styles.fileEditor}>
<div className={styles.fileEditorToolbar}>
<div className="file-breadcrumbs">
{filePath.split("/").map((part, i, arr) => (
<span key={i}>
@@ -166,7 +167,7 @@ export const FileEditor: React.FC<FileEditorProps> = ({
</span>
))}
</div>
<div className="file-actions">
<div className={styles.fileActions}>
{mode === "view" && !isBinary && (
<button
className="btn-primary"
@@ -210,7 +211,7 @@ export const FileEditor: React.FC<FileEditorProps> = ({
</div>
</div>
<div className="file-editor-content">
<div className={styles.fileEditorContent}>
{mode === "view" && (
<SyntaxHighlighter
code={content}
+24 -23
View File
@@ -11,6 +11,7 @@ import {
} from "../api/git_repositories";
import { Icon } from "./icon";
import { MergeDialog } from "./merge-dialog";
import styles from "./features/git/GitToolbar.module.css";
interface GitToolbarProps {
projectId: string;
@@ -134,16 +135,16 @@ export const GitToolbar = ({
const canSync = hasRemote;
return (
<div className="git-toolbar">
{error && <div className="toolbar-error">{error}</div>}
<div className={styles.gitToolbar}>
{error && <div className={styles.toolbarError}>{error}</div>}
<div className="toolbar-row">
<div className="toolbar-group">
<div className={styles.toolbarRow}>
<div className={styles.toolbarGroup}>
<select
value={currentBranch}
onChange={(e) => handleCheckout(e.target.value)}
disabled={loading}
className="branch-select"
className={styles.branchSelect}
>
{branches.map((b) => (
<option key={b} value={b}>
@@ -158,7 +159,7 @@ export const GitToolbar = ({
))}
</select>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={() => setShowNewBranch(!showNewBranch)}
disabled={loading}
type="button"
@@ -167,9 +168,9 @@ export const GitToolbar = ({
</button>
</div>
<div className="toolbar-group">
<div className={styles.toolbarGroup}>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={handleFetch}
disabled={loading || !canSync}
type="button"
@@ -177,25 +178,25 @@ export const GitToolbar = ({
<Icon name="fetch" size="sm" /> Fetch
</button>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={handlePull}
disabled={loading || !canSync}
type="button"
>
<Icon name="pull" size="sm" /> Pull
{status?.behind ? <span className="badge">{status.behind}</span> : null}
{status?.behind ? <span className={styles.badge}>{status.behind}</span> : null}
</button>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={handlePush}
disabled={loading || !canSync || !status?.ahead}
type="button"
>
<Icon name="push" size="sm" /> Push
{status?.ahead ? <span className="badge">{status.ahead}</span> : null}
{status?.ahead ? <span className={styles.badge}>{status.ahead}</span> : null}
</button>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={() => setShowMergeDialog(true)}
disabled={loading}
type="button"
@@ -206,18 +207,18 @@ export const GitToolbar = ({
</div>
{showNewBranch && (
<div className="toolbar-row new-branch-form">
<div className={`${styles.toolbarRow} ${styles.newBranchForm}`}>
<input
type="text"
placeholder="Branch name"
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
className="toolbar-input"
className={styles.toolbarInput}
/>
<select
value={newBranchBase}
onChange={(e) => setNewBranchBase(e.target.value)}
className="toolbar-input"
className={styles.toolbarInput}
>
<option value="">Base: HEAD</option>
{branches.map((b) => (
@@ -225,7 +226,7 @@ export const GitToolbar = ({
))}
</select>
<button
className="toolbar-button primary"
className={styles.toolbarButtonPrimary}
onClick={handleCreateBranch}
disabled={loading || !newBranchName.trim()}
type="button"
@@ -233,7 +234,7 @@ export const GitToolbar = ({
<Icon name="add" size="sm" /> Create
</button>
<button
className="toolbar-button"
className={styles.toolbarButton}
onClick={() => setShowNewBranch(false)}
type="button"
>
@@ -243,11 +244,11 @@ export const GitToolbar = ({
)}
{hasChanges && status && (
<div className="toolbar-row status-summary">
{status.modified.length > 0 && <span className="status-badge modified"><Icon name="edit" size="sm" /> {status.modified.length} modified</span>}
{status.added.length > 0 && <span className="status-badge added"><Icon name="add" size="sm" /> {status.added.length} added</span>}
{status.deleted.length > 0 && <span className="status-badge deleted"><Icon name="delete" size="sm" /> {status.deleted.length} deleted</span>}
{status.untracked.length > 0 && <span className="status-badge untracked"><Icon name="warning" size="sm" /> {status.untracked.length} untracked</span>}
<div className={`${styles.toolbarRow} ${styles.statusSummary}`}>
{status.modified.length > 0 && <span className={styles.statusBadgeModified}><Icon name="edit" size="sm" /> {status.modified.length} modified</span>}
{status.added.length > 0 && <span className={styles.statusBadgeAdded}><Icon name="add" size="sm" /> {status.added.length} added</span>}
{status.deleted.length > 0 && <span className={styles.statusBadgeDeleted}><Icon name="delete" size="sm" /> {status.deleted.length} deleted</span>}
{status.untracked.length > 0 && <span className={styles.statusBadgeUntracked}><Icon name="warning" size="sm" /> {status.untracked.length} untracked</span>}
</div>
)}
+7 -6
View File
@@ -1,3 +1,4 @@
import styles from "./features/git/MergeDialog.module.css";
import { useState } from "react";
import { mergeBranches } from "../api/git_repositories";
@@ -65,8 +66,8 @@ export const MergeDialog = ({
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Merge Branch</h2>
<div className="merge-form">
<div className="form-field">
<div className={styles.mergeForm}>
<div className={styles.formField}>
<label>Source Branch (merge from)</label>
<select
value={sourceBranch}
@@ -82,17 +83,17 @@ export const MergeDialog = ({
</select>
</div>
<div className="form-field">
<div className={styles.formField}>
<label>Target Branch (merge into)</label>
<input
type="text"
value={currentBranch}
disabled
className="input-disabled"
className={styles.inputDisabled}
/>
</div>
<div className="form-field">
<div className={styles.formField}>
<label>Commit Message (optional)</label>
<textarea
value={commitMessage}
@@ -105,7 +106,7 @@ export const MergeDialog = ({
{error && <div className="error-text">{error}</div>}
{success && (
<div className="success-text">Merge successful!</div>
<div className={styles.successText}>Merge successful!</div>
)}
<div className="modal-actions">
+14 -13
View File
@@ -10,6 +10,7 @@ import type {
ServerControlMessage,
TerminalConnectionState,
} from "../types/terminal";
import styles from "./features/terminal/TerminalComponent.module.css";
interface TerminalProps {
instanceId: string;
@@ -231,11 +232,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
}, [state.status, sendResize]);
return (
<div className="terminal-wrapper">
<div className="terminal-header">
<div className="terminal-status">
<div className={styles.terminalWrapper}>
<div className={styles.terminalHeader}>
<div className={styles.terminalStatus}>
<span
className="status-dot"
className={styles.statusDot}
style={{
backgroundColor: STATUS_DOT_COLORS[state.status],
}}
@@ -246,9 +247,9 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
: getStatusText(state)
}
/>
<span className="status-text">{getStatusText(state)}</span>
<span className={styles.statusText}>{getStatusText(state)}</span>
</div>
<div className="terminal-actions">
<div className={styles.terminalActions}>
{state.status === "disconnected" && (
<button
className="secondary-button small"
@@ -259,7 +260,7 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
</button>
)}
{onClose && (
<button className="terminal-close" onClick={onClose} type="button">
<button className={styles.terminalClose} onClick={onClose} type="button">
Close
</button>
)}
@@ -267,11 +268,11 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
</div>
{sessionEnded && (
<div className="terminal-overlay">
<div className="terminal-overlay-content">
<div className={styles.terminalOverlay}>
<div className={styles.terminalOverlayContent}>
<h3>Session Ended</h3>
<p>{sessionEnded.message}</p>
<div className="terminal-overlay-actions">
<div className={styles.terminalOverlayActions}>
<button
className="primary-button small"
onClick={() => {
@@ -297,13 +298,13 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
)}
{state.status === "reconnecting" && (
<div className="terminal-reconnect-banner">
<span className="spinner" />
<div className={styles.terminalReconnectBanner}>
<span className={styles.spinner} />
{state.error}
</div>
)}
<div ref={terminalRef} className="terminal-container" />
<div ref={terminalRef} className={styles.terminalContainer} />
</div>
);
};
-580
View File
@@ -1514,429 +1514,6 @@ a {
border-radius: 999px;
}
/* Git Toolbar - Top Bar Styles */
.git-toolbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 1.5rem;
background: var(--bg);
border-bottom: 1px solid var(--border);
min-height: 48px;
}
.toolbar-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-button {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.toolbar-button:hover:not(:disabled) {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.toolbar-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbar-button.primary {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.branch-select {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
cursor: pointer;
min-width: 140px;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 4px;
background: var(--brand);
color: white;
font-size: 0.7rem;
font-weight: 600;
border-radius: 999px;
}
.toolbar-error {
color: #ef4444;
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
background: rgba(239, 68, 68, 0.1);
border-radius: 4px;
}
.toolbar-input {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
font-size: 0.85rem;
}
.new-branch-form {
padding: 0.75rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
margin-top: 0.5rem;
}
.status-summary {
gap: 0.75rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.status-badge.modified {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
}
.status-badge.added {
background: rgba(16, 185, 129, 0.1);
color: #059669;
}
.status-badge.deleted {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.status-badge.untracked {
background: rgba(107, 114, 128, 0.1);
color: #4b5563;
}
/* File Editor */
.file-editor {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.file-editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.file-actions {
display: flex;
gap: 0.5rem;
}
.file-editor-content {
flex: 1;
overflow: hidden;
background: var(--bg);
display: flex;
flex-direction: column;
}
/* Syntax Highlighter */
.syntax-highlighter {
display: flex;
flex-direction: column;
height: 100%;
}
.highlighter-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.language-badge {
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
background: var(--bg);
border-radius: 4px;
color: var(--muted);
}
.copy-button {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
color: var(--ink);
}
.copy-button:hover {
background: var(--brand);
color: white;
border-color: var(--brand);
}
.code-container {
display: flex;
flex: 1;
overflow: auto;
font-family: 'Fira Code', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.line-numbers {
display: flex;
flex-direction: column;
padding: 1rem 0.5rem;
background: var(--panel);
border-right: 1px solid var(--border);
color: var(--muted);
text-align: right;
user-select: none;
min-width: 3rem;
}
.line-number {
padding: 0 0.5rem;
}
.code-block {
flex: 1;
margin: 0;
padding: 1rem;
overflow: visible;
background: transparent;
}
.code-block code {
display: block;
background: transparent;
}
/* Code Editor */
.code-editor {
height: 100%;
overflow: auto;
}
.editor-textarea {
font-family: 'Fira Code', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
min-height: 100%;
}
.editor-textarea-input {
background: transparent;
color: var(--ink);
caret-color: var(--ink);
}
.editor-line {
display: flex;
}
.editor-line-number {
display: inline-block;
width: 3rem;
padding: 0 0.5rem;
text-align: right;
color: var(--muted);
user-select: none;
background: var(--panel);
border-right: 1px solid var(--border);
}
.editor-line-content {
flex: 1;
padding: 0 0.5rem;
white-space: pre;
}
/* Commit Dialog */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.commit-dialog {
background: var(--panel);
border-radius: 14px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: auto;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.dialog-header h3 {
margin: 0;
font-size: 1.1rem;
}
.dialog-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--muted);
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.dialog-close:hover {
background: var(--bg);
color: var(--ink);
}
.dialog-body {
padding: 1.5rem;
}
.file-info {
margin: 0 0 1rem;
color: var(--muted);
}
.diff-preview {
margin-bottom: 1.5rem;
}
.diff-preview h4 {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--muted);
}
.diff-content {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: auto;
max-height: 300px;
font-family: 'Fira Code', 'Monaco', 'Courier New', monospace;
font-size: 13px;
}
.diff-line {
display: flex;
padding: 0.15rem 0.5rem;
gap: 0.5rem;
}
.diff-line-number {
color: var(--muted);
min-width: 2rem;
text-align: right;
user-select: none;
}
.diff-marker {
width: 1rem;
text-align: center;
font-weight: bold;
}
.diff-added {
background: rgba(16, 185, 129, 0.1);
}
.diff-added .diff-marker {
color: #059669;
}
.diff-removed {
background: rgba(239, 68, 68, 0.1);
}
.diff-removed .diff-marker {
color: #dc2626;
}
.diff-same {
background: transparent;
}
.warning-message {
padding: 0.75rem;
background: rgba(245, 158, 11, 0.1);
color: #d97706;
border-radius: 8px;
margin-bottom: 1rem;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
@@ -2676,160 +2253,3 @@ a.nav-item,
}
/* ============================================
Responsive Terminal — Updated
============================================ */
.terminal-wrapper {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
background: #1e1e1e;
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
flex-shrink: 0;
gap: 0.5rem;
}
.terminal-status {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.terminal-status .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.terminal-status .status-text {
font-size: 0.8rem;
color: #d4d4d4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.terminal-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-shrink: 0;
}
.terminal-close {
padding: 0.25rem 0.6rem;
background: transparent;
border: 1px solid #666;
border-radius: 6px;
color: #d4d4d4;
cursor: pointer;
font-size: 0.8rem;
}
.terminal-close:hover {
background: #3e3e3e;
}
.terminal-container {
flex: 1;
min-height: 0;
}
.terminal-container .xterm-viewport {
background: #1e1e1e !important;
}
/* Terminal overlay for session ended */
.terminal-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: grid;
place-content: center;
z-index: 10;
}
.terminal-overlay-content {
background: #2d2d2d;
border: 1px solid #3e3e3e;
border-radius: 10px;
padding: 1.5rem;
text-align: center;
max-width: 400px;
color: #d4d4d4;
}
.terminal-overlay-content h3 {
margin: 0 0 0.5rem;
color: #f14c4c;
}
.terminal-overlay-content p {
margin: 0 0 1rem;
font-size: 0.9rem;
}
.terminal-overlay-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
/* Reconnect banner */
.terminal-reconnect-banner {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: #3e3e3e;
color: #f5f543;
font-size: 0.8rem;
flex-shrink: 0;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive terminal */
@media (max-width: 767px) {
.terminal-page {
padding: var(--space-2);
gap: var(--space-2);
}
.terminal-page-header h1 {
font-size: 1.25rem;
}
.terminal-overlay-content {
margin: 0 1rem;
}
}