#6797: Implement new sidebar

This commit is contained in:
checktheroads 2021-07-29 17:33:10 -07:00
parent 5a8835f41a
commit 3752cb3e56
24 changed files with 2128 additions and 285 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -41,6 +41,32 @@
"eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1",
"prettier-eslint": "^12.0.0",
"stylelint": "^13.13.1",
"stylelint-config-twbs-bootstrap": "^2.2.3",
"typescript": "^4.2.3"
},
"stylelint": {
"extends": "stylelint-config-twbs-bootstrap/scss",
"rules": {
"selector-max-class": 16,
"selector-max-compound-selectors": 16,
"selector-no-qualifying-type": [
true,
{
"ignore": [
"attribute",
"class"
]
}
],
"number-leading-zero": "always",
"string-quotes": "single",
"selector-pseudo-element-colon-notation": "single",
"declaration-property-value-disallowed-list": {
"border": "none",
"outline": "none"
},
"scss/selector-no-union-class-name": true
}
}
}

View File

@ -1,22 +1,246 @@
import { getElement, getElements } from './util';
import { StateManager } from './state';
import { getElements, isElement } from './util';
const breakpoints = {
sm: 540,
md: 720,
lg: 960,
xl: 1140,
};
type NavState = { pinned: boolean };
type BodyAttr = 'show' | 'hide' | 'hidden' | 'pinned';
function toggleBodyPosition(position: HTMLBodyElement['style']['position']): void {
for (const element of getElements('body')) {
element.style.position = position;
class SideNav {
/**
* Sidenav container element.
*/
private base: HTMLDivElement;
/**
* SideNav internal state manager.
*/
private state: StateManager<NavState>;
constructor(base: HTMLDivElement) {
this.base = base;
this.state = new StateManager<NavState>({ pinned: true }, { persist: true });
this.init();
this.initLinks();
}
/**
* Determine if `document.body` has a sidenav attribute.
*/
private bodyHas(attr: BodyAttr): boolean {
return document.body.hasAttribute(`data-sidenav-${attr}`);
}
/**
* Remove sidenav attributes from `document.body`.
*/
private bodyRemove(...attrs: BodyAttr[]): void {
for (const attr of attrs) {
document.body.removeAttribute(`data-sidenav-${attr}`);
}
}
/**
* Add sidenav attributes to `document.body`.
*/
private bodyAdd(...attrs: BodyAttr[]): void {
for (const attr of attrs) {
document.body.setAttribute(`data-sidenav-${attr}`, '');
}
}
/**
* Set initial values & add event listeners.
*/
private init() {
for (const toggler of this.base.querySelectorAll('.sidenav-toggle')) {
toggler.addEventListener('click', event => this.onToggle(event));
}
for (const toggler of getElements<HTMLButtonElement>('.sidenav-toggle-mobile')) {
toggler.addEventListener('click', event => this.onMobileToggle(event));
}
if (window.innerWidth > 1200) {
if (this.state.get('pinned')) {
this.pin();
}
if (!this.state.get('pinned')) {
this.unpin();
}
window.addEventListener('resize', () => this.onResize());
}
if (window.innerWidth < 1200) {
this.bodyRemove('hide');
this.bodyAdd('hidden');
window.addEventListener('resize', () => this.onResize());
}
this.base.addEventListener('mouseenter', () => this.onEnter());
this.base.addEventListener('mouseleave', () => this.onLeave());
}
/**
* If the sidenav is shown, expand active nav links. Otherwise, collapse them.
*/
private initLinks(): void {
for (const link of this.getActiveLinks()) {
if (this.bodyHas('show')) {
this.activateLink(link, 'expand');
} else if (this.bodyHas('hidden')) {
this.activateLink(link, 'collapse');
}
}
}
private show(): void {
this.bodyAdd('show');
this.bodyRemove('hidden', 'hide');
}
private hide(): void {
this.bodyAdd('hidden');
this.bodyRemove('pinned', 'show');
for (const collapse of this.base.querySelectorAll('.collapse')) {
collapse.classList.remove('show');
}
}
/**
* Pin the sidenav.
*/
private pin(): void {
this.bodyAdd('show', 'pinned');
this.bodyRemove('hidden');
this.state.set('pinned', true);
}
/**
* Unpin the sidenav.
*/
private unpin(): void {
this.bodyRemove('pinned', 'show');
this.bodyAdd('hidden');
for (const collapse of this.base.querySelectorAll('.collapse')) {
collapse.classList.remove('show');
}
this.state.set('pinned', false);
}
/**
* Starting from the bottom-most active link in the element tree, work backwards to determine the
* link's containing `.collapse` element and the `.collapse` element's containing `.nav-link`
* element. Once found, expand (or collapse) the `.collapse` element and add (or remove) the
* `.active` class to the the parent `.nav-link` element.
*
* @param link Active nav link
* @param action Expand or Collapse
*/
private activateLink(link: HTMLAnchorElement, action: 'expand' | 'collapse'): void {
// Find the closest .collapse element, which should contain `link`.
const collapse = link.closest('.collapse') as Nullable<HTMLDivElement>;
if (isElement(collapse)) {
// Find the closest `.nav-link`, which should be adjacent to the `.collapse` element.
const groupLink = collapse.parentElement?.querySelector('.nav-link');
if (isElement(groupLink)) {
groupLink.classList.add('active');
switch (action) {
case 'expand':
groupLink.setAttribute('aria-expanded', 'true');
collapse.classList.add('show');
link.classList.add('active');
break;
case 'collapse':
groupLink.setAttribute('aria-expanded', 'false');
collapse.classList.remove('show');
link.classList.remove('active');
break;
}
}
}
}
/**
* Find any nav links with `href` attributes matching the current path, to determine which nav
* link should be considered active.
*/
private *getActiveLinks(): Generator<HTMLAnchorElement> {
for (const link of this.base.querySelectorAll<HTMLAnchorElement>(
'.navbar-nav .nav .nav-item a.nav-link',
)) {
const href = new RegExp(link.href, 'gi');
if (Boolean(window.location.href.match(href))) {
yield link;
}
}
}
/**
* Show the sidenav and expand any active sections.
*/
private onEnter(): void {
if (!this.bodyHas('pinned')) {
this.bodyRemove('hide', 'hidden');
this.bodyAdd('show');
for (const link of this.getActiveLinks()) {
this.activateLink(link, 'expand');
}
}
}
/**
* Hide the sidenav and collapse any active sections.
*/
private onLeave(): void {
if (!this.bodyHas('pinned')) {
this.bodyRemove('show');
this.bodyAdd('hide');
for (const link of this.getActiveLinks()) {
this.activateLink(link, 'collapse');
}
setTimeout(() => {
this.bodyRemove('hide');
this.bodyAdd('hidden');
}, 300);
}
}
/**
* Close the (unpinned) sidenav when the window is resized.
*/
private onResize(): void {
if (this.bodyHas('show') && !this.bodyHas('pinned')) {
this.bodyRemove('show');
this.bodyAdd('hidden');
}
}
/**
* Pin & unpin the sidenav when the pin button is toggled.
*/
private onToggle(event: Event): void {
event.preventDefault();
if (this.state.get('pinned')) {
this.unpin();
} else {
this.pin();
}
}
private onMobileToggle(event: Event): void {
event.preventDefault();
if (this.bodyHas('hidden')) {
this.show();
} else {
this.hide();
}
}
}
export function initSideNav() {
const element = getElement<HTMLAnchorElement>('sidebarMenu');
if (element !== null && document.body.clientWidth < breakpoints.lg) {
element.addEventListener('shown.bs.collapse', () => toggleBodyPosition('fixed'));
element.addEventListener('hidden.bs.collapse', () => toggleBodyPosition('relative'));
for (const sidenav of getElements<HTMLDivElement>('.sidenav')) {
new SideNav(sidenav);
}
}

View File

@ -56,6 +56,13 @@ export function isTruthy<V extends string | number | boolean | null | undefined>
return false;
}
/**
* Type guard to determine if a value is an `Element`.
*/
export function isElement(obj: Element | null | undefined): obj is Element {
return typeof obj !== null && typeof obj !== 'undefined';
}
/**
* Retrieve the CSRF token from cookie storage.
*/
@ -152,6 +159,22 @@ export function getElement<E extends HTMLElement>(id: string): Nullable<E> {
return document.getElementById(id) as Nullable<E>;
}
export function removeElements(...selectors: string[]): void {
for (const element of getElements(...selectors)) {
element.remove();
}
}
export function elementWidth<E extends HTMLElement>(element: Nullable<E>): number {
let width = 0;
if (element !== null) {
const style = getComputedStyle(element);
const pre = style.width.replace('px', '');
width = parseFloat(pre);
}
return width;
}
/**
* scrollTo() wrapper that calculates a Y offset relative to `element`, but also factors in an
* offset relative to div#content-title. This ensures we scroll to the element, but leave enough

View File

@ -2,3 +2,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@import '../node_modules/@mdi/font/css/materialdesignicons.min.css';
@import '../node_modules/flatpickr/dist/flatpickr.css';
@import '../node_modules/simplebar/dist/simplebar.css';

View File

@ -1,9 +1,11 @@
// Netbox-specific Styles and Overrides.
@use 'sass:map';
@import './sidenav.scss';
:root {
--nbx-sidebar-bg: #{$gray-200};
--nbx-sidebar-scroll: #{$gray-500};
--nbx-sidebar-link-color: #{$gray-800};
--nbx-sidebar-link-hover-bg: #{$blue-100};
--nbx-sidebar-title-color: #{$text-muted};
@ -21,9 +23,11 @@
--nbx-cable-termination-border-color: #{$gray-300};
--nbx-search-filter-border-left-color: #{$gray-300};
--nbx-color-mode-toggle-color: #{$primary};
--nbx-sidenav-pin-color: #{$orange};
&[data-netbox-color-mode='dark'] {
--nbx-sidebar-bg: #{$gray-900};
--nbx-sidebar-scroll: #{$gray-700};
--nbx-sidebar-link-color: #{$gray-100};
--nbx-sidebar-link-hover-bg: #{rgba($blue-300, 0.15)};
--nbx-sidebar-title-color: #{$gray-600};
@ -41,6 +45,7 @@
--nbx-cable-termination-border-color: #{$gray-700};
--nbx-search-filter-border-left-color: #{$gray-600};
--nbx-color-mode-toggle-color: #{$yellow-300};
--nbx-sidenav-pin-color: #{$yellow};
}
}
@ -119,6 +124,13 @@ small {
background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat;
}
.btn.btn-ghost-#{$color} {
color: $value;
&:hover {
background-color: rgba($value, 0.12);
}
}
// Use Bootstrap's method of coloring the .alert-link class automatically.
// See: https://github.com/twbs/bootstrap/blob/2bdbb42dcf6bfb99b5e9e5444d9e64589eb8c08f/scss/_alert.scss#L50-L52
.toast.bg-#{$color},
@ -384,147 +396,6 @@ main.layout {
max-height: 100vh;
overflow-x: auto;
overflow-y: hidden;
.sidenav {
width: 4.5rem;
background-color: var(--nbx-sidebar-bg);
border-right: 1px solid $border-color;
// TODO: Figure out how to make the menu vertically scroll properly.
// overflow-x: hidden;
// overflow-y: auto;
padding-bottom: 1.5rem;
z-index: 5000;
& {
-ms-overflow-style: none; // Internet Explorer 10+
scrollbar-width: none; // Firefox
}
&::-webkit-scrollbar {
display: none; // Safari and Chrome
}
.nav-link {
font-size: $font-size-lg;
border-radius: unset;
transition: color 0s;
@include media-breakpoint-up(sm) {
font-size: $font-size-sm;
}
@include media-breakpoint-up(md) {
font-size: $font-size-base;
}
@include media-breakpoint-up(lg) {
font-size: $font-size-lg;
}
@include media-breakpoint-up(xl) {
font-size: $h4-font-size;
}
&:hover:not(.active) {
background-color: $accordion-button-active-bg;
}
&:after {
display: none;
}
}
.nav-item {
position: relative;
.nav-label {
opacity: 0;
z-index: 0;
height: 100%;
display: flex;
padding: $spacer;
position: absolute;
align-items: center;
margin-left: 4.5rem;
pointer-events: none;
justify-content: flex-start;
font-weight: $font-weight-bold;
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out, z-index 0.12s ease-in-out;
transform: translateX(-50px);
background-color: $accordion-button-active-bg;
color: $nav-link-color;
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
[data-netbox-color-mode='dark'] &[class] {
color: shade-color($primary, 75%);
}
}
&:hover .nav-label {
transform: translateX(-1px);
z-index: 99;
opacity: 1;
box-shadow: 1rem 0 2rem rgba($black, 0.15);
}
&:hover .nav-link {
color: $nav-link-color;
[data-netbox-color-mode='dark'] &[class] {
color: shade-color($primary, 50%);
}
}
}
.sidenav-logo {
position: relative;
& .sidenav-logo-reveal {
opacity: 0;
z-index: 0;
height: 100%;
width: max-content;
display: flex;
padding: $spacer;
position: absolute;
align-items: center;
justify-content: flex-start;
font-weight: $font-weight-bold;
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out, z-index 0.12s ease-in-out;
transform: translateX(-100%);
background-color: var(--nbx-sidebar-bg);
border-bottom-right-radius: $border-radius;
}
&:hover .sidenav-logo-reveal {
transform: translateX(-1px);
z-index: 2000;
opacity: 1;
}
}
.dropdown {
.dropdown-header {
font-weight: $font-weight-bold;
text-transform: uppercase;
color: var(--nbx-sidebar-title-color);
font-size: $font-size-sm;
}
.dropdown-item-group {
display: inline-flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding-right: map.get($spacers, 3);
&.disabled {
cursor: not-allowed;
}
}
.dropdown-item {
padding-left: map.get($spacers, 4);
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
}
}
}
main.login-container {
@ -650,6 +521,7 @@ nav.nav.nav-pills {
// Ensure the content container is full-height, and that the content block is also full height so
// that the footer is always at the bottom.
div.content-container {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
@ -657,6 +529,10 @@ div.content-container {
overflow-x: hidden;
overflow-y: auto;
@include media-breakpoint-down(lg) {
width: 100%;
}
div.content {
flex: 1;
}
@ -1112,16 +988,12 @@ html {
// Shade the home page content background-color.
&[data-netbox-path='/'] {
.content-container,
.search
// ,.sidenav-logo-reveal
{
.search {
background-color: $gray-100 !important;
}
&[data-netbox-color-mode='dark'] {
.content-container,
.search
// ,.sidenav-logo-reveal
{
.search {
background-color: $darkest !important;
}
}

View File

@ -0,0 +1,407 @@
@use 'sass:map';
@mixin parent-link {
.navbar-nav .nav-item .nav-link[data-bs-toggle] {
@content;
}
}
@mixin child-link {
.collapse .nav .nav-item .nav-link {
@content;
}
}
@mixin sidenav-open {
body[data-sidenav-show],
body[data-sidenav-pinned] {
.sidenav {
@content;
}
}
}
@mixin sidenav-closed {
body[data-sidenav-hide],
body[data-sidenav-hidden] {
.sidenav {
@content;
}
}
}
@mixin sidenav-pinned {
body[data-sidenav-pinned] {
.sidenav {
@content;
}
}
}
@mixin sidenav-show {
body[data-sidenav-show] {
.sidenav {
@content;
}
}
}
@mixin sidenav-hide {
body[data-sidenav-hide] {
.sidenav {
@content;
}
}
}
@mixin sidenav-peek {
.g-sidenav-show:not(.g-sidenav-pinned) {
.sidenav {
@content;
}
}
}
$transition-100ms-ease-in-out: all 0.1s ease-in-out;
.sidenav {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 1050;
display: block;
width: 100%;
max-width: $sidenav-width-closed;
padding-top: 0;
padding-right: 0;
padding-left: 0;
background-color: var(--nbx-sidebar-bg);
border-right: 1px solid $border-color;
transition: $transition-100ms-ease-in-out;
// Media fixes for iPhone 5 like resolutions
@include media-breakpoint-down(lg) {
transform: translateX(-$sidenav-width-closed);
+ .content-container[class] {
margin-left: 0;
}
}
+ .content-container {
margin-left: $sidenav-width-closed;
transition: $transition-100ms-ease-in-out;
}
// Navbar brand
.sidenav-brand {
margin-right: 0;
}
.sidenav-inner {
padding-right: $sidenav-spacing-x;
padding-left: $sidenav-spacing-x;
@include media-breakpoint-up(md) {
padding-right: 0;
padding-left: 0;
}
}
.sidenav-brand-img,
.sidenav-brand > img {
max-width: 100%;
max-height: calc(#{$sidenav-width-open} - 1rem);
}
.navbar-heading {
padding-top: $nav-link-padding-y;
padding-bottom: $nav-link-padding-y;
font-size: $font-size-xs;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.sidenav-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 78px;
padding: $spacer;
transition: $transition-100ms-ease-in-out;
}
.sidenav-toggle {
display: none;
}
.sidenav-collapse {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
padding-right: $sidenav-spacing-x;
padding-left: $sidenav-spacing-x;
margin-right: -$sidenav-spacing-x;
margin-left: -$sidenav-spacing-x;
> * {
min-width: 100%;
}
@include media-breakpoint-up(md) {
margin-right: 0;
margin-left: 0;
}
}
// Child Link nav-item
.nav .nav-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
@include child-link() {
width: 100%;
padding-top: $sidenav-link-spacing-y / 2.675;
padding-right: map.get($spacers, 1);
padding-bottom: $sidenav-link-spacing-y / 2.675;
/* stylelint-disable */
padding-left: $sidenav-link-spacing-x + $sidenav-icon-width + $sidenav-link-spacing-x / 4;
/* stylelint-enable */
margin-top: $sidenav-link-spacing-y / 3.3;
margin-bottom: $sidenav-link-spacing-y / 3.3;
font-size: $font-size-xs;
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
.sidenav-normal {
color: $text-muted;
&:hover {
opacity: 0.8;
}
}
.sidenav-mini-icon {
width: $sidenav-link-spacing-x;
text-align: center;
transition: $transition-100ms-ease-in-out;
}
}
@include parent-link() {
width: unset;
height: 100%;
font-weight: $font-weight-bold;
&.active {
color: $accordion-button-active-color;
background: $accordion-button-active-bg;
}
&:after {
display: inline-block;
margin-left: auto;
/* stylelint-disable */
font-family: 'Material Design Icons';
/* stylelint-enable */
font-style: normal;
font-weight: 700;
font-variant: normal;
color: $text-muted;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
content: '\f0142';
transition: $transition-100ms-ease-in-out;
}
// Expanded
&[aria-expanded='true'] {
&.active:after {
color: $accordion-button-active-color;
}
&:after {
color: $primary;
transform: rotate(90deg);
}
}
.nav-link-text {
padding-left: 0.25rem;
transition: $transition-100ms-ease-in-out;
}
}
.navbar-nav {
flex-direction: column;
margin-right: -$sidenav-spacing-x;
margin-left: -$sidenav-spacing-x;
.nav-item {
margin-top: 2px;
&.disabled {
cursor: not-allowed;
opacity: 0.8;
}
// All Links
.nav-link {
display: flex;
align-items: center;
width: 100%;
padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
font-size: $font-size-sm;
white-space: nowrap;
transition: $transition-100ms-ease-in-out;
// &.disabled {
// opacity: 0.8;
// }
&.active {
position: relative;
color: var(--nbx-sidebar-link-hover-bg);
background-color: var(--nbx-sidebar-link-hover-bg);
}
// Icon
> i {
min-width: $sidenav-icon-width;
font-size: calc(45px / 2);
text-align: center;
}
}
}
.nav-group-label {
display: block;
font-size: $font-size-xs;
font-weight: $font-weight-bold;
color: $primary;
text-transform: uppercase;
white-space: nowrap;
}
}
}
@include sidenav-pinned() {
.sidenav-toggle-icon {
color: var(--nbx-sidenav-pin-color);
transform: rotate(90deg);
}
}
@include sidenav-peek() {
.sidenav-toggle-icon {
transform: rotate(0deg);
// transform: rotate(90deg);
}
}
@include sidenav-open() {
max-width: $sidenav-width-open;
.sidenav-brand,
.navbar-heading {
display: block;
}
.sidenav-brand {
opacity: 1;
transform: translateX(0);
}
.sidenav-brand-icon {
position: absolute;
opacity: 0;
}
@include media-breakpoint-down(lg) {
transform: translateX(0);
}
@include media-breakpoint-up(xl) {
+ .content-container {
margin-left: $sidenav-width-open;
}
}
}
@include sidenav-closed() {
.sidenav-header {
padding: $spacer * 0.5;
}
.sidenav-brand {
position: absolute;
opacity: 0;
transform: translateX(-150%);
}
.sidenav-brand-icon {
opacity: 1;
}
.navbar-nav > .nav-item {
> .nav-link {
&:after {
content: '';
}
}
}
.nav-item .collapse {
display: none;
}
.nav-link-text {
opacity: 0;
}
@include parent-link() {
&.active {
margin-right: 0;
margin-left: 0;
border-radius: unset;
}
}
}
@include sidenav-show() {
.sidenav-brand {
display: block;
}
.nav-item .collapse {
height: auto;
transition: $transition-100ms-ease-in-out;
}
.nav-item .nav-link .nav-link-text {
opacity: 1;
}
.nav-item .sidenav-mini-icon {
opacity: 0;
}
@include media-breakpoint-up(lg) {
.sidenav-toggle {
display: inline-block;
}
}
}
.simplebar-track.simplebar-vertical {
right: 0;
width: 6px;
background-color: transparent;
.simplebar-scrollbar:before {
right: 0;
width: 3px;
background: var(--nbx-sidebar-scroll);
border-radius: $border-radius;
}
&.simplebar-hover .simplebar-scrollbar:before {
width: 5px;
}
}

