fold-ng 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +27 -1
- package/fesm2022/fold-ng.mjs +83 -16
- package/fesm2022/fold-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/tsconfig.lib.tsbuildinfo +1 -1
- package/types/fold-ng.d.ts +65 -6
- package/types/fold-ng.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,31 @@ All notable changes to **fold-ng** are documented here. The format follows
|
|
|
8
8
|
|
|
9
9
|
_Nothing yet._
|
|
10
10
|
|
|
11
|
+
## [0.2.0] - 2026-07-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`fold-app-shell` — the mobile drawer is now a real modal.** While open, the
|
|
16
|
+
off-canvas primary rail is a named `role="dialog"` + `aria-modal="true"` (new
|
|
17
|
+
`drawerLabel` input, default `"Menu"`), and every other region is `inert` so a
|
|
18
|
+
screen reader can't wander behind it. New `drawerId` — exposed via
|
|
19
|
+
`exportAs="foldAppShell"` — lets the app point its hamburger's `aria-controls`
|
|
20
|
+
at the drawer (see the trigger contract in the component docs).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`fold-app-shell` skip-link** moves focus to `<main>` directly and prevents
|
|
25
|
+
the fragment navigation, so it works under hash routing (`withHashLocation`),
|
|
26
|
+
where a `#id` jump would otherwise be treated as a route change.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **`FoldComponentPanelDescriptor.ariaLabel`** is now typed `string | undefined`
|
|
31
|
+
(was `string`) — a non-breaking widening; the descriptor always carries the key.
|
|
32
|
+
- **Internals hardened**, no API change: the library now compiles under
|
|
33
|
+
`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `strictStandalone`,
|
|
34
|
+
`typeCheckHostBindings`, and the full `strictTypeChecked` ESLint preset.
|
|
35
|
+
|
|
11
36
|
## [0.1.0] - 2026-07-24
|
|
12
37
|
|
|
13
38
|
First public release line — **production-quality, pre-1.0 (`0.x`)**: fully
|
|
@@ -39,5 +64,6 @@ design-token stylesheet.
|
|
|
39
64
|
`currentColor`; `prefers-reduced-motion` + `forced-colors` are respected;
|
|
40
65
|
strings localise via inputs / providers (`provideFoldPanelLabels`).
|
|
41
66
|
|
|
42
|
-
[unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.
|
|
67
|
+
[unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.2.0...HEAD
|
|
68
|
+
[0.2.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.2.0
|
|
43
69
|
[0.1.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.1.0
|
package/fesm2022/fold-ng.mjs
CHANGED
|
@@ -63,6 +63,9 @@ class FocusTrapDirective {
|
|
|
63
63
|
}
|
|
64
64
|
const first = focusables[0];
|
|
65
65
|
const last = focusables[focusables.length - 1];
|
|
66
|
+
if (!first || !last) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
66
69
|
const active = this.document.activeElement;
|
|
67
70
|
if (event.shiftKey && active === first) {
|
|
68
71
|
event.preventDefault();
|
|
@@ -304,7 +307,38 @@ const MOBILE_BREAKPOINT = 768;
|
|
|
304
307
|
* ## Accessibility
|
|
305
308
|
* The shell renders a **skip-link** as its first Tab stop — jumping keyboard
|
|
306
309
|
* users past the rails straight to the `<main>` content (`skipLinkLabel` sets
|
|
307
|
-
* its text). The `<main>` is focusable (`tabindex="-1"`) so the jump lands
|
|
310
|
+
* its text). The `<main>` is focusable (`tabindex="-1"`) so the jump lands; the
|
|
311
|
+
* link moves focus directly rather than following the fragment, so it works
|
|
312
|
+
* under hash routing too.
|
|
313
|
+
*
|
|
314
|
+
* The mobile **drawer is a real modal**, matching the panel host's bar: while
|
|
315
|
+
* open it is a named `role="dialog"` + `aria-modal="true"` (`drawerLabel`), the
|
|
316
|
+
* focus-trap moves focus in and restores it to the trigger on close, and every
|
|
317
|
+
* other region (header, content, footer, second rail) is `inert` — so a
|
|
318
|
+
* screen-reader's virtual cursor can't wander behind the open drawer.
|
|
319
|
+
*
|
|
320
|
+
* **Trigger contract (app-owned).** The shell owns the drawer/dialog; the app
|
|
321
|
+
* owns the hamburger that opens it, so the shell can't set ARIA on that button —
|
|
322
|
+
* it exposes what's needed and documents the wiring. For the pairing to be fully
|
|
323
|
+
* announced, the hamburger should carry `[attr.aria-expanded]="navOpen()"`,
|
|
324
|
+
* `aria-haspopup="dialog"`, and `[attr.aria-controls]="shell.drawerId"` (read the
|
|
325
|
+
* shell via a template ref, `#shell="foldAppShell"`):
|
|
326
|
+
*
|
|
327
|
+
* ```html
|
|
328
|
+
* <fold-app-shell #shell="foldAppShell" [(mobileNavOpen)]="navOpen">
|
|
329
|
+
* <app-menu railPrimary />
|
|
330
|
+
* <header header>
|
|
331
|
+
* <button
|
|
332
|
+
* class="hamburger"
|
|
333
|
+
* aria-haspopup="dialog"
|
|
334
|
+
* [attr.aria-expanded]="navOpen()"
|
|
335
|
+
* [attr.aria-controls]="shell.drawerId"
|
|
336
|
+
* (click)="navOpen.set(!navOpen())"
|
|
337
|
+
* >☰</button>
|
|
338
|
+
* </header>
|
|
339
|
+
* <router-outlet />
|
|
340
|
+
* </fold-app-shell>
|
|
341
|
+
* ```
|
|
308
342
|
*
|
|
309
343
|
* @selector `fold-app-shell`
|
|
310
344
|
*
|
|
@@ -359,8 +393,21 @@ class FoldAppShellComponent {
|
|
|
359
393
|
* rails straight to `<main>`. Override for a non-English app. */
|
|
360
394
|
skipLinkLabel = input("Skip to content", /* @ts-ignore */
|
|
361
395
|
...(ngDevMode ? [{ debugName: "skipLinkLabel" }] : /* istanbul ignore next */ []));
|
|
396
|
+
/** The mobile drawer's accessible name — it becomes a `role="dialog"` while
|
|
397
|
+
* open, so it must be named. `"Menu"` by default (not `"Navigation"`, which
|
|
398
|
+
* would echo the `<nav>` landmark inside). Override for a non-English app. */
|
|
399
|
+
drawerLabel = input("Menu", /* @ts-ignore */
|
|
400
|
+
...(ngDevMode ? [{ debugName: "drawerLabel" }] : /* istanbul ignore next */ []));
|
|
401
|
+
document = inject(DOCUMENT);
|
|
362
402
|
/** SSR-safe id linking the skip-link to the `<main>` it targets. */
|
|
363
403
|
contentId = inject(FoldIdService).next("fold-shell-content");
|
|
404
|
+
/**
|
|
405
|
+
* SSR-safe id on the mobile drawer (the primary rail). Public and reachable
|
|
406
|
+
* via `exportAs="foldAppShell"` so the app's hamburger can point
|
|
407
|
+
* `aria-controls` at the dialog it opens — see the trigger contract in the
|
|
408
|
+
* class docs.
|
|
409
|
+
*/
|
|
410
|
+
drawerId = inject(FoldIdService).next("fold-shell-drawer");
|
|
364
411
|
/**
|
|
365
412
|
* How the primary rail's navigation is reached on mobile:
|
|
366
413
|
* - `"drawer"` (default) — the built-in off-canvas drawer slides the projected
|
|
@@ -422,6 +469,16 @@ class FoldAppShellComponent {
|
|
|
422
469
|
closeMobileNav() {
|
|
423
470
|
this.mobileNavOpen.set(false);
|
|
424
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Skip-link activation. Moves focus to `<main>` directly instead of letting
|
|
474
|
+
* the browser follow the `#id` fragment — a hash-routing app (`withHashLocation`)
|
|
475
|
+
* would treat that fragment as a route change. `preventDefault` keeps the URL
|
|
476
|
+
* clean; the anchor keeps its `href` for link semantics and no-JS fallback.
|
|
477
|
+
*/
|
|
478
|
+
skipToContent(event) {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
this.document.getElementById(this.contentId)?.focus();
|
|
481
|
+
}
|
|
425
482
|
/* Map each input to its CSS var — `null` when unset so the stylesheet
|
|
426
483
|
variable (or its fallback default) keeps winning; a set input overrides it. */
|
|
427
484
|
railWidthVar = computed(() => pxVar(this.railWidth()), /* @ts-ignore */
|
|
@@ -431,11 +488,11 @@ class FoldAppShellComponent {
|
|
|
431
488
|
headerHeightMobileVar = computed(() => pxVar(this.headerHeightMobile()), /* @ts-ignore */
|
|
432
489
|
...(ngDevMode ? [{ debugName: "headerHeightMobileVar" }] : /* istanbul ignore next */ []));
|
|
433
490
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldAppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
434
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldAppShellComponent, isStandalone: true, selector: "fold-app-shell", inputs: { railWidth: { classPropertyName: "railWidth", publicName: "railWidth", isSignal: true, isRequired: false, transformFunction: null }, headerHeight: { classPropertyName: "headerHeight", publicName: "headerHeight", isSignal: true, isRequired: false, transformFunction: null }, headerHeightMobile: { classPropertyName: "headerHeightMobile", publicName: "headerHeightMobile", isSignal: true, isRequired: false, transformFunction: null }, headerLayout: { classPropertyName: "headerLayout", publicName: "headerLayout", isSignal: true, isRequired: false, transformFunction: null }, footerLayout: { classPropertyName: "footerLayout", publicName: "footerLayout", isSignal: true, isRequired: false, transformFunction: null }, footerBehavior: { classPropertyName: "footerBehavior", publicName: "footerBehavior", isSignal: true, isRequired: false, transformFunction: null }, contentScroll: { classPropertyName: "contentScroll", publicName: "contentScroll", isSignal: true, isRequired: false, transformFunction: null }, skipLinkLabel: { classPropertyName: "skipLinkLabel", publicName: "skipLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, mobileNav: { classPropertyName: "mobileNav", publicName: "mobileNav", isSignal: true, isRequired: false, transformFunction: null }, mobileNavOpen: { classPropertyName: "mobileNavOpen", publicName: "mobileNavOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mobileNavOpen: "mobileNavOpenChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "style.--fold-shell-rail-width": "railWidthVar()", "style.--fold-shell-header-height": "headerHeightVar()", "style.--fold-shell-header-height-mobile": "headerHeightMobileVar()", "class.header-full": "headerLayout() === \"full\"", "class.footer-full": "footerLayout() === \"full\"", "class.footer-scroll": "footerBehavior() === \"scroll\"", "class.mobile-drawer": "mobileNav() === \"drawer\"", "class.mobile-nav-open": "drawerOpen()", "class.content-auto": "contentScroll() === \"auto\"" } }, ngImport: i0, template: "<!-- Skip-link: the first Tab stop \u2014 visually hidden until focused, then jumps\n keyboard users past the rails to <main> (which is focusable, tabindex=-1). -->\n<a class=\"skip-link\" [href]=\"'#' + contentId\">{{ skipLinkLabel() }}</a>\n\n<div\n class=\"rail-primary\"\n foldSurface=\"chrome\"\n [foldFocusTrap]=\"drawerOpen()\"\n tabindex=\"-1\"\n>\n <ng-content select=\"[railPrimary]\" />\n</div>\n<div class=\"rail-secondary\" foldSurface=\"chrome\">\n <ng-content select=\"[railSecondary]\" />\n</div>\n\n<!-- Mobile drawer scrim \u2014 only rendered while the drawer is actually open (the\n shell is narrow AND mobileNavOpen). A darkening veil that dismisses on\n click; Escape and widening close it too. aria-hidden: the focus-trapped\n drawer + Escape are the accessible path, the scrim is a mouse convenience. -->\n@if (drawerOpen()) {\n <div class=\"mobile-scrim\" (click)=\"closeMobileNav()\" aria-hidden=\"true\"></div>\n}\n<header class=\"header\" foldSurface=\"chrome\">\n <ng-content select=\"[header]\" />\n</header>\n\n<!-- One footer, stamped in one of two positions \u2014 a grid row (pinned, always in\n sight) or inside the content scroll (revealed on the way down). A single\n <ng-content> in a template avoids duplicate-selector projection loss. -->\n<ng-template #footerTpl>\n <footer\n class=\"footer\"\n [class.footer-inflow]=\"footerBehavior() === 'scroll'\"\n foldSurface=\"chrome\"\n >\n <ng-content select=\"[footer]\" />\n </footer>\n</ng-template>\n\n<main class=\"content\" foldSurface=\"page\" [id]=\"contentId\" tabindex=\"-1\">\n <ng-content />\n @if (footerBehavior() === \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n }\n</main>\n@if (footerBehavior() !== \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n}\n", styles: ["@charset \"UTF-8\";:host{display:grid;height:100%;width:100%;position:relative;box-sizing:border-box;overflow:hidden;background:var(--fold-color-bg-page);grid-template-columns:auto auto minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height, 56px) 1fr;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\"}.skip-link{position:absolute;top:0;left:0;z-index:60;margin:var(--fold-space-sm, 8px);padding:8px 14px;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);color:var(--fold-color-text);font-size:var(--fold-text-sm);font-weight:600;text-decoration:none;transform:translateY(-150%);transition:transform .12s ease}.skip-link:focus{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.skip-link{transition:none}}:host(.header-full){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\"}:host(:not(.footer-scroll):has([footer])){grid-template-rows:var(--fold-shell-header-height, 56px) 1fr auto;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.header-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}.rail-primary{grid-area:rail-primary;min-height:0}.rail-secondary{grid-area:rail-secondary;min-height:0}.header{grid-area:header;min-width:0}.footer{grid-area:footer;min-width:0;min-height:0}.content{grid-area:content;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}.content:focus{outline:none}:host(.footer-scroll) .content,:host(.content-auto) .content{overflow:auto}.footer-inflow{margin-top:auto}.rail-primary:has([data-elevated]),.rail-secondary:has([data-elevated]),.header:has([data-elevated]),.footer:has([data-elevated]),.content:has([data-elevated]){padding:var(--fold-surface-inset, var(--fold-space-md))}@media(max-width:768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@container (max-width: 1024px){.rail-secondary{display:none}}@container (max-width: 768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@keyframes fold-shell-scrim-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.rail-primary,.mobile-scrim{transition:none;animation:none}}\n"], dependencies: [{ kind: "directive", type: FoldSurfaceDirective, selector: "[foldSurface]", inputs: ["foldSurface"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FocusTrapDirective, selector: "[foldFocusTrap]", inputs: ["foldFocusTrap"] }] });
|
|
491
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldAppShellComponent, isStandalone: true, selector: "fold-app-shell", inputs: { railWidth: { classPropertyName: "railWidth", publicName: "railWidth", isSignal: true, isRequired: false, transformFunction: null }, headerHeight: { classPropertyName: "headerHeight", publicName: "headerHeight", isSignal: true, isRequired: false, transformFunction: null }, headerHeightMobile: { classPropertyName: "headerHeightMobile", publicName: "headerHeightMobile", isSignal: true, isRequired: false, transformFunction: null }, headerLayout: { classPropertyName: "headerLayout", publicName: "headerLayout", isSignal: true, isRequired: false, transformFunction: null }, footerLayout: { classPropertyName: "footerLayout", publicName: "footerLayout", isSignal: true, isRequired: false, transformFunction: null }, footerBehavior: { classPropertyName: "footerBehavior", publicName: "footerBehavior", isSignal: true, isRequired: false, transformFunction: null }, contentScroll: { classPropertyName: "contentScroll", publicName: "contentScroll", isSignal: true, isRequired: false, transformFunction: null }, skipLinkLabel: { classPropertyName: "skipLinkLabel", publicName: "skipLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, drawerLabel: { classPropertyName: "drawerLabel", publicName: "drawerLabel", isSignal: true, isRequired: false, transformFunction: null }, mobileNav: { classPropertyName: "mobileNav", publicName: "mobileNav", isSignal: true, isRequired: false, transformFunction: null }, mobileNavOpen: { classPropertyName: "mobileNavOpen", publicName: "mobileNavOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mobileNavOpen: "mobileNavOpenChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "style.--fold-shell-rail-width": "railWidthVar()", "style.--fold-shell-header-height": "headerHeightVar()", "style.--fold-shell-header-height-mobile": "headerHeightMobileVar()", "class.header-full": "headerLayout() === \"full\"", "class.footer-full": "footerLayout() === \"full\"", "class.footer-scroll": "footerBehavior() === \"scroll\"", "class.mobile-drawer": "mobileNav() === \"drawer\"", "class.mobile-nav-open": "drawerOpen()", "class.content-auto": "contentScroll() === \"auto\"" } }, exportAs: ["foldAppShell"], ngImport: i0, template: "<!-- Skip-link: the first Tab stop \u2014 visually hidden until focused, then jumps\n keyboard users past the rails to <main> (which is focusable, tabindex=-1). -->\n<a class=\"skip-link\" [href]=\"'#' + contentId\" (click)=\"skipToContent($event)\">{{\n skipLinkLabel()\n}}</a>\n\n<!-- The primary rail. While the mobile drawer is open it is a real modal dialog\n (named, aria-modal, focus-trapped) and every sibling region is inert \u2014 the\n same bar the panel host holds; on desktop it's a plain nav rail. -->\n<div\n class=\"rail-primary\"\n [id]=\"drawerId\"\n foldSurface=\"chrome\"\n [foldFocusTrap]=\"drawerOpen()\"\n [attr.role]=\"drawerOpen() ? 'dialog' : null\"\n [attr.aria-modal]=\"drawerOpen() ? 'true' : null\"\n [attr.aria-label]=\"drawerOpen() ? drawerLabel() : null\"\n tabindex=\"-1\"\n>\n <ng-content select=\"[railPrimary]\" />\n</div>\n<div class=\"rail-secondary\" foldSurface=\"chrome\" [inert]=\"drawerOpen()\">\n <ng-content select=\"[railSecondary]\" />\n</div>\n\n<!-- Mobile drawer scrim \u2014 only rendered while the drawer is actually open (the\n shell is narrow AND mobileNavOpen). A darkening veil that dismisses on\n click; Escape and widening close it too. aria-hidden: the focus-trapped\n drawer + Escape are the accessible path, the scrim is a mouse convenience. -->\n@if (drawerOpen()) {\n <div class=\"mobile-scrim\" (click)=\"closeMobileNav()\" aria-hidden=\"true\"></div>\n}\n<header class=\"header\" foldSurface=\"chrome\" [inert]=\"drawerOpen()\">\n <ng-content select=\"[header]\" />\n</header>\n\n<!-- One footer, stamped in one of two positions \u2014 a grid row (pinned, always in\n sight) or inside the content scroll (revealed on the way down). A single\n <ng-content> in a template avoids duplicate-selector projection loss. -->\n<ng-template #footerTpl>\n <footer\n class=\"footer\"\n [class.footer-inflow]=\"footerBehavior() === 'scroll'\"\n foldSurface=\"chrome\"\n [inert]=\"drawerOpen()\"\n >\n <ng-content select=\"[footer]\" />\n </footer>\n</ng-template>\n\n<main\n class=\"content\"\n foldSurface=\"page\"\n [id]=\"contentId\"\n tabindex=\"-1\"\n [inert]=\"drawerOpen()\"\n>\n <ng-content />\n @if (footerBehavior() === \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n }\n</main>\n@if (footerBehavior() !== \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n}\n", styles: ["@charset \"UTF-8\";:host{display:grid;height:100%;width:100%;position:relative;box-sizing:border-box;overflow:hidden;background:var(--fold-color-bg-page);grid-template-columns:auto auto minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height, 56px) 1fr;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\"}.skip-link{position:absolute;top:0;left:0;z-index:60;margin:var(--fold-space-sm, 8px);padding:8px 14px;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);color:var(--fold-color-text);font-size:var(--fold-text-sm);font-weight:600;text-decoration:none;transform:translateY(-150%);transition:transform .12s ease}.skip-link:focus{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.skip-link{transition:none}}:host(.header-full){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\"}:host(:not(.footer-scroll):has([footer])){grid-template-rows:var(--fold-shell-header-height, 56px) 1fr auto;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.header-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}.rail-primary{grid-area:rail-primary;min-height:0}.rail-secondary{grid-area:rail-secondary;min-height:0}.header{grid-area:header;min-width:0}.footer{grid-area:footer;min-width:0;min-height:0}.content{grid-area:content;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}.content:focus{outline:none}:host(.footer-scroll) .content,:host(.content-auto) .content{overflow:auto}.footer-inflow{margin-top:auto}.rail-primary:has([data-elevated]),.rail-secondary:has([data-elevated]),.header:has([data-elevated]),.footer:has([data-elevated]),.content:has([data-elevated]){padding:var(--fold-surface-inset, var(--fold-space-md))}@media(max-width:768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@container (max-width: 1024px){.rail-secondary{display:none}}@container (max-width: 768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@keyframes fold-shell-scrim-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.rail-primary,.mobile-scrim{transition:none;animation:none}}\n"], dependencies: [{ kind: "directive", type: FoldSurfaceDirective, selector: "[foldSurface]", inputs: ["foldSurface"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: FocusTrapDirective, selector: "[foldFocusTrap]", inputs: ["foldFocusTrap"] }] });
|
|
435
492
|
}
|
|
436
493
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldAppShellComponent, decorators: [{
|
|
437
494
|
type: Component,
|
|
438
|
-
args: [{ selector: "fold-app-shell", standalone: true, imports: [FoldSurfaceDirective, NgTemplateOutlet, FocusTrapDirective], host: {
|
|
495
|
+
args: [{ selector: "fold-app-shell", standalone: true, exportAs: "foldAppShell", imports: [FoldSurfaceDirective, NgTemplateOutlet, FocusTrapDirective], host: {
|
|
439
496
|
"[style.--fold-shell-rail-width]": "railWidthVar()",
|
|
440
497
|
"[style.--fold-shell-header-height]": "headerHeightVar()",
|
|
441
498
|
"[style.--fold-shell-header-height-mobile]": "headerHeightMobileVar()",
|
|
@@ -445,8 +502,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
445
502
|
"[class.mobile-drawer]": 'mobileNav() === "drawer"',
|
|
446
503
|
"[class.mobile-nav-open]": "drawerOpen()",
|
|
447
504
|
"[class.content-auto]": 'contentScroll() === "auto"',
|
|
448
|
-
}, template: "<!-- Skip-link: the first Tab stop \u2014 visually hidden until focused, then jumps\n keyboard users past the rails to <main> (which is focusable, tabindex=-1). -->\n<a class=\"skip-link\" [href]=\"'#' + contentId\">{{
|
|
449
|
-
}], ctorParameters: () => [], propDecorators: { railWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "railWidth", required: false }] }], headerHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerHeight", required: false }] }], headerHeightMobile: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerHeightMobile", required: false }] }], headerLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerLayout", required: false }] }], footerLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLayout", required: false }] }], footerBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerBehavior", required: false }] }], contentScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentScroll", required: false }] }], skipLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkLabel", required: false }] }], mobileNav: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileNav", required: false }] }], mobileNavOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileNavOpen", required: false }] }, { type: i0.Output, args: ["mobileNavOpenChange"] }], onEscape: [{
|
|
505
|
+
}, template: "<!-- Skip-link: the first Tab stop \u2014 visually hidden until focused, then jumps\n keyboard users past the rails to <main> (which is focusable, tabindex=-1). -->\n<a class=\"skip-link\" [href]=\"'#' + contentId\" (click)=\"skipToContent($event)\">{{\n skipLinkLabel()\n}}</a>\n\n<!-- The primary rail. While the mobile drawer is open it is a real modal dialog\n (named, aria-modal, focus-trapped) and every sibling region is inert \u2014 the\n same bar the panel host holds; on desktop it's a plain nav rail. -->\n<div\n class=\"rail-primary\"\n [id]=\"drawerId\"\n foldSurface=\"chrome\"\n [foldFocusTrap]=\"drawerOpen()\"\n [attr.role]=\"drawerOpen() ? 'dialog' : null\"\n [attr.aria-modal]=\"drawerOpen() ? 'true' : null\"\n [attr.aria-label]=\"drawerOpen() ? drawerLabel() : null\"\n tabindex=\"-1\"\n>\n <ng-content select=\"[railPrimary]\" />\n</div>\n<div class=\"rail-secondary\" foldSurface=\"chrome\" [inert]=\"drawerOpen()\">\n <ng-content select=\"[railSecondary]\" />\n</div>\n\n<!-- Mobile drawer scrim \u2014 only rendered while the drawer is actually open (the\n shell is narrow AND mobileNavOpen). A darkening veil that dismisses on\n click; Escape and widening close it too. aria-hidden: the focus-trapped\n drawer + Escape are the accessible path, the scrim is a mouse convenience. -->\n@if (drawerOpen()) {\n <div class=\"mobile-scrim\" (click)=\"closeMobileNav()\" aria-hidden=\"true\"></div>\n}\n<header class=\"header\" foldSurface=\"chrome\" [inert]=\"drawerOpen()\">\n <ng-content select=\"[header]\" />\n</header>\n\n<!-- One footer, stamped in one of two positions \u2014 a grid row (pinned, always in\n sight) or inside the content scroll (revealed on the way down). A single\n <ng-content> in a template avoids duplicate-selector projection loss. -->\n<ng-template #footerTpl>\n <footer\n class=\"footer\"\n [class.footer-inflow]=\"footerBehavior() === 'scroll'\"\n foldSurface=\"chrome\"\n [inert]=\"drawerOpen()\"\n >\n <ng-content select=\"[footer]\" />\n </footer>\n</ng-template>\n\n<main\n class=\"content\"\n foldSurface=\"page\"\n [id]=\"contentId\"\n tabindex=\"-1\"\n [inert]=\"drawerOpen()\"\n>\n <ng-content />\n @if (footerBehavior() === \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n }\n</main>\n@if (footerBehavior() !== \"scroll\") {\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n}\n", styles: ["@charset \"UTF-8\";:host{display:grid;height:100%;width:100%;position:relative;box-sizing:border-box;overflow:hidden;background:var(--fold-color-bg-page);grid-template-columns:auto auto minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height, 56px) 1fr;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\"}.skip-link{position:absolute;top:0;left:0;z-index:60;margin:var(--fold-space-sm, 8px);padding:8px 14px;border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-md);background:var(--fold-color-surface-card);color:var(--fold-color-text);font-size:var(--fold-text-sm);font-weight:600;text-decoration:none;transform:translateY(-150%);transition:transform .12s ease}.skip-link:focus{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.skip-link{transition:none}}:host(.header-full){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\"}:host(:not(.footer-scroll):has([footer])){grid-template-rows:var(--fold-shell-header-height, 56px) 1fr auto;grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.header-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"rail-primary rail-secondary footer\"}:host(.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"rail-primary rail-secondary header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-areas:\"header header header\" \"rail-primary rail-secondary content\" \"footer footer footer\"}.rail-primary{grid-area:rail-primary;min-height:0}.rail-secondary{grid-area:rail-secondary;min-height:0}.header{grid-area:header;min-width:0}.footer{grid-area:footer;min-width:0;min-height:0}.content{grid-area:content;position:relative;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}.content:focus{outline:none}:host(.footer-scroll) .content,:host(.content-auto) .content{overflow:auto}.footer-inflow{margin-top:auto}.rail-primary:has([data-elevated]),.rail-secondary:has([data-elevated]),.header:has([data-elevated]),.footer:has([data-elevated]),.content:has([data-elevated]){padding:var(--fold-surface-inset, var(--fold-space-md))}@media(max-width:768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@container (max-width: 1024px){.rail-secondary{display:none}}@container (max-width: 768px){:host,:host(.header-full){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr;grid-template-areas:\"header\" \"content\"}:host(:not(.footer-scroll):has([footer])),:host(.header-full:not(.footer-scroll):has([footer])),:host(.footer-full:not(.footer-scroll):has([footer])),:host(.header-full.footer-full:not(.footer-scroll):has([footer])){grid-template-columns:minmax(0,1fr);grid-template-rows:var(--fold-shell-header-height-mobile, 52px) 1fr auto;grid-template-areas:\"header\" \"content\" \"footer\"}.rail-primary,.rail-secondary{display:none}:host(.mobile-drawer) .rail-primary{display:block;position:absolute;top:var(--fold-shell-header-height-mobile, 52px);bottom:0;left:0;z-index:41;width:auto;max-width:85%;overflow-y:auto;visibility:hidden;transform:translate(-100%);transition:transform .24s cubic-bezier(.4,0,.2,1),visibility .24s}:host(.mobile-drawer.mobile-nav-open) .rail-primary{visibility:visible;transform:translate(0)}.mobile-scrim{position:absolute;inset:var(--fold-shell-header-height-mobile, 52px) 0 0 0;z-index:40;border:0;padding:0;background:var(--fold-color-scrim);animation:fold-shell-scrim-in .24s ease}:host(.mobile-nav-open) .content{overflow:hidden}}@keyframes fold-shell-scrim-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.rail-primary,.mobile-scrim{transition:none;animation:none}}\n"] }]
|
|
506
|
+
}], ctorParameters: () => [], propDecorators: { railWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "railWidth", required: false }] }], headerHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerHeight", required: false }] }], headerHeightMobile: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerHeightMobile", required: false }] }], headerLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerLayout", required: false }] }], footerLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLayout", required: false }] }], footerBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerBehavior", required: false }] }], contentScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentScroll", required: false }] }], skipLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkLabel", required: false }] }], drawerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawerLabel", required: false }] }], mobileNav: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileNav", required: false }] }], mobileNavOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileNavOpen", required: false }] }, { type: i0.Output, args: ["mobileNavOpenChange"] }], onEscape: [{
|
|
450
507
|
type: HostListener,
|
|
451
508
|
args: ["document:keydown.escape"]
|
|
452
509
|
}] } });
|
|
@@ -568,7 +625,10 @@ class FoldPaletteRegistry {
|
|
|
568
625
|
/** Deterministic colour for a seed, from the active palette. Reactive. */
|
|
569
626
|
colorFor(seed) {
|
|
570
627
|
const palette = this._current();
|
|
571
|
-
|
|
628
|
+
const idx = Math.abs(foldHashSeed(seed)) % palette.length;
|
|
629
|
+
// `_current` always holds a non-empty palette (foldResolvePalette falls back
|
|
630
|
+
// to `vivid`), so the modulo index resolves; the fallbacks are for the type.
|
|
631
|
+
return palette[idx] ?? palette[0] ?? "transparent";
|
|
572
632
|
}
|
|
573
633
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldPaletteRegistry, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
574
634
|
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.8", ngImport: i0, type: FoldPaletteRegistry });
|
|
@@ -662,9 +722,9 @@ class FoldAvatarComponent {
|
|
|
662
722
|
if (!n) {
|
|
663
723
|
return "?";
|
|
664
724
|
}
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
667
|
-
return (
|
|
725
|
+
const [first = "", second = ""] = n.split(/\s+/);
|
|
726
|
+
if (second) {
|
|
727
|
+
return (first.charAt(0) + second.charAt(0)).toUpperCase();
|
|
668
728
|
}
|
|
669
729
|
return n.substring(0, 2).toUpperCase();
|
|
670
730
|
}, /* @ts-ignore */
|
|
@@ -2712,7 +2772,7 @@ class FoldNavLauncherComponent {
|
|
|
2712
2772
|
resolvedCols = computed(() => {
|
|
2713
2773
|
const cols = this.columns();
|
|
2714
2774
|
if (cols !== "auto") {
|
|
2715
|
-
return
|
|
2775
|
+
return cols;
|
|
2716
2776
|
}
|
|
2717
2777
|
return this.tiles().length <= 4 ? 2 : 3;
|
|
2718
2778
|
}, /* @ts-ignore */
|
|
@@ -4666,6 +4726,12 @@ const HYSTERESIS = 32;
|
|
|
4666
4726
|
* - `[tabNav]` → the tab bar (an `fold-tab-nav`, or anything else).
|
|
4667
4727
|
* - default slot → the content for the active tab.
|
|
4668
4728
|
*
|
|
4729
|
+
* **A11y — this owns placement, not the ARIA tabs pattern.** The projected bar
|
|
4730
|
+
* carries the `tablist`/`tab` roles; the content is yours. If the tabs switch
|
|
4731
|
+
* in-page panels, give that content `role="tabpanel"` named by the active tab;
|
|
4732
|
+
* if they navigate between routes, keep plain nav semantics and add no tab
|
|
4733
|
+
* roles (the tabs pattern is for panel-switching, not navigation).
|
|
4734
|
+
*
|
|
4669
4735
|
* A side rail needs a *vertical* bar, a folded one a *horizontal* bar. Rather
|
|
4670
4736
|
* than guess, the layout exposes {@link stacked} (`exportAs`) so the projected
|
|
4671
4737
|
* nav follows the layout in one binding:
|
|
@@ -4780,7 +4846,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
4780
4846
|
* @selector `fold-page-layout`
|
|
4781
4847
|
*/
|
|
4782
4848
|
class FoldPageLayoutComponent {
|
|
4783
|
-
/** The page heading. Omit for a header-less page
|
|
4849
|
+
/** The page heading (`<h1>`). Omit for a header-less page — or when the page
|
|
4850
|
+
* already carries its `<h1>` elsewhere (e.g. a leading `fold-hero-section`),
|
|
4851
|
+
* so a page never ends up with two top-level headings. */
|
|
4784
4852
|
title = input(/* @ts-ignore */
|
|
4785
4853
|
...(ngDevMode ? [undefined, { debugName: "title" }] : /* istanbul ignore next */ []));
|
|
4786
4854
|
/** An optional leading icon shown beside the title. */
|
|
@@ -5638,14 +5706,13 @@ class FoldPanelHostComponent {
|
|
|
5638
5706
|
/** Only the last-opened panel traps focus, so stacked panels don't fight. */
|
|
5639
5707
|
isTopMost(panel) {
|
|
5640
5708
|
const panels = this.panels();
|
|
5641
|
-
|
|
5709
|
+
const top = panels[panels.length - 1];
|
|
5710
|
+
return top?.id === panel.id;
|
|
5642
5711
|
}
|
|
5643
5712
|
/** Escape closes the top-most (last-opened) panel. */
|
|
5644
5713
|
onEscape() {
|
|
5645
|
-
const
|
|
5646
|
-
|
|
5647
|
-
panels[panels.length - 1].onClose();
|
|
5648
|
-
}
|
|
5714
|
+
const top = this.panels().at(-1);
|
|
5715
|
+
top?.onClose();
|
|
5649
5716
|
}
|
|
5650
5717
|
onBackdrop(event, panel) {
|
|
5651
5718
|
if (event.target === event.currentTarget) {
|