mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-15 09:14:08 +00:00
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>
192 lines
5.6 KiB
Vue
192 lines
5.6 KiB
Vue
<template>
|
|
<!-- MOBILE MENU -->
|
|
<TransitionRoot as="template" :show="globalStore.isSidebarOpen">
|
|
<Dialog
|
|
as="div"
|
|
class="fixed inset-0 z-40 flex md:hidden"
|
|
@close="globalStore.setSidebarVisibility(false)"
|
|
>
|
|
<TransitionChild
|
|
as="template"
|
|
enter="transition-opacity ease-linear duration-300"
|
|
enter-from="opacity-0"
|
|
enter-to="opacity-100"
|
|
leave="transition-opacity ease-linear duration-300"
|
|
leave-from="opacity-100"
|
|
leave-to="opacity-0"
|
|
>
|
|
<DialogOverlay class="fixed inset-0 bg-gray-600/75" />
|
|
</TransitionChild>
|
|
|
|
<TransitionChild
|
|
as="template"
|
|
enter="transition ease-in-out duration-300"
|
|
enter-from="-translate-x-full"
|
|
enter-to="translate-x-0"
|
|
leave="transition ease-in-out duration-300"
|
|
leave-from="translate-x-0"
|
|
leave-to="-translate-x-full"
|
|
>
|
|
<div class="relative flex flex-col flex-1 w-full max-w-xs bg-surface">
|
|
<TransitionChild
|
|
as="template"
|
|
enter="ease-in-out duration-300"
|
|
enter-from="opacity-0"
|
|
enter-to="opacity-100"
|
|
leave="ease-in-out duration-300"
|
|
leave-from="opacity-100"
|
|
leave-to="opacity-0"
|
|
>
|
|
<div class="absolute top-0 right-0 pt-2 -mr-12">
|
|
<button
|
|
class="
|
|
flex
|
|
items-center
|
|
justify-center
|
|
w-10
|
|
h-10
|
|
ml-1
|
|
rounded-full
|
|
focus:outline-hidden
|
|
focus:ring-2
|
|
focus:ring-inset
|
|
focus:ring-white
|
|
"
|
|
@click="globalStore.setSidebarVisibility(false)"
|
|
>
|
|
<span class="sr-only">Close sidebar</span>
|
|
<BaseIcon
|
|
name="XMarkIcon"
|
|
class="w-6 h-6 text-white"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</TransitionChild>
|
|
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
|
|
<div class="flex items-center shrink-0 px-4 mb-10">
|
|
<MainLogo
|
|
class="block h-auto max-w-full w-36 text-primary-400"
|
|
alt="InvoiceShelf Logo"
|
|
/>
|
|
</div>
|
|
|
|
<nav
|
|
v-for="(menu, index) in globalStore.menuGroups"
|
|
:key="index"
|
|
class="mt-5 space-y-1"
|
|
>
|
|
<div
|
|
v-if="menu[0] && menu[0].group_label"
|
|
class="px-4 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider"
|
|
>
|
|
{{ $t(menu[0].group_label) }}
|
|
</div>
|
|
<router-link
|
|
v-for="item in menu"
|
|
:key="item.name"
|
|
:to="item.link"
|
|
:class="[
|
|
hasActiveUrl(item.link)
|
|
? 'text-primary-600 bg-primary-50 font-semibold'
|
|
: '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)"
|
|
>
|
|
<BaseIcon
|
|
:name="item.icon"
|
|
:class="[
|
|
hasActiveUrl(item.link)
|
|
? 'text-primary-500'
|
|
: 'text-subtle',
|
|
'mr-3 shrink-0 h-5 w-5',
|
|
]"
|
|
@click="globalStore.setSidebarVisibility(false)"
|
|
/>
|
|
{{ $t(item.title) }}
|
|
</router-link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</TransitionChild>
|
|
<div class="shrink-0 w-14">
|
|
<!-- Force sidebar to shrink to fit close icon -->
|
|
</div>
|
|
</Dialog>
|
|
</TransitionRoot>
|
|
|
|
<!-- DESKTOP MENU -->
|
|
<div
|
|
class="
|
|
hidden
|
|
w-56
|
|
h-screen
|
|
pb-32
|
|
overflow-y-auto
|
|
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
|
|
"
|
|
>
|
|
<div
|
|
v-for="(menu, index) in globalStore.menuGroups"
|
|
:key="index"
|
|
class="p-0 m-0 mt-6 list-none"
|
|
>
|
|
<div
|
|
v-if="menu[0] && menu[0].group_label"
|
|
class="px-6 mt-6 mb-2 text-xs font-semibold text-subtle uppercase tracking-wider"
|
|
>
|
|
{{ $t(menu[0].group_label) }}
|
|
</div>
|
|
<router-link
|
|
v-for="item in menu"
|
|
:key="item"
|
|
:to="item.link"
|
|
:class="[
|
|
hasActiveUrl(item.link)
|
|
? 'text-primary-600 bg-primary-50 font-semibold'
|
|
: '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',
|
|
]"
|
|
>
|
|
<BaseIcon
|
|
:name="item.icon"
|
|
:class="[
|
|
hasActiveUrl(item.link)
|
|
? 'text-primary-500'
|
|
: 'text-subtle group-hover:text-body',
|
|
'mr-3 shrink-0 h-5 w-5',
|
|
]"
|
|
/>
|
|
|
|
{{ $t(item.title) }}
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import MainLogo from '@/scripts/components/icons/MainLogo.vue'
|
|
|
|
import {
|
|
Dialog,
|
|
DialogOverlay,
|
|
TransitionChild,
|
|
TransitionRoot,
|
|
} from '@headlessui/vue'
|
|
|
|
import { useRoute } from 'vue-router'
|
|
import { useGlobalStore } from '@/scripts/admin/stores/global'
|
|
|
|
const route = useRoute()
|
|
const globalStore = useGlobalStore()
|
|
|
|
function hasActiveUrl(url) {
|
|
return route.path.indexOf(url) > -1
|
|
}
|
|
</script>
|