View File

@ -122,11 +122,13 @@ $theme-color-addons: (
'pink-900': $pink-900,
);
/* stylelint-disable */
$font-family-sans-serif: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
$font-family-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
/* stylelint-enable */
// This is the same value as the default from Bootstrap, but it needs to be in scope prior to
// importing _variables.scss from Bootstrap.
@ -137,3 +139,12 @@ $accordion-padding-x: 0.8125rem;
$sidebar-width: 280px;
$sidebar-bottom-height: 4rem;
// Sidebar/Sidenav
$sidenav-width-closed: 4rem;
$sidenav-width-open: 16rem;
$sidenav-icon-width: 2rem;
$sidenav-link-px: 1rem;
$sidenav-spacing-x: 1.5rem;
$sidenav-link-spacing-x: 1rem;
$sidenav-link-spacing-y: 0.675rem;

View File

@ -1,7 +1,7 @@
// Dark Mode Theme Variables and Overrides.
@use 'sass:map';
@import './theme-base.scss';
@import './theme-base';
$primary: $blue-300;
$secondary: $gray-500;
@ -176,7 +176,7 @@ $accordion-border-color: $border-color;
$accordion-button-color: $accordion-color;
$accordion-button-bg: $accordion-bg;
$accordion-button-active-bg: shade-color($blue-300, 10%);
$accordion-button-active-color: shade-color($blue-500, 10%);
$accordion-button-active-color: color-contrast($accordion-button-active-bg);
$accordion-button-focus-border-color: $input-focus-border-color;
$accordion-icon-color: $accordion-color;
$accordion-icon-active-color: $accordion-button-active-color;

File diff suppressed because it is too large Load Diff

View File

@ -6,32 +6,24 @@
{% load static %}
{% block layout %}
<div class="container-fluid px-0">
<main class="layout">
{# Sidebar #}
{% include 'base/sidebar.html' %}
{% include 'base/sidenav.html' %}
{# Body #}
<div class="content-container">
{# Top bar #}
<nav class="navbar navbar-light sticky-top flex-md-nowrap ps-6 p-3 search container-fluid">
{# Mobile Navigation #}
<div class="d-md-none w-100 d-flex justify-content-between align-items-center my-3">
<a class="p-2 sidebar-logo d-block d-md-none" href="{% url 'home' %}">
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" width="100%" />
</a>
<button
type="button"
aria-expanded="false"
data-bs-toggle="collapse"
aria-controls="sidebar-menu"
data-bs-target="#sidebar-menu"
aria-label="Toggle Navigation"
class="navbar-toggler position-relative collapsed"
>
<button type="button" aria-label="Toggle Navigation" class="navbar-toggler sidenav-toggle-mobile">
<span class="navbar-toggler-icon"></span>
</button>
</div>
@ -53,7 +45,7 @@
</div>
</div>
</nav>
{% if settings.BANNER_TOP %}
@ -104,11 +96,51 @@
{# Page footer #}
<footer class="footer container-fluid pb-3 pt-4 px-0">
<div class="row align-items-center justify-content-end mx-0">
<div class="col text-center small text-muted">
<div class="row align-items-center justify-content-between mx-0">
{# Docs & Community Links #}
<div class="col">
<nav class="nav justify-content-start">
{# Documentation #}
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{# REST API #}
<a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank">
<i title="REST API" class="mdi mdi-code-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{# API docs #}
<a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank">
<i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{# GraphQL API #}
{% if settings.GRAPHQL_ENABLED %}
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{% endif %}
{# GitHub #}
<a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank">
<i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{# NetDev Slack #}
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
</nav>
</div>
{# System Info #}
<div class="col text-end small text-muted">
<span class="fw-light d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
</div>
</div>
</footer>

View File

@ -1,24 +0,0 @@
{% load nav %}
{% load static %}
<div class="d-flex flex-column flex-shrink-0 sidenav">
{# Logo Container #}
<div class="sidenav-logo">
{# Full logo, hidden until icon is hovered. #}
<a class="sidenav-logo-reveal" href="{% url 'home' %}">
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox Logo" height="39px" />
</a>
{# Logo Icon #}
<a href="/" class="sidenav-logo-icon d-block p-3 link-dark text-decoration-none">
<img src="{% static 'netbox_icon.svg' %}" />
</a>
</div>
{# Navigation Items #}
{% nav %}
</div>

View File

@ -0,0 +1,38 @@
{% load nav %}
{% load static %}
<nav class="sidenav" id="sidenav" data-simplebar>
<div class="sidenav-header">
{# Brand #}
{# Full Logo #}
<a class="sidenav-brand" href="/">
<img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo">
</a>
{# Icon Logo #}
<a class="sidenav-brand-icon" href="/">
<img src="{% static 'netbox_icon.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo">
</a>
{# Pin/Unpin Toggle #}
<button class="btn btn-sm btn-ghost-primary sidenav-toggle">
<div class="sidenav-toggle-icon">
<i class="mdi mdi-pin"></i>
</div>
</button>
</div>
<div class="sidenav-inner h-100 mb-auto">
{# Collapse #}
<div class="collapse sidenav-collapse">
{# Nav Items #}
{% nav %}
</div>
</div>
</nav>

View File

@ -1,70 +1,57 @@
{% load helpers %}
<ul class="nav nav-pills nav-flush flex-column mb-auto text-center">
{% comment %} <li class="nav-item">
<a href="/" class="nav-link active py-3" aria-current="page" title="Home" data-bs-toggle="tooltip" data-bs-placement="right">
<i class="mdi mdi-home"></i>
</a>
</li> {% endcomment %}
<ul class="navbar-nav">
{% for menu in nav_items %}
<li class="nav-item">
<a class="nav-link" href="#menu{{ menu.label }}" data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="menu{{ menu.label }}">
<i class="{{ menu.icon_class }}"></i>
<span class="nav-link-text">{{ menu.label }}</span>
</a>
<div class="collapse" id="menu{{ menu.label }}">
<ul class="nav nav-sm flex-column">
{% for menu in nav_items %}
<li class="nav-item sidenav-dropdown">
<div class="nav-label">
{{ menu.label }}
</div>
<div class="dropdown dropend" title="{{ menu.label }}">
<a href="#" class="nav-link py-3 dropdown-toggle" id="menu{{ menu.label }}" data-bs-toggle="dropdown" aria-expanded="false">
<i class="{{ menu.icon_class }}"></i>
</a>
<ul class="dropdown-menu shadow" aria-labelledby="menu{{ menu.label }}">
<li><h4 class="dropdown-header text-dark">{{ menu.label }}</h4>{{ menu.has_link }}</li>
<hr class="dropdown-divider" />
{% for group in menu.groups %}
{# Within each main menu, there are groups of menu items #}
{% if group.label != menu.label %}
<li><h6 class="dropdown-header">{{ group.label }}</h6></li>
{% endif %}
{% for item in group.items %}
{# Each Menu Item #}
{% if request.user|has_perms:item.permissions %}
<li class="dropdown-item-group">
<a class="dropdown-item" href="{% url item.link %}">{{ item.link_text }}</a>
{# Menu item buttons (if any) #}
{% if item.buttons %}
<div class="btn-group ps-1">
{% for button in item.buttons %}
{% if request.user|has_perms:button.permissions %}
<a class="btn btn-sm btn-{{ button.color }} lh-1" href="{% url button.link %}" title="{{ button.title }}">
<i class="{{ button.icon_class }}"></i>
</a>
{% endif %}
{% for group in menu.groups %}
{# Within each main menu, there are groups of menu items #}
<li class="nav-item">
<div class="nav-link">
{# Group Label #}
<span class="nav-group-label">{{ group.label }}</span>
</div>
</li>
{% for item in group.items %}
{# Each Item #}
{% if request.user|has_perms:item.permissions %}
<li class="nav-item">
<a href="{% url item.link %}" class="nav-link">
<span class="sidenav-normal">{{ item.link_text }}</span>
</a>
{# Menu item buttons (if any) #}
{% if item.buttons %}
<div class="btn-group px-2">
{% for button in item.buttons %}
{% if request.user|has_perms:button.permissions %}
<a class="btn btn-sm btn-{{ button.color }} lh-1" href="{% url button.link %}" title="{{ button.title }}">
<i class="{{ button.icon_class }}"></i>
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</li>
{% else %}
{# Display a disabled link (no permission) #}
<li class="nav-item disabled">
<a href="#" class="nav-link disabled" aria-disabled="true" disabled>
<span class="sidenav-normal">{{ item.link_text }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</div>
{% endif %}
</li>
{% else %}
{# Display a disabled link (no permission) #}
<li class="dropdown-item-group disabled">
<a class="dropdown-item disabled" href="#" aria-disabled="true" disabled>
{{ item.link_text }}
</a>
</li>
{% endif %}
{% endfor %}
{# Show a divider if not the last group #}
{% if forloop.counter != menu.groups|length %}
<hr class="dropdown-divider my-2" />
{% endif %}
{% endfor %}
</ul>
</div>
</li>
{% endfor %}
{% endfor %}
</ul>
</div>
</li>
{% endfor %}
</ul>