mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-11 19:08:20 +00:00
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:
parent
5a3a0c8f32
commit
7e7cff371f
4 changed files with 81 additions and 33 deletions
|
@ -25,6 +25,7 @@ import INetwork from '~icons/mdi/lan';
|
||||||
enum NavState { Open, Reduced, Collapsed };
|
enum NavState { Open, Reduced, Collapsed };
|
||||||
const NavStateCount = 3;
|
const NavStateCount = 3;
|
||||||
let navState = $ref(NavState.Open);
|
let navState = $ref(NavState.Open);
|
||||||
|
let reducedDynamicWidth = $ref(2.5);
|
||||||
|
|
||||||
const navRoutesNew = [
|
const navRoutesNew = [
|
||||||
{ caption: 'Dashboard', icon: IDashboard, href: '/' },
|
{ caption: 'Dashboard', icon: IDashboard, href: '/' },
|
||||||
|
@ -131,7 +132,7 @@ onMounted(async() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="authState === AuthState.Authenticated" :class="{
|
<div v-if="authState === AuthState.Authenticated" :style="`--reduced-dynamic-width: ${reducedDynamicWidth}rem;`" :class="{
|
||||||
'layout': 1,
|
'layout': 1,
|
||||||
'nav-state-open': navState === NavState.Open,
|
'nav-state-open': navState === NavState.Open,
|
||||||
'nav-state-collapsed': navState === NavState.Collapsed,
|
'nav-state-collapsed': navState === NavState.Collapsed,
|
||||||
|
@ -146,7 +147,9 @@ onMounted(async() => {
|
||||||
|
|
||||||
<div class="nav-body cl-secondary cl-force-dark">
|
<div class="nav-body cl-secondary cl-force-dark">
|
||||||
<div>
|
<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>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
|
<router-link class="button" to="/help"><i-material-symbols-help-outline/></router-link>
|
||||||
|
@ -194,6 +197,8 @@ onMounted(async() => {
|
||||||
"NH PH"
|
"NH PH"
|
||||||
"NB PC";
|
"NB PC";
|
||||||
}
|
}
|
||||||
|
.layout:not(.nav-state-open) { --reduced-width: var(--reduced-dynamic-width); }
|
||||||
|
.nav-state-open { --reduced-width: 3.5rem; }
|
||||||
.login { place-items: center; }
|
.login { place-items: center; }
|
||||||
|
|
||||||
.nav-head { grid-area: NH; }
|
.nav-head { grid-area: NH; }
|
||||||
|
@ -202,6 +207,8 @@ onMounted(async() => {
|
||||||
.page-content { grid-area: PC; }
|
.page-content { grid-area: PC; }
|
||||||
|
|
||||||
.nav-head { font-weight: bold; text-align: center; }
|
.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 > svg { display: none; }
|
||||||
.nav-head > h1 { flex-grow: 1; }
|
.nav-head > h1 { flex-grow: 1; }
|
||||||
|
|
||||||
|
@ -228,6 +235,22 @@ onMounted(async() => {
|
||||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||||
background-attachment: local, local, scroll, scroll;
|
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 */
|
||||||
.page-header {
|
.page-header {
|
||||||
|
@ -242,7 +265,6 @@ onMounted(async() => {
|
||||||
left: 0%;
|
left: 0%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: left 0.2s ease-out, width 0.2s ease-out;
|
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 .nav-body { width: calc(0% + var(--reduced-width)); }
|
||||||
.nav-state-reduced .page-content {
|
.nav-state-reduced .page-content {
|
||||||
|
@ -259,6 +281,8 @@ onMounted(async() => {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-state-reduced > .nav-body > .flex-row > * { width: var(--reduced-width); }
|
||||||
|
|
||||||
/* Mobile Layout */
|
/* Mobile Layout */
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
.layout {
|
.layout {
|
||||||
|
@ -270,9 +294,8 @@ onMounted(async() => {
|
||||||
"NB PC";
|
"NB PC";
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-head > svg {
|
.nav-head > svg { display: initial; }
|
||||||
display: initial;
|
.nav-head > h1 { text-align: left; }
|
||||||
}
|
|
||||||
|
|
||||||
.nav-state-collapsed .page-header {
|
.nav-state-collapsed .page-header {
|
||||||
left: calc(-100vw + 100%);
|
left: calc(-100vw + 100%);
|
||||||
|
|
|
@ -83,5 +83,9 @@ watchEffect(() => {
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
.pillbar > button { padding: 0.25rem; 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>
|
</style>
|
|
@ -1,19 +1,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NavRoute } from './NavElements.vue';
|
import { NavRoute } from './NavElements.vue';
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
let props = withDefaults(defineProps<{
|
||||||
|
// Two-Way Bindings
|
||||||
|
expanded?: boolean,
|
||||||
|
|
||||||
|
// One-Way Bindings
|
||||||
icon?: string | Component,
|
icon?: string | Component,
|
||||||
caption?: string,
|
caption?: string,
|
||||||
children: NavRoute[],
|
children: NavRoute[],
|
||||||
clickHandler?: () => void,
|
clickHandler?: () => void,
|
||||||
}>(), {
|
}>(), {
|
||||||
|
// Two-Way Bindings
|
||||||
|
expanded: false,
|
||||||
|
|
||||||
|
// One-Way Bindings
|
||||||
icon: '',
|
icon: '',
|
||||||
caption: '',
|
caption: '',
|
||||||
children: () => [],
|
children: () => [],
|
||||||
clickHandler: () => {},
|
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[]) {
|
function tallyChildren(routes: NavRoute[]) {
|
||||||
let count = routes.length;
|
let count = routes.length;
|
||||||
|
@ -25,39 +40,38 @@ function tallyChildren(routes: NavRoute[]) {
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<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">
|
<div class="button" @click="expanded = !expanded">
|
||||||
<component v-if="(typeof icon !== 'string')" :is="icon"/>
|
<component v-if="(typeof icon !== 'string')" :is="icon"/>
|
||||||
<template v-else>{{ icon }}</template>
|
<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"/>
|
<i-material-symbols-expand-more class="nav-dropdown-expand-icon" width="1em" height="1em"/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-dropdown-body">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style scoped>
|
||||||
span {
|
.nav-dropdown-body {
|
||||||
flex-grow: 1;
|
max-height: 0px;
|
||||||
}
|
border-left: 1px solid white;
|
||||||
.nav-dropdown-body > :is(button, .button) {
|
|
||||||
padding-left: calc(0.5rem - 1px);
|
padding-left: calc(0.5rem - 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-dropdown-body {
|
span {
|
||||||
transition: all 0.1s ease-out;
|
display: flex;
|
||||||
max-height: 0px;
|
flex-flow: row nowrap;
|
||||||
border-left: 1px solid white;
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
.nav-dropdown-expanded > .nav-dropdown-body {
|
|
||||||
max-height: var(--predicted-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-dropdown-expand-icon {
|
.nav-dropdown-expand-icon { min-width: 1.5rem; min-height: 1.5rem; }
|
||||||
transition: all 0.1s ease-out;
|
|
||||||
}
|
/* Expanded State with Transitions */
|
||||||
.nav-dropdown-expanded > div > .nav-dropdown-expand-icon {
|
.nav-dropdown-expand-icon, .nav-dropdown-body { transition: all 0.1s ease-out; }
|
||||||
transform: rotate(180deg);
|
.nav-dropdown-expanded > div > span > .nav-dropdown-expand-icon { transform: rotate(180deg); }
|
||||||
}
|
.nav-dropdown-expanded > .nav-dropdown-body { max-height: var(--predicted-height); }
|
||||||
</style>
|
</style>
|
|
@ -14,15 +14,22 @@ withDefaults(defineProps<{
|
||||||
routes: () => [],
|
routes: () => [],
|
||||||
clickHandler: () => {},
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<template v-if="routes">
|
<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">
|
<router-link v-if="route.href" :to="route.href" class="button" @click="clickHandler" :title="route.caption">
|
||||||
<component :is="route.icon"/>
|
<component :is="route.icon"/>
|
||||||
{{ route.caption }}
|
{{ route.caption }}
|
||||||
</router-link>
|
</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>
|
</template>
|
||||||
</template>
|
</template>
|
Loading…
Add table
Reference in a new issue