diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..e4fba218
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b7cd71ca..05b8fa9e 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -18,8 +18,9 @@
font-family: 'JetBrains Mono', 'Space Grotesk', 'Noto Sans SC', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- color: #000000;
- background-color: #ffffff;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ transition: background-color 0.2s, color 0.2s;
}
/* 滚动条样式 */
diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css
new file mode 100644
index 00000000..26803592
--- /dev/null
+++ b/frontend/src/assets/theme.css
@@ -0,0 +1,59 @@
+/* ===== Light mode (default) ===== */
+:root {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f5f5;
+ --bg-surface: #fafafa;
+ --bg-input: #fafafa;
+ --bg-hover: #f0f0f0;
+
+ --text-primary: #000000;
+ --text-secondary: #333333;
+ --text-muted: #666666;
+ --text-faint: #999999;
+
+ --border: #e0e0e0;
+ --border-light: #eeeeee;
+ --border-medium: #cccccc;
+
+ --accent: #ff6b35;
+ --status-success: #1a936f;
+ --status-error: #c5283d;
+}
+
+/* ===== Dark mode ===== */
+html.dark {
+ --bg-primary: #111111;
+ --bg-secondary: #1a1a1a;
+ --bg-surface: #1e1e1e;
+ --bg-input: #1a1a1a;
+ --bg-hover: #2a2a2a;
+
+ --text-primary: #e5e5e5;
+ --text-secondary: #cccccc;
+ --text-muted: #a0a0a0;
+ --text-faint: #555555;
+
+ --border: #2e2e2e;
+ --border-light: #222222;
+ --border-medium: #3a3a3a;
+
+ --accent: #ff6b35;
+ --status-success: #1a936f;
+ --status-error: #c5283d;
+}
+
+/* ===== Global dark mode overrides ===== */
+html.dark body {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+html.dark ::-webkit-scrollbar-track {
+ background: #1a1a1a;
+}
+html.dark ::-webkit-scrollbar-thumb {
+ background: #444;
+}
+html.dark ::-webkit-scrollbar-thumb:hover {
+ background: #666;
+}
diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue
index db188298..e211346d 100644
--- a/frontend/src/components/GraphPanel.vue
+++ b/frontend/src/components/GraphPanel.vue
@@ -818,10 +818,11 @@ onUnmounted(() => {
position: relative;
width: 100%;
height: 100%;
- background-color: #FAFAFA;
- background-image: radial-gradient(#D0D0D0 1.5px, transparent 1.5px);
+ background-color: var(--bg-surface);
+ background-image: radial-gradient(var(--border) 1.5px, transparent 1.5px);
background-size: 24px 24px;
overflow: hidden;
+ transition: background-color 0.2s;
}
.panel-header {
@@ -834,14 +835,14 @@ onUnmounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
- background: linear-gradient(to bottom, rgba(255,255,255,0.95), rgba(255,255,255,0));
+ background: linear-gradient(to bottom, var(--bg-surface) 60%, transparent);
pointer-events: none;
}
.panel-title {
font-size: 14px;
font-weight: 600;
- color: #333;
+ color: var(--text-secondary);
pointer-events: auto;
}
@@ -855,24 +856,24 @@ onUnmounted(() => {
.tool-btn {
height: 32px;
padding: 0 12px;
- border: 1px solid #E0E0E0;
- background: #FFF;
+ border: 1px solid var(--border);
+ background: var(--bg-primary);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
- color: #666;
+ color: var(--text-muted);
transition: all 0.2s;
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.04);
font-size: 13px;
}
.tool-btn:hover {
- background: #F5F5F5;
- color: #000;
- border-color: #CCC;
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ border-color: var(--border-medium);
}
.tool-btn .btn-text {
@@ -902,7 +903,7 @@ onUnmounted(() => {
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
- color: #999;
+ color: var(--text-faint);
}
.empty-icon {
@@ -916,11 +917,11 @@ onUnmounted(() => {
position: absolute;
bottom: 24px;
left: 24px;
- background: rgba(255,255,255,0.95);
+ background: var(--bg-primary);
padding: 12px 16px;
border-radius: 8px;
- border: 1px solid #EAEAEA;
- box-shadow: 0 4px 16px rgba(0,0,0,0.06);
+ border: 1px solid var(--border);
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1);
z-index: 10;
}
@@ -928,7 +929,7 @@ onUnmounted(() => {
display: block;
font-size: 11px;
font-weight: 600;
- color: #E91E63;
+ color: #e91e63;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -946,7 +947,7 @@ onUnmounted(() => {
align-items: center;
gap: 6px;
font-size: 12px;
- color: #555;
+ color: var(--text-muted);
}
.legend-dot {
@@ -968,11 +969,11 @@ onUnmounted(() => {
display: flex;
align-items: center;
gap: 10px;
- background: #FFF;
+ background: var(--bg-primary);
padding: 8px 14px;
border-radius: 20px;
- border: 1px solid #E0E0E0;
- box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+ border: 1px solid var(--border);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
z-index: 10;
}
@@ -996,7 +997,7 @@ onUnmounted(() => {
left: 0;
right: 0;
bottom: 0;
- background-color: #E0E0E0;
+ background-color: var(--border-medium);
border-radius: 22px;
transition: 0.3s;
}
@@ -1008,7 +1009,7 @@ onUnmounted(() => {
width: 16px;
left: 3px;
bottom: 3px;
- background-color: white;
+ background-color: var(--bg-primary);
border-radius: 50%;
transition: 0.3s;
}
@@ -1023,7 +1024,7 @@ input:checked + .slider:before {
.toggle-label {
font-size: 12px;
- color: #666;
+ color: var(--text-muted);
}
/* Detail Panel - Right Side */
@@ -1033,10 +1034,10 @@ input:checked + .slider:before {
right: 20px;
width: 320px;
max-height: calc(100% - 100px);
- background: #FFF;
- border: 1px solid #EAEAEA;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
border-radius: 10px;
- box-shadow: 0 8px 32px rgba(0,0,0,0.1);
+ box-shadow: 0 8px 32px rgba(0,0,0,0.15);
overflow: hidden;
font-family: 'Noto Sans SC', system-ui, sans-serif;
font-size: 13px;
@@ -1050,14 +1051,14 @@ input:checked + .slider:before {
justify-content: space-between;
align-items: center;
padding: 14px 16px;
- background: #FAFAFA;
- border-bottom: 1px solid #EEE;
+ background: var(--bg-surface);
+ border-bottom: 1px solid var(--border-light);
flex-shrink: 0;
}
.detail-title {
font-weight: 600;
- color: #333;
+ color: var(--text-secondary);
font-size: 14px;
}
@@ -1075,14 +1076,14 @@ input:checked + .slider:before {
border: none;
font-size: 20px;
cursor: pointer;
- color: #999;
+ color: var(--text-faint);
line-height: 1;
padding: 0;
transition: color 0.2s;
}
.detail-close:hover {
- color: #333;
+ color: var(--text-primary);
}
.detail-content {
@@ -1099,14 +1100,14 @@ input:checked + .slider:before {
}
.detail-label {
- color: #888;
+ color: var(--text-faint);
font-size: 12px;
font-weight: 500;
min-width: 80px;
}
.detail-value {
- color: #333;
+ color: var(--text-secondary);
flex: 1;
word-break: break-word;
}
@@ -1114,24 +1115,24 @@ input:checked + .slider:before {
.detail-value.uuid-text {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
- color: #666;
+ color: var(--text-muted);
}
.detail-value.fact-text {
line-height: 1.5;
- color: #444;
+ color: var(--text-muted);
}
.detail-section {
margin-top: 16px;
padding-top: 14px;
- border-top: 1px solid #F0F0F0;
+ border-top: 1px solid var(--border-light);
}
.section-title {
font-size: 12px;
font-weight: 600;
- color: #666;
+ color: var(--text-muted);
margin-bottom: 10px;
}
@@ -1147,19 +1148,19 @@ input:checked + .slider:before {
}
.property-key {
- color: #888;
+ color: var(--text-faint);
font-weight: 500;
min-width: 90px;
}
.property-value {
- color: #333;
+ color: var(--text-secondary);
flex: 1;
}
.summary-text {
line-height: 1.6;
- color: #444;
+ color: var(--text-muted);
font-size: 12px;
}
@@ -1172,11 +1173,11 @@ input:checked + .slider:before {
.label-tag {
display: inline-block;
padding: 4px 12px;
- background: #F5F5F5;
- border: 1px solid #E0E0E0;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
border-radius: 16px;
font-size: 11px;
- color: #555;
+ color: var(--text-muted);
}
.episodes-list {
@@ -1188,24 +1189,24 @@ input:checked + .slider:before {
.episode-tag {
display: inline-block;
padding: 6px 10px;
- background: #F8F8F8;
- border: 1px solid #E8E8E8;
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
- color: #666;
+ color: var(--text-muted);
word-break: break-all;
}
/* Edge relation header */
.edge-relation-header {
- background: #F8F8F8;
+ background: var(--bg-surface);
padding: 12px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 13px;
font-weight: 500;
- color: #333;
+ color: var(--text-secondary);
line-height: 1.5;
word-break: break-word;
}
@@ -1317,8 +1318,8 @@ input:checked + .slider:before {
.self-loop-count {
margin-left: auto;
font-size: 11px;
- color: #666;
- background: rgba(255,255,255,0.8);
+ color: var(--text-muted);
+ background: var(--bg-surface);
padding: 2px 8px;
border-radius: 10px;
}
@@ -1330,8 +1331,8 @@ input:checked + .slider:before {
}
.self-loop-item {
- background: #FAFAFA;
- border: 1px solid #EAEAEA;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
border-radius: 8px;
}
@@ -1340,24 +1341,24 @@ input:checked + .slider:before {
align-items: center;
gap: 8px;
padding: 10px 12px;
- background: #F5F5F5;
+ background: var(--bg-hover);
cursor: pointer;
transition: background 0.2s;
}
.self-loop-item-header:hover {
- background: #EEEEEE;
+ background: var(--border);
}
.self-loop-item.expanded .self-loop-item-header {
- background: #E8E8E8;
+ background: var(--border);
}
.self-loop-index {
font-size: 10px;
font-weight: 600;
- color: #888;
- background: #E0E0E0;
+ color: var(--text-faint);
+ background: var(--border-medium);
padding: 2px 6px;
border-radius: 4px;
}
@@ -1365,7 +1366,7 @@ input:checked + .slider:before {
.self-loop-name {
font-size: 12px;
font-weight: 500;
- color: #333;
+ color: var(--text-secondary);
flex: 1;
}
@@ -1377,20 +1378,20 @@ input:checked + .slider:before {
justify-content: center;
font-size: 14px;
font-weight: 600;
- color: #888;
- background: #E0E0E0;
+ color: var(--text-faint);
+ background: var(--border-medium);
border-radius: 4px;
transition: all 0.2s;
}
.self-loop-item.expanded .self-loop-toggle {
- background: #D0D0D0;
- color: #666;
+ background: var(--border);
+ color: var(--text-muted);
}
.self-loop-item-content {
padding: 12px;
- border-top: 1px solid #EAEAEA;
+ border-top: 1px solid var(--border);
}
.self-loop-item-content .detail-row {
diff --git a/frontend/src/components/LanguageSwitcher.vue b/frontend/src/components/LanguageSwitcher.vue
index 723d64b7..7c2bd079 100644
--- a/frontend/src/components/LanguageSwitcher.vue
+++ b/frontend/src/components/LanguageSwitcher.vue
@@ -66,11 +66,12 @@ onUnmounted(() => {
font-family: 'JetBrains Mono', monospace;
}
-/* Light theme (default - for white header backgrounds) */
+/* Inherits color from parent so it works on both dark navbars and light headers */
.switcher-trigger {
background: transparent;
- color: #333;
- border: 1px solid #CCC;
+ color: inherit;
+ border: 1px solid currentColor;
+ opacity: 0.75;
padding: 4px 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
@@ -78,11 +79,11 @@ onUnmounted(() => {
display: flex;
align-items: center;
gap: 6px;
- transition: border-color 0.2s, opacity 0.2s;
+ transition: opacity 0.2s;
}
.switcher-trigger:hover {
- border-color: #999;
+ opacity: 1;
}
.caret {
@@ -94,30 +95,30 @@ onUnmounted(() => {
top: 100%;
right: 0;
margin-top: 4px;
- background: #FFFFFF;
- border: 1px solid #DDD;
+ background: var(--bg-surface, #fff);
+ border: 1px solid var(--border, #ddd);
list-style: none;
padding: 4px 0;
min-width: 100%;
z-index: 1000;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.switcher-option {
padding: 6px 12px;
font-size: 0.8rem;
- color: #333;
+ color: var(--text-secondary, #333);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.switcher-option:hover {
- background: #F0F0F0;
+ background: var(--bg-hover, #f0f0f0);
}
.switcher-option.active {
- color: var(--orange, #FF4500);
+ color: var(--accent, #ff6b35);
}
diff --git a/frontend/src/components/Step1GraphBuild.vue b/frontend/src/components/Step1GraphBuild.vue
index 687d1c7b..40933c41 100644
--- a/frontend/src/components/Step1GraphBuild.vue
+++ b/frontend/src/components/Step1GraphBuild.vue
@@ -275,7 +275,7 @@ watch(() => props.systemLogs.length, () => {
diff --git a/frontend/src/main.js b/frontend/src/main.js
index cc3d101e..5557c77e 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,5 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
+import './assets/theme.css'
import router from './router'
import i18n from './i18n'
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index ca7ef6ff..4aa2744a 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -4,6 +4,7 @@