feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark

The legacy panel CSS (custom.min.css ported as legacy.css) tinted every
non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary)
overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue —
producing the mixed blue/green button look on dark mode. Drop legacy.css
entirely and let AD-Vue 4's algorithms own the palette.

Centralize antdThemeConfig in useTheme.js so every page resolves to the
same source of truth (light = defaultAlgorithm, dark = darkAlgorithm,
ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/
Elevated tokens). Each page's <a-config-provider> now imports the
shared computed instead of defining its own copy.

Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-08 17:39:36 +02:00
parent 1e1a585541
commit 5f1aba28b0
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
12 changed files with 38 additions and 56 deletions

View file

@ -1,4 +1,5 @@
import { reactive, computed, watchEffect } from 'vue'; import { reactive, computed, watchEffect } from 'vue';
import { theme as antdTheme } from 'ant-design-vue';
// Single shared theme state. `import { theme } from '@/composables/useTheme.js'` // Single shared theme state. `import { theme } from '@/composables/useTheme.js'`
// from any component to read/toggle. Boot side-effects (apply current // from any component to read/toggle. Boot side-effects (apply current
@ -24,6 +25,27 @@ export const theme = reactive({
export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light')); export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light'));
// AD-Vue 4 theme config consumed by every page's <a-config-provider>.
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
// blue primary. Ultra-dark layers deeper background tokens on top of
// darkAlgorithm so layouts/cards/popups all darken together.
const ULTRA_DARK_TOKENS = {
colorBgBase: '#000',
colorBgLayout: '#000',
colorBgContainer: '#0a0a0a',
colorBgElevated: '#141414',
};
export const antdThemeConfig = computed(() => {
if (!theme.isDark) {
return { algorithm: antdTheme.defaultAlgorithm };
}
return {
algorithm: antdTheme.darkAlgorithm,
token: theme.isUltra ? ULTRA_DARK_TOKENS : undefined,
};
});
export function toggleTheme() { export function toggleTheme() {
theme.isDark = !theme.isDark; theme.isDark = !theme.isDark;
} }

View file

@ -1,7 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue'; import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
import '@/styles/legacy.css';
import { setupAxios } from '@/api/axios-init.js'; import { setupAxios } from '@/api/axios-init.js';
import '@/composables/useTheme.js'; import '@/composables/useTheme.js';

View file

@ -1,11 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue'; import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
// Legacy panel CSS — overrides AD-Vue defaults to match the
// pre-migration look (palette, dark mode contrast, tag colors,
// table/tooltip styling). Loaded after AD-Vue's reset so its
// rules win.
import '@/styles/legacy.css';
import { setupAxios } from '@/api/axios-init.js'; import { setupAxios } from '@/api/axios-init.js';
// Importing useTheme triggers the boot side-effect that applies the // Importing useTheme triggers the boot side-effect that applies the

View file

@ -1,7 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue'; import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
import '@/styles/legacy.css';
import { setupAxios } from '@/api/axios-init.js'; import { setupAxios } from '@/api/axios-init.js';
// Importing this module triggers the boot side-effect that applies the // Importing this module triggers the boot side-effect that applies the

