fix sidebar speed dial buttons
This commit is contained in:
parent
080d9ff1b4
commit
a01f72bbc1
2 changed files with 166 additions and 52 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref, nextTick } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import { useNotificationStore } from "@/stores/notifications-primevue"
|
import { useNotificationStore } from "@/stores/notifications-primevue"
|
||||||
|
|
@ -19,17 +19,27 @@ import {
|
||||||
NavArrowLeft,
|
NavArrowLeft,
|
||||||
NavArrowRight,
|
NavArrowRight,
|
||||||
} from "@iconoir/vue";
|
} from "@iconoir/vue";
|
||||||
import SpeedDial from "primevue/speeddial";
|
import SidebarSpeedDial from "./SidebarSpeedDial.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const modalStore = useModalStore();
|
const modalStore = useModalStore();
|
||||||
const notifications = useNotificationStore();
|
const notifications = useNotificationStore();
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
|
const pendingOpen = ref(null);
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
isCollapsed.value = !isCollapsed.value;
|
isCollapsed.value = !isCollapsed.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSidebarAndDial = (category) => {
|
||||||
|
isCollapsed.value = false;
|
||||||
|
pendingOpen.value = category.name;
|
||||||
|
// ensure re-render picks up forceOpen
|
||||||
|
nextTick(() => {
|
||||||
|
pendingOpen.value = category.name;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const clientButtons = ref([
|
const clientButtons = ref([
|
||||||
{
|
{
|
||||||
label: "Customer Lookup",
|
label: "Customer Lookup",
|
||||||
|
|
@ -89,7 +99,7 @@ const createButtons = ref([
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lable: "Note",
|
label: "Note",
|
||||||
command: () => {
|
command: () => {
|
||||||
notifications.addWarning("Sending Notes coming soon!");
|
notifications.addWarning("Sending Notes coming soon!");
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +110,7 @@ const categories = ref([
|
||||||
{ name: "Home", icon: Home, url: "/" },
|
{ name: "Home", icon: Home, url: "/" },
|
||||||
{ name: "Calendar", icon: Calendar, url: "/calendar" },
|
{ name: "Calendar", icon: Calendar, url: "/calendar" },
|
||||||
{
|
{
|
||||||
name: "Clients",
|
name: "CRM",
|
||||||
icon: Community,
|
icon: Community,
|
||||||
buttons: clientButtons,
|
buttons: clientButtons,
|
||||||
},
|
},
|
||||||
|
|
@ -150,31 +160,23 @@ const handleCategoryClick = (category) => {
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<SpeedDial
|
<SidebarSpeedDial
|
||||||
:model="category.buttons"
|
|
||||||
direction="down"
|
|
||||||
type="linear"
|
|
||||||
radius="50"
|
|
||||||
v-if="!isCollapsed"
|
v-if="!isCollapsed"
|
||||||
>
|
|
||||||
<template #button="{ toggleCallback }">
|
|
||||||
<button
|
|
||||||
class="sidebar-button"
|
|
||||||
@click="toggleCallback"
|
|
||||||
:key="category.name"
|
:key="category.name"
|
||||||
|
:icon="category.icon"
|
||||||
|
:label="category.name"
|
||||||
|
:items="category.buttons"
|
||||||
|
:force-open="pendingOpen === category.name"
|
||||||
|
@opened="pendingOpen = null"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="sidebar-button"
|
||||||
|
:key="category.name"
|
||||||
|
:title="category.name"
|
||||||
|
@click="openSidebarAndDial(category)"
|
||||||
>
|
>
|
||||||
<component :is="category.icon" class="button-icon" />
|
<component :is="category.icon" class="button-icon" />
|
||||||
<span class="button-text">{{ category.name }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template #item="{ item, toggleCallback }">
|
|
||||||
<button class="create-item" @click="toggleCallback" :key="item.label">
|
|
||||||
<span class="p-menuitem-text">{{ item.label }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</SpeedDial>
|
|
||||||
<button v-else class="sidebar-button" :key="category.name" :title="category.name">
|
|
||||||
<component :is="category.icon" class="button-icon" />
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -217,22 +219,6 @@ const handleCategoryClick = (category) => {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-item {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-item:hover {
|
|
||||||
background-color: var(--surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-text {
|
.button-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -263,6 +249,13 @@ const handleCategoryClick = (category) => {
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.speeddial-caret {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-button:active {
|
.sidebar-button:active {
|
||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
@ -338,21 +331,63 @@ const handleCategoryClick = (category) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SpeedDial customization */
|
/* SpeedDial customization */
|
||||||
:deep(.p-speeddial) {
|
.sidebar-speeddial {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-speeddial-list) {
|
.sidebar-submenu {
|
||||||
background-color: var(--surface-card);
|
background-color: var(--surface-card);
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
border: 1px solid var(--surface-border);
|
border: 1px solid var(--surface-border);
|
||||||
padding: 4px;
|
border-radius: 6px;
|
||||||
margin-top: 2px;
|
margin-top: 6px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-speeddial-item) {
|
.sidebar-sub-button {
|
||||||
margin: 2px 0;
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sub-button:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-button-text {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-accordion-enter-active,
|
||||||
|
.sidebar-accordion-leave-active {
|
||||||
|
transition: max-height 0.25s ease, opacity 0.2s ease, margin 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-accordion-enter-from,
|
||||||
|
.sidebar-accordion-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-accordion-enter-to,
|
||||||
|
.sidebar-accordion-leave-from {
|
||||||
|
max-height: 600px;
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments for smaller screens */
|
/* Responsive adjustments for smaller screens */
|
||||||
|
|
|
||||||
79
frontend/src/components/SidebarSpeedDial.vue
Normal file
79
frontend/src/components/SidebarSpeedDial.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: [Object, Function],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
initiallyOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
forceOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["opened", "closed"]);
|
||||||
|
|
||||||
|
const isOpen = ref(props.initiallyOpen);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
isOpen.value = !isOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItem = (item) => {
|
||||||
|
if (typeof item?.command === "function") {
|
||||||
|
item.command();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.forceOpen,
|
||||||
|
(value) => {
|
||||||
|
if (value) {
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(isOpen, (value) => {
|
||||||
|
if (value) {
|
||||||
|
emit("opened", props.label);
|
||||||
|
} else {
|
||||||
|
emit("closed", props.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sidebar-speeddial" :class="{ open: isOpen }">
|
||||||
|
<button class="sidebar-button" @click="toggle" :aria-expanded="isOpen">
|
||||||
|
<component :is="icon" class="button-icon" />
|
||||||
|
<span class="button-text">{{ label }}</span>
|
||||||
|
<span class="speeddial-caret" aria-hidden="true">{{ isOpen ? "-" : "+" }}</span>
|
||||||
|
</button>
|
||||||
|
<transition name="sidebar-accordion">
|
||||||
|
<div v-show="isOpen" class="sidebar-submenu">
|
||||||
|
<button
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.label"
|
||||||
|
class="sidebar-sub-button"
|
||||||
|
@click="handleItem(item)"
|
||||||
|
>
|
||||||
|
<span class="sub-button-text">{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue