Files
2026-10-01 11:34:12 +02:00

917 lines
30 KiB
SCSS

@use 'sass:map';
@use '../config' as *;
@mixin navbar-vertical-nav {
// Applies to every navbar-nav in the vertical navbar — the menu inside
// .navbar-collapse and e.g. a .navbar-footer list outside of it.
.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)};
--nav-link-icon-width: #{$sidebar-nav-icon-width};
--nav-link-icon-height: #{$sidebar-nav-icon-height};
--sidebar-inset: #{$sidebar-inset};
--sidebar-nav-padding-y: #{$sidebar-nav-padding-y};
--sidebar-nav-padding-x: #{$sidebar-nav-padding-x};
--sidebar-nav-indent: #{$sidebar-nav-indent};
--sidebar-section-title-font-size: #{$sidebar-section-title-font-size};
--sidebar-section-title-margin-top: #{$sidebar-section-title-margin-top};
// 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: var(--sidebar-inset);
margin-inline-start: 0;
margin-inline-end: 0;
.nav-link {
justify-content: start;
padding: var(--sidebar-nav-padding-y) var(--sidebar-nav-padding-x);
// In a vertical list the badge sits at the end of the row.
.badge:not(:empty) {
margin-inline-start: auto;
}
}
// One line, ellipsized — a title never wraps, so rows keep their height
// while the folding sidebar animates its width.
.nav-link-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 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);
}
// A group that only holds the current page is emphasized, not filled: the
// pill marks the page itself, and painting the group with the same pill
// reads as two selections. The folded rail puts it back — there the titles
// are collapsed and the submenu is a flyout, so the pill is the only thing
// left that can point at the group.
// stylelint-disable-next-line selector-max-class
.nav-item.active > .nav-link.dropdown-toggle {
background: transparent;
}
// Group label — aligned with the item text, muted like the icons.
.nav-section-title {
display: block;
padding: 0.25rem var(--sidebar-nav-padding-x);
margin-top: var(--sidebar-section-title-margin-top);
font-size: var(--sidebar-section-title-font-size);
font-weight: var(--font-weight-medium);
color: #{color-transparent(var(--navbar-color), 0.7)};
text-transform: uppercase;
letter-spacing: 0.04em;
&:first-child {
margin-top: 0;
}
}
}
// .navbar-side wraps the menu's trailing group (a Customize link, a help
// link) in its own .navbar-nav. The base rule lays it out as a row for the
// horizontal navbar; here it stacks like the menu, never grows, and the
// auto margin pins it to the bottom of the scroll area when the menu is
// shorter than the sidebar.
.navbar-side {
flex-grow: 0;
flex-direction: column;
align-items: stretch;
justify-content: start;
padding-bottom: 0.25rem;
margin-top: auto;
.navbar-nav {
flex-grow: 0;
}
}
.navbar-collapse {
flex-direction: column;
[class^='container'] {
flex-direction: column;
align-items: stretch;
padding: 0;
}
.dropdown-menu-columns {
flex-direction: column;
}
.dropdown-menu {
position: static;
z-index: auto;
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 + the icon column — items align with the parent
// link's title, whatever the icon size and paddings are set to.
padding-inline-start: calc(var(--sidebar-nav-padding-x) + var(--nav-link-icon-width) + var(--nav-link-icon-margin-end));
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: calc(var(--sidebar-nav-padding-x) + var(--nav-link-icon-width) + var(--nav-link-icon-margin-end) + var(--sidebar-nav-indent));
}
// 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: calc(var(--sidebar-nav-padding-x) + var(--nav-link-icon-width) + var(--nav-link-icon-margin-end) + 2 * var(--sidebar-nav-indent));
}
}
.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;
}
// The list inset and link padding are identical in both states, so the
// icons keep their exact position — only the titles collapse. Aside-wide
// (not .navbar-collapse-scoped), so the footer outside the collapse folds
// the same way.
.navbar-nav .nav-link {
--nav-link-icon-margin-end: 0;
}
// Only icons are readable in the rail, so the group holding the current page
// takes the pill back — the emphasis the expanded sidebar carries on the
// title alone cannot be seen here. Scoped through .navbar-nav to match the
// specificity of the rule it overrides, which wins on source order.
// stylelint-disable-next-line selector-max-class
.navbar-nav .nav-item.active > .nav-link.dropdown-toggle {
background: var(--navbar-active-bg);
}
// Collapsed, not display: none — titles stay in the accessibility tree.
.nav-link-title {
width: 0;
overflow: hidden;
white-space: nowrap;
opacity: 0;
}
.navbar-collapse {
// Flyouts must escape the nav's scroll area; the folded rail therefore
// cannot scroll (documented — the hover variant is the escape hatch).
overflow: visible;
// The label text cannot fit the rail — indent it away (stays in the
// accessibility tree, line box keeps the element height stable) and show
// a short separator instead, centered within the WHOLE gap between the
// groups: the box's 50% is shifted up by half the label's top margin.
.nav-section-title {
position: relative;
overflow: hidden;
text-indent: -9999px;
white-space: nowrap;
&::before {
position: absolute;
inset-inline-start: calc(50% - 0.625rem);
top: calc(50% - var(--sidebar-section-title-margin-top) / 2);
width: 1.25rem;
height: var(--border-width);
content: '';
background: #{color-transparent(var(--navbar-color), 0.2)};
}
// Nothing above the first group to separate — no line, and no margin
// shift to compensate for either.
&:first-child::before {
content: none;
}
}
.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;
z-index: $zindex-fixed;
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% + var(--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);
// Section labels only make sense in a vertical list — the vertical
// navbar styles re-enable them.
.nav-section-title {
display: none;
}
.nav-link {
position: relative;
justify-content: center;
min-width: 2.5rem;
min-height: 2.5rem;
@include border-radius(var(--border-radius));
// A badge with text is part of the link's row, after the title. Only an
// empty badge is a notification dot pinned to the link's corner, so the
// absolute positioning is limited to `:empty` — a text badge positioned
// that way overlapped the title and, in a stacked list, hung past the
// link's edge and made the page scroll sideways.
.badge {
margin-inline-start: $spacer-2;
&:empty {
position: absolute;
inset-inline-end: 0.5rem;
top: 0.5rem;
margin-inline-start: 0;
/* rtl:ignore */
transform: translate(calc(var(--dir) * 50%), -50%);
}
}
}
}
}
// The vertical navbar is a vertical list at every width: the stacked mobile
// menu below its breakpoint, the sidebar from it up. Emitted once instead of
// inside every breakpoint of both loops; it stays ahead of them in source
// order, so the per-breakpoint rules below still override it.
@if $enable-navbar-vertical {
.navbar-vertical[class*='navbar-expand'] {
@include navbar-vertical-nav();
}
}
.navbar-expand {
@each $breakpoint in map.keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
&#{$infix} {
@if $next {
@include media-breakpoint-down($next) {
@include navbar-vertical-nav();
// Below the breakpoint the vertical navbar is a top bar row: toggler,
// brand, and whatever trails them (a user block, or nothing). Auto
// margins keep the brand centered either way — space-between alone
// would push it to the far edge when nothing follows it.
&.navbar-vertical .navbar-brand {
margin-inline: auto;
}
// In the stacked mobile menu nothing pins the trailing group to the
// bottom, so a small gap marks it as a separate part of the menu.
&.navbar-vertical .navbar-side {
margin-top: $sidebar-inset;
}
// Below the breakpoint the navbar-footer sits in the top bar row:
// the avatar alone, with its menu dropping down instead of up.
.navbar-footer {
.nav-link-title {
display: none;
}
.dropup .dropdown-menu {
inset-inline-start: auto;
inset-inline-end: 0;
top: 100%;
bottom: auto;
}
}
}
}
@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: calc(var(--sidebar-width) + var(--sidebar-gap, 0px));
}
}
&.navbar-vertical.navbar-right,
&.navbar-vertical.navbar-end {
~ .navbar,
~ .page-wrapper {
margin-inline-start: 0;
margin-inline-end: calc(var(--sidebar-width) + var(--sidebar-gap, 0px));
}
}
}
// In the first, tightest segment after a horizontal navbar expands there
// is no room for icons — hide them there, derived from the navbar's own
// navbar-expand-* breakpoint, so the markup needs no display utilities.
// The vertical navbar always shows its icons.
@if $next and breakpoint-next($next, $grid-breakpoints) {
@include media-breakpoint-only($next) {
&:not(.navbar-vertical) .navbar-nav .nav-link-icon {
display: none;
}
}
}
}
}
}
// 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;
// The button's padding leaves less room than the bar is wide — without
// this the middle bar gets flex-squeezed while the pseudo bars keep their
// inherited width, and the open-state X loses a leg.
flex-shrink: 0;
width: 1.25em;
height: 2px;
background: currentColor;
mask-image: none; // the bars below replace the masked hamburger from bootstrap/_navbar
@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
// The group at the end of a navbar: a row on the end side of a horizontal
// navbar. In a vertical navbar the same class pins the group to the bottom
// of the menu — see navbar-vertical-nav().
.navbar-side {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
margin: 0;
}
// Navbar sticky
// Runtime route only (data-bs-navbar="sticky" on <html>); static markup wraps
// the navbar in its own .sticky-top. Just the top bar sticks — on the two-row
// navbar the menu row scrolls away, so the sticky strip stays one row high.
html[data-bs-navbar='sticky'] .page > .navbar:not(.navbar-vertical) {
position: sticky;
top: 0;
z-index: $zindex-sticky;
}
// Navbar primary
// Runtime route only (data-bs-navbar-theme="primary" on <html>): the primary
// color as the navbar surface. The text palette comes from the dark color
// mode, which _extends.scss applies to the same selector; static markup gets
// the same look from .bg-primary with data-bs-theme="dark" on the navbar.
html[data-bs-navbar-theme='primary'] .page > .navbar {
--navbar-bg: var(--primary);
--navbar-border-color: transparent;
}
// The menu row of the two-row navbar, which is not a direct child of .page.
html[data-bs-navbar-theme='primary'] .page > [class*='navbar-expand'] .navbar {
--navbar-bg: var(--primary);
--navbar-border-color: transparent;
}
// 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 <html> (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);
// The sidebar is rendered for the expanded state, so the active submenu can
// arrive open (.show). Folded, it would paint as a flyout until sidebar.ts
// closes it on DOMContentLoaded — a flicker on every reload. Menus opened by
// the Dropdown plugin carry a data-bs-popper attribute, so only the
// server-rendered ones hide.
.navbar-vertical .dropdown-menu.show:not([data-bs-popper]):not([data-tblr-popper]) {
display: none;
}
}
.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 position (data-bs-navbar-position on <html>). A page can render both
// navbars — the vertical aside first, the horizontal navbar(s) after — and
// these rules show one of them. Horizontal is the default and writes no
// attribute, so its rule matches on the attribute's absence.
//
// Both rules are guarded with :has() so they only fire on a page that really
// renders the other navbar: a page with a single navbar keeps it, whatever a
// stale stored choice says, and can never end up with no navigation at all.
html:not([data-bs-navbar-position='vertical']) .page:has(> [class*='navbar-expand']:not(.navbar-vertical)) {
> .navbar-vertical {
display: none;
}
// The hidden aside must leave no gap: its siblings offset themselves by
// --sidebar-width, and this declaration sits on the same elements as the
// folded copy above, so it wins by coming later.
// A length, not a bare 0: the offset is added up in calc(). A floating
// navbar keeps its own gap from the viewport edge.
> .navbar,
> .page-wrapper {
--sidebar-width: 0px;
--sidebar-gap: var(--navbar-margin, 0px);
}
}
html[data-bs-navbar-position='vertical'] .page:has(> .navbar-vertical) {
> [class*='navbar-expand']:not(.navbar-vertical) {
display: none;
}
}
.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;
@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 {
--sidebar-brand-padding-x: #{$sidebar-brand-padding-x};
justify-content: space-between;
padding: (($navbar-height - $navbar-brand-image-height) * 0.5) var(--sidebar-brand-padding-x);
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);
}
&:hover [data-bs-toggle='sidebar-folded'],
&:has(:focus-visible) [data-bs-toggle='sidebar-folded'] {
opacity: 1;
}
// The nav is the sidebar's scroll area — the brand above and the
// footer below stay pinned.
.navbar-collapse {
align-items: stretch;
min-height: 0;
overflow-y: auto;
}
.navbar-nav {
flex-grow: 1;
flex-direction: column;
min-height: auto;
}
// Bottom-pinned footer area, e.g. a user block. Sits after the
// growing nav, separated with the navbar border.
.navbar-footer {
// In the markup the footer sits before the collapse (so it
// stays in the mobile top bar row); order moves it to the
// bottom of the desktop column.
order: 1;
padding-block: $sidebar-inset;
margin-top: auto;
border-top: var(--navbar-border-width) var(--border-style) var(--navbar-border-color);
// Tighter link padding than the menu — the avatar brings its
// own bulk. Overrides the variables the nav-link padding reads,
// so it wins regardless of rule order.
.navbar-nav {
--sidebar-nav-padding-y: 0.5rem;
--sidebar-nav-padding-x: 0.5rem;
flex-grow: 0;
}
.nav-link .avatar {
// Never squeezed by the appearing title during the hover
// unfold animation.
flex-shrink: 0;
margin-inline-end: var(--nav-link-icon-margin-end);
}
}
> [class^='container'] {
flex-direction: column;
align-items: stretch;
justify-content: start;
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);
}
// Folded state, both routes; the hover variant folds only while
// not hovered and not holding visible keyboard focus. Plain
// :focus-within would keep the sidebar stuck open after a mouse
// click, because the clicked link keeps focus.
// One :is() instead of a selector list — the mixin's rules are long
// and a list would repeat each of them three times.
&:is(.navbar-folded, .navbar-folded-hover:not(:hover):not(:has(:focus-visible)), [data-bs-sidebar='folded'] *, [data-bs-sidebar='folded-hover'] :not(:hover):not(:has(:focus-visible))) {
@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.
&:hover,
&:has(:focus-visible) {
width: $sidebar-width;
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;
}
}
// Navbar floating
// The navbar keeps a gap from the edges around it, like .offcanvas-floating,
// with an outline on every side instead of the single border line.
//
// Two routes to the same state: the .navbar-floating class on the navbar
// (static markup), or data-bs-navbar-style="floating" on <html> (the theme
// settings), which covers whichever navigation the page shows — the menu row
// of the two-row navbar included, which inherits the variables from its
// navbar-expand-* wrapper.
//
// The variables exist only from the navbar-expand-* breakpoint up, so below it
// every fallback applies and the navbar stays attached.
$navbar-floating-attr: "html[data-bs-navbar-style='floating'] .page >";
.navbar-floating,
#{$navbar-floating-attr} .navbar,
#{$navbar-floating-attr} [class*='navbar-expand'] .navbar {
margin: var(--navbar-margin, 0);
outline: var(--navbar-outline-width, 0) solid var(--navbar-border-color);
outline-offset: calc(-1 * var(--navbar-outline-width, 0px));
@include border-radius(var(--navbar-border-radius, 0));
}
#{$navbar-floating-attr} [class*='navbar-expand'] .navbar {
margin-top: 0;
}
@each $breakpoint in map.keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
@include media-breakpoint-up($next) {
.navbar-floating.navbar-expand#{$infix},
#{$navbar-floating-attr} .navbar-expand#{$infix} {
--navbar-margin: #{$navbar-floating-margin};
--navbar-border-radius: #{$navbar-floating-border-radius};
--navbar-outline-width: var(--navbar-border-width);
}
}
}
// The page next to a floating sidebar moves over by the sidebar's two margins,
// and a floating navbar next to any sidebar keeps its own gap from it. The
// sibling offsets read --sidebar-gap only from the sidebar's breakpoint up.
.navbar-vertical ~ .navbar-floating {
--sidebar-gap: var(--navbar-margin, 0px);
}
.navbar-floating.navbar-vertical ~ :is(.navbar, .page-wrapper),
#{$navbar-floating-attr} .navbar-vertical ~ :is(.navbar, .page-wrapper) {
--sidebar-gap: calc(2 * #{$navbar-floating-margin});
}