View file

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { theme as antdTheme, Modal, message } from 'ant-design-vue'; import { Modal, message } from 'ant-design-vue';
import { import {
SwapOutlined, SwapOutlined,
PieChartOutlined, PieChartOutlined,
@ -12,7 +12,7 @@ import {
import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils'; import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils';
import { Inbound } from '@/models/inbound.js'; import { Inbound } from '@/models/inbound.js';
import { theme as themeState } from '@/composables/useTheme.js'; import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
import { useMediaQuery } from '@/composables/useMediaQuery.js'; import { useMediaQuery } from '@/composables/useMediaQuery.js';
import AppSidebar from '@/components/AppSidebar.vue'; import AppSidebar from '@/components/AppSidebar.vue';
import CustomStatistic from '@/components/CustomStatistic.vue'; import CustomStatistic from '@/components/CustomStatistic.vue';
@ -28,10 +28,6 @@ import { useInbounds } from './useInbounds.js';
const { t } = useI18n(); const { t } = useI18n();
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { const {
fetched, fetched,
refreshing, refreshing,

View file

@ -1,7 +1,6 @@
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { theme as antdTheme } from 'ant-design-vue';
import { import {
BarsOutlined, BarsOutlined,
ControlOutlined, ControlOutlined,
@ -19,7 +18,7 @@ import {
const { t } = useI18n(); const { t } = useI18n();
import { HttpUtil, SizeFormatter, TimeFormatter } from '@/utils'; import { HttpUtil, SizeFormatter, TimeFormatter } from '@/utils';
import { theme as themeState } from '@/composables/useTheme.js'; import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
import { useStatus } from '@/composables/useStatus.js'; import { useStatus } from '@/composables/useStatus.js';
import { useMediaQuery } from '@/composables/useMediaQuery.js'; import { useMediaQuery } from '@/composables/useMediaQuery.js';
import AppSidebar from '@/components/AppSidebar.vue'; import AppSidebar from '@/components/AppSidebar.vue';
@ -34,11 +33,6 @@ import CpuHistoryModal from './CpuHistoryModal.vue';
import XrayLogModal from './XrayLogModal.vue'; import XrayLogModal from './XrayLogModal.vue';
import VersionModal from './VersionModal.vue'; import VersionModal from './VersionModal.vue';
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { status, fetched, refresh } = useStatus(); const { status, fetched, refresh } = useStatus();
const { isMobile } = useMediaQuery(); const { isMobile } = useMediaQuery();

View file

@ -1,22 +1,18 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { UserOutlined, LockOutlined, KeyOutlined, SettingOutlined } from '@ant-design/icons-vue'; import { UserOutlined, LockOutlined, KeyOutlined, SettingOutlined } from '@ant-design/icons-vue';
import { theme as antdTheme } from 'ant-design-vue';
import { HttpUtil } from '@/utils'; import { HttpUtil } from '@/utils';
import { currentTheme, theme as themeState } from '@/composables/useTheme.js'; import {
antdThemeConfig,
currentTheme,
theme as themeState,
} from '@/composables/useTheme.js';
import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue'; import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue';
const { t } = useI18n(); const { t } = useI18n();
// Drive AD-Vue 4's built-in dark algorithm from our useTheme state.
// This re-themes every AD-Vue component without depending on the
// legacy panel's custom.min.css.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
// Cycle the title between "Hello" and "Welcome" matches the legacy // Cycle the title between "Hello" and "Welcome" matches the legacy
// panel's Vue 2 .is-visible / .is-hidden DOM-class swap, but driven // panel's Vue 2 .is-visible / .is-hidden DOM-class swap, but driven
// reactively + with a Vue 3 <Transition> for the fade. // reactively + with a Vue 3 <Transition> for the fade.

View file

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { theme as antdTheme, Modal } from 'ant-design-vue'; import { Modal } from 'ant-design-vue';
import { import {
SettingOutlined, SettingOutlined,
SafetyOutlined, SafetyOutlined,
@ -11,7 +11,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { HttpUtil, PromiseUtil } from '@/utils'; import { HttpUtil, PromiseUtil } from '@/utils';
import { theme as themeState } from '@/composables/useTheme.js'; import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
import { useMediaQuery } from '@/composables/useMediaQuery.js'; import { useMediaQuery } from '@/composables/useMediaQuery.js';
import AppSidebar from '@/components/AppSidebar.vue'; import AppSidebar from '@/components/AppSidebar.vue';
import { useAllSetting } from './useAllSetting.js'; import { useAllSetting } from './useAllSetting.js';
@ -23,10 +23,6 @@ import SubscriptionFormatsTab from './SubscriptionFormatsTab.vue';
const { t } = useI18n(); const { t } = useI18n();
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { fetched, spinning, saveDisabled, allSetting, fetchAll, saveAll } = useAllSetting(); const { fetched, spinning, saveDisabled, allSetting, fetchAll, saveAll } = useAllSetting();
const { isMobile } = useMediaQuery(); const { isMobile } = useMediaQuery();

View file

@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { theme as antdTheme, Modal } from 'ant-design-vue'; import { Modal, message } from 'ant-design-vue';
import { import {
SettingOutlined, SettingOutlined,
SwapOutlined, SwapOutlined,
@ -12,9 +12,8 @@ import {
QuestionCircleOutlined, QuestionCircleOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { theme as themeState } from '@/composables/useTheme.js'; import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
import { useMediaQuery } from '@/composables/useMediaQuery.js'; import { useMediaQuery } from '@/composables/useMediaQuery.js';
import { message } from 'ant-design-vue';
import AppSidebar from '@/components/AppSidebar.vue'; import AppSidebar from '@/components/AppSidebar.vue';
import BasicsTab from './BasicsTab.vue'; import BasicsTab from './BasicsTab.vue';
import RoutingTab from './RoutingTab.vue'; import RoutingTab from './RoutingTab.vue';
@ -27,17 +26,6 @@ import { useXraySetting } from './useXraySetting.js';
const { t } = useI18n(); const { t } = useI18n();
// Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing,
// Outbounds, Balancers, DNS) land in subsequent 6-iivi commits they
// each need their own tree of structured forms or a dedicated modal.
// For now they show an a-empty placeholder so the navigation is
// stable and users can still edit the full config via the Advanced
// (JSON) tab.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { const {
fetched, fetched,
spinning, spinning,

View file

@ -1,7 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue'; import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
import '@/styles/legacy.css';
import { setupAxios } from '@/api/axios-init.js'; import { setupAxios } from '@/api/axios-init.js';
// Importing useTheme triggers the boot side-effect that applies the // Importing useTheme triggers the boot side-effect that applies the

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import Antd, { message } from 'ant-design-vue'; import Antd, { message } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
import '@/styles/legacy.css';
import { setupAxios } from '@/api/axios-init.js'; import { setupAxios } from '@/api/axios-init.js';
import '@/composables/useTheme.js'; import '@/composables/useTheme.js';