mirror of
https://github.com/tabler/tabler.git
synced 2026-08-27 12:36:28 +04:00
Add sidebar user block with pinned footer zone
- three-zone sidebar: pinned brand, scrollable nav, navbar-footer with a user dropup demo - titles never wrap (nowrap + ellipsis) and icons/avatar keep size during the fold animation - btn-floating moved to the end side so it does not cover the sidebar
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Moved `btn-floating` to the end side of the screen, so it does not cover the sidebar.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a `navbar-footer` sidebar zone with a user block; the sidebar nav scrolls between the pinned brand and footer.
|
||||
+109
-66
@@ -3,6 +3,67 @@
|
||||
@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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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-collapse {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -12,56 +73,6 @@
|
||||
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)};
|
||||
--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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -146,20 +157,26 @@
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Collapsed, not display: none — titles stay in the accessibility tree.
|
||||
.nav-link-title {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
}
|
||||
// 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
|
||||
@@ -537,7 +554,6 @@ Navbar vertical
|
||||
align-items: start;
|
||||
width: var(--sidebar-width);
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
@include transition(transform $transition-time);
|
||||
|
||||
&.navbar-right,
|
||||
@@ -570,8 +586,12 @@ Navbar vertical
|
||||
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 {
|
||||
@@ -585,11 +605,35 @@ Navbar vertical
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom-pinned footer area, e.g. a user block. Sits after the
|
||||
// growing nav, separated with the navbar border.
|
||||
.navbar-footer {
|
||||
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.25rem;
|
||||
--sidebar-nav-padding-x: 0.25rem;
|
||||
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;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -633,7 +677,6 @@ Navbar vertical
|
||||
&:hover,
|
||||
&:has(:focus-visible) {
|
||||
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);
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
.btn-floating {
|
||||
--btn-floating-offset: #{$btn-floating-offset};
|
||||
position: fixed;
|
||||
inset-inline-start: var(--btn-floating-offset);
|
||||
// End side, Material-FAB style — the start side belongs to the sidebar.
|
||||
inset-inline-end: var(--btn-floating-offset);
|
||||
bottom: var(--btn-floating-offset);
|
||||
z-index: $zindex-fixed;
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
}
|
||||
|
||||
.nav-link-icon {
|
||||
flex-shrink: 0;
|
||||
width: var(--nav-link-icon-width, #{$nav-link-icon-size});
|
||||
height: var(--nav-link-icon-height, #{$nav-link-icon-size});
|
||||
margin-inline-end: var(--nav-link-icon-margin-end);
|
||||
|
||||
@@ -242,6 +242,38 @@ Group related menu items with a label. Add a `nav-section-title` list item befor
|
||||
|
||||
The label is aligned with the item text and muted, so it does not compete with the links. In the folded sidebar, the label text is replaced with a short separator line centered in the gap between the groups, and the label keeps its height, so the menu does not shift when the sidebar folds. Section labels render only in vertical navbars — a horizontal navbar hides them.
|
||||
|
||||
### Sidebar footer and user block
|
||||
|
||||
The vertical sidebar has three zones: the brand at the top, the nav as its own scroll area, and an optional `navbar-footer` pinned at the bottom. A common use is a user block with a drop-up menu:
|
||||
|
||||
```html
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-brand">...</div>
|
||||
<nav class="collapse navbar-collapse" id="sidebar-menu">...</nav>
|
||||
<div class="navbar-footer">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropup">
|
||||
<a href="#" class="nav-link" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<span class="avatar" style="background-image: url(...)"></span>
|
||||
<span class="nav-link-title">
|
||||
Paweł Kuna
|
||||
<div class="small text-secondary">UI Designer</div>
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Profile</a>
|
||||
<a class="dropdown-item" href="#">Sign out</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
```
|
||||
|
||||
The nav scrolls independently, so the footer stays visible with long menus. In the folded sidebar, the name and role collapse like link titles and the avatar is centered in the rail.
|
||||
|
||||
## Folded sidebar
|
||||
|
||||
Add `navbar-folded` to a vertical navbar to fold it into a narrow, icon-only rail. Link titles are hidden, icons are centered and submenus open as floating flyout menus next to the rail. The page content moves over automatically, so you get more space for your app.
|
||||
|
||||
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
|
||||
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Dashboard" pageHeader="Folded hover layout" pretitle="Overview" pageMenu="layout.folded-hover" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFoldedHover hideTopbar>
|
||||
<DefaultLayout title="Dashboard" pageHeader="Folded hover layout" pretitle="Overview" pageMenu="layout.folded-hover" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFoldedHover sidebarUser hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
|
||||
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Dashboard" pageHeader="Folded sidebar layout" pretitle="Overview" pageMenu="layout.folded" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFolded hideTopbar>
|
||||
<DefaultLayout title="Dashboard" pageHeader="Folded sidebar layout" pretitle="Overview" pageMenu="layout.folded" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarFolded sidebarUser hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -6,7 +6,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
|
||||
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Dashboard" pageHeader="Vertical layout" pretitle="Overview" pageLibs={['apexcharts', 'jsvectormap']} pageMenu="layout.vertical" sidebar sidebarFoldToggle hideTopbar>
|
||||
<DefaultLayout title="Dashboard" pageHeader="Vertical layout" pretitle="Overview" pageLibs={['apexcharts', 'jsvectormap']} pageMenu="layout.vertical" sidebar sidebarFoldToggle sidebarUser hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@ import NavbarToggler from './NavbarToggler.astro'
|
||||
import Logo from './Logo.astro'
|
||||
import NavbarSide from './NavbarSide.astro'
|
||||
import NavbarMenu from './NavbarMenu.astro'
|
||||
import SidebarUser from './SidebarUser.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import type { Breakpoint } from './NavbarSide.astro'
|
||||
|
||||
@@ -19,11 +20,13 @@ interface Props {
|
||||
foldedHover?: boolean | undefined
|
||||
/** renders a bottom-pinned button toggling the folded state at runtime */
|
||||
foldToggle?: boolean | undefined
|
||||
/** renders a user block in the sidebar footer */
|
||||
user?: boolean | undefined
|
||||
/** equivalent of the page-menu front matter (active menu entry), e.g. "layout.vertical" */
|
||||
pageMenu?: string | undefined
|
||||
}
|
||||
|
||||
const { dark = false, breakpoint = 'lg', end = false, transparent = false, folded = false, foldedHover = false, foldToggle = false, pageMenu } = Astro.props
|
||||
const { dark = false, breakpoint = 'lg', end = false, transparent = false, folded = false, foldedHover = false, foldToggle = false, user = false, pageMenu } = Astro.props
|
||||
|
||||
const isFolded = folded || foldedHover
|
||||
|
||||
@@ -53,6 +56,8 @@ const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expan
|
||||
{/* A folded sidebar renders flyout submenus, so the active branch is not kept open inline. */}
|
||||
<NavbarMenu autoOpen={!isFolded} class="pt-lg-3" keepOpen={!isFolded} sections pageMenu={pageMenu} />
|
||||
</nav>
|
||||
|
||||
{user && <SidebarUser />}
|
||||
</div>
|
||||
</aside>
|
||||
<!-- END SIDEBAR -->
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
// person-id=1 → people[0] (1-based index).
|
||||
import { people, personById } from '@shared/lib/people'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import { requireIndex } from '@shared/lib/array'
|
||||
|
||||
interface Props {
|
||||
personId?: number
|
||||
}
|
||||
|
||||
const { personId = 1 } = Astro.props
|
||||
|
||||
const person = requireIndex(people, personId - 1)
|
||||
---
|
||||
|
||||
<!-- BEGIN SIDEBAR USER -->
|
||||
<div class="navbar-footer d-none d-lg-block">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropup">
|
||||
<a href="#" class="nav-link" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<Avatar person={personById(personId)} />
|
||||
<span class="nav-link-title">
|
||||
{person.full_name}
|
||||
<div class="small text-secondary">{person.job_title}</div>
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="./profile.html"><Icon name="user" class="dropdown-item-icon" /> Profile</a>
|
||||
<a class="dropdown-item" href="#"><Icon name="chart-pie" class="dropdown-item-icon" /> Analytics</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="./settings.html">Settings & Privacy</a>
|
||||
<a class="dropdown-item" href="#">Help</a>
|
||||
<a class="dropdown-item" href="./sign-in.html">Sign out</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END SIDEBAR USER -->
|
||||
@@ -44,6 +44,8 @@ interface Props {
|
||||
sidebarFoldedHover?: boolean
|
||||
/** renders the runtime fold toggle button in the sidebar */
|
||||
sidebarFoldToggle?: boolean
|
||||
/** layout-sidebar-user — user block in the sidebar footer */
|
||||
sidebarUser?: boolean
|
||||
/** layout-navbar-transparent (sidebar variant) */
|
||||
navbarTransparent?: boolean
|
||||
/** layout-navbar-condensed */
|
||||
@@ -80,6 +82,7 @@ const {
|
||||
sidebarFolded,
|
||||
sidebarFoldedHover,
|
||||
sidebarFoldToggle,
|
||||
sidebarUser,
|
||||
navbarTransparent,
|
||||
navbarCondensed,
|
||||
navbarDark,
|
||||
@@ -100,7 +103,7 @@ const headerTitle = pageHeader === false ? undefined : (pageHeader ?? title)
|
||||
sidebar && (
|
||||
<Fragment>
|
||||
<Fragment set:html={'<!-- BEGIN SIDEBAR -->'} />
|
||||
<Sidebar dark={sidebarDark} end={sidebarEnd} transparent={navbarTransparent} folded={sidebarFolded} foldedHover={sidebarFoldedHover} foldToggle={sidebarFoldToggle} breakpoint="lg" pageMenu={pageMenu} />
|
||||
<Sidebar dark={sidebarDark} end={sidebarEnd} transparent={navbarTransparent} folded={sidebarFolded} foldedHover={sidebarFoldedHover} foldToggle={sidebarFoldToggle} user={sidebarUser} breakpoint="lg" pageMenu={pageMenu} />
|
||||
<Fragment set:html={'<!-- END SIDEBAR -->'} />
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user