@frame-kit/ui-ng-patterns 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ import * as i1 from '@angular/cdk/drag-drop';
17
17
  import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
18
18
  import { LoaderComponent } from '@frame-kit/ui-ng/ui/loader';
19
19
  import { DropdownMenuComponent, DropdownTriggerDirective, DropdownPanelDirective } from '@frame-kit/ui-ng/ui/dropdown-menu';
20
+ import { NavBrandComponent } from '@frame-kit/ui-ng/ui/nav-brand';
20
21
 
21
22
  /**
22
23
  * Page-level dashboard header: title + optional description on the left, up to
@@ -2190,6 +2191,114 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2190
2191
  args: ['attr.id']
2191
2192
  }] } });
2192
2193
 
2194
+ /**
2195
+ * The chrome of a signed-in dashboard, built on `fk-app-shell`: a full-height
2196
+ * aside with a brand header and collapse toggle, a sticky frosted topbar with
2197
+ * a mobile hamburger, a capped page area with an optional loading veil, and a
2198
+ * footer. Every dashboard that uses it gets the identical structure — the
2199
+ * same hamburger, the same collapse, the same scroll behaviour — and differs
2200
+ * only in what it projects.
2201
+ *
2202
+ * <fk-dashboard-layout [pageLoading]="navigating()">
2203
+ * <fk-nav-brand layoutBrand brandText="Acme" />
2204
+ * <fk-dashboard-nav layoutAside [groups]="groups()" [collapsed]="collapsed()" />
2205
+ * <nav layoutTopbarStart>Acme / Overview</nav>
2206
+ * <fk-user-menu layoutTopbarEnd … />
2207
+ * <router-outlet />
2208
+ * <span layoutFooter>© Acme</span>
2209
+ * </fk-dashboard-layout>
2210
+ *
2211
+ * A dashboard that needs a trailing panel — a documentation reader, a detail
2212
+ * inspector — projects it into `[layoutEnd]` and chooses its `endMode`. One
2213
+ * that needs a panel beside the page, such as a secondary navigation, projects
2214
+ * it into `[layoutContentPanel]`, which sits in the content row ahead of the
2215
+ * scroll container so the page scrolls under it rather than with it.
2216
+ *
2217
+ * The aside's toggle and the topbar's hamburger drive the underlying shell;
2218
+ * `collapsed` reports the rail state so projected controls can follow it.
2219
+ * The toggle glyphs are projection slots with a built-in fallback, so no icon
2220
+ * registry is required.
2221
+ */
2222
+ class DashboardLayoutComponent {
2223
+ // ===== INPUTS =====
2224
+ /** Passed to `fk-app-shell`. `icon` collapses to a rail; `side` hides entirely. */
2225
+ mode = input('icon', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2226
+ sidenavWidth = input('260px', ...(ngDevMode ? [{ debugName: "sidenavWidth" }] : /* istanbul ignore next */ []));
2227
+ collapsedWidth = input('64px', ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : /* istanbul ignore next */ []));
2228
+ /** Layout mode for the trailing panel, when `[layoutEnd]` is projected. */
2229
+ endMode = input('over', ...(ngDevMode ? [{ debugName: "endMode" }] : /* istanbul ignore next */ []));
2230
+ endWidth = input('400px', ...(ngDevMode ? [{ debugName: "endWidth" }] : /* istanbul ignore next */ []));
2231
+ endCollapsedWidth = input('64px', ...(ngDevMode ? [{ debugName: "endCollapsedWidth" }] : /* istanbul ignore next */ []));
2232
+ /** Veil the page area with the projected `[layoutPageLoader]` content. */
2233
+ pageLoading = input(false, ...(ngDevMode ? [{ debugName: "pageLoading" }] : /* istanbul ignore next */ []));
2234
+ /** Accessible name for the aside's collapse toggle. */
2235
+ toggleAriaLabel = input('Toggle navigation', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : /* istanbul ignore next */ []));
2236
+ /** Accessible name for the topbar's mobile hamburger. */
2237
+ menuAriaLabel = input('Open navigation', ...(ngDevMode ? [{ debugName: "menuAriaLabel" }] : /* istanbul ignore next */ []));
2238
+ // ===== BASE PROPS =====
2239
+ className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2240
+ id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
2241
+ // ===== COMPUTED =====
2242
+ shell = viewChild(AppShellComponent, ...(ngDevMode ? [{ debugName: "shell" }] : /* istanbul ignore next */ []));
2243
+ /** Whether the aside is in its icon rail. Projected controls follow this. */
2244
+ collapsed = computed(() => this.shell()?.isCollapsed() ?? false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2245
+ /** Whether the trailing panel is showing. Projected controls follow this. */
2246
+ endOpen = computed(() => this.shell()?.endOpen() ?? false, ...(ngDevMode ? [{ debugName: "endOpen" }] : /* istanbul ignore next */ []));
2247
+ classes = computed(() => [
2248
+ 'fk-dashboard-layout',
2249
+ this.collapsed() ? 'fk-dashboard-layout--collapsed' : '',
2250
+ this.pageLoading() ? 'fk-dashboard-layout--page-loading' : '',
2251
+ this.className(),
2252
+ ]
2253
+ .filter(Boolean)
2254
+ .join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2255
+ get hostClass() {
2256
+ return this.classes();
2257
+ }
2258
+ get hostId() {
2259
+ return this.id();
2260
+ }
2261
+ /** The aside header's toggle: collapse or expand the rail. */
2262
+ toggleSidenav() {
2263
+ this.shell()?.toggleSidenav();
2264
+ }
2265
+ /** The topbar hamburger: open the off-canvas aside on narrow viewports. */
2266
+ openSidenav() {
2267
+ this.shell()?.openSidenav();
2268
+ }
2269
+ /**
2270
+ * Collapse the rail from outside the header's own toggle — what a dashboard
2271
+ * calls when it opens the trailing panel and wants the room.
2272
+ */
2273
+ closeSidenav() {
2274
+ this.shell()?.closeSidenav();
2275
+ }
2276
+ /** Show the projected `[layoutEnd]` panel. */
2277
+ openEnd() {
2278
+ this.shell()?.openEnd();
2279
+ }
2280
+ /** Hide it. */
2281
+ closeEnd() {
2282
+ this.shell()?.closeEnd();
2283
+ }
2284
+ /** Hide it if it is showing, show it if it is not. */
2285
+ toggleEnd() {
2286
+ this.shell()?.toggleEnd();
2287
+ }
2288
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2289
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardLayoutComponent, isStandalone: true, selector: "fk-dashboard-layout", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, sidenavWidth: { classPropertyName: "sidenavWidth", publicName: "sidenavWidth", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, endMode: { classPropertyName: "endMode", publicName: "endMode", isSignal: true, isRequired: false, transformFunction: null }, endWidth: { classPropertyName: "endWidth", publicName: "endWidth", isSignal: true, isRequired: false, transformFunction: null }, endCollapsedWidth: { classPropertyName: "endCollapsedWidth", publicName: "endCollapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, pageLoading: { classPropertyName: "pageLoading", publicName: "pageLoading", isSignal: true, isRequired: false, transformFunction: null }, toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, menuAriaLabel: { classPropertyName: "menuAriaLabel", publicName: "menuAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId" } }, viewQueries: [{ propertyName: "shell", first: true, predicate: AppShellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Banners sit above the shell, in flow, inside the fixed-height host: the\n shell absorbs whatever height they leave, so a banner appearing never adds a\n document scrollbar.\n-->\n<ng-content select=\"[layoutBanner]\" />\n\n<fk-app-shell\n [mode]=\"mode()\"\n [sidenavWidth]=\"sidenavWidth()\"\n [collapsedWidth]=\"collapsedWidth()\"\n [endMode]=\"endMode()\"\n [endWidth]=\"endWidth()\"\n [endCollapsedWidth]=\"endCollapsedWidth()\"\n>\n <div appShellSidenav class=\"fk-dashboard-layout__aside\">\n <div class=\"fk-dashboard-layout__aside-header\">\n <div class=\"fk-dashboard-layout__brand\">\n <ng-content select=\"[layoutBrand]\" />\n </div>\n\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__toggle\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggleSidenav()\"\n >\n <ng-content select=\"[layoutToggleIcon]\" />\n </button>\n </div>\n\n <div class=\"fk-dashboard-layout__aside-body\">\n <ng-content select=\"[layoutAside]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__aside-footer\">\n <ng-content select=\"[layoutAsideFooter]\" />\n </div>\n </div>\n\n <div appShellContent class=\"fk-dashboard-layout__content\">\n <!--\n A panel beside the page \u2014 a secondary navigation, a filter rail \u2014 sits in\n the content row ahead of the scroll container, so the topbar and the page\n scroll under it rather than it scrolling with them.\n -->\n <ng-content select=\"[layoutContentPanel]\" />\n\n <!--\n The scroll container. The topbar sticks at its top and the page scrolls\n under it, which is what lets the frosted bar show content through.\n -->\n <div class=\"fk-dashboard-layout__body\">\n <header class=\"fk-dashboard-layout__topbar\">\n <div class=\"fk-dashboard-layout__topbar-inner\">\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__menu\"\n [attr.aria-label]=\"menuAriaLabel()\"\n (click)=\"openSidenav()\"\n >\n <ng-content select=\"[layoutMenuIcon]\" />\n </button>\n\n <div class=\"fk-dashboard-layout__topbar-start\">\n <ng-content select=\"[layoutTopbarStart]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__topbar-end\">\n <ng-content select=\"[layoutTopbarEnd]\" />\n </div>\n </div>\n </header>\n\n <div class=\"fk-dashboard-layout__page\">\n <ng-content />\n\n @if (pageLoading()) {\n <div class=\"fk-dashboard-layout__page-loader\" aria-live=\"polite\">\n <ng-content select=\"[layoutPageLoader]\" />\n </div>\n }\n </div>\n </div>\n </div>\n\n <!--\n A plain div, not a second `aside`: `fk-app-shell` already makes its end\n region an aside, and nesting one inside it would declare two landmarks for\n one panel.\n -->\n <div appShellEnd class=\"fk-dashboard-layout__end\">\n <ng-content select=\"[layoutEnd]\" />\n </div>\n\n <footer appShellFooter class=\"fk-dashboard-layout__footer\">\n <ng-content select=\"[layoutFooter]\" />\n </footer>\n</fk-app-shell>\n", styles: [":host{display:flex;flex-direction:column;height:var(--fk-dashboard-layout-height, 100vh);overflow:hidden;--_max-width: var(--fk-dashboard-layout-max-width, 1400px);--_topbar-height: var(--fk-dashboard-layout-topbar-height, 4rem)}fk-app-shell{flex:1;min-height:0}.fk-dashboard-layout__aside{display:flex;flex-direction:column;height:100%;background:var(--fk-dashboard-layout-aside-bg, var(--fk-color-surface, #ffffff));border-right:1px solid var(--fk-dashboard-layout-aside-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__aside-header{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem);flex-shrink:0;padding:var(--fk-dashboard-layout-aside-header-padding, var(--fk-rhythm-4, 1rem) var(--fk-rhythm-5, 1.25rem))}.fk-dashboard-layout__brand{flex:1 1 auto;min-width:0;display:flex;align-items:center}.fk-dashboard-layout__aside-body{flex:1;min-height:0;overflow-y:auto}.fk-dashboard-layout__aside-footer{flex-shrink:0}.fk-dashboard-layout__aside-footer:empty{display:none}.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{display:grid;place-items:center;flex:none;padding:0;border:1px solid transparent;border-radius:var(--fk-radius-sm, .25rem);background:none;color:var(--fk-dashboard-layout-toggle-color, var(--fk-color-muted, #8a98a8));cursor:pointer;transition:border-color .15s,background-color .15s,color .15s}.fk-dashboard-layout__toggle:hover,.fk-dashboard-layout__menu:hover{border-color:var(--fk-dashboard-layout-toggle-hover-border-color, var(--fk-color-border, #d9e2ee));background:var(--fk-dashboard-layout-toggle-hover-bg, var(--fk-color-surface, #ffffff));color:var(--fk-dashboard-layout-toggle-hover-color, var(--fk-color-text, #1f2d3d))}.fk-dashboard-layout__toggle:focus-visible,.fk-dashboard-layout__menu:focus-visible{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-dashboard-layout__toggle:not(:has(*)):before,.fk-dashboard-layout__menu:not(:has(*)):before{content:\"\";width:1rem;height:.75rem;background:linear-gradient(currentColor 0 2px,transparent 2px 5px,currentColor 5px 7px,transparent 7px 10px,currentColor 10px 12px)}.fk-dashboard-layout__toggle{width:var(--fk-dashboard-layout-toggle-size, 2rem);height:var(--fk-dashboard-layout-toggle-size, 2rem)}.fk-dashboard-layout__menu{width:var(--fk-dashboard-layout-menu-size, 2.25rem);height:var(--fk-dashboard-layout-menu-size, 2.25rem)}.fk-dashboard-layout__content{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__content:has(>:not(.fk-dashboard-layout__body)){flex-direction:row;width:100%}.fk-dashboard-layout__body{flex:1;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain}.fk-dashboard-layout__topbar{position:sticky;top:0;z-index:20;height:var(--_topbar-height);background:var(--fk-dashboard-layout-topbar-bg, rgba(251, 251, 253, .82));backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));-webkit-backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));border-bottom:1px solid var(--fk-dashboard-layout-topbar-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__topbar-inner{display:flex;align-items:center;gap:var(--fk-rhythm-4, 1rem);height:100%;max-width:var(--_max-width);margin-inline:auto;padding-inline:var(--fk-dashboard-layout-topbar-padding-inline, var(--fk-rhythm-8, 2rem))}.fk-dashboard-layout__topbar-start{display:flex;align-items:center;min-width:0}.fk-dashboard-layout__topbar-end{display:flex;align-items:center;gap:var(--fk-rhythm-3, .75rem);margin-left:auto}.fk-dashboard-layout__menu{display:none}.fk-dashboard-layout__end{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__end:empty{display:none}.fk-dashboard-layout__page{container-type:inline-size;position:relative;max-width:var(--_max-width);margin-inline:auto;padding:var(--fk-dashboard-layout-page-padding, var(--fk-rhythm-4, 1rem));min-height:calc(100% - var(--_topbar-height))}.fk-dashboard-layout__page-loader{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;min-height:12rem;background:var(--fk-dashboard-layout-page-loader-bg, color-mix(in srgb, var(--fk-color-bg, #fbfbfd) 72%, transparent));backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}.fk-dashboard-layout__footer{position:relative;z-index:100;display:flex;align-items:center;justify-content:center;flex-shrink:0;height:var(--fk-dashboard-layout-footer-height, 2.5rem);background:var(--fk-dashboard-layout-footer-bg, var(--fk-color-surface, #ffffff));border-top:1px solid var(--fk-dashboard-layout-footer-border-color, var(--fk-color-border, #d9e2ee));font-family:var(--fk-dashboard-layout-footer-font-family, var(--fk-font-family-base, sans-serif));font-size:var(--fk-dashboard-layout-footer-font-size, var(--fk-typography-small-font-size, .75rem));color:var(--fk-dashboard-layout-footer-color, var(--fk-color-muted, #8a98a8))}.fk-dashboard-layout__footer:empty{display:none}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__aside-header{justify-content:center;padding-inline:0}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__brand{display:none}@media(min-width:48em){.fk-dashboard-layout__page{padding:var(--fk-dashboard-layout-page-padding-wide, var(--fk-rhythm-6, 1.5rem) var(--fk-rhythm-8, 2rem))}}@media(max-width:760px){.fk-dashboard-layout__menu{display:grid}.fk-dashboard-layout__topbar-inner{padding-inline:var(--fk-rhythm-4, 1rem)}}@media(prefers-reduced-motion:reduce){.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{transition:none}}\n"], dependencies: [{ kind: "component", type: AppShellComponent, selector: "fk-app-shell", inputs: ["mode", "sidenavWidth", "collapsedWidth", "breakpoint", "sticky", "collapseOnMobile", "endMode", "endWidth", "endCollapsedWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2290
+ }
2291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardLayoutComponent, decorators: [{
2292
+ type: Component,
2293
+ args: [{ selector: 'fk-dashboard-layout', standalone: true, imports: [AppShellComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Banners sit above the shell, in flow, inside the fixed-height host: the\n shell absorbs whatever height they leave, so a banner appearing never adds a\n document scrollbar.\n-->\n<ng-content select=\"[layoutBanner]\" />\n\n<fk-app-shell\n [mode]=\"mode()\"\n [sidenavWidth]=\"sidenavWidth()\"\n [collapsedWidth]=\"collapsedWidth()\"\n [endMode]=\"endMode()\"\n [endWidth]=\"endWidth()\"\n [endCollapsedWidth]=\"endCollapsedWidth()\"\n>\n <div appShellSidenav class=\"fk-dashboard-layout__aside\">\n <div class=\"fk-dashboard-layout__aside-header\">\n <div class=\"fk-dashboard-layout__brand\">\n <ng-content select=\"[layoutBrand]\" />\n </div>\n\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__toggle\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggleSidenav()\"\n >\n <ng-content select=\"[layoutToggleIcon]\" />\n </button>\n </div>\n\n <div class=\"fk-dashboard-layout__aside-body\">\n <ng-content select=\"[layoutAside]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__aside-footer\">\n <ng-content select=\"[layoutAsideFooter]\" />\n </div>\n </div>\n\n <div appShellContent class=\"fk-dashboard-layout__content\">\n <!--\n A panel beside the page \u2014 a secondary navigation, a filter rail \u2014 sits in\n the content row ahead of the scroll container, so the topbar and the page\n scroll under it rather than it scrolling with them.\n -->\n <ng-content select=\"[layoutContentPanel]\" />\n\n <!--\n The scroll container. The topbar sticks at its top and the page scrolls\n under it, which is what lets the frosted bar show content through.\n -->\n <div class=\"fk-dashboard-layout__body\">\n <header class=\"fk-dashboard-layout__topbar\">\n <div class=\"fk-dashboard-layout__topbar-inner\">\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__menu\"\n [attr.aria-label]=\"menuAriaLabel()\"\n (click)=\"openSidenav()\"\n >\n <ng-content select=\"[layoutMenuIcon]\" />\n </button>\n\n <div class=\"fk-dashboard-layout__topbar-start\">\n <ng-content select=\"[layoutTopbarStart]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__topbar-end\">\n <ng-content select=\"[layoutTopbarEnd]\" />\n </div>\n </div>\n </header>\n\n <div class=\"fk-dashboard-layout__page\">\n <ng-content />\n\n @if (pageLoading()) {\n <div class=\"fk-dashboard-layout__page-loader\" aria-live=\"polite\">\n <ng-content select=\"[layoutPageLoader]\" />\n </div>\n }\n </div>\n </div>\n </div>\n\n <!--\n A plain div, not a second `aside`: `fk-app-shell` already makes its end\n region an aside, and nesting one inside it would declare two landmarks for\n one panel.\n -->\n <div appShellEnd class=\"fk-dashboard-layout__end\">\n <ng-content select=\"[layoutEnd]\" />\n </div>\n\n <footer appShellFooter class=\"fk-dashboard-layout__footer\">\n <ng-content select=\"[layoutFooter]\" />\n </footer>\n</fk-app-shell>\n", styles: [":host{display:flex;flex-direction:column;height:var(--fk-dashboard-layout-height, 100vh);overflow:hidden;--_max-width: var(--fk-dashboard-layout-max-width, 1400px);--_topbar-height: var(--fk-dashboard-layout-topbar-height, 4rem)}fk-app-shell{flex:1;min-height:0}.fk-dashboard-layout__aside{display:flex;flex-direction:column;height:100%;background:var(--fk-dashboard-layout-aside-bg, var(--fk-color-surface, #ffffff));border-right:1px solid var(--fk-dashboard-layout-aside-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__aside-header{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem);flex-shrink:0;padding:var(--fk-dashboard-layout-aside-header-padding, var(--fk-rhythm-4, 1rem) var(--fk-rhythm-5, 1.25rem))}.fk-dashboard-layout__brand{flex:1 1 auto;min-width:0;display:flex;align-items:center}.fk-dashboard-layout__aside-body{flex:1;min-height:0;overflow-y:auto}.fk-dashboard-layout__aside-footer{flex-shrink:0}.fk-dashboard-layout__aside-footer:empty{display:none}.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{display:grid;place-items:center;flex:none;padding:0;border:1px solid transparent;border-radius:var(--fk-radius-sm, .25rem);background:none;color:var(--fk-dashboard-layout-toggle-color, var(--fk-color-muted, #8a98a8));cursor:pointer;transition:border-color .15s,background-color .15s,color .15s}.fk-dashboard-layout__toggle:hover,.fk-dashboard-layout__menu:hover{border-color:var(--fk-dashboard-layout-toggle-hover-border-color, var(--fk-color-border, #d9e2ee));background:var(--fk-dashboard-layout-toggle-hover-bg, var(--fk-color-surface, #ffffff));color:var(--fk-dashboard-layout-toggle-hover-color, var(--fk-color-text, #1f2d3d))}.fk-dashboard-layout__toggle:focus-visible,.fk-dashboard-layout__menu:focus-visible{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-dashboard-layout__toggle:not(:has(*)):before,.fk-dashboard-layout__menu:not(:has(*)):before{content:\"\";width:1rem;height:.75rem;background:linear-gradient(currentColor 0 2px,transparent 2px 5px,currentColor 5px 7px,transparent 7px 10px,currentColor 10px 12px)}.fk-dashboard-layout__toggle{width:var(--fk-dashboard-layout-toggle-size, 2rem);height:var(--fk-dashboard-layout-toggle-size, 2rem)}.fk-dashboard-layout__menu{width:var(--fk-dashboard-layout-menu-size, 2.25rem);height:var(--fk-dashboard-layout-menu-size, 2.25rem)}.fk-dashboard-layout__content{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__content:has(>:not(.fk-dashboard-layout__body)){flex-direction:row;width:100%}.fk-dashboard-layout__body{flex:1;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain}.fk-dashboard-layout__topbar{position:sticky;top:0;z-index:20;height:var(--_topbar-height);background:var(--fk-dashboard-layout-topbar-bg, rgba(251, 251, 253, .82));backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));-webkit-backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));border-bottom:1px solid var(--fk-dashboard-layout-topbar-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__topbar-inner{display:flex;align-items:center;gap:var(--fk-rhythm-4, 1rem);height:100%;max-width:var(--_max-width);margin-inline:auto;padding-inline:var(--fk-dashboard-layout-topbar-padding-inline, var(--fk-rhythm-8, 2rem))}.fk-dashboard-layout__topbar-start{display:flex;align-items:center;min-width:0}.fk-dashboard-layout__topbar-end{display:flex;align-items:center;gap:var(--fk-rhythm-3, .75rem);margin-left:auto}.fk-dashboard-layout__menu{display:none}.fk-dashboard-layout__end{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__end:empty{display:none}.fk-dashboard-layout__page{container-type:inline-size;position:relative;max-width:var(--_max-width);margin-inline:auto;padding:var(--fk-dashboard-layout-page-padding, var(--fk-rhythm-4, 1rem));min-height:calc(100% - var(--_topbar-height))}.fk-dashboard-layout__page-loader{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;min-height:12rem;background:var(--fk-dashboard-layout-page-loader-bg, color-mix(in srgb, var(--fk-color-bg, #fbfbfd) 72%, transparent));backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}.fk-dashboard-layout__footer{position:relative;z-index:100;display:flex;align-items:center;justify-content:center;flex-shrink:0;height:var(--fk-dashboard-layout-footer-height, 2.5rem);background:var(--fk-dashboard-layout-footer-bg, var(--fk-color-surface, #ffffff));border-top:1px solid var(--fk-dashboard-layout-footer-border-color, var(--fk-color-border, #d9e2ee));font-family:var(--fk-dashboard-layout-footer-font-family, var(--fk-font-family-base, sans-serif));font-size:var(--fk-dashboard-layout-footer-font-size, var(--fk-typography-small-font-size, .75rem));color:var(--fk-dashboard-layout-footer-color, var(--fk-color-muted, #8a98a8))}.fk-dashboard-layout__footer:empty{display:none}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__aside-header{justify-content:center;padding-inline:0}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__brand{display:none}@media(min-width:48em){.fk-dashboard-layout__page{padding:var(--fk-dashboard-layout-page-padding-wide, var(--fk-rhythm-6, 1.5rem) var(--fk-rhythm-8, 2rem))}}@media(max-width:760px){.fk-dashboard-layout__menu{display:grid}.fk-dashboard-layout__topbar-inner{padding-inline:var(--fk-rhythm-4, 1rem)}}@media(prefers-reduced-motion:reduce){.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{transition:none}}\n"] }]
2294
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], sidenavWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidenavWidth", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], endMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "endMode", required: false }] }], endWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "endWidth", required: false }] }], endCollapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "endCollapsedWidth", required: false }] }], pageLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLoading", required: false }] }], toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], menuAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuAriaLabel", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], shell: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AppShellComponent), { isSignal: true }] }], hostClass: [{
2295
+ type: HostBinding,
2296
+ args: ['class']
2297
+ }], hostId: [{
2298
+ type: HostBinding,
2299
+ args: ['attr.id']
2300
+ }] } });
2301
+
2193
2302
  /**
2194
2303
  * A dashboard's sidebar navigation: the caller's groups, with items the
2195
2304
  * caller may not open already removed, rendered as one `fk-sidebar-nav` per
@@ -2215,8 +2324,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2215
2324
  class DashboardNavComponent {
2216
2325
  // ===== INPUTS =====
2217
2326
  groups = input.required(...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
2218
- /** Icon-rail mode, passed through to every segment. */
2219
- collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2327
+ /**
2328
+ * Icon-rail mode, passed through to every segment. Left `null` — the
2329
+ * default — each segment reads the collapse state of an ancestor
2330
+ * `fk-app-shell`, which is what `fk-dashboard-layout` is. Set it
2331
+ * explicitly only to drive the rail without a shell present.
2332
+ *
2333
+ * It defaults to inheriting rather than to `false` because the failure is
2334
+ * silent otherwise: a nav told nothing renders its expanded rows into a
2335
+ * collapsed rail and they are squashed, with no error to follow.
2336
+ */
2337
+ collapsed = input(null, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2220
2338
  /** Project `[navSlotBefore]` content immediately before this group. */
2221
2339
  slotBeforeGroupId = input(null, ...(ngDevMode ? [{ debugName: "slotBeforeGroupId" }] : /* istanbul ignore next */ []));
2222
2340
  /** Project default `<ng-content>` immediately after this group. */
@@ -2225,7 +2343,21 @@ class DashboardNavComponent {
2225
2343
  className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2226
2344
  id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
2227
2345
  ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2346
+ // ===== INJECTED =====
2347
+ // The layout first, and it matters which: `fk-app-shell` sits inside
2348
+ // `fk-dashboard-layout`'s own template, so a nav projected into the
2349
+ // layout is declared outside the shell and can never inject it. The
2350
+ // layout is the ancestor it is actually written inside.
2351
+ layout = inject(DashboardLayoutComponent, {
2352
+ optional: true,
2353
+ });
2354
+ shell = inject(AppShellComponent, { optional: true });
2228
2355
  // ===== COMPUTED =====
2356
+ /** The rail state in force: this input, else whichever ancestor owns it. */
2357
+ isCollapsed = computed(() => this.collapsed() ??
2358
+ this.layout?.collapsed() ??
2359
+ this.shell?.isCollapsed() ??
2360
+ false, ...(ngDevMode ? [{ debugName: "isCollapsed" }] : /* istanbul ignore next */ []));
2229
2361
  /**
2230
2362
  * Visible groups sliced into render segments, each carrying a single-group
2231
2363
  * `SidebarNavModel`.
@@ -2279,7 +2411,7 @@ class DashboardNavComponent {
2279
2411
  }, ...(ngDevMode ? [{ debugName: "segmentsAfterSlot" }] : /* istanbul ignore next */ []));
