mirror of
https://github.com/tabler/tabler.git
synced 2026-08-28 12:56:24 +04:00
- 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
595 lines
16 KiB
SCSS
595 lines
16 KiB
SCSS
@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 <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) {
|
|
$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;
|
|
}
|
|
}
|