Sidebar improvements

- Now uses soft nesting
- Fixed width when expanded, dynamic when reduced
- Also added a border to selected pillbar variant
This commit is contained in:
adro 2023-11-03 17:04:03 +01:00
parent 5a3a0c8f32
commit 7e7cff371f
4 changed files with 81 additions and 33 deletions

View file

@ -25,6 +25,7 @@ import INetwork from '~icons/mdi/lan';
enum NavState { Open, Reduced, Collapsed };
const NavStateCount = 3;
let navState = $ref(NavState.Open);
let reducedDynamicWidth = $ref(2.5);
const navRoutesNew = [
{ caption: 'Dashboard', icon: IDashboard, href: '/' },
@ -131,7 +132,7 @@ onMounted(async() => {
</script>
<template>
<div v-if="authState === AuthState.Authenticated" :class="{
<div v-if="authState === AuthState.Authenticated" :style="`--reduced-dynamic-width: ${reducedDynamicWidth}rem;`" :class="{
'layout': 1,
'nav-state-open': navState === NavState.Open,
'nav-state-collapsed': navState === NavState.Collapsed,
@ -146,7 +147,9 @@ onMounted(async() => {
<div class="nav-body cl-secondary cl-force-dark">
<div>
<NavElements :routes="navRoutesNew" :click-handler="collapseNavIfMobile"/>
<div>
<NavElements :routes="navRoutesNew" :click-handler="collapseNavIfMobile" @update:expanded-depth="(val) => reducedDynamicWidth = 2.5 + 0.5 * val"/>
</div>
</div>
<div class="flex-row">
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
@ -194,6 +197,8 @@ onMounted(async() => {
"NH PH"
"NB PC";
}
.layout:not(.nav-state-open) { --reduced-width: var(--reduced-dynamic-width); }
.nav-state-open { --reduced-width: 3.5rem; }
.login { place-items: center; }
.nav-head { grid-area: NH; }
@ -202,6 +207,8 @@ onMounted(async() => {
.page-content { grid-area: PC; }
.nav-head { font-weight: bold; text-align: center; }
.nav-head:focus { background: var(--cl-bg); }
.nav-head:hover { background: var(--cl-bg-el); }
.nav-head > svg { display: none; }
.nav-head > h1 { flex-grow: 1; }
@ -228,6 +235,22 @@ onMounted(async() => {
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
background-attachment: local, local, scroll, scroll;
}
.nav-body > :first-child > * {
display: grid;
grid-template-columns: calc(var(--reduced-width) - 0.25rem) 1fr; /* -0.25rem adjustment is for halved 0.5rem padding */
place-self: start;
transition: grid-template-columns 0.2s ease-out;
width: 100%;
}
.nav-body > :first-child > * > *, .nav-dropdown > *, .nav-dropdown > :first-child, .nav-dropdown-body > * {
grid-column: 1 / 3;
display: grid;
grid-template-columns: subgrid;
}
:is(.nav-body > :first-child > * > *, .nav-dropdown > *, .nav-dropdown > :first-child, .nav-dropdown-body > *) > svg {
place-self: center;
}
/* Page */
.page-header {
@ -242,7 +265,6 @@ onMounted(async() => {
left: 0%;
width: 100%;
transition: left 0.2s ease-out, width 0.2s ease-out;
--reduced-width: 2.5rem;
}
.nav-state-reduced .nav-body { width: calc(0% + var(--reduced-width)); }
.nav-state-reduced .page-content {
@ -259,6 +281,8 @@ onMounted(async() => {
align-items: start;
}
.nav-state-reduced > .nav-body > .flex-row > * { width: var(--reduced-width); }
/* Mobile Layout */
@media only screen and (max-width: 768px) {
.layout {
@ -270,9 +294,8 @@ onMounted(async() => {
"NB PC";
}
.nav-head > svg {
display: initial;
}
.nav-head > svg { display: initial; }
.nav-head > h1 { text-align: left; }
.nav-state-collapsed .page-header {
left: calc(-100vw + 100%);

View file

@ -83,5 +83,9 @@ watchEffect(() => {
gap: 0.25rem;
}
.pillbar > button { padding: 0.25rem; gap: 0.25rem; }
.pillbar > .selected { background-color: var(--cl-bg-sl); }
.pillbar > .selected {
background-color: var(--cl-bg-sl);
border: 1px solid var(--cl-fg);
padding: calc(0.25rem - 1px);
}
</style>

View file

@ -1,19 +1,34 @@
<script setup lang="ts">
import { NavRoute } from './NavElements.vue';
withDefaults(defineProps<{
let props = withDefaults(defineProps<{
// Two-Way Bindings
expanded?: boolean,
// One-Way Bindings
icon?: string | Component,
caption?: string,
children: NavRoute[],
clickHandler?: () => void,
}>(), {
// Two-Way Bindings
expanded: false,
// One-Way Bindings
icon: '',
caption: '',
children: () => [],
clickHandler: () => {},
});
let expanded = $ref(false);
const emit = defineEmits<{ (e: 'update:expandedDepth', value: number): void }>();
// Local Variables for Two-Way Binding
let expanded = $ref(props.expanded ?? false);
let lowerDepth = $ref(0);
// Sync to v-model
watchEffect(() => emit('update:expandedDepth', expanded ? (lowerDepth || 0) + 1 : 0));
function tallyChildren(routes: NavRoute[]) {
let count = routes.length;
@ -25,39 +40,38 @@ function tallyChildren(routes: NavRoute[]) {
</script>
<template>
<div :class="{'nav-dropdown-expanded': expanded}" :style="`--predicted-height: ${2.5 * tallyChildren(children)}rem;`">
<div :class="{'nav-dropdown': 1, 'nav-dropdown-expanded': expanded}" :style="`--predicted-height: ${2.5 * tallyChildren(children)}rem;`">
<div class="button" @click="expanded = !expanded">
<component v-if="(typeof icon !== 'string')" :is="icon"/>
<template v-else>{{ icon }}</template>
<span v-text="caption"/>
<span>
{{ caption }}
<i-material-symbols-expand-more class="nav-dropdown-expand-icon" width="1em" height="1em"/>
</span>
</div>
<div class="nav-dropdown-body">
<NavElements :routes="children" :click-handler="clickHandler"/>
<NavElements :routes="children" :click-handler="clickHandler" @update:expanded-depth="(val) => lowerDepth = val"/>
</div>
</div>
</template>
<style>
span {
flex-grow: 1;
}
.nav-dropdown-body > :is(button, .button) {
<style scoped>
.nav-dropdown-body {
max-height: 0px;
border-left: 1px solid white;
padding-left: calc(0.5rem - 1px);
}
.nav-dropdown-body {
transition: all 0.1s ease-out;
max-height: 0px;
border-left: 1px solid white;
}
.nav-dropdown-expanded > .nav-dropdown-body {
max-height: var(--predicted-height);
span {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
}
.nav-dropdown-expand-icon {
transition: all 0.1s ease-out;
}
.nav-dropdown-expanded > div > .nav-dropdown-expand-icon {
transform: rotate(180deg);
}
.nav-dropdown-expand-icon { min-width: 1.5rem; min-height: 1.5rem; }
/* Expanded State with Transitions */
.nav-dropdown-expand-icon, .nav-dropdown-body { transition: all 0.1s ease-out; }
.nav-dropdown-expanded > div > span > .nav-dropdown-expand-icon { transform: rotate(180deg); }
.nav-dropdown-expanded > .nav-dropdown-body { max-height: var(--predicted-height); }
</style>

View file

@ -14,15 +14,22 @@ withDefaults(defineProps<{
routes: () => [],
clickHandler: () => {},
});
const emit = defineEmits<{
(e: 'update:expandedDepth', value: number): void,
}>();
const lowerDepths: {[index: number]: number} = $ref({});
watch($$(lowerDepths), () => emit('update:expandedDepth', Math.max(...Object.entries(lowerDepths).map(x => x[1])) ?? 0), { deep: true });
</script>
<template>
<template v-if="routes">
<template v-for="route of routes" :key="route.href">
<template v-for="[index, route] of routes.entries()" :key="route.href">
<router-link v-if="route.href" :to="route.href" class="button" @click="clickHandler" :title="route.caption">
<component :is="route.icon"/>
{{ route.caption }}
</router-link>
<NavDropdown v-else v-bind="route"/>
<NavDropdown v-else v-bind="route" @update:expanded-depth="(val) => lowerDepths[index] = val" :click-handler="clickHandler"/>
</template>
</template>
</template>