Add folded sidebar with flyout menus, pin toggle and light default

- navbar-folded / navbar-folded-hover with flyout submenus and persisted pin toggle
- sidebar refresh: light by default, 16rem wide, rounded inset pills, zero-jump fold
- demo pages, Theme Settings section, docs and btn-action usage rule
This commit is contained in:
codecalm
2026-08-20 22:27:53 +02:00
parent 3069c5193b
commit 6481499ccd
29 changed files with 637 additions and 44 deletions
+6 -3
View File
@@ -1578,7 +1578,7 @@ $navbar-light-brand-color: var(--body-color) !default;
$navbar-light-active-color: var(--body-color) !default;
$navbar-light-hover-color: var(--body-color) !default;
$navbar-light-disabled-color: var(--disabled-color) !default;
$navbar-light-active-bg: rgba(0, 0, 0, 0.2) !default;
$navbar-light-active-bg: color-transparent(var(--primary), 0.08) !default;
// Spelled with the literal prefix: this value is url-encoded into the svg data
// uri below, so the build-time prefixing in .build/build-css.ts never sees a
// `var()` here to rewrite (it arrives as `var%28--body-color%29`).
@@ -1589,6 +1589,7 @@ $navbar-light-brand-hover-color: $navbar-light-active-color !default;
$navbar-dark-color: rgba($white, $text-secondary-opacity) !default;
$navbar-dark-hover-color: rgba($white, 0.75) !default;
$navbar-dark-active-bg: rgba($white, 0.06) !default;
$navbar-dark-brand-color: $white !default;
$navbar-dark-active-color: $white !default;
$navbar-dark-disabled-color: var(--disabled-color) !default;
@@ -1612,13 +1613,15 @@ $navbar-toggler-transition: box-shadow 0.15s ease-in-out !default;
$navbar-toggler-animation-time: 0.2s !default;
$navbar-overlap-height: 9rem !default;
$navbar-nav-link-hover-bg: color-transparent(var(--nav-link-color), 0.04) !default;
$navbar-nav-link-hover-bg: color-transparent(var(--nav-link-color), 0.06) !default;
$navbar-active-border-color: var(--primary) !default;
// scss-docs-end navbar-variables
// Sidebar
$sidebar-width: 15rem !default;
$sidebar-width: 16rem !default;
$sidebar-folded-width: 4rem !default;
$sidebar-inset: 0.5rem !default;
// Page
$page-title-font-size: var(--font-size-h2) !default;
+1
View File
@@ -274,6 +274,7 @@
--navbar-hover-color: #{$navbar-dark-hover-color};
--navbar-disabled-color: #{$navbar-dark-disabled-color};
--navbar-active-color: #{$navbar-dark-active-color};
--navbar-active-bg: #{$navbar-dark-active-bg};
--navbar-brand-color: #{$navbar-dark-brand-color};
--navbar-brand-hover-color: #{$navbar-dark-brand-hover-color};
--navbar-toggler-border-color: #{$navbar-dark-toggler-border-color};
+190 -15
View File
@@ -13,12 +13,28 @@
}
.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.5rem calc(#{$container-padding-x} / 2);
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);
}
}
@@ -40,8 +56,11 @@
display: flex;
width: auto;
min-width: 0;
padding-inline-start: add(calc(#{$container-padding-x} / 2), 1.75rem);
// 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);
@@ -51,19 +70,20 @@
&.active,
&:active {
font-weight: var(--font-weight-medium);
background: var(--navbar-active-bg);
}
}
.dropdown-menu .dropdown-item {
padding-inline-start: add(calc(#{$container-padding-x} / 2), 3.25rem);
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: add(calc(#{$container-padding-x} / 2), 4.75rem);
padding-inline-start: 5.5rem;
}
}
@@ -71,12 +91,100 @@
margin-inline-start: auto;
}
// The active pill is the selection indicator no underline or edge bar.
.nav-item.active::after {
inset-inline-end: auto;
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;
bottom: 0;
border-inline-start-width: 3px;
border-bottom-width: 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});
}
}
}
@@ -183,7 +291,7 @@ Navbar
&.navbar-vertical {
~ .navbar,
~ .page-wrapper {
margin-inline-start: $sidebar-width;
margin-inline-start: var(--sidebar-width);
}
}
@@ -192,7 +300,7 @@ Navbar
~ .navbar,
~ .page-wrapper {
margin-inline-start: 0;
margin-inline-end: $sidebar-width;
margin-inline-end: var(--sidebar-width);
}
}
}
@@ -321,6 +429,29 @@ Navbar side
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);
}
.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) {
@@ -335,7 +466,7 @@ Navbar vertical
bottom: 0;
z-index: $zindex-fixed;
align-items: start;
width: $sidebar-width;
width: var(--sidebar-width);
padding: 0;
overflow-y: scroll;
@include transition(transform $transition-time);
@@ -346,9 +477,26 @@ Navbar vertical
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: center;
padding: (($navbar-height - $navbar-brand-image-height) * 0.5) 0;
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 {
@@ -375,7 +523,7 @@ Navbar vertical
}
~ .page {
padding-inline-start: $sidebar-width;
padding-inline-start: var(--sidebar-width);
[class^='container'] {
padding-inline-start: 1.5rem;
@@ -386,10 +534,37 @@ Navbar vertical
&.navbar-right ~ .page,
&.navbar-end ~ .page {
padding-inline-start: 0;
padding-inline-end: $sidebar-width;
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);
}
}
}
}
}
+3
View File
@@ -63,6 +63,9 @@
--page-padding: #{$page-padding};
--page-padding-y: #{$page-padding-y};
--sidebar-width: #{$sidebar-width};
--sidebar-folded-width: #{$sidebar-folded-width};
@include media-breakpoint-down($cards-grid-breakpoint) {
--page-padding: #{$page-padding-sm};
}
+1 -1
View File
@@ -100,7 +100,7 @@
width: $nav-link-icon-size;
height: $nav-link-icon-size;
margin-inline-end: var(--nav-link-icon-margin-end);
color: $nav-link-icon-color;
color: var(--nav-link-icon-color, #{$nav-link-icon-color});
svg {
display: block;