1<template>
2 <TransitionRoot :show="sidebarOpened">
3 <Dialog as="div" @close="sidebarOpened = false" class="fixed inset-0">
4 <TransitionChild
5 as="template"
6 enter="transition ease-in-out duration-200 transform"
7 enter-from="-translate-x-full rtl:translate-x-full"
8 enter-to="translate-x-0"
9 leave="transition ease-in-out duration-200 transform"
10 leave-from="translate-x-0"
11 leave-to="-translate-x-full rtl:translate-x-full"
12 >
13 <div class="relative z-10 h-full w-fit">
14 <AppSidebar mobile :profile-settings="profileSettings" />
15 </div>
16 </TransitionChild>
17 <TransitionChild
18 as="template"
19 enter="transition-opacity ease-linear duration-200"
20 enter-from="opacity-0"
21 enter-to="opacity-100"
22 leave="transition-opacity ease-linear duration-200"
23 leave-from="opacity-100"
24 leave-to="opacity-0"
25 >
26 <DialogOverlay class="fixed inset-0 bg-black-overlay-500" />
27 </TransitionChild>
28 </Dialog>
29 </TransitionRoot>
30</template>
31
32<script setup lang="ts">
33import {
34 Dialog,
35 DialogOverlay,
36 TransitionChild,
37 TransitionRoot,
38} from "@headlessui/vue";
39import { computed, h, watch } from "vue";
40import { useRoute } from "vue-router";
41
42import { useAuthStore } from "@/stores/auth";
43import { CUSTOMER_PORTAL_ROOT } from "@/utils";
44import { useColorScheme } from "frappe-ui";
45import LucideMoon from "~icons/lucide/moon";
46import LucideSun from "~icons/lucide/sun";
47
48import { mobileSidebarOpened as sidebarOpened } from "@/composables/mobile";
49import { useApps } from "@/composables/useApps";
50import { __ } from "@/translation";
51import AppSidebar from "./AppSidebar.vue";
52import { useAgentStatusStore } from "@/stores/agentStatus";
53
54const { colorScheme, toggleColorScheme } = useColorScheme();
55const { appsMenuOption } = useApps();
56const route = useRoute();
57const authStore = useAuthStore();
58
59const agentStatusStore = useAgentStatusStore();
60
61// The menu injects `size-4` onto icon components; keep that as the outer
62// footprint (so it lines up with the lucide icons) and center the actual
63// 2.5 dot inside it, same as the availability indicator elsewhere.
64const statusDot = (status: string) =>
65 h("span", { class: "flex items-center justify-center" }, [
66 h("span", {
67 class: [
68 "size-2.5 shrink-0 rounded-full",
69 agentStatusStore.statusColor(status),
70 ],
71 }),
72 ]);
73
74// Native Dropdown submenu (same pattern as appsMenuOption) instead of a nested
75// Popover: reka-ui keeps the submenu inside the viewport on narrow screens,
76// where a right-placed popover overlapped the menu items below it.
77const availabilityMenuOption = computed(() => ({
78 label: agentStatusStore.myStatus
79 ? __(agentStatusStore.myStatus)
80 : __("Set status"),
81 icon: () => statusDot(agentStatusStore.myStatus),
82 submenu: agentStatusStore.statusOptions.map((option) => ({
83 label: __(option),
84 icon: () => statusDot(option),
85 onClick: () => agentStatusStore.setMyStatus(option),
86 })),
87}));
88
89const themeMenuItem = computed(() => ({
90 label: __("Toggle theme"),
91 icon: colorScheme.value === "dark" ? LucideSun : LucideMoon,
92 onClick: () => toggleColorScheme(),
93}));
94
95const profileSettings = computed(() => [
96 appsMenuOption.value,
97 ...(authStore.hasAgentRecord ? [availabilityMenuOption.value] : []),
98 {
99 label: __("Customer portal"),
100 icon: "lucide-users",
101 onClick: () => window.open(CUSTOMER_PORTAL_ROOT),
102 },
103 {
104 icon: "lucide-life-buoy",
105 label: __("Support"),
106 onClick: () => window.open("https://t.me/opendesk"),
107 },
108 {
109 icon: "lucide-book-open",
110 label: __("Docs"),
111 onClick: () => window.open("https://github.com/ikhsanddy/opendesk"),
112 },
113 themeMenuItem.value,
114 {
115 label: __("Log out"),
116 icon: "lucide-log-out",
117 onClick: () => authStore.logout(),
118 },
119]);
120
121watch(
122 () => route.fullPath,
123 () => (sidebarOpened.value = false)
124);
125</script>
126