2280
2412
  classes = computed(() => [
2281
2413
  'fk-dashboard-nav',
2282
- this.collapsed() ? 'fk-dashboard-nav--collapsed' : '',
2414
+ this.isCollapsed() ? 'fk-dashboard-nav--collapsed' : '',
2283
2415
  this.className(),
2284
2416
  ]
2285
2417
  .filter(Boolean)
@@ -2302,11 +2434,11 @@ class DashboardNavComponent {
2302
2434
  return this.ariaLabel();
2303
2435
  }
2304
2436
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2305
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardNavComponent, isStandalone: true, selector: "fk-dashboard-nav", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, slotBeforeGroupId: { classPropertyName: "slotBeforeGroupId", publicName: "slotBeforeGroupId", isSignal: true, isRequired: false, transformFunction: null }, slotAfterGroupId: { classPropertyName: "slotAfterGroupId", publicName: "slotAfterGroupId", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId", "attr.role": "this.hostRole", "attr.aria-label": "this.hostAriaLabel" } }, ngImport: i0, template: "@for (segment of segmentsBeforeSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"collapsed()\" />\n}\n\n<!--\n Exactly one outlet for `[navSlotBefore]`, never inside a conditional. With\n no anchored group, or no groups at all, both loops are empty and the slot\n lands at the top \u2014 so a caller with nothing to navigate to still gets its\n switcher, which is the one way back to somewhere it can use.\n-->\n<ng-content select=\"[navSlotBefore]\" />\n\n@for (segment of segmentsAfterSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"collapsed()\" />\n @if (segment.after) {\n <ng-content />\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--fk-dashboard-nav-gap, var(--fk-rhythm-6, 1.5rem));padding:var(--fk-dashboard-nav-padding, var(--fk-rhythm-4, 1rem))}:host(.fk-dashboard-nav--collapsed){padding-inline:var(--fk-dashboard-nav-padding-inline-collapsed, var(--fk-rhythm-2, .5rem))}.fk-dashboard-nav__separator{border-top:1px solid var(--fk-dashboard-nav-separator-color, var(--fk-color-border, #d9e2ee))}\n"], dependencies: [{ kind: "component", type: SidebarNavComponent, selector: "fk-sidebar-nav", inputs: ["model", "collapsed", "className"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2437
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardNavComponent, isStandalone: true, selector: "fk-dashboard-nav", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, slotBeforeGroupId: { classPropertyName: "slotBeforeGroupId", publicName: "slotBeforeGroupId", isSignal: true, isRequired: false, transformFunction: null }, slotAfterGroupId: { classPropertyName: "slotAfterGroupId", publicName: "slotAfterGroupId", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId", "attr.role": "this.hostRole", "attr.aria-label": "this.hostAriaLabel" } }, ngImport: i0, template: "@for (segment of segmentsBeforeSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"isCollapsed()\" />\n}\n\n<!--\n Exactly one outlet for `[navSlotBefore]`, never inside a conditional. With\n no anchored group, or no groups at all, both loops are empty and the slot\n lands at the top \u2014 so a caller with nothing to navigate to still gets its\n switcher, which is the one way back to somewhere it can use.\n-->\n<ng-content select=\"[navSlotBefore]\" />\n\n@for (segment of segmentsAfterSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"isCollapsed()\" />\n @if (segment.after) {\n <ng-content />\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--fk-dashboard-nav-gap, var(--fk-rhythm-6, 1.5rem));padding:var(--fk-dashboard-nav-padding, var(--fk-rhythm-4, 1rem))}:host(.fk-dashboard-nav--collapsed){padding-inline:var(--fk-dashboard-nav-padding-inline-collapsed, var(--fk-rhythm-2, .5rem))}.fk-dashboard-nav__separator{border-top:1px solid var(--fk-dashboard-nav-separator-color, var(--fk-color-border, #d9e2ee))}\n"], dependencies: [{ kind: "component", type: SidebarNavComponent, selector: "fk-sidebar-nav", inputs: ["model", "collapsed", "className"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2306
2438
  }
2307
2439
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardNavComponent, decorators: [{
2308
2440
  type: Component,
2309
- args: [{ selector: 'fk-dashboard-nav', standalone: true, imports: [SidebarNavComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "@for (segment of segmentsBeforeSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"collapsed()\" />\n}\n\n<!--\n Exactly one outlet for `[navSlotBefore]`, never inside a conditional. With\n no anchored group, or no groups at all, both loops are empty and the slot\n lands at the top \u2014 so a caller with nothing to navigate to still gets its\n switcher, which is the one way back to somewhere it can use.\n-->\n<ng-content select=\"[navSlotBefore]\" />\n\n@for (segment of segmentsAfterSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"collapsed()\" />\n @if (segment.after) {\n <ng-content />\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--fk-dashboard-nav-gap, var(--fk-rhythm-6, 1.5rem));padding:var(--fk-dashboard-nav-padding, var(--fk-rhythm-4, 1rem))}:host(.fk-dashboard-nav--collapsed){padding-inline:var(--fk-dashboard-nav-padding-inline-collapsed, var(--fk-rhythm-2, .5rem))}.fk-dashboard-nav__separator{border-top:1px solid var(--fk-dashboard-nav-separator-color, var(--fk-color-border, #d9e2ee))}\n"] }]
2441
+ args: [{ selector: 'fk-dashboard-nav', standalone: true, imports: [SidebarNavComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "@for (segment of segmentsBeforeSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"isCollapsed()\" />\n}\n\n<!--\n Exactly one outlet for `[navSlotBefore]`, never inside a conditional. With\n no anchored group, or no groups at all, both loops are empty and the slot\n lands at the top \u2014 so a caller with nothing to navigate to still gets its\n switcher, which is the one way back to somewhere it can use.\n-->\n<ng-content select=\"[navSlotBefore]\" />\n\n@for (segment of segmentsAfterSlot(); track segment.id) {\n @if (segment.separated) {\n <div class=\"fk-dashboard-nav__separator\" aria-hidden=\"true\"></div>\n }\n <fk-sidebar-nav [model]=\"segment.model\" [collapsed]=\"isCollapsed()\" />\n @if (segment.after) {\n <ng-content />\n }\n}\n", styles: [":host{display:flex;flex-direction:column;gap:var(--fk-dashboard-nav-gap, var(--fk-rhythm-6, 1.5rem));padding:var(--fk-dashboard-nav-padding, var(--fk-rhythm-4, 1rem))}:host(.fk-dashboard-nav--collapsed){padding-inline:var(--fk-dashboard-nav-padding-inline-collapsed, var(--fk-rhythm-2, .5rem))}.fk-dashboard-nav__separator{border-top:1px solid var(--fk-dashboard-nav-separator-color, var(--fk-color-border, #d9e2ee))}\n"] }]
2310
2442
  }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: true }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], slotBeforeGroupId: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotBeforeGroupId", required: false }] }], slotAfterGroupId: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotAfterGroupId", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hostClass: [{
2311
2443
  type: HostBinding,
2312
2444
  args: ['class']
@@ -2330,116 +2462,107 @@ function toModelItem(item) {
2330
2462
  }
2331
2463
 
2332
2464
  /**
2333
- * The chrome of a signed-in dashboard, built on `fk-app-shell`: a full-height
2334
- * aside with a brand header and collapse toggle, a sticky frosted topbar with
2335
- * a mobile hamburger, a capped page area with an optional loading veil, and a
2336
- * footer. Every dashboard that uses it gets the identical structure — the
2337
- * same hamburger, the same collapse, the same scroll behaviour — and differs
2338
- * only in what it projects.
2339
- *
2340
- * <fk-dashboard-layout [pageLoading]="navigating()">
2341
- * <fk-nav-brand layoutBrand brandText="Acme" />
2342
- * <fk-dashboard-nav layoutAside [groups]="groups()" [collapsed]="collapsed()" />
2343
- * <nav layoutTopbarStart>Acme / Overview</nav>
2344
- * <fk-user-menu layoutTopbarEnd … />
2345
- * <router-outlet />
2346
- * <span layoutFooter>© Acme</span>
2347
- * </fk-dashboard-layout>
2348
- *
2349
- * A dashboard that needs a trailing panel — a documentation reader, a detail
2350
- * inspector — projects it into `[layoutEnd]` and chooses its `endMode`. One
2351
- * that needs a panel beside the page, such as a secondary navigation, projects
2352
- * it into `[layoutContentPanel]`, which sits in the content row ahead of the
2353
- * scroll container so the page scrolls under it rather than with it.
2354
- *
2355
- * The aside's toggle and the topbar's hamburger drive the underlying shell;
2356
- * `collapsed` reports the rail state so projected controls can follow it.
2357
- * The toggle glyphs are projection slots with a built-in fallback, so no icon
2358
- * registry is required.
2465
+ * A whole dashboard shell in one element: the collapsing aside and its
2466
+ * toggle, the brand block at its head, the icon-rail navigation, and the
2467
+ * banner, topbar, panel and footer slots a product hangs its own content
2468
+ * off.
2469
+ *
2470
+ * `fk-dashboard-layout` and `fk-dashboard-nav` remain the pieces to reach
2471
+ * for when a product wants a different arrangement. This is the arrangement
2472
+ * most products want, assembled once: every consumer that builds it by hand
2473
+ * repeats the same half-dozen bindings, and the ones that matter fail
2474
+ * quietly when forgotten rather than loudly.
2475
+ *
2476
+ * Everything a product supplies is projected on through, so nothing is
2477
+ * given up by using it: banners, a trailing panel, a second nav column
2478
+ * beside the page, switchers pinned into the rail.
2479
+ *
2480
+ * @example
2481
+ * ```html
2482
+ * <fk-dashboard-chrome
2483
+ * #chrome
2484
+ * brandText="Acme"
2485
+ * brandDescription="Admin"
2486
+ * brandHref="/"
2487
+ * [navGroups]="nav()"
2488
+ * >
2489
+ * <fk-icon-badge chromeBrandMark name="acme-mark" shape="rounded" size="sm" />
2490
+ * <nav chromeTopbarStart>…</nav>
2491
+ * <fk-user-menu chromeTopbarEnd … />
2492
+ * <router-outlet />
2493
+ * </fk-dashboard-chrome>
2494
+ * ```
2359
2495
  */
2360
- class DashboardLayoutComponent {
2361
- // ===== INPUTS =====
2362
- /** Passed to `fk-app-shell`. `icon` collapses to a rail; `side` hides entirely. */
2496
+ class DashboardChromeComponent {
2497
+ // ===== BRAND =====
2498
+ /** Product name beside the mark. */
2499
+ brandText = input('', ...(ngDevMode ? [{ debugName: "brandText" }] : /* istanbul ignore next */ []));
2500
+ /** The line under it — which product this is. */
2501
+ brandDescription = input('', ...(ngDevMode ? [{ debugName: "brandDescription" }] : /* istanbul ignore next */ []));
2502
+ /** Where the brand block navigates. */
2503
+ brandHref = input('/', ...(ngDevMode ? [{ debugName: "brandHref" }] : /* istanbul ignore next */ []));
2504
+ // ===== NAVIGATION =====
2505
+ /** Ordered nav groups, already filtered to what this reader may open. */
2506
+ navGroups = input([], ...(ngDevMode ? [{ debugName: "navGroups" }] : /* istanbul ignore next */ []));
2507
+ /** Group id the `[chromeNavBefore]` content is placed in front of. */
2508
+ navSlotBeforeGroupId = input(null, ...(ngDevMode ? [{ debugName: "navSlotBeforeGroupId" }] : /* istanbul ignore next */ []));
2509
+ /** Group id the `[chromeNavAfter]` content follows. */
2510
+ navSlotAfterGroupId = input(null, ...(ngDevMode ? [{ debugName: "navSlotAfterGroupId" }] : /* istanbul ignore next */ []));
2511
+ // ===== LAYOUT =====
2512
+ /** Aside behaviour. `icon` leaves a rail behind when collapsed. */
2363
2513
  mode = input('icon', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2364
- sidenavWidth = input('260px', ...(ngDevMode ? [{ debugName: "sidenavWidth" }] : /* istanbul ignore next */ []));
2365
- collapsedWidth = input('64px', ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : /* istanbul ignore next */ []));
2366
- /** Layout mode for the trailing panel, when `[layoutEnd]` is projected. */
2514
+ /** Trailing-panel behaviour, for a product that projects `[chromeEnd]`. */
2367
2515
  endMode = input('over', ...(ngDevMode ? [{ debugName: "endMode" }] : /* istanbul ignore next */ []));
2368
- endWidth = input('400px', ...(ngDevMode ? [{ debugName: "endWidth" }] : /* istanbul ignore next */ []));
2369
- endCollapsedWidth = input('64px', ...(ngDevMode ? [{ debugName: "endCollapsedWidth" }] : /* istanbul ignore next */ []));
2370
- /** Veil the page area with the projected `[layoutPageLoader]` content. */
2516
+ /** Covers the content column while a routed page resolves. */
2371
2517
  pageLoading = input(false, ...(ngDevMode ? [{ debugName: "pageLoading" }] : /* istanbul ignore next */ []));
2372
- /** Accessible name for the aside's collapse toggle. */
2373
- toggleAriaLabel = input('Toggle navigation', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : /* istanbul ignore next */ []));
2374
- /** Accessible name for the topbar's mobile hamburger. */
2375
- menuAriaLabel = input('Open navigation', ...(ngDevMode ? [{ debugName: "menuAriaLabel" }] : /* istanbul ignore next */ []));
2518
+ toggleAriaLabel = input('', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : /* istanbul ignore next */ []));
2519
+ menuAriaLabel = input('', ...(ngDevMode ? [{ debugName: "menuAriaLabel" }] : /* istanbul ignore next */ []));
2376
2520
  // ===== BASE PROPS =====
2377
2521
  className = input('', ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2378
- id = input(null, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
2379
- // ===== COMPUTED =====
2380
- shell = viewChild(AppShellComponent, ...(ngDevMode ? [{ debugName: "shell" }] : /* istanbul ignore next */ []));
2381
- /** Whether the aside is in its icon rail. Projected controls follow this. */
2382
- collapsed = computed(() => this.shell()?.isCollapsed() ?? false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2383
- /** Whether the trailing panel is showing. Projected controls follow this. */
2384
- endOpen = computed(() => this.shell()?.endOpen() ?? false, ...(ngDevMode ? [{ debugName: "endOpen" }] : /* istanbul ignore next */ []));
2385
- classes = computed(() => [
2386
- 'fk-dashboard-layout',
2387
- this.collapsed() ? 'fk-dashboard-layout--collapsed' : '',
2388
- this.pageLoading() ? 'fk-dashboard-layout--page-loading' : '',
2389
- this.className(),
2390
- ]
2391
- .filter(Boolean)
2392
- .join(' '), ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2393
- get hostClass() {
2394
- return this.classes();
2395
- }
2396
- get hostId() {
2397
- return this.id();
2398
- }
2399
- /** The aside header's toggle: collapse or expand the rail. */
2400
- toggleSidenav() {
2401
- this.shell()?.toggleSidenav();
2402
- }
2403
- /** The topbar hamburger: open the off-canvas aside on narrow viewports. */
2404
- openSidenav() {
2405
- this.shell()?.openSidenav();
2406
- }
2522
+ // Optional rather than required: content a product projects reads the
2523
+ // rail state before the view has resolved, and a required query throws
2524
+ // when read that early.
2525
+ layout = viewChild(DashboardLayoutComponent, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
2407
2526
  /**
2408
- * Collapse the rail from outside the header's own toggle — what a dashboard
2409
- * calls when it opens the trailing panel and wants the room.
2527
+ * Whether the aside is collapsed to its rail. For projected content that
2528
+ * has to follow it — a switcher pinned into the nav shrinks to its glyph
2529
+ * the same way the rows do.
2410
2530
  */
2531
+ collapsed() {
2532
+ return this.layout()?.collapsed() ?? false;
2533
+ }
2534
+ /** Whether the trailing panel is open. */
2535
+ endOpen() {
2536
+ return this.layout()?.endOpen() ?? false;
2537
+ }
2538
+ /** Collapse the aside to its rail. */
2411
2539
  closeSidenav() {
2412
- this.shell()?.closeSidenav();
2540
+ this.layout()?.closeSidenav();
2413
2541
  }
2414
- /** Show the projected `[layoutEnd]` panel. */
2542
+ /** Open the trailing panel. */
2415
2543
  openEnd() {
2416
- this.shell()?.openEnd();
2544
+ this.layout()?.openEnd();
2417
2545
  }
2418
- /** Hide it. */
2546
+ /** Close the trailing panel. */
2419
2547
  closeEnd() {
2420
- this.shell()?.closeEnd();
2421
- }
2422
- /** Hide it if it is showing, show it if it is not. */
2423
- toggleEnd() {
2424
- this.shell()?.toggleEnd();
2548
+ this.layout()?.closeEnd();
2425
2549
  }
2426
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2427
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: DashboardLayoutComponent, isStandalone: true, selector: "fk-dashboard-layout", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, sidenavWidth: { classPropertyName: "sidenavWidth", publicName: "sidenavWidth", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, endMode: { classPropertyName: "endMode", publicName: "endMode", isSignal: true, isRequired: false, transformFunction: null }, endWidth: { classPropertyName: "endWidth", publicName: "endWidth", isSignal: true, isRequired: false, transformFunction: null }, endCollapsedWidth: { classPropertyName: "endCollapsedWidth", publicName: "endCollapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, pageLoading: { classPropertyName: "pageLoading", publicName: "pageLoading", isSignal: true, isRequired: false, transformFunction: null }, toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, menuAriaLabel: { classPropertyName: "menuAriaLabel", publicName: "menuAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClass", "attr.id": "this.hostId" } }, viewQueries: [{ propertyName: "shell", first: true, predicate: AppShellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Banners sit above the shell, in flow, inside the fixed-height host: the\n shell absorbs whatever height they leave, so a banner appearing never adds a\n document scrollbar.\n-->\n<ng-content select=\"[layoutBanner]\" />\n\n<fk-app-shell\n [mode]=\"mode()\"\n [sidenavWidth]=\"sidenavWidth()\"\n [collapsedWidth]=\"collapsedWidth()\"\n [endMode]=\"endMode()\"\n [endWidth]=\"endWidth()\"\n [endCollapsedWidth]=\"endCollapsedWidth()\"\n>\n <div appShellSidenav class=\"fk-dashboard-layout__aside\">\n <div class=\"fk-dashboard-layout__aside-header\">\n <div class=\"fk-dashboard-layout__brand\">\n <ng-content select=\"[layoutBrand]\" />\n </div>\n\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__toggle\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggleSidenav()\"\n >\n <ng-content select=\"[layoutToggleIcon]\" />\n </button>\n </div>\n\n <div class=\"fk-dashboard-layout__aside-body\">\n <ng-content select=\"[layoutAside]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__aside-footer\">\n <ng-content select=\"[layoutAsideFooter]\" />\n </div>\n </div>\n\n <div appShellContent class=\"fk-dashboard-layout__content\">\n <!--\n A panel beside the page \u2014 a secondary navigation, a filter rail \u2014 sits in\n the content row ahead of the scroll container, so the topbar and the page\n scroll under it rather than it scrolling with them.\n -->\n <ng-content select=\"[layoutContentPanel]\" />\n\n <!--\n The scroll container. The topbar sticks at its top and the page scrolls\n under it, which is what lets the frosted bar show content through.\n -->\n <div class=\"fk-dashboard-layout__body\">\n <header class=\"fk-dashboard-layout__topbar\">\n <div class=\"fk-dashboard-layout__topbar-inner\">\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__menu\"\n [attr.aria-label]=\"menuAriaLabel()\"\n (click)=\"openSidenav()\"\n >\n <ng-content select=\"[layoutMenuIcon]\" />\n </button>\n\n <div class=\"fk-dashboard-layout__topbar-start\">\n <ng-content select=\"[layoutTopbarStart]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__topbar-end\">\n <ng-content select=\"[layoutTopbarEnd]\" />\n </div>\n </div>\n </header>\n\n <div class=\"fk-dashboard-layout__page\">\n <ng-content />\n\n @if (pageLoading()) {\n <div class=\"fk-dashboard-layout__page-loader\" aria-live=\"polite\">\n <ng-content select=\"[layoutPageLoader]\" />\n </div>\n }\n </div>\n </div>\n </div>\n\n <!--\n A plain div, not a second `aside`: `fk-app-shell` already makes its end\n region an aside, and nesting one inside it would declare two landmarks for\n one panel.\n -->\n <div appShellEnd class=\"fk-dashboard-layout__end\">\n <ng-content select=\"[layoutEnd]\" />\n </div>\n\n <footer appShellFooter class=\"fk-dashboard-layout__footer\">\n <ng-content select=\"[layoutFooter]\" />\n </footer>\n</fk-app-shell>\n", styles: [":host{display:flex;flex-direction:column;height:var(--fk-dashboard-layout-height, 100vh);overflow:hidden;--_max-width: var(--fk-dashboard-layout-max-width, 1400px);--_topbar-height: var(--fk-dashboard-layout-topbar-height, 4rem)}fk-app-shell{flex:1;min-height:0}.fk-dashboard-layout__aside{display:flex;flex-direction:column;height:100%;background:var(--fk-dashboard-layout-aside-bg, var(--fk-color-surface, #ffffff));border-right:1px solid var(--fk-dashboard-layout-aside-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__aside-header{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem);flex-shrink:0;padding:var(--fk-dashboard-layout-aside-header-padding, var(--fk-rhythm-4, 1rem) var(--fk-rhythm-5, 1.25rem))}.fk-dashboard-layout__brand{flex:1 1 auto;min-width:0;display:flex;align-items:center}.fk-dashboard-layout__aside-body{flex:1;min-height:0;overflow-y:auto}.fk-dashboard-layout__aside-footer{flex-shrink:0}.fk-dashboard-layout__aside-footer:empty{display:none}.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{display:grid;place-items:center;flex:none;padding:0;border:1px solid transparent;border-radius:var(--fk-radius-sm, .25rem);background:none;color:var(--fk-dashboard-layout-toggle-color, var(--fk-color-muted, #8a98a8));cursor:pointer;transition:border-color .15s,background-color .15s,color .15s}.fk-dashboard-layout__toggle:hover,.fk-dashboard-layout__menu:hover{border-color:var(--fk-dashboard-layout-toggle-hover-border-color, var(--fk-color-border, #d9e2ee));background:var(--fk-dashboard-layout-toggle-hover-bg, var(--fk-color-surface, #ffffff));color:var(--fk-dashboard-layout-toggle-hover-color, var(--fk-color-text, #1f2d3d))}.fk-dashboard-layout__toggle:focus-visible,.fk-dashboard-layout__menu:focus-visible{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-dashboard-layout__toggle:not(:has(*)):before,.fk-dashboard-layout__menu:not(:has(*)):before{content:\"\";width:1rem;height:.75rem;background:linear-gradient(currentColor 0 2px,transparent 2px 5px,currentColor 5px 7px,transparent 7px 10px,currentColor 10px 12px)}.fk-dashboard-layout__toggle{width:var(--fk-dashboard-layout-toggle-size, 2rem);height:var(--fk-dashboard-layout-toggle-size, 2rem)}.fk-dashboard-layout__menu{width:var(--fk-dashboard-layout-menu-size, 2.25rem);height:var(--fk-dashboard-layout-menu-size, 2.25rem)}.fk-dashboard-layout__content{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__content:has(>:not(.fk-dashboard-layout__body)){flex-direction:row;width:100%}.fk-dashboard-layout__body{flex:1;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain}.fk-dashboard-layout__topbar{position:sticky;top:0;z-index:20;height:var(--_topbar-height);background:var(--fk-dashboard-layout-topbar-bg, rgba(251, 251, 253, .82));backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));-webkit-backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));border-bottom:1px solid var(--fk-dashboard-layout-topbar-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__topbar-inner{display:flex;align-items:center;gap:var(--fk-rhythm-4, 1rem);height:100%;max-width:var(--_max-width);margin-inline:auto;padding-inline:var(--fk-dashboard-layout-topbar-padding-inline, var(--fk-rhythm-8, 2rem))}.fk-dashboard-layout__topbar-start{display:flex;align-items:center;min-width:0}.fk-dashboard-layout__topbar-end{display:flex;align-items:center;gap:var(--fk-rhythm-3, .75rem);margin-left:auto}.fk-dashboard-layout__menu{display:none}.fk-dashboard-layout__end{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__end:empty{display:none}.fk-dashboard-layout__page{container-type:inline-size;position:relative;max-width:var(--_max-width);margin-inline:auto;padding:var(--fk-dashboard-layout-page-padding, var(--fk-rhythm-4, 1rem));min-height:calc(100% - var(--_topbar-height))}.fk-dashboard-layout__page-loader{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;min-height:12rem;background:var(--fk-dashboard-layout-page-loader-bg, color-mix(in srgb, var(--fk-color-bg, #fbfbfd) 72%, transparent));backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}.fk-dashboard-layout__footer{position:relative;z-index:100;display:flex;align-items:center;justify-content:center;flex-shrink:0;height:var(--fk-dashboard-layout-footer-height, 2.5rem);background:var(--fk-dashboard-layout-footer-bg, var(--fk-color-surface, #ffffff));border-top:1px solid var(--fk-dashboard-layout-footer-border-color, var(--fk-color-border, #d9e2ee));font-family:var(--fk-dashboard-layout-footer-font-family, var(--fk-font-family-base, sans-serif));font-size:var(--fk-dashboard-layout-footer-font-size, var(--fk-typography-small-font-size, .75rem));color:var(--fk-dashboard-layout-footer-color, var(--fk-color-muted, #8a98a8))}.fk-dashboard-layout__footer:empty{display:none}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__aside-header{justify-content:center;padding-inline:0}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__brand{display:none}@media(min-width:48em){.fk-dashboard-layout__page{padding:var(--fk-dashboard-layout-page-padding-wide, var(--fk-rhythm-6, 1.5rem) var(--fk-rhythm-8, 2rem))}}@media(max-width:760px){.fk-dashboard-layout__menu{display:grid}.fk-dashboard-layout__topbar-inner{padding-inline:var(--fk-rhythm-4, 1rem)}}@media(prefers-reduced-motion:reduce){.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{transition:none}}\n"], dependencies: [{ kind: "component", type: AppShellComponent, selector: "fk-app-shell", inputs: ["mode", "sidenavWidth", "collapsedWidth", "breakpoint", "sticky", "collapseOnMobile", "endMode", "endWidth", "endCollapsedWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2550
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardChromeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2551
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.9", type: DashboardChromeComponent, isStandalone: true, selector: "fk-dashboard-chrome", inputs: { brandText: { classPropertyName: "brandText", publicName: "brandText", isSignal: true, isRequired: false, transformFunction: null }, brandDescription: { classPropertyName: "brandDescription", publicName: "brandDescription", isSignal: true, isRequired: false, transformFunction: null }, brandHref: { classPropertyName: "brandHref", publicName: "brandHref", isSignal: true, isRequired: false, transformFunction: null }, navGroups: { classPropertyName: "navGroups", publicName: "navGroups", isSignal: true, isRequired: false, transformFunction: null }, navSlotBeforeGroupId: { classPropertyName: "navSlotBeforeGroupId", publicName: "navSlotBeforeGroupId", isSignal: true, isRequired: false, transformFunction: null }, navSlotAfterGroupId: { classPropertyName: "navSlotAfterGroupId", publicName: "navSlotAfterGroupId", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, endMode: { classPropertyName: "endMode", publicName: "endMode", isSignal: true, isRequired: false, transformFunction: null }, pageLoading: { classPropertyName: "pageLoading", publicName: "pageLoading", isSignal: true, isRequired: false, transformFunction: null }, toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, menuAriaLabel: { classPropertyName: "menuAriaLabel", publicName: "menuAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "layout", first: true, predicate: DashboardLayoutComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fk-dashboard-layout\n [mode]=\"mode()\"\n [endMode]=\"endMode()\"\n [pageLoading]=\"pageLoading()\"\n [toggleAriaLabel]=\"toggleAriaLabel()\"\n [menuAriaLabel]=\"menuAriaLabel()\"\n [className]=\"className()\"\n>\n <ng-content select=\"[chromeBanner]\" ngProjectAs=\"[layoutBanner]\" />\n\n <fk-nav-brand\n layoutBrand\n [brandText]=\"brandText()\"\n [description]=\"brandDescription()\"\n [href]=\"brandHref()\"\n >\n <ng-content select=\"[chromeBrandMark]\" ngProjectAs=\"[navLogo]\" />\n </fk-nav-brand>\n\n <!--\n The layout draws its own hamburger without these, which would not match\n a product's icon set. Supplied here so no consumer has to know that.\n -->\n <fk-icon layoutToggleIcon name=\"menu-outline\" size=\"18px\" />\n <fk-icon layoutMenuIcon name=\"menu-outline\" />\n\n <!--\n No `collapsed` binding: the nav reads the rail state from this layout,\n which is the ancestor shell it looks for.\n -->\n <fk-dashboard-nav\n layoutAside\n [groups]=\"navGroups()\"\n [slotBeforeGroupId]=\"navSlotBeforeGroupId()\"\n [slotAfterGroupId]=\"navSlotAfterGroupId()\"\n >\n <ng-content select=\"[chromeNavBefore]\" ngProjectAs=\"[navSlotBefore]\" />\n <ng-content select=\"[chromeNavAfter]\" />\n </fk-dashboard-nav>\n\n <ng-content select=\"[chromeTopbarStart]\" ngProjectAs=\"[layoutTopbarStart]\" />\n <ng-content select=\"[chromeTopbarEnd]\" ngProjectAs=\"[layoutTopbarEnd]\" />\n\n <!-- The routed page, and anything else in the content column. -->\n <ng-content />\n\n <ng-content\n select=\"[chromeContentPanel]\"\n ngProjectAs=\"[layoutContentPanel]\"\n />\n <ng-content select=\"[chromeEnd]\" ngProjectAs=\"[layoutEnd]\" />\n <ng-content select=\"[chromeFooter]\" ngProjectAs=\"[layoutFooter]\" />\n</fk-dashboard-layout>\n", dependencies: [{ kind: "component", type: DashboardLayoutComponent, selector: "fk-dashboard-layout", inputs: ["mode", "sidenavWidth", "collapsedWidth", "endMode", "endWidth", "endCollapsedWidth", "pageLoading", "toggleAriaLabel", "menuAriaLabel", "className", "id"] }, { kind: "component", type: DashboardNavComponent, selector: "fk-dashboard-nav", inputs: ["groups", "collapsed", "slotBeforeGroupId", "slotAfterGroupId", "className", "id", "ariaLabel"] }, { kind: "component", type: IconComponent, selector: "fk-icon", inputs: ["name", "size", "color", "className", "id", "ariaLabel", "ariaHidden"] }, { kind: "component", type: NavBrandComponent, selector: "fk-nav-brand", inputs: ["brandText", "href", "description", "id", "className", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2428
2552
  }
2429
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardLayoutComponent, decorators: [{
2553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DashboardChromeComponent, decorators: [{
2430
2554
  type: Component,
2431
- args: [{ selector: 'fk-dashboard-layout', standalone: true, imports: [AppShellComponent], encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Banners sit above the shell, in flow, inside the fixed-height host: the\n shell absorbs whatever height they leave, so a banner appearing never adds a\n document scrollbar.\n-->\n<ng-content select=\"[layoutBanner]\" />\n\n<fk-app-shell\n [mode]=\"mode()\"\n [sidenavWidth]=\"sidenavWidth()\"\n [collapsedWidth]=\"collapsedWidth()\"\n [endMode]=\"endMode()\"\n [endWidth]=\"endWidth()\"\n [endCollapsedWidth]=\"endCollapsedWidth()\"\n>\n <div appShellSidenav class=\"fk-dashboard-layout__aside\">\n <div class=\"fk-dashboard-layout__aside-header\">\n <div class=\"fk-dashboard-layout__brand\">\n <ng-content select=\"[layoutBrand]\" />\n </div>\n\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__toggle\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggleSidenav()\"\n >\n <ng-content select=\"[layoutToggleIcon]\" />\n </button>\n </div>\n\n <div class=\"fk-dashboard-layout__aside-body\">\n <ng-content select=\"[layoutAside]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__aside-footer\">\n <ng-content select=\"[layoutAsideFooter]\" />\n </div>\n </div>\n\n <div appShellContent class=\"fk-dashboard-layout__content\">\n <!--\n A panel beside the page \u2014 a secondary navigation, a filter rail \u2014 sits in\n the content row ahead of the scroll container, so the topbar and the page\n scroll under it rather than it scrolling with them.\n -->\n <ng-content select=\"[layoutContentPanel]\" />\n\n <!--\n The scroll container. The topbar sticks at its top and the page scrolls\n under it, which is what lets the frosted bar show content through.\n -->\n <div class=\"fk-dashboard-layout__body\">\n <header class=\"fk-dashboard-layout__topbar\">\n <div class=\"fk-dashboard-layout__topbar-inner\">\n <button\n type=\"button\"\n class=\"fk-dashboard-layout__menu\"\n [attr.aria-label]=\"menuAriaLabel()\"\n (click)=\"openSidenav()\"\n >\n <ng-content select=\"[layoutMenuIcon]\" />\n </button>\n\n <div class=\"fk-dashboard-layout__topbar-start\">\n <ng-content select=\"[layoutTopbarStart]\" />\n </div>\n\n <div class=\"fk-dashboard-layout__topbar-end\">\n <ng-content select=\"[layoutTopbarEnd]\" />\n </div>\n </div>\n </header>\n\n <div class=\"fk-dashboard-layout__page\">\n <ng-content />\n\n @if (pageLoading()) {\n <div class=\"fk-dashboard-layout__page-loader\" aria-live=\"polite\">\n <ng-content select=\"[layoutPageLoader]\" />\n </div>\n }\n </div>\n </div>\n </div>\n\n <!--\n A plain div, not a second `aside`: `fk-app-shell` already makes its end\n region an aside, and nesting one inside it would declare two landmarks for\n one panel.\n -->\n <div appShellEnd class=\"fk-dashboard-layout__end\">\n <ng-content select=\"[layoutEnd]\" />\n </div>\n\n <footer appShellFooter class=\"fk-dashboard-layout__footer\">\n <ng-content select=\"[layoutFooter]\" />\n </footer>\n</fk-app-shell>\n", styles: [":host{display:flex;flex-direction:column;height:var(--fk-dashboard-layout-height, 100vh);overflow:hidden;--_max-width: var(--fk-dashboard-layout-max-width, 1400px);--_topbar-height: var(--fk-dashboard-layout-topbar-height, 4rem)}fk-app-shell{flex:1;min-height:0}.fk-dashboard-layout__aside{display:flex;flex-direction:column;height:100%;background:var(--fk-dashboard-layout-aside-bg, var(--fk-color-surface, #ffffff));border-right:1px solid var(--fk-dashboard-layout-aside-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__aside-header{display:flex;align-items:center;gap:var(--fk-rhythm-2, .5rem);flex-shrink:0;padding:var(--fk-dashboard-layout-aside-header-padding, var(--fk-rhythm-4, 1rem) var(--fk-rhythm-5, 1.25rem))}.fk-dashboard-layout__brand{flex:1 1 auto;min-width:0;display:flex;align-items:center}.fk-dashboard-layout__aside-body{flex:1;min-height:0;overflow-y:auto}.fk-dashboard-layout__aside-footer{flex-shrink:0}.fk-dashboard-layout__aside-footer:empty{display:none}.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{display:grid;place-items:center;flex:none;padding:0;border:1px solid transparent;border-radius:var(--fk-radius-sm, .25rem);background:none;color:var(--fk-dashboard-layout-toggle-color, var(--fk-color-muted, #8a98a8));cursor:pointer;transition:border-color .15s,background-color .15s,color .15s}.fk-dashboard-layout__toggle:hover,.fk-dashboard-layout__menu:hover{border-color:var(--fk-dashboard-layout-toggle-hover-border-color, var(--fk-color-border, #d9e2ee));background:var(--fk-dashboard-layout-toggle-hover-bg, var(--fk-color-surface, #ffffff));color:var(--fk-dashboard-layout-toggle-hover-color, var(--fk-color-text, #1f2d3d))}.fk-dashboard-layout__toggle:focus-visible,.fk-dashboard-layout__menu:focus-visible{outline:none;box-shadow:var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, .18))}.fk-dashboard-layout__toggle:not(:has(*)):before,.fk-dashboard-layout__menu:not(:has(*)):before{content:\"\";width:1rem;height:.75rem;background:linear-gradient(currentColor 0 2px,transparent 2px 5px,currentColor 5px 7px,transparent 7px 10px,currentColor 10px 12px)}.fk-dashboard-layout__toggle{width:var(--fk-dashboard-layout-toggle-size, 2rem);height:var(--fk-dashboard-layout-toggle-size, 2rem)}.fk-dashboard-layout__menu{width:var(--fk-dashboard-layout-menu-size, 2.25rem);height:var(--fk-dashboard-layout-menu-size, 2.25rem)}.fk-dashboard-layout__content{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__content:has(>:not(.fk-dashboard-layout__body)){flex-direction:row;width:100%}.fk-dashboard-layout__body{flex:1;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain}.fk-dashboard-layout__topbar{position:sticky;top:0;z-index:20;height:var(--_topbar-height);background:var(--fk-dashboard-layout-topbar-bg, rgba(251, 251, 253, .82));backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));-webkit-backdrop-filter:blur(var(--fk-dashboard-layout-topbar-blur, 14px));border-bottom:1px solid var(--fk-dashboard-layout-topbar-border-color, var(--fk-color-border, #d9e2ee))}.fk-dashboard-layout__topbar-inner{display:flex;align-items:center;gap:var(--fk-rhythm-4, 1rem);height:100%;max-width:var(--_max-width);margin-inline:auto;padding-inline:var(--fk-dashboard-layout-topbar-padding-inline, var(--fk-rhythm-8, 2rem))}.fk-dashboard-layout__topbar-start{display:flex;align-items:center;min-width:0}.fk-dashboard-layout__topbar-end{display:flex;align-items:center;gap:var(--fk-rhythm-3, .75rem);margin-left:auto}.fk-dashboard-layout__menu{display:none}.fk-dashboard-layout__end{display:flex;flex-direction:column;height:100%;min-height:0}.fk-dashboard-layout__end:empty{display:none}.fk-dashboard-layout__page{container-type:inline-size;position:relative;max-width:var(--_max-width);margin-inline:auto;padding:var(--fk-dashboard-layout-page-padding, var(--fk-rhythm-4, 1rem));min-height:calc(100% - var(--_topbar-height))}.fk-dashboard-layout__page-loader{position:absolute;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;min-height:12rem;background:var(--fk-dashboard-layout-page-loader-bg, color-mix(in srgb, var(--fk-color-bg, #fbfbfd) 72%, transparent));backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}.fk-dashboard-layout__footer{position:relative;z-index:100;display:flex;align-items:center;justify-content:center;flex-shrink:0;height:var(--fk-dashboard-layout-footer-height, 2.5rem);background:var(--fk-dashboard-layout-footer-bg, var(--fk-color-surface, #ffffff));border-top:1px solid var(--fk-dashboard-layout-footer-border-color, var(--fk-color-border, #d9e2ee));font-family:var(--fk-dashboard-layout-footer-font-family, var(--fk-font-family-base, sans-serif));font-size:var(--fk-dashboard-layout-footer-font-size, var(--fk-typography-small-font-size, .75rem));color:var(--fk-dashboard-layout-footer-color, var(--fk-color-muted, #8a98a8))}.fk-dashboard-layout__footer:empty{display:none}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__aside-header{justify-content:center;padding-inline:0}:host(.fk-dashboard-layout--collapsed) .fk-dashboard-layout__brand{display:none}@media(min-width:48em){.fk-dashboard-layout__page{padding:var(--fk-dashboard-layout-page-padding-wide, var(--fk-rhythm-6, 1.5rem) var(--fk-rhythm-8, 2rem))}}@media(max-width:760px){.fk-dashboard-layout__menu{display:grid}.fk-dashboard-layout__topbar-inner{padding-inline:var(--fk-rhythm-4, 1rem)}}@media(prefers-reduced-motion:reduce){.fk-dashboard-layout__toggle,.fk-dashboard-layout__menu{transition:none}}\n"] }]
2432
- }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], sidenavWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidenavWidth", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], endMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "endMode", required: false }] }], endWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "endWidth", required: false }] }], endCollapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "endCollapsedWidth", required: false }] }], pageLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLoading", required: false }] }], toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], menuAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuAriaLabel", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], shell: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AppShellComponent), { isSignal: true }] }], hostClass: [{
2433
- type: HostBinding,
2434
- args: ['class']
2435
- }], hostId: [{
2436
- type: HostBinding,
2437
- args: ['attr.id']
2438
- }] } });
2555
+ args: [{ selector: 'fk-dashboard-chrome', standalone: true, imports: [
2556
+ DashboardLayoutComponent,
2557
+ DashboardNavComponent,
2558
+ IconComponent,
2559
+ NavBrandComponent,
2560
+ ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<fk-dashboard-layout\n [mode]=\"mode()\"\n [endMode]=\"endMode()\"\n [pageLoading]=\"pageLoading()\"\n [toggleAriaLabel]=\"toggleAriaLabel()\"\n [menuAriaLabel]=\"menuAriaLabel()\"\n [className]=\"className()\"\n>\n <ng-content select=\"[chromeBanner]\" ngProjectAs=\"[layoutBanner]\" />\n\n <fk-nav-brand\n layoutBrand\n [brandText]=\"brandText()\"\n [description]=\"brandDescription()\"\n [href]=\"brandHref()\"\n >\n <ng-content select=\"[chromeBrandMark]\" ngProjectAs=\"[navLogo]\" />\n </fk-nav-brand>\n\n <!--\n The layout draws its own hamburger without these, which would not match\n a product's icon set. Supplied here so no consumer has to know that.\n -->\n <fk-icon layoutToggleIcon name=\"menu-outline\" size=\"18px\" />\n <fk-icon layoutMenuIcon name=\"menu-outline\" />\n\n <!--\n No `collapsed` binding: the nav reads the rail state from this layout,\n which is the ancestor shell it looks for.\n -->\n <fk-dashboard-nav\n layoutAside\n [groups]=\"navGroups()\"\n [slotBeforeGroupId]=\"navSlotBeforeGroupId()\"\n [slotAfterGroupId]=\"navSlotAfterGroupId()\"\n >\n <ng-content select=\"[chromeNavBefore]\" ngProjectAs=\"[navSlotBefore]\" />\n <ng-content select=\"[chromeNavAfter]\" />\n </fk-dashboard-nav>\n\n <ng-content select=\"[chromeTopbarStart]\" ngProjectAs=\"[layoutTopbarStart]\" />\n <ng-content select=\"[chromeTopbarEnd]\" ngProjectAs=\"[layoutTopbarEnd]\" />\n\n <!-- The routed page, and anything else in the content column. -->\n <ng-content />\n\n <ng-content\n select=\"[chromeContentPanel]\"\n ngProjectAs=\"[layoutContentPanel]\"\n />\n <ng-content select=\"[chromeEnd]\" ngProjectAs=\"[layoutEnd]\" />\n <ng-content select=\"[chromeFooter]\" ngProjectAs=\"[layoutFooter]\" />\n</fk-dashboard-layout>\n" }]
2561
+ }], propDecorators: { brandText: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandText", required: false }] }], brandDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandDescription", required: false }] }], brandHref: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandHref", required: false }] }], navGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "navGroups", required: false }] }], navSlotBeforeGroupId: [{ type: i0.Input, args: [{ isSignal: true, alias: "navSlotBeforeGroupId", required: false }] }], navSlotAfterGroupId: [{ type: i0.Input, args: [{ isSignal: true, alias: "navSlotAfterGroupId", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], endMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "endMode", required: false }] }], pageLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLoading", required: false }] }], toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], menuAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuAriaLabel", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], layout: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DashboardLayoutComponent), { isSignal: true }] }] } });
2439
2562
 
2440
2563
  /**
2441
2564
  * Generated bundle index. Do not edit.
2442
2565
  */
2443
2566
 
2444
- export { BackNavComponent, ColumnsComponent, ContentColumnComponent, ContentColumnHeadlineComponent, ContentDetailHeaderComponent, ContentDetailPageComponent, ContentPageSectionComponent, ContentRowComponent, ContentSplitLayoutComponent, ContextSwitcherComponent, DashboardLayoutComponent, DashboardNavComponent, DetailPanelComponent, DialogFooterComponent, DialogHeaderComponent, DialogShellComponent, InlineLoaderComponent, OnboardingGuideComponent, PageHeaderComponent, SearchBarLayoutComponent, SidebarNavComponent, SidebarNavItemDirective, TableEmptyStateComponent };
2567
+ export { BackNavComponent, ColumnsComponent, ContentColumnComponent, ContentColumnHeadlineComponent, ContentDetailHeaderComponent, ContentDetailPageComponent, ContentPageSectionComponent, ContentRowComponent, ContentSplitLayoutComponent, ContextSwitcherComponent, DashboardChromeComponent, DashboardLayoutComponent, DashboardNavComponent, DetailPanelComponent, DialogFooterComponent, DialogHeaderComponent, DialogShellComponent, InlineLoaderComponent, OnboardingGuideComponent, PageHeaderComponent, SearchBarLayoutComponent, SidebarNavComponent, SidebarNavItemDirective, TableEmptyStateComponent };
2445
2568
  //# sourceMappingURL=frame-kit-ui-ng-patterns-dashboard.mjs.map