@use 'sass:map'; @use '../config' as *; @mixin navbar-vertical-nav { .navbar-collapse { flex-direction: column; [class^='container'] { flex-direction: column; align-items: stretch; padding: 0; } .navbar-nav { // Sidebar icons carry more meaning than inline tab icons, so they get // stronger contrast than the global 50% $nav-link-icon-color. Derived // from --navbar-color, which is defined at this scope (unlike // --nav-link-color, which only exists inside .nav). --nav-link-icon-color: #{color-transparent(var(--navbar-color), 0.7)}; // Inset the list so the rounded hover/active pills never touch the // sidebar edges. The same inset and link padding apply in the folded // state, so icons keep their exact position when the sidebar folds. padding-inline: $sidebar-inset; margin-inline-start: 0; margin-inline-end: 0; .nav-link { justify-content: start; padding: 0.75rem 0.75rem; } // stylelint-disable-next-line selector-max-class .nav-item.active > .nav-link { --nav-link-icon-color: currentColor; font-weight: var(--font-weight-medium); background: var(--navbar-active-bg); } } .dropdown-menu-columns { flex-direction: column; } .dropdown-menu { position: static; min-width: 0; padding: 0; margin: 0; color: inherit; background: transparent; border: none; box-shadow: none; .dropdown-item { display: flex; width: auto; min-width: 0; // Link padding (0.75rem) + the icon column (1.75rem) — items align // with the parent link's title. padding-inline-start: 2.5rem; color: inherit; @include border-radius(var(--border-radius)); &.disabled { color: var(--disabled-color); pointer-events: none; background-color: transparent; } &.active, &:active { font-weight: var(--font-weight-medium); background: var(--navbar-active-bg); } } .dropdown-menu .dropdown-item { padding-inline-start: 4rem; } // Third-level submenu in the mobile vertical navbar — genuinely nested two // dropdown-menu levels deep. // stylelint-disable-next-line selector-max-class, selector-max-compound-selectors .dropdown-menu .dropdown-menu .dropdown-item { padding-inline-start: 5.5rem; } } .dropdown-toggle::after { margin-inline-start: auto; } // The active pill is the selection indicator — no underline or edge bar. .nav-item.active::after { content: none; } } } // Icon-only rail: hides link titles, centers icons and turns the inline submenus // flattened by navbar-vertical-nav() back into floating flyout dropdowns. @mixin navbar-vertical-folded { // Flyouts must escape the rail, so the folded sidebar itself cannot scroll. overflow: visible; // A wide brand (e.g. a full logo) must not push the container beyond the // rail width; the brand crops instead. [class^='container'] { min-width: 0; } // The brand keeps the expanded state's start padding, so the logo does not // move when the sidebar folds; anything wider than the rail crops. .navbar-brand { overflow: hidden; } // The pin button lives on the expanded sidebar only; the folded rail // unfolds by hovering (folded-hover) or via an outside control. [data-bs-toggle='sidebar-folded'] { display: none; } .navbar-collapse { // The list inset and link padding are identical in both states, so the // icons keep their exact position — only the titles collapse. .navbar-nav .nav-link { --nav-link-icon-margin-end: 0; } // Collapsed, not display: none — titles stay in the accessibility tree. .nav-link-title { width: 0; overflow: hidden; white-space: nowrap; opacity: 0; } .nav-link.dropdown-toggle::after { display: none; } // Restore the multi-column layout that navbar-vertical-nav() stacks — // it keeps long flyout menus from overflowing the viewport. .dropdown-menu-columns { flex-direction: row; } .dropdown-menu { position: absolute; inset-inline-start: 100%; top: 0; min-width: var(--dropdown-min-width); padding: var(--dropdown-padding-y) var(--dropdown-padding-x); margin: 0; color: var(--dropdown-color); background-color: var(--dropdown-bg); background-clip: padding-box; // No border — like the base .dropdown-menu, whose edge is the 1px ring // built into --shadow-dropdown. box-shadow: var(--dropdown-box-shadow); @include border-radius(var(--dropdown-border-radius)); .dropdown-item { padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x); color: var(--dropdown-link-color); @include border-radius(0); } // Undo the tree indents navbar-vertical-nav() puts on nested levels. .dropdown-menu .dropdown-item { padding-inline-start: var(--dropdown-item-padding-x); } // stylelint-disable-next-line selector-max-class, selector-max-compound-selectors .dropdown-menu .dropdown-menu .dropdown-item { padding-inline-start: var(--dropdown-item-padding-x); } } // The nav list is inset, so 100% of the .dropdown lands short of the rail // edge — compensate for the inset and add a small gap, so the flyout's // border does not sit flush against the rail's edge border. Nested // (dropend) flyouts are not inset and keep the plain 100%. .nav-item > .dropdown-menu { inset-inline-start: calc(100% + #{$sidebar-inset + 0.25rem}); } } } /** Navbar */ .navbar { --navbar-bg: var(--bg-surface); --navbar-border-width: #{$navbar-border-width}; --navbar-active-border-color: #{$navbar-active-border-color}; --navbar-active-bg: #{$navbar-light-active-bg}; --navbar-border-color: #{$navbar-border-color}; --navbar-hover-color: #{$navbar-hover-color}; align-items: stretch; min-height: $navbar-height; color: var(--navbar-color); background: var(--navbar-bg); box-shadow: inset 0 calc(-1 * var(--navbar-border-width)) 0 0 var(--navbar-border-color); @include media-print() { display: none; } .navbar-collapse & { flex-grow: 1; } &.collapsing { min-height: 0; } .dropdown-menu { position: absolute; z-index: $zindex-fixed; } .navbar-nav { --nav-link-icon-margin-end: #{$nav-link-icon-margin-end}; min-height: subtract($navbar-height, 2 * $navbar-padding-y); .nav-link { position: relative; justify-content: center; min-width: 2.5rem; min-height: 2.5rem; @include border-radius(var(--border-radius)); .badge { position: absolute; inset-inline-end: 0.5rem; top: 0.5rem; /* rtl:ignore */ transform: translate(calc(var(--dir) * 50%), -50%); } } } } .navbar-expand { @each $breakpoint in map.keys($grid-breakpoints) { $next: breakpoint-next($breakpoint, $grid-breakpoints); $infix: breakpoint-infix($next, $grid-breakpoints); &#{$infix} { @include media-breakpoint-down(breakpoint-next($breakpoint)) { @include navbar-vertical-nav(); } @include media-breakpoint-up($next) { .navbar-collapse { flex: 1 1 auto; width: auto; } .nav-item.active { position: relative; .nav-link { color: var(--navbar-active-color); } &::after { position: absolute; inset-inline-start: 0; inset-inline-end: 0; bottom: -0.25rem; content: ''; border: 0 var(--border-style) var(--navbar-active-border-color); border-bottom-width: 2px; } } &.navbar-vertical { box-shadow: inset calc(-1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color); &.navbar-right, &.navbar-end { box-shadow: inset calc(1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color); } } &.navbar-vertical { ~ .navbar, ~ .page-wrapper { margin-inline-start: var(--sidebar-width); } } &.navbar-vertical.navbar-right, &.navbar-vertical.navbar-end { ~ .navbar, ~ .page-wrapper { margin-inline-start: 0; margin-inline-end: var(--sidebar-width); } } } } } } /** Navbar brand */ .navbar-brand { display: inline-flex; gap: $spacer-2; align-items: center; margin: 0; font-weight: $navbar-brand-font-weight; line-height: 1; } .navbar-brand-image { width: auto; height: $navbar-brand-image-height; } .navbar-brand-gray { --navbar-logo-color: var(--tertiary); opacity: 0.5; } /** Navbar toggler */ .navbar-toggler { position: relative; display: flex; align-items: center; justify-content: center; width: $navbar-brand-image-height; height: $navbar-brand-image-height; border: 0; } .navbar-toggler-icon { position: relative; width: 1.25em; height: 2px; background: currentColor; @include border-radius(var(--border-radius-pill)); @include transition(top $navbar-toggler-animation-time $navbar-toggler-animation-time, bottom $navbar-toggler-animation-time $navbar-toggler-animation-time, transform $navbar-toggler-animation-time, opacity 0s $navbar-toggler-animation-time); &::before, &::after { position: absolute; inset-inline-start: 0; display: block; width: inherit; height: inherit; content: ''; background: inherit; border-radius: inherit; @include transition(inherit); } &::before { top: -0.45em; } &::after { bottom: -0.45em; } .navbar-toggler[aria-expanded='true'] & { transform: rotate(45deg); @include transition(top $transition-time, bottom $transition-time, transform $transition-time $transition-time, opacity 0s $transition-time); &::before { top: 0; transform: rotate(-90deg); } &::after { bottom: 0; opacity: 0; } } } /** Navbar transparent */ .navbar-transparent { --navbar-border-color: transparent !important; background: transparent !important; } /** Navbar nav */ .navbar-nav { --nav-link-hover-bg: #{$navbar-nav-link-hover-bg}; --nav-link-icon-color: #{$nav-link-icon-color}; --nav-link-hover-icon-color: #{$nav-link-hover-icon-color}; align-items: stretch; padding: 0; margin: 0; .nav-item { display: flex; flex-direction: column; justify-content: center; } } /** Navbar side */ .navbar-side { display: flex; flex-direction: row; align-items: center; justify-content: space-around; margin: 0; } /** Navbar vertical */ @if $enable-navbar-vertical { // Folded sidebar: every geometry consumer (aside width, page paddings, // page-wrapper/navbar margins) reads --sidebar-width, so flipping this one // variable re-lays-out the whole page. Two routes to the same state: // a .navbar-folded/.navbar-folded-hover class on the aside (static markup), // or data-bs-sidebar="folded"/"folded-hover" on (set pre-render from // localStorage by tabler-theme.js, toggled by [data-bs-toggle="sidebar-folded"]). // html[…] beats the :root declaration of --sidebar-width on the same element; // a bare :where()/[…] selector would lose to it and never take effect. html[data-bs-sidebar^='folded'] { --sidebar-width: var(--sidebar-folded-width); } .navbar-vertical:is(.navbar-folded, .navbar-folded-hover) { --sidebar-width: var(--sidebar-folded-width); // The class sits on the aside, so its layout siblings need their own copy. ~ .page, ~ .page-wrapper, ~ .navbar { --sidebar-width: var(--sidebar-folded-width); } } .navbar-vertical { &.navbar-expand { @each $breakpoint in map.keys($grid-breakpoints) { $next: breakpoint-next($breakpoint, $grid-breakpoints); $infix: breakpoint-infix($next, $grid-breakpoints); &#{$infix} { @include media-breakpoint-up($next) { position: fixed; inset-inline-start: 0; top: 0; bottom: 0; z-index: $zindex-fixed; align-items: start; width: var(--sidebar-width); padding: 0; overflow-y: scroll; @include transition(transform $transition-time); &.navbar-right, &.navbar-end { inset-inline-start: auto; inset-inline-end: 0; } // Logo on the start side, room for an action (e.g. the pin // toggle) on the end side. The inline padding centers the logo // mark in the folded rail and is identical in both states, so the // logo mark and the nav icons share one center axis and nothing // moves when the sidebar folds. .navbar-brand { justify-content: space-between; padding: (($navbar-height - $navbar-brand-image-height) * 0.5) (($sidebar-folded-width - $navbar-brand-image-height) * 0.5); margin-inline-end: 0; } // The fold toggle keeps its layout slot (no jumping) but only // becomes visible while the sidebar is hovered or focused. [data-bs-toggle='sidebar-folded'] { opacity: 0; @include transition(opacity $transition-time); } &:is(:hover, :focus-within) [data-bs-toggle='sidebar-folded'] { opacity: 1; } .navbar-collapse { align-items: stretch; } .navbar-nav { flex-grow: 1; flex-direction: column; min-height: auto; .nav-link { padding-top: 0.5rem; padding-bottom: 0.5rem; } } > [class^='container'] { flex-direction: column; align-items: stretch; justify-content: start; min-height: 100%; padding: 0; } ~ .page { padding-inline-start: var(--sidebar-width); [class^='container'] { padding-inline-start: 1.5rem; padding-inline-end: 1.5rem; } } &.navbar-right ~ .page, &.navbar-end ~ .page { padding-inline-start: 0; padding-inline-end: var(--sidebar-width); } @include navbar-vertical-nav(); // Folded state, both routes; the hover variant folds only while // neither hovered nor focused from the keyboard. &:is(.navbar-folded, .navbar-folded-hover:not(:hover, :focus-within)), :where([data-bs-sidebar='folded']) &, :where([data-bs-sidebar='folded-hover']) &:not(:hover, :focus-within) { @include navbar-vertical-folded(); } &.navbar-folded-hover, :where([data-bs-sidebar='folded-hover']) & { @include transition(transform $transition-time, width $transition-time); .nav-link-title { @include transition(opacity $transition-time); } // Temporarily unfolds as an overlay: siblings keep the folded // --sidebar-width, so page content does not reflow. &:is(:hover, :focus-within) { width: $sidebar-width; overflow-y: auto; box-shadow: inset calc(-1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color), var(--shadow-lg); } } } } } } } @include media-print() { .navbar-vertical ~ .page { padding-inline-start: 0; padding-inline-end: 0; } } } .navbar-overlap { &::after { position: absolute; inset-inline-start: 0; inset-inline-end: 0; top: 100%; z-index: -1; height: $navbar-overlap-height; content: ''; background: inherit; box-shadow: inherit; } }