diff --git a/master-frontend/src/App.vue b/master-frontend/src/App.vue
index 17ff68e..8ff879a 100644
--- a/master-frontend/src/App.vue
+++ b/master-frontend/src/App.vue
@@ -164,8 +164,8 @@ html, body, #app {
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- background: #11111b;
- color: #cdd6f4;
+ background: var(--c-crust);
+ color: var(--c-text);
}
.app-layout {
@@ -186,34 +186,34 @@ body {
.toolbar-area {
grid-area: toolbar;
- background: #1e1e2e;
- border-bottom: 1px solid #313244;
+ background: var(--c-base);
+ border-bottom: 1px solid var(--c-surface0);
}
.tree-area {
grid-area: tree;
- background: #181825;
- border-right: 1px solid #313244;
+ background: var(--c-mantle);
+ border-right: 1px solid var(--c-surface0);
overflow-y: auto;
}
.content-area {
grid-area: content;
- background: #11111b;
+ background: var(--c-crust);
overflow: hidden;
}
.panel-area {
grid-area: panel;
- background: #181825;
- border-left: 1px solid #313244;
+ background: var(--c-mantle);
+ border-left: 1px solid var(--c-surface0);
overflow-y: auto;
}
.log-area {
grid-area: log;
- background: #1e1e2e;
- border-top: 1px solid #313244;
+ background: var(--c-base);
+ border-top: 1px solid var(--c-surface0);
overflow: hidden;
}
diff --git a/master-frontend/src/components/CommunicationSettings.vue b/master-frontend/src/components/CommunicationSettings.vue
index 7f4d73c..ae1a46c 100644
--- a/master-frontend/src/components/CommunicationSettings.vue
+++ b/master-frontend/src/components/CommunicationSettings.vue
@@ -54,12 +54,12 @@ const { t } = useI18n()
diff --git a/master-frontend/src/components/ConnectionTree.vue b/master-frontend/src/components/ConnectionTree.vue
index 559667b..f3a9ee2 100644
--- a/master-frontend/src/components/ConnectionTree.vue
+++ b/master-frontend/src/components/ConnectionTree.vue
@@ -195,13 +195,13 @@ function fcLabel(fn: string): string {
padding: 8px 12px;
font-size: 11px;
text-transform: uppercase;
- color: #6c7086;
+ color: var(--c-subtext0);
letter-spacing: 0.5px;
}
.tree-empty {
padding: 24px 12px;
- color: #6c7086;
+ color: var(--c-subtext0);
text-align: center;
}
@@ -216,18 +216,18 @@ function fcLabel(fn: string): string {
}
.tree-node:hover {
- background: #1e1e2e;
+ background: var(--c-hover-bg);
}
.tree-node.selected {
- background: #89b4fa;
- color: #1e1e2e;
+ background: var(--c-selection-bg);
+ color: var(--c-selection-text);
}
.tree-node.selected .node-meta,
.tree-node.selected .node-slave {
- color: #1e1e2e;
- opacity: 0.7;
+ color: var(--c-selection-text);
+ opacity: 1;
}
.tree-child {
@@ -238,7 +238,7 @@ function fcLabel(fn: string): string {
font-size: 8px;
width: 12px;
text-align: center;
- color: #6c7086;
+ color: var(--c-subtext0);
}
.node-status {
@@ -248,11 +248,11 @@ function fcLabel(fn: string): string {
flex-shrink: 0;
}
-.node-status.connected { background: #a6e3a1; }
-.node-status.disconnected { background: #f38ba8; }
-.node-status.error { background: #fab387; }
+.node-status.connected { background: var(--c-green); }
+.node-status.disconnected { background: var(--c-red); }
+.node-status.error { background: var(--c-peach); }
.node-status.reconnecting {
- background: #f9e2af;
+ background: var(--c-yellow);
animation: reconnecting-pulse 1s ease-in-out infinite;
}
@@ -266,12 +266,12 @@ function fcLabel(fn: string): string {
height: 6px;
border-radius: 50%;
flex-shrink: 0;
- background: #45475a;
+ background: var(--c-surface1);
}
.poll-indicator.active {
- background: #a6e3a1;
- box-shadow: 0 0 4px #a6e3a1;
+ background: var(--c-green);
+ box-shadow: 0 0 4px var(--c-green);
}
.node-label {
@@ -283,20 +283,20 @@ function fcLabel(fn: string): string {
.node-slave {
font-size: 10px;
- color: #6c7086;
+ color: var(--c-subtext0);
}
.node-meta {
font-size: 10px;
- color: #6c7086;
+ color: var(--c-subtext0);
font-family: monospace;
}
/* Context Menu */
.context-menu {
position: fixed;
- background: #1e1e2e;
- border: 1px solid #45475a;
+ background: var(--c-base);
+ border: 1px solid var(--c-surface1);
border-radius: 6px;
padding: 4px 0;
z-index: 999;
@@ -311,10 +311,10 @@ function fcLabel(fn: string): string {
}
.ctx-item:hover {
- background: #313244;
+ background: var(--c-control-bg);
}
.ctx-item.danger {
- color: #f38ba8;
+ color: var(--c-red);
}
diff --git a/master-frontend/src/components/DataTable.vue b/master-frontend/src/components/DataTable.vue
index f59a669..c5287f1 100644
--- a/master-frontend/src/components/DataTable.vue
+++ b/master-frontend/src/components/DataTable.vue
@@ -418,26 +418,26 @@ const valueColumnLabel = computed(() => {
align-items: center;
justify-content: center;
height: 100%;
- color: #6c7086;
+ color: var(--c-subtext0);
font-size: 13px;
text-align: center;
}
.empty-hint {
font-size: 11px;
- color: #45475a;
+ color: var(--c-subtext0);
margin-top: 6px;
}
.error-text {
- color: #f38ba8;
+ color: var(--c-red);
font-size: 12px;
}
.error-badge {
font-size: 10px;
- background: #f38ba8;
- color: #1e1e2e;
+ background: var(--c-red);
+ color: var(--c-on-accent);
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
@@ -450,9 +450,9 @@ const valueColumnLabel = computed(() => {
align-items: center;
gap: 8px;
padding: 6px 10px;
- border-bottom: 1px solid #313244;
+ border-bottom: 1px solid var(--c-surface0);
flex-shrink: 0;
- background: #1e1e2e;
+ background: var(--c-base);
}
.header-title {
@@ -462,24 +462,24 @@ const valueColumnLabel = computed(() => {
text-overflow: ellipsis;
font-size: 12px;
font-weight: 600;
- color: #89b4fa;
+ color: var(--c-blue);
white-space: nowrap;
}
.format-select {
flex: none;
padding: 2px 6px;
- background: #313244;
- border: 1px solid #45475a;
+ background: var(--c-control-bg);
+ border: 1px solid var(--c-surface1);
border-radius: 4px;
- color: #cdd6f4;
+ color: var(--c-text);
font-size: 11px;
cursor: pointer;
}
.format-select:focus {
outline: none;
- border-color: #89b4fa;
+ border-color: var(--c-blue);
}
.search-input {
@@ -487,37 +487,37 @@ const valueColumnLabel = computed(() => {
min-width: min(140px, 100%);
max-width: 160px;
padding: 3px 8px;
- background: #313244;
- border: 1px solid #45475a;
+ background: var(--c-control-bg);
+ border: 1px solid var(--c-surface1);
border-radius: 4px;
- color: #cdd6f4;
+ color: var(--c-text);
font-size: 12px;
margin-left: auto;
}
.search-input:focus {
outline: none;
- border-color: #89b4fa;
+ border-color: var(--c-blue);
}
.mode-btn {
padding: 2px 8px;
- background: #313244;
- border: 1px solid #45475a;
+ background: var(--c-control-bg);
+ border: 1px solid var(--c-surface1);
border-radius: 4px;
- color: #cdd6f4;
+ color: var(--c-text);
cursor: pointer;
font-size: 11px;
font-family: monospace;
}
.mode-btn:hover {
- background: #45475a;
+ background: var(--c-surface1);
}
.register-count {
font-size: 11px;
- color: #6c7086;
+ color: var(--c-subtext0);
white-space: nowrap;
}
@@ -543,12 +543,12 @@ const valueColumnLabel = computed(() => {
}
.table th {
- background: #1e1e2e;
- color: #6c7086;
+ background: var(--c-mantle);
+ color: var(--c-subtext0);
font-weight: 500;
padding: 6px 10px;
text-align: left;
- border-bottom: 1px solid #313244;
+ border-bottom: 1px solid var(--c-surface0);
z-index: 1;
}
@@ -557,37 +557,37 @@ const valueColumnLabel = computed(() => {
}
.table tbody tr:hover {
- background: #1e1e2e;
+ background: var(--c-hover-bg);
}
.table tbody tr.selected {
- background: #89b4fa;
- color: #1e1e2e;
+ background: var(--c-selection-bg);
+ color: var(--c-selection-text);
}
.table td {
padding: 4px 10px;
- border-bottom: 1px solid #1e1e2e;
+ border-bottom: 1px solid var(--c-row-border);
}
.col-addr {
- font-family: 'SF Mono', 'Fira Code', monospace;
+ font-family: var(--font-mono);
width: 90px;
- color: #89b4fa;
+ color: var(--c-blue);
}
.table tbody tr.selected .col-addr {
- color: #1e1e2e;
+ color: var(--c-selection-text);
}
.col-raw {
font-family: monospace;
width: 120px;
- color: #6c7086;
+ color: var(--c-subtext0);
}
.col-display {
- font-family: 'SF Mono', 'Fira Code', monospace;
+ font-family: var(--font-mono);
}
.bool-value {
@@ -599,13 +599,13 @@ const valueColumnLabel = computed(() => {
}
.bool-value.on {
- background: #a6e3a1;
- color: #1e1e2e;
+ background: var(--c-success-bg);
+ color: var(--c-success-text);
}
.bool-value.off {
- background: #45475a;
- color: #6c7086;
+ background: var(--c-neutral-bg);
+ color: var(--c-neutral-text);
}
/* Virtual rows */
@@ -614,20 +614,20 @@ const valueColumnLabel = computed(() => {
align-items: center;
cursor: pointer;
font-size: 12px;
- border-bottom: 1px solid #1e1e2e;
+ border-bottom: 1px solid var(--c-row-border);
}
.virtual-row:hover {
- background: #1e1e2e;
+ background: var(--c-hover-bg);
}
.virtual-row.selected {
- background: #89b4fa;
- color: #1e1e2e;
+ background: var(--c-selection-bg);
+ color: var(--c-selection-text);
}
.virtual-row.selected .col-addr {
- color: #1e1e2e;
+ color: var(--c-selection-text);
}
.vcol {
@@ -648,12 +648,12 @@ const valueColumnLabel = computed(() => {
width: 120px;
min-width: 120px;
font-family: monospace;
- color: #6c7086;
+ color: var(--c-subtext0);
}
.vcol.col-display {
flex: 1;
min-width: 0;
- font-family: 'SF Mono', 'Fira Code', monospace;
+ font-family: var(--font-mono);
}
diff --git a/master-frontend/src/components/NewConnectionDialog.vue b/master-frontend/src/components/NewConnectionDialog.vue
index 3084b69..3c60ec3 100644
--- a/master-frontend/src/components/NewConnectionDialog.vue
+++ b/master-frontend/src/components/NewConnectionDialog.vue
@@ -393,25 +393,25 @@ async function submit() {
diff --git a/master-frontend/src/components/NewScanGroupDialog.vue b/master-frontend/src/components/NewScanGroupDialog.vue
index 2080d17..2a13d30 100644
--- a/master-frontend/src/components/NewScanGroupDialog.vue
+++ b/master-frontend/src/components/NewScanGroupDialog.vue
@@ -93,17 +93,17 @@ async function submit() {
diff --git a/master-frontend/src/components/ScanDialog.vue b/master-frontend/src/components/ScanDialog.vue
index 54101ea..fa7721e 100644
--- a/master-frontend/src/components/ScanDialog.vue
+++ b/master-frontend/src/components/ScanDialog.vue
@@ -345,46 +345,46 @@ async function scanAndAddAllRegistersForSlaves(slaveIds: number[]) {
diff --git a/master-frontend/src/components/ValuePanel.vue b/master-frontend/src/components/ValuePanel.vue
index 67f3534..b1868be 100644
--- a/master-frontend/src/components/ValuePanel.vue
+++ b/master-frontend/src/components/ValuePanel.vue
@@ -355,17 +355,17 @@ const writeCategory = computed<'register' | 'coil' | null>(() => {
diff --git a/master-frontend/src/components/WriteDialog.vue b/master-frontend/src/components/WriteDialog.vue
index 37d3a6c..843873f 100644
--- a/master-frontend/src/components/WriteDialog.vue
+++ b/master-frontend/src/components/WriteDialog.vue
@@ -180,26 +180,26 @@ async function submit() {
diff --git a/master-frontend/src/main.ts b/master-frontend/src/main.ts
index 2578901..4975400 100644
--- a/master-frontend/src/main.ts
+++ b/master-frontend/src/main.ts
@@ -1,6 +1,8 @@
import 'shared-frontend/src/styles/tokens.css'
import 'shared-frontend/src/styles/toolbar.css'
+import { initTheme } from 'shared-frontend'
import { createApp } from 'vue'
import App from './App.vue'
+initTheme()
createApp(App).mount('#app')
diff --git a/shared-frontend/src/components/LangToggle.vue b/shared-frontend/src/components/LangToggle.vue
index bca445d..9b2dff0 100644
--- a/shared-frontend/src/components/LangToggle.vue
+++ b/shared-frontend/src/components/LangToggle.vue
@@ -34,7 +34,7 @@ function pick(next: Locale) {
margin-right: 8px;
border-radius: 4px;
overflow: hidden;
- background: var(--c-surface0);
+ background: var(--c-control-bg);
border: 1px solid var(--c-surface1);
}
@@ -50,13 +50,13 @@ function pick(next: Locale) {
}
.lang-toggle-btn:hover:not(.active) {
- background: var(--c-surface1);
+ background: var(--c-hover-bg);
color: var(--c-text);
}
.lang-toggle-btn.active {
- background: var(--c-surface1);
- color: var(--c-text);
+ background: var(--c-selection-bg);
+ color: var(--c-selection-text);
font-weight: 600;
}
diff --git a/shared-frontend/src/components/ThemeToggle.vue b/shared-frontend/src/components/ThemeToggle.vue
index bd636ad..5e161a0 100644
--- a/shared-frontend/src/components/ThemeToggle.vue
+++ b/shared-frontend/src/components/ThemeToggle.vue
@@ -34,7 +34,7 @@ function toggle() {
height: 24px;
margin-right: 8px;
border-radius: 4px;
- background: var(--c-surface0);
+ background: var(--c-control-bg);
border: 1px solid var(--c-surface1);
color: var(--c-subtext0);
cursor: pointer;
@@ -42,7 +42,7 @@ function toggle() {
}
.theme-toggle:hover {
- background: var(--c-surface1);
+ background: var(--c-hover-bg);
color: var(--c-text);
}
diff --git a/shared-frontend/src/styles/tokens.css b/shared-frontend/src/styles/tokens.css
index e96b4a2..1ba2464 100644
--- a/shared-frontend/src/styles/tokens.css
+++ b/shared-frontend/src/styles/tokens.css
@@ -1,6 +1,6 @@
-/* Catppuccin palettes + semantic aliases.
+/* Theme palettes + semantic aliases.
Imported once from main.ts so every component can use var(--…).
- Mocha (dark) is the default; [data-theme='light'] switches to Latte. */
+ Mocha (dark) is the default; light uses neutral white and gray surfaces. */
:root, :root[data-theme='dark'] {
/* Base layers (dark → light) */
@@ -29,33 +29,56 @@
--c-row-danger-bg: #3d2a30;
+ /* Roles whose foreground/background relationship changes with the theme. */
+ --c-on-accent: var(--c-base);
+ --c-accent-hover: var(--c-sapphire);
+ --c-control-bg: var(--c-surface0);
+ --c-hover-bg: var(--c-base);
+ --c-selection-bg: var(--c-blue);
+ --c-selection-text: var(--c-base);
+ --c-row-border: var(--c-base);
+ --c-success-bg: var(--c-green);
+ --c-success-text: var(--c-base);
+ --c-neutral-bg: var(--c-surface1);
+ --c-neutral-text: var(--c-subtext1);
+
/* Mono stack with broader fallback chain than the original SF Mono / Fira Code. */
--font-mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Menlo, Consolas, monospace;
}
:root[data-theme='light'] {
- /* Catppuccin Latte */
- --c-crust: #dce0e8;
- --c-mantle: #e6e9ef;
- --c-base: #eff1f5;
- --c-surface0: #ccd0da;
- --c-surface1: #bcc0cc;
- --c-surface2: #acb0be;
- --c-overlay0: #9ca0b0;
- --c-subtext0: #6c6f85;
- --c-subtext1: #5c5f77;
- --c-text: #4c4f69;
-
- --c-blue: #1e66f5;
- --c-lavender: #7287fd;
- --c-sapphire: #209fb5;
- --c-sky: #04a5e5;
- --c-teal: #179299;
- --c-green: #40a02b;
- --c-yellow: #df8e1d;
- --c-peach: #fe640b;
- --c-red: #d20f39;
- --c-mauve: #8839ef;
-
- --c-row-danger-bg: #ecd6de;
+ --c-crust: #ffffff;
+ --c-mantle: #f5f7fa;
+ --c-base: #ffffff;
+ --c-surface0: #e2e7ee;
+ --c-surface1: #c7d0db;
+ --c-surface2: #aebbc9;
+ --c-overlay0: #66758a;
+ --c-subtext0: #526176;
+ --c-subtext1: #3f4e63;
+ --c-text: #202c3c;
+
+ --c-blue: #245dcc;
+ --c-lavender: #5753bb;
+ --c-sapphire: #176b86;
+ --c-sky: #126e99;
+ --c-teal: #16746d;
+ --c-green: #217345;
+ --c-yellow: #8a6400;
+ --c-peach: #a94d12;
+ --c-red: #b42340;
+ --c-mauve: #793ac1;
+
+ --c-row-danger-bg: #fff0f2;
+ --c-on-accent: #ffffff;
+ --c-accent-hover: #1c4ca8;
+ --c-control-bg: #ffffff;
+ --c-hover-bg: #edf2f8;
+ --c-selection-bg: #e5efff;
+ --c-selection-text: #193e80;
+ --c-row-border: #e9edf2;
+ --c-success-bg: #e4f3e9;
+ --c-success-text: #21663e;
+ --c-neutral-bg: #edf0f4;
+ --c-neutral-text: #546174;
}