mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 03:04:05 +00:00
Add dark mode with CSS custom property theme system
Define 13 semantic color tokens (surface, text, border, hover) with light/dark values in themes.css. Register with Tailwind via @theme inline. Migrate all 335 Vue files from hardcoded gray/white classes to semantic tokens. Add theme toggle (sun/moon/system) in user avatar dropdown. Replace @tailwindcss/forms with custom form reset using theme vars. Add status badge and alert tokens for dark mode. Theme-aware chart grid/labels, skeleton placeholders, and editor. Inline script in <head> prevents flash of wrong theme on load. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,16 +47,16 @@
|
||||
overflow-visible
|
||||
text-sm
|
||||
ease-linear
|
||||
bg-white
|
||||
bg-surface
|
||||
border-0
|
||||
rounded
|
||||
cursor-pointer
|
||||
md:hidden md:ml-0
|
||||
hover:bg-gray-100
|
||||
hover:bg-hover-strong
|
||||
"
|
||||
@click.prevent="onToggle"
|
||||
>
|
||||
<BaseIcon name="Bars3Icon" class="!w-6 !h-6 text-gray-500" />
|
||||
<BaseIcon name="Bars3Icon" class="!w-6 !h-6 text-muted" />
|
||||
</div>
|
||||
|
||||
<ul class="flex float-right h-8 m-0 list-none md:h-9">
|
||||
@@ -81,7 +81,7 @@
|
||||
md:h-9 md:w-9
|
||||
"
|
||||
>
|
||||
<BaseIcon name="PlusIcon" class="w-5 h-5 text-gray-600" />
|
||||
<BaseIcon name="PlusIcon" class="w-5 h-5 text-body" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentTextIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('invoices.new_invoice') }}
|
||||
@@ -103,7 +103,7 @@
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('estimates.new_estimate') }}
|
||||
@@ -116,7 +116,7 @@
|
||||
>
|
||||
<BaseIcon
|
||||
name="UserIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('customers.new_customer') }}
|
||||
@@ -148,11 +148,30 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<div class="px-3 py-2">
|
||||
<div class="flex items-center justify-between rounded-lg bg-surface-secondary p-1">
|
||||
<button
|
||||
v-for="opt in themeOptions"
|
||||
:key="opt.value"
|
||||
:class="[
|
||||
'flex items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors',
|
||||
currentTheme === opt.value
|
||||
? 'bg-surface text-heading shadow-sm'
|
||||
: 'text-muted hover:text-body',
|
||||
]"
|
||||
@click.stop="setTheme(opt.value)"
|
||||
>
|
||||
<BaseIcon :name="opt.icon" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link to="/admin/user-settings">
|
||||
<BaseDropdownItem>
|
||||
<BaseIcon
|
||||
name="CogIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('navigation.settings') }}
|
||||
@@ -162,7 +181,7 @@
|
||||
<BaseDropdownItem @click="logout">
|
||||
<BaseIcon
|
||||
name="ArrowRightOnRectangleIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ $t('navigation.logout') }}
|
||||
@@ -176,7 +195,7 @@
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/scripts/admin/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
||||
|
||||
@@ -228,4 +247,35 @@ async function logout() {
|
||||
function onToggle() {
|
||||
globalStore.setSidebarVisibility(true)
|
||||
}
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'light', icon: 'SunIcon' },
|
||||
{ value: 'dark', icon: 'MoonIcon' },
|
||||
{ value: 'system', icon: 'ComputerDesktopIcon' },
|
||||
]
|
||||
|
||||
const currentTheme = ref(localStorage.getItem('theme') || 'system')
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
currentTheme.value = theme
|
||||
localStorage.setItem('theme', theme)
|
||||
applyTheme(theme)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
applyTheme(currentTheme.value)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (currentTheme.value === 'system') {
|
||||
applyTheme('system')
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
leave-from="translate-x-0"
|
||||
leave-to="-translate-x-full"
|
||||
>
|
||||
<div class="relative flex flex-col flex-1 w-full max-w-xs bg-white">
|
||||
<div class="relative flex flex-col flex-1 w-full max-w-xs bg-surface">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-in-out duration-300"
|
||||
@@ -78,7 +78,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="menu[0] && menu[0].group_label"
|
||||
class="px-4 mt-6 mb-2 text-xs font-semibold text-gray-400 uppercase tracking-wider"
|
||||
class="px-4 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider"
|
||||
>
|
||||
{{ $t(menu[0].group_label) }}
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-600 bg-primary-50 font-semibold'
|
||||
: 'text-gray-600 hover:bg-gray-50',
|
||||
: 'text-body hover:bg-hover',
|
||||
'cursor-pointer mx-3 px-3 py-2.5 flex items-center rounded-lg text-sm not-italic font-medium transition-colors',
|
||||
]"
|
||||
@click="globalStore.setSidebarVisibility(false)"
|
||||
@@ -99,7 +99,7 @@
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500'
|
||||
: 'text-gray-400',
|
||||
: 'text-subtle',
|
||||
'mr-3 shrink-0 h-5 w-5',
|
||||
]"
|
||||
@click="globalStore.setSidebarVisibility(false)"
|
||||
@@ -124,8 +124,8 @@
|
||||
h-screen
|
||||
pb-32
|
||||
overflow-y-auto
|
||||
bg-white
|
||||
border-r border-gray-200 border-solid
|
||||
bg-surface
|
||||
border-r border-line-default border-solid
|
||||
xl:w-64
|
||||
md:fixed md:flex md:flex-col md:inset-y-0
|
||||
pt-16
|
||||
@@ -138,7 +138,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="menu[0] && menu[0].group_label"
|
||||
class="px-6 mt-6 mb-2 text-xs font-semibold text-gray-400 uppercase tracking-wider"
|
||||
class="px-6 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider"
|
||||
>
|
||||
{{ $t(menu[0].group_label) }}
|
||||
</div>
|
||||
@@ -149,7 +149,7 @@
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-600 bg-primary-50 font-semibold'
|
||||
: 'text-gray-600 hover:bg-gray-50',
|
||||
: 'text-body hover:bg-hover',
|
||||
'cursor-pointer mx-3 px-3 py-2.5 group flex items-center rounded-lg text-sm not-italic font-medium transition-colors',
|
||||
]"
|
||||
>
|
||||
@@ -158,7 +158,7 @@
|
||||
:class="[
|
||||
hasActiveUrl(item.link)
|
||||
? 'text-primary-500'
|
||||
: 'text-gray-400 group-hover:text-gray-600',
|
||||
: 'text-subtle group-hover:text-body',
|
||||
'mr-3 shrink-0 h-5 w-5',
|
||||
]"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user