mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add folded sidebar with flyout menus, pin toggle and light default (#2905)
This commit is contained in:
1 parent
80db0db3fd
commit
4f6e99b715
37 files changed
+980
-64
No files matched your search
@@ -64,6 +64,7 @@ Task-specific instructions live in `.agents/skills/<name>/SKILL.md` — read the
|
||||
- Follow Tabler's CSS custom properties pattern: `--component-property`.
|
||||
- Cards: `card` for containers, `card-body` for content, `card-header` / `card-title` for headers.
|
||||
- Buttons: `btn` for all buttons; `btn-primary` for primary actions; plain `btn` for secondary actions (do not use `btn-outline-secondary`); `btn-sm` for small buttons; `w-100` for full width.
|
||||
- `btn-action` is always combined with `btn` (typically `btn btn-action btn-sm` for icon actions in headers and toolbars) — never use `btn-action` on its own.
|
||||
- Forms: `form-control` for inputs, `form-label` for labels, `form-check` for checkboxes/radios, `form-select` for dropdowns.
|
||||
- Layout: Bootstrap grid (`row`, `col-*`), `container-xl` for main containers, `page-wrapper` / `page-body` for page structure.
|
||||
- Badges: plain `badge` class; do not use `badge-outline` or `badge-primary`; do not change badge text color.
|
||||
|
||||
@@ -84,6 +84,8 @@ Use the list wrappers instead of margin utilities on each child:
|
||||
|
||||
The pluralised class names (`.badges-list`, `.tags-list`) are deprecated aliases of `.badge-list` / `.tag-list`. Do not add `me-2`/`mb-2` to individual items.
|
||||
|
||||
Icon action buttons: `btn-action` is always combined with `btn` (`btn btn-action`, plus `btn-sm` when small) — never use `btn-action` on its own.
|
||||
|
||||
## 6. Data and repetition
|
||||
|
||||
- Loop over `shared/data/*.json` (`site.json` colors, `people.json`, `flags.json`) instead of pasting 20 near-identical blocks.
|
||||
|
||||
@@ -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 folded sidebar (`navbar-folded`, `navbar-folded-hover`) with flyout submenus, a pin toggle and a light `16rem` sidebar by default.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed the `navbar-toggler` icon: the middle bar was flex-squeezed, breaking the open-state X and shortening the hamburger.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added `nav-section-title` group labels to the vertical sidebar, shown as short separators in the folded state.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Dropdown } from './bootstrap'
|
||||
|
||||
/*
|
||||
Folded sidebar toggle
|
||||
*/
|
||||
// js-docs-start sidebar-folded-toggle
|
||||
const syncSidebarToggles = (folded: boolean): void => {
|
||||
for (const toggle of document.querySelectorAll('[data-bs-toggle="sidebar-folded"]')) {
|
||||
toggle.setAttribute('aria-pressed', String(folded))
|
||||
}
|
||||
}
|
||||
|
||||
const sidebarIsFolded = (): boolean => (document.documentElement.getAttribute('data-bs-sidebar') ?? '').startsWith('folded') || !!document.querySelector('.navbar-vertical.navbar-folded, .navbar-vertical.navbar-folded-hover')
|
||||
|
||||
const hideSidebarDropdowns = (except?: Element | null): void => {
|
||||
for (const toggle of document.querySelectorAll<HTMLElement>('.navbar-vertical [data-bs-toggle="dropdown"][aria-expanded="true"]')) {
|
||||
if (toggle !== except) {
|
||||
;(Dropdown.getInstance(toggle) as InstanceType<typeof Dropdown> | null)?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A sidebar rendered for the expanded state keeps its submenus open on outside
|
||||
// clicks (data-bs-auto-close="false"). Folded flyouts must not: close them on
|
||||
// any click outside their menu, and close the other flyouts when one opens.
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
if (!sidebarIsFolded()) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target as Element
|
||||
if (target.closest('.navbar-vertical .dropdown-menu')) {
|
||||
return
|
||||
}
|
||||
|
||||
hideSidebarDropdowns(target.closest('.navbar-vertical [data-bs-toggle="dropdown"]'))
|
||||
})
|
||||
|
||||
// When a hover-expanding sidebar collapses back to the rail, close its open
|
||||
// submenus — an inline submenu left open would flash as a floating flyout.
|
||||
// Static folded rails are excluded: crossing the small gap between the rail
|
||||
// and an open flyout must not close it. mouseleave does not bubble, so the
|
||||
// listener runs in the capture phase.
|
||||
document.addEventListener(
|
||||
'mouseleave',
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as Element
|
||||
if (!(target instanceof Element) || !target.matches('.navbar-vertical')) {
|
||||
return
|
||||
}
|
||||
|
||||
const hoverVariant = document.documentElement.getAttribute('data-bs-sidebar') === 'folded-hover' || target.classList.contains('navbar-folded-hover')
|
||||
if (hoverVariant) {
|
||||
hideSidebarDropdowns()
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
// The folded state may come pre-render from localStorage (tabler-theme.js), so
|
||||
// align the toggle buttons with it once the DOM is available. Server-rendered
|
||||
// open submenus are also closed, so they don't hang as open flyouts.
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const folded = (document.documentElement.getAttribute('data-bs-sidebar') ?? '').startsWith('folded')
|
||||
syncSidebarToggles(folded)
|
||||
|
||||
if (folded) {
|
||||
for (const menu of document.querySelectorAll('.navbar-vertical .dropdown-menu.show')) {
|
||||
menu.classList.remove('show')
|
||||
menu.parentElement?.querySelector('[data-bs-toggle="dropdown"]')?.setAttribute('aria-expanded', 'false')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
const trigger = (event.target as Element).closest('[data-bs-toggle="sidebar-folded"]')
|
||||
if (!trigger) {
|
||||
return
|
||||
}
|
||||
|
||||
const html = document.documentElement
|
||||
const willFold = !(html.getAttribute('data-bs-sidebar') ?? '').startsWith('folded')
|
||||
|
||||
// Unpinning folds to the hover variant: the rail expands on hover, where
|
||||
// the pin button becomes visible again and can pin the sidebar back open.
|
||||
if (willFold) {
|
||||
html.setAttribute('data-bs-sidebar', 'folded-hover')
|
||||
} else {
|
||||
html.removeAttribute('data-bs-sidebar')
|
||||
}
|
||||
|
||||
localStorage.setItem('tabler-sidebar', willFold ? 'folded-hover' : 'default')
|
||||
syncSidebarToggles(willFold)
|
||||
|
||||
// Close open sidebar dropdowns so an inline submenu doesn't turn into a
|
||||
// stuck flyout (and vice versa) when the layout mode changes.
|
||||
hideSidebarDropdowns()
|
||||
|
||||
document.dispatchEvent(new CustomEvent('tabler:sidebar-folded', { detail: { folded: willFold } }))
|
||||
})
|
||||
// js-docs-end sidebar-folded-toggle
|
||||
@@ -9,6 +9,7 @@ interface ThemeConfig {
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
'sidebar': string
|
||||
}
|
||||
|
||||
const themeConfig: ThemeConfig = {
|
||||
@@ -17,6 +18,7 @@ const themeConfig: ThemeConfig = {
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
'sidebar': 'default',
|
||||
}
|
||||
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
|
||||
@@ -2,6 +2,7 @@ import './src/autosize'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
import './src/dropdown'
|
||||
import './src/sidebar'
|
||||
import './src/tooltip'
|
||||
import './src/popover'
|
||||
import './src/switch-icon'
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||
import { Dropdown } from '../../src/bootstrap'
|
||||
|
||||
import '../../src/sidebar'
|
||||
|
||||
describe('sidebar folded toggle', () => {
|
||||
let fixtureEl: HTMLElement
|
||||
|
||||
beforeAll(() => {
|
||||
fixtureEl = getFixture()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="sidebar-folded" aria-pressed="false">Toggle</button>'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clearFixture()
|
||||
document.documentElement.removeAttribute('data-bs-sidebar')
|
||||
localStorage.removeItem('tabler-sidebar')
|
||||
})
|
||||
|
||||
const trigger = (): HTMLButtonElement => fixtureEl.querySelector('button')!
|
||||
|
||||
it('folds the sidebar to the hover variant on first click', () => {
|
||||
trigger().click()
|
||||
|
||||
expect(document.documentElement.getAttribute('data-bs-sidebar')).toBe('folded-hover')
|
||||
expect(localStorage.getItem('tabler-sidebar')).toBe('folded-hover')
|
||||
expect(trigger().getAttribute('aria-pressed')).toBe('true')
|
||||
})
|
||||
|
||||
it('unfolds the sidebar on second click', () => {
|
||||
trigger().click()
|
||||
trigger().click()
|
||||
|
||||
expect(document.documentElement.hasAttribute('data-bs-sidebar')).toBe(false)
|
||||
expect(localStorage.getItem('tabler-sidebar')).toBe('default')
|
||||
expect(trigger().getAttribute('aria-pressed')).toBe('false')
|
||||
})
|
||||
|
||||
it('unfolds from the static folded state', () => {
|
||||
document.documentElement.setAttribute('data-bs-sidebar', 'folded')
|
||||
|
||||
trigger().click()
|
||||
|
||||
expect(document.documentElement.hasAttribute('data-bs-sidebar')).toBe(false)
|
||||
expect(localStorage.getItem('tabler-sidebar')).toBe('default')
|
||||
})
|
||||
|
||||
it('dispatches tabler:sidebar-folded with the new state', () => {
|
||||
const listener = vi.fn()
|
||||
document.addEventListener('tabler:sidebar-folded', listener, { once: true })
|
||||
|
||||
trigger().click()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
expect((listener.mock.calls[0][0] as CustomEvent).detail).toEqual({ folded: true })
|
||||
})
|
||||
|
||||
it('closes an open sidebar dropdown on an outside click when folded', () => {
|
||||
document.documentElement.setAttribute('data-bs-sidebar', 'folded-hover')
|
||||
fixtureEl.innerHTML = ['<aside class="navbar navbar-vertical">', ' <div class="dropdown"><a class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true"></a><div class="dropdown-menu show"></div></div>', '</aside>', '<button type="button" id="outside">Outside</button>'].join('')
|
||||
const hide = vi.fn()
|
||||
vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never)
|
||||
|
||||
fixtureEl.querySelector<HTMLButtonElement>('#outside')!.click()
|
||||
|
||||
expect(hide).toHaveBeenCalledTimes(1)
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('keeps an open sidebar dropdown when clicking inside its menu when folded', () => {
|
||||
document.documentElement.setAttribute('data-bs-sidebar', 'folded-hover')
|
||||
fixtureEl.innerHTML = ['<aside class="navbar navbar-vertical">', ' <div class="dropdown"><a class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true"></a><div class="dropdown-menu show"><button type="button" id="inside">Item</button></div></div>', '</aside>'].join('')
|
||||
const hide = vi.fn()
|
||||
vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never)
|
||||
|
||||
fixtureEl.querySelector<HTMLButtonElement>('#inside')!.click()
|
||||
|
||||
expect(hide).not.toHaveBeenCalled()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('closes open submenus when the pointer leaves a hover-variant sidebar', () => {
|
||||
document.documentElement.setAttribute('data-bs-sidebar', 'folded-hover')
|
||||
fixtureEl.innerHTML = ['<aside class="navbar navbar-vertical">', ' <div class="dropdown"><a class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true"></a><div class="dropdown-menu show"></div></div>', '</aside>'].join('')
|
||||
const hide = vi.fn()
|
||||
vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never)
|
||||
|
||||
fixtureEl.querySelector('aside')!.dispatchEvent(new MouseEvent('mouseleave'))
|
||||
|
||||
expect(hide).toHaveBeenCalledTimes(1)
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('keeps flyouts open when the pointer leaves a static folded sidebar', () => {
|
||||
document.documentElement.setAttribute('data-bs-sidebar', 'folded')
|
||||
fixtureEl.innerHTML = ['<aside class="navbar navbar-vertical">', ' <div class="dropdown"><a class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true"></a><div class="dropdown-menu show"></div></div>', '</aside>'].join('')
|
||||
const hide = vi.fn()
|
||||
vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never)
|
||||
|
||||
fixtureEl.querySelector('aside')!.dispatchEvent(new MouseEvent('mouseleave'))
|
||||
|
||||
expect(hide).not.toHaveBeenCalled()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('ignores clicks outside a toggle', () => {
|
||||
fixtureEl.innerHTML = '<button type="button">Plain</button>'
|
||||
|
||||
fixtureEl.querySelector('button')!.click()
|
||||
|
||||
expect(document.documentElement.hasAttribute('data-bs-sidebar')).toBe(false)
|
||||
expect(localStorage.getItem('tabler-sidebar')).toBeNull()
|
||||
})
|
||||
})
|
||||
+4
-4
@@ -84,19 +84,19 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "./dist/css/tabler.css",
|
||||
"maxSize": "80 kB"
|
||||
"maxSize": "83 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.min.css",
|
||||
"maxSize": "75 kB"
|
||||
"maxSize": "78 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.rtl.css",
|
||||
"maxSize": "79 kB"
|
||||
"maxSize": "83 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler.rtl.min.css",
|
||||
"maxSize": "75 kB"
|
||||
"maxSize": "78 kB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/css/tabler-flags.css",
|
||||
|
||||
@@ -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,23 @@ $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;
|
||||
$sidebar-nav-padding-y: 0.75rem !default;
|
||||
$sidebar-nav-padding-x: 0.75rem !default;
|
||||
$sidebar-nav-icon-width: 1.5rem !default;
|
||||
$sidebar-nav-icon-height: 1.25rem !default;
|
||||
$sidebar-nav-indent: 1.5rem !default;
|
||||
$sidebar-brand-padding-x: ($sidebar-folded-width - $navbar-brand-image-height) * 0.5 !default;
|
||||
$sidebar-section-title-font-size: 0.75rem !default;
|
||||
$sidebar-section-title-margin-top: 1rem !default;
|
||||
|
||||
// Page
|
||||
$page-title-font-size: var(--font-size-h2) !default;
|
||||
|
||||
@@ -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};
|
||||
|
||||
+341
-26
@@ -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,16 +73,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
|
||||
.nav-link {
|
||||
justify-content: start;
|
||||
padding: 0.5rem calc(#{$container-padding-x} / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -40,8 +91,11 @@
|
||||
display: flex;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
padding-inline-start: add(calc(#{$container-padding-x} / 2), 1.75rem);
|
||||
// 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);
|
||||
@@ -51,19 +105,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: 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: add(calc(#{$container-padding-x} / 2), 4.75rem);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +126,133 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
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% + var(--sidebar-inset) + 0.25rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +294,12 @@ Navbar
|
||||
--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;
|
||||
@@ -145,6 +327,21 @@ Navbar
|
||||
&#{$infix} {
|
||||
@include media-breakpoint-down(breakpoint-next($breakpoint)) {
|
||||
@include navbar-vertical-nav();
|
||||
|
||||
// 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) {
|
||||
@@ -183,7 +380,7 @@ Navbar
|
||||
&.navbar-vertical {
|
||||
~ .navbar,
|
||||
~ .page-wrapper {
|
||||
margin-inline-start: $sidebar-width;
|
||||
margin-inline-start: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +389,19 @@ Navbar
|
||||
~ .navbar,
|
||||
~ .page-wrapper {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: $sidebar-width;
|
||||
margin-inline-end: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +446,10 @@ Navbar toggler
|
||||
|
||||
.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;
|
||||
@@ -321,6 +534,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,9 +571,8 @@ 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);
|
||||
|
||||
&.navbar-right,
|
||||
@@ -346,13 +581,36 @@ 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;
|
||||
--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 {
|
||||
@@ -366,16 +624,44 @@ Navbar vertical
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
~ .page {
|
||||
padding-inline-start: $sidebar-width;
|
||||
padding-inline-start: var(--sidebar-width);
|
||||
|
||||
[class^='container'] {
|
||||
padding-inline-start: 1.5rem;
|
||||
@@ -386,10 +672,39 @@ 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
|
||||
// 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.
|
||||
&:is(.navbar-folded, .navbar-folded-hover:not(:hover):not(:has(:focus-visible))),
|
||||
:where([data-bs-sidebar='folded']) &,
|
||||
:where([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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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,10 +97,11 @@
|
||||
}
|
||||
|
||||
.nav-link-icon {
|
||||
width: $nav-link-icon-size;
|
||||
height: $nav-link-icon-size;
|
||||
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);
|
||||
color: $nav-link-icon-color;
|
||||
color: var(--nav-link-icon-color, #{$nav-link-icon-color});
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
|
||||
@@ -575,7 +575,7 @@ Use the `.btn-link` class to create buttons that look like links but maintain bu
|
||||
|
||||
## Action buttons
|
||||
|
||||
Use the `.btn-action` class to create subtle action buttons that are perfect for card headers, toolbars, or other interface elements where you want minimal visual impact.
|
||||
Use the `.btn-action` class to create subtle action buttons that are perfect for card headers, toolbars, or other interface elements where you want minimal visual impact. Always combine it with the `btn` class — use `btn btn-action`, never `btn-action` on its own. Add `btn-sm` for a smaller action button.
|
||||
|
||||
<Example centered>
|
||||
<div class="btn-actions">
|
||||
|
||||
@@ -6,6 +6,7 @@ related: [/ui/layout/page-headers, /ui/layout/navbars]
|
||||
---
|
||||
|
||||
import Example from '@components/Example.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
|
||||
<Info>
|
||||
Before you start with this section, make sure you have followed the [installation guideline](/ui/getting-started/installation).
|
||||
@@ -102,14 +103,14 @@ To create a sidebar layout, you can use the following code snippet. This code sn
|
||||
|
||||
<Example>
|
||||
<div class="page">
|
||||
<aside class="navbar navbar-vertical navbar-expand-sm position-absolute" data-bs-theme="dark">
|
||||
<aside class="navbar navbar-vertical navbar-expand-sm position-absolute">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark">
|
||||
<a href="#">
|
||||
<img src="/static/logo-white.svg" width="110" height="32" alt="Tabler" class="navbar-brand-image" />
|
||||
<img src="/static/logo.svg" width="110" height="32" alt="Tabler" class="navbar-brand-image" />
|
||||
</a>
|
||||
</h1>
|
||||
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||||
@@ -227,6 +228,168 @@ To create a sidebar layout, you can use the following code snippet. This code sn
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
### Section labels
|
||||
|
||||
Group related menu items with a label. Add a `nav-section-title` list item before the first item of a group:
|
||||
|
||||
```html
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-section-title">Overview</li>
|
||||
<li class="nav-item">...</li>
|
||||
<li class="nav-item">...</li>
|
||||
<li class="nav-section-title">Components</li>
|
||||
<li class="nav-item">...</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
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 avatar-sm" 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. Below the navbar's breakpoint, the footer moves into the mobile top bar: only the avatar shows, and its menu drops down instead of up.
|
||||
|
||||
## 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.
|
||||
|
||||
The folded mode works only above the navbar's `navbar-expand-*` breakpoint. Below it, the sidebar keeps the standard mobile behavior with the toggler button.
|
||||
|
||||
Use `data-bs-auto-close="outside"` on submenu toggles in a folded sidebar, so an open flyout closes when the user clicks somewhere else.
|
||||
|
||||
<Example height="26rem">
|
||||
<div class="page">
|
||||
<aside class="navbar navbar-vertical navbar-expand-sm navbar-folded position-absolute">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-folded" aria-controls="sidebar-folded" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark">
|
||||
<a href="#">
|
||||
<img src="/static/logo-small.svg" width="32" height="32" alt="Tabler" class="navbar-brand-image" />
|
||||
</a>
|
||||
</h1>
|
||||
<div class="collapse navbar-collapse" id="sidebar-folded">
|
||||
<ul class="navbar-nav pt-lg-3">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#" aria-current="page">
|
||||
<span class="nav-link-icon"><Icon name="home" /></span>
|
||||
<span class="nav-link-title">Home</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#sidebar-folded-reports" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="nav-link-icon"><Icon name="chart-bar" /></span>
|
||||
<span class="nav-link-title">Reports</span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Overview</a>
|
||||
<a class="dropdown-item" href="#">Sales</a>
|
||||
<a class="dropdown-item" href="#">Traffic</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">
|
||||
<span class="nav-link-icon"><Icon name="settings" /></span>
|
||||
<span class="nav-link-title">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="page-wrapper">
|
||||
<main id="content" class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
<div class="col-6">
|
||||
<div class="card">
|
||||
<div class="card-body" style="height: 10rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card">
|
||||
<div class="card-body" style="height: 10rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
Use a small, square logo in a folded sidebar. The brand area is only `4rem` wide, so a wide logo will be cropped.
|
||||
|
||||
A folded rail does not scroll. If your menu has more items than the screen can fit, use the hover variant below or keep the sidebar expanded.
|
||||
|
||||
### Unfold on hover
|
||||
|
||||
Use `navbar-folded-hover` instead of `navbar-folded` to get a folded rail that unfolds when the user hovers over it or moves keyboard focus into it. The expanded sidebar overlays the page content, so nothing moves around. This variant also restores scrolling of long menus while the sidebar is unfolded.
|
||||
|
||||
```html
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg navbar-folded-hover">
|
||||
...
|
||||
</aside>
|
||||
```
|
||||
|
||||
### Pin button and saved state
|
||||
|
||||
To let users fold and unfold the sidebar at runtime, add a button with `data-bs-toggle="sidebar-folded"` anywhere on the page. A good place is the brand area: the logo stays on the start side and the pin button sits on the end side. The button is visible only while the sidebar is expanded. Unpinning folds the sidebar to the hover variant, so hovering the rail expands it again and shows the pin button:
|
||||
|
||||
```html
|
||||
<div class="navbar-brand">
|
||||
<a href="." aria-label="Tabler">
|
||||
<svg class="navbar-brand-image"><!-- logo mark --></svg>
|
||||
</a>
|
||||
<button type="button" class="btn btn-action btn-sm" data-bs-toggle="sidebar-folded" aria-pressed="false" aria-label="Pin sidebar">
|
||||
<svg><!-- pin icon --></svg>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Use a small, square logo mark in the brand area, so the logo does not jump when the sidebar folds and unfolds.
|
||||
|
||||
The button toggles the `data-bs-sidebar="folded-hover"` attribute on the `<html>` element, which folds every vertical navbar on the page. Set `data-bs-sidebar="folded"` yourself for a static rail that does not expand on hover. The choice is saved to `localStorage` under the `tabler-sidebar` key. The `tabler-theme.js` script applies the saved state before the page renders, so there is no flash of the wrong layout on reload. You can also set the state with the `?sidebar=folded` URL parameter.
|
||||
|
||||
After each toggle, Tabler dispatches a `tabler:sidebar-folded` event on `document`. Use it to resize charts or maps after the page content changes width:
|
||||
|
||||
```js
|
||||
document.addEventListener('tabler:sidebar-folded', (event) => {
|
||||
console.log(event.detail.folded) // true or false
|
||||
myChart.resize()
|
||||
})
|
||||
```
|
||||
|
||||
You can also add tooltips with the link title to icon-only links, so users see the name of each item on hover.
|
||||
|
||||
## Accessibility
|
||||
|
||||
A layout is what a screen reader user navigates by, so the landmarks matter more here than on any single component.
|
||||
|
||||
@@ -130,7 +130,7 @@ const person3 = requireIndex(people, 3)
|
||||
</div>
|
||||
<CardActions>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><Icon name="dots-vertical" /></a>
|
||||
<a href="#" class="btn btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><Icon name="dots-vertical" /></a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href="#">Edit user</a>
|
||||
<a class="dropdown-item" href="#">Change permissions</a>
|
||||
@@ -151,10 +151,10 @@ const person3 = requireIndex(people, 3)
|
||||
<CardSubtitle>A compact row of icon-only actions in the header.</CardSubtitle>
|
||||
</div>
|
||||
<CardActions class="btn-actions">
|
||||
<a href="#" class="btn-action"><Icon name="refresh" /></a>
|
||||
<a href="#" class="btn-action"><Icon name="chevron-up" /></a>
|
||||
<a href="#" class="btn-action"><Icon name="dots-vertical" /></a>
|
||||
<a href="#" class="btn-action"><Icon name="x" /></a>
|
||||
<a href="#" class="btn btn-action"><Icon name="refresh" /></a>
|
||||
<a href="#" class="btn btn-action"><Icon name="chevron-up" /></a>
|
||||
<a href="#" class="btn btn-action"><Icon name="dots-vertical" /></a>
|
||||
<a href="#" class="btn btn-action"><Icon name="x" /></a>
|
||||
</CardActions>
|
||||
</CardHeader>
|
||||
<BodyPlaceholder />
|
||||
|
||||
@@ -4,7 +4,7 @@ import HomepageContent from '@shared/components/HomepageContent.astro'
|
||||
import HeaderActionsButtons from '@shared/components/layout/HeaderActionsButtons.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Dashboard" pageHeader="Combo layout" pretitle="Overview" pageMenu="layout.combo" pageLibs={['apexcharts', 'jsvectormap']} sidebar sidebarDark navbarHideBrand navbarClass="d-none d-lg-flex" navbarCondensed>
|
||||
<DefaultLayout title="Dashboard" pageHeader="Combo layout" pretitle="Overview" pageMenu="layout.combo" pageLibs={['apexcharts', 'jsvectormap']} sidebar navbarHideBrand navbarClass="d-none d-lg-flex" navbarCondensed>
|
||||
<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="Fluid vertical layout" pretitle="Overview" pageMenu="layout.fluid-vertical" pageLibs={['apexcharts', 'jsvectormap']} bodyClass="layout-fluid" sidebar sidebarDark hideTopbar>
|
||||
<DefaultLayout title="Dashboard" pageHeader="Fluid vertical layout" pretitle="Overview" pageMenu="layout.fluid-vertical" pageLibs={['apexcharts', 'jsvectormap']} bodyClass="layout-fluid" sidebar hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
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 sidebarUser hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
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 sidebarUser hideTopbar>
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
<HomepageContent />
|
||||
</DefaultLayout>
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
// layout-sidebar: true, layout-sidebar-dark: true, layout-hide-topbar: true,
|
||||
// layout-sidebar: true, layout-hide-topbar: true,
|
||||
// layout: homepage (page-header overridden by the page front matter)
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
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 sidebarDark 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>
|
||||
@@ -5,7 +5,7 @@ import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Layout playground" pretitle="Playground" sidebar sidebarDark>
|
||||
<DefaultLayout title="Layout playground" pretitle="Playground" sidebar>
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
|
||||
@@ -10,7 +10,14 @@ const defaults = {
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
'sidebar': 'default',
|
||||
}
|
||||
|
||||
const sidebarModes = [
|
||||
{ value: 'default', label: 'Expanded' },
|
||||
{ value: 'folded', label: 'Folded' },
|
||||
{ value: 'folded-hover', label: 'Folded with hover' },
|
||||
]
|
||||
---
|
||||
|
||||
<!-- BEGIN THEME SETTINGS -->
|
||||
@@ -118,6 +125,24 @@ const defaults = {
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="border-0 p-0 mb-4">
|
||||
<legend class="form-label float-none mb-0" style="font-size: inherit;">Sidebar</legend>
|
||||
<FormHint as="p">Choose how the vertical sidebar behaves.</FormHint>
|
||||
|
||||
<div>
|
||||
{
|
||||
sidebarModes.map((mode) => (
|
||||
<label class="form-check">
|
||||
<div class="form-selectgroup-item">
|
||||
<input type="radio" name="sidebar" value={mode.value} class="form-check-input" checked={mode.value === defaults.sidebar} />
|
||||
<div class="form-check-label">{mode.label}</div>
|
||||
</div>
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto space-y">
|
||||
|
||||
@@ -36,6 +36,7 @@ const logoSvg = smallLogo ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0
|
||||
{!hideLogo && <Fragment set:html={logoSvg} />}
|
||||
{showTitle && 'Dashboard'}
|
||||
</a>
|
||||
<slot />
|
||||
</div>
|
||||
<Fragment set:html={'<!-- END NAVBAR LOGO -->'} />
|
||||
</Fragment>
|
||||
|
||||
@@ -175,7 +175,7 @@ const headerClass = [
|
||||
<span class="badge badge-sm bg-red text-red-fg">
|
||||
New
|
||||
</span>
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<span class="nav-link-icon">
|
||||
<Icon name="settings" />
|
||||
</span>
|
||||
<span class="nav-link-title"> Theme Settings </span>
|
||||
|
||||
@@ -15,6 +15,8 @@ interface Level1 {
|
||||
'disabled'?: boolean
|
||||
'right'?: boolean
|
||||
'columns'?: number
|
||||
/** section label rendered before this item (sidebar only, needs the `sections` prop) */
|
||||
'section'?: string
|
||||
'children'?: Record<string, Record<string, unknown>>
|
||||
}
|
||||
|
||||
@@ -26,10 +28,12 @@ interface Props {
|
||||
longTitles?: boolean
|
||||
keepOpen?: boolean
|
||||
autoOpen?: boolean
|
||||
/** renders `section` fields from the menu data as group labels */
|
||||
sections?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { pageMenu = '', sample = false, hideIcons = false, longTitles = false, keepOpen = false, autoOpen = false, class: className } = Astro.props
|
||||
const { pageMenu = '', sample = false, hideIcons = false, longTitles = false, keepOpen = false, autoOpen = false, sections = false, class: className } = Astro.props
|
||||
|
||||
const currentPage = pageMenu.split('.')
|
||||
const items = Object.entries((sample ? menuSample : menu) as Record<string, Level1>)
|
||||
@@ -54,6 +58,7 @@ const chunk = <T,>(arr: T[], size: number): T[][] => {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{sections && item.section && <li class="nav-section-title">{item.section}</li>}
|
||||
<Fragment set:html={'<!-- BEGIN NAVBAR ITEM -->'} />
|
||||
<li class:list={['nav-item', active && 'active', hasChildren && 'dropdown']}>
|
||||
<a
|
||||
@@ -66,7 +71,7 @@ const chunk = <T,>(arr: T[], size: number): T[][] => {
|
||||
aria-expanded={hasChildren ? (autoOpen && key === currentPage[0] ? 'true' : 'false') : undefined}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
>
|
||||
{!hideIcons && <span class="nav-link-icon d-md-none d-lg-inline-block">{item.icon && <Icon name={item.icon} />}</span>}
|
||||
{!hideIcons && <span class="nav-link-icon">{item.icon && <Icon name={item.icon} />}</span>}
|
||||
|
||||
<span class="nav-link-title">{title}</span>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import CardTitle from '@ui/CardTitle.astro'
|
||||
<CardTitle>My Apps</CardTitle>
|
||||
|
||||
<CardActions class="btn-actions">
|
||||
<a href="#" class="btn-action">
|
||||
<a href="#" class="btn btn-action">
|
||||
<Icon name="settings" />
|
||||
</a>
|
||||
</CardActions>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
@@ -10,15 +11,25 @@ interface Props {
|
||||
breakpoint?: Breakpoint | undefined
|
||||
/** layout-sidebar-end — navbar-end class */
|
||||
end?: boolean | undefined
|
||||
/** layout-navbar-transparent — navbar-transparent class */
|
||||
/** layout-sidebar-transparent — navbar-transparent class */
|
||||
transparent?: boolean | undefined
|
||||
/** layout-sidebar-folded — navbar-folded class (icon-only rail with flyout submenus) */
|
||||
folded?: boolean | undefined
|
||||
/** layout-sidebar-folded-hover — navbar-folded-hover class (folded rail that unfolds on hover/focus) */
|
||||
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, pageMenu } = Astro.props
|
||||
const { dark = false, breakpoint = 'lg', end = false, transparent = false, folded = false, foldedHover = false, foldToggle = false, user = false, pageMenu } = Astro.props
|
||||
|
||||
const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expand-${breakpoint}`, transparent && 'navbar-transparent'].filter(Boolean).join(' ')
|
||||
const isFolded = folded || foldedHover
|
||||
|
||||
const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expand-${breakpoint}`, transparent && 'navbar-transparent', folded && 'navbar-folded', foldedHover && 'navbar-folded-hover'].filter(Boolean).join(' ')
|
||||
---
|
||||
|
||||
<!-- BEGIN SIDEBAR -->
|
||||
@@ -26,13 +37,26 @@ const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expan
|
||||
<div class="container-fluid">
|
||||
<NavbarToggler target="sidebar-menu" />
|
||||
|
||||
<Logo header />
|
||||
{/* Small logo mark: the full wordmark would crop in the folded rail and jump during the hover-expand transition. */}
|
||||
<Logo header smallLogo>
|
||||
{
|
||||
foldToggle && (
|
||||
<button type="button" class="btn btn-action btn-sm d-none d-lg-inline-flex" data-bs-toggle="sidebar-folded" aria-pressed={folded ? 'true' : 'false'} aria-label="Pin sidebar">
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</Logo>
|
||||
|
||||
{/* Only the Sponsor button renders (no show-* flags). */}
|
||||
<NavbarSide class="d-lg-none" breakpoint={breakpoint} />
|
||||
{
|
||||
/* Before the collapse, like the regular navbar's side items — on mobile it
|
||||
stays in the top bar row; on desktop CSS order moves it to the bottom. */
|
||||
}
|
||||
{user && <SidebarUser />}
|
||||
|
||||
<nav class="collapse navbar-collapse" id="sidebar-menu" aria-label="Sidebar">
|
||||
<NavbarMenu autoOpen class="pt-lg-3" keepOpen pageMenu={pageMenu} />
|
||||
{/* 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>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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">
|
||||
<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)} size="sm" />
|
||||
<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 -->
|
||||
@@ -2,6 +2,7 @@
|
||||
"dashboards": {
|
||||
"title": "Dashboards",
|
||||
"icon": "home",
|
||||
"section": "Overview",
|
||||
"children": {
|
||||
"default": {
|
||||
"url": "index.html",
|
||||
@@ -22,6 +23,7 @@
|
||||
"base": {
|
||||
"title": "Interface",
|
||||
"icon": "package",
|
||||
"section": "Components",
|
||||
"columns": 2,
|
||||
"children": {
|
||||
"all-elements": {
|
||||
@@ -383,6 +385,7 @@
|
||||
"layout": {
|
||||
"icon": "layout-2",
|
||||
"title": "Layout",
|
||||
"section": "Layout",
|
||||
"columns": 2,
|
||||
"children": {
|
||||
"boxed": {
|
||||
@@ -436,12 +439,23 @@
|
||||
"vertical-transparent": {
|
||||
"url": "layout-vertical-transparent.html",
|
||||
"title": "Vertical transparent"
|
||||
},
|
||||
"folded": {
|
||||
"url": "layout-folded.html",
|
||||
"title": "Folded sidebar",
|
||||
"badge": "New"
|
||||
},
|
||||
"folded-hover": {
|
||||
"url": "layout-folded-hover.html",
|
||||
"title": "Folded hover",
|
||||
"badge": "New"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"title": "Plugins",
|
||||
"icon": "puzzle",
|
||||
"section": "More",
|
||||
"children": {
|
||||
"charts": {
|
||||
"url": "charts.html",
|
||||
|
||||
@@ -148,6 +148,7 @@ const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
'sidebar': 'default',
|
||||
}
|
||||
|
||||
var url = new URL(window.location)
|
||||
|
||||
@@ -38,6 +38,14 @@ interface Props {
|
||||
rtl?: boolean
|
||||
/** layout-sidebar-end */
|
||||
sidebarEnd?: boolean
|
||||
/** layout-sidebar-folded — icon-only sidebar rail with flyout submenus */
|
||||
sidebarFolded?: boolean
|
||||
/** layout-sidebar-folded-hover — folded rail that unfolds on hover/focus */
|
||||
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 */
|
||||
@@ -54,7 +62,35 @@ interface Props {
|
||||
navbarClass?: string
|
||||
}
|
||||
|
||||
const { title, pageLibs, pageHeader, pageHeaderFile, pretitle, description, pageMenu, sidebar, sidebarDark, hideTopbar, containerCentered, wrapperFull, containerClass, bodyClass, rtl, sidebarEnd, navbarTransparent, navbarCondensed, navbarDark, navbarOverlap, navbarSticky, navbarHideBrand, navbarClass } = Astro.props
|
||||
const {
|
||||
title,
|
||||
pageLibs,
|
||||
pageHeader,
|
||||
pageHeaderFile,
|
||||
pretitle,
|
||||
description,
|
||||
pageMenu,
|
||||
sidebar,
|
||||
sidebarDark,
|
||||
hideTopbar,
|
||||
containerCentered,
|
||||
wrapperFull,
|
||||
containerClass,
|
||||
bodyClass,
|
||||
rtl,
|
||||
sidebarEnd,
|
||||
sidebarFolded,
|
||||
sidebarFoldedHover,
|
||||
sidebarFoldToggle,
|
||||
sidebarUser,
|
||||
navbarTransparent,
|
||||
navbarCondensed,
|
||||
navbarDark,
|
||||
navbarOverlap,
|
||||
navbarSticky,
|
||||
navbarHideBrand,
|
||||
navbarClass,
|
||||
} = Astro.props
|
||||
|
||||
// Most pages repeat the page title verbatim, so `pageHeader` falls back to `title`.
|
||||
// Pages that deliberately have no header (blank, playgrounds) pass pageHeader={false}.
|
||||
@@ -67,7 +103,7 @@ const headerTitle = pageHeader === false ? undefined : (pageHeader ?? title)
|
||||
sidebar && (
|
||||
<Fragment>
|
||||
<Fragment set:html={'<!-- BEGIN SIDEBAR -->'} />
|
||||
<Sidebar dark={sidebarDark} end={sidebarEnd} transparent={navbarTransparent} 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