Merge feature

This commit is contained in:
jeremystretch
2021-07-30 15:13:55 -04:00
32 changed files with 2347 additions and 286 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@@ -32,9 +32,7 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) {
* Initialize Search Bar Elements.
*/
function initSearchBar() {
for (const dropdown of getElements<HTMLUListElement>(
'div.search-container ul.search-obj-selector',
)) {
for (const dropdown of getElements<HTMLUListElement>('.search-obj-selector')) {
for (const button of dropdown.querySelectorAll<HTMLButtonElement>(
'li > button.dropdown-item',
)) {

View File

@@ -123,6 +123,11 @@ class APISelect {
*/
private disabledOptions: Array<string> = [];
/**
* Array of properties which if truthy on an API object should be considered disabled.
*/
private disabledAttributes: Array<string> = DISABLED_ATTRIBUTES;
constructor(base: HTMLSelectElement) {
// Initialize readonly properties.
this.base = base;
@@ -141,6 +146,7 @@ class APISelect {
this.loadEvent = new Event(`netbox.select.onload.${base.name}`);
this.placeholder = this.getPlaceholder();
this.disabledOptions = this.getDisabledOptions();
this.disabledAttributes = this.getDisabledAttributes();
this.slim = new SlimSelect({
select: this.base,
@@ -158,12 +164,18 @@ class APISelect {
this.updateQueryParams(filter);
}
// Add any already-resolved key/value pairs to the API query parameters.
for (const [key, value] of this.filterParams.entries()) {
if (isTruthy(value)) {
this.queryParams.set(key, value);
}
}
for (const filter of this.pathValues.keys()) {
this.updatePathValues(filter);
}
// TODO: Re-enable this. Disabled because `_depth` field is missing from brief responses.
// this.queryParams.set('brief', true);
this.queryParams.set('brief', true);
this.queryParams.set('limit', 0);
this.updateQueryUrl();
@@ -323,7 +335,7 @@ class APISelect {
if (!this.preSorted) {
this.preSorted = true;
}
text = `<span class="depth">${'─'.repeat(result._depth)}</span> ${text}`;
text = `<span class="depth">${'─'.repeat(result._depth)}&nbsp;</span>${text}`;
}
const data = {} as Record<string, string>;
const value = result.id.toString();
@@ -336,7 +348,7 @@ class APISelect {
data[key] = String(v);
}
// Set option to disabled if the result contains a matching key and is truthy.
if (DISABLED_ATTRIBUTES.some(key => key.toLowerCase() === k.toLowerCase())) {
if (this.disabledAttributes.some(key => key.toLowerCase() === k.toLowerCase())) {
if (typeof v === 'string' && v.toLowerCase() !== 'false') {
disabled = true;
} else if (typeof v === 'boolean' && v === true) {
@@ -547,6 +559,19 @@ class APISelect {
return disabledOptions;
}
/**
* Get this element's disabled attribute keys. For example, if `disabled-indicator` is set to
* `'_occupied'` and an API object contains `{ _occupied: true }`, the option will be disabled.
*/
private getDisabledAttributes(): string[] {
let disabled = [...DISABLED_ATTRIBUTES] as string[];
const attr = this.base.getAttribute('disabled-indicator');
if (isTruthy(attr)) {
disabled = [...disabled, attr];
}
return disabled;
}
/**
* Parse the `data-url` attribute to add any Django template variables to `pathValues` as keys
* with empty values. As those keys' corresponding form fields' values change, `pathValues` will

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},
@@ -160,6 +172,34 @@ table td > .progress {
min-width: 6rem;
}
.nav-mobile {
display: none;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 100%;
@include media-breakpoint-down(lg) {
display: flex;
}
.nav-mobile-top {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
}
.search-container {
display: flex;
width: 100%;
@include media-breakpoint-down(lg) {
display: none;
}
}
.card > .table.table-flush {
margin-bottom: 0;
overflow: hidden;
@@ -359,6 +399,8 @@ div.title-container {
nav.search {
background-color: var(--nbx-body-bg);
// Don't overtake dropdowns
z-index: 999;
form button.dropdown-toggle {
border-color: $input-border-color;
font-weight: $input-group-addon-font-weight;
@@ -374,6 +416,16 @@ nav.search {
}
}
main.layout {
display: flex;
flex-wrap: nowrap;
height: 100vh;
height: -webkit-fill-available;
max-height: 100vh;
overflow-x: auto;
overflow-y: hidden;
}
main.login-container {
display: flex;
height: calc(100vh - 4rem);
@@ -390,6 +442,18 @@ main.login-container {
}
}
.footer {
padding-top: map.get($spacers, 4);
padding-right: 0;
padding-bottom: map.get($spacers, 3);
padding-left: 0;
@include media-breakpoint-down(md) {
// Pad the bottom of the footer on mobile devices to account for mobile browser controls.
margin-bottom: 8rem;
}
}
footer.login-footer {
height: 4rem;
margin-top: auto;
@@ -425,7 +489,6 @@ h3.accordion-item-title,
h4.accordion-item-title,
h5.accordion-item-title,
h6.accordion-item-title {
// padding: 0 0.5rem;
padding: 0.25rem 0.5rem;
font-weight: $font-weight-bold;
text-transform: uppercase;
@@ -474,7 +537,7 @@ li.dropdown-item.dropdown-item-btns {
height: calc(100vh - 48px);
padding-top: 0.5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
overflow-y: auto; // Scrollable contents if viewport is shorter than content.
}
.navbar-brand {
@@ -498,13 +561,16 @@ 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;
overflow: hidden;
width: calc(100% - 4.5rem);
overflow-x: hidden;
overflow-y: auto;
@include media-breakpoint-up(md) {
margin-left: $sidebar-width;
@include media-breakpoint-down(lg) {
width: 100%;
}
div.content {
@@ -527,7 +593,7 @@ div.content-container {
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
z-index: 100; // Behind the navbar
border-right: 1px solid $border-color;
background-color: var(--nbx-sidebar-bg);
max-height: 100%;
@@ -632,6 +698,11 @@ div.content-container {
}
.search-obj-selector {
@include media-breakpoint-down(lg) {
// Limit the height and enable scrolling on mobile devices.
max-height: 75vh;
overflow-y: auto;
}
.dropdown-item,
.dropdown-header {
font-size: $font-size-sm;
@@ -807,7 +878,7 @@ div.field-group:not(:first-of-type) {
label.required {
font-weight: $font-weight-bold;
&::after {
&:after {
font-family: 'Material Design Icons';
content: '\f06C4';
font-weight: normal;
@@ -827,29 +898,34 @@ div.bulk-buttons {
display: flex;
justify-content: space-between;
margin: $spacer / 2 0;
// Each group of buttons needs to be contained separately for alignment purposes. This way, you
// can put some buttons in a group that aligns left, and other buttons in a group that aligns
// right.
& > div.bulk-button-group {
> div.bulk-button-group {
display: flex;
flex-wrap: wrap;
&:first-of-type:not(:last-of-type) {
// If there are multiple bulk button groups and this is the first, the first button in the
// group should *not* have left spacing applied, so the button group aligns with the rest
// of the page elements.
& > *:first-child {
> *:first-child {
margin-left: 0;
}
}
&:last-of-type:not(:first-of-type) {
// If there are multiple bulk button groups and this is the last, the last button in the
// group should *not* have right spacing applied, so the button group aligns with the rest
// of the page elements.
& > *:last-child {
> *:last-child {
margin-right: 0;
}
}
// However, the rest of the buttons should have spacing applied in all directions.
& > * {
> * {
margin: $spacer / 4;
}
}
@@ -963,12 +1039,12 @@ html {
&[data-netbox-path='/'] {
.content-container,
.search {
background-color: $gray-100;
background-color: $gray-100 !important;
}
&[data-netbox-color-mode='dark'] {
.content-container,
.search {
background-color: $darkest;
background-color: $darkest !important;
}
}
}

View File

@@ -71,6 +71,10 @@ $spacing-s: $input-padding-x;
border-color: currentColor;
}
}
.placeholder .depth {
// Don't show the depth indicator outside of the menu.
display: none;
}
span.placeholder > *,
span.placeholder {
line-height: $input-line-height;

View File

@@ -0,0 +1,420 @@
@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 mobile resolutions.
@include media-breakpoint-down(lg) {
transform: translateX(-$sidenav-width-closed);
+ .content-container[class] {
margin-left: 0;
}
.profile-button-container[class] {
display: block;
}
}
.profile-button-container {
display: none;
padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
}
+ .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 media-breakpoint-up(xl) {
+ .content-container {
margin-left: $sidenav-width-open;
}
}
}
@include sidenav-peek() {
.sidenav-toggle-icon {
transform: rotate(0deg);
}
}
@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 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;
@@ -175,13 +175,11 @@ $accordion-bg: transparent;
$accordion-border-color: $border-color;
$accordion-button-color: $accordion-color;
$accordion-button-bg: $accordion-bg;
$accordion-body-active-bg: rgba($blue-300, 0.2);
$accordion-button-active-bg: rgba($blue-300, 0.25);
$accordion-button-active-color: $gray-300;
$accordion-button-active-bg: shade-color($blue-300, 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;
$accordion-button-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-color}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>");
$accordion-button-active-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-active-color}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>");

File diff suppressed because it is too large Load Diff