feat(app): add light theme setting

This commit is contained in:
Danila Poyarkov 2026-04-30 15:52:59 +03:00
parent 7ba678117f
commit 0d16354c89
7 changed files with 101 additions and 2 deletions

View file

@ -96,6 +96,12 @@ pub fn install_app_menu<R: tauri::Runtime>(app: &mut tauri::App<R>) -> tauri::Re
)
.build()?;
let theme_menu = SubmenuBuilder::new(app, "Theme")
.item(&MenuItemBuilder::new("Light").id("theme-light").build(app)?)
.item(&MenuItemBuilder::new("Dark").id("theme-dark").build(app)?)
.item(&MenuItemBuilder::new("Auto").id("theme-auto").build(app)?)
.build()?;
let view_menu = SubmenuBuilder::new(app, "View")
.item(
&MenuItemBuilder::new("Zoom In")
@ -141,6 +147,9 @@ pub fn install_app_menu<R: tauri::Runtime>(app: &mut tauri::App<R>) -> tauri::Re
.accelerator("CmdOrCtrl+\\")
.build(app)?,
)
.separator()
.item(&theme_menu)
.separator()
.item(&PredefinedMenuItem::fullscreen(app, None)?)
.separator()
.item(

View file

@ -7,11 +7,13 @@ import { provideEditor } from '@open-pencil/vue'
import AppToast from '@/components/AppToast.vue'
import { useEditorStore } from '@/app/editor/active-store'
import { toast } from '@/app/shell/ui'
import { useAppTheme } from '@/app/shell/theme'
useHead({ titleTemplate: (title) => (title ? `${title} — OpenPencil` : 'OpenPencil') })
const store = useEditorStore()
provideEditor(store)
useAppTheme()
onMounted(() => {
toast.setupGlobalErrorHandler()

View file

@ -40,6 +40,18 @@
--color-component: #9747ff;
}
html[data-theme='light'] {
--color-panel: #ffffff;
--color-canvas: #f3f4f6;
--color-border: #d8dce3;
--color-hover: #eef1f5;
--color-accent: #2563eb;
--color-surface: #202124;
--color-muted: #6b7280;
--color-input: #ffffff;
--color-component: #7c3aed;
}
html,
body,
#app {

View file

@ -2,6 +2,7 @@ import { computed } from 'vue'
import { useEditorStore } from '@/app/editor/active-store'
import { openFileDialog } from '@/app/shell/menu/use'
import { useAppTheme } from '@/app/shell/theme'
import { useEditorCommands, useI18n } from '@open-pencil/vue'
import type { MenuEntry } from '@open-pencil/vue'
@ -15,6 +16,7 @@ export function useAppMenu(mod: string) {
const store = useEditorStore()
const { menuItem: commandMenuItem } = useEditorCommands()
const { menu: t, panels: p, locale, availableLocales, localeLabels, setLocale } = useI18n()
const { theme, setTheme } = useAppTheme()
const languageMenu = computed<MenuEntry[]>(() =>
availableLocales.map((code) => ({
@ -123,6 +125,32 @@ export function useAppMenu(mod: string) {
}
},
{ separator: true as const },
{
label: 'Theme',
sub: [
{
label: 'Light',
checked: theme.value === 'light',
onCheckedChange: (checked: boolean) => {
if (checked) setTheme('light')
}
},
{
label: 'Dark',
checked: theme.value === 'dark',
onCheckedChange: (checked: boolean) => {
if (checked) setTheme('dark')
}
},
{
label: 'Auto',
checked: theme.value === 'auto',
onCheckedChange: (checked: boolean) => {
if (checked) setTheme('auto')
}
}
]
},
{
label: t.value.language,
sub: languageMenu.value

View file

@ -2,6 +2,7 @@ import { tryOnScopeDispose } from '@vueuse/core'
import { useEditorStore } from '@/app/editor/active-store'
import { importFileDialog, openFileDialog } from '@/app/shell/menu/files'
import { useAppTheme } from '@/app/shell/theme'
import { createTab, closeTab, activeTab } from '@/app/tabs'
import { IS_TAURI } from '@/constants'
@ -38,10 +39,16 @@ export function useMenu() {
if (!IS_TAURI) return
let unlisten: (() => void) | undefined
const { setTheme } = useAppTheme()
const themeActions: Partial<Record<string, () => void>> = {
'theme-dark': () => setTheme('dark'),
'theme-light': () => setTheme('light'),
'theme-auto': () => setTheme('auto')
}
void import('@tauri-apps/api/event').then(({ listen }) => {
void listen<string>('menu-event', (event) => {
const action = MENU_ACTIONS[event.payload]
const action = MENU_ACTIONS[event.payload] ?? themeActions[event.payload]
if (action) action()
}).then((fn) => {
unlisten = fn

41
src/app/shell/theme.ts Normal file
View file

@ -0,0 +1,41 @@
import { useLocalStorage, usePreferredDark } from '@vueuse/core'
import { computed, watchEffect } from 'vue'
import { IS_BROWSER } from '@open-pencil/core/constants'
export type AppTheme = 'dark' | 'light' | 'auto'
const THEME_STORAGE_KEY = 'open-pencil:theme'
const DEFAULT_THEME: AppTheme = 'dark'
const theme = useLocalStorage<AppTheme>(THEME_STORAGE_KEY, DEFAULT_THEME)
const prefersDark = usePreferredDark()
const resolvedTheme = computed<'dark' | 'light'>(() => {
if (theme.value === 'auto') return prefersDark.value ? 'dark' : 'light'
return theme.value
})
function applyTheme(value: 'dark' | 'light', setting: AppTheme): void {
if (!IS_BROWSER) return
document.documentElement.dataset.theme = value
document.documentElement.dataset.themeSetting = setting
document.documentElement.style.colorScheme = value
}
export function useAppTheme() {
watchEffect(() => applyTheme(resolvedTheme.value, theme.value))
const isLight = computed(() => resolvedTheme.value === 'light')
function setTheme(value: AppTheme): void {
theme.value = value
}
function toggleTheme(): void {
theme.value = isLight.value ? 'dark' : 'light'
}
return { theme, resolvedTheme, isLight, setTheme, toggleTheme }
}
applyTheme(resolvedTheme.value, theme.value)

View file

@ -30,7 +30,7 @@ function onClose(e: MouseEvent, tabId: string) {
v-if="tabs.length > 1"
v-model="modelValue"
activation-mode="automatic"
class="scrollbar-none flex h-9 shrink-0 items-end overflow-x-auto border-b border-border bg-[#1e1e1e]"
class="scrollbar-none flex h-9 shrink-0 items-end overflow-x-auto border-b border-border bg-canvas"
>
<TabsList class="flex h-full items-end">
<TabsTrigger