diff --git a/.agents/rules/main.mdc b/.agents/rules/main.mdc index 0af24066a..afbae0078 100644 --- a/.agents/rules/main.mdc +++ b/.agents/rules/main.mdc @@ -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. diff --git a/.agents/skills/demo-pages/SKILL.md b/.agents/skills/demo-pages/SKILL.md index 98215266a..3f055f8e4 100644 --- a/.agents/skills/demo-pages/SKILL.md +++ b/.agents/skills/demo-pages/SKILL.md @@ -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. diff --git a/.changeset/folded-sidebar.md b/.changeset/folded-sidebar.md new file mode 100644 index 000000000..0df5560ff --- /dev/null +++ b/.changeset/folded-sidebar.md @@ -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. diff --git a/core/js/src/sidebar.ts b/core/js/src/sidebar.ts new file mode 100644 index 000000000..587723e3e --- /dev/null +++ b/core/js/src/sidebar.ts @@ -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('.navbar-vertical [data-bs-toggle="dropdown"][aria-expanded="true"]')) { + if (toggle !== except) { + ;(Dropdown.getInstance(toggle) as InstanceType | 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 diff --git a/core/js/tabler-theme.ts b/core/js/tabler-theme.ts index 597a9a19d..d1099ed75 100644 --- a/core/js/tabler-theme.ts +++ b/core/js/tabler-theme.ts @@ -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), { diff --git a/core/js/tabler.ts b/core/js/tabler.ts index 1ddc24a76..35d459052 100644 --- a/core/js/tabler.ts +++ b/core/js/tabler.ts @@ -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' diff --git a/core/js/tests/unit/sidebar.spec.ts b/core/js/tests/unit/sidebar.spec.ts new file mode 100644 index 000000000..0f6e0da13 --- /dev/null +++ b/core/js/tests/unit/sidebar.spec.ts @@ -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 = '' + }) + + 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 = ['', ''].join('') + const hide = vi.fn() + vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never) + + fixtureEl.querySelector('#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 = [''].join('') + const hide = vi.fn() + vi.spyOn(Dropdown, 'getInstance').mockReturnValue({ hide } as never) + + fixtureEl.querySelector('#inside')!.click() + + expect(hide).not.toHaveBeenCalled() + vi.restoreAllMocks() + }) + + it('ignores clicks outside a toggle', () => { + fixtureEl.innerHTML = '' + + fixtureEl.querySelector('button')!.click() + + expect(document.documentElement.hasAttribute('data-bs-sidebar')).toBe(false) + expect(localStorage.getItem('tabler-sidebar')).toBeNull() + }) +}) diff --git a/core/package.json b/core/package.json index 3844f2fa1..74e1c23b5 100644 --- a/core/package.json +++ b/core/package.json @@ -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", diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index bc4691d78..44455ea2b 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -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; diff --git a/core/scss/bootstrap/_navbar.scss b/core/scss/bootstrap/_navbar.scss index 5e88d7dc5..4e26f6ffe 100644 --- a/core/scss/bootstrap/_navbar.scss +++ b/core/scss/bootstrap/_navbar.scss @@ -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}; diff --git a/core/scss/layout/_navbar.scss b/core/scss/layout/_navbar.scss index 96624a78e..d56bd5644 100644 --- a/core/scss/layout/_navbar.scss +++ b/core/scss/layout/_navbar.scss @@ -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 (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); + } + } } } } diff --git a/core/scss/layout/_root.scss b/core/scss/layout/_root.scss index bf8c343ed..b8ab9eede 100644 --- a/core/scss/layout/_root.scss +++ b/core/scss/layout/_root.scss @@ -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}; } diff --git a/core/scss/ui/_nav.scss b/core/scss/ui/_nav.scss index df4a74dee..5584f0c57 100644 --- a/core/scss/ui/_nav.scss +++ b/core/scss/ui/_nav.scss @@ -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; diff --git a/docs/content/ui/components/button.mdx b/docs/content/ui/components/button.mdx index 2bc1e1ea8..36242b3a7 100644 --- a/docs/content/ui/components/button.mdx +++ b/docs/content/ui/components/button.mdx @@ -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.
diff --git a/docs/content/ui/layout/page-layouts.mdx b/docs/content/ui/layout/page-layouts.mdx index be67cb6d1..935470de1 100644 --- a/docs/content/ui/layout/page-layouts.mdx +++ b/docs/content/ui/layout/page-layouts.mdx @@ -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
-