mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
915 lines
30 KiB
SCSS
915 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;
|
|
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;
|
|
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});
|
|
}
|