feat: add dark theme support with CSS custom properties and Naive UI integration
Implement runtime theme switching using CSS custom properties delegated through SCSS variables, with light/dark/system modes, FOUC prevention, sidebar toggle, and settings selector. Add theme-aware video assets for sidebar and chat thinking indicator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { NConfigProvider, NMessageProvider, NDialogProvider, NNotificationProvider } from 'naive-ui'
|
import { darkTheme, NConfigProvider, NMessageProvider, NDialogProvider, NNotificationProvider } from 'naive-ui'
|
||||||
import { themeOverrides } from '@/styles/theme'
|
import { getThemeOverrides } from '@/styles/theme'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
||||||
import { useKeyboard } from '@/composables/useKeyboard'
|
import { useKeyboard } from '@/composables/useKeyboard'
|
||||||
import { useAppStore } from '@/stores/hermes/app'
|
import { useAppStore } from '@/stores/hermes/app'
|
||||||
|
|
||||||
|
const { isDark } = useTheme()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const ready = ref(false)
|
const ready = ref(false)
|
||||||
|
|
||||||
|
const themeOverrides = computed(() => getThemeOverrides(isDark.value))
|
||||||
|
const naiveTheme = computed(() => isDark.value ? darkTheme : null)
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.name === 'login')
|
const isLoginPage = computed(() => route.name === 'login')
|
||||||
|
|
||||||
// Close mobile sidebar on route change
|
// Close mobile sidebar on route change
|
||||||
@@ -39,7 +44,7 @@ useKeyboard()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NConfigProvider :theme-overrides="themeOverrides">
|
<NConfigProvider :theme="naiveTheme" :theme-overrides="themeOverrides">
|
||||||
<NMessageProvider>
|
<NMessageProvider>
|
||||||
<NDialogProvider>
|
<NDialogProvider>
|
||||||
<NNotificationProvider>
|
<NNotificationProvider>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -331,7 +331,7 @@ function isImage(type: string): boolean {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
color: #fff;
|
color: var(--text-on-overlay);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -394,8 +394,8 @@ function isImage(type: string): boolean {
|
|||||||
|
|
||||||
// Drag-over state
|
// Drag-over state
|
||||||
.input-wrapper.drag-over {
|
.input-wrapper.drag-over {
|
||||||
border-color: #4a90d9;
|
border-color: var(--accent-info);
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
background-color: rgba(74, 144, 217, 0.04);
|
background-color: rgba(var(--accent-info-rgb), 0.04);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const renderedHtml = computed(() => md.render(props.content))
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: rgba($accent-primary, 0.08);
|
background: rgba(var(--accent-primary-rgb), 0.08);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -189,4 +189,20 @@ const renderedHtml = computed(() => md.render(props.content))
|
|||||||
.hljs-title\.function_ { color: #1a1a1a; }
|
.hljs-title\.function_ { color: #1a1a1a; }
|
||||||
.hljs-params { color: #2a2a2a; }
|
.hljs-params { color: #2a2a2a; }
|
||||||
.hljs-meta { color: #999999; }
|
.hljs-meta { color: #999999; }
|
||||||
|
|
||||||
|
// Dark mode highlight.js — inverted pure ink
|
||||||
|
.dark .hljs { color: #d0d0d0; }
|
||||||
|
.dark .hljs-keyword,
|
||||||
|
.dark .hljs-selector-tag { color: #f0f0f0; font-weight: 600; }
|
||||||
|
.dark .hljs-string,
|
||||||
|
.dark .hljs-attr { color: #aaaaaa; }
|
||||||
|
.dark .hljs-number { color: #cccccc; }
|
||||||
|
.dark .hljs-comment { color: #666666; font-style: italic; }
|
||||||
|
.dark .hljs-built_in { color: #bbbbbb; }
|
||||||
|
.dark .hljs-type { color: #c6c6c6; }
|
||||||
|
.dark .hljs-variable { color: #f0f0f0; }
|
||||||
|
.dark .hljs-title,
|
||||||
|
.dark .hljs-title\.function_ { color: #f0f0f0; }
|
||||||
|
.dark .hljs-params { color: #d0d0d0; }
|
||||||
|
.dark .hljs-meta { color: #666666; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ const formattedToolResult = computed(() => {
|
|||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background-color: $msg-user-bg;
|
background-color: $msg-user-bg;
|
||||||
border-radius: $radius-md $radius-md 4px $radius-md;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ const formattedToolResult = computed(() => {
|
|||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background-color: $msg-assistant-bg;
|
background-color: $msg-assistant-bg;
|
||||||
border-radius: $radius-md $radius-md $radius-md 4px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ const formattedToolResult = computed(() => {
|
|||||||
border-left: 3px solid $warning;
|
border-left: 3px solid $warning;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
background-color: rgba($warning, 0.06);
|
background-color: rgba(var(--warning-rgb), 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +261,7 @@ const formattedToolResult = computed(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-attachments {
|
.msg-attachments {
|
||||||
@@ -315,6 +316,10 @@ const formattedToolResult = computed(() => {
|
|||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-line {
|
.tool-line {
|
||||||
@@ -369,7 +374,7 @@ const formattedToolResult = computed(() => {
|
|||||||
.tool-error-badge {
|
.tool-error-badge {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: $error;
|
color: $error;
|
||||||
background: rgba($error, 0.08);
|
background: rgba(var(--error-rgb), 0.08);
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ watch(currentToolCalls, scrollToBottom)
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background-color: #ffffff;
|
background-color: $bg-card;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ async function handleDelete() {
|
|||||||
transition: border-color $transition-fast;
|
transition: border-color $transition-fast;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: rgba($accent-primary, 0.3);
|
border-color: rgba(var(--accent-primary-rgb), 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,22 +180,22 @@ async function handleDelete() {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background: rgba($success, 0.12);
|
background: rgba(var(--success-rgb), 0.12);
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.info {
|
&.info {
|
||||||
background: rgba($accent-primary, 0.12);
|
background: rgba(var(--accent-primary-rgb), 0.12);
|
||||||
color: $accent-primary;
|
color: $accent-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: rgba($warning, 0.12);
|
background: rgba(var(--warning-rgb), 0.12);
|
||||||
color: $warning;
|
color: $warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background: rgba($error, 0.12);
|
background: rgba(var(--error-rgb), 0.12);
|
||||||
color: $error;
|
color: $error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async function handleDelete() {
|
|||||||
transition: border-color $transition-fast;
|
transition: border-color $transition-fast;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: rgba($accent-primary, 0.3);
|
border-color: rgba(var(--accent-primary-rgb), 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +98,12 @@ async function handleDelete() {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&.builtin {
|
&.builtin {
|
||||||
background: rgba($accent-primary, 0.12);
|
background: rgba(var(--accent-primary-rgb), 0.12);
|
||||||
color: $accent-primary;
|
color: $accent-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.custom {
|
&.custom {
|
||||||
background: rgba($success, 0.12);
|
background: rgba(var(--success-rgb), 0.12);
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,11 +181,11 @@ async function handleExport() {
|
|||||||
transition: border-color $transition-fast;
|
transition: border-color $transition-fast;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: rgba($accent-primary, 0.3);
|
border-color: rgba(var(--accent-primary-rgb), 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-color: rgba($success, 0.4);
|
border-color: rgba(var(--success-rgb), 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NSwitch, useMessage } from 'naive-ui'
|
import { NSwitch, NSelect, useMessage } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useSettingsStore } from '@/stores/hermes/settings'
|
import { useSettingsStore } from '@/stores/hermes/settings'
|
||||||
|
import { useTheme, type ThemeMode } from '@/composables/useTheme'
|
||||||
import SettingRow from './SettingRow.vue'
|
import SettingRow from './SettingRow.vue'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { mode, setMode } = useTheme()
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ label: t('settings.display.themeLight'), value: 'light' },
|
||||||
|
{ label: t('settings.display.themeDark'), value: 'dark' },
|
||||||
|
{ label: t('settings.display.themeSystem'), value: 'system' },
|
||||||
|
]
|
||||||
|
|
||||||
async function save(values: Record<string, any>) {
|
async function save(values: Record<string, any>) {
|
||||||
try {
|
try {
|
||||||
@@ -16,10 +24,19 @@ async function save(values: Record<string, any>) {
|
|||||||
message.error(t('settings.saveFailed'))
|
message.error(t('settings.saveFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleThemeChange(val: string) {
|
||||||
|
const m = val as ThemeMode
|
||||||
|
setMode(m)
|
||||||
|
save({ skin: m })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
|
<SettingRow :label="t('settings.display.theme')" :hint="t('settings.display.themeHint')">
|
||||||
|
<NSelect :value="mode" :options="themeOptions" size="small" :consistent-menu-width="false" class="input-sm" @update:value="handleThemeChange" />
|
||||||
|
</SettingRow>
|
||||||
<SettingRow :label="t('settings.display.streaming')" :hint="t('settings.display.streamingHint')">
|
<SettingRow :label="t('settings.display.streaming')" :hint="t('settings.display.streamingHint')">
|
||||||
<NSwitch :value="settingsStore.display.streaming" @update:value="v => save({ streaming: v })" />
|
<NSwitch :value="settingsStore.display.streaming" @update:value="v => save({ streaming: v })" />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const configured = computed(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.configured {
|
&.configured {
|
||||||
border-color: rgba($success, 0.2);
|
border-color: rgba(var(--success-rgb), 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ const configured = computed(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary, 0.03);
|
background-color: rgba(var(--text-primary-rgb), 0.03);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ watch(() => `${props.category}/${props.skill}`, loadSkill, { immediate: true })
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ async function handleToggle(category: string, skillName: string, newEnabled: boo
|
|||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.04);
|
background: rgba(var(--accent-primary-rgb), 0.04);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ async function handleToggle(category: string, skillName: string, newEnabled: boo
|
|||||||
.category-count {
|
.category-count {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -200,12 +200,12 @@ async function handleToggle(category: string, skillName: string, newEnabled: boo
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: rgba($accent-primary, 0.1);
|
background: rgba(var(--accent-primary-rgb), 0.1);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ import { computed } from 'vue'
|
|||||||
border-radius: 2px 2px 0 0;
|
border-radius: 2px 2px 0 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
transition: height 0.3s ease;
|
transition: height 0.3s ease;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: #66bb6a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-col {
|
.bar-col {
|
||||||
@@ -140,7 +144,7 @@ import { computed } from 'vue'
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: $text-primary;
|
background: $text-primary;
|
||||||
color: #fff;
|
color: var(--text-on-accent);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ function formatTokens(n: number): string {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
min-width: 2px;
|
min-width: 2px;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: #66bb6a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-tokens {
|
.model-tokens {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, onUnmounted } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useMessage } from "naive-ui";
|
import { useMessage } from "naive-ui";
|
||||||
@@ -7,62 +7,20 @@ import { useAppStore } from "@/stores/hermes/app";
|
|||||||
import ModelSelector from "./ModelSelector.vue";
|
import ModelSelector from "./ModelSelector.vue";
|
||||||
import ProfileSelector from "./ProfileSelector.vue";
|
import ProfileSelector from "./ProfileSelector.vue";
|
||||||
import LanguageSwitch from "./LanguageSwitch.vue";
|
import LanguageSwitch from "./LanguageSwitch.vue";
|
||||||
import danceVideo from "@/assets/dance.mp4";
|
import ThemeSwitch from "./ThemeSwitch.vue";
|
||||||
|
import danceVideoLight from "@/assets/dance-light.mp4";
|
||||||
|
import danceVideoDark from "@/assets/dance-dark.mp4";
|
||||||
|
|
||||||
|
import { useTheme } from "@/composables/useTheme";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { isDark } = useTheme();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const canvasRef = ref<HTMLCanvasElement>();
|
|
||||||
|
|
||||||
const selectedKey = computed(() => route.name as string);
|
const selectedKey = computed(() => route.name as string);
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const canvas = canvasRef.value;
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const video = document.createElement("video");
|
|
||||||
video.src = danceVideo;
|
|
||||||
video.muted = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
video.autoplay = true;
|
|
||||||
|
|
||||||
video.addEventListener("loadeddata", () => {
|
|
||||||
canvas.width = video.videoWidth;
|
|
||||||
canvas.height = video.videoHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
if (video.readyState >= 2 && ctx && canvas) {
|
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
if (video.currentTime >= video.duration - 0.05) {
|
|
||||||
video.currentTime = 0;
|
|
||||||
}
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
}
|
|
||||||
|
|
||||||
video.addEventListener("canplay", () => {
|
|
||||||
draw();
|
|
||||||
});
|
|
||||||
|
|
||||||
video.play();
|
|
||||||
|
|
||||||
const onVisible = () => {
|
|
||||||
if (document.visibilityState === "visible" && video.paused) {
|
|
||||||
video.play();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("visibilitychange", onVisible);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener("visibilitychange", onVisible);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleNav(key: string) {
|
function handleNav(key: string) {
|
||||||
router.push({ name: key });
|
router.push({ name: key });
|
||||||
}
|
}
|
||||||
@@ -82,7 +40,7 @@ async function handleUpdate() {
|
|||||||
<div class="sidebar-logo" @click="router.push('/hermes/chat')">
|
<div class="sidebar-logo" @click="router.push('/hermes/chat')">
|
||||||
<img src="/logo.png" alt="Hermes" class="logo-img" />
|
<img src="/logo.png" alt="Hermes" class="logo-img" />
|
||||||
<span class="logo-text">Hermes</span>
|
<span class="logo-text">Hermes</span>
|
||||||
<canvas ref="canvasRef" class="logo-dance" />
|
<video class="logo-dance" :src="isDark ? danceVideoDark : danceVideoLight" autoplay loop muted playsinline />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
@@ -360,6 +318,7 @@ async function handleUpdate() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<span>Hermes Web UI v{{ appStore.serverVersion || "0.1.0" }}</span>
|
<span>Hermes Web UI v{{ appStore.serverVersion || "0.1.0" }}</span>
|
||||||
|
<ThemeSwitch />
|
||||||
<a v-if="appStore.updateAvailable" class="update-hint" :class="{ loading: appStore.updating }" @click="handleUpdate">
|
<a v-if="appStore.updateAvailable" class="update-hint" :class="{ loading: appStore.updating }" @click="handleUpdate">
|
||||||
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
|
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
|
||||||
</a>
|
</a>
|
||||||
@@ -398,8 +357,12 @@ async function handleUpdate() {
|
|||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #ffffff;
|
background-color: $bg-card;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -419,6 +382,7 @@ async function handleUpdate() {
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +417,12 @@ async function handleUpdate() {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($accent-primary, 0.06);
|
background-color: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: rgba($accent-primary, 0.12);
|
background-color: rgba(var(--accent-primary-rgb), 0.12);
|
||||||
color: $accent-primary;
|
color: $accent-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,7 +454,7 @@ async function handleUpdate() {
|
|||||||
|
|
||||||
&.connected .status-dot {
|
&.connected .status-dot {
|
||||||
background-color: $success;
|
background-color: $success;
|
||||||
box-shadow: 0 0 6px rgba($success, 0.5);
|
box-shadow: 0 0 6px rgba(var(--success-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disconnected .status-dot {
|
&.disconnected .status-dot {
|
||||||
@@ -507,8 +471,9 @@ async function handleUpdate() {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: 2px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-hint {
|
.update-hint {
|
||||||
@@ -516,15 +481,15 @@ async function handleUpdate() {
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
background: #333333;
|
background: var(--accent-primary);
|
||||||
color: rgba(#fff, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background $transition-fast;
|
transition: background $transition-fast;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #3d3d3d;
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
|
const { isDark, toggleTheme } = useTheme()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="theme-switch" :title="isDark ? 'Light mode' : 'Dark mode'" @click="toggleTheme">
|
||||||
|
<!-- Sun icon (shown in dark mode) -->
|
||||||
|
<svg v-if="isDark" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<line x1="12" y1="1" x2="12" y2="3" />
|
||||||
|
<line x1="12" y1="21" x2="12" y2="23" />
|
||||||
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||||
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||||
|
<line x1="1" y1="12" x2="3" y2="12" />
|
||||||
|
<line x1="21" y1="12" x2="23" y2="12" />
|
||||||
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||||
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||||
|
</svg>
|
||||||
|
<!-- Moon icon (shown in light mode) -->
|
||||||
|
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.theme-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
export type ThemeMode = 'light' | 'dark' | 'system'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'hermes_theme'
|
||||||
|
|
||||||
|
const mode = ref<ThemeMode>(
|
||||||
|
(localStorage.getItem(STORAGE_KEY) as ThemeMode) || 'system',
|
||||||
|
)
|
||||||
|
|
||||||
|
const isDark = ref(false)
|
||||||
|
|
||||||
|
function applyTheme(dark: boolean) {
|
||||||
|
isDark.value = dark
|
||||||
|
document.documentElement.classList.toggle('dark', dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDark(m: ThemeMode): boolean {
|
||||||
|
if (m === 'system') {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
}
|
||||||
|
return m === 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial resolve
|
||||||
|
applyTheme(resolveDark(mode.value))
|
||||||
|
|
||||||
|
// Listen for system preference changes
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
mediaQuery.addEventListener('change', () => {
|
||||||
|
if (mode.value === 'system') {
|
||||||
|
applyTheme(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch mode changes
|
||||||
|
watch(mode, (newMode) => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, newMode)
|
||||||
|
applyTheme(resolveDark(newMode))
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
function setMode(m: ThemeMode) {
|
||||||
|
mode.value = m
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
mode.value = isDark.value ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
isDark,
|
||||||
|
setMode,
|
||||||
|
toggleTheme,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,6 +292,11 @@ export default {
|
|||||||
bellOnCompleteHint: 'Play sound when AI finishes',
|
bellOnCompleteHint: 'Play sound when AI finishes',
|
||||||
busyInputMode: 'Busy Input Mode',
|
busyInputMode: 'Busy Input Mode',
|
||||||
busyInputModeHint: 'Allow input while AI is processing',
|
busyInputModeHint: 'Allow input while AI is processing',
|
||||||
|
theme: 'Theme',
|
||||||
|
themeHint: 'Choose light, dark, or follow system preference',
|
||||||
|
themeLight: 'Light',
|
||||||
|
themeDark: 'Dark',
|
||||||
|
themeSystem: 'System',
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
maxTurns: 'Max Turns',
|
maxTurns: 'Max Turns',
|
||||||
|
|||||||
@@ -292,6 +292,11 @@ export default {
|
|||||||
bellOnCompleteHint: 'AI 回复完成时播放提示音',
|
bellOnCompleteHint: 'AI 回复完成时播放提示音',
|
||||||
busyInputMode: '忙碌输入模式',
|
busyInputMode: '忙碌输入模式',
|
||||||
busyInputModeHint: 'AI 处理中仍可输入',
|
busyInputModeHint: 'AI 处理中仍可输入',
|
||||||
|
theme: '主题',
|
||||||
|
themeHint: '选择浅色、暗色或跟随系统',
|
||||||
|
themeLight: '浅色',
|
||||||
|
themeDark: '暗色',
|
||||||
|
themeSystem: '跟随系统',
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
maxTurns: '最大轮次',
|
maxTurns: '最大轮次',
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { i18n } from './i18n'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './styles/global.scss'
|
import './styles/global.scss'
|
||||||
|
|
||||||
|
// Apply dark class before mount to prevent FOUC
|
||||||
|
const savedTheme = localStorage.getItem('hermes_theme') || 'system'
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
if (savedTheme === 'dark' || (savedTheme === 'system' && prefersDark)) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
}
|
||||||
|
|
||||||
// Read token from URL BEFORE router initializes (hash router strips params)
|
// Read token from URL BEFORE router initializes (hash router strips params)
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const hashQuery = window.location.hash.split('?')[1]
|
const hashQuery = window.location.hash.split('?')[1]
|
||||||
|
|||||||
@@ -19,6 +19,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Theme transition (applied programmatically on toggle, not on load)
|
||||||
|
html.theme-transitioning,
|
||||||
|
html.theme-transitioning *,
|
||||||
|
html.theme-transitioning *::before,
|
||||||
|
html.theme-transitioning *::after {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease, fill 0.3s ease, stroke 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -67,7 +76,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba($accent-primary, 0.3);
|
background: rgba(var(--accent-primary-rgb), 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared page header
|
// Shared page header
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GlobalThemeOverrides } from 'naive-ui'
|
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
|
||||||
export const themeOverrides: GlobalThemeOverrides = {
|
export const lightThemeOverrides: GlobalThemeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
primaryColor: '#333333',
|
primaryColor: '#333333',
|
||||||
primaryColorHover: '#1a1a1a',
|
primaryColorHover: '#1a1a1a',
|
||||||
@@ -69,3 +69,83 @@ export const themeOverrides: GlobalThemeOverrides = {
|
|||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const darkThemeOverrides: GlobalThemeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#e0e0e0',
|
||||||
|
primaryColorHover: '#f5f5f5',
|
||||||
|
primaryColorPressed: '#ffffff',
|
||||||
|
primaryColorSuppl: '#e0e0e0',
|
||||||
|
bodyColor: '#1a1a1a',
|
||||||
|
cardColor: '#2a2a2a',
|
||||||
|
modalColor: '#2a2a2a',
|
||||||
|
popoverColor: '#2a2a2a',
|
||||||
|
tableColor: '#2a2a2a',
|
||||||
|
inputColor: '#2a2a2a',
|
||||||
|
actionColor: '#252525',
|
||||||
|
textColorBase: '#e0e0e0',
|
||||||
|
textColor1: '#e0e0e0',
|
||||||
|
textColor2: '#a0a0a0',
|
||||||
|
textColor3: '#666666',
|
||||||
|
dividerColor: '#3a3a3a',
|
||||||
|
borderColor: '#3a3a3a',
|
||||||
|
hoverColor: 'rgba(255, 255, 255, 0.06)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderRadiusSmall: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontSizeMedium: '14px',
|
||||||
|
heightMedium: '36px',
|
||||||
|
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||||
|
fontFamilyMono: 'JetBrains Mono, Fira Code, Consolas, monospace',
|
||||||
|
},
|
||||||
|
Layout: {
|
||||||
|
color: '#1a1a1a',
|
||||||
|
siderColor: '#202020',
|
||||||
|
headerColor: '#1a1a1a',
|
||||||
|
},
|
||||||
|
Menu: {
|
||||||
|
itemTextColorActive: '#e0e0e0',
|
||||||
|
itemTextColorActiveHover: '#e0e0e0',
|
||||||
|
itemTextColorChildActive: '#e0e0e0',
|
||||||
|
itemIconColorActive: '#e0e0e0',
|
||||||
|
itemIconColorActiveHover: '#ffffff',
|
||||||
|
itemColorActive: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
itemColorActiveHover: 'rgba(255, 255, 255, 0.12)',
|
||||||
|
arrowColorActive: '#e0e0e0',
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
textColorPrimary: '#1a1a1a',
|
||||||
|
colorPrimary: '#e0e0e0',
|
||||||
|
colorHoverPrimary: '#f5f5f5',
|
||||||
|
colorPressedPrimary: '#ffffff',
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
colorFocus: '#2a2a2a',
|
||||||
|
border: '1px solid #3a3a3a',
|
||||||
|
borderHover: '1px solid #666666',
|
||||||
|
borderFocus: '1px solid #e0e0e0',
|
||||||
|
placeholderColor: '#666666',
|
||||||
|
caretColor: '#e0e0e0',
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
borderColor: '#3a3a3a',
|
||||||
|
},
|
||||||
|
Modal: {
|
||||||
|
color: '#2a2a2a',
|
||||||
|
},
|
||||||
|
Tag: {
|
||||||
|
borderRadius: '6px',
|
||||||
|
},
|
||||||
|
Switch: {
|
||||||
|
railColor: '#3a3a3a',
|
||||||
|
railColorActive: '#66bb6a',
|
||||||
|
loadingColor: '#e0e0e0',
|
||||||
|
opacityDisabled: 0.4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeOverrides(isDark: boolean): GlobalThemeOverrides {
|
||||||
|
return isDark ? darkThemeOverrides : lightThemeOverrides
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,150 @@
|
|||||||
// 黑白水墨 — Pure Ink
|
// 黑白水墨 — Pure Ink
|
||||||
// 纯黑白灰,无彩色
|
// 纯黑白灰,无彩色
|
||||||
|
// 支持 light / dark 双主题
|
||||||
|
|
||||||
|
// ─── CSS Custom Properties ─────────────────────────────────────
|
||||||
|
|
||||||
|
:root {
|
||||||
|
// Backgrounds
|
||||||
|
--bg-primary: #fafafa;
|
||||||
|
--bg-secondary: #f0f0f0;
|
||||||
|
--bg-sidebar: #f5f5f5;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--bg-card-hover: #fafafa;
|
||||||
|
--bg-input: #ffffff;
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
--border-color: #e0e0e0;
|
||||||
|
--border-light: #ebebeb;
|
||||||
|
|
||||||
|
// Accent
|
||||||
|
--accent-primary: #333333;
|
||||||
|
--accent-hover: #1a1a1a;
|
||||||
|
--accent-muted: #888888;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
--text-primary: #1a1a1a;
|
||||||
|
--text-secondary: #666666;
|
||||||
|
--text-muted: #999999;
|
||||||
|
|
||||||
|
// Status
|
||||||
|
--success: #2e7d32;
|
||||||
|
--error: #c62828;
|
||||||
|
--warning: #f57f17;
|
||||||
|
|
||||||
|
// Message bubbles
|
||||||
|
--msg-user-bg: #f5f5f5;
|
||||||
|
--msg-assistant-bg: #f5f5f5;
|
||||||
|
--msg-system-border: #bdbdbd;
|
||||||
|
|
||||||
|
// Code
|
||||||
|
--code-bg: #f4f4f4;
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
--text-on-accent: #ffffff;
|
||||||
|
--text-on-overlay: #ffffff;
|
||||||
|
--accent-info: #4a90d9;
|
||||||
|
|
||||||
|
// RGB components (for rgba() usage)
|
||||||
|
--accent-primary-rgb: 51, 51, 51;
|
||||||
|
--accent-hover-rgb: 26, 26, 26;
|
||||||
|
--text-primary-rgb: 26, 26, 26;
|
||||||
|
--text-muted-rgb: 153, 153, 153;
|
||||||
|
--success-rgb: 46, 125, 50;
|
||||||
|
--error-rgb: 198, 40, 40;
|
||||||
|
--warning-rgb: 245, 127, 23;
|
||||||
|
--accent-info-rgb: 74, 144, 217;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
// Backgrounds
|
||||||
|
--bg-primary: #1a1a1a;
|
||||||
|
--bg-secondary: #252525;
|
||||||
|
--bg-sidebar: #202020;
|
||||||
|
--bg-card: #333333;
|
||||||
|
--bg-card-hover: #333333;
|
||||||
|
--bg-input: #2a2a2a;
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
--border-color: #3a3a3a;
|
||||||
|
--border-light: #333333;
|
||||||
|
|
||||||
|
// Accent
|
||||||
|
--accent-primary: #e0e0e0;
|
||||||
|
--accent-hover: #f5f5f5;
|
||||||
|
--accent-muted: #888888;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-muted: #666666;
|
||||||
|
|
||||||
|
// Status
|
||||||
|
--success: #66bb6a;
|
||||||
|
--error: #ef5350;
|
||||||
|
--warning: #ffb74d;
|
||||||
|
|
||||||
|
// Message bubbles
|
||||||
|
--msg-user-bg: #252525;
|
||||||
|
--msg-assistant-bg: #252525;
|
||||||
|
--msg-system-border: #555555;
|
||||||
|
|
||||||
|
// Code
|
||||||
|
--code-bg: #1e1e1e;
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
--text-on-accent: #1a1a1a;
|
||||||
|
--text-on-overlay: #ffffff;
|
||||||
|
--accent-info: #6ba3d6;
|
||||||
|
|
||||||
|
// RGB components
|
||||||
|
--accent-primary-rgb: 224, 224, 224;
|
||||||
|
--accent-hover-rgb: 245, 245, 245;
|
||||||
|
--text-primary-rgb: 224, 224, 224;
|
||||||
|
--text-muted-rgb: 102, 102, 102;
|
||||||
|
--success-rgb: 102, 187, 106;
|
||||||
|
--error-rgb: 239, 83, 80;
|
||||||
|
--warning-rgb: 255, 183, 77;
|
||||||
|
--accent-info-rgb: 107, 163, 214;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SCSS Variables (delegate to CSS custom properties) ────────
|
||||||
|
|
||||||
// Backgrounds
|
// Backgrounds
|
||||||
$bg-primary: #fafafa;
|
$bg-primary: var(--bg-primary);
|
||||||
$bg-secondary: #f0f0f0;
|
$bg-secondary: var(--bg-secondary);
|
||||||
$bg-sidebar: #f5f5f5;
|
$bg-sidebar: var(--bg-sidebar);
|
||||||
$bg-card: #ffffff;
|
$bg-card: var(--bg-card);
|
||||||
$bg-card-hover: #fafafa;
|
$bg-card-hover: var(--bg-card-hover);
|
||||||
$bg-input: #ffffff;
|
$bg-input: var(--bg-input);
|
||||||
|
|
||||||
// Borders
|
// Borders
|
||||||
$border-color: #e0e0e0;
|
$border-color: var(--border-color);
|
||||||
$border-light: #ebebeb;
|
$border-light: var(--border-light);
|
||||||
|
|
||||||
// Accent
|
// Accent
|
||||||
$accent-primary: #333333;
|
$accent-primary: var(--accent-primary);
|
||||||
$accent-hover: #1a1a1a;
|
$accent-hover: var(--accent-hover);
|
||||||
$accent-muted: #888888;
|
$accent-muted: var(--accent-muted);
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
$text-primary: #1a1a1a;
|
$text-primary: var(--text-primary);
|
||||||
$text-secondary: #666666;
|
$text-secondary: var(--text-secondary);
|
||||||
$text-muted: #999999;
|
$text-muted: var(--text-muted);
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
$success: #2e7d32;
|
$success: var(--success);
|
||||||
$error: #c62828;
|
$error: var(--error);
|
||||||
$warning: #f57f17;
|
$warning: var(--warning);
|
||||||
$info: $accent-primary;
|
$info: $accent-primary;
|
||||||
|
|
||||||
// Message bubbles
|
// Message bubbles
|
||||||
$msg-user-bg: #e8e8e8;
|
$msg-user-bg: var(--msg-user-bg);
|
||||||
$msg-assistant-bg: #f5f5f5;
|
$msg-assistant-bg: var(--msg-assistant-bg);
|
||||||
$msg-system-border: #bdbdbd;
|
$msg-system-border: var(--msg-system-border);
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
$code-bg: #f4f4f4;
|
$code-bg: var(--code-bg);
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
$font-ui: 'Inter', system-ui, -apple-system, sans-serif;
|
$font-ui: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ async function handleLogin() {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
background: $text-primary;
|
background: $text-primary;
|
||||||
color: #fff;
|
color: var(--text-on-accent);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ onMounted(async () => {
|
|||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($accent-primary, 0.03);
|
background-color: rgba(var(--accent-primary-rgb), 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.level-error {
|
&.level-error {
|
||||||
@@ -225,7 +225,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
&.level-warning {
|
&.level-warning {
|
||||||
border-left-color: $warning;
|
border-left-color: $warning;
|
||||||
.log-message { color: #d9720f; }
|
.log-message { color: $warning; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,10 +244,10 @@ onMounted(async () => {
|
|||||||
min-width: 42px;
|
min-width: 42px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&.level-error { background: rgba($error, 0.12); color: $error; }
|
&.level-error { background: rgba(var(--error-rgb), 0.12); color: $error; }
|
||||||
&.level-warning { background: rgba($warning, 0.12); color: #d9720f; }
|
&.level-warning { background: rgba(var(--warning-rgb), 0.12); color: $warning; }
|
||||||
&.level-debug { background: rgba($accent-primary, 0.06); color: $text-muted; }
|
&.level-debug { background: rgba(var(--accent-primary-rgb), 0.06); color: $text-muted; }
|
||||||
&.level-info { background: rgba($accent-primary, 0.06); color: $text-muted; }
|
&.level-info { background: rgba(var(--accent-primary-rgb), 0.06); color: $text-muted; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-logger {
|
.log-logger {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function handleSelect(category: string, skill: string) {
|
|||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -562,7 +562,7 @@ onUnmounted(() => {
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
|
|
||||||
.session-item-delete {
|
.session-item-delete {
|
||||||
@@ -571,7 +571,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: rgba($accent-primary, 0.1);
|
background: rgba(var(--accent-primary-rgb), 0.1);
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -604,7 +604,7 @@ onUnmounted(() => {
|
|||||||
.session-item-shell {
|
.session-item-shell {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: $accent-primary;
|
color: $accent-primary;
|
||||||
background: rgba($accent-primary, 0.08);
|
background: rgba(var(--accent-primary-rgb), 0.08);
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@@ -634,7 +634,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $error;
|
color: $error;
|
||||||
background: rgba($error, 0.1);
|
background: rgba(var(--error-rgb), 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +648,7 @@ onUnmounted(() => {
|
|||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($accent-primary, 0.06);
|
background: rgba(var(--accent-primary-rgb), 0.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user