ngwr 14.9.0 → 14.10.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.
@@ -93,6 +93,11 @@ class WrSidebar {
93
93
  /** Auto-expand the group containing the active route. @default true */
94
94
  autoExpand = input(true, { ...(ngDevMode ? { debugName: "autoExpand" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
95
95
  isGroup = isGroup;
96
+ /**
97
+ * What an entry without its own `activeOptions` gets. It is the router's own
98
+ * default, written out because a binding cannot be left unset per entry.
99
+ */
100
+ defaultActiveOptions = { exact: false };
96
101
  opened = signal(new Set(), /* @ts-ignore */
97
102
  ...(ngDevMode ? [{ debugName: "opened" }] : /* istanbul ignore next */ []));
98
103
  /** Group titles already seeded from `defaultOpen`, so it seeds once and not again. */
@@ -194,11 +199,11 @@ class WrSidebar {
194
199
  return null;
195
200
  }
196
201
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: WrSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
197
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: WrSidebar, isStandalone: true, selector: "wr-sidebar", inputs: { entries: { classPropertyName: "entries", publicName: "entries", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, defaultGroupIcon: { classPropertyName: "defaultGroupIcon", publicName: "defaultGroupIcon", isSignal: true, isRequired: false, transformFunction: null }, defaultItemIcon: { classPropertyName: "defaultItemIcon", publicName: "defaultItemIcon", isSignal: true, isRequired: false, transformFunction: null }, autoExpand: { classPropertyName: "autoExpand", publicName: "autoExpand", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedAriaLabel()" }, classAttribute: "wr-sidebar" }, ngImport: i0, template: "<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated \u2014 `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__entry\" routerLinkActive=\"wr-sidebar__entry--active\" [routerLink]=\"asItem(entry).url\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron \u2014 not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__item\" routerLinkActive=\"wr-sidebar__item--active\" [routerLink]=\"child.url\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: WrIcon, selector: "wr-icon", inputs: ["name"] }], encapsulation: i0.ViewEncapsulation.None });
202
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: WrSidebar, isStandalone: true, selector: "wr-sidebar", inputs: { entries: { classPropertyName: "entries", publicName: "entries", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, defaultGroupIcon: { classPropertyName: "defaultGroupIcon", publicName: "defaultGroupIcon", isSignal: true, isRequired: false, transformFunction: null }, defaultItemIcon: { classPropertyName: "defaultItemIcon", publicName: "defaultItemIcon", isSignal: true, isRequired: false, transformFunction: null }, autoExpand: { classPropertyName: "autoExpand", publicName: "autoExpand", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedAriaLabel()" }, classAttribute: "wr-sidebar" }, ngImport: i0, template: "<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated \u2014 `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__entry\"\n routerLinkActive=\"wr-sidebar__entry--active\"\n [routerLink]=\"asItem(entry).url\"\n [routerLinkActiveOptions]=\"asItem(entry).activeOptions ?? defaultActiveOptions\"\n >\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron \u2014 not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__item\"\n routerLinkActive=\"wr-sidebar__item--active\"\n [routerLink]=\"child.url\"\n [routerLinkActiveOptions]=\"child.activeOptions ?? defaultActiveOptions\"\n >\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: WrIcon, selector: "wr-icon", inputs: ["name"] }], encapsulation: i0.ViewEncapsulation.None });
198
203
  }
199
204
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: WrSidebar, decorators: [{
200
205
  type: Component,
201
- args: [{ selector: 'wr-sidebar', encapsulation: ViewEncapsulation.None, host: { class: 'wr-sidebar', role: 'navigation', '[attr.aria-label]': 'resolvedAriaLabel()' }, imports: [RouterLink, RouterLinkActive, WrIcon], template: "<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated \u2014 `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__entry\" routerLinkActive=\"wr-sidebar__entry--active\" [routerLink]=\"asItem(entry).url\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron \u2014 not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__item\" routerLinkActive=\"wr-sidebar__item--active\" [routerLink]=\"child.url\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n" }]
206
+ args: [{ selector: 'wr-sidebar', encapsulation: ViewEncapsulation.None, host: { class: 'wr-sidebar', role: 'navigation', '[attr.aria-label]': 'resolvedAriaLabel()' }, imports: [RouterLink, RouterLinkActive, WrIcon], template: "<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated \u2014 `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__entry\"\n routerLinkActive=\"wr-sidebar__entry--active\"\n [routerLink]=\"asItem(entry).url\"\n [routerLinkActiveOptions]=\"asItem(entry).activeOptions ?? defaultActiveOptions\"\n >\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron \u2014 not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__item\"\n routerLinkActive=\"wr-sidebar__item--active\"\n [routerLink]=\"child.url\"\n [routerLinkActiveOptions]=\"child.activeOptions ?? defaultActiveOptions\"\n >\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n" }]
202
207
  }], ctorParameters: () => [], propDecorators: { entries: [{ type: i0.Input, args: [{ isSignal: true, alias: "entries", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], defaultGroupIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultGroupIcon", required: false }] }], defaultItemIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultItemIcon", required: false }] }], autoExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoExpand", required: false }] }] } });
203
208
 
204
209
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-sidebar.mjs","sources":["../../../projects/lib/sidebar/sidebar.ts","../../../projects/lib/sidebar/sidebar.html","../../../projects/lib/sidebar/ngwr-sidebar.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { Component, ViewEncapsulation, effect, inject, input, signal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\n\nimport { filter, map, startWith } from 'rxjs';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { WrIcon } from 'ngwr/icon';\n\nimport type { WrSidebarEntry, WrSidebarGroup, WrSidebarItem } from './interfaces';\n\nfunction isGroup(entry: WrSidebarEntry): entry is WrSidebarGroup {\n return 'children' in entry;\n}\n\n/**\n * Instance counter for the group-body ids. Deterministic under prerender, unlike\n * `randomId()`, and unique across sidebars — a title-only id collided between two\n * sidebars on one page, and `aria-controls` then pointed at the wrong body.\n */\nlet sidebarUid = 0;\n\n/**\n * A `routerLink` segment array as an absolute path.\n *\n * The documented form carries the leading slash on the FIRST segment\n * (`['/settings', 'profile']`), and prefixing another one produced\n * `//settings/profile` — a path no URL ever equals, which is why auto-expand\n * never fired for anyone following the example. Both forms normalise here.\n */\nfunction routePath(segments: readonly (string | number)[]): string {\n return `/${segments.join('/')}`.replace(/\\/{2,}/g, '/');\n}\n\n/**\n * Whether `url` is `route` or something below it. A bare `startsWith` matched a\n * SIBLING whose path merely began the same way, so visiting `/table` expanded the\n * group that owns `/tabs`.\n */\nfunction matchesRoute(url: string, route: string): boolean {\n if (!url.startsWith(route)) return false;\n const rest = url.slice(route.length);\n return rest === '' || rest.startsWith('/') || rest.startsWith('?') || rest.startsWith('#');\n}\n\n/**\n * Data-driven sidebar nav. Pass `[entries]` — either flat\n * `WrSidebarItem`s (direct links) or `WrSidebarGroup`s (expand to reveal\n * child items). Active route auto-expands its containing group.\n *\n * The host pads its rows by `--wr-sidebar-padding` (`0.5rem`), so a hovered or\n * active row's tint stays off the edge of whatever holds the nav, a bordered\n * card or a `<wr-layout-sider>`, which pads nothing itself. Where the container\n * already supplies the inset, set the hook to `0` on the sidebar, not on the\n * container: the host declares its own default, which shadows an inherited one\n * (`.my-panel .wr-sidebar { --wr-sidebar-padding: 0 }`, or inline).\n *\n * @example\n * ```html\n * <wr-sidebar [entries]=\"[\n * { title: 'Dashboard', icon: 'home', url: ['/dashboard'] },\n * {\n * title: 'Settings',\n * icon: 'cog',\n * children: [\n * { title: 'Profile', url: ['/settings', 'profile'] },\n * { title: 'Billing', url: ['/settings', 'billing'] }\n * ]\n * }\n * ]\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/sidebar\n */\n@Component({\n selector: 'wr-sidebar',\n templateUrl: './sidebar.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-sidebar', role: 'navigation', '[attr.aria-label]': 'resolvedAriaLabel()' },\n imports: [RouterLink, RouterLinkActive, WrIcon],\n})\nexport class WrSidebar {\n readonly entries = input<readonly WrSidebarEntry[]>([]);\n\n /**\n * Accessible name for the navigation landmark. Falls back to `sidebar.label`,\n * then `'Sidebar'` — a page with a sidebar AND a table of contents has two\n * navigation landmarks, and an unnamed one is announced as just \"navigation\".\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'sidebar.label', 'Sidebar');\n\n /** Default icon for entries without one. @default 'folder' */\n readonly defaultGroupIcon = input<string>('folder');\n\n /** Default icon for items without one. @default 'caret-forward' */\n readonly defaultItemIcon = input<string>('caret-forward');\n\n /** Auto-expand the group containing the active route. @default true */\n readonly autoExpand = input(true, { transform: coerceBooleanProperty });\n\n protected readonly isGroup = isGroup;\n\n private readonly opened = signal<ReadonlySet<string>>(new Set());\n\n /** Group titles already seeded from `defaultOpen`, so it seeds once and not again. */\n private readonly seeded = new Set<string>();\n\n private readonly uid = ++sidebarUid;\n\n private readonly router = inject(Router);\n private readonly currentUrl = toSignal(\n this.router.events.pipe(\n filter((e): e is NavigationEnd => e instanceof NavigationEnd),\n map(e => e.urlAfterRedirects),\n startWith(this.router.url)\n ),\n { initialValue: this.router.url }\n );\n\n /**\n * Last URL we resolved to a group — prevents re-opening one the user collapsed.\n * A URL no entry claims is deliberately NOT recorded: see the effect below.\n */\n private lastAutoUrl: string | null = null;\n\n constructor() {\n // Seed `defaultOpen` groups — once per group, not once per `entries` emission.\n // Re-seeding meant any later change to the array (a badge count, a filtered\n // list) re-opened a group the user had deliberately collapsed.\n effect(() => {\n const fresh: string[] = [];\n for (const entry of this.entries()) {\n if (isGroup(entry) && entry.defaultOpen && !this.seeded.has(entry.title)) {\n this.seeded.add(entry.title);\n fresh.push(entry.title);\n }\n }\n if (fresh.length > 0) {\n this.opened.update(prev => new Set([...prev, ...fresh]));\n }\n });\n\n // Auto-expand the group containing the current URL.\n effect(() => {\n if (!this.autoExpand()) return;\n const url = this.currentUrl();\n if (url === this.lastAutoUrl) return;\n // Latch AFTER the lookup, never before it. `entries` often arrives from an\n // HTTP call, so the first run matches nothing — and latching first told the\n // re-run that landed with the real nav that this URL was already handled.\n // A deep link into a `defaultOpen: false` group then stayed collapsed, with\n // the active link out of the tab order, until the user navigated away and\n // back. Only a run that found a group has actually handled the URL; the\n // user-collapse latch is unaffected, since that run is also the one that\n // records it.\n const match = this.findGroupForUrl(url);\n if (!match) return;\n this.lastAutoUrl = url;\n if (this.opened().has(match)) return;\n this.opened.update(prev => {\n const next = new Set(prev);\n next.add(match);\n return next;\n });\n });\n }\n\n protected isOpen(title: string): boolean {\n return this.opened().has(title);\n }\n\n /**\n * Stable id for the group body, referenced by the toggle's `aria-controls`.\n *\n * Keyed by POSITION, not by the title. Slugging the title kept the id readable\n * but was not injective: it lowercases and collapses every non-`[a-z0-9]` run,\n * so `Data & charts` and `Data / charts` produced the same string, and a title\n * with no ASCII alphanumerics at all — `Компоненты`, `数据` — produced the empty\n * one. A whole Russian or Chinese sidebar therefore emitted N identical ids and\n * every toggle's `aria-controls` resolved to the FIRST group's body. The toggle\n * and its body read `$index` from the same `@for` iteration, so the two always\n * name each other — reordering `entries` renumbers both together.\n */\n protected groupBodyId(index: number): string {\n return `wr-sidebar-${this.uid}-group-${index}`;\n }\n\n protected toggleGroup(title: string): void {\n this.opened.update(prev => {\n const next = new Set(prev);\n if (next.has(title)) next.delete(title);\n else next.add(title);\n return next;\n });\n }\n\n protected asItem(entry: WrSidebarEntry): WrSidebarItem {\n return entry as WrSidebarItem;\n }\n\n protected asGroup(entry: WrSidebarEntry): WrSidebarGroup {\n return entry as WrSidebarGroup;\n }\n\n private findGroupForUrl(url: string): string | null {\n for (const entry of this.entries()) {\n if (!isGroup(entry)) continue;\n const hit = entry.children.some(child => matchesRoute(url, routePath(child.url)));\n if (hit) return entry.title;\n }\n return null;\n }\n}\n","<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated — `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__entry\" routerLinkActive=\"wr-sidebar__entry--active\" [routerLink]=\"asItem(entry).url\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron — not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a class=\"wr-sidebar__item\" routerLinkActive=\"wr-sidebar__item--active\" [routerLink]=\"child.url\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-sidebar.mjs","sources":["../../../projects/lib/sidebar/sidebar.ts","../../../projects/lib/sidebar/sidebar.html","../../../projects/lib/sidebar/ngwr-sidebar.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { Component, ViewEncapsulation, effect, inject, input, signal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';\n\nimport { filter, map, startWith } from 'rxjs';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { WrIcon } from 'ngwr/icon';\n\nimport type { WrSidebarEntry, WrSidebarGroup, WrSidebarItem } from './interfaces';\n\nfunction isGroup(entry: WrSidebarEntry): entry is WrSidebarGroup {\n return 'children' in entry;\n}\n\n/**\n * Instance counter for the group-body ids. Deterministic under prerender, unlike\n * `randomId()`, and unique across sidebars — a title-only id collided between two\n * sidebars on one page, and `aria-controls` then pointed at the wrong body.\n */\nlet sidebarUid = 0;\n\n/**\n * A `routerLink` segment array as an absolute path.\n *\n * The documented form carries the leading slash on the FIRST segment\n * (`['/settings', 'profile']`), and prefixing another one produced\n * `//settings/profile` — a path no URL ever equals, which is why auto-expand\n * never fired for anyone following the example. Both forms normalise here.\n */\nfunction routePath(segments: readonly (string | number)[]): string {\n return `/${segments.join('/')}`.replace(/\\/{2,}/g, '/');\n}\n\n/**\n * Whether `url` is `route` or something below it. A bare `startsWith` matched a\n * SIBLING whose path merely began the same way, so visiting `/table` expanded the\n * group that owns `/tabs`.\n */\nfunction matchesRoute(url: string, route: string): boolean {\n if (!url.startsWith(route)) return false;\n const rest = url.slice(route.length);\n return rest === '' || rest.startsWith('/') || rest.startsWith('?') || rest.startsWith('#');\n}\n\n/**\n * Data-driven sidebar nav. Pass `[entries]` — either flat\n * `WrSidebarItem`s (direct links) or `WrSidebarGroup`s (expand to reveal\n * child items). Active route auto-expands its containing group.\n *\n * The host pads its rows by `--wr-sidebar-padding` (`0.5rem`), so a hovered or\n * active row's tint stays off the edge of whatever holds the nav, a bordered\n * card or a `<wr-layout-sider>`, which pads nothing itself. Where the container\n * already supplies the inset, set the hook to `0` on the sidebar, not on the\n * container: the host declares its own default, which shadows an inherited one\n * (`.my-panel .wr-sidebar { --wr-sidebar-padding: 0 }`, or inline).\n *\n * @example\n * ```html\n * <wr-sidebar [entries]=\"[\n * { title: 'Dashboard', icon: 'home', url: ['/dashboard'] },\n * {\n * title: 'Settings',\n * icon: 'cog',\n * children: [\n * { title: 'Profile', url: ['/settings', 'profile'] },\n * { title: 'Billing', url: ['/settings', 'billing'] }\n * ]\n * }\n * ]\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/sidebar\n */\n@Component({\n selector: 'wr-sidebar',\n templateUrl: './sidebar.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-sidebar', role: 'navigation', '[attr.aria-label]': 'resolvedAriaLabel()' },\n imports: [RouterLink, RouterLinkActive, WrIcon],\n})\nexport class WrSidebar {\n readonly entries = input<readonly WrSidebarEntry[]>([]);\n\n /**\n * Accessible name for the navigation landmark. Falls back to `sidebar.label`,\n * then `'Sidebar'` — a page with a sidebar AND a table of contents has two\n * navigation landmarks, and an unnamed one is announced as just \"navigation\".\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'sidebar.label', 'Sidebar');\n\n /** Default icon for entries without one. @default 'folder' */\n readonly defaultGroupIcon = input<string>('folder');\n\n /** Default icon for items without one. @default 'caret-forward' */\n readonly defaultItemIcon = input<string>('caret-forward');\n\n /** Auto-expand the group containing the active route. @default true */\n readonly autoExpand = input(true, { transform: coerceBooleanProperty });\n\n protected readonly isGroup = isGroup;\n\n /**\n * What an entry without its own `activeOptions` gets. It is the router's own\n * default, written out because a binding cannot be left unset per entry.\n */\n protected readonly defaultActiveOptions = { exact: false };\n\n private readonly opened = signal<ReadonlySet<string>>(new Set());\n\n /** Group titles already seeded from `defaultOpen`, so it seeds once and not again. */\n private readonly seeded = new Set<string>();\n\n private readonly uid = ++sidebarUid;\n\n private readonly router = inject(Router);\n private readonly currentUrl = toSignal(\n this.router.events.pipe(\n filter((e): e is NavigationEnd => e instanceof NavigationEnd),\n map(e => e.urlAfterRedirects),\n startWith(this.router.url)\n ),\n { initialValue: this.router.url }\n );\n\n /**\n * Last URL we resolved to a group — prevents re-opening one the user collapsed.\n * A URL no entry claims is deliberately NOT recorded: see the effect below.\n */\n private lastAutoUrl: string | null = null;\n\n constructor() {\n // Seed `defaultOpen` groups — once per group, not once per `entries` emission.\n // Re-seeding meant any later change to the array (a badge count, a filtered\n // list) re-opened a group the user had deliberately collapsed.\n effect(() => {\n const fresh: string[] = [];\n for (const entry of this.entries()) {\n if (isGroup(entry) && entry.defaultOpen && !this.seeded.has(entry.title)) {\n this.seeded.add(entry.title);\n fresh.push(entry.title);\n }\n }\n if (fresh.length > 0) {\n this.opened.update(prev => new Set([...prev, ...fresh]));\n }\n });\n\n // Auto-expand the group containing the current URL.\n effect(() => {\n if (!this.autoExpand()) return;\n const url = this.currentUrl();\n if (url === this.lastAutoUrl) return;\n // Latch AFTER the lookup, never before it. `entries` often arrives from an\n // HTTP call, so the first run matches nothing — and latching first told the\n // re-run that landed with the real nav that this URL was already handled.\n // A deep link into a `defaultOpen: false` group then stayed collapsed, with\n // the active link out of the tab order, until the user navigated away and\n // back. Only a run that found a group has actually handled the URL; the\n // user-collapse latch is unaffected, since that run is also the one that\n // records it.\n const match = this.findGroupForUrl(url);\n if (!match) return;\n this.lastAutoUrl = url;\n if (this.opened().has(match)) return;\n this.opened.update(prev => {\n const next = new Set(prev);\n next.add(match);\n return next;\n });\n });\n }\n\n protected isOpen(title: string): boolean {\n return this.opened().has(title);\n }\n\n /**\n * Stable id for the group body, referenced by the toggle's `aria-controls`.\n *\n * Keyed by POSITION, not by the title. Slugging the title kept the id readable\n * but was not injective: it lowercases and collapses every non-`[a-z0-9]` run,\n * so `Data & charts` and `Data / charts` produced the same string, and a title\n * with no ASCII alphanumerics at all — `Компоненты`, `数据` — produced the empty\n * one. A whole Russian or Chinese sidebar therefore emitted N identical ids and\n * every toggle's `aria-controls` resolved to the FIRST group's body. The toggle\n * and its body read `$index` from the same `@for` iteration, so the two always\n * name each other — reordering `entries` renumbers both together.\n */\n protected groupBodyId(index: number): string {\n return `wr-sidebar-${this.uid}-group-${index}`;\n }\n\n protected toggleGroup(title: string): void {\n this.opened.update(prev => {\n const next = new Set(prev);\n if (next.has(title)) next.delete(title);\n else next.add(title);\n return next;\n });\n }\n\n protected asItem(entry: WrSidebarEntry): WrSidebarItem {\n return entry as WrSidebarItem;\n }\n\n protected asGroup(entry: WrSidebarEntry): WrSidebarGroup {\n return entry as WrSidebarGroup;\n }\n\n private findGroupForUrl(url: string): string | null {\n for (const entry of this.entries()) {\n if (!isGroup(entry)) continue;\n const hit = entry.children.some(child => matchesRoute(url, routePath(child.url)));\n if (hit) return entry.title;\n }\n return null;\n }\n}\n","<!-- A div, not a <nav>: the host already IS the navigation landmark, and two\n nested ones announce the same list twice. -->\n<div class=\"wr-sidebar__inner\">\n @for (entry of entries(); track entry.title) {\n @if (!isGroup(entry)) {\n <!--\n A span, not a greyed anchor, exactly as the nested branch below does it.\n `--disabled` only paints: `pointer-events: none` stops a real pointer but\n not the keyboard, so a disabled entry stayed in the tab order and Enter\n navigated — `disabled` on a `WrSidebarItem` is documented as \"no\n navigation\". An empty `url` is the same case: `[routerLink]=\"[]\"` resolves\n to the current route, which is a link that goes nowhere.\n -->\n @if (asItem(entry).disabled || !asItem(entry).url.length) {\n <span class=\"wr-sidebar__entry wr-sidebar__entry--disabled\">\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__entry\"\n routerLinkActive=\"wr-sidebar__entry--active\"\n [routerLink]=\"asItem(entry).url\"\n [routerLinkActiveOptions]=\"asItem(entry).activeOptions ?? defaultActiveOptions\"\n >\n @if (asItem(entry).icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n @if (asItem(entry).badge) {\n <small class=\"wr-sidebar__badge\">{{ asItem(entry).badge }}</small>\n }\n </a>\n }\n } @else {\n <section class=\"wr-sidebar__group\" [class.wr-sidebar__group--collapsed]=\"!isOpen(entry.title)\">\n <button\n type=\"button\"\n class=\"wr-sidebar__group-toggle\"\n [attr.aria-expanded]=\"isOpen(entry.title)\"\n [attr.aria-controls]=\"groupBodyId($index)\"\n (click)=\"toggleGroup(entry.title)\"\n >\n @if (asGroup(entry).icon || defaultGroupIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__title\">{{ entry.title }}</span>\n <!-- Inline chrome chevron — not user-customizable. -->\n <svg\n class=\"wr-icon__svg wr-sidebar__chevron\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n <div\n class=\"wr-sidebar__group-wrap\"\n [attr.id]=\"groupBodyId($index)\"\n [attr.inert]=\"!isOpen(entry.title) ? '' : null\"\n >\n <div class=\"wr-sidebar__group-body\">\n <ul class=\"wr-sidebar__list\">\n @for (child of asGroup(entry).children; track child.title) {\n <li>\n @if (child.disabled || !child.url.length) {\n <span class=\"wr-sidebar__item wr-sidebar__item--disabled\">\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </span>\n } @else {\n <a\n class=\"wr-sidebar__item\"\n routerLinkActive=\"wr-sidebar__item--active\"\n [routerLink]=\"child.url\"\n [routerLinkActiveOptions]=\"child.activeOptions ?? defaultActiveOptions\"\n >\n @if (child.icon || defaultItemIcon(); as name) {\n <wr-icon class=\"wr-sidebar__icon\" [name]=\"name\" />\n }\n <span class=\"wr-sidebar__item-title\">{{ child.title }}</span>\n @if (child.badge) {\n <small class=\"wr-sidebar__badge\">{{ child.badge }}</small>\n }\n </a>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </section>\n }\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@ import { InjectionToken } from '@angular/core';
16
16
  * Distinct from `Version` exported by `@angular/core`, which reports the
17
17
  * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
18
18
  */
19
- const NGWR_VERSION = '14.9.0';
19
+ const NGWR_VERSION = '14.10.0';
20
20
 
21
21
  /**
22
22
  * @license
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '14.9.0';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAEH;;;;;;;;;AASG;AACI,MAAM,YAAY,GAAG;;ACjB5B;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;MACU,kBAAkB,GAAG,IAAI,cAAc,CAAS,oBAAoB,EAAE;AACjF,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,YAAY;AAC5B,CAAA;;AC5BD;;AAEG;;"}
1
+ {"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '14.10.0';\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAEH;;;;;;;;;AASG;AACI,MAAM,YAAY,GAAG;;ACjB5B;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;MACU,kBAAkB,GAAG,IAAI,cAAc,CAAS,oBAAoB,EAAE;AACjF,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,YAAY;AAC5B,CAAA;;AC5BD;;AAEG;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngwr",
3
- "version": "14.9.0",
3
+ "version": "14.10.0",
4
4
  "bin": {
5
5
  "ngwr-mcp": "mcp/server.js"
6
6
  },
@@ -76,10 +76,24 @@ interface WrEditorHarnessFilters extends BaseHarnessFilters {
76
76
  * {@link getTool} hands back a `WrButtonHarness`; what that harness does not read
77
77
  * (`aria-pressed`, `aria-keyshortcuts`, the roving tab stop) is answered here.
78
78
  *
79
- * Every method reads the MOUNTED surface. ProseMirror takes it over in
79
+ * **A read-only editor has no surface at all**, and that is the state to know
80
+ * about before reading anything here. `readonly` set before the editor mounts
81
+ * means ProseMirror is never created: the document is drawn as ordinary markup
82
+ * in a focusable `role="group"`, which takes over the id, the name and the tab
83
+ * stop. So the reads answer from whichever element is on screen —
84
+ * {@link getText}, {@link getLabel}, {@link isDisabled}, {@link isInvalid},
85
+ * {@link focus} — while everything that needs a live view ({@link setText},
86
+ * {@link paste}, {@link pressKey}, {@link selectAll}) throws a sentence saying
87
+ * there is nothing to type into rather than passing on a surface that is not
88
+ * there. {@link isMounted} is the question itself, and {@link isReadonly} keeps
89
+ * answering: the static document exists in that one state, which is how it is
90
+ * read, since ARIA allows no `aria-readonly` on a group.
91
+ *
92
+ * Otherwise every method reads the MOUNTED surface. ProseMirror takes it over in
80
93
  * `afterNextRender`, which the harness's own stabilising lets run; under a server
81
- * `PLATFORM_ID` it never does, and the static preview the server renders is not
82
- * something this harness pretends to be the editor.
94
+ * `PLATFORM_ID` it never does, and the transient preview the server renders — the
95
+ * one a browser is about to replace — is not something this harness pretends to
96
+ * be the editor.
83
97
  *
84
98
  * **A spec under jsdom installs one stub first.** After an edit made while the
85
99
  * surface has focus, ProseMirror scrolls the selection into view, which measures a
@@ -116,15 +130,22 @@ declare class WrEditorHarness extends ComponentHarness {
116
130
  static hostSelector: string;
117
131
  /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
118
132
  static with(options?: WrEditorHarnessFilters): HarnessPredicate<WrEditorHarness>;
119
- private readonly surface;
133
+ /** The element carrying the editor's id, name and ARIA — the surface, or the static document. */
134
+ private readonly control;
135
+ private readonly liveSurface;
136
+ private readonly staticDoc;
120
137
  /**
121
- * Whether ProseMirror owns the surface yet — read from the `contenteditable` it
138
+ * Whether ProseMirror owns a surface yet — read from the `contenteditable` it
122
139
  * writes there, which nothing else does.
123
140
  *
124
- * `true` in any browser-platform spec by the time a harness method runs. `false`
125
- * under a server `PLATFORM_ID`, where the editor is the static preview for good —
126
- * and every other method here throws, rather than answer about a preview as if it
127
- * were the editor.
141
+ * `true` in any browser-platform spec on an editor that is not read-only, by the
142
+ * time a harness method runs. `false` in the two states where no mount is coming:
143
+ * under a server `PLATFORM_ID`, where what shows is the transient preview and
144
+ * every other method here throws rather than answer about it as if it were the
145
+ * editor; and on a READ-ONLY editor, which never mounts by design and whose
146
+ * static document is read here like any other — this is the method that tells
147
+ * those two apart from a mounted one, and {@link isReadonly} tells them apart
148
+ * from each other.
128
149
  */
129
150
  isMounted(): Promise<boolean>;
130
151
  /**
@@ -140,11 +161,16 @@ declare class WrEditorHarness extends ComponentHarness {
140
161
  *
141
162
  * This is the drawn half of the two questions an editor answers; the class docs say
142
163
  * why the other half, the value, is read from the model instead.
164
+ *
165
+ * A READ-ONLY editor draws its document without ProseMirror, and this reads that
166
+ * document — it is the editor there, not a stand-in for one.
143
167
  */
144
168
  getText(): Promise<string>;
145
169
  /**
146
- * The surface's accessible name — `ariaLabel` when one is bound, else the
147
- * surrounding `<wr-form-field>`'s label, else the catalog's `editor.label`.
170
+ * The editor's accessible name — `ariaLabel` when one is bound, else the
171
+ * surrounding `<wr-form-field>`'s label, else the catalog's `editor.label`. Read
172
+ * off whichever element carries it: the surface, or a read-only editor's static
173
+ * document, which takes the name over with the tab stop.
148
174
  *
149
175
  * A field's label reaches the surface through `aria-labelledby`, never `<label for>`:
150
176
  * a contenteditable is not labelable. So the reference is resolved the way assistive
@@ -161,25 +187,38 @@ declare class WrEditorHarness extends ComponentHarness {
161
187
  * `placeholder` is bound: the component publishes it only while the document is
162
188
  * empty, the moment it is also drawn. A document holding an empty heading still
163
189
  * counts as empty.
190
+ *
191
+ * Always `null` on a READ-ONLY editor, however `placeholder` is bound, and that is
192
+ * the truth rather than a hole: there is no input to hint at, and
193
+ * `aria-placeholder` is not among the attributes the static document's
194
+ * `role="group"` allows, so the component draws none there either.
164
195
  */
165
196
  getPlaceholder(): Promise<string | null>;
166
197
  /**
167
- * Whether the editor is disabled — the surface's `aria-disabled`.
198
+ * Whether the editor is disabled — `aria-disabled`, on the surface or on the static
199
+ * document, whichever is drawn.
168
200
  *
169
- * Read from the surface rather than the `wr-editor--disabled` host class, because a
201
+ * Read from there rather than from the `wr-editor--disabled` host class, because a
170
202
  * contenteditable has no `disabled` property: the attribute is what a screen reader
171
203
  * is told, and ProseMirror's own `contenteditable="false"` is what turns the
172
204
  * keyboard away. The component sets both.
173
205
  */
174
206
  isDisabled(): Promise<boolean>;
175
207
  /**
176
- * Whether the editor shows its document and refuses edits — the surface's
177
- * `aria-readonly`, which `role="textbox"` allows. Read-only keeps the tab stop, so
178
- * {@link focus} still lands; disabled does not.
208
+ * Whether the editor shows its document and refuses edits. Read-only keeps the tab
209
+ * stop, so {@link focus} still lands; disabled does not.
210
+ *
211
+ * Two DOM facts answer it, because the two shapes of read-only look nothing alike.
212
+ * An editor that was read-only before it could mount has no surface at all, and the
213
+ * static document it draws instead is drawn in that state and in no other — so its
214
+ * presence is the answer, and it is the only one available: ARIA defines no
215
+ * `aria-readonly` for `role="group"`, and `aria-disabled` would say something else.
216
+ * One that mounted first keeps its surface and reports `aria-readonly` there, which
217
+ * `role="textbox"` does allow.
179
218
  */
180
219
  isReadonly(): Promise<boolean>;
181
220
  /**
182
- * Whether the surface is announced invalid — the `aria-invalid` the surrounding
221
+ * Whether the editor is announced invalid — the `aria-invalid` the surrounding
183
222
  * `<wr-form-field>` hands it when a bound field fails validation and has been
184
223
  * touched. An editor outside a field is never invalid.
185
224
  */
@@ -198,18 +237,20 @@ declare class WrEditorHarness extends ComponentHarness {
198
237
  * An empty `text` empties the document, which writes `''` (`null` in `json` format).
199
238
  *
200
239
  * Throws on a disabled or read-only editor, where no user could change a word — the
201
- * surface is not contenteditable then, and a write the DOM accepted anyway would be
202
- * a green spec for something that cannot happen.
240
+ * surface is not contenteditable then, and is not even rendered when read-only came
241
+ * first, so a write the DOM accepted anyway would be a green spec for something that
242
+ * cannot happen.
203
243
  */
204
244
  setText(text: string): Promise<void>;
205
245
  /**
206
246
  * Paste `text` as plain text at the selection, replacing whatever is selected — a
207
247
  * `paste` event carrying `text/plain`, handled by ProseMirror's own paste path.
208
248
  *
209
- * On a READ-ONLY editor the event is still delivered, because a browser delivers it:
210
- * the refusal is the editor's to make, and asserting the value did not change is how
211
- * a spec proves it did. Throws on a disabled one, whose surface cannot hold focus for
212
- * a paste to arrive at.
249
+ * On an editor that was MOUNTED before it went read-only the event is still
250
+ * delivered, because a browser delivers it: the refusal is the editor's to make, and
251
+ * asserting the value did not change is how a spec proves it did. One that was
252
+ * read-only from the start has no surface for a paste to arrive at, and throws
253
+ * saying so. Throws on a disabled one too, whose surface cannot hold focus.
213
254
  */
214
255
  paste(text: string): Promise<void>;
215
256
  /**
@@ -230,7 +271,8 @@ declare class WrEditorHarness extends ComponentHarness {
230
271
  * Throws on a read-only or disabled editor: ProseMirror hands the keyboard back to
231
272
  * the browser once the surface is not editable, and the browser's native select-all
232
273
  * is exactly what jsdom does not have, so the chord would select nothing and say so
233
- * to no one.
274
+ * to no one — and an editor that was read-only before it could mount has no surface
275
+ * to aim the chord at in the first place.
234
276
  */
235
277
  selectAll(): Promise<void>;
236
278
  /**
@@ -249,22 +291,22 @@ declare class WrEditorHarness extends ComponentHarness {
249
291
  */
250
292
  pressKey(key: TestKey | string, modifiers?: ModifierKeys): Promise<void>;
251
293
  /**
252
- * Move focus to the text surface. A read-only editor keeps its tab stop and takes
253
- * it.
294
+ * Move focus to the text — the mounted surface, or a read-only editor's static
295
+ * document, which keeps a tab stop for exactly this.
254
296
  *
255
- * Throws on a disabled one rather than focusing it. In a browser its surface cannot
256
- * take focus — no `tabindex`, and `contenteditable="false"` — but jsdom counts ANY
257
- * `contenteditable` attribute as focusable, `false` included, so the focus would
258
- * land in the spec and never in the app.
297
+ * Throws on a disabled editor rather than focusing it. In a browser its surface
298
+ * cannot take focus — no `tabindex`, and `contenteditable="false"` — but jsdom
299
+ * counts ANY `contenteditable` attribute as focusable, `false` included, so the
300
+ * focus would land in the spec and never in the app.
259
301
  */
260
302
  focus(): Promise<void>;
261
303
  /**
262
- * Take focus away from the surface — the whole control losing focus, which is what
304
+ * Take focus away from the text — the whole control losing focus, which is what
263
305
  * emits `touch` and lets a bound field show its error. Focus moving to the editor's
264
306
  * own toolbar or link panel is not a blur of the control, and does not emit it.
265
307
  */
266
308
  blur(): Promise<void>;
267
- /** Whether the text surface holds focus. A toolbar button holding it is not the surface. */
309
+ /** Whether the text holds focus. A toolbar button holding it is not the text. */
268
310
  isFocused(): Promise<boolean>;
269
311
  /** Whether a toolbar is drawn — gone with `toolbar="false"`, and with a list holding no tool. */
270
312
  hasToolbar(): Promise<boolean>;
@@ -363,10 +405,14 @@ declare class WrEditorHarness extends ComponentHarness {
363
405
  * would read as "the address was accepted".
364
406
  */
365
407
  getLinkError(): Promise<string | null>;
366
- /** The surface, or a sentence saying the editor is still the server's static preview. */
408
+ /** The element drawing the editor's own document — the mounted surface, or the static one. */
409
+ private drawnDocument;
410
+ /** The surface, or a sentence saying what is on screen instead of one. */
367
411
  private mountedSurface;
368
412
  /** The surface, when a user could still reach it with the keyboard. */
369
413
  private focusableSurface;
414
+ /** Whatever a keyboard user could put focus on: the surface, or the static document. */
415
+ private focusableControl;
370
416
  /** The surface, when a user could change its text. */
371
417
  private writableSurface;
372
418
  private dispatchPaste;
@@ -161,6 +161,16 @@ interface WrToolState {
161
161
  * editor is read-only and carries `wr-editor--refused`: the model still holds the
162
162
  * refused value, and an edit would write the document on screen over it.
163
163
  *
164
+ * **`readonly` never mounts ProseMirror.** The document is drawn as ordinary
165
+ * Angular markup instead — the same rendering the server prerenders — so a page
166
+ * showing thirty saved comments boots no editor at all, and the headings, lists
167
+ * and tables of what it shows stay in the accessibility tree rather than being
168
+ * swallowed by a textbox, every descendant of which is presentational. It is
169
+ * lazy, not permanent: the first time `readonly` is lifted the editor mounts,
170
+ * and from there behaves exactly as one that never was read-only. Setting it
171
+ * again keeps the mounted view — throwing a live session's undo history away to
172
+ * save one view would cost more than the view.
173
+ *
164
174
  * Headings stop at three levels: an h4–h6 read from HTML or markdown becomes an
165
175
  * h3 rather than a paragraph. Underline exists in `html` and `json` only —
166
176
  * markdown cannot spell it, so in that format the tool, the shortcut and the mark
@@ -198,13 +208,22 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
198
208
  */
199
209
  readonly disabled: _angular_core.InputSignalWithTransform<boolean, any>;
200
210
  /**
201
- * Refuse edits while the text stays focusable, selectable and announced.
211
+ * Refuse edits while the text stays reachable, selectable and announced.
202
212
  * Bound automatically from the field's readonly state when used with
203
213
  * `[formField]`.
204
214
  *
205
- * The surface keeps its tab stop and reports `aria-readonly`; typing, paste,
206
- * drop, the shortcuts and every toolbar command are refused. The toolbar goes
207
- * inert, since none of its tools can apply.
215
+ * Set before the editor mounts, it means ProseMirror is never started: the
216
+ * document is drawn as ordinary markup in a focusable `role="group"` that
217
+ * keeps the tab stop and carries the editor's name, so a viewer costs no
218
+ * editor and the structure of what it shows is still announced. `aria-readonly`
219
+ * is deliberately absent there — ARIA does not allow it on `group`, and
220
+ * `aria-disabled`, the only attribute that would apply, means something else —
221
+ * so the cue is the `wr-editor--readonly` class. The toolbar goes inert, since
222
+ * none of its tools can apply; a pure viewer binds `[toolbar]="false"`.
223
+ *
224
+ * A view that is already mounted stays mounted, refuses typing, paste, drop,
225
+ * the shortcuts and every toolbar command, and reports `aria-readonly` on its
226
+ * surface as before.
208
227
  *
209
228
  * @default false
210
229
  */
@@ -216,7 +235,8 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
216
235
  */
217
236
  readonly placeholder: _angular_core.InputSignal<string>;
218
237
  /**
219
- * Accessible name of the text surface. Falls back to the surrounding
238
+ * Accessible name of the text — of the surface, or of the document a
239
+ * read-only editor draws in its place. Falls back to the surrounding
220
240
  * `<wr-form-field>`'s label, then to `editor.label`.
221
241
  */
222
242
  readonly ariaLabel: _angular_core.InputSignal<string | null>;
@@ -263,6 +283,12 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
263
283
  protected readonly surfaceId: string;
264
284
  /** ProseMirror owns the surface from here on; until then the preview stands in. */
265
285
  protected readonly mounted: _angular_core.WritableSignal<boolean>;
286
+ /**
287
+ * Read-only and nothing mounted: the preview is not standing in for a surface,
288
+ * it IS the editor, and the surface is not rendered at all. One id, one name
289
+ * and one tab stop move onto it with the job. @internal
290
+ */
291
+ protected readonly staticMode: Signal<boolean>;
266
292
  protected readonly focused: _angular_core.WritableSignal<boolean>;
267
293
  private readonly editorState;
268
294
  /**
@@ -278,6 +304,7 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
278
304
  /** Apple keyboards name the modifier ⌘. Read in the browser only — see `mount()`. */
279
305
  private readonly apple;
280
306
  private readonly surface;
307
+ private readonly staticDoc;
281
308
  private readonly toolButtons;
282
309
  private readonly linkPopover;
283
310
  private view;
@@ -291,6 +318,12 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
291
318
  */
292
319
  private lastSerialized;
293
320
  private linkToSurface;
321
+ /**
322
+ * Whether the static path has already named the refusal the model is holding.
323
+ * A read-only editor reports one itself, and a mount that follows would
324
+ * otherwise warn a second time about the same value.
325
+ */
326
+ private refusalWarned;
294
327
  protected readonly toolbarLabel: Signal<string>;
295
328
  protected readonly linkUrlLabel: Signal<string>;
296
329
  protected readonly linkApplyLabel: Signal<string>;
@@ -329,19 +362,29 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
329
362
  private readonly empty;
330
363
  /** The task-item words, handed to ProseMirror through its node views. */
331
364
  private readonly nodeViews;
365
+ /**
366
+ * Nothing in the static mode: a hint for input, where no input is taken, and
367
+ * `aria-placeholder` is not among the attributes `role="group"` allows — so
368
+ * one drawn there would be text no screen reader ever reaches.
369
+ */
332
370
  protected readonly ariaPlaceholder: Signal<string | null>;
333
371
  protected readonly linkDraft: _angular_core.WritableSignal<string>;
334
372
  /** Whether the selection is in a link — what the panel's remove button needs. */
335
373
  protected readonly linkActive: Signal<boolean>;
336
374
  /**
337
- * The value rendered by Angular until ProseMirror takes over: on the server,
338
- * and for the first browser frame. Read from the doc JSON, so it shows exactly
339
- * what the schema kept — the same sanitised document the editor will mount.
375
+ * The value rendered by Angular rather than by ProseMirror: on the server, for
376
+ * the first browser frame, and — for as long as it lasts — in the static mode,
377
+ * where no mount is coming. Read from the doc JSON, so it shows exactly what
378
+ * the schema kept: the same sanitised document the editor would mount.
340
379
  */
341
380
  protected readonly preview: Signal<WrEditorJson | null>;
342
381
  protected readonly classes: Signal<string>;
343
382
  constructor();
344
- /** Focus the text surface — what Signal Forms calls to focus this control. */
383
+ /**
384
+ * Focus the editor — what Signal Forms calls to focus this control. Whatever
385
+ * is on screen takes it: the mounted surface, or the static document, which
386
+ * keeps a tab stop for exactly this reason.
387
+ */
345
388
  focus(options?: FocusOptions): void;
346
389
  protected label(tool: WrEditorCommandTool): string;
347
390
  protected icon(tool: WrEditorCommandTool): readonly string[];
@@ -396,6 +439,17 @@ declare class WrEditor implements FormValueControl<WrEditorValue> {
396
439
  * (IME) replaces the composition too: the model is the source of truth.
397
440
  */
398
441
  private sync;
442
+ /**
443
+ * The check `mount()` makes, made without a mount: the same warning and the
444
+ * same `refused` flag, so an unreadable value is refused whether or not
445
+ * ProseMirror ever starts. Without it a read-only editor met one with silence
446
+ * — `preview()` renders nothing for a document it cannot read, and the model
447
+ * goes on holding a value nothing has said a word about.
448
+ *
449
+ * The parsed document is dropped; `preview()` reads its own. The cost is one
450
+ * extra parse per write, and only while read-only, where a write is rare.
451
+ */
452
+ private readStatically;
399
453
  /**
400
454
  * A new format whose value could not be read: keep the document, moved into
401
455
  * the new format's schema — markdown's lacks underline, so that mark is
@@ -1,4 +1,5 @@
1
1
  import * as _angular_core from '@angular/core';
2
+ import { IsActiveMatchOptions } from '@angular/router';
2
3
  import { WrIconName } from 'ngwr/icon';
3
4
 
4
5
  /**
@@ -8,6 +9,13 @@ import { WrIconName } from 'ngwr/icon';
8
9
  * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
9
10
  */
10
11
 
12
+ /**
13
+ * What `[routerLinkActiveOptions]` accepts: the short form, or any subset of the
14
+ * router's own match options (`paths`, `queryParams`, `matrixParams`, `fragment`).
15
+ */
16
+ type WrSidebarActiveOptions = {
17
+ readonly exact: boolean;
18
+ } | Partial<IsActiveMatchOptions>;
11
19
  /** A single navigable entry. */
12
20
  interface WrSidebarItem {
13
21
  readonly title: string;
@@ -18,6 +26,16 @@ interface WrSidebarItem {
18
26
  readonly badge?: string;
19
27
  /** Render as disabled (no navigation). */
20
28
  readonly disabled?: boolean;
29
+ /**
30
+ * How the router decides this entry is active — the value of
31
+ * `[routerLinkActiveOptions]`. The default matches a path PREFIX, so an entry at
32
+ * `['/']` is active on every route; `{ exact: true }` is what a "Home" entry
33
+ * wants. It decides the active class only: `autoExpand` finds the group holding
34
+ * the current route by the same prefix rule either way.
35
+ *
36
+ * @default { exact: false }
37
+ */
38
+ readonly activeOptions?: WrSidebarActiveOptions;
21
39
  }
22
40
  /** Expandable group of items. */
23
41
  interface WrSidebarGroup {
@@ -76,6 +94,13 @@ declare class WrSidebar {
76
94
  /** Auto-expand the group containing the active route. @default true */
77
95
  readonly autoExpand: _angular_core.InputSignalWithTransform<boolean, any>;
78
96
  protected readonly isGroup: typeof isGroup;
97
+ /**
98
+ * What an entry without its own `activeOptions` gets. It is the router's own
99
+ * default, written out because a binding cannot be left unset per entry.
100
+ */
101
+ protected readonly defaultActiveOptions: {
102
+ exact: boolean;
103
+ };
79
104
  private readonly opened;
80
105
  /** Group titles already seeded from `defaultOpen`, so it seeds once and not again. */
81
106
  private readonly seeded;
@@ -16,7 +16,7 @@ import { InjectionToken } from '@angular/core';
16
16
  * Distinct from `Version` exported by `@angular/core`, which reports the
17
17
  * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
18
18
  */
19
- declare const NGWR_VERSION = "14.9.0";
19
+ declare const NGWR_VERSION = "14.10.0";
20
20
 
21
21
  /**
22
22
  * @license