refactor: extract CSS modules for session/settings and delete styles.css (Task 2.3)

- Create InstanceList.module.css, AppShell.module.css, SettingsTabLayout.module.css
- Create CommitPanel.module.css, FileViewer.module.css
- Create page CSS files: sessions, repo-workspace, dashboard, projects,
  git-history, ssh-keys, settings
- Update components to import and use CSS modules
- Delete monolithic styles.css (2,255 lines)
- Update main.tsx to import page CSS and new modules

Quality gates: tsc (pass), eslint (pass), build (pass)
Refs: repo-restructure Task 2.3
This commit is contained in:
Developer
2026-06-02 21:09:57 +00:00
parent c8c490eb2b
commit cccf4379d8
27 changed files with 2146 additions and 3388 deletions
+15 -14
View File
@@ -8,6 +8,7 @@ import { useAuth } from "../state/auth";
import { useSessions } from "../state/sessions";
import { Icon } from "./icon";
import type { IconName } from "../utils/icons";
import styles from "./layout/AppShell.module.css";
const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
{ to: "/", label: "Home", icon: "dashboard" },
@@ -28,12 +29,12 @@ const SessionItem = ({ session }: { session: Session }) => {
href={session.url ?? `/projects/${session.project_id}`}
target={session.url ? "_blank" : undefined}
rel={session.url ? "noopener noreferrer" : undefined}
className="nav-item session-item"
className={`${styles.navItem} ${styles.sessionItem}`}
title={`${displayName} (${session.status})`}
>
<span className={`session-status ${isRunning ? "running" : ""}`} />
<span className={`${styles.sessionStatus} ${isRunning ? styles.running : ""}`} />
<Icon name={session.tool_icon as IconName} size="sm" />
<span className="session-name">{displayName}</span>
<span className={styles.sessionName}>{displayName}</span>
</a>
);
};
@@ -62,12 +63,12 @@ export const AppShell = () => {
}, [loadSessions]);
return (
<div className="shell">
<header className="shell-header">
<Link className="brand" to="/">
<div className={styles.shell}>
<header className={styles.shellHeader}>
<Link className={styles.brand} to="/">
Headquarter
</Link>
<div className="header-actions">
<div className={styles.headerActions}>
<Link className="user-chip" to="/profile">
{user?.name ?? "User"}
</Link>
@@ -84,8 +85,8 @@ export const AppShell = () => {
</div>
</header>
<div className="shell-body">
<aside className="shell-nav" aria-label="Primary navigation">
<div className={styles.shellBody}>
<aside className={styles.shellNav} aria-label="Primary navigation">
{NAV_ITEMS.map((item) => {
const isHome = item.to === "/";
const activeCount = sessions.filter(
@@ -96,14 +97,14 @@ export const AppShell = () => {
key={item.to}
to={item.to}
className={({ isActive }) =>
isActive ? "nav-item nav-item-active" : "nav-item"
isActive ? `${styles.navItem} ${styles.navItemActive}` : styles.navItem
}
end={item.to === "/"}
>
<Icon name={item.icon} size="sm" />
{item.label}
{isHome && activeCount > 0 && (
<span className="nav-badge">{activeCount}</span>
<span className={styles.navBadge}>{activeCount}</span>
)}
</NavLink>
);
@@ -112,8 +113,8 @@ export const AppShell = () => {
{sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length >
0 && (
<>
<div className="nav-divider" />
<div className="nav-section-title">Live sessions</div>
<div className={styles.navDivider} />
<div className={styles.navSectionTitle}>Live sessions</div>
{sessions
.filter((s) => ACTIVE_STATUSES.includes(s.status))
.map((session) => (
@@ -123,7 +124,7 @@ export const AppShell = () => {
)}
</aside>
<main className="shell-content">
<main className={styles.shellContent}>
<Outlet />
</main>
</div>
+19 -18
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import { commitChanges } from "../api/git_repositories";
import styles from "./features/git/CommitPanel.module.css";
interface CommitPanelProps {
projectId: string;
@@ -49,49 +50,49 @@ export const CommitPanel = ({
if (!hasChanges) return null;
return (
<div className="commit-panel">
<div className={styles.commitPanel}>
<h4>Changes</h4>
<div className="file-list">
<div className={styles.fileList}>
{modified.map((file) => (
<div key={file} className="file-item modified">
<span className="file-status">M</span>
<span className="file-name">{file}</span>
<div key={file} className={`${styles.fileItem} modified`}>
<span className={styles.fileStatus}>M</span>
<span>{file}</span>
</div>
))}
{added.map((file) => (
<div key={file} className="file-item added">
<span className="file-status">A</span>
<span className="file-name">{file}</span>
<div key={file} className={`${styles.fileItem} added`}>
<span className={styles.fileStatus}>A</span>
<span>{file}</span>
</div>
))}
{deleted.map((file) => (
<div key={file} className="file-item deleted">
<span className="file-status">D</span>
<span className="file-name">{file}</span>
<div key={file} className={`${styles.fileItem} deleted`}>
<span className={styles.fileStatus}>D</span>
<span>{file}</span>
</div>
))}
{untracked.map((file) => (
<div key={file} className="file-item untracked">
<span className="file-status">?</span>
<span className="file-name">{file}</span>
<div key={file} className={`${styles.fileItem} untracked`}>
<span className={styles.fileStatus}>?</span>
<span>{file}</span>
</div>
))}
</div>
<div className="commit-form">
<div className={styles.commitForm}>
<textarea
placeholder="Commit message"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={2}
className="commit-message-input"
className={styles.commitMessageInput}
/>
{error && <div className="commit-error">{error}</div>}
{error && <div className={styles.commitError}>{error}</div>}
<button
onClick={handleCommit}
disabled={loading || !message.trim()}
className="commit-button"
className={styles.commitButton}
type="button"
>
{loading ? "Committing..." : "Commit"}
@@ -0,0 +1,76 @@
.commitPanel {
padding: 1rem;
border-top: 1px solid var(--border);
background: var(--panel);
}
.commitPanel h4 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.fileList {
max-height: 150px;
overflow: auto;
margin-bottom: 0.75rem;
}
.fileItem {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
font-size: 0.8125rem;
}
.fileStatus {
font-weight: bold;
font-size: 0.75rem;
width: 1rem;
text-align: center;
}
.fileItem.modified .fileStatus { color: #f59e0b; }
.fileItem.added .fileStatus { color: #10b981; }
.fileItem.deleted .fileStatus { color: #ef4444; }
.fileItem.untracked .fileStatus { color: #6b7280; }
.commitForm {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.commitMessageInput {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--ink);
font-family: inherit;
font-size: 0.875rem;
resize: vertical;
}
.commitButton {
padding: 0.5rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.commitButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.commitError {
color: #ef4444;
font-size: 0.8125rem;
}
@@ -0,0 +1,45 @@
.fileViewer {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.fileViewerHeader {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.fileBreadcrumbs {
font-size: 0.875rem;
font-family: monospace;
}
.breadcrumbSep {
color: var(--muted);
margin: 0 0.25rem;
}
.fileContent {
padding: 1rem;
overflow: auto;
max-height: calc(100vh - 200px);
}
.fileContent pre {
margin: 0;
font-family: "IBM Plex Mono", monospace;
font-size: 0.875rem;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.fileViewerEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 300px;
}
@@ -0,0 +1,73 @@
.instanceList {
margin-top: var(--space-4);
}
.instanceListHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-4);
}
.instanceListHeader h3 {
margin: 0;
}
.instanceGrid {
display: grid;
gap: var(--space-3);
}
.instanceCard {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
}
.instanceInfo {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.instanceName {
font-weight: 600;
}
.instanceMeta {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--muted);
}
.statusDot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.instanceActions {
display: flex;
gap: var(--space-2);
align-items: center;
}
.errorBadge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
color: var(--danger);
}
.inlineConfirm {
display: flex;
gap: var(--space-2);
align-items: center;
}
@@ -0,0 +1,89 @@
.settingsLayout {
display: flex;
gap: 2rem;
padding: 1.5rem 0;
}
.settingsSidebar {
width: 200px;
flex-shrink: 0;
}
.settingsNav {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.settingsNavLink {
padding: 0.625rem 1rem;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
font-size: 0.95rem;
transition: all 0.2s;
}
.settingsNavLink:hover {
background: var(--bg);
color: var(--ink);
}
.settingsNavLinkActive {
background: var(--brand);
color: white;
font-weight: 500;
}
.settingsContent {
flex: 1;
min-width: 0;
}
.settingsPanel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.5rem;
}
.settingsBreadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: var(--muted);
margin-bottom: 1rem;
}
.settingsBreadcrumb a {
color: var(--brand);
text-decoration: none;
}
.settingsBreadcrumb a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.settingsLayout {
flex-direction: column;
gap: 1rem;
}
.settingsSidebar {
width: 100%;
}
.settingsNav {
flex-direction: row;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.settingsNavLink {
white-space: nowrap;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
}
+12 -11
View File
@@ -13,6 +13,7 @@ import {
startInstance,
stopInstance,
} from "../api/sessions";
import styles from "./features/session/InstanceList.module.css";
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
@@ -186,8 +187,8 @@ export const InstanceList = ({
};
return (
<div className="instance-list">
<div className="instance-list-header">
<div className={styles.instanceList}>
<div className={styles.instanceListHeader}>
<h3>Tool Instances</h3>
<button
className="secondary-button small"
@@ -206,30 +207,30 @@ export const InstanceList = ({
) : instances.length === 0 ? (
<p className="muted">No instances yet. Launch a tool to get started.</p>
) : (
<div className="instance-grid">
<div className={styles.instanceGrid}>
{instances.map((instance) => (
<div key={instance.id} className="instance-card">
<div className="instance-info">
<div className="instance-name">
<div key={instance.id} className={styles.instanceCard}>
<div className={styles.instanceInfo}>
<div className={styles.instanceName}>
{instance.display_name ||
instance.tool_type_name ||
"Unnamed Instance"}
</div>
<div className="instance-meta">
<div className={styles.instanceMeta}>
<span
className="status-dot"
className={styles.statusDot}
style={{ backgroundColor: getStatusColor(instance.status) }}
/>
{instance.status}
{isTunnelUnhealthy(instance) && (
<span className="error-badge" title="Tunnel unreachable">
<span className={styles.errorBadge} title="Tunnel unreachable">
<Icon name="warning" size="sm" />
tunnel error
</span>
)}
</div>
</div>
<div className="instance-actions">
<div className={styles.instanceActions}>
{instance.status === "running" &&
instance.url &&
instance.tool_type_interfaces.includes("web") && (
@@ -286,7 +287,7 @@ export const InstanceList = ({
{instance.status === "running" && (
<>
{stopConfirmId === instance.id ? (
<div className="inline-confirm">
<div className={styles.inlineConfirm}>
<span>Stop?</span>
<button
className="ghost-button small danger-text"
@@ -0,0 +1,147 @@
.shell {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.shellHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.85rem 1.25rem;
border-bottom: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 88%, transparent);
backdrop-filter: blur(7px);
}
.brand {
font-weight: 700;
letter-spacing: 0.02em;
}
.headerActions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.shellBody {
display: grid;
grid-template-columns: 230px 1fr;
min-height: calc(100vh - 57px);
}
.shellNav {
border-right: 1px solid var(--border);
padding: 1rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
background: color-mix(in srgb, var(--panel) 65%, transparent);
}
.navItem {
padding: 0.65rem 0.75rem;
border-radius: 10px;
color: var(--muted);
}
.navItem:hover {
background: #ece7df;
color: var(--ink);
}
.navItemActive {
background: var(--brand);
color: #f7fff7;
}
.navSectionTitle {
margin-top: 0.5rem;
padding: 0.25rem 0.75rem;
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.navDivider {
height: 1px;
background: var(--border);
margin: 0.5rem 0;
}
.navBadge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: var(--primary);
color: var(--primary-fg);
border-radius: 9px;
font-size: 11px;
font-weight: 600;
margin-left: auto;
}
.sessionItem {
position: relative;
padding-left: var(--space-6);
}
.sessionStatus {
position: absolute;
left: var(--space-2);
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.sessionStatus.running {
background: var(--success);
}
.sessionName {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140px;
}
.shellContent {
padding: 1.25rem;
overflow-x: hidden;
}
@media (max-width: 767px) {
.shellBody {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.shellNav {
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
padding: 0.5rem;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.navItem {
padding: 0.5rem 0.75rem;
white-space: nowrap;
font-size: var(--font-size-sm);
}
.shellContent {
padding: var(--space-4);
}
}
@@ -1,5 +1,6 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import styles from "./features/settings/SettingsTabLayout.module.css";
interface Tab {
id: string;
@@ -21,15 +22,15 @@ export const SettingsTabLayout: React.FC<SettingsTabLayoutProps> = ({
const location = useLocation();
return (
<div className="settings-layout">
<aside className="settings-sidebar">
<nav className="settings-nav">
<div className={styles.settingsLayout}>
<aside className={styles.settingsSidebar}>
<nav className={styles.settingsNav}>
{tabs.map((tab) => (
<Link
key={tab.id}
to={`${basePath}/${tab.path}`}
className={`settings-nav-link ${
location.pathname.includes(tab.path) ? "active" : ""
className={`${styles.settingsNavLink} ${
location.pathname.includes(tab.path) ? styles.settingsNavLinkActive : ""
}`}
>
{tab.label}
@@ -37,7 +38,7 @@ export const SettingsTabLayout: React.FC<SettingsTabLayoutProps> = ({
))}
</nav>
</aside>
<main className="settings-content">{children}</main>
<main className={styles.settingsContent}>{children}</main>
</div>
);
};
+7
View File
@@ -9,6 +9,13 @@ import "./styles/tokens.css";
import "./styles/global.css";
import "./styles/utilities.css";
import "./styles/syntax-highlight.css";
import "./styles/pages/sessions.css";
import "./styles/pages/repo-workspace.css";
import "./styles/pages/dashboard.css";
import "./styles/pages/projects.css";
import "./styles/pages/git-history.css";
import "./styles/pages/ssh-keys.css";
import "./styles/pages/settings.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
.home-page {
max-width: 1240px;
}
.home-hero {
display: flex;
justify-content: space-between;
gap: var(--space-4);
align-items: flex-start;
}
.home-hero-actions {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.home-summary-grid,
.home-project-grid,
.home-session-grid {
display: grid;
gap: var(--space-4);
}
.home-summary-grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.home-project-grid,
.home-session-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.home-section h2,
.settings-header h1,
.settings-panel h2 {
margin: 0;
}
.home-section h3,
.home-section p {
margin: 0;
}
.eyebrow {
margin: 0;
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
+255
View File
@@ -0,0 +1,255 @@
.history-actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
.branch-selector {
padding: 0.45rem 0.7rem;
border: 1px solid var(--border);
border-radius: 10px;
font: inherit;
background: var(--panel);
color: var(--ink);
}
.history-container {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
min-height: 60vh;
}
.commit-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
max-height: 70vh;
}
.commit-list.with-detail {
grid-column: 1;
}
.commit-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
cursor: pointer;
transition: background-color 0.15s ease;
}
.commit-item:hover {
background: #ece7df;
}
.commit-item.selected {
border-color: var(--brand);
background: #f0f7f4;
}
.commit-graph {
font-family: monospace;
font-size: 0.9rem;
color: var(--brand);
white-space: pre;
flex-shrink: 0;
min-width: 60px;
}
.graph-line {
display: inline-block;
}
.commit-content {
flex: 1;
min-width: 0;
}
.commit-header {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.35rem;
}
.commit-hash {
font-family: monospace;
font-size: 0.85rem;
color: var(--brand);
background: #f0f7f4;
padding: 0.15rem 0.4rem;
border-radius: 6px;
}
.commit-refs {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.ref-tag {
font-size: 0.75rem;
padding: 0.15rem 0.4rem;
background: var(--brand);
color: white;
border-radius: 999px;
}
.commit-message {
margin: 0 0 0.35rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.commit-meta {
display: flex;
gap: 0.75rem;
font-size: 0.85rem;
color: var(--muted);
}
.commit-detail-panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.25rem;
overflow-y: auto;
max-height: 70vh;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
.detail-header h3 {
margin: 0;
}
.detail-content {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.detail-section h4 {
margin: 0 0 0.5rem;
color: var(--muted);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.commit-hash-full {
font-family: monospace;
font-size: 0.85rem;
color: var(--brand);
margin: 0;
}
.commit-message-full {
margin: 0.5rem 0 0;
line-height: 1.5;
white-space: pre-wrap;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
background: #f5f3ee;
border-radius: 10px;
}
.stat.additions {
background: #f0fdf4;
}
.stat.deletions {
background: #fef2f2;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--ink);
}
.stat.additions .stat-value {
color: #16a34a;
}
.stat.deletions .stat-value {
color: #dc2626;
}
.stat-label {
font-size: 0.8rem;
color: var(--muted);
}
.parent-list {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.parent-hash {
font-family: monospace;
font-size: 0.85rem;
padding: 0.2rem 0.5rem;
background: #f5f3ee;
border-radius: 6px;
}
.diff-content {
font-family: monospace;
font-size: 0.8rem;
line-height: 1.5;
background: #f5f3ee;
padding: 0.75rem;
border-radius: 10px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
@media (min-width: 1024px) {
.history-container {
grid-template-columns: 1fr 400px;
}
.commit-list.with-detail {
grid-column: 1;
}
.commit-detail-panel {
grid-column: 2;
position: sticky;
top: 1rem;
}
}
+33
View File
@@ -0,0 +1,33 @@
.project-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.project-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.project-info h3 {
margin: 0 0 0.35rem;
}
.project-info p {
margin: 0;
}
.project-actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-shrink: 0;
}
.delete-confirm {
display: flex;
gap: 0.5rem;
align-items: center;
}
@@ -0,0 +1,142 @@
.repo-workspace {
display: flex;
flex-direction: column;
height: calc(100vh - 60px);
overflow: hidden;
}
.workspace-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.workspace-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.workspace-header-icon {
font-size: 1.5rem;
}
.workspace-header-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.workspace-header-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.workspace-header-subtitle {
color: var(--muted);
font-size: 0.875rem;
}
.workspace-header-actions {
display: flex;
gap: 0.5rem;
}
.workspace-header-action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
color: var(--ink);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.workspace-header-action-btn:hover {
background: var(--bg);
border-color: var(--brand);
}
.workspace-title {
display: flex;
align-items: center;
gap: 1rem;
}
.workspace-title h1 {
margin: 0;
font-size: 1.25rem;
}
.repo-name {
color: var(--muted);
font-size: 0.875rem;
}
.workspace-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.workspace-sidebar {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--border);
background: var(--panel);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-section {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.sidebar-section label {
margin: 0;
}
.workspace-main {
flex: 1;
overflow: hidden;
padding: 1rem;
background: var(--bg);
display: flex;
flex-direction: column;
}
@media (max-width: 767px) {
.workspace-layout {
flex-direction: column;
}
.workspace-sidebar {
width: 100%;
min-width: auto;
max-height: 40vh;
border-right: none;
border-bottom: 1px solid var(--border);
}
.workspace-header {
flex-direction: column;
gap: var(--space-3);
align-items: flex-start;
padding: var(--space-4);
}
.workspace-header-actions {
width: 100%;
flex-wrap: wrap;
}
}
+176
View File
@@ -0,0 +1,176 @@
.sessions-page {
max-width: 1200px;
}
.last-session-section {
margin-bottom: var(--space-6);
}
.last-session-card {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-5);
border: 2px solid var(--primary);
}
.last-session-info h3 {
margin: 0 0 var(--space-1) 0;
font-size: 1.25rem;
}
.active-sessions-section {
margin-bottom: var(--space-6);
}
.active-sessions-section h2 {
display: flex;
align-items: center;
gap: var(--space-2);
}
.active-sessions-section .badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 6px;
background: var(--success);
color: white;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-4);
}
.session-card {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
}
.session-info h4 {
margin: 0 0 var(--space-1) 0;
font-size: 1rem;
}
.session-url {
margin: var(--space-1) 0;
font-size: 0.8rem;
word-break: break-all;
}
.session-url a {
color: var(--color-primary);
text-decoration: none;
}
.session-url a:hover {
text-decoration: underline;
}
.session-actions {
display: flex;
gap: var(--space-2);
}
.recent-sessions-section {
margin-bottom: var(--space-6);
}
.recent-sessions-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.recent-session-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
}
.recent-session-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.recent-session-name {
font-weight: 500;
}
.recent-session-actions {
display: flex;
gap: var(--space-2);
align-items: center;
}
.delete-confirm-inline,
.stop-confirm-inline {
display: flex;
gap: var(--space-2);
align-items: center;
}
.confirm-text {
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.create-session-section {
margin-bottom: var(--space-6);
}
.create-session-form {
max-width: 600px;
}
.create-session-form .form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-4);
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
text-transform: capitalize;
}
.status-badge.running {
background: var(--success-light, #dcfce7);
color: var(--success, #16a34a);
}
.status-badge.stopped {
background: var(--muted-bg, #f3f4f6);
color: var(--muted, #6b7280);
}
.status-badge.pending {
background: var(--warning-light, #fef3c7);
color: var(--warning, #d97706);
}
.status-badge.error {
background: var(--danger-light, #fee2e2);
color: var(--danger, #dc2626);
}
+40
View File
@@ -0,0 +1,40 @@
.settings-page {
max-width: 1240px;
}
.settings-header h1,
.settings-panel h2 {
margin: 0;
}
.settings-tabs {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.settings-tab {
padding: 0.6rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--muted);
background: var(--panel);
}
.settings-tab.active {
background: var(--brand);
color: white;
border-color: transparent;
}
.settings-actions,
.form-actions {
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
}
.project-settings-page {
padding: 1.5rem;
}
+66
View File
@@ -0,0 +1,66 @@
.keys-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.key-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1rem;
}
.key-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.key-header h3 {
margin: 0;
}
.key-meta {
margin-bottom: 0.75rem;
}
.key-public {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
background: #f5f3ee;
border-radius: 8px;
}
.key-public code {
font-size: 0.85rem;
word-break: break-all;
flex: 1;
}
.ssh-key-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.ssh-key-item {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-4);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
}
@media (min-width: 768px) {
.ssh-key-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}