diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue
index 51857e6e..3e6de6e5 100644
--- a/frontend/src/components/AppSidebar.vue
+++ b/frontend/src/components/AppSidebar.vue
@@ -9,7 +9,7 @@ import {
ClusterOutlined,
LogoutOutlined,
CloseOutlined,
- MenuFoldOutlined,
+ MenuOutlined,
} from '@ant-design/icons-vue';
import { currentTheme } from '@/composables/useTheme.js';
@@ -58,11 +58,23 @@ const tabs = computed(() => [
{ key: `${prefix}logout`, icon: 'logout', title: t('logout') },
]);
+// Logout sits in its own pinned-to-bottom block on the drawer; the
+// remaining items are the navigation proper. The full-height sider on
+// desktop still uses `tabs` as-is so the desktop look is unchanged.
+const navTabs = computed(() => tabs.value.filter((tab) => tab.icon !== 'logout'));
+const utilTabs = computed(() => tabs.value.filter((tab) => tab.icon === 'logout'));
+
const activeTab = ref([props.requestUri]);
const drawerOpen = ref(false);
const collapsed = ref(JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false'));
+// Drawer width is capped against the viewport — AD-Vue's default 378px
+// overflows on narrow phones (e.g. 360px portrait), leaving the page
+// hidden behind the mask. `min()` keeps it sane on both phones and
+// tablets while never exceeding 320px on larger displays.
+const drawerWidth = 'min(82vw, 320px)';
+
function openLink(key) {
if (key.startsWith('http')) {
window.open(key);
@@ -91,6 +103,9 @@ function closeDrawer() {
@@ -123,21 +154,96 @@ function closeDrawer() {
height: 100%;
}
+/* `.sider-brand` and `.drawer-brand` share the same light-theme colour
+ * but differ in layout — the sider one is centered with its own
+ * top-of-sidebar padding + border, the drawer one sits inside a flex
+ * header next to the close button. Dark/ultra colour overrides live
+ * in the non-scoped block at the bottom (theme classes attach to
+ * body / html). */
+.sider-brand,
+.drawer-brand {
+ font-weight: 600;
+ font-size: 18px;
+ letter-spacing: 0.5px;
+ color: rgba(0, 0, 0, 0.88);
+}
+
+.sider-brand {
+ text-align: center;
+ padding: 16px 12px;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.15);
+ user-select: none;
+}
+
+.sider-brand-collapsed {
+ font-size: 16px;
+ padding: 16px 4px;
+ letter-spacing: 0;
+}
+
.drawer-handle {
position: fixed;
- top: 16px;
- left: 16px;
+ top: 12px;
+ left: 12px;
z-index: 1100;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border: none;
- width: 36px;
- height: 36px;
+ width: 40px;
+ height: 40px;
border-radius: 50%;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
+ font-size: 18px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+}
+
+.drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.15);
+}
+
+.drawer-close {
+ background: transparent;
+ border: none;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 16px;
+ color: rgba(0, 0, 0, 0.65);
+}
+
+.drawer-close:hover,
+.drawer-close:focus-visible {
+ background: rgba(128, 128, 128, 0.18);
+}
+
+.drawer-menu :deep(.ant-menu-item) {
+ height: 48px;
+ line-height: 48px;
+ margin: 0;
+ border-radius: 0;
+}
+
+.drawer-menu :deep(.ant-menu-item .anticon) {
+ font-size: 16px;
+}
+
+/* Push the utility (Logout) block to the bottom of the flex-column
+ * drawer body and separate it from the nav block with a hairline. The
+ * border colour is theme-neutral so it reads on both light and dark. */
+.drawer-utility {
+ margin-top: auto;
+ border-top: 1px solid rgba(128, 128, 128, 0.15);
}
@media (max-width: 768px) {
@@ -161,3 +267,48 @@ function closeDrawer() {
}
}
+
+
diff --git a/frontend/src/components/DateTimePicker.vue b/frontend/src/components/DateTimePicker.vue
index a9ff91ec..750d2594 100644
--- a/frontend/src/components/DateTimePicker.vue
+++ b/frontend/src/components/DateTimePicker.vue
@@ -235,8 +235,8 @@ function onAntChange(next) {
/* ===== Dark (navy) ======================================================= */
body.dark .persian-datepicker-input {
- background: #142340;
- border-color: #1f3358;
+ background: #252526;
+ border-color: #3c3c3c;
color: rgba(255, 255, 255, 0.88);
}
@@ -251,14 +251,14 @@ body.dark .persian-datepicker-input:focus {
body.dark .vpd-main .vpd-icon-btn {
background: rgba(255, 255, 255, 0.04) !important;
- border: 1px solid #1f3358 !important;
+ border: 1px solid #3c3c3c !important;
border-right: none !important;
border-radius: 6px 0 0 6px !important;
color: rgba(255, 255, 255, 0.75) !important;
}
body.dark .vpd-wrapper .vpd-content {
- background: #1a2c4d;
+ background: #2d2d30;
color: rgba(255, 255, 255, 0.88);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.32),
0 3px 6px -4px rgba(0, 0, 0, 0.48),
@@ -266,7 +266,7 @@ body.dark .vpd-wrapper .vpd-content {
}
body.dark .vpd-wrapper .vpd-body {
- background: #1a2c4d;
+ background: #2d2d30;
color: rgba(255, 255, 255, 0.88);
}
@@ -315,7 +315,7 @@ body.dark .vpd-wrapper .vpd-actions button:hover {
body.dark .vpd-wrapper .vpd-addon-list,
body.dark .vpd-wrapper .vpd-addon-list-content {
- background: #1a2c4d;
+ background: #2d2d30;
color: rgba(255, 255, 255, 0.88);
}
diff --git a/frontend/src/composables/useTheme.js b/frontend/src/composables/useTheme.js
index bac910fd..59d3e009 100644
--- a/frontend/src/composables/useTheme.js
+++ b/frontend/src/composables/useTheme.js
@@ -27,14 +27,15 @@ export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light'));
// AD-Vue 4 theme config consumed by every page's .
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
-// blue primary. Dark uses a navy palette across page/cards/modals so
-// the sidebar blends with the rest of the surface; ultra-dark stays
-// neutral black on top of darkAlgorithm.
+// blue primary. Dark uses a neutral grey palette modelled on VS Code's
+// Dark+ chrome (`#1e1e1e` editor, `#252526` sidebar, `#2d2d30` panel),
+// so the panel reads as a familiar modern IDE rather than the older
+// navy shade. Ultra-dark stays pure-black on darkAlgorithm.
const DARK_TOKENS = {
- colorBgBase: '#0a1426',
- colorBgLayout: '#0a1426',
- colorBgContainer: '#142340',
- colorBgElevated: '#1a2c4d',
+ colorBgBase: '#1e1e1e',
+ colorBgLayout: '#1e1e1e',
+ colorBgContainer: '#252526',
+ colorBgElevated: '#2d2d30',
};
const ULTRA_DARK_TOKENS = {
colorBgBase: '#000',
@@ -47,13 +48,12 @@ const ULTRA_DARK_TOKENS = {
// + trigger backgrounds and `#001529` / `#000c17` as the dark Menu item
// backgrounds (see node_modules/ant-design-vue/es/{layout,menu}/style/
// index.js). Override at the component-token level so the sider blends
-// with darkAlgorithm's neutral surfaces.
-// Dark theme uses a refined navy for the sidebar — distinct from the
-// neutral ultra-dark and warmer than AD-Vue's stock #001529.
+// with darkAlgorithm's neutral surfaces. Sider/trigger use the same
+// `#252526` / `#333333` tones VS Code does for its activity bar.
const DARK_LAYOUT_TOKENS = {
- colorBgHeader: '#0d1d33',
- colorBgTrigger: '#15294a',
- colorBgBody: '#000',
+ colorBgHeader: '#252526',
+ colorBgTrigger: '#333333',
+ colorBgBody: '#1e1e1e',
};
const ULTRA_DARK_LAYOUT_TOKENS = {
colorBgHeader: '#0a0a0a',
@@ -61,9 +61,9 @@ const ULTRA_DARK_LAYOUT_TOKENS = {
colorBgBody: '#000',
};
const DARK_MENU_TOKENS = {
- colorItemBg: '#0d1d33',
- colorSubItemBg: '#08142a',
- menuSubMenuBg: '#0d1d33',
+ colorItemBg: '#252526',
+ colorSubItemBg: '#1e1e1e',
+ menuSubMenuBg: '#252526',
};
const ULTRA_DARK_MENU_TOKENS = {
colorItemBg: '#0a0a0a',
diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue
index 084f8135..a46ed98d 100644
--- a/frontend/src/pages/inbounds/InboundsPage.vue
+++ b/frontend/src/pages/inbounds/InboundsPage.vue
@@ -650,8 +650,8 @@ function onRowAction({ key, dbInbound }) {
}
.inbounds-page.is-dark {
- --bg-page: #0a1222;
- --bg-card: #151f31;
+ --bg-page: #1e1e1e;
+ --bg-card: #252526;
}
.inbounds-page.is-dark.is-ultra {
diff --git a/frontend/src/pages/index/IndexPage.vue b/frontend/src/pages/index/IndexPage.vue
index 1c9e28b5..292e4156 100644
--- a/frontend/src/pages/index/IndexPage.vue
+++ b/frontend/src/pages/index/IndexPage.vue
@@ -336,8 +336,8 @@ async function openConfig() {
}
.index-page.is-dark {
- --bg-page: #0a1222;
- --bg-card: #151f31;
+ --bg-page: #1e1e1e;
+ --bg-card: #252526;
}
.index-page.is-dark.is-ultra {
diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue
index 4954cf1f..a106fba2 100644
--- a/frontend/src/pages/login/LoginPage.vue
+++ b/frontend/src/pages/login/LoginPage.vue
@@ -13,6 +13,19 @@ import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue';
const { t } = useI18n();
+const fetched = ref(false);
+const submitting = ref(false);
+const twoFactorEnable = ref(false);
+const version = computed(() => window.X_UI_CUR_VER || '');
+
+const user = reactive({
+ username: '',
+ password: '',
+ twoFactorCode: '',
+});
+
+const basePath = window.X_UI_BASE_PATH || '';
+
const headlineWords = computed(() => [t('pages.login.hello'), t('pages.login.title')]);
const HEADLINE_INTERVAL_MS = 2000;
const headlineIndex = ref(0);
@@ -28,23 +41,9 @@ onBeforeUnmount(() => {
if (headlineTimer != null) window.clearInterval(headlineTimer);
});
-const fetched = ref(false);
-const submitting = ref(false);
-const twoFactorEnable = ref(false);
-
-const user = reactive({
- username: '',
- password: '',
- twoFactorCode: '',
-});
-
-const basePath = window.X_UI_BASE_PATH || '';
-
onMounted(async () => {
const msg = await HttpUtil.post('/getTwoFactorEnable');
- if (msg.success) {
- twoFactorEnable.value = !!msg.obj;
- }
+ if (msg.success) twoFactorEnable.value = !!msg.obj;
fetched.value = true;
});
@@ -52,9 +51,7 @@ async function login() {
submitting.value = true;
try {
const msg = await HttpUtil.post('/login', user);
- if (msg.success) {
- window.location.href = basePath + 'panel/';
- }
+ if (msg.success) window.location.href = basePath + 'panel/';
} finally {
submitting.value = false;
}
@@ -70,102 +67,85 @@ function onLangChange(next) {
-