mirror of
https://github.com/tabler/tabler.git
synced 2026-08-28 12:56:24 +04:00
Add folded sidebar with flyout menus, pin toggle and light default
- navbar-folded / navbar-folded-hover with flyout submenus and persisted pin toggle - sidebar refresh: light by default, 16rem wide, rounded inset pills, zero-jump fold - demo pages, Theme Settings section, docs and btn-action usage rule
This commit is contained in:
@@ -38,6 +38,7 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
- 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,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,80 @@
|
||||
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"]'))
|
||||
})
|
||||
|
||||
// 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,94 @@
|
||||
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('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": "82 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,15 @@ $navbar-toggler-transition: box-shadow 0.15s ease-in-out !default;
|
||||
$navbar-toggler-animation-time: 0.2s !default;
|
||||
$navbar-overlap-height: 9rem !default;
|
||||
|
||||
$navbar-nav-link-hover-bg: color-transparent(var(--nav-link-color), 0.04) !default;
|
||||
$navbar-nav-link-hover-bg: color-transparent(var(--nav-link-color), 0.06) !default;
|
||||
|
||||
$navbar-active-border-color: var(--primary) !default;
|
||||
// scss-docs-end navbar-variables
|
||||
|
||||
// Sidebar
|
||||
$sidebar-width: 15rem !default;
|
||||
$sidebar-width: 16rem !default;
|
||||
$sidebar-folded-width: 4rem !default;
|
||||
$sidebar-inset: 0.5rem !default;
|
||||
|
||||
// Page
|
||||
$page-title-font-size: var(--font-size-h2) !default;
|
||||
|
||||
@@ -274,6 +274,7 @@
|
||||
--navbar-hover-color: #{$navbar-dark-hover-color};
|
||||
--navbar-disabled-color: #{$navbar-dark-disabled-color};
|
||||
--navbar-active-color: #{$navbar-dark-active-color};
|
||||
--navbar-active-bg: #{$navbar-dark-active-bg};
|
||||
--navbar-brand-color: #{$navbar-dark-brand-color};
|
||||
--navbar-brand-hover-color: #{$navbar-dark-brand-hover-color};
|
||||
--navbar-toggler-border-color: #{$navbar-dark-toggler-border-color};
|
||||
|
||||
+190
-15
@@ -13,12 +13,28 @@
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
// Sidebar icons carry more meaning than inline tab icons, so they get
|
||||
// stronger contrast than the global 50% $nav-link-icon-color. Derived
|
||||
// from --navbar-color, which is defined at this scope (unlike
|
||||
// --nav-link-color, which only exists inside .nav).
|
||||
--nav-link-icon-color: #{color-transparent(var(--navbar-color), 0.7)};
|
||||
// Inset the list so the rounded hover/active pills never touch the
|
||||
// sidebar edges. The same inset and link padding apply in the folded
|
||||
// state, so icons keep their exact position when the sidebar folds.
|
||||
padding-inline: $sidebar-inset;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
|
||||
.nav-link {
|
||||
justify-content: start;
|
||||
padding: 0.5rem calc(#{$container-padding-x} / 2);
|
||||
padding: 0.75rem 0.75rem;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line selector-max-class
|
||||
.nav-item.active > .nav-link {
|
||||
--nav-link-icon-color: currentColor;
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--navbar-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +56,11 @@
|
||||
display: flex;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
padding-inline-start: add(calc(#{$container-padding-x} / 2), 1.75rem);
|
||||
// Link padding (0.75rem) + the icon column (1.75rem) — items align
|
||||
// with the parent link's title.
|
||||
padding-inline-start: 2.5rem;
|
||||
color: inherit;
|
||||
@include border-radius(var(--border-radius));
|
||||
|
||||
&.disabled {
|
||||
color: var(--disabled-color);
|
||||
@@ -51,19 +70,20 @@
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--navbar-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item {
|
||||
padding-inline-start: add(calc(#{$container-padding-x} / 2), 3.25rem);
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
|
||||
// Third-level submenu in the mobile vertical navbar — genuinely nested two
|
||||
// dropdown-menu levels deep.
|
||||
// stylelint-disable-next-line selector-max-class, selector-max-compound-selectors
|
||||
.dropdown-menu .dropdown-menu .dropdown-item {
|
||||
padding-inline-start: add(calc(#{$container-padding-x} / 2), 4.75rem);
|
||||
padding-inline-start: 5.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +91,100 @@
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
// The active pill is the selection indicator — no underline or edge bar.
|
||||
.nav-item.active::after {
|
||||
inset-inline-end: auto;
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon-only rail: hides link titles, centers icons and turns the inline submenus
|
||||
// flattened by navbar-vertical-nav() back into floating flyout dropdowns.
|
||||
@mixin navbar-vertical-folded {
|
||||
// Flyouts must escape the rail, so the folded sidebar itself cannot scroll.
|
||||
overflow: visible;
|
||||
|
||||
// A wide brand (e.g. a full logo) must not push the container beyond the
|
||||
// rail width; the brand crops instead.
|
||||
[class^='container'] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// The brand keeps the expanded state's start padding, so the logo does not
|
||||
// move when the sidebar folds; anything wider than the rail crops.
|
||||
.navbar-brand {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// The pin button lives on the expanded sidebar only; the folded rail
|
||||
// unfolds by hovering (folded-hover) or via an outside control.
|
||||
[data-bs-toggle='sidebar-folded'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
// The list inset and link padding are identical in both states, so the
|
||||
// icons keep their exact position — only the titles collapse.
|
||||
.navbar-nav .nav-link {
|
||||
--nav-link-icon-margin-end: 0;
|
||||
}
|
||||
|
||||
// Collapsed, not display: none — titles stay in the accessibility tree.
|
||||
.nav-link-title {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-link.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Restore the multi-column layout that navbar-vertical-nav() stacks —
|
||||
// it keeps long flyout menus from overflowing the viewport.
|
||||
.dropdown-menu-columns {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
inset-inline-start: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-inline-start-width: 3px;
|
||||
border-bottom-width: 0;
|
||||
min-width: var(--dropdown-min-width);
|
||||
padding: var(--dropdown-padding-y) var(--dropdown-padding-x);
|
||||
margin: 0;
|
||||
color: var(--dropdown-color);
|
||||
background-color: var(--dropdown-bg);
|
||||
background-clip: padding-box;
|
||||
// No border — like the base .dropdown-menu, whose edge is the 1px ring
|
||||
// built into --shadow-dropdown.
|
||||
box-shadow: var(--dropdown-box-shadow);
|
||||
@include border-radius(var(--dropdown-border-radius));
|
||||
|
||||
.dropdown-item {
|
||||
padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
|
||||
color: var(--dropdown-link-color);
|
||||
@include border-radius(0);
|
||||
}
|
||||
|
||||
// Undo the tree indents navbar-vertical-nav() puts on nested levels.
|
||||
.dropdown-menu .dropdown-item {
|
||||
padding-inline-start: var(--dropdown-item-padding-x);
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line selector-max-class, selector-max-compound-selectors
|
||||
.dropdown-menu .dropdown-menu .dropdown-item {
|
||||
padding-inline-start: var(--dropdown-item-padding-x);
|
||||
}
|
||||
}
|
||||
|
||||
// The nav list is inset, so 100% of the .dropdown lands short of the rail
|
||||
// edge — compensate for the inset and add a small gap, so the flyout's
|
||||
// border does not sit flush against the rail's edge border. Nested
|
||||
// (dropend) flyouts are not inset and keep the plain 100%.
|
||||
.nav-item > .dropdown-menu {
|
||||
inset-inline-start: calc(100% + #{$sidebar-inset + 0.25rem});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +291,7 @@ Navbar
|
||||
&.navbar-vertical {
|
||||
~ .navbar,
|
||||
~ .page-wrapper {
|
||||
margin-inline-start: $sidebar-width;
|
||||
margin-inline-start: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +300,7 @@ Navbar
|
||||
~ .navbar,
|
||||
~ .page-wrapper {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: $sidebar-width;
|
||||
margin-inline-end: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,6 +429,29 @@ Navbar side
|
||||
Navbar vertical
|
||||
*/
|
||||
@if $enable-navbar-vertical {
|
||||
// Folded sidebar: every geometry consumer (aside width, page paddings,
|
||||
// page-wrapper/navbar margins) reads --sidebar-width, so flipping this one
|
||||
// variable re-lays-out the whole page. Two routes to the same state:
|
||||
// a .navbar-folded/.navbar-folded-hover class on the aside (static markup),
|
||||
// or data-bs-sidebar="folded"/"folded-hover" on <html> (set pre-render from
|
||||
// localStorage by tabler-theme.js, toggled by [data-bs-toggle="sidebar-folded"]).
|
||||
// html[…] beats the :root declaration of --sidebar-width on the same element;
|
||||
// a bare :where()/[…] selector would lose to it and never take effect.
|
||||
html[data-bs-sidebar^='folded'] {
|
||||
--sidebar-width: var(--sidebar-folded-width);
|
||||
}
|
||||
|
||||
.navbar-vertical:is(.navbar-folded, .navbar-folded-hover) {
|
||||
--sidebar-width: var(--sidebar-folded-width);
|
||||
|
||||
// The class sits on the aside, so its layout siblings need their own copy.
|
||||
~ .page,
|
||||
~ .page-wrapper,
|
||||
~ .navbar {
|
||||
--sidebar-width: var(--sidebar-folded-width);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-vertical {
|
||||
&.navbar-expand {
|
||||
@each $breakpoint in map.keys($grid-breakpoints) {
|
||||
@@ -335,7 +466,7 @@ Navbar vertical
|
||||
bottom: 0;
|
||||
z-index: $zindex-fixed;
|
||||
align-items: start;
|
||||
width: $sidebar-width;
|
||||
width: var(--sidebar-width);
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
@include transition(transform $transition-time);
|
||||
@@ -346,9 +477,26 @@ Navbar vertical
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
// Logo on the start side, room for an action (e.g. the pin
|
||||
// toggle) on the end side. The inline padding centers the logo
|
||||
// mark in the folded rail and is identical in both states, so the
|
||||
// logo mark and the nav icons share one center axis and nothing
|
||||
// moves when the sidebar folds.
|
||||
.navbar-brand {
|
||||
justify-content: center;
|
||||
padding: (($navbar-height - $navbar-brand-image-height) * 0.5) 0;
|
||||
justify-content: space-between;
|
||||
padding: (($navbar-height - $navbar-brand-image-height) * 0.5) (($sidebar-folded-width - $navbar-brand-image-height) * 0.5);
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
// The fold toggle keeps its layout slot (no jumping) but only
|
||||
// becomes visible while the sidebar is hovered or focused.
|
||||
[data-bs-toggle='sidebar-folded'] {
|
||||
opacity: 0;
|
||||
@include transition(opacity $transition-time);
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) [data-bs-toggle='sidebar-folded'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
@@ -375,7 +523,7 @@ Navbar vertical
|
||||
}
|
||||
|
||||
~ .page {
|
||||
padding-inline-start: $sidebar-width;
|
||||
padding-inline-start: var(--sidebar-width);
|
||||
|
||||
[class^='container'] {
|
||||
padding-inline-start: 1.5rem;
|
||||
@@ -386,10 +534,37 @@ Navbar vertical
|
||||
&.navbar-right ~ .page,
|
||||
&.navbar-end ~ .page {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: $sidebar-width;
|
||||
padding-inline-end: var(--sidebar-width);
|
||||
}
|
||||
|
||||
@include navbar-vertical-nav();
|
||||
|
||||
// Folded state, both routes; the hover variant folds only while
|
||||
// neither hovered nor focused from the keyboard.
|
||||
&:is(.navbar-folded, .navbar-folded-hover:not(:hover, :focus-within)),
|
||||
:where([data-bs-sidebar='folded']) &,
|
||||
:where([data-bs-sidebar='folded-hover']) &:not(:hover, :focus-within) {
|
||||
@include navbar-vertical-folded();
|
||||
}
|
||||
|
||||
&.navbar-folded-hover,
|
||||
:where([data-bs-sidebar='folded-hover']) & {
|
||||
@include transition(transform $transition-time, width $transition-time);
|
||||
|
||||
.nav-link-title {
|
||||
@include transition(opacity $transition-time);
|
||||
}
|
||||
|
||||
// Temporarily unfolds as an overlay: siblings keep the folded
|
||||
// --sidebar-width, so page content does not reflow.
|
||||
&:is(:hover, :focus-within) {
|
||||
width: $sidebar-width;
|
||||
overflow-y: auto;
|
||||
box-shadow:
|
||||
inset calc(-1 * var(--navbar-border-width)) 0 0 0 var(--navbar-border-color),
|
||||
var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
width: $nav-link-icon-size;
|
||||
height: $nav-link-icon-size;
|
||||
margin-inline-end: var(--nav-link-icon-margin-end);
|
||||
color: $nav-link-icon-color;
|
||||
color: var(--nav-link-icon-color, #{$nav-link-icon-color});
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
|
||||
@@ -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'
|
||||
|
||||
Before you start with this section, make sure you have followed the [installation guideline](/ui/getting-started/installation).
|
||||
|
||||
@@ -100,14 +101,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">
|
||||
@@ -225,6 +226,120 @@ To create a sidebar layout, you can use the following code snippet. This code sn
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## 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 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 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 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Icon from '@ui/Icon.astro'
|
||||
import type { Breakpoint } from './NavbarSide.astro'
|
||||
|
||||
interface Props {
|
||||
@@ -10,15 +11,23 @@ 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
|
||||
/** 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, 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 +35,23 @@ 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} />
|
||||
|
||||
<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} pageMenu={pageMenu} />
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -436,6 +436,16 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,12 @@ 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-navbar-transparent (sidebar variant) */
|
||||
navbarTransparent?: boolean
|
||||
/** layout-navbar-condensed */
|
||||
@@ -54,7 +60,34 @@ 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,
|
||||
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 +100,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} breakpoint="lg" pageMenu={pageMenu} />
|
||||
<Fragment set:html={'<!-- END SIDEBAR -->'} />
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user