@akcelik/strct 5.12.0 → 5.14.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.
@@ -1,8 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, model, output, contentChildren, HostListener, effect, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID, forwardRef, DestroyRef, TemplateRef, contentChild, viewChild, InjectionToken, untracked, ViewContainerRef, Injector, viewChildren, Renderer2, Pipe, afterRenderEffect } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, HostListener, TemplateRef, contentChildren, viewChild, effect, untracked, model, output, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID, forwardRef, DestroyRef, contentChild, InjectionToken, ViewContainerRef, Injector, viewChildren, Renderer2, Pipe, afterRenderEffect } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { DOCUMENT as DOCUMENT$1, NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
5
- import { RouterLink, RouterLinkActive } from '@angular/router';
5
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router';
6
7
  import * as i1 from '@angular/forms';
7
8
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
8
9
 
@@ -1026,122 +1027,788 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1026
1027
  args: [{ selector: '[strctOverlay]' }]
1027
1028
  }], ctorParameters: () => [], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctOverlay", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctOverlayPlacement", required: false }] }], matchWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctOverlayMatchWidth", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctOverlayGap", required: false }] }] } });
1028
1029
 
1029
- let shellCounter = 0;
1030
- /** Shared layout state between shell parts. */
1031
- class StrctShellService {
1032
- mobileNavOpen = signal(false, /* @ts-ignore */
1033
- ...(ngDevMode ? [{ debugName: "mobileNavOpen" }] : /* istanbul ignore next */ []));
1034
- /** Id of the vertical nav controlled by the header drawer toggle. */
1035
- navId = `strct-vnav-${++shellCounter}`;
1030
+ /**
1031
+ * Dev-mode diagnostics for combinations a component accepts but cannot honour.
1032
+ *
1033
+ * Every message names what was ignored and what to do instead. Keyed: a
1034
+ * condition warns once per page load, not once per change detection.
1035
+ *
1036
+ * Production cost: none, PROVIDED each call site is guarded INLINE with
1037
+ * if (typeof ngDevMode !== 'undefined' && ngDevMode) { … }
1038
+ * Production builds define `ngDevMode` as false, and the minifier then drops
1039
+ * the whole block — messages included. A helper function returning that same
1040
+ * expression would NOT be folded across the call, so the check and its
1041
+ * strings would ship; that is why no such helper exists here.
1042
+ */
1043
+ const warned = new Set();
1044
+ function strctDevWarn(key, message) {
1045
+ if (typeof ngDevMode === 'undefined' || !ngDevMode || warned.has(key))
1046
+ return;
1047
+ warned.add(key);
1048
+ console.warn(message);
1049
+ }
1050
+ /** Test hook: forget which warnings have fired. Not part of the public API. */
1051
+ function resetStrctDevWarnings() {
1052
+ warned.clear();
1036
1053
  }
1054
+
1037
1055
  /**
1038
- * Application frame: a full-viewport grid of header / body / footer rows.
1039
- * <strct-shell>
1040
- * <strct-header>…</strct-header>
1041
- * <div strctShellMain>… sidebar + content …</div>
1042
- * <strct-footer>…</strct-footer>
1043
- * </strct-shell>
1056
+ * Click-to-open menu:
1057
+ * <strct-dropdown align="end">
1058
+ * <button strct-button strctDropdownTrigger>Actions</button>
1059
+ * <strct-dropdown-item>Rename</strct-dropdown-item>
1060
+ * <strct-dropdown-item critical>Delete</strct-dropdown-item>
1061
+ * </strct-dropdown>
1062
+ *
1063
+ * With `popover`, the panel holds *form controls* instead of menu items: an
1064
+ * inner click no longer closes it (only outside click / Escape do), and the
1065
+ * semantics switch from `role="menu"` to a labeled `role="dialog"` — a panel
1066
+ * of selects is not a menu to a screen reader either:
1067
+ *
1068
+ * <strct-dropdown popover popoverLabel="Filters">
1069
+ * <button strct-button strctDropdownTrigger>Filters</button>
1070
+ * <strct-field label="Severity">…</strct-field>
1071
+ * </strct-dropdown>
1044
1072
  */
1045
- class StrctShell {
1073
+ class StrctDropdown {
1074
+ host = inject(ElementRef);
1075
+ /** Horizontal alignment of the menu. */
1076
+ align = input('start', /* @ts-ignore */
1077
+ ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
1046
1078
  /**
1047
- * Id of the main region. Given one, the shell renders "Skip to main content"
1048
- * as its first focusable element — invisible until focused. Null (the
1049
- * default) renders nothing, so existing shells are unchanged.
1079
+ * Popover mode for panels holding form controls (filter/settings panels):
1080
+ * inner clicks never close the panel — only outside click / Escape do —
1081
+ * and it renders as a labeled `role="dialog"` instead of a menu.
1050
1082
  */
1051
- skipLinkTarget = input(null, /* @ts-ignore */
1052
- ...(ngDevMode ? [{ debugName: "skipLinkTarget" }] : /* istanbul ignore next */ []));
1053
- /** The link's text (localisable). */
1054
- skipLinkLabel = input('Skip to main content', /* @ts-ignore */
1055
- ...(ngDevMode ? [{ debugName: "skipLinkLabel" }] : /* istanbul ignore next */ []));
1056
- /** The name of the backdrop that closes the navigation drawer on a phone. */
1057
- closeNavLabel = input('Close navigation', /* @ts-ignore */
1058
- ...(ngDevMode ? [{ debugName: "closeNavLabel" }] : /* istanbul ignore next */ []));
1059
- /** The drawer's open state lives on the shell, and so does its backdrop. */
1060
- shell = inject(StrctShellService);
1061
- doc = inject(DOCUMENT$1);
1083
+ popover = input(false, { ...(ngDevMode ? { debugName: "popover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1084
+ /** Accessible name of the popover dialog (localizable). */
1085
+ popoverLabel = input('Filters', /* @ts-ignore */
1086
+ ...(ngDevMode ? [{ debugName: "popoverLabel" }] : /* istanbul ignore next */ []));
1062
1087
  /**
1063
- * Moves focus rather than only the scroll position: an href alone scrolls
1064
- * without focusing, so the next Tab would resume from the link.
1088
+ * Only with `popover`: the panel's rows are controls in their own right — a
1089
+ * theme switcher, a filter — so they keep a real tab stop instead of the
1090
+ * `tabindex="-1"` a menu's items carry. Without it nothing inside a popover
1091
+ * can be reached from the keyboard at all.
1065
1092
  */
1066
- skipToMain(event) {
1067
- const id = this.skipLinkTarget();
1068
- const target = id ? this.doc.getElementById(id) : null;
1069
- if (!target)
1093
+ focusable = input(false, { ...(ngDevMode ? { debugName: "focusable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1094
+ open = signal(false, /* @ts-ignore */
1095
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1096
+ constructor() {
1097
+ // The static `popover` attribute collides with the native HTML Popover
1098
+ // API: the UA styles [popover] hosts (Canvas background, medium border,
1099
+ // fixed positioning) — a white frame around the trigger in dark themes.
1100
+ // Angular has already read the input by now; the DOM attribute must go.
1101
+ this.host.nativeElement.removeAttribute('popover');
1102
+ }
1103
+ /** Where focus was when the menu opened — restored on Escape/selection. */
1104
+ lastFocused = null;
1105
+ toggle() {
1106
+ const willOpen = !this.open();
1107
+ if (willOpen)
1108
+ this.lastFocused = document.activeElement;
1109
+ this.open.set(willOpen);
1110
+ if (willOpen && !this.popover())
1111
+ this.focusInitialItem();
1112
+ }
1113
+ /** Open (if closed) and move focus into the menu — ArrowDown on the trigger. */
1114
+ openMenu() {
1115
+ if (this.open())
1070
1116
  return;
1071
- event.preventDefault();
1072
- if (!target.hasAttribute('tabindex'))
1073
- target.setAttribute('tabindex', '-1');
1074
- target.focus();
1075
- // Optional: not every environment implements it (jsdom does not).
1076
- target.scrollIntoView?.({ block: 'start' });
1117
+ this.lastFocused = document.activeElement;
1118
+ this.open.set(true);
1119
+ if (!this.popover())
1120
+ this.focusInitialItem();
1077
1121
  }
1078
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
1079
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctShell, isStandalone: true, selector: "strct-shell", inputs: { skipLinkTarget: { classPropertyName: "skipLinkTarget", publicName: "skipLinkTarget", isSignal: true, isRequired: false, transformFunction: null }, skipLinkLabel: { classPropertyName: "skipLinkLabel", publicName: "skipLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, closeNavLabel: { classPropertyName: "closeNavLabel", publicName: "closeNavLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-shell" }, providers: [StrctShellService], ngImport: i0, template: `
1080
- @if (skipLinkTarget()) {
1081
- <!-- The first focusable element on the page, invisible until focused. -->
1082
- <a
1083
- class="strct-shell__skip"
1084
- [attr.href]="'#' + skipLinkTarget()"
1085
- (click)="skipToMain($event)"
1086
- >{{ skipLinkLabel() }}</a
1087
- >
1122
+ close(restoreFocus = false) {
1123
+ this.open.set(false);
1124
+ if (restoreFocus)
1125
+ this.lastFocused?.focus?.();
1088
1126
  }
1089
- <ng-content select="strct-header" />
1090
- @if (shell.mobileNavOpen()) {
1091
- <!-- Outside the drawer, which is transformed and would otherwise be this
1092
- fixed element's containing block (BUG-53-01). -->
1093
- <div
1094
- class="strct-vnav__backdrop"
1095
- tabindex="-1"
1096
- role="button"
1097
- [attr.aria-label]="closeNavLabel()"
1098
- (click)="shell.mobileNavOpen.set(false)"
1099
- (keydown.enter)="shell.mobileNavOpen.set(false)"
1100
- (keydown.space)="shell.mobileNavOpen.set(false)"
1101
- ></div>
1127
+ /**
1128
+ * Menu mode closes ONLY on a real item activation — a click that lands on
1129
+ * the menu's padding or a divider (a 2px miss) keeps it open instead of
1130
+ * throwing the whole interaction away. Popover form controls never close.
1131
+ */
1132
+ onInnerActivate(event) {
1133
+ if (this.popover())
1134
+ return;
1135
+ const target = event.target;
1136
+ if (target?.closest('.strct-dd__itemaction'))
1137
+ return; // the second action is not the item
1138
+ const item = target?.closest('strct-dropdown-item');
1139
+ if (item && item.getAttribute('aria-disabled') !== 'true')
1140
+ this.close(true);
1102
1141
  }
1103
- <div class="strct-shell__main"><ng-content /></div>
1104
- <ng-content select="strct-footer" />
1105
- `, isInline: true, styles: [".strct-shell{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden;background:var(--bg-0)}.strct-shell__main{display:flex;min-height:0;overflow:hidden}.strct-shell__skip{position:absolute;inset-inline-start:0;top:0;z-index:1;transform:translateY(-120%);padding:var(--space-2) var(--space-3);border-radius:0 0 var(--radius-md) 0;background:var(--acc);color:var(--inv);font-size:var(--text-sm);text-decoration:none}.strct-shell__skip:focus-visible{transform:none;outline:2px solid var(--acc50);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1106
- }
1107
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctShell, decorators: [{
1108
- type: Component,
1109
- args: [{ selector: 'strct-shell', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [StrctShellService], template: `
1110
- @if (skipLinkTarget()) {
1111
- <!-- The first focusable element on the page, invisible until focused. -->
1112
- <a
1113
- class="strct-shell__skip"
1114
- [attr.href]="'#' + skipLinkTarget()"
1115
- (click)="skipToMain($event)"
1116
- >{{ skipLinkLabel() }}</a
1117
- >
1142
+ /** APG menu keyboarding: arrows rove, Home/End jump, Enter/Space activate. */
1143
+ onMenuKeydown(event) {
1144
+ if (this.popover())
1145
+ return;
1146
+ const items = this.navItems();
1147
+ if (!items.length)
1148
+ return;
1149
+ const idx = items.indexOf(event.target);
1150
+ const key = event.key;
1151
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
1152
+ event.preventDefault();
1153
+ const next = idx === -1
1154
+ ? key === 'ArrowDown'
1155
+ ? 0
1156
+ : items.length - 1
1157
+ : (idx + (key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
1158
+ items[next].focus();
1159
+ }
1160
+ else if (key === 'Home' || key === 'End') {
1161
+ event.preventDefault();
1162
+ items[key === 'Home' ? 0 : items.length - 1].focus();
1163
+ }
1164
+ else if (key === 'Enter' || key === ' ') {
1165
+ event.preventDefault();
1166
+ const target = event.target;
1167
+ if (target.getAttribute('aria-disabled') !== 'true')
1168
+ target.click();
1169
+ }
1170
+ else if (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'Delete') {
1171
+ this.onItemActionKey(event, key);
1172
+ }
1173
+ else if (key === 'Tab') {
1174
+ this.close();
1175
+ }
1118
1176
  }
1119
- <ng-content select="strct-header" />
1120
- @if (shell.mobileNavOpen()) {
1121
- <!-- Outside the drawer, which is transformed and would otherwise be this
1122
- fixed element's containing block (BUG-53-01). -->
1123
- <div
1124
- class="strct-vnav__backdrop"
1125
- tabindex="-1"
1126
- role="button"
1127
- [attr.aria-label]="closeNavLabel()"
1128
- (click)="shell.mobileNavOpen.set(false)"
1129
- (keydown.enter)="shell.mobileNavOpen.set(false)"
1130
- (keydown.space)="shell.mobileNavOpen.set(false)"
1131
- ></div>
1177
+ /**
1178
+ * An item with a trailing action has two targets: the right arrow reaches the
1179
+ * action from its item, the left arrow returns, and Delete on the item
1180
+ * triggers it (the WAI-ARIA pattern for a secondary action). Keys that do not
1181
+ * apply are left alone, so submenus keep their own arrow behaviour.
1182
+ */
1183
+ onItemActionKey(event, key) {
1184
+ const target = event.target;
1185
+ if (key === 'ArrowLeft') {
1186
+ const item = target.closest('strct-dropdown-item');
1187
+ if (!target.classList.contains('strct-dd__itemaction') || !item)
1188
+ return;
1189
+ event.preventDefault();
1190
+ item.focus();
1191
+ return;
1192
+ }
1193
+ const item = target.closest('strct-dropdown-item');
1194
+ const action = item?.querySelector('.strct-dd__itemaction');
1195
+ if (!item || item !== target || !action)
1196
+ return;
1197
+ event.preventDefault();
1198
+ if (key === 'ArrowRight')
1199
+ action.focus();
1200
+ else
1201
+ action.click();
1132
1202
  }
1133
- <div class="strct-shell__main"><ng-content /></div>
1134
- <ng-content select="strct-footer" />
1135
- `, host: { class: 'strct-shell' }, styles: [".strct-shell{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden;background:var(--bg-0)}.strct-shell__main{display:flex;min-height:0;overflow:hidden}.strct-shell__skip{position:absolute;inset-inline-start:0;top:0;z-index:1;transform:translateY(-120%);padding:var(--space-2) var(--space-3);border-radius:0 0 var(--radius-md) 0;background:var(--acc);color:var(--inv);font-size:var(--text-sm);text-decoration:none}.strct-shell__skip:focus-visible{transform:none;outline:2px solid var(--acc50);outline-offset:2px}\n"] }]
1136
- }], propDecorators: { skipLinkTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkTarget", required: false }] }], skipLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkLabel", required: false }] }], closeNavLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeNavLabel", required: false }] }] } });
1137
- /** Top application bar. Holds brand on the left and actions on the right. */
1138
- class StrctHeader {
1139
- /** Accessible label of the mobile drawer toggle. */
1203
+ enabledItems() {
1204
+ return [
1205
+ ...this.host.nativeElement.querySelectorAll('strct-dropdown-item:not([aria-disabled="true"]), strct-submenu .strct-submenu__trigger'),
1206
+ ];
1207
+ }
1208
+ /** What the arrow keys visit: enabled items, plus disabled ones that carry a
1209
+ * hint (so the reason can be read). The menu still opens on an enabled one. */
1210
+ navItems() {
1211
+ const host = this.host.nativeElement;
1212
+ const rows = [
1213
+ ...host.querySelectorAll('strct-dropdown-item'),
1214
+ ...host.querySelectorAll('strct-submenu .strct-submenu__trigger'),
1215
+ ].filter((el) => el.getAttribute('aria-disabled') !== 'true' ||
1216
+ el.classList.contains('strct-dd__item--hinted'));
1217
+ // Two queries, so restore DOM order explicitly. (A single comma-separated
1218
+ // selector is DOM-ordered in browsers, but not in every DOM implementation
1219
+ // — jsdom returns it grouped, which scrambles arrow-key order in tests.)
1220
+ return rows.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
1221
+ }
1222
+ /** Focus the selected item if there is one, else the first — after render. */
1223
+ focusInitialItem() {
1224
+ setTimeout(() => {
1225
+ const items = this.enabledItems();
1226
+ (items.find((i) => i.getAttribute('aria-checked') === 'true') ?? items[0])?.focus();
1227
+ });
1228
+ }
1229
+ onDocClick(event) {
1230
+ if (this.open() && !this.host.nativeElement.contains(event.target)) {
1231
+ this.close();
1232
+ }
1233
+ }
1234
+ onEscape() {
1235
+ if (this.open())
1236
+ this.close(true);
1237
+ }
1238
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
1239
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDropdown, isStandalone: true, selector: "strct-dropdown", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, popover: { classPropertyName: "popover", publicName: "popover", isSignal: true, isRequired: false, transformFunction: null }, popoverLabel: { classPropertyName: "popoverLabel", publicName: "popoverLabel", isSignal: true, isRequired: false, transformFunction: null }, focusable: { classPropertyName: "focusable", publicName: "focusable", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" }, classAttribute: "strct-dd" }, ngImport: i0, template: `
1240
+ <!--
1241
+ The wrapper is intentionally inert: the projected trigger should be a
1242
+ real button (nesting interactives fails axe). Its native click —
1243
+ pointer or Enter/Space — bubbles here; the strctDropdownTrigger
1244
+ directive puts aria-haspopup/aria-expanded on the button itself.
1245
+ -->
1246
+ <div #trigger class="strct-dd__trigger" (click)="toggle()">
1247
+ <ng-content select="[strctDropdownTrigger]" />
1248
+ </div>
1249
+ <!--
1250
+ One panel for both modes (a default ng-content per @if branch would strand
1251
+ the projected content in the inactive branch) — mode picks the semantics.
1252
+ -->
1253
+ @if (open()) {
1254
+ <div
1255
+ class="strct-dd__menu"
1256
+ [class.strct-dd__menu--popover]="popover()"
1257
+ [strctOverlay]="trigger"
1258
+ [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
1259
+ [attr.role]="popover() ? 'dialog' : 'menu'"
1260
+ [attr.aria-label]="popover() ? popoverLabel() : null"
1261
+ [attr.tabindex]="popover() ? -1 : 0"
1262
+ (click)="onInnerActivate($event)"
1263
+ (keydown)="onMenuKeydown($event)"
1264
+ >
1265
+ <ng-content />
1266
+ </div>
1267
+ }
1268
+ `, isInline: true, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-dropdown);min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"], dependencies: [{ kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1269
+ }
1270
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdown, decorators: [{
1271
+ type: Component,
1272
+ args: [{ selector: 'strct-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctOverlay], template: `
1273
+ <!--
1274
+ The wrapper is intentionally inert: the projected trigger should be a
1275
+ real button (nesting interactives fails axe). Its native click —
1276
+ pointer or Enter/Space — bubbles here; the strctDropdownTrigger
1277
+ directive puts aria-haspopup/aria-expanded on the button itself.
1278
+ -->
1279
+ <div #trigger class="strct-dd__trigger" (click)="toggle()">
1280
+ <ng-content select="[strctDropdownTrigger]" />
1281
+ </div>
1282
+ <!--
1283
+ One panel for both modes (a default ng-content per @if branch would strand
1284
+ the projected content in the inactive branch) — mode picks the semantics.
1285
+ -->
1286
+ @if (open()) {
1287
+ <div
1288
+ class="strct-dd__menu"
1289
+ [class.strct-dd__menu--popover]="popover()"
1290
+ [strctOverlay]="trigger"
1291
+ [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
1292
+ [attr.role]="popover() ? 'dialog' : 'menu'"
1293
+ [attr.aria-label]="popover() ? popoverLabel() : null"
1294
+ [attr.tabindex]="popover() ? -1 : 0"
1295
+ (click)="onInnerActivate($event)"
1296
+ (keydown)="onMenuKeydown($event)"
1297
+ >
1298
+ <ng-content />
1299
+ </div>
1300
+ }
1301
+ `, host: { class: 'strct-dd' }, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-dropdown);min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"] }]
1302
+ }], ctorParameters: () => [], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "popover", required: false }] }], popoverLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "popoverLabel", required: false }] }], focusable: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusable", required: false }] }], onDocClick: [{
1303
+ type: HostListener,
1304
+ args: ['document:click', ['$event']]
1305
+ }], onEscape: [{
1306
+ type: HostListener,
1307
+ args: ['document:keydown.escape']
1308
+ }] } });
1309
+ /**
1310
+ * Marks (and upgrades) the dropdown's trigger element. The attribute alone is
1311
+ * enough for content projection; importing the directive additionally wires
1312
+ * `aria-haspopup` / `aria-expanded` onto the real button.
1313
+ */
1314
+ class StrctDropdownTrigger {
1315
+ dd = inject(StrctDropdown, { optional: true });
1316
+ onArrowDown(event) {
1317
+ if (!this.dd || this.dd.popover())
1318
+ return;
1319
+ event.preventDefault();
1320
+ this.dd.openMenu();
1321
+ }
1322
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1323
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownTrigger, isStandalone: true, selector: "[strctDropdownTrigger]", host: { listeners: { "keydown.arrowdown": "onArrowDown($event)" }, properties: { "attr.aria-haspopup": "dd ? (dd.popover() ? 'dialog' : 'menu') : null", "attr.aria-expanded": "dd ? dd.open() : null" } }, ngImport: i0 });
1324
+ }
1325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownTrigger, decorators: [{
1326
+ type: Directive,
1327
+ args: [{
1328
+ selector: '[strctDropdownTrigger]',
1329
+ host: {
1330
+ '[attr.aria-haspopup]': "dd ? (dd.popover() ? 'dialog' : 'menu') : null",
1331
+ '[attr.aria-expanded]': 'dd ? dd.open() : null',
1332
+ '(keydown.arrowdown)': 'onArrowDown($event)',
1333
+ },
1334
+ }]
1335
+ }] });
1336
+ let dropdownItemCounter = 0;
1337
+ /** A selectable row inside a `<strct-dropdown>`. */
1338
+ /**
1339
+ * A second action at the end of a menu item — "delete this saved view" beside
1340
+ * "open it". It stops its own click from reaching the item, so the item is not
1341
+ * activated and the menu stays open, and it stays out of the Tab order: the
1342
+ * right arrow reaches it from its item, the left arrow returns, and Delete on
1343
+ * the item triggers it.
1344
+ */
1345
+ class StrctDropdownItemAction {
1346
+ onClick(event) {
1347
+ event.stopPropagation();
1348
+ }
1349
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1350
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownItemAction, isStandalone: true, selector: "[strctDropdownItemAction]", host: { attributes: { "tabindex": "-1" }, listeners: { "click": "onClick($event)" }, classAttribute: "strct-dd__itemaction" }, ngImport: i0 });
1351
+ }
1352
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemAction, decorators: [{
1353
+ type: Directive,
1354
+ args: [{
1355
+ selector: '[strctDropdownItemAction]',
1356
+ host: {
1357
+ class: 'strct-dd__itemaction',
1358
+ tabindex: '-1',
1359
+ '(click)': 'onClick($event)',
1360
+ },
1361
+ }]
1362
+ }] });
1363
+ /**
1364
+ * A leading icon for a menu row, in a colour of its own:
1365
+ * `<strct-icon strctDropdownItemIcon tone="var(--prod-acc)" name="shield" />`.
1366
+ * The row keeps its text aligned whether or not the icon is there.
1367
+ */
1368
+ class StrctDropdownItemIcon {
1369
+ /** A colour token or value — a product's accent, say. */
1370
+ tone = input('', /* @ts-ignore */
1371
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
1372
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1373
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: StrctDropdownItemIcon, isStandalone: true, selector: "[strctDropdownItemIcon]", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.color": "tone() || null" }, classAttribute: "strct-dd__itemlead" }, ngImport: i0 });
1374
+ }
1375
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemIcon, decorators: [{
1376
+ type: Directive,
1377
+ args: [{
1378
+ selector: '[strctDropdownItemIcon]',
1379
+ host: { class: 'strct-dd__itemlead', '[style.color]': 'tone() || null' },
1380
+ }]
1381
+ }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }] } });
1382
+ /**
1383
+ * The row's label, when the row also carries a description. A screen reader
1384
+ * reads a row in DOM order, and a projected slot can only be declared before
1385
+ * the catch-all one — so marking the label is what keeps "HyperStruct Backup"
1386
+ * ahead of "2 VMs past RPO" in what the row is called. A row without a
1387
+ * description does not need it.
1388
+ */
1389
+ class StrctDropdownItemLabel {
1390
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1391
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownItemLabel, isStandalone: true, selector: "[strctDropdownItemLabel]", host: { classAttribute: "strct-dd__itemlabel" }, ngImport: i0 });
1392
+ }
1393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemLabel, decorators: [{
1394
+ type: Directive,
1395
+ args: [{
1396
+ selector: '[strctDropdownItemLabel]',
1397
+ host: { class: 'strct-dd__itemlabel' },
1398
+ }]
1399
+ }] });
1400
+ /**
1401
+ * A second, quieter line under a row's label: "● 2 VMs past RPO" under
1402
+ * "HyperStruct Backup". It is inside the row, so it is part of what the row
1403
+ * is called — put the label in `[strctDropdownItemLabel]` so it is read
1404
+ * first.
1405
+ */
1406
+ class StrctDropdownItemDescription {
1407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemDescription, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1408
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownItemDescription, isStandalone: true, selector: "[strctDropdownItemDescription]", host: { classAttribute: "strct-dd__itemdesc" }, ngImport: i0 });
1409
+ }
1410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemDescription, decorators: [{
1411
+ type: Directive,
1412
+ args: [{
1413
+ selector: '[strctDropdownItemDescription]',
1414
+ host: { class: 'strct-dd__itemdesc' },
1415
+ }]
1416
+ }] });
1417
+ class StrctDropdownItem {
1418
+ /**
1419
+ * Select-like usage: bind `selected` and the item becomes a
1420
+ * `menuitemradio` with `aria-checked`, a leading ✓ on the current choice
1421
+ * and an aligned lead slot — so reopening the menu shows what is chosen.
1422
+ * Leave unbound for plain action items.
1423
+ */
1424
+ selected = input(null, /* @ts-ignore */
1425
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1426
+ /** Danger. */
1427
+ critical = input(false, { ...(ngDevMode ? { debugName: "critical" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1428
+ /** Static disable flag. */
1429
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1430
+ /**
1431
+ * A short explanation — typically why a disabled item is unavailable. Shown
1432
+ * as the tooltip and given to assistive technology as the description; never
1433
+ * rendered inline. A disabled item with a hint stays keyboard-reachable so
1434
+ * its reason can be read; activation stays blocked.
1435
+ */
1436
+ hint = input(null, /* @ts-ignore */
1437
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
1438
+ hintId = `strct-dd-item-${++dropdownItemCounter}-hint`;
1439
+ /** The panel this row belongs to, when it is inside one. */
1440
+ panel = inject(StrctDropdown, { optional: true });
1441
+ /**
1442
+ * A menu's rows are roved with the arrow keys, so they stay out of the tab
1443
+ * order. A `popover [focusable]` is a panel of real controls, and its rows
1444
+ * keep a tab stop of their own.
1445
+ */
1446
+ tabindex = computed(() => {
1447
+ if (this.disabled() && !this.hint())
1448
+ return null;
1449
+ return this.panel?.popover() && this.panel.focusable() ? 0 : -1;
1450
+ }, /* @ts-ignore */
1451
+ ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
1452
+ constructor() {
1453
+ // Without pointer-events:none (see the hinted style), a disabled item
1454
+ // would pass clicks to the consumer's (click). Capture-phase at the target
1455
+ // runs before those listeners, and stopping here also keeps the menu's own
1456
+ // activate-and-close handler from seeing it.
1457
+ const host = inject((ElementRef)).nativeElement;
1458
+ // Dev-only: a description reads before an unmarked label, which is the
1459
+ // one thing this slot must not do.
1460
+ afterNextRender(() => {
1461
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
1462
+ if (host.querySelector('.strct-dd__itemdesc') &&
1463
+ !host.querySelector('.strct-dd__itemlabel')) {
1464
+ strctDevWarn('dd-item-description-without-label', '[strct] strct-dropdown-item: a [strctDropdownItemDescription] row reads its ' +
1465
+ 'description before its label, because a slot can only be declared before the ' +
1466
+ 'catch-all one. Wrap the label in [strctDropdownItemLabel].');
1467
+ }
1468
+ }
1469
+ });
1470
+ host.addEventListener('click', (event) => {
1471
+ if (!this.disabled())
1472
+ return;
1473
+ event.preventDefault();
1474
+ event.stopImmediatePropagation();
1475
+ }, { capture: true });
1476
+ }
1477
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
1478
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDropdownItem, isStandalone: true, selector: "strct-dropdown-item", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, critical: { classPropertyName: "critical", publicName: "critical", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.role": "selected() === null ? 'menuitem' : 'menuitemradio'", "attr.aria-checked": "selected()", "attr.tabindex": "tabindex()", "attr.title": "hint() || null", "attr.aria-describedby": "hint() ? hintId : null", "class.strct-dd__item--hinted": "!!hint()", "class.strct-dd__item--critical": "critical()", "class.strct-dd__item--selected": "selected() === true", "attr.aria-disabled": "disabled() || null" }, classAttribute: "strct-dd__item" }, ngImport: i0, template: `@if (selected() !== null) {
1479
+ <span class="strct-dd__check" aria-hidden="true">
1480
+ @if (selected()) {
1481
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
1482
+ }
1483
+ </span>
1484
+ }
1485
+ <!-- Every selective slot is declared before the catch-all, or the
1486
+ catch-all would swallow its content; the visual order is CSS's
1487
+ (the label is raw text, which only source order can place). -->
1488
+ <ng-content select="[strctDropdownItemIcon]" />
1489
+ <ng-content select="[strctDropdownItemAction]" />
1490
+ <span class="strct-dd__itemtext">
1491
+ <ng-content select="[strctDropdownItemLabel]" />
1492
+ <ng-content select="[strctDropdownItemDescription]" />
1493
+ </span>
1494
+ <ng-content />
1495
+ @if (hint()) {
1496
+ <!-- hidden: kept out of the item's accessible NAME, still read as its
1497
+ description through aria-describedby. -->
1498
+ <span [id]="hintId" hidden>{{ hint() }}</span>
1499
+ }`, isInline: true, styles: [".strct-dd__item{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-dd__item:hover,.strct-dd__item:focus-visible{background:var(--bg-3);outline:none}.strct-dd__itemaction{order:1;margin-inline-start:auto;flex:none}.strct-dd__itemlead{flex:none;display:inline-flex;align-items:center}.strct-dd__itemlabel{min-width:0;overflow:hidden;text-overflow:ellipsis}.strct-dd__itemtext{display:contents}.strct-dd__item:has(.strct-dd__itemdesc) .strct-dd__itemtext{display:flex;flex-direction:column;row-gap:2px;min-width:0}.strct-dd__itemdesc{display:flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--t2)}.strct-dd__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-dd__item--selected{color:var(--t1);font-weight:600;background:var(--acc-s)}.strct-dd__item--critical{color:var(--critical)}.strct-dd__item--critical:hover{background:var(--critical-bg)}.strct-dd__item[aria-disabled=true]{color:var(--t4);pointer-events:none}.strct-dd__item--hinted[aria-disabled=true]{pointer-events:auto;cursor:not-allowed}.strct-dd__item--hinted[aria-disabled=true]:hover{background:transparent}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1500
+ }
1501
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItem, decorators: [{
1502
+ type: Component,
1503
+ args: [{ selector: 'strct-dropdown-item', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `@if (selected() !== null) {
1504
+ <span class="strct-dd__check" aria-hidden="true">
1505
+ @if (selected()) {
1506
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
1507
+ }
1508
+ </span>
1509
+ }
1510
+ <!-- Every selective slot is declared before the catch-all, or the
1511
+ catch-all would swallow its content; the visual order is CSS's
1512
+ (the label is raw text, which only source order can place). -->
1513
+ <ng-content select="[strctDropdownItemIcon]" />
1514
+ <ng-content select="[strctDropdownItemAction]" />
1515
+ <span class="strct-dd__itemtext">
1516
+ <ng-content select="[strctDropdownItemLabel]" />
1517
+ <ng-content select="[strctDropdownItemDescription]" />
1518
+ </span>
1519
+ <ng-content />
1520
+ @if (hint()) {
1521
+ <!-- hidden: kept out of the item's accessible NAME, still read as its
1522
+ description through aria-describedby. -->
1523
+ <span [id]="hintId" hidden>{{ hint() }}</span>
1524
+ }`, host: {
1525
+ class: 'strct-dd__item',
1526
+ '[attr.role]': "selected() === null ? 'menuitem' : 'menuitemradio'",
1527
+ '[attr.aria-checked]': 'selected()',
1528
+ // A disabled item is skipped — unless it has a hint to be read.
1529
+ '[attr.tabindex]': 'tabindex()',
1530
+ '[attr.title]': 'hint() || null',
1531
+ '[attr.aria-describedby]': 'hint() ? hintId : null',
1532
+ '[class.strct-dd__item--hinted]': '!!hint()',
1533
+ '[class.strct-dd__item--critical]': 'critical()',
1534
+ '[class.strct-dd__item--selected]': 'selected() === true',
1535
+ '[attr.aria-disabled]': 'disabled() || null',
1536
+ }, styles: [".strct-dd__item{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-dd__item:hover,.strct-dd__item:focus-visible{background:var(--bg-3);outline:none}.strct-dd__itemaction{order:1;margin-inline-start:auto;flex:none}.strct-dd__itemlead{flex:none;display:inline-flex;align-items:center}.strct-dd__itemlabel{min-width:0;overflow:hidden;text-overflow:ellipsis}.strct-dd__itemtext{display:contents}.strct-dd__item:has(.strct-dd__itemdesc) .strct-dd__itemtext{display:flex;flex-direction:column;row-gap:2px;min-width:0}.strct-dd__itemdesc{display:flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--t2)}.strct-dd__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-dd__item--selected{color:var(--t1);font-weight:600;background:var(--acc-s)}.strct-dd__item--critical{color:var(--critical)}.strct-dd__item--critical:hover{background:var(--critical-bg)}.strct-dd__item[aria-disabled=true]{color:var(--t4);pointer-events:none}.strct-dd__item--hinted[aria-disabled=true]{pointer-events:auto;cursor:not-allowed}.strct-dd__item--hinted[aria-disabled=true]:hover{background:transparent}\n"] }]
1537
+ }], ctorParameters: () => [], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], critical: [{ type: i0.Input, args: [{ isSignal: true, alias: "critical", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
1538
+ /**
1539
+ * A named group of rows — "Create" above the create actions, where a divider
1540
+ * alone leaves what the group is unsaid. The label is not an item: the arrows
1541
+ * pass over it and it is never activated.
1542
+ */
1543
+ class StrctDropdownGroup {
1544
+ /** What the group is — read when a screen reader enters it. */
1545
+ label = input('', /* @ts-ignore */
1546
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1547
+ labelId = `strct-dd-group-${++dropdownItemCounter}-label`;
1548
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
1549
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctDropdownGroup, isStandalone: true, selector: "strct-dropdown-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId" }, classAttribute: "strct-dd__group" }, ngImport: i0, template: `<div class="strct-dd__grouplabel" [id]="labelId" role="presentation">
1550
+ {{ label() }}
1551
+ </div>
1552
+ <ng-content />`, isInline: true, styles: [".strct-dd__group{display:block}.strct-dd__group+.strct-dd__group{margin-top:4px;border-top:1px solid var(--b2);padding-top:4px}.strct-dd__grouplabel{padding:6px 10px 2px;font-size:var(--text-xs, 11px);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--t3)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1553
+ }
1554
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownGroup, decorators: [{
1555
+ type: Component,
1556
+ args: [{ selector: 'strct-dropdown-group', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `<div class="strct-dd__grouplabel" [id]="labelId" role="presentation">
1557
+ {{ label() }}
1558
+ </div>
1559
+ <ng-content />`, host: {
1560
+ class: 'strct-dd__group',
1561
+ role: 'group',
1562
+ '[attr.aria-labelledby]': 'labelId',
1563
+ }, styles: [".strct-dd__group{display:block}.strct-dd__group+.strct-dd__group{margin-top:4px;border-top:1px solid var(--b2);padding-top:4px}.strct-dd__grouplabel{padding:6px 10px 2px;font-size:var(--text-xs, 11px);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--t3)}\n"] }]
1564
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
1565
+ /** Thin separator between groups of menu items. */
1566
+ class StrctDropdownDivider {
1567
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownDivider, deps: [], target: i0.ɵɵFactoryTarget.Component });
1568
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownDivider, isStandalone: true, selector: "strct-dropdown-divider", host: { attributes: { "role": "separator" }, classAttribute: "strct-dd__divider" }, ngImport: i0, template: '', isInline: true, styles: [".strct-dd__divider{display:block;height:1px;margin:4px 6px;background:var(--b2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1569
+ }
1570
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownDivider, decorators: [{
1571
+ type: Component,
1572
+ args: [{ selector: 'strct-dropdown-divider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', host: { class: 'strct-dd__divider', role: 'separator' }, styles: [".strct-dd__divider{display:block;height:1px;margin:4px 6px;background:var(--b2)}\n"] }]
1573
+ }] });
1574
+
1575
+ let shellCounter = 0;
1576
+ /** Shared layout state between shell parts. */
1577
+ class StrctShellService {
1578
+ mobileNavOpen = signal(false, /* @ts-ignore */
1579
+ ...(ngDevMode ? [{ debugName: "mobileNavOpen" }] : /* istanbul ignore next */ []));
1580
+ /** Id of the vertical nav controlled by the header drawer toggle. */
1581
+ navId = `strct-vnav-${++shellCounter}`;
1582
+ /**
1583
+ * The header's drawer toggle, so closing the drawer can hand focus back to
1584
+ * what opened it. The header registers it; nothing else writes it.
1585
+ */
1586
+ drawerToggle = null;
1587
+ /**
1588
+ * Closes the navigation drawer. On a phone the drawer holds the
1589
+ * navigation, so a choice made in it opens a page *behind* it; the shell
1590
+ * calls this on every completed navigation unless told not to.
1591
+ */
1592
+ closeNav() {
1593
+ if (this.mobileNavOpen())
1594
+ this.mobileNavOpen.set(false);
1595
+ }
1596
+ }
1597
+ /**
1598
+ * Application frame: a full-viewport grid of header / body / footer rows.
1599
+ * <strct-shell>
1600
+ * <strct-header>…</strct-header>
1601
+ * <div strctShellMain>… sidebar + content …</div>
1602
+ * <strct-footer>…</strct-footer>
1603
+ * </strct-shell>
1604
+ */
1605
+ class StrctShell {
1606
+ /**
1607
+ * Id of the main region. Given one, the shell renders "Skip to main content"
1608
+ * as its first focusable element — invisible until focused. Null (the
1609
+ * default) renders nothing, so existing shells are unchanged.
1610
+ */
1611
+ skipLinkTarget = input(null, /* @ts-ignore */
1612
+ ...(ngDevMode ? [{ debugName: "skipLinkTarget" }] : /* istanbul ignore next */ []));
1613
+ /** The link's text (localisable). */
1614
+ skipLinkLabel = input('Skip to main content', /* @ts-ignore */
1615
+ ...(ngDevMode ? [{ debugName: "skipLinkLabel" }] : /* istanbul ignore next */ []));
1616
+ /** The name of the backdrop that closes the navigation drawer on a phone. */
1617
+ closeNavLabel = input('Close navigation', /* @ts-ignore */
1618
+ ...(ngDevMode ? [{ debugName: "closeNavLabel" }] : /* istanbul ignore next */ []));
1619
+ /**
1620
+ * Close the drawer when a navigation completes — the subscription every app
1621
+ * with a router ends up writing. It needs a `Router`; without one nothing
1622
+ * happens. Set it to `false` to keep the drawer open across navigations,
1623
+ * and close it yourself with `StrctShellService.closeNav()`.
1624
+ */
1625
+ closeNavOnNavigate = input(true, { ...(ngDevMode ? { debugName: "closeNavOnNavigate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1626
+ /** The drawer's open state lives on the shell, and so does its backdrop. */
1627
+ shell = inject(StrctShellService);
1628
+ doc = inject(DOCUMENT$1);
1629
+ router = inject(Router, { optional: true });
1630
+ constructor() {
1631
+ // An app without the router package still works: the token is optional and
1632
+ // the events stream is simply never there.
1633
+ this.router?.events.pipe(takeUntilDestroyed()).subscribe((event) => {
1634
+ if (event instanceof NavigationEnd && this.closeNavOnNavigate())
1635
+ this.shell.closeNav();
1636
+ });
1637
+ }
1638
+ /**
1639
+ * Moves focus rather than only the scroll position: an href alone scrolls
1640
+ * without focusing, so the next Tab would resume from the link.
1641
+ */
1642
+ skipToMain(event) {
1643
+ const id = this.skipLinkTarget();
1644
+ const target = id ? this.doc.getElementById(id) : null;
1645
+ if (!target)
1646
+ return;
1647
+ event.preventDefault();
1648
+ if (!target.hasAttribute('tabindex'))
1649
+ target.setAttribute('tabindex', '-1');
1650
+ target.focus();
1651
+ // Optional: not every environment implements it (jsdom does not).
1652
+ target.scrollIntoView?.({ block: 'start' });
1653
+ }
1654
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
1655
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctShell, isStandalone: true, selector: "strct-shell", inputs: { skipLinkTarget: { classPropertyName: "skipLinkTarget", publicName: "skipLinkTarget", isSignal: true, isRequired: false, transformFunction: null }, skipLinkLabel: { classPropertyName: "skipLinkLabel", publicName: "skipLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, closeNavLabel: { classPropertyName: "closeNavLabel", publicName: "closeNavLabel", isSignal: true, isRequired: false, transformFunction: null }, closeNavOnNavigate: { classPropertyName: "closeNavOnNavigate", publicName: "closeNavOnNavigate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-shell" }, providers: [StrctShellService], ngImport: i0, template: `
1656
+ @if (skipLinkTarget()) {
1657
+ <!-- The first focusable element on the page, invisible until focused. -->
1658
+ <a
1659
+ class="strct-shell__skip"
1660
+ [attr.href]="'#' + skipLinkTarget()"
1661
+ (click)="skipToMain($event)"
1662
+ >{{ skipLinkLabel() }}</a
1663
+ >
1664
+ }
1665
+ <ng-content select="strct-header" />
1666
+ @if (shell.mobileNavOpen()) {
1667
+ <!-- Outside the drawer, which is transformed and would otherwise be this
1668
+ fixed element's containing block (BUG-53-01). -->
1669
+ <div
1670
+ class="strct-vnav__backdrop"
1671
+ tabindex="-1"
1672
+ role="button"
1673
+ [attr.aria-label]="closeNavLabel()"
1674
+ (click)="shell.mobileNavOpen.set(false)"
1675
+ (keydown.enter)="shell.mobileNavOpen.set(false)"
1676
+ (keydown.space)="shell.mobileNavOpen.set(false)"
1677
+ ></div>
1678
+ }
1679
+ <div class="strct-shell__main"><ng-content /></div>
1680
+ <ng-content select="strct-footer" />
1681
+ `, isInline: true, styles: [".strct-shell{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden;background:var(--bg-0)}.strct-shell__main{display:flex;min-height:0;overflow:hidden}.strct-shell__skip{position:absolute;inset-inline-start:0;top:0;z-index:1;transform:translateY(-120%);padding:var(--space-2) var(--space-3);border-radius:0 0 var(--radius-md) 0;background:var(--acc);color:var(--inv);font-size:var(--text-sm);text-decoration:none}.strct-shell__skip:focus-visible{transform:none;outline:2px solid var(--acc50);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1682
+ }
1683
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctShell, decorators: [{
1684
+ type: Component,
1685
+ args: [{ selector: 'strct-shell', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [StrctShellService], template: `
1686
+ @if (skipLinkTarget()) {
1687
+ <!-- The first focusable element on the page, invisible until focused. -->
1688
+ <a
1689
+ class="strct-shell__skip"
1690
+ [attr.href]="'#' + skipLinkTarget()"
1691
+ (click)="skipToMain($event)"
1692
+ >{{ skipLinkLabel() }}</a
1693
+ >
1694
+ }
1695
+ <ng-content select="strct-header" />
1696
+ @if (shell.mobileNavOpen()) {
1697
+ <!-- Outside the drawer, which is transformed and would otherwise be this
1698
+ fixed element's containing block (BUG-53-01). -->
1699
+ <div
1700
+ class="strct-vnav__backdrop"
1701
+ tabindex="-1"
1702
+ role="button"
1703
+ [attr.aria-label]="closeNavLabel()"
1704
+ (click)="shell.mobileNavOpen.set(false)"
1705
+ (keydown.enter)="shell.mobileNavOpen.set(false)"
1706
+ (keydown.space)="shell.mobileNavOpen.set(false)"
1707
+ ></div>
1708
+ }
1709
+ <div class="strct-shell__main"><ng-content /></div>
1710
+ <ng-content select="strct-footer" />
1711
+ `, host: { class: 'strct-shell' }, styles: [".strct-shell{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden;background:var(--bg-0)}.strct-shell__main{display:flex;min-height:0;overflow:hidden}.strct-shell__skip{position:absolute;inset-inline-start:0;top:0;z-index:1;transform:translateY(-120%);padding:var(--space-2) var(--space-3);border-radius:0 0 var(--radius-md) 0;background:var(--acc);color:var(--inv);font-size:var(--text-sm);text-decoration:none}.strct-shell__skip:focus-visible{transform:none;outline:2px solid var(--acc50);outline-offset:2px}\n"] }]
1712
+ }], ctorParameters: () => [], propDecorators: { skipLinkTarget: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkTarget", required: false }] }], skipLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLinkLabel", required: false }] }], closeNavLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeNavLabel", required: false }] }], closeNavOnNavigate: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeNavOnNavigate", required: false }] }] } });
1713
+ /**
1714
+ * An action in the header that knows whether it may move into the overflow
1715
+ * menu on a phone:
1716
+ *
1717
+ * <ng-container *strctHeaderAction="{ label: 'Create', icon: 'plus', run: create }">
1718
+ * <button strct-button variant="flat" iconOnly aria-label="Create">…</button>
1719
+ * </ng-container>
1720
+ *
1721
+ * Content projected into the header without this directive never moves.
1722
+ */
1723
+ class StrctHeaderAction {
1724
+ config = input.required({ ...(ngDevMode ? { debugName: "config" } : /* istanbul ignore next */ {}), alias: 'strctHeaderAction' });
1725
+ template = inject(TemplateRef);
1726
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeaderAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1727
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: StrctHeaderAction, isStandalone: true, selector: "[strctHeaderAction]", inputs: { config: { classPropertyName: "config", publicName: "strctHeaderAction", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1728
+ }
1729
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeaderAction, decorators: [{
1730
+ type: Directive,
1731
+ args: [{ selector: '[strctHeaderAction]' }]
1732
+ }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctHeaderAction", required: true }] }] } });
1733
+ /** Top application bar. Holds brand on the left and actions on the right. */
1734
+ class StrctHeader {
1735
+ /** Accessible label of the mobile drawer toggle. */
1140
1736
  drawerToggleAriaLabel = input('Toggle navigation', /* @ts-ignore */
1141
1737
  ...(ngDevMode ? [{ debugName: "drawerToggleAriaLabel" }] : /* istanbul ignore next */ []));
1738
+ /**
1739
+ * A thin line in a product's colour under the header — a colour token or
1740
+ * any CSS colour. It is drawn inset, so the header keeps its height and
1741
+ * nothing under it moves. Empty leaves today's border alone.
1742
+ */
1743
+ accent = input('', /* @ts-ignore */
1744
+ ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
1745
+ /**
1746
+ * Below this width (px) the actions marked `overflow: 'phone'` move into a
1747
+ * "More" menu. A console header carries more icons than a phone has room
1748
+ * for, and an app should not have to draw two copies of each.
1749
+ */
1750
+ overflowBreakpoint = input(600, /* @ts-ignore */
1751
+ ...(ngDevMode ? [{ debugName: "overflowBreakpoint" }] : /* istanbul ignore next */ []));
1752
+ /** The "More" trigger's accessible name (localisable). */
1753
+ overflowLabel = input('More', /* @ts-ignore */
1754
+ ...(ngDevMode ? [{ debugName: "overflowLabel" }] : /* istanbul ignore next */ []));
1755
+ actions = contentChildren(StrctHeaderAction, /* @ts-ignore */
1756
+ ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
1757
+ /** Whether the viewport is below `overflowBreakpoint`. */
1758
+ narrow = signal(false, /* @ts-ignore */
1759
+ ...(ngDevMode ? [{ debugName: "narrow" }] : /* istanbul ignore next */ []));
1760
+ inlineActions = computed(() => this.actions().filter((a) => !this.narrow() || a.config().overflow === 'never'), /* @ts-ignore */
1761
+ ...(ngDevMode ? [{ debugName: "inlineActions" }] : /* istanbul ignore next */ []));
1762
+ overflowActions = computed(() => this.narrow() ? this.actions().filter((a) => a.config().overflow !== 'never') : [], /* @ts-ignore */
1763
+ ...(ngDevMode ? [{ debugName: "overflowActions" }] : /* istanbul ignore next */ []));
1764
+ /** An action that brings its own control makes the panel a dialog, not a menu. */
1765
+ overflowIsPanel = computed(() => this.overflowActions().some((a) => !a.config().run), /* @ts-ignore */
1766
+ ...(ngDevMode ? [{ debugName: "overflowIsPanel" }] : /* istanbul ignore next */ []));
1767
+ overflowPanel = viewChild('overflow', /* @ts-ignore */
1768
+ ...(ngDevMode ? [{ debugName: "overflowPanel" }] : /* istanbul ignore next */ []));
1769
+ /** In panel mode nothing closes on activation, so a command closes it. */
1770
+ runAndClose(run) {
1771
+ run();
1772
+ this.overflowPanel()?.close(true);
1773
+ }
1142
1774
  shell = inject(StrctShellService);
1775
+ hostEl = inject(ElementRef);
1776
+ constructor() {
1777
+ // Dev-only: the shell clips what overflows the header, so a header that
1778
+ // does not fit a phone fails silently — the triggers are simply not
1779
+ // there. Say so, with the two numbers that make it obvious.
1780
+ afterNextRender(() => {
1781
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
1782
+ const el = this.hostEl.nativeElement;
1783
+ const over = el.scrollWidth - el.clientWidth;
1784
+ if (over > 1) {
1785
+ strctDevWarn('header-overflow', `[strct] strct-header: its content is ${el.scrollWidth}px wide in a ` +
1786
+ `${el.clientWidth}px header, so ${over}px of it is clipped and cannot be ` +
1787
+ 'reached. Mark the triggers that may move with *strctHeaderAction so they ' +
1788
+ 'collect into the header\u2019s "More" menu below overflowBreakpoint.');
1789
+ }
1790
+ }
1791
+ });
1792
+ // The breakpoint has to be read in script, not only in CSS: moving an
1793
+ // action means rendering it somewhere else, which no media query can do.
1794
+ effect((onCleanup) => {
1795
+ const px = this.overflowBreakpoint();
1796
+ if (typeof matchMedia !== 'function')
1797
+ return;
1798
+ const query = matchMedia(`(max-width: ${px}px)`);
1799
+ const update = () => this.narrow.set(query.matches);
1800
+ update();
1801
+ query.addEventListener('change', update);
1802
+ onCleanup(() => query.removeEventListener('change', update));
1803
+ });
1804
+ // The drawer hands focus back here when it closes, so the shell has to
1805
+ // know which element opened it.
1806
+ afterNextRender(() => {
1807
+ this.shell.drawerToggle = this.hostEl.nativeElement.querySelector('.strct-header__drawer-toggle');
1808
+ });
1809
+ }
1143
1810
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
1144
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctHeader, isStandalone: true, selector: "strct-header", inputs: { drawerToggleAriaLabel: { classPropertyName: "drawerToggleAriaLabel", publicName: "drawerToggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-header" }, ngImport: i0, template: `
1811
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctHeader, isStandalone: true, selector: "strct-header", inputs: { drawerToggleAriaLabel: { classPropertyName: "drawerToggleAriaLabel", publicName: "drawerToggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, overflowBreakpoint: { classPropertyName: "overflowBreakpoint", publicName: "overflowBreakpoint", isSignal: true, isRequired: false, transformFunction: null }, overflowLabel: { classPropertyName: "overflowLabel", publicName: "overflowLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-header--accent": "accent()", "style.--strct-header-accent": "accent() || null" }, classAttribute: "strct-header" }, queries: [{ propertyName: "actions", predicate: StrctHeaderAction, isSignal: true }], viewQueries: [{ propertyName: "overflowPanel", first: true, predicate: ["overflow"], descendants: true, isSignal: true }], ngImport: i0, template: `
1145
1812
  <button
1146
1813
  type="button"
1147
1814
  class="strct-header__drawer-toggle"
@@ -1153,11 +1820,74 @@ class StrctHeader {
1153
1820
  <strct-icon name="menu" [size]="18" />
1154
1821
  </button>
1155
1822
  <ng-content />
1156
- `, isInline: true, styles: [".strct-vnav__backdrop{position:fixed;inset:0;z-index:calc(var(--z-nav) - 1);background:var(--backdrop);animation:strct-vnav-backdrop-in .2s ease}@media(min-width:769px){.strct-vnav__backdrop{display:none}}@keyframes strct-vnav-backdrop-in{0%{opacity:0}to{opacity:1}}.strct-header{display:flex;align-items:center;gap:14px;height:56px;padding:0 18px;background:var(--hdr);border-bottom:1px solid var(--b2);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *){color:color-mix(in srgb,var(--hdr-fg) 72%,transparent)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):hover:not(:disabled){background:color-mix(in srgb,var(--hdr-fg) 12%,transparent);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):focus-visible{outline-color:color-mix(in srgb,var(--hdr-fg) 60%,transparent)}.strct-header__drawer-toggle{display:none;align-items:center;justify-content:center;padding:4px;border:0;border-radius:5px;background:transparent;color:color-mix(in srgb,var(--hdr-fg) 85%,transparent);cursor:pointer}.strct-header__drawer-toggle:hover{background:#ffffff1f;color:var(--hdr-fg)}@media(max-width:768px){.strct-header__drawer-toggle{display:inline-flex}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1823
+ @if (actions().length) {
1824
+ <span class="strct-header__actions">
1825
+ @for (action of inlineActions(); track action) {
1826
+ <ng-container *ngTemplateOutlet="action.template" />
1827
+ }
1828
+ @if (overflowActions().length) {
1829
+ <!-- A menu may only hold menu items, so a panel that carries a
1830
+ control of its own (a theme switcher, a segmented choice) is a
1831
+ labelled dialog instead — the dropdown's popover mode. A panel
1832
+ of plain commands stays a menu, with its arrow keys. -->
1833
+ <strct-dropdown
1834
+ #overflow
1835
+ align="end"
1836
+ [popover]="overflowIsPanel()"
1837
+ [focusable]="overflowIsPanel()"
1838
+ [popoverLabel]="overflowLabel()"
1839
+ >
1840
+ <button
1841
+ type="button"
1842
+ class="strct-header__more"
1843
+ strctDropdownTrigger
1844
+ [attr.aria-label]="overflowLabel()"
1845
+ >
1846
+ <strct-icon name="dots" [size]="18" />
1847
+ </button>
1848
+ @for (action of overflowActions(); track action) {
1849
+ @if (action.config().run; as run) {
1850
+ @if (overflowIsPanel()) {
1851
+ <button type="button" class="strct-header__morecmd" (click)="runAndClose(run)">
1852
+ @if (action.config().icon; as icon) {
1853
+ <strct-icon [name]="icon" [size]="15" />
1854
+ }
1855
+ {{ action.config().label }}
1856
+ </button>
1857
+ } @else {
1858
+ <strct-dropdown-item (click)="run()">
1859
+ @if (action.config().icon; as icon) {
1860
+ <strct-icon strctDropdownItemIcon [name]="icon" [size]="15" />
1861
+ }
1862
+ {{ action.config().label }}
1863
+ </strct-dropdown-item>
1864
+ }
1865
+ } @else {
1866
+ <div
1867
+ class="strct-header__moreslot"
1868
+ role="group"
1869
+ [attr.aria-label]="action.config().label"
1870
+ >
1871
+ <ng-container *ngTemplateOutlet="action.template" />
1872
+ </div>
1873
+ }
1874
+ }
1875
+ </strct-dropdown>
1876
+ }
1877
+ </span>
1878
+ }
1879
+ `, isInline: true, styles: [".strct-vnav__backdrop{position:fixed;inset:0;z-index:calc(var(--z-nav) - 1);background:var(--backdrop);animation:strct-vnav-backdrop-in .2s ease}@media(min-width:769px){.strct-vnav__backdrop{display:none}}@keyframes strct-vnav-backdrop-in{0%{opacity:0}to{opacity:1}}.strct-header{display:flex;align-items:center;gap:var(--strct-header-gap, 14px);height:56px;padding:0 var(--strct-header-pad, 18px);background:var(--hdr);border-bottom:1px solid var(--b2);color:var(--hdr-fg)}.strct-header--accent{box-shadow:inset 0 -3px 0 var(--strct-header-accent, var(--acc))}.strct-header__actions{display:flex;align-items:center;gap:inherit;min-width:0}.strct-header__more{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:0;border-radius:var(--radius-sm);background:transparent;color:inherit;cursor:pointer}.strct-header__more:hover{background:#ffffff1f}.strct-header__moreslot{display:flex;align-items:center;padding:6px 10px}.strct-header__morecmd{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;padding:9px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:start;cursor:pointer}.strct-header__morecmd:hover,.strct-header__morecmd:focus-visible{background:var(--bg-3);outline:none}@media(max-width:600px){.strct-header{gap:var(--strct-header-gap, 4px);padding:0 var(--strct-header-pad, 6px)}}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *){color:color-mix(in srgb,var(--hdr-fg) 72%,transparent)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):hover:not(:disabled){background:color-mix(in srgb,var(--hdr-fg) 12%,transparent);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):focus-visible{outline-color:color-mix(in srgb,var(--hdr-fg) 60%,transparent)}.strct-header__drawer-toggle{display:none;align-items:center;justify-content:center;padding:4px;border:0;border-radius:5px;background:transparent;color:color-mix(in srgb,var(--hdr-fg) 85%,transparent);cursor:pointer}.strct-header__drawer-toggle:hover{background:#ffffff1f;color:var(--hdr-fg)}@media(max-width:768px){.strct-header__drawer-toggle{display:inline-flex}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }, { kind: "component", type: StrctDropdown, selector: "strct-dropdown", inputs: ["align", "popover", "popoverLabel", "focusable"] }, { kind: "directive", type: StrctDropdownTrigger, selector: "[strctDropdownTrigger]" }, { kind: "component", type: StrctDropdownItem, selector: "strct-dropdown-item", inputs: ["selected", "critical", "disabled", "hint"] }, { kind: "directive", type: StrctDropdownItemIcon, selector: "[strctDropdownItemIcon]", inputs: ["tone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1157
1880
  }
1158
1881
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctHeader, decorators: [{
1159
1882
  type: Component,
1160
- args: [{ selector: 'strct-header', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1883
+ args: [{ selector: 'strct-header', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
1884
+ NgTemplateOutlet,
1885
+ StrctIcon,
1886
+ StrctDropdown,
1887
+ StrctDropdownTrigger,
1888
+ StrctDropdownItem,
1889
+ StrctDropdownItemIcon,
1890
+ ], template: `
1161
1891
  <button
1162
1892
  type="button"
1163
1893
  class="strct-header__drawer-toggle"
@@ -1169,8 +1899,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1169
1899
  <strct-icon name="menu" [size]="18" />
1170
1900
  </button>
1171
1901
  <ng-content />
1172
- `, host: { class: 'strct-header' }, styles: [".strct-vnav__backdrop{position:fixed;inset:0;z-index:calc(var(--z-nav) - 1);background:var(--backdrop);animation:strct-vnav-backdrop-in .2s ease}@media(min-width:769px){.strct-vnav__backdrop{display:none}}@keyframes strct-vnav-backdrop-in{0%{opacity:0}to{opacity:1}}.strct-header{display:flex;align-items:center;gap:14px;height:56px;padding:0 18px;background:var(--hdr);border-bottom:1px solid var(--b2);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *){color:color-mix(in srgb,var(--hdr-fg) 72%,transparent)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):hover:not(:disabled){background:color-mix(in srgb,var(--hdr-fg) 12%,transparent);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):focus-visible{outline-color:color-mix(in srgb,var(--hdr-fg) 60%,transparent)}.strct-header__drawer-toggle{display:none;align-items:center;justify-content:center;padding:4px;border:0;border-radius:5px;background:transparent;color:color-mix(in srgb,var(--hdr-fg) 85%,transparent);cursor:pointer}.strct-header__drawer-toggle:hover{background:#ffffff1f;color:var(--hdr-fg)}@media(max-width:768px){.strct-header__drawer-toggle{display:inline-flex}}\n"] }]
1173
- }], propDecorators: { drawerToggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawerToggleAriaLabel", required: false }] }] } });
1902
+ @if (actions().length) {
1903
+ <span class="strct-header__actions">
1904
+ @for (action of inlineActions(); track action) {
1905
+ <ng-container *ngTemplateOutlet="action.template" />
1906
+ }
1907
+ @if (overflowActions().length) {
1908
+ <!-- A menu may only hold menu items, so a panel that carries a
1909
+ control of its own (a theme switcher, a segmented choice) is a
1910
+ labelled dialog instead — the dropdown's popover mode. A panel
1911
+ of plain commands stays a menu, with its arrow keys. -->
1912
+ <strct-dropdown
1913
+ #overflow
1914
+ align="end"
1915
+ [popover]="overflowIsPanel()"
1916
+ [focusable]="overflowIsPanel()"
1917
+ [popoverLabel]="overflowLabel()"
1918
+ >
1919
+ <button
1920
+ type="button"
1921
+ class="strct-header__more"
1922
+ strctDropdownTrigger
1923
+ [attr.aria-label]="overflowLabel()"
1924
+ >
1925
+ <strct-icon name="dots" [size]="18" />
1926
+ </button>
1927
+ @for (action of overflowActions(); track action) {
1928
+ @if (action.config().run; as run) {
1929
+ @if (overflowIsPanel()) {
1930
+ <button type="button" class="strct-header__morecmd" (click)="runAndClose(run)">
1931
+ @if (action.config().icon; as icon) {
1932
+ <strct-icon [name]="icon" [size]="15" />
1933
+ }
1934
+ {{ action.config().label }}
1935
+ </button>
1936
+ } @else {
1937
+ <strct-dropdown-item (click)="run()">
1938
+ @if (action.config().icon; as icon) {
1939
+ <strct-icon strctDropdownItemIcon [name]="icon" [size]="15" />
1940
+ }
1941
+ {{ action.config().label }}
1942
+ </strct-dropdown-item>
1943
+ }
1944
+ } @else {
1945
+ <div
1946
+ class="strct-header__moreslot"
1947
+ role="group"
1948
+ [attr.aria-label]="action.config().label"
1949
+ >
1950
+ <ng-container *ngTemplateOutlet="action.template" />
1951
+ </div>
1952
+ }
1953
+ }
1954
+ </strct-dropdown>
1955
+ }
1956
+ </span>
1957
+ }
1958
+ `, host: {
1959
+ class: 'strct-header',
1960
+ '[class.strct-header--accent]': 'accent()',
1961
+ '[style.--strct-header-accent]': 'accent() || null',
1962
+ }, styles: [".strct-vnav__backdrop{position:fixed;inset:0;z-index:calc(var(--z-nav) - 1);background:var(--backdrop);animation:strct-vnav-backdrop-in .2s ease}@media(min-width:769px){.strct-vnav__backdrop{display:none}}@keyframes strct-vnav-backdrop-in{0%{opacity:0}to{opacity:1}}.strct-header{display:flex;align-items:center;gap:var(--strct-header-gap, 14px);height:56px;padding:0 var(--strct-header-pad, 18px);background:var(--hdr);border-bottom:1px solid var(--b2);color:var(--hdr-fg)}.strct-header--accent{box-shadow:inset 0 -3px 0 var(--strct-header-accent, var(--acc))}.strct-header__actions{display:flex;align-items:center;gap:inherit;min-width:0}.strct-header__more{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border:0;border-radius:var(--radius-sm);background:transparent;color:inherit;cursor:pointer}.strct-header__more:hover{background:#ffffff1f}.strct-header__moreslot{display:flex;align-items:center;padding:6px 10px}.strct-header__morecmd{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;padding:9px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:start;cursor:pointer}.strct-header__morecmd:hover,.strct-header__morecmd:focus-visible{background:var(--bg-3);outline:none}@media(max-width:600px){.strct-header{gap:var(--strct-header-gap, 4px);padding:0 var(--strct-header-pad, 6px)}}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *){color:color-mix(in srgb,var(--hdr-fg) 72%,transparent)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):hover:not(:disabled){background:color-mix(in srgb,var(--hdr-fg) 12%,transparent);color:var(--hdr-fg)}.strct-header .strct-btn--flat:not(.strct-modal__dialog *,.strct-drawer *,.strct-popover__panel *,.strct-dd__menu *,.strct-tour__card *):focus-visible{outline-color:color-mix(in srgb,var(--hdr-fg) 60%,transparent)}.strct-header__drawer-toggle{display:none;align-items:center;justify-content:center;padding:4px;border:0;border-radius:5px;background:transparent;color:color-mix(in srgb,var(--hdr-fg) 85%,transparent);cursor:pointer}.strct-header__drawer-toggle:hover{background:#ffffff1f;color:var(--hdr-fg)}@media(max-width:768px){.strct-header__drawer-toggle{display:inline-flex}}\n"] }]
1963
+ }], ctorParameters: () => [], propDecorators: { drawerToggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawerToggleAriaLabel", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], overflowBreakpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "overflowBreakpoint", required: false }] }], overflowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "overflowLabel", required: false }] }], actions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctHeaderAction), { isSignal: true }] }], overflowPanel: [{ type: i0.ViewChild, args: ['overflow', { isSignal: true }] }] } });
1174
1964
  /** Bottom status bar. */
1175
1965
  class StrctFooter {
1176
1966
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctFooter, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -1181,40 +1971,186 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1181
1971
  args: [{ selector: 'strct-footer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `<ng-content />`, host: { class: 'strct-footer' }, styles: [".strct-footer{display:flex;align-items:center;gap:12px;height:32px;padding:0 16px;background:var(--bg-1);border-top:1px solid var(--b2);font-size:12px;color:var(--t2)}\n"] }]
1182
1972
  }] });
1183
1973
 
1974
+ /**
1975
+ * Shared focus-lifecycle helpers for transient overlay surfaces (tooltip,
1976
+ * drawer, menus, popovers …): remember the trigger before opening, move
1977
+ * focus in on open, cycle Tab inside, and hand focus back on close.
1978
+ * Modeled on the modal's local implementation (`modal/modal.ts`).
1979
+ */
1980
+ /** Elements considered tabbable inside an overlay, queried in DOM order. */
1981
+ const FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
1982
+ /** Currently focused element, if it is an HTMLElement (remember the trigger before opening). */
1983
+ function saveFocusedElement() {
1984
+ const active = document.activeElement;
1985
+ return active instanceof HTMLElement ? active : null;
1986
+ }
1987
+ /** Focus `el` again if it is still in the document; null-safe. */
1988
+ function restoreFocus(el) {
1989
+ if (el?.isConnected)
1990
+ el.focus();
1991
+ }
1992
+ /**
1993
+ * Focus the first tabbable descendant of `container`.
1994
+ * Returns whether something was focused; with nothing tabbable the container
1995
+ * itself is NOT focused — the caller decides the fallback.
1996
+ */
1997
+ function focusFirstIn(container) {
1998
+ const first = container.querySelector(FOCUSABLE_SELECTOR);
1999
+ if (!first)
2000
+ return false;
2001
+ first.focus();
2002
+ return true;
2003
+ }
2004
+ /**
2005
+ * Tab trap for modal-ish overlays: cycle Tab/Shift+Tab within the
2006
+ * container's tabbable descendants, wrapping at both ends (preventDefault
2007
+ * when it wraps). Wire it to the container's `keydown.tab` /
2008
+ * `keydown.shift.tab` events.
2009
+ */
2010
+ function keepTabInside(event, container) {
2011
+ const items = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
2012
+ if (!items.length) {
2013
+ event.preventDefault();
2014
+ return;
2015
+ }
2016
+ const first = items[0];
2017
+ const last = items[items.length - 1];
2018
+ const active = container.ownerDocument.activeElement;
2019
+ if (event.shiftKey && (active === first || !container.contains(active))) {
2020
+ event.preventDefault();
2021
+ last.focus();
2022
+ }
2023
+ else if (!event.shiftKey && active === last) {
2024
+ event.preventDefault();
2025
+ first.focus();
2026
+ }
2027
+ }
2028
+
1184
2029
  /** Scrollable left sidebar surface. Holds the icon nav and/or a tree. */
1185
2030
  class StrctVerticalNav {
1186
2031
  shell = inject(StrctShellService);
2032
+ host = inject(ElementRef);
2033
+ /**
2034
+ * How wide the drawer may get on a phone — only in drawer mode, so the
2035
+ * width an app sets for its desktop sidebar is untouched. The default
2036
+ * leaves 48px of page to tap the drawer away.
2037
+ */
2038
+ drawerMaxWidth = input('', /* @ts-ignore */
2039
+ ...(ngDevMode ? [{ debugName: "drawerMaxWidth" }] : /* istanbul ignore next */ []));
2040
+ /**
2041
+ * Draw a close control at the drawer's top. Escape and the backdrop close
2042
+ * it too, but neither is visible, and a touch screen has no Escape.
2043
+ */
2044
+ closeButton = input(false, { ...(ngDevMode ? { debugName: "closeButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2045
+ /** The close control's accessible name (localisable). */
2046
+ closeLabel = input('Close navigation', /* @ts-ignore */
2047
+ ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
2048
+ /**
2049
+ * Keep Tab inside the open drawer and move focus into it, the way a dialog
2050
+ * does. It is only ever on in drawer mode: a sidebar that is part of the
2051
+ * page must not trap anything.
2052
+ */
2053
+ trapFocus = input(true, { ...(ngDevMode ? { debugName: "trapFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2054
+ /** True while the drawer is open AND the viewport is in drawer mode. */
2055
+ trapped() {
2056
+ return this.shell.mobileNavOpen() && this.trapFocus() && this.isDrawer();
2057
+ }
2058
+ constructor() {
2059
+ // Opening the drawer without moving focus leaves the keyboard behind the
2060
+ // page it covers; closing it without handing focus back leaves it nowhere.
2061
+ effect(() => {
2062
+ const open = this.shell.mobileNavOpen();
2063
+ untracked(() => {
2064
+ if (!this.isDrawer())
2065
+ return;
2066
+ if (open) {
2067
+ const el = this.host.nativeElement;
2068
+ // After the open class has painted, or the drawer is still
2069
+ // translated off-screen and focusing it scrolls the page.
2070
+ requestAnimationFrame(() => {
2071
+ if (!focusFirstIn(el)) {
2072
+ el.setAttribute('tabindex', '-1');
2073
+ el.focus();
2074
+ }
2075
+ });
2076
+ }
2077
+ else {
2078
+ restoreFocus(this.shell.drawerToggle);
2079
+ }
2080
+ });
2081
+ });
2082
+ }
1187
2083
  /** Closes the mobile nav on Escape. */
1188
2084
  onEscape() {
1189
2085
  if (this.shell.mobileNavOpen())
1190
- this.shell.mobileNavOpen.set(false);
2086
+ this.shell.closeNav();
2087
+ }
2088
+ onTab(event) {
2089
+ if (this.trapped())
2090
+ keepTabInside(event, this.host.nativeElement);
2091
+ }
2092
+ /**
2093
+ * The drawer breakpoint is the media query the styles use; matchMedia is
2094
+ * the only way the component can read it, and a server render has none.
2095
+ */
2096
+ isDrawer() {
2097
+ return typeof matchMedia === 'function' ? matchMedia('(max-width: 768px)').matches : false;
1191
2098
  }
1192
2099
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctVerticalNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
1193
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.4", type: StrctVerticalNav, isStandalone: true, selector: "strct-vertical-nav", host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "id": "shell.navId", "class.strct-vnav--open": "shell.mobileNavOpen()" }, classAttribute: "strct-vnav" }, ngImport: i0, template: `
2100
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctVerticalNav, isStandalone: true, selector: "strct-vertical-nav", inputs: { drawerMaxWidth: { classPropertyName: "drawerMaxWidth", publicName: "drawerMaxWidth", isSignal: true, isRequired: false, transformFunction: null }, closeButton: { classPropertyName: "closeButton", publicName: "closeButton", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, trapFocus: { classPropertyName: "trapFocus", publicName: "trapFocus", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:keydown.escape": "onEscape()", "keydown.tab": "onTab($event)", "keydown.shift.tab": "onTab($event)" }, properties: { "id": "shell.navId", "class.strct-vnav--open": "shell.mobileNavOpen()", "style.--strct-vnav-drawer-max": "drawerMaxWidth() || null", "attr.aria-modal": "trapped() ? \"true\" : null", "attr.role": "trapped() ? \"dialog\" : null" }, classAttribute: "strct-vnav" }, ngImport: i0, template: `
1194
2101
  <!-- The backdrop is drawn by strct-shell. Here it was a fixed child of a
1195
2102
  transformed element, so its inset: 0 was the drawer's own box and its
1196
2103
  z-index put it above the drawer's content: the page beside the drawer
1197
2104
  was never dimmed, and every tap on the rail or the menu landed on the
1198
2105
  backdrop and closed the drawer. -->
2106
+ @if (closeButton()) {
2107
+ <!-- Only in drawer mode (CSS): on a wide screen the sidebar is just
2108
+ part of the page and has nothing to close. -->
2109
+ <button
2110
+ type="button"
2111
+ class="strct-vnav__close"
2112
+ [attr.aria-label]="closeLabel()"
2113
+ (click)="shell.closeNav()"
2114
+ >
2115
+ <strct-icon name="close" [size]="16" />
2116
+ </button>
2117
+ }
1199
2118
  <ng-content />
1200
- `, isInline: true, styles: [".strct-vnav{display:flex;flex-direction:column;width:264px;flex-shrink:0;background:var(--bg-1);border-inline-end:1px solid var(--b2);overflow:hidden}@media(max-width:768px){.strct-vnav{position:fixed;top:0;inset-inline-start:0;bottom:0;z-index:var(--z-nav);transform:translate(-100%);transition:transform .2s ease}[dir=rtl] .strct-vnav{transform:translate(100%)}.strct-vnav--open,[dir=rtl] .strct-vnav--open{transform:translate(0)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2119
+ `, isInline: true, styles: [".strct-vnav{display:flex;flex-direction:column;width:264px;flex-shrink:0;background:var(--bg-1);border-inline-end:1px solid var(--b2);overflow:hidden}.strct-vnav__close{display:none}@media(max-width:768px){.strct-vnav{position:fixed;top:0;inset-inline-start:0;bottom:0;z-index:var(--z-nav);transform:translate(-100%);transition:transform .2s ease;max-width:var(--strct-vnav-drawer-max, calc(100vw - 48px) )}.strct-vnav__close{display:inline-flex;align-items:center;justify-content:center;align-self:flex-end;flex:none;width:34px;height:34px;margin:8px 8px 0;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--t2);cursor:pointer}.strct-vnav__close:hover{background:var(--bg-3);color:var(--t1)}[dir=rtl] .strct-vnav{transform:translate(100%)}.strct-vnav--open,[dir=rtl] .strct-vnav--open{transform:translate(0)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1201
2120
  }
1202
2121
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctVerticalNav, decorators: [{
1203
2122
  type: Component,
1204
- args: [{ selector: 'strct-vertical-nav', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2123
+ args: [{ selector: 'strct-vertical-nav', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1205
2124
  <!-- The backdrop is drawn by strct-shell. Here it was a fixed child of a
1206
2125
  transformed element, so its inset: 0 was the drawer's own box and its
1207
2126
  z-index put it above the drawer's content: the page beside the drawer
1208
2127
  was never dimmed, and every tap on the rail or the menu landed on the
1209
2128
  backdrop and closed the drawer. -->
2129
+ @if (closeButton()) {
2130
+ <!-- Only in drawer mode (CSS): on a wide screen the sidebar is just
2131
+ part of the page and has nothing to close. -->
2132
+ <button
2133
+ type="button"
2134
+ class="strct-vnav__close"
2135
+ [attr.aria-label]="closeLabel()"
2136
+ (click)="shell.closeNav()"
2137
+ >
2138
+ <strct-icon name="close" [size]="16" />
2139
+ </button>
2140
+ }
1210
2141
  <ng-content />
1211
2142
  `, host: {
1212
2143
  class: 'strct-vnav',
1213
2144
  '[id]': 'shell.navId',
1214
2145
  '[class.strct-vnav--open]': 'shell.mobileNavOpen()',
2146
+ '[style.--strct-vnav-drawer-max]': 'drawerMaxWidth() || null',
2147
+ '[attr.aria-modal]': 'trapped() ? "true" : null',
2148
+ '[attr.role]': 'trapped() ? "dialog" : null',
1215
2149
  '(document:keydown.escape)': 'onEscape()',
1216
- }, styles: [".strct-vnav{display:flex;flex-direction:column;width:264px;flex-shrink:0;background:var(--bg-1);border-inline-end:1px solid var(--b2);overflow:hidden}@media(max-width:768px){.strct-vnav{position:fixed;top:0;inset-inline-start:0;bottom:0;z-index:var(--z-nav);transform:translate(-100%);transition:transform .2s ease}[dir=rtl] .strct-vnav{transform:translate(100%)}.strct-vnav--open,[dir=rtl] .strct-vnav--open{transform:translate(0)}}\n"] }]
1217
- }] });
2150
+ '(keydown.tab)': 'onTab($event)',
2151
+ '(keydown.shift.tab)': 'onTab($event)',
2152
+ }, styles: [".strct-vnav{display:flex;flex-direction:column;width:264px;flex-shrink:0;background:var(--bg-1);border-inline-end:1px solid var(--b2);overflow:hidden}.strct-vnav__close{display:none}@media(max-width:768px){.strct-vnav{position:fixed;top:0;inset-inline-start:0;bottom:0;z-index:var(--z-nav);transform:translate(-100%);transition:transform .2s ease;max-width:var(--strct-vnav-drawer-max, calc(100vw - 48px) )}.strct-vnav__close{display:inline-flex;align-items:center;justify-content:center;align-self:flex-end;flex:none;width:34px;height:34px;margin:8px 8px 0;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--t2);cursor:pointer}.strct-vnav__close:hover{background:var(--bg-3);color:var(--t1)}[dir=rtl] .strct-vnav{transform:translate(100%)}.strct-vnav--open,[dir=rtl] .strct-vnav--open{transform:translate(0)}}\n"] }]
2153
+ }], ctorParameters: () => [], propDecorators: { drawerMaxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawerMaxWidth", required: false }] }], closeButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeButton", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], trapFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "trapFocus", required: false }] }] } });
1218
2154
  /** Horizontal strip of icon tabs (e.g. section switcher at the top of a sidebar). */
1219
2155
  class StrctNav {
1220
2156
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -2199,31 +3135,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
2199
3135
  args: [{ selector: 'strct-button-group', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `<ng-content />`, host: { class: 'strct-btn-group', role: 'group' }, styles: [".strct-btn-group{display:inline-flex}.strct-btn-group>.strct-btn{border-radius:0}.strct-btn-group>.strct-btn:not(:first-child){margin-inline-start:-1px}.strct-btn-group>.strct-btn:first-child{border-start-start-radius:var(--radius-md);border-end-start-radius:var(--radius-md)}.strct-btn-group>.strct-btn:last-child{border-start-end-radius:var(--radius-md);border-end-end-radius:var(--radius-md)}.strct-btn-group>.strct-btn:hover,.strct-btn-group>.strct-btn:focus-visible{position:relative;z-index:var(--z-base)}\n"] }]
2200
3136
  }] });
2201
3137
 
2202
- /**
2203
- * Dev-mode diagnostics for combinations a component accepts but cannot honour.
2204
- *
2205
- * Every message names what was ignored and what to do instead. Keyed: a
2206
- * condition warns once per page load, not once per change detection.
2207
- *
2208
- * Production cost: none, PROVIDED each call site is guarded INLINE with
2209
- * if (typeof ngDevMode !== 'undefined' && ngDevMode) { … }
2210
- * Production builds define `ngDevMode` as false, and the minifier then drops
2211
- * the whole block — messages included. A helper function returning that same
2212
- * expression would NOT be folded across the call, so the check and its
2213
- * strings would ship; that is why no such helper exists here.
2214
- */
2215
- const warned = new Set();
2216
- function strctDevWarn(key, message) {
2217
- if (typeof ngDevMode === 'undefined' || !ngDevMode || warned.has(key))
2218
- return;
2219
- warned.add(key);
2220
- console.warn(message);
2221
- }
2222
- /** Test hook: forget which warnings have fired. Not part of the public API. */
2223
- function resetStrctDevWarnings() {
2224
- warned.clear();
2225
- }
2226
-
2227
3138
  /**
2228
3139
  * Dev-mode diagnostics for attributes a component does not have.
2229
3140
  *
@@ -2323,6 +3234,9 @@ class StrctBadge {
2323
3234
  const el = this.hostEl.nativeElement;
2324
3235
  strctCheckHostInputs(el, 'strct-badge', {
2325
3236
  status: 'neutral, accent, success, warning, critical',
3237
+ // A real input since 5.13.0 — a colour of its own, so it takes any
3238
+ // colour token or value rather than one of a list.
3239
+ tone: 'a colour token or value, e.g. var(--prod-acc)',
2326
3240
  });
2327
3241
  }
2328
3242
  });
@@ -2330,10 +3244,25 @@ class StrctBadge {
2330
3244
  /** Visual status color. */
2331
3245
  status = input('neutral', /* @ts-ignore */
2332
3246
  ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
3247
+ /**
3248
+ * A colour of your own — `tone="var(--prod-acc)"` — for a distinction the
3249
+ * five statuses do not carry, such as which product a row belongs to. The
3250
+ * text takes it and the border a 30% wash of it; `solid` fills with it and
3251
+ * writes in `--strct-badge-on` (`--inv` by default). It overrides `status`.
3252
+ *
3253
+ * The same can be set from a stylesheet with `--strct-badge-fg`,
3254
+ * `--strct-badge-border` and `--strct-badge-on`, which is the better route
3255
+ * when a rule already selects the badges (`.product-badge { … }`).
3256
+ *
3257
+ * The colour is yours to pick, so its contrast is yours to check: the
3258
+ * library's own check covers `--acc` and the status tokens.
3259
+ */
3260
+ tone = input('', /* @ts-ignore */
3261
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
2333
3262
  /** Opt-in filled style instead of the default outlined style. */
2334
3263
  solid = input(false, { ...(ngDevMode ? { debugName: "solid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2335
3264
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
2336
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctBadge, isStandalone: true, selector: "strct-badge", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-badge--accent": "status() === 'accent'", "class.strct-badge--success": "status() === 'success'", "class.strct-badge--warning": "status() === 'warning'", "class.strct-badge--warninging": "status() === 'warning'", "class.strct-badge--critical": "status() === 'critical'", "class.strct-badge--solid": "solid()" }, classAttribute: "strct-badge" }, ngImport: i0, template: `<ng-content />`, isInline: true, styles: [".strct-badge{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;line-height:1;letter-spacing:.2px;padding-block:3px;padding-inline:9px 7px;border-radius:var(--radius-sm);white-space:nowrap;color:var(--t2);background:transparent;border:1px solid var(--b3);border-inline-start-width:3px}.strct-badge--accent{color:var(--acc);border-color:var(--acc30)}.strct-badge--success{color:var(--success);border-color:var(--success)}.strct-badge--warning{color:var(--warning);border-color:var(--warning)}.strct-badge--critical{color:var(--critical);border-color:var(--critical)}.strct-badge--solid{color:var(--t1);background:var(--bg-a);border-color:transparent;border-inline-start-width:1px;padding-inline-start:7px}.strct-badge--solid.strct-badge--accent{background:var(--acc);color:var(--inv)}.strct-badge--solid.strct-badge--success{background:var(--success);color:var(--inv)}.strct-badge--solid.strct-badge--warning{background:var(--warning);color:var(--inv)}.strct-badge--solid.strct-badge--critical{background:var(--critical);color:var(--inv)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3265
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctBadge, isStandalone: true, selector: "strct-badge", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-badge--accent": "status() === 'accent'", "class.strct-badge--success": "status() === 'success'", "class.strct-badge--warning": "status() === 'warning'", "class.strct-badge--warninging": "status() === 'warning'", "class.strct-badge--critical": "status() === 'critical'", "class.strct-badge--solid": "solid()", "class.strct-badge--tone": "tone()", "style.--strct-badge-fg": "tone() || null" }, classAttribute: "strct-badge" }, ngImport: i0, template: `<ng-content />`, isInline: true, styles: [".strct-badge{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;line-height:1;letter-spacing:.2px;padding-block:3px;padding-inline:9px 7px;border-radius:var(--radius-sm);white-space:nowrap;color:var(--t2);background:transparent;border:1px solid var(--b3);border-inline-start-width:3px}.strct-badge--accent{color:var(--acc);border-color:var(--acc30)}.strct-badge--success{color:var(--success);border-color:var(--success)}.strct-badge--warning{color:var(--warning);border-color:var(--warning)}.strct-badge--critical{color:var(--critical);border-color:var(--critical)}.strct-badge--tone{color:var(--strct-badge-fg, var(--t2));border-color:var( --strct-badge-border, color-mix(in srgb, var(--strct-badge-fg, var(--b3)) 30%, transparent) )}.strct-badge--solid{color:var(--t1);background:var(--bg-a);border-color:transparent;border-inline-start-width:1px;padding-inline-start:7px}.strct-badge--solid.strct-badge--accent{background:var(--acc);color:var(--inv)}.strct-badge--solid.strct-badge--success{background:var(--success);color:var(--inv)}.strct-badge--solid.strct-badge--warning{background:var(--warning);color:var(--inv)}.strct-badge--solid.strct-badge--critical{background:var(--critical);color:var(--inv)}.strct-badge--solid.strct-badge--tone{background:var(--strct-badge-fg, var(--bg-a));color:var(--strct-badge-on, var(--inv));border-color:transparent}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2337
3266
  }
2338
3267
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctBadge, decorators: [{
2339
3268
  type: Component,
@@ -2346,8 +3275,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
2346
3275
  '[class.strct-badge--warninging]': "status() === 'warning'",
2347
3276
  '[class.strct-badge--critical]': "status() === 'critical'",
2348
3277
  '[class.strct-badge--solid]': 'solid()',
2349
- }, styles: [".strct-badge{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;line-height:1;letter-spacing:.2px;padding-block:3px;padding-inline:9px 7px;border-radius:var(--radius-sm);white-space:nowrap;color:var(--t2);background:transparent;border:1px solid var(--b3);border-inline-start-width:3px}.strct-badge--accent{color:var(--acc);border-color:var(--acc30)}.strct-badge--success{color:var(--success);border-color:var(--success)}.strct-badge--warning{color:var(--warning);border-color:var(--warning)}.strct-badge--critical{color:var(--critical);border-color:var(--critical)}.strct-badge--solid{color:var(--t1);background:var(--bg-a);border-color:transparent;border-inline-start-width:1px;padding-inline-start:7px}.strct-badge--solid.strct-badge--accent{background:var(--acc);color:var(--inv)}.strct-badge--solid.strct-badge--success{background:var(--success);color:var(--inv)}.strct-badge--solid.strct-badge--warning{background:var(--warning);color:var(--inv)}.strct-badge--solid.strct-badge--critical{background:var(--critical);color:var(--inv)}\n"] }]
2350
- }], ctorParameters: () => [], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }] } });
3278
+ '[class.strct-badge--tone]': 'tone()',
3279
+ '[style.--strct-badge-fg]': 'tone() || null',
3280
+ }, styles: [".strct-badge{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;line-height:1;letter-spacing:.2px;padding-block:3px;padding-inline:9px 7px;border-radius:var(--radius-sm);white-space:nowrap;color:var(--t2);background:transparent;border:1px solid var(--b3);border-inline-start-width:3px}.strct-badge--accent{color:var(--acc);border-color:var(--acc30)}.strct-badge--success{color:var(--success);border-color:var(--success)}.strct-badge--warning{color:var(--warning);border-color:var(--warning)}.strct-badge--critical{color:var(--critical);border-color:var(--critical)}.strct-badge--tone{color:var(--strct-badge-fg, var(--t2));border-color:var( --strct-badge-border, color-mix(in srgb, var(--strct-badge-fg, var(--b3)) 30%, transparent) )}.strct-badge--solid{color:var(--t1);background:var(--bg-a);border-color:transparent;border-inline-start-width:1px;padding-inline-start:7px}.strct-badge--solid.strct-badge--accent{background:var(--acc);color:var(--inv)}.strct-badge--solid.strct-badge--success{background:var(--success);color:var(--inv)}.strct-badge--solid.strct-badge--warning{background:var(--warning);color:var(--inv)}.strct-badge--solid.strct-badge--critical{background:var(--critical);color:var(--inv)}.strct-badge--solid.strct-badge--tone{background:var(--strct-badge-fg, var(--bg-a));color:var(--strct-badge-on, var(--inv));border-color:transparent}\n"] }]
3281
+ }], ctorParameters: () => [], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }] } });
2351
3282
 
2352
3283
  /** Content rendered before the tag's text — a status dot, an icon. */
2353
3284
  class StrctTagLeading {
@@ -2831,115 +3762,60 @@ class StrctSpinner {
2831
3762
  * else on screen.
2832
3763
  */
2833
3764
  caption = input('', /* @ts-ignore */
2834
- ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
2835
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSpinner, deps: [], target: i0.ɵɵFactoryTarget.Component });
2836
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctSpinner, isStandalone: true, selector: "strct-spinner", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar" }, properties: { "class.strct-spinner--captioned": "caption()", "attr.aria-label": "caption() || label()", "class.strct-spinner--sm": "size() === 'sm'", "class.strct-spinner--lg": "size() === 'lg'" }, classAttribute: "strct-spinner" }, ngImport: i0, template: `
2837
- @if (caption()) {
2838
- <span class="strct-spinner__ring"></span>
2839
- <span class="strct-spinner__caption">{{ caption() }}</span>
2840
- }
2841
- `, isInline: true, styles: [".strct-spinner{display:inline-block;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm{width:14px;height:14px;border-width:2px}.strct-spinner--lg{width:34px;height:34px;border-width:3px}@keyframes strct-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.strct-spinner{animation-duration:1.6s}}.strct-spinner--captioned{display:inline-flex;align-items:center;gap:var(--space-2);width:auto;height:auto;border:0;border-radius:0;animation:none}.strct-spinner__ring{flex:none;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm .strct-spinner__ring{width:14px;height:14px;border-width:2px}.strct-spinner--lg .strct-spinner__ring{width:34px;height:34px;border-width:3px}@media(prefers-reduced-motion:reduce){.strct-spinner__ring{animation-duration:1.6s}}.strct-spinner__caption{font-size:var(--text-sm);color:var(--t3);line-height:1.3}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2842
- }
2843
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSpinner, decorators: [{
2844
- type: Component,
2845
- args: [{ selector: 'strct-spinner', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2846
- @if (caption()) {
2847
- <span class="strct-spinner__ring"></span>
2848
- <span class="strct-spinner__caption">{{ caption() }}</span>
2849
- }
2850
- `, host: {
2851
- class: 'strct-spinner',
2852
- role: 'progressbar',
2853
- '[class.strct-spinner--captioned]': 'caption()',
2854
- '[attr.aria-label]': 'caption() || label()',
2855
- '[class.strct-spinner--sm]': "size() === 'sm'",
2856
- '[class.strct-spinner--lg]': "size() === 'lg'",
2857
- }, styles: [".strct-spinner{display:inline-block;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm{width:14px;height:14px;border-width:2px}.strct-spinner--lg{width:34px;height:34px;border-width:3px}@keyframes strct-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.strct-spinner{animation-duration:1.6s}}.strct-spinner--captioned{display:inline-flex;align-items:center;gap:var(--space-2);width:auto;height:auto;border:0;border-radius:0;animation:none}.strct-spinner__ring{flex:none;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm .strct-spinner__ring{width:14px;height:14px;border-width:2px}.strct-spinner--lg .strct-spinner__ring{width:34px;height:34px;border-width:3px}@media(prefers-reduced-motion:reduce){.strct-spinner__ring{animation-duration:1.6s}}.strct-spinner__caption{font-size:var(--text-sm);color:var(--t3);line-height:1.3}\n"] }]
2858
- }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }] } });
2859
-
2860
- /**
2861
- * Shimmering loading placeholder.
2862
- * <strct-skeleton width="60%" height="14px" />
2863
- * <strct-skeleton circle width="40px" height="40px" />
2864
- */
2865
- class StrctSkeleton {
2866
- /** Width (CSS length). */
2867
- width = input('100%', /* @ts-ignore */
2868
- ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2869
- /** Height in pixels. */
2870
- height = input('14px', /* @ts-ignore */
2871
- ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
2872
- /** Circle. */
2873
- circle = input(false, { ...(ngDevMode ? { debugName: "circle" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2874
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
2875
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctSkeleton, isStandalone: true, selector: "strct-skeleton", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, circle: { classPropertyName: "circle", publicName: "circle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation", "aria-hidden": "true" }, properties: { "class.strct-skel--circle": "circle()", "style.width": "width()", "style.height": "height()" }, classAttribute: "strct-skel" }, ngImport: i0, template: '', isInline: true, styles: [".strct-skel{display:block;border-radius:5px;background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:400% 100%;animation:strct-skel-shimmer 1.4s ease infinite}.strct-skel--circle{border-radius:50%}@keyframes strct-skel-shimmer{0%{background-position:100% 50%}to{background-position:0 50%}}@media(prefers-reduced-motion:reduce){.strct-skel{animation:none;background:var(--skeleton-hi, var(--acc18))}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2876
- }
2877
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSkeleton, decorators: [{
2878
- type: Component,
2879
- args: [{ selector: 'strct-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', host: {
2880
- class: 'strct-skel',
2881
- role: 'presentation',
2882
- 'aria-hidden': 'true',
2883
- '[class.strct-skel--circle]': 'circle()',
2884
- '[style.width]': 'width()',
2885
- '[style.height]': 'height()',
2886
- }, styles: [".strct-skel{display:block;border-radius:5px;background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:400% 100%;animation:strct-skel-shimmer 1.4s ease infinite}.strct-skel--circle{border-radius:50%}@keyframes strct-skel-shimmer{0%{background-position:100% 50%}to{background-position:0 50%}}@media(prefers-reduced-motion:reduce){.strct-skel{animation:none;background:var(--skeleton-hi, var(--acc18))}}\n"] }]
2887
- }], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], circle: [{ type: i0.Input, args: [{ isSignal: true, alias: "circle", required: false }] }] } });
2888
-
2889
- /**
2890
- * Shared focus-lifecycle helpers for transient overlay surfaces (tooltip,
2891
- * drawer, menus, popovers …): remember the trigger before opening, move
2892
- * focus in on open, cycle Tab inside, and hand focus back on close.
2893
- * Modeled on the modal's local implementation (`modal/modal.ts`).
2894
- */
2895
- /** Elements considered tabbable inside an overlay, queried in DOM order. */
2896
- const FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
2897
- /** Currently focused element, if it is an HTMLElement (remember the trigger before opening). */
2898
- function saveFocusedElement() {
2899
- const active = document.activeElement;
2900
- return active instanceof HTMLElement ? active : null;
2901
- }
2902
- /** Focus `el` again if it is still in the document; null-safe. */
2903
- function restoreFocus(el) {
2904
- if (el?.isConnected)
2905
- el.focus();
2906
- }
2907
- /**
2908
- * Focus the first tabbable descendant of `container`.
2909
- * Returns whether something was focused; with nothing tabbable the container
2910
- * itself is NOT focused — the caller decides the fallback.
2911
- */
2912
- function focusFirstIn(container) {
2913
- const first = container.querySelector(FOCUSABLE_SELECTOR);
2914
- if (!first)
2915
- return false;
2916
- first.focus();
2917
- return true;
2918
- }
2919
- /**
2920
- * Tab trap for modal-ish overlays: cycle Tab/Shift+Tab within the
2921
- * container's tabbable descendants, wrapping at both ends (preventDefault
2922
- * when it wraps). Wire it to the container's `keydown.tab` /
2923
- * `keydown.shift.tab` events.
2924
- */
2925
- function keepTabInside(event, container) {
2926
- const items = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
2927
- if (!items.length) {
2928
- event.preventDefault();
2929
- return;
2930
- }
2931
- const first = items[0];
2932
- const last = items[items.length - 1];
2933
- const active = container.ownerDocument.activeElement;
2934
- if (event.shiftKey && (active === first || !container.contains(active))) {
2935
- event.preventDefault();
2936
- last.focus();
3765
+ ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
3766
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSpinner, deps: [], target: i0.ɵɵFactoryTarget.Component });
3767
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctSpinner, isStandalone: true, selector: "strct-spinner", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar" }, properties: { "class.strct-spinner--captioned": "caption()", "attr.aria-label": "caption() || label()", "class.strct-spinner--sm": "size() === 'sm'", "class.strct-spinner--lg": "size() === 'lg'" }, classAttribute: "strct-spinner" }, ngImport: i0, template: `
3768
+ @if (caption()) {
3769
+ <span class="strct-spinner__ring"></span>
3770
+ <span class="strct-spinner__caption">{{ caption() }}</span>
2937
3771
  }
2938
- else if (!event.shiftKey && active === last) {
2939
- event.preventDefault();
2940
- first.focus();
3772
+ `, isInline: true, styles: [".strct-spinner{display:inline-block;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm{width:14px;height:14px;border-width:2px}.strct-spinner--lg{width:34px;height:34px;border-width:3px}@keyframes strct-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.strct-spinner{animation-duration:1.6s}}.strct-spinner--captioned{display:inline-flex;align-items:center;gap:var(--space-2);width:auto;height:auto;border:0;border-radius:0;animation:none}.strct-spinner__ring{flex:none;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm .strct-spinner__ring{width:14px;height:14px;border-width:2px}.strct-spinner--lg .strct-spinner__ring{width:34px;height:34px;border-width:3px}@media(prefers-reduced-motion:reduce){.strct-spinner__ring{animation-duration:1.6s}}.strct-spinner__caption{font-size:var(--text-sm);color:var(--t3);line-height:1.3}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3773
+ }
3774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSpinner, decorators: [{
3775
+ type: Component,
3776
+ args: [{ selector: 'strct-spinner', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
3777
+ @if (caption()) {
3778
+ <span class="strct-spinner__ring"></span>
3779
+ <span class="strct-spinner__caption">{{ caption() }}</span>
2941
3780
  }
3781
+ `, host: {
3782
+ class: 'strct-spinner',
3783
+ role: 'progressbar',
3784
+ '[class.strct-spinner--captioned]': 'caption()',
3785
+ '[attr.aria-label]': 'caption() || label()',
3786
+ '[class.strct-spinner--sm]': "size() === 'sm'",
3787
+ '[class.strct-spinner--lg]': "size() === 'lg'",
3788
+ }, styles: [".strct-spinner{display:inline-block;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm{width:14px;height:14px;border-width:2px}.strct-spinner--lg{width:34px;height:34px;border-width:3px}@keyframes strct-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.strct-spinner{animation-duration:1.6s}}.strct-spinner--captioned{display:inline-flex;align-items:center;gap:var(--space-2);width:auto;height:auto;border:0;border-radius:0;animation:none}.strct-spinner__ring{flex:none;width:22px;height:22px;border:2.5px solid var(--b3);border-top-color:var(--acc);border-radius:50%;animation:strct-spin .7s linear infinite}.strct-spinner--sm .strct-spinner__ring{width:14px;height:14px;border-width:2px}.strct-spinner--lg .strct-spinner__ring{width:34px;height:34px;border-width:3px}@media(prefers-reduced-motion:reduce){.strct-spinner__ring{animation-duration:1.6s}}.strct-spinner__caption{font-size:var(--text-sm);color:var(--t3);line-height:1.3}\n"] }]
3789
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }] } });
3790
+
3791
+ /**
3792
+ * Shimmering loading placeholder.
3793
+ * <strct-skeleton width="60%" height="14px" />
3794
+ * <strct-skeleton circle width="40px" height="40px" />
3795
+ */
3796
+ class StrctSkeleton {
3797
+ /** Width (CSS length). */
3798
+ width = input('100%', /* @ts-ignore */
3799
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
3800
+ /** Height in pixels. */
3801
+ height = input('14px', /* @ts-ignore */
3802
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3803
+ /** Circle. */
3804
+ circle = input(false, { ...(ngDevMode ? { debugName: "circle" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3805
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
3806
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: StrctSkeleton, isStandalone: true, selector: "strct-skeleton", inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, circle: { classPropertyName: "circle", publicName: "circle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation", "aria-hidden": "true" }, properties: { "class.strct-skel--circle": "circle()", "style.width": "width()", "style.height": "height()" }, classAttribute: "strct-skel" }, ngImport: i0, template: '', isInline: true, styles: [".strct-skel{display:block;border-radius:5px;background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:400% 100%;animation:strct-skel-shimmer 1.4s ease infinite}.strct-skel--circle{border-radius:50%}@keyframes strct-skel-shimmer{0%{background-position:100% 50%}to{background-position:0 50%}}@media(prefers-reduced-motion:reduce){.strct-skel{animation:none;background:var(--skeleton-hi, var(--acc18))}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2942
3807
  }
3808
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSkeleton, decorators: [{
3809
+ type: Component,
3810
+ args: [{ selector: 'strct-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', host: {
3811
+ class: 'strct-skel',
3812
+ role: 'presentation',
3813
+ 'aria-hidden': 'true',
3814
+ '[class.strct-skel--circle]': 'circle()',
3815
+ '[style.width]': 'width()',
3816
+ '[style.height]': 'height()',
3817
+ }, styles: [".strct-skel{display:block;border-radius:5px;background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:400% 100%;animation:strct-skel-shimmer 1.4s ease infinite}.strct-skel--circle{border-radius:50%}@keyframes strct-skel-shimmer{0%{background-position:100% 50%}to{background-position:0 50%}}@media(prefers-reduced-motion:reduce){.strct-skel{animation:none;background:var(--skeleton-hi, var(--acc18))}}\n"] }]
3818
+ }], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], circle: [{ type: i0.Input, args: [{ isSignal: true, alias: "circle", required: false }] }] } });
2943
3819
 
2944
3820
  /**
2945
3821
  * Floating action button that fans out to reveal actions. Project icon buttons
@@ -5348,637 +6224,222 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
5348
6224
  >
5349
6225
  <strct-icon name="close" [size]="14" />
5350
6226
  </button>
5351
- </div>
5352
- }
5353
- <div class="strct-modal__body">
5354
- <ng-content />
5355
- @if (lazyContent(); as lazy) {
5356
- <ng-container [ngTemplateOutlet]="lazy.template" />
5357
- }
5358
- </div>
5359
- @if (!hideFooter() && !chromeless()) {
5360
- <div class="strct-modal__foot"><ng-content select="[strctModalFooter]" /></div>
5361
- }
5362
- </div>
5363
- </div>
5364
- }
5365
- `, host: {
5366
- '(document:keydown.escape)': 'onEscape($event)',
5367
- }, styles: [".strct-modal__overlay{position:fixed;inset:0;z-index:var(--z-modal);display:flex;align-items:center;justify-content:center;padding:var(--space-5);background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-modal-fade .12s ease}@media(max-width:768px){.strct-modal__overlay{padding:var(--space-3)}}.strct-modal__dialog{width:100%;max-height:calc(100vh - 48px);display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-elevated);overflow:hidden;animation:strct-modal-rise .14s ease}.strct-modal__dialog--sm{max-width:min(480px,calc(100vw - 32px))}.strct-modal__dialog--md{max-width:min(640px,calc(100vw - 32px))}.strct-modal__dialog--lg{max-width:min(860px,calc(100vw - 32px))}.strct-modal__dialog--xl{max-width:min(1080px,calc(100vw - 32px))}.strct-modal__head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--b1)}.strct-modal__head--drag{cursor:move;-webkit-user-select:none;user-select:none;touch-action:none}.strct-modal__overlay--glass{background:#00000061;-webkit-backdrop-filter:blur(10px) saturate(1.25);backdrop-filter:blur(10px) saturate(1.25)}.strct-modal__dialog--glass{background:color-mix(in srgb,var(--bg-1) 72%,transparent);-webkit-backdrop-filter:blur(24px) saturate(1.4);backdrop-filter:blur(24px) saturate(1.4)}.strct-modal__dialog--glass .strct-modal__foot{background:color-mix(in srgb,var(--bg-2) 55%,transparent)}.strct-modal__title{font-size:14px;font-weight:600;color:var(--t1)}.strct-modal__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-modal__close:hover{color:var(--t1);background:var(--bg-3)}.strct-modal__body{padding:var(--space-4);overflow-y:auto;color:var(--t2);font-size:13px}.strct-modal__dialog--chromeless{width:calc(232px + var(--strct-wiz-content-min, 864px) + 2px);max-width:calc(100vw - 32px)}.strct-modal__dialog--chromeless:has(.strct-wiz__layout--aside){width:calc(232px + var(--strct-wiz-content-min, 864px) + 280px + 2px)}.strct-modal__dialog--chromeless .strct-modal__body{padding:0;overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0}.strct-modal__dialog--chromeless .strct-modal__body>*{flex:1;min-height:0}.strct-modal__foot{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--b1);background:var(--bg-2)}@keyframes strct-modal-fade{0%{opacity:0}}@keyframes strct-modal-rise{0%{opacity:0;transform:translateY(8px) scale(.98)}}@media(prefers-reduced-motion:reduce){.strct-modal__overlay,.strct-modal__dialog{animation:none}}\n"] }]
5368
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], hideFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideFooter", required: false }] }], chromeless: [{ type: i0.Input, args: [{ isSignal: true, alias: "chromeless", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], closeOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEscape", required: false }] }], closeOnBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdrop", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], backdropClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdropClass", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], lazyContent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctModalContent), { isSignal: true }] }] } });
5369
-
5370
- /** Marks the drawer's footer action area: `<ng-container strctDrawerFooter>…`. */
5371
- class StrctDrawerFooter {
5372
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawerFooter, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5373
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDrawerFooter, isStandalone: true, selector: "[strctDrawerFooter]", ngImport: i0 });
5374
- }
5375
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawerFooter, decorators: [{
5376
- type: Directive,
5377
- args: [{ selector: '[strctDrawerFooter]' }]
5378
- }] });
5379
- /**
5380
- * Slide-out overlay panel anchored to an edge of the viewport — for inspecting
5381
- * or editing a record without losing the underlying table's scroll/selection.
5382
- *
5383
- * <strct-drawer [(open)]="editing" side="end" title="Virtual machine">
5384
- * … body …
5385
- * <ng-container strctDrawerFooter>
5386
- * <button strct-button (click)="editing.set(false)">Cancel</button>
5387
- * </ng-container>
5388
- * </strct-drawer>
5389
- */
5390
- class StrctDrawer {
5391
- elementRef = inject((ElementRef));
5392
- doc = inject(DOCUMENT);
5393
- /** Open state (two-way). */
5394
- open = model(false, /* @ts-ignore */
5395
- ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
5396
- /** Edge to anchor to. Defaults to `end` (right in LTR). */
5397
- side = input('end', /* @ts-ignore */
5398
- ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
5399
- /** Extent along the sliding axis. */
5400
- size = input('md', /* @ts-ignore */
5401
- ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
5402
- /** Header title. */
5403
- title = input('', /* @ts-ignore */
5404
- ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
5405
- /** Show the close button and allow backdrop/Esc dismissal. */
5406
- dismissable = input(true, { ...(ngDevMode ? { debugName: "dismissable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
5407
- /** Accessible name of the dialog when no `title` is set (localizable). */
5408
- ariaLabel = input('Panel', /* @ts-ignore */
5409
- ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
5410
- /** Accessible label of the X close button (localizable). */
5411
- closeLabel = input('Close', /* @ts-ignore */
5412
- ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
5413
- footerDef = contentChild(StrctDrawerFooter, /* @ts-ignore */
5414
- ...(ngDevMode ? [{ debugName: "footerDef" }] : /* istanbul ignore next */ []));
5415
- hasFooter = computed(() => !!this.footerDef(), /* @ts-ignore */
5416
- ...(ngDevMode ? [{ debugName: "hasFooter" }] : /* istanbul ignore next */ []));
5417
- /** Element that had focus before the drawer opened, restored on close. */
5418
- previousActive = null;
5419
- /** Whether this instance currently holds a scroll lock. */
5420
- locked = false;
5421
- constructor() {
5422
- effect(() => {
5423
- const open = this.open();
5424
- if (open && !this.locked) {
5425
- this.locked = true;
5426
- lockBodyScroll(this.doc);
5427
- this.previousActive = saveFocusedElement();
5428
- // Move focus into the panel once it has rendered.
5429
- setTimeout(() => this.focusInitial());
5430
- }
5431
- else if (!open && this.locked) {
5432
- this.locked = false;
5433
- unlockBodyScroll(this.doc);
5434
- restoreFocus(this.previousActive);
5435
- this.previousActive = null;
5436
- }
5437
- });
5438
- // Release the lock if the drawer is destroyed while still open.
5439
- inject(DestroyRef).onDestroy(() => {
5440
- if (this.locked) {
5441
- this.locked = false;
5442
- unlockBodyScroll(this.doc);
5443
- }
5444
- });
5445
- }
5446
- onBackdrop() {
5447
- if (this.dismissable())
5448
- this.close();
5449
- }
5450
- onEscape(event) {
5451
- if (!this.open() || !this.dismissable())
5452
- return;
5453
- // A host modal/drawer must not also close on the same keypress.
5454
- event.stopPropagation();
5455
- this.close();
5456
- }
5457
- /** Wrap Tab focus within the panel. */
5458
- onTab(event) {
5459
- const panel = this.panel();
5460
- if (panel)
5461
- keepTabInside(event, panel);
5462
- }
5463
- close() {
5464
- this.open.set(false);
5465
- }
5466
- panel() {
5467
- return this.elementRef.nativeElement.querySelector('.strct-drawer');
5468
- }
5469
- focusInitial() {
5470
- const panel = this.panel();
5471
- if (!panel)
5472
- return;
5473
- // Nothing tabbable (empty body, dismissable=false) → focus the panel itself.
5474
- if (!focusFirstIn(panel))
5475
- panel.focus();
5476
- }
5477
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
5478
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDrawer, isStandalone: true, selector: "strct-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, dismissable: { classPropertyName: "dismissable", publicName: "dismissable", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" }, classAttribute: "strct-drawer-host" }, queries: [{ propertyName: "footerDef", first: true, predicate: StrctDrawerFooter, descendants: true, isSignal: true }], ngImport: i0, template: `
5479
- @if (open()) {
5480
- <!-- Backdrop: pointer-only dismiss target (click-through when not
5481
- dismissable). Keyboard users dismiss via Escape. -->
5482
- <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
5483
- <div
5484
- class="strct-drawer__backdrop"
5485
- [class.strct-drawer__backdrop--bare]="!dismissable()"
5486
- (click)="onBackdrop()"
5487
- ></div>
5488
- <aside
5489
- class="strct-drawer strct-drawer--{{ side() }} strct-drawer--{{ size() }}"
5490
- role="dialog"
5491
- aria-modal="true"
5492
- tabindex="-1"
5493
- [attr.aria-label]="title() || ariaLabel()"
5494
- (keydown.tab)="onTab($event)"
5495
- (keydown.shift.tab)="onTab($event)"
5496
- >
5497
- @if (title() || dismissable()) {
5498
- <header class="strct-drawer__head">
5499
- <span class="strct-drawer__title">{{ title() }}</span>
5500
- @if (dismissable()) {
5501
- <button
5502
- type="button"
5503
- class="strct-drawer__close"
5504
- [attr.aria-label]="closeLabel()"
5505
- (click)="close()"
5506
- >
5507
- <strct-icon name="close" [size]="15" />
5508
- </button>
5509
- }
5510
- </header>
5511
- }
5512
- <div class="strct-drawer__body"><ng-content /></div>
5513
- @if (hasFooter()) {
5514
- <footer class="strct-drawer__foot"><ng-content select="[strctDrawerFooter]" /></footer>
5515
- }
5516
- </aside>
5517
- }
5518
- `, isInline: true, styles: [".strct-drawer__backdrop{position:fixed;inset:0;z-index:var(--z-overlay);background:var(--backdrop);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);animation:strct-drawer-fade .16s ease}.strct-drawer__backdrop--bare{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;pointer-events:none}.strct-drawer{position:fixed;z-index:calc(var(--z-overlay) + 1);display:flex;flex-direction:column;background:var(--bg-1);box-shadow:var(--shh)}.strct-drawer--start,.strct-drawer--end{top:0;bottom:0;border-radius:0}.strct-drawer--start{inset-inline-start:0;border-inline-end:1px solid var(--b2);animation:strct-drawer-in-start .18s ease}.strct-drawer--end{inset-inline-end:0;border-inline-start:1px solid var(--b2);animation:strct-drawer-in-end .18s ease}[dir=rtl] .strct-drawer--start{animation-name:strct-drawer-in-end}[dir=rtl] .strct-drawer--end{animation-name:strct-drawer-in-start}.strct-drawer--top,.strct-drawer--bottom{left:0;right:0}.strct-drawer--top{top:0;border-bottom:1px solid var(--b2);animation:strct-drawer-in-top .18s ease}.strct-drawer--bottom{bottom:0;border-top:1px solid var(--b2);animation:strct-drawer-in-bottom .18s ease}.strct-drawer--start.strct-drawer--sm,.strct-drawer--end.strct-drawer--sm{width:300px}.strct-drawer--start.strct-drawer--md,.strct-drawer--end.strct-drawer--md{width:420px}.strct-drawer--start.strct-drawer--lg,.strct-drawer--end.strct-drawer--lg{width:600px}.strct-drawer--top.strct-drawer--sm,.strct-drawer--bottom.strct-drawer--sm{height:220px}.strct-drawer--top.strct-drawer--md,.strct-drawer--bottom.strct-drawer--md{height:340px}.strct-drawer--top.strct-drawer--lg,.strct-drawer--bottom.strct-drawer--lg{height:480px}.strct-drawer{max-width:100vw;max-height:100vh}.strct-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--b1);flex-shrink:0}.strct-drawer__title{font-size:15px;font-weight:600;color:var(--t1)}.strct-drawer__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-drawer__close:hover{color:var(--t1);background:var(--bg-3)}.strct-drawer__body{flex:1;min-height:0;overflow-y:auto;padding:16px 18px;font-size:13px;color:var(--t1)}.strct-drawer__foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--b1);flex-shrink:0}@keyframes strct-drawer-fade{0%{opacity:0}}@keyframes strct-drawer-in-start{0%{transform:translate(-100%)}}@keyframes strct-drawer-in-end{0%{transform:translate(100%)}}@keyframes strct-drawer-in-top{0%{transform:translateY(-100%)}}@keyframes strct-drawer-in-bottom{0%{transform:translateY(100%)}}@media(prefers-reduced-motion:reduce){.strct-drawer__backdrop,.strct-drawer{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5519
- }
5520
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawer, decorators: [{
5521
- type: Component,
5522
- args: [{ selector: 'strct-drawer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
5523
- @if (open()) {
5524
- <!-- Backdrop: pointer-only dismiss target (click-through when not
5525
- dismissable). Keyboard users dismiss via Escape. -->
5526
- <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
5527
- <div
5528
- class="strct-drawer__backdrop"
5529
- [class.strct-drawer__backdrop--bare]="!dismissable()"
5530
- (click)="onBackdrop()"
5531
- ></div>
5532
- <aside
5533
- class="strct-drawer strct-drawer--{{ side() }} strct-drawer--{{ size() }}"
5534
- role="dialog"
5535
- aria-modal="true"
5536
- tabindex="-1"
5537
- [attr.aria-label]="title() || ariaLabel()"
5538
- (keydown.tab)="onTab($event)"
5539
- (keydown.shift.tab)="onTab($event)"
5540
- >
5541
- @if (title() || dismissable()) {
5542
- <header class="strct-drawer__head">
5543
- <span class="strct-drawer__title">{{ title() }}</span>
5544
- @if (dismissable()) {
5545
- <button
5546
- type="button"
5547
- class="strct-drawer__close"
5548
- [attr.aria-label]="closeLabel()"
5549
- (click)="close()"
5550
- >
5551
- <strct-icon name="close" [size]="15" />
5552
- </button>
6227
+ </div>
6228
+ }
6229
+ <div class="strct-modal__body">
6230
+ <ng-content />
6231
+ @if (lazyContent(); as lazy) {
6232
+ <ng-container [ngTemplateOutlet]="lazy.template" />
5553
6233
  }
5554
- </header>
5555
- }
5556
- <div class="strct-drawer__body"><ng-content /></div>
5557
- @if (hasFooter()) {
5558
- <footer class="strct-drawer__foot"><ng-content select="[strctDrawerFooter]" /></footer>
5559
- }
5560
- </aside>
6234
+ </div>
6235
+ @if (!hideFooter() && !chromeless()) {
6236
+ <div class="strct-modal__foot"><ng-content select="[strctModalFooter]" /></div>
6237
+ }
6238
+ </div>
6239
+ </div>
5561
6240
  }
5562
6241
  `, host: {
5563
- class: 'strct-drawer-host',
5564
6242
  '(document:keydown.escape)': 'onEscape($event)',
5565
- }, styles: [".strct-drawer__backdrop{position:fixed;inset:0;z-index:var(--z-overlay);background:var(--backdrop);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);animation:strct-drawer-fade .16s ease}.strct-drawer__backdrop--bare{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;pointer-events:none}.strct-drawer{position:fixed;z-index:calc(var(--z-overlay) + 1);display:flex;flex-direction:column;background:var(--bg-1);box-shadow:var(--shh)}.strct-drawer--start,.strct-drawer--end{top:0;bottom:0;border-radius:0}.strct-drawer--start{inset-inline-start:0;border-inline-end:1px solid var(--b2);animation:strct-drawer-in-start .18s ease}.strct-drawer--end{inset-inline-end:0;border-inline-start:1px solid var(--b2);animation:strct-drawer-in-end .18s ease}[dir=rtl] .strct-drawer--start{animation-name:strct-drawer-in-end}[dir=rtl] .strct-drawer--end{animation-name:strct-drawer-in-start}.strct-drawer--top,.strct-drawer--bottom{left:0;right:0}.strct-drawer--top{top:0;border-bottom:1px solid var(--b2);animation:strct-drawer-in-top .18s ease}.strct-drawer--bottom{bottom:0;border-top:1px solid var(--b2);animation:strct-drawer-in-bottom .18s ease}.strct-drawer--start.strct-drawer--sm,.strct-drawer--end.strct-drawer--sm{width:300px}.strct-drawer--start.strct-drawer--md,.strct-drawer--end.strct-drawer--md{width:420px}.strct-drawer--start.strct-drawer--lg,.strct-drawer--end.strct-drawer--lg{width:600px}.strct-drawer--top.strct-drawer--sm,.strct-drawer--bottom.strct-drawer--sm{height:220px}.strct-drawer--top.strct-drawer--md,.strct-drawer--bottom.strct-drawer--md{height:340px}.strct-drawer--top.strct-drawer--lg,.strct-drawer--bottom.strct-drawer--lg{height:480px}.strct-drawer{max-width:100vw;max-height:100vh}.strct-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--b1);flex-shrink:0}.strct-drawer__title{font-size:15px;font-weight:600;color:var(--t1)}.strct-drawer__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-drawer__close:hover{color:var(--t1);background:var(--bg-3)}.strct-drawer__body{flex:1;min-height:0;overflow-y:auto;padding:16px 18px;font-size:13px;color:var(--t1)}.strct-drawer__foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--b1);flex-shrink:0}@keyframes strct-drawer-fade{0%{opacity:0}}@keyframes strct-drawer-in-start{0%{transform:translate(-100%)}}@keyframes strct-drawer-in-end{0%{transform:translate(100%)}}@keyframes strct-drawer-in-top{0%{transform:translateY(-100%)}}@keyframes strct-drawer-in-bottom{0%{transform:translateY(100%)}}@media(prefers-reduced-motion:reduce){.strct-drawer__backdrop,.strct-drawer{animation:none}}\n"] }]
5566
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], dismissable: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissable", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], footerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDrawerFooter), { isSignal: true }] }] } });
6243
+ }, styles: [".strct-modal__overlay{position:fixed;inset:0;z-index:var(--z-modal);display:flex;align-items:center;justify-content:center;padding:var(--space-5);background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-modal-fade .12s ease}@media(max-width:768px){.strct-modal__overlay{padding:var(--space-3)}}.strct-modal__dialog{width:100%;max-height:calc(100vh - 48px);display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-elevated);overflow:hidden;animation:strct-modal-rise .14s ease}.strct-modal__dialog--sm{max-width:min(480px,calc(100vw - 32px))}.strct-modal__dialog--md{max-width:min(640px,calc(100vw - 32px))}.strct-modal__dialog--lg{max-width:min(860px,calc(100vw - 32px))}.strct-modal__dialog--xl{max-width:min(1080px,calc(100vw - 32px))}.strct-modal__head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--b1)}.strct-modal__head--drag{cursor:move;-webkit-user-select:none;user-select:none;touch-action:none}.strct-modal__overlay--glass{background:#00000061;-webkit-backdrop-filter:blur(10px) saturate(1.25);backdrop-filter:blur(10px) saturate(1.25)}.strct-modal__dialog--glass{background:color-mix(in srgb,var(--bg-1) 72%,transparent);-webkit-backdrop-filter:blur(24px) saturate(1.4);backdrop-filter:blur(24px) saturate(1.4)}.strct-modal__dialog--glass .strct-modal__foot{background:color-mix(in srgb,var(--bg-2) 55%,transparent)}.strct-modal__title{font-size:14px;font-weight:600;color:var(--t1)}.strct-modal__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-modal__close:hover{color:var(--t1);background:var(--bg-3)}.strct-modal__body{padding:var(--space-4);overflow-y:auto;color:var(--t2);font-size:13px}.strct-modal__dialog--chromeless{width:calc(232px + var(--strct-wiz-content-min, 864px) + 2px);max-width:calc(100vw - 32px)}.strct-modal__dialog--chromeless:has(.strct-wiz__layout--aside){width:calc(232px + var(--strct-wiz-content-min, 864px) + 280px + 2px)}.strct-modal__dialog--chromeless .strct-modal__body{padding:0;overflow:hidden;display:flex;flex-direction:column;flex:1;min-height:0}.strct-modal__dialog--chromeless .strct-modal__body>*{flex:1;min-height:0}.strct-modal__foot{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--b1);background:var(--bg-2)}@keyframes strct-modal-fade{0%{opacity:0}}@keyframes strct-modal-rise{0%{opacity:0;transform:translateY(8px) scale(.98)}}@media(prefers-reduced-motion:reduce){.strct-modal__overlay,.strct-modal__dialog{animation:none}}\n"] }]
6244
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], hideFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideFooter", required: false }] }], chromeless: [{ type: i0.Input, args: [{ isSignal: true, alias: "chromeless", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], closeOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEscape", required: false }] }], closeOnBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdrop", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], backdropClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdropClass", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], lazyContent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctModalContent), { isSignal: true }] }] } });
5567
6245
 
6246
+ /** Marks the drawer's footer action area: `<ng-container strctDrawerFooter>…`. */
6247
+ class StrctDrawerFooter {
6248
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawerFooter, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6249
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDrawerFooter, isStandalone: true, selector: "[strctDrawerFooter]", ngImport: i0 });
6250
+ }
6251
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawerFooter, decorators: [{
6252
+ type: Directive,
6253
+ args: [{ selector: '[strctDrawerFooter]' }]
6254
+ }] });
5568
6255
  /**
5569
- * Click-to-open menu:
5570
- * <strct-dropdown align="end">
5571
- * <button strct-button strctDropdownTrigger>Actions</button>
5572
- * <strct-dropdown-item>Rename</strct-dropdown-item>
5573
- * <strct-dropdown-item critical>Delete</strct-dropdown-item>
5574
- * </strct-dropdown>
5575
- *
5576
- * With `popover`, the panel holds *form controls* instead of menu items: an
5577
- * inner click no longer closes it (only outside click / Escape do), and the
5578
- * semantics switch from `role="menu"` to a labeled `role="dialog"` — a panel
5579
- * of selects is not a menu to a screen reader either:
6256
+ * Slide-out overlay panel anchored to an edge of the viewport — for inspecting
6257
+ * or editing a record without losing the underlying table's scroll/selection.
5580
6258
  *
5581
- * <strct-dropdown popover popoverLabel="Filters">
5582
- * <button strct-button strctDropdownTrigger>Filters</button>
5583
- * <strct-field label="Severity">…</strct-field>
5584
- * </strct-dropdown>
6259
+ * <strct-drawer [(open)]="editing" side="end" title="Virtual machine">
6260
+ * … body …
6261
+ * <ng-container strctDrawerFooter>
6262
+ * <button strct-button (click)="editing.set(false)">Cancel</button>
6263
+ * </ng-container>
6264
+ * </strct-drawer>
5585
6265
  */
5586
- class StrctDropdown {
5587
- host = inject(ElementRef);
5588
- /** Horizontal alignment of the menu. */
5589
- align = input('start', /* @ts-ignore */
5590
- ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
5591
- /**
5592
- * Popover mode for panels holding form controls (filter/settings panels):
5593
- * inner clicks never close the panel — only outside click / Escape do —
5594
- * and it renders as a labeled `role="dialog"` instead of a menu.
5595
- */
5596
- popover = input(false, { ...(ngDevMode ? { debugName: "popover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
5597
- /** Accessible name of the popover dialog (localizable). */
5598
- popoverLabel = input('Filters', /* @ts-ignore */
5599
- ...(ngDevMode ? [{ debugName: "popoverLabel" }] : /* istanbul ignore next */ []));
5600
- /**
5601
- * Only with `popover`: the panel's rows are controls in their own right — a
5602
- * theme switcher, a filter — so they keep a real tab stop instead of the
5603
- * `tabindex="-1"` a menu's items carry. Without it nothing inside a popover
5604
- * can be reached from the keyboard at all.
5605
- */
5606
- focusable = input(false, { ...(ngDevMode ? { debugName: "focusable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
5607
- open = signal(false, /* @ts-ignore */
6266
+ class StrctDrawer {
6267
+ elementRef = inject((ElementRef));
6268
+ doc = inject(DOCUMENT);
6269
+ /** Open state (two-way). */
6270
+ open = model(false, /* @ts-ignore */
5608
6271
  ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
6272
+ /** Edge to anchor to. Defaults to `end` (right in LTR). */
6273
+ side = input('end', /* @ts-ignore */
6274
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
6275
+ /** Extent along the sliding axis. */
6276
+ size = input('md', /* @ts-ignore */
6277
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
6278
+ /** Header title. */
6279
+ title = input('', /* @ts-ignore */
6280
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
6281
+ /** Show the close button and allow backdrop/Esc dismissal. */
6282
+ dismissable = input(true, { ...(ngDevMode ? { debugName: "dismissable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6283
+ /** Accessible name of the dialog when no `title` is set (localizable). */
6284
+ ariaLabel = input('Panel', /* @ts-ignore */
6285
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
6286
+ /** Accessible label of the X close button (localizable). */
6287
+ closeLabel = input('Close', /* @ts-ignore */
6288
+ ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
6289
+ footerDef = contentChild(StrctDrawerFooter, /* @ts-ignore */
6290
+ ...(ngDevMode ? [{ debugName: "footerDef" }] : /* istanbul ignore next */ []));
6291
+ hasFooter = computed(() => !!this.footerDef(), /* @ts-ignore */
6292
+ ...(ngDevMode ? [{ debugName: "hasFooter" }] : /* istanbul ignore next */ []));
6293
+ /** Element that had focus before the drawer opened, restored on close. */
6294
+ previousActive = null;
6295
+ /** Whether this instance currently holds a scroll lock. */
6296
+ locked = false;
5609
6297
  constructor() {
5610
- // The static `popover` attribute collides with the native HTML Popover
5611
- // API: the UA styles [popover] hosts (Canvas background, medium border,
5612
- // fixed positioning) — a white frame around the trigger in dark themes.
5613
- // Angular has already read the input by now; the DOM attribute must go.
5614
- this.host.nativeElement.removeAttribute('popover');
5615
- }
5616
- /** Where focus was when the menu opened — restored on Escape/selection. */
5617
- lastFocused = null;
5618
- toggle() {
5619
- const willOpen = !this.open();
5620
- if (willOpen)
5621
- this.lastFocused = document.activeElement;
5622
- this.open.set(willOpen);
5623
- if (willOpen && !this.popover())
5624
- this.focusInitialItem();
5625
- }
5626
- /** Open (if closed) and move focus into the menu — ArrowDown on the trigger. */
5627
- openMenu() {
5628
- if (this.open())
5629
- return;
5630
- this.lastFocused = document.activeElement;
5631
- this.open.set(true);
5632
- if (!this.popover())
5633
- this.focusInitialItem();
5634
- }
5635
- close(restoreFocus = false) {
5636
- this.open.set(false);
5637
- if (restoreFocus)
5638
- this.lastFocused?.focus?.();
5639
- }
5640
- /**
5641
- * Menu mode closes ONLY on a real item activation — a click that lands on
5642
- * the menu's padding or a divider (a 2px miss) keeps it open instead of
5643
- * throwing the whole interaction away. Popover form controls never close.
5644
- */
5645
- onInnerActivate(event) {
5646
- if (this.popover())
5647
- return;
5648
- const target = event.target;
5649
- if (target?.closest('.strct-dd__itemaction'))
5650
- return; // the second action is not the item
5651
- const item = target?.closest('strct-dropdown-item');
5652
- if (item && item.getAttribute('aria-disabled') !== 'true')
5653
- this.close(true);
6298
+ effect(() => {
6299
+ const open = this.open();
6300
+ if (open && !this.locked) {
6301
+ this.locked = true;
6302
+ lockBodyScroll(this.doc);
6303
+ this.previousActive = saveFocusedElement();
6304
+ // Move focus into the panel once it has rendered.
6305
+ setTimeout(() => this.focusInitial());
6306
+ }
6307
+ else if (!open && this.locked) {
6308
+ this.locked = false;
6309
+ unlockBodyScroll(this.doc);
6310
+ restoreFocus(this.previousActive);
6311
+ this.previousActive = null;
6312
+ }
6313
+ });
6314
+ // Release the lock if the drawer is destroyed while still open.
6315
+ inject(DestroyRef).onDestroy(() => {
6316
+ if (this.locked) {
6317
+ this.locked = false;
6318
+ unlockBodyScroll(this.doc);
6319
+ }
6320
+ });
5654
6321
  }
5655
- /** APG menu keyboarding: arrows rove, Home/End jump, Enter/Space activate. */
5656
- onMenuKeydown(event) {
5657
- if (this.popover())
5658
- return;
5659
- const items = this.navItems();
5660
- if (!items.length)
5661
- return;
5662
- const idx = items.indexOf(event.target);
5663
- const key = event.key;
5664
- if (key === 'ArrowDown' || key === 'ArrowUp') {
5665
- event.preventDefault();
5666
- const next = idx === -1
5667
- ? key === 'ArrowDown'
5668
- ? 0
5669
- : items.length - 1
5670
- : (idx + (key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
5671
- items[next].focus();
5672
- }
5673
- else if (key === 'Home' || key === 'End') {
5674
- event.preventDefault();
5675
- items[key === 'Home' ? 0 : items.length - 1].focus();
5676
- }
5677
- else if (key === 'Enter' || key === ' ') {
5678
- event.preventDefault();
5679
- const target = event.target;
5680
- if (target.getAttribute('aria-disabled') !== 'true')
5681
- target.click();
5682
- }
5683
- else if (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'Delete') {
5684
- this.onItemActionKey(event, key);
5685
- }
5686
- else if (key === 'Tab') {
6322
+ onBackdrop() {
6323
+ if (this.dismissable())
5687
6324
  this.close();
5688
- }
5689
6325
  }
5690
- /**
5691
- * An item with a trailing action has two targets: the right arrow reaches the
5692
- * action from its item, the left arrow returns, and Delete on the item
5693
- * triggers it (the WAI-ARIA pattern for a secondary action). Keys that do not
5694
- * apply are left alone, so submenus keep their own arrow behaviour.
5695
- */
5696
- onItemActionKey(event, key) {
5697
- const target = event.target;
5698
- if (key === 'ArrowLeft') {
5699
- const item = target.closest('strct-dropdown-item');
5700
- if (!target.classList.contains('strct-dd__itemaction') || !item)
5701
- return;
5702
- event.preventDefault();
5703
- item.focus();
5704
- return;
5705
- }
5706
- const item = target.closest('strct-dropdown-item');
5707
- const action = item?.querySelector('.strct-dd__itemaction');
5708
- if (!item || item !== target || !action)
6326
+ onEscape(event) {
6327
+ if (!this.open() || !this.dismissable())
5709
6328
  return;
5710
- event.preventDefault();
5711
- if (key === 'ArrowRight')
5712
- action.focus();
5713
- else
5714
- action.click();
5715
- }
5716
- enabledItems() {
5717
- return [
5718
- ...this.host.nativeElement.querySelectorAll('strct-dropdown-item:not([aria-disabled="true"]), strct-submenu .strct-submenu__trigger'),
5719
- ];
5720
- }
5721
- /** What the arrow keys visit: enabled items, plus disabled ones that carry a
5722
- * hint (so the reason can be read). The menu still opens on an enabled one. */
5723
- navItems() {
5724
- const host = this.host.nativeElement;
5725
- const rows = [
5726
- ...host.querySelectorAll('strct-dropdown-item'),
5727
- ...host.querySelectorAll('strct-submenu .strct-submenu__trigger'),
5728
- ].filter((el) => el.getAttribute('aria-disabled') !== 'true' ||
5729
- el.classList.contains('strct-dd__item--hinted'));
5730
- // Two queries, so restore DOM order explicitly. (A single comma-separated
5731
- // selector is DOM-ordered in browsers, but not in every DOM implementation
5732
- // — jsdom returns it grouped, which scrambles arrow-key order in tests.)
5733
- return rows.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
6329
+ // A host modal/drawer must not also close on the same keypress.
6330
+ event.stopPropagation();
6331
+ this.close();
5734
6332
  }
5735
- /** Focus the selected item if there is one, else the first — after render. */
5736
- focusInitialItem() {
5737
- setTimeout(() => {
5738
- const items = this.enabledItems();
5739
- (items.find((i) => i.getAttribute('aria-checked') === 'true') ?? items[0])?.focus();
5740
- });
6333
+ /** Wrap Tab focus within the panel. */
6334
+ onTab(event) {
6335
+ const panel = this.panel();
6336
+ if (panel)
6337
+ keepTabInside(event, panel);
5741
6338
  }
5742
- onDocClick(event) {
5743
- if (this.open() && !this.host.nativeElement.contains(event.target)) {
5744
- this.close();
5745
- }
6339
+ close() {
6340
+ this.open.set(false);
5746
6341
  }
5747
- onEscape() {
5748
- if (this.open())
5749
- this.close(true);
6342
+ panel() {
6343
+ return this.elementRef.nativeElement.querySelector('.strct-drawer');
5750
6344
  }
5751
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
5752
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDropdown, isStandalone: true, selector: "strct-dropdown", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, popover: { classPropertyName: "popover", publicName: "popover", isSignal: true, isRequired: false, transformFunction: null }, popoverLabel: { classPropertyName: "popoverLabel", publicName: "popoverLabel", isSignal: true, isRequired: false, transformFunction: null }, focusable: { classPropertyName: "focusable", publicName: "focusable", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" }, classAttribute: "strct-dd" }, ngImport: i0, template: `
5753
- <!--
5754
- The wrapper is intentionally inert: the projected trigger should be a
5755
- real button (nesting interactives fails axe). Its native click —
5756
- pointer or Enter/Space — bubbles here; the strctDropdownTrigger
5757
- directive puts aria-haspopup/aria-expanded on the button itself.
5758
- -->
5759
- <div #trigger class="strct-dd__trigger" (click)="toggle()">
5760
- <ng-content select="[strctDropdownTrigger]" />
5761
- </div>
5762
- <!--
5763
- One panel for both modes (a default ng-content per @if branch would strand
5764
- the projected content in the inactive branch) — mode picks the semantics.
5765
- -->
6345
+ focusInitial() {
6346
+ const panel = this.panel();
6347
+ if (!panel)
6348
+ return;
6349
+ // Nothing tabbable (empty body, dismissable=false) → focus the panel itself.
6350
+ if (!focusFirstIn(panel))
6351
+ panel.focus();
6352
+ }
6353
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
6354
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDrawer, isStandalone: true, selector: "strct-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, dismissable: { classPropertyName: "dismissable", publicName: "dismissable", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" }, classAttribute: "strct-drawer-host" }, queries: [{ propertyName: "footerDef", first: true, predicate: StrctDrawerFooter, descendants: true, isSignal: true }], ngImport: i0, template: `
5766
6355
  @if (open()) {
6356
+ <!-- Backdrop: pointer-only dismiss target (click-through when not
6357
+ dismissable). Keyboard users dismiss via Escape. -->
6358
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
5767
6359
  <div
5768
- class="strct-dd__menu"
5769
- [class.strct-dd__menu--popover]="popover()"
5770
- [strctOverlay]="trigger"
5771
- [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
5772
- [attr.role]="popover() ? 'dialog' : 'menu'"
5773
- [attr.aria-label]="popover() ? popoverLabel() : null"
5774
- [attr.tabindex]="popover() ? -1 : 0"
5775
- (click)="onInnerActivate($event)"
5776
- (keydown)="onMenuKeydown($event)"
6360
+ class="strct-drawer__backdrop"
6361
+ [class.strct-drawer__backdrop--bare]="!dismissable()"
6362
+ (click)="onBackdrop()"
6363
+ ></div>
6364
+ <aside
6365
+ class="strct-drawer strct-drawer--{{ side() }} strct-drawer--{{ size() }}"
6366
+ role="dialog"
6367
+ aria-modal="true"
6368
+ tabindex="-1"
6369
+ [attr.aria-label]="title() || ariaLabel()"
6370
+ (keydown.tab)="onTab($event)"
6371
+ (keydown.shift.tab)="onTab($event)"
5777
6372
  >
5778
- <ng-content />
5779
- </div>
6373
+ @if (title() || dismissable()) {
6374
+ <header class="strct-drawer__head">
6375
+ <span class="strct-drawer__title">{{ title() }}</span>
6376
+ @if (dismissable()) {
6377
+ <button
6378
+ type="button"
6379
+ class="strct-drawer__close"
6380
+ [attr.aria-label]="closeLabel()"
6381
+ (click)="close()"
6382
+ >
6383
+ <strct-icon name="close" [size]="15" />
6384
+ </button>
6385
+ }
6386
+ </header>
6387
+ }
6388
+ <div class="strct-drawer__body"><ng-content /></div>
6389
+ @if (hasFooter()) {
6390
+ <footer class="strct-drawer__foot"><ng-content select="[strctDrawerFooter]" /></footer>
6391
+ }
6392
+ </aside>
5780
6393
  }
5781
- `, isInline: true, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-dropdown);min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"], dependencies: [{ kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
6394
+ `, isInline: true, styles: [".strct-drawer__backdrop{position:fixed;inset:0;z-index:var(--z-overlay);background:var(--backdrop);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);animation:strct-drawer-fade .16s ease}.strct-drawer__backdrop--bare{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;pointer-events:none}.strct-drawer{position:fixed;z-index:calc(var(--z-overlay) + 1);display:flex;flex-direction:column;background:var(--bg-1);box-shadow:var(--shh)}.strct-drawer--start,.strct-drawer--end{top:0;bottom:0;border-radius:0}.strct-drawer--start{inset-inline-start:0;border-inline-end:1px solid var(--b2);animation:strct-drawer-in-start .18s ease}.strct-drawer--end{inset-inline-end:0;border-inline-start:1px solid var(--b2);animation:strct-drawer-in-end .18s ease}[dir=rtl] .strct-drawer--start{animation-name:strct-drawer-in-end}[dir=rtl] .strct-drawer--end{animation-name:strct-drawer-in-start}.strct-drawer--top,.strct-drawer--bottom{left:0;right:0}.strct-drawer--top{top:0;border-bottom:1px solid var(--b2);animation:strct-drawer-in-top .18s ease}.strct-drawer--bottom{bottom:0;border-top:1px solid var(--b2);animation:strct-drawer-in-bottom .18s ease}.strct-drawer--start.strct-drawer--sm,.strct-drawer--end.strct-drawer--sm{width:300px}.strct-drawer--start.strct-drawer--md,.strct-drawer--end.strct-drawer--md{width:420px}.strct-drawer--start.strct-drawer--lg,.strct-drawer--end.strct-drawer--lg{width:600px}.strct-drawer--top.strct-drawer--sm,.strct-drawer--bottom.strct-drawer--sm{height:220px}.strct-drawer--top.strct-drawer--md,.strct-drawer--bottom.strct-drawer--md{height:340px}.strct-drawer--top.strct-drawer--lg,.strct-drawer--bottom.strct-drawer--lg{height:480px}.strct-drawer{max-width:100vw;max-height:100vh}.strct-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--b1);flex-shrink:0}.strct-drawer__title{font-size:15px;font-weight:600;color:var(--t1)}.strct-drawer__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-drawer__close:hover{color:var(--t1);background:var(--bg-3)}.strct-drawer__body{flex:1;min-height:0;overflow-y:auto;padding:16px 18px;font-size:13px;color:var(--t1)}.strct-drawer__foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--b1);flex-shrink:0}@keyframes strct-drawer-fade{0%{opacity:0}}@keyframes strct-drawer-in-start{0%{transform:translate(-100%)}}@keyframes strct-drawer-in-end{0%{transform:translate(100%)}}@keyframes strct-drawer-in-top{0%{transform:translateY(-100%)}}@keyframes strct-drawer-in-bottom{0%{transform:translateY(100%)}}@media(prefers-reduced-motion:reduce){.strct-drawer__backdrop,.strct-drawer{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5782
6395
  }
5783
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdown, decorators: [{
6396
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDrawer, decorators: [{
5784
6397
  type: Component,
5785
- args: [{ selector: 'strct-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctOverlay], template: `
5786
- <!--
5787
- The wrapper is intentionally inert: the projected trigger should be a
5788
- real button (nesting interactives fails axe). Its native click —
5789
- pointer or Enter/Space — bubbles here; the strctDropdownTrigger
5790
- directive puts aria-haspopup/aria-expanded on the button itself.
5791
- -->
5792
- <div #trigger class="strct-dd__trigger" (click)="toggle()">
5793
- <ng-content select="[strctDropdownTrigger]" />
5794
- </div>
5795
- <!--
5796
- One panel for both modes (a default ng-content per @if branch would strand
5797
- the projected content in the inactive branch) — mode picks the semantics.
5798
- -->
6398
+ args: [{ selector: 'strct-drawer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
5799
6399
  @if (open()) {
6400
+ <!-- Backdrop: pointer-only dismiss target (click-through when not
6401
+ dismissable). Keyboard users dismiss via Escape. -->
6402
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
5800
6403
  <div
5801
- class="strct-dd__menu"
5802
- [class.strct-dd__menu--popover]="popover()"
5803
- [strctOverlay]="trigger"
5804
- [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
5805
- [attr.role]="popover() ? 'dialog' : 'menu'"
5806
- [attr.aria-label]="popover() ? popoverLabel() : null"
5807
- [attr.tabindex]="popover() ? -1 : 0"
5808
- (click)="onInnerActivate($event)"
5809
- (keydown)="onMenuKeydown($event)"
5810
- >
5811
- <ng-content />
5812
- </div>
5813
- }
5814
- `, host: { class: 'strct-dd' }, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-dropdown);min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-md);box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"] }]
5815
- }], ctorParameters: () => [], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "popover", required: false }] }], popoverLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "popoverLabel", required: false }] }], focusable: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusable", required: false }] }], onDocClick: [{
5816
- type: HostListener,
5817
- args: ['document:click', ['$event']]
5818
- }], onEscape: [{
5819
- type: HostListener,
5820
- args: ['document:keydown.escape']
5821
- }] } });
5822
- /**
5823
- * Marks (and upgrades) the dropdown's trigger element. The attribute alone is
5824
- * enough for content projection; importing the directive additionally wires
5825
- * `aria-haspopup` / `aria-expanded` onto the real button.
5826
- */
5827
- class StrctDropdownTrigger {
5828
- dd = inject(StrctDropdown, { optional: true });
5829
- onArrowDown(event) {
5830
- if (!this.dd || this.dd.popover())
5831
- return;
5832
- event.preventDefault();
5833
- this.dd.openMenu();
5834
- }
5835
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5836
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownTrigger, isStandalone: true, selector: "[strctDropdownTrigger]", host: { listeners: { "keydown.arrowdown": "onArrowDown($event)" }, properties: { "attr.aria-haspopup": "dd ? (dd.popover() ? 'dialog' : 'menu') : null", "attr.aria-expanded": "dd ? dd.open() : null" } }, ngImport: i0 });
5837
- }
5838
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownTrigger, decorators: [{
5839
- type: Directive,
5840
- args: [{
5841
- selector: '[strctDropdownTrigger]',
5842
- host: {
5843
- '[attr.aria-haspopup]': "dd ? (dd.popover() ? 'dialog' : 'menu') : null",
5844
- '[attr.aria-expanded]': 'dd ? dd.open() : null',
5845
- '(keydown.arrowdown)': 'onArrowDown($event)',
5846
- },
5847
- }]
5848
- }] });
5849
- let dropdownItemCounter = 0;
5850
- /** A selectable row inside a `<strct-dropdown>`. */
5851
- /**
5852
- * A second action at the end of a menu item — "delete this saved view" beside
5853
- * "open it". It stops its own click from reaching the item, so the item is not
5854
- * activated and the menu stays open, and it stays out of the Tab order: the
5855
- * right arrow reaches it from its item, the left arrow returns, and Delete on
5856
- * the item triggers it.
5857
- */
5858
- class StrctDropdownItemAction {
5859
- onClick(event) {
5860
- event.stopPropagation();
5861
- }
5862
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemAction, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5863
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownItemAction, isStandalone: true, selector: "[strctDropdownItemAction]", host: { attributes: { "tabindex": "-1" }, listeners: { "click": "onClick($event)" }, classAttribute: "strct-dd__itemaction" }, ngImport: i0 });
5864
- }
5865
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItemAction, decorators: [{
5866
- type: Directive,
5867
- args: [{
5868
- selector: '[strctDropdownItemAction]',
5869
- host: {
5870
- class: 'strct-dd__itemaction',
5871
- tabindex: '-1',
5872
- '(click)': 'onClick($event)',
5873
- },
5874
- }]
5875
- }] });
5876
- class StrctDropdownItem {
5877
- /**
5878
- * Select-like usage: bind `selected` and the item becomes a
5879
- * `menuitemradio` with `aria-checked`, a leading ✓ on the current choice
5880
- * and an aligned lead slot — so reopening the menu shows what is chosen.
5881
- * Leave unbound for plain action items.
5882
- */
5883
- selected = input(null, /* @ts-ignore */
5884
- ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
5885
- /** Danger. */
5886
- critical = input(false, { ...(ngDevMode ? { debugName: "critical" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
5887
- /** Static disable flag. */
5888
- disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
5889
- /**
5890
- * A short explanation — typically why a disabled item is unavailable. Shown
5891
- * as the tooltip and given to assistive technology as the description; never
5892
- * rendered inline. A disabled item with a hint stays keyboard-reachable so
5893
- * its reason can be read; activation stays blocked.
5894
- */
5895
- hint = input(null, /* @ts-ignore */
5896
- ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
5897
- hintId = `strct-dd-item-${++dropdownItemCounter}-hint`;
5898
- /** The panel this row belongs to, when it is inside one. */
5899
- panel = inject(StrctDropdown, { optional: true });
5900
- /**
5901
- * A menu's rows are roved with the arrow keys, so they stay out of the tab
5902
- * order. A `popover [focusable]` is a panel of real controls, and its rows
5903
- * keep a tab stop of their own.
5904
- */
5905
- tabindex = computed(() => {
5906
- if (this.disabled() && !this.hint())
5907
- return null;
5908
- return this.panel?.popover() && this.panel.focusable() ? 0 : -1;
5909
- }, /* @ts-ignore */
5910
- ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
5911
- constructor() {
5912
- // Without pointer-events:none (see the hinted style), a disabled item
5913
- // would pass clicks to the consumer's (click). Capture-phase at the target
5914
- // runs before those listeners, and stopping here also keeps the menu's own
5915
- // activate-and-close handler from seeing it.
5916
- const host = inject((ElementRef)).nativeElement;
5917
- host.addEventListener('click', (event) => {
5918
- if (!this.disabled())
5919
- return;
5920
- event.preventDefault();
5921
- event.stopImmediatePropagation();
5922
- }, { capture: true });
5923
- }
5924
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
5925
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctDropdownItem, isStandalone: true, selector: "strct-dropdown-item", inputs: { selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, critical: { classPropertyName: "critical", publicName: "critical", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.role": "selected() === null ? 'menuitem' : 'menuitemradio'", "attr.aria-checked": "selected()", "attr.tabindex": "tabindex()", "attr.title": "hint() || null", "attr.aria-describedby": "hint() ? hintId : null", "class.strct-dd__item--hinted": "!!hint()", "class.strct-dd__item--critical": "critical()", "class.strct-dd__item--selected": "selected() === true", "attr.aria-disabled": "disabled() || null" }, classAttribute: "strct-dd__item" }, ngImport: i0, template: `@if (selected() !== null) {
5926
- <span class="strct-dd__check" aria-hidden="true">
5927
- @if (selected()) {
5928
- <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
6404
+ class="strct-drawer__backdrop"
6405
+ [class.strct-drawer__backdrop--bare]="!dismissable()"
6406
+ (click)="onBackdrop()"
6407
+ ></div>
6408
+ <aside
6409
+ class="strct-drawer strct-drawer--{{ side() }} strct-drawer--{{ size() }}"
6410
+ role="dialog"
6411
+ aria-modal="true"
6412
+ tabindex="-1"
6413
+ [attr.aria-label]="title() || ariaLabel()"
6414
+ (keydown.tab)="onTab($event)"
6415
+ (keydown.shift.tab)="onTab($event)"
6416
+ >
6417
+ @if (title() || dismissable()) {
6418
+ <header class="strct-drawer__head">
6419
+ <span class="strct-drawer__title">{{ title() }}</span>
6420
+ @if (dismissable()) {
6421
+ <button
6422
+ type="button"
6423
+ class="strct-drawer__close"
6424
+ [attr.aria-label]="closeLabel()"
6425
+ (click)="close()"
6426
+ >
6427
+ <strct-icon name="close" [size]="15" />
6428
+ </button>
6429
+ }
6430
+ </header>
5929
6431
  }
5930
- </span>
5931
- }
5932
- <!-- Declared before the catch-all so its selector wins; CSS order and an
5933
- auto margin put it at the item's end. -->
5934
- <ng-content select="[strctDropdownItemAction]" />
5935
- <ng-content />
5936
- @if (hint()) {
5937
- <!-- hidden: kept out of the item's accessible NAME, still read as its
5938
- description through aria-describedby. -->
5939
- <span [id]="hintId" hidden>{{ hint() }}</span>
5940
- }`, isInline: true, styles: [".strct-dd__item{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-dd__item:hover,.strct-dd__item:focus-visible{background:var(--bg-3);outline:none}.strct-dd__itemaction{order:1;margin-inline-start:auto;flex:none}.strct-dd__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-dd__item--selected{color:var(--t1);font-weight:600;background:var(--acc-s)}.strct-dd__item--critical{color:var(--critical)}.strct-dd__item--critical:hover{background:var(--critical-bg)}.strct-dd__item[aria-disabled=true]{color:var(--t4);pointer-events:none}.strct-dd__item--hinted[aria-disabled=true]{pointer-events:auto;cursor:not-allowed}.strct-dd__item--hinted[aria-disabled=true]:hover{background:transparent}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5941
- }
5942
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownItem, decorators: [{
5943
- type: Component,
5944
- args: [{ selector: 'strct-dropdown-item', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `@if (selected() !== null) {
5945
- <span class="strct-dd__check" aria-hidden="true">
5946
- @if (selected()) {
5947
- <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
6432
+ <div class="strct-drawer__body"><ng-content /></div>
6433
+ @if (hasFooter()) {
6434
+ <footer class="strct-drawer__foot"><ng-content select="[strctDrawerFooter]" /></footer>
5948
6435
  }
5949
- </span>
6436
+ </aside>
5950
6437
  }
5951
- <!-- Declared before the catch-all so its selector wins; CSS order and an
5952
- auto margin put it at the item's end. -->
5953
- <ng-content select="[strctDropdownItemAction]" />
5954
- <ng-content />
5955
- @if (hint()) {
5956
- <!-- hidden: kept out of the item's accessible NAME, still read as its
5957
- description through aria-describedby. -->
5958
- <span [id]="hintId" hidden>{{ hint() }}</span>
5959
- }`, host: {
5960
- class: 'strct-dd__item',
5961
- '[attr.role]': "selected() === null ? 'menuitem' : 'menuitemradio'",
5962
- '[attr.aria-checked]': 'selected()',
5963
- // A disabled item is skipped — unless it has a hint to be read.
5964
- '[attr.tabindex]': 'tabindex()',
5965
- '[attr.title]': 'hint() || null',
5966
- '[attr.aria-describedby]': 'hint() ? hintId : null',
5967
- '[class.strct-dd__item--hinted]': '!!hint()',
5968
- '[class.strct-dd__item--critical]': 'critical()',
5969
- '[class.strct-dd__item--selected]': 'selected() === true',
5970
- '[attr.aria-disabled]': 'disabled() || null',
5971
- }, styles: [".strct-dd__item{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-dd__item:hover,.strct-dd__item:focus-visible{background:var(--bg-3);outline:none}.strct-dd__itemaction{order:1;margin-inline-start:auto;flex:none}.strct-dd__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-dd__item--selected{color:var(--t1);font-weight:600;background:var(--acc-s)}.strct-dd__item--critical{color:var(--critical)}.strct-dd__item--critical:hover{background:var(--critical-bg)}.strct-dd__item[aria-disabled=true]{color:var(--t4);pointer-events:none}.strct-dd__item--hinted[aria-disabled=true]{pointer-events:auto;cursor:not-allowed}.strct-dd__item--hinted[aria-disabled=true]:hover{background:transparent}\n"] }]
5972
- }], ctorParameters: () => [], propDecorators: { selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], critical: [{ type: i0.Input, args: [{ isSignal: true, alias: "critical", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
5973
- /** Thin separator between groups of menu items. */
5974
- class StrctDropdownDivider {
5975
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownDivider, deps: [], target: i0.ɵɵFactoryTarget.Component });
5976
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.4", type: StrctDropdownDivider, isStandalone: true, selector: "strct-dropdown-divider", host: { attributes: { "role": "separator" }, classAttribute: "strct-dd__divider" }, ngImport: i0, template: '', isInline: true, styles: [".strct-dd__divider{display:block;height:1px;margin:4px 6px;background:var(--b2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5977
- }
5978
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctDropdownDivider, decorators: [{
5979
- type: Component,
5980
- args: [{ selector: 'strct-dropdown-divider', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', host: { class: 'strct-dd__divider', role: 'separator' }, styles: [".strct-dd__divider{display:block;height:1px;margin:4px 6px;background:var(--b2)}\n"] }]
5981
- }] });
6438
+ `, host: {
6439
+ class: 'strct-drawer-host',
6440
+ '(document:keydown.escape)': 'onEscape($event)',
6441
+ }, styles: [".strct-drawer__backdrop{position:fixed;inset:0;z-index:var(--z-overlay);background:var(--backdrop);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);animation:strct-drawer-fade .16s ease}.strct-drawer__backdrop--bare{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;pointer-events:none}.strct-drawer{position:fixed;z-index:calc(var(--z-overlay) + 1);display:flex;flex-direction:column;background:var(--bg-1);box-shadow:var(--shh)}.strct-drawer--start,.strct-drawer--end{top:0;bottom:0;border-radius:0}.strct-drawer--start{inset-inline-start:0;border-inline-end:1px solid var(--b2);animation:strct-drawer-in-start .18s ease}.strct-drawer--end{inset-inline-end:0;border-inline-start:1px solid var(--b2);animation:strct-drawer-in-end .18s ease}[dir=rtl] .strct-drawer--start{animation-name:strct-drawer-in-end}[dir=rtl] .strct-drawer--end{animation-name:strct-drawer-in-start}.strct-drawer--top,.strct-drawer--bottom{left:0;right:0}.strct-drawer--top{top:0;border-bottom:1px solid var(--b2);animation:strct-drawer-in-top .18s ease}.strct-drawer--bottom{bottom:0;border-top:1px solid var(--b2);animation:strct-drawer-in-bottom .18s ease}.strct-drawer--start.strct-drawer--sm,.strct-drawer--end.strct-drawer--sm{width:300px}.strct-drawer--start.strct-drawer--md,.strct-drawer--end.strct-drawer--md{width:420px}.strct-drawer--start.strct-drawer--lg,.strct-drawer--end.strct-drawer--lg{width:600px}.strct-drawer--top.strct-drawer--sm,.strct-drawer--bottom.strct-drawer--sm{height:220px}.strct-drawer--top.strct-drawer--md,.strct-drawer--bottom.strct-drawer--md{height:340px}.strct-drawer--top.strct-drawer--lg,.strct-drawer--bottom.strct-drawer--lg{height:480px}.strct-drawer{max-width:100vw;max-height:100vh}.strct-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--b1);flex-shrink:0}.strct-drawer__title{font-size:15px;font-weight:600;color:var(--t1)}.strct-drawer__close{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-drawer__close:hover{color:var(--t1);background:var(--bg-3)}.strct-drawer__body{flex:1;min-height:0;overflow-y:auto;padding:16px 18px;font-size:13px;color:var(--t1)}.strct-drawer__foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-top:1px solid var(--b1);flex-shrink:0}@keyframes strct-drawer-fade{0%{opacity:0}}@keyframes strct-drawer-in-start{0%{transform:translate(-100%)}}@keyframes strct-drawer-in-end{0%{transform:translate(100%)}}@keyframes strct-drawer-in-top{0%{transform:translateY(-100%)}}@keyframes strct-drawer-in-bottom{0%{transform:translateY(100%)}}@media(prefers-reduced-motion:reduce){.strct-drawer__backdrop,.strct-drawer{animation:none}}\n"] }]
6442
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], dismissable: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissable", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], footerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDrawerFooter), { isSignal: true }] }] } });
5982
6443
 
5983
6444
  /**
5984
6445
  * Right-click (context) menu. Wraps a trigger area and shows a menu at the
@@ -7108,14 +7569,102 @@ class StrctSectionMenu {
7108
7569
  next.set(sec.label, !open);
7109
7570
  this.overrides.set(next);
7110
7571
  }
7111
- pick(item) {
7572
+ /** Enabled entry with a router destination → renders as `<a routerLink>`. */
7573
+ isRouted(item) {
7574
+ return item.routerLink != null && !item.disabled;
7575
+ }
7576
+ /**
7577
+ * Active state of a router-link entry: an explicit `activeId` is
7578
+ * authoritative; without one the router's own active state drives it — the
7579
+ * rail's contract.
7580
+ */
7581
+ isActive(item, routerActive) {
7582
+ const id = this.activeId();
7583
+ return id !== null ? item.id === id : routerActive;
7584
+ }
7585
+ pick(item, event) {
7112
7586
  if (item.disabled)
7113
7587
  return;
7588
+ // A modified or non-primary click on a link belongs to the browser (a new
7589
+ // tab, a copied address) — never to us: no activeId change, no (select).
7590
+ if (event &&
7591
+ (item.routerLink != null || item.href) &&
7592
+ (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
7593
+ return;
7594
+ }
7114
7595
  this.activeId.set(item.id);
7115
7596
  this.select.emit(item);
7116
7597
  }
7117
7598
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSectionMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
7118
7599
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctSectionMenu, isStandalone: true, selector: "strct-section-menu", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: true, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, showIcons: { classPropertyName: "showIcons", publicName: "showIcons", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeId: "activeIdChange", select: "select" }, host: { classAttribute: "strct-sm-host" }, ngImport: i0, template: `
7600
+ <!-- What every entry holds, whatever element carries it. -->
7601
+ <ng-template #rowContent let-item>
7602
+ @if (showIcons() && item.icon) {
7603
+ <strct-icon
7604
+ class="strct-sm__item-icon"
7605
+ [name]="item.icon"
7606
+ [size]="16"
7607
+ [strokeWidth]="1.4"
7608
+ />
7609
+ }
7610
+ <span class="strct-sm__item-label">{{ item.label }}</span>
7611
+ @if (item.trailingIcon) {
7612
+ <strct-icon
7613
+ class="strct-sm__trailing"
7614
+ [name]="item.trailingIcon"
7615
+ [size]="14"
7616
+ [strokeWidth]="1.5"
7617
+ />
7618
+ }
7619
+ @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {
7620
+ <span class="strct-sm__badge strct-sm__badge--{{ item.badgeStatus ?? 'accent' }}">
7621
+ {{ item.badge }}
7622
+ </span>
7623
+ } @else if (item.dot) {
7624
+ <span class="strct-sm__dot strct-sm__dot--{{ item.dotStatus ?? 'accent' }}"></span>
7625
+ }
7626
+ </ng-template>
7627
+
7628
+ <!-- A router link, a plain link, or a button (the default). A page with an
7629
+ address of its own has to be openable in a new tab, as the rail
7630
+ beside this menu already is. -->
7631
+ <ng-template #row let-item>
7632
+ @if (isRouted(item)) {
7633
+ <a
7634
+ class="strct-sm__item"
7635
+ [routerLink]="item.routerLink"
7636
+ routerLinkActive
7637
+ #rla="routerLinkActive"
7638
+ [class.strct-sm__item--active]="isActive(item, rla.isActive)"
7639
+ [attr.aria-current]="isActive(item, rla.isActive) ? 'page' : null"
7640
+ (click)="pick(item, $event)"
7641
+ >
7642
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7643
+ </a>
7644
+ } @else if (item.href && !item.disabled) {
7645
+ <a
7646
+ class="strct-sm__item"
7647
+ [href]="item.href"
7648
+ [class.strct-sm__item--active]="item.id === activeId()"
7649
+ [attr.aria-current]="item.id === activeId() ? 'page' : null"
7650
+ (click)="pick(item, $event)"
7651
+ >
7652
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7653
+ </a>
7654
+ } @else {
7655
+ <button
7656
+ type="button"
7657
+ class="strct-sm__item"
7658
+ [class.strct-sm__item--active]="item.id === activeId()"
7659
+ [disabled]="item.disabled"
7660
+ [attr.aria-current]="item.id === activeId() ? 'page' : null"
7661
+ (click)="pick(item)"
7662
+ >
7663
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7664
+ </button>
7665
+ }
7666
+ </ng-template>
7667
+
7119
7668
  <nav class="strct-sm" [attr.aria-label]="ariaLabel()">
7120
7669
  @for (sec of sections(); track sec.label) {
7121
7670
  <div class="strct-sm__section" [class.strct-sm__section--collapsible]="collapsible()">
@@ -7146,54 +7695,86 @@ class StrctSectionMenu {
7146
7695
  @if (isOpen(sec)) {
7147
7696
  <div class="strct-sm__items" role="group">
7148
7697
  @for (item of sec.items; track item.id) {
7149
- <button
7150
- type="button"
7151
- class="strct-sm__item"
7152
- [class.strct-sm__item--active]="item.id === activeId()"
7153
- [disabled]="item.disabled"
7154
- [attr.aria-current]="item.id === activeId() ? 'page' : null"
7155
- (click)="pick(item)"
7156
- >
7157
- @if (showIcons() && item.icon) {
7158
- <strct-icon
7159
- class="strct-sm__item-icon"
7160
- [name]="item.icon"
7161
- [size]="16"
7162
- [strokeWidth]="1.4"
7163
- />
7164
- }
7165
- <span class="strct-sm__item-label">{{ item.label }}</span>
7166
- @if (item.trailingIcon) {
7167
- <strct-icon
7168
- class="strct-sm__trailing"
7169
- [name]="item.trailingIcon"
7170
- [size]="14"
7171
- [strokeWidth]="1.5"
7172
- />
7173
- }
7174
- @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {
7175
- <span
7176
- class="strct-sm__badge strct-sm__badge--{{ item.badgeStatus ?? 'accent' }}"
7177
- >
7178
- {{ item.badge }}
7179
- </span>
7180
- } @else if (item.dot) {
7181
- <span
7182
- class="strct-sm__dot strct-sm__dot--{{ item.dotStatus ?? 'accent' }}"
7183
- ></span>
7184
- }
7185
- </button>
7698
+ <ng-container *ngTemplateOutlet="row; context: { $implicit: item }" />
7186
7699
  }
7187
7700
  </div>
7188
7701
  }
7189
7702
  </div>
7190
7703
  }
7191
7704
  </nav>
7192
- `, isInline: true, styles: [".strct-sm{display:flex;flex-direction:column;gap:1px}.strct-sm__section--collapsible+.strct-sm__section--collapsible{margin-top:1px}.strct-sm__cat{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--t2);font-size:13px;font-weight:500;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease,color .13s ease}.strct-sm__cat:hover{background:var(--bg-3);color:var(--t1)}.strct-sm__cat:focus-visible,.strct-sm__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-sm__chevron{display:inline-flex;color:var(--t3);flex-shrink:0;transition:transform .15s ease}.strct-sm__chevron--open{transform:rotate(90deg)}.strct-sm__cat-icon{color:var(--t3);flex-shrink:0}.strct-sm__cat-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__cat-static{padding:11px 10px 5px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--t3)}.strct-sm__items{display:flex;flex-direction:column}.strct-sm__item{display:flex;align-items:center;gap:9px;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease}.strct-sm__section--collapsible .strct-sm__item{padding-inline-start:31px}.strct-sm__item:hover:not(:disabled){background:var(--bg-3)}.strct-sm__item--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-sm__item--active .strct-sm__item-icon{color:var(--acc)}.strct-sm__item:disabled{opacity:.5;cursor:not-allowed}.strct-sm__item-icon{color:var(--t2);flex-shrink:0}.strct-sm__item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__trailing{flex-shrink:0;color:var(--t3)}.strct-sm__badge{flex-shrink:0;min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;line-height:1;border-radius:9px;font-variant-numeric:tabular-nums}.strct-sm__badge--accent{background:var(--acc-m);color:var(--acc)}.strct-sm__badge--success{background:var(--success-bg);color:var(--success)}.strct-sm__badge--warning{background:var(--warning-bg);color:var(--warning)}.strct-sm__badge--critical{background:var(--critical-bg);color:var(--critical)}.strct-sm__dot{flex-shrink:0;width:7px;height:7px;border-radius:50%}.strct-sm__dot--accent{background:var(--acc)}.strct-sm__dot--success{background:var(--success)}.strct-sm__dot--warning{background:var(--warning)}.strct-sm__dot--critical{background:var(--critical)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7705
+ `, isInline: true, styles: [".strct-sm{display:flex;flex-direction:column;gap:1px}.strct-sm__section--collapsible+.strct-sm__section--collapsible{margin-top:1px}.strct-sm__cat{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--t2);font-size:13px;font-weight:500;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease,color .13s ease}.strct-sm__cat:hover{background:var(--bg-3);color:var(--t1)}.strct-sm__cat:focus-visible,.strct-sm__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-sm__chevron{display:inline-flex;color:var(--t3);flex-shrink:0;transition:transform .15s ease}.strct-sm__chevron--open{transform:rotate(90deg)}.strct-sm__cat-icon{color:var(--t3);flex-shrink:0}.strct-sm__cat-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__cat-static{padding:11px 10px 5px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--t3)}.strct-sm__items{display:flex;flex-direction:column}.strct-sm__item{display:flex;align-items:center;gap:9px;width:100%;box-sizing:border-box;text-decoration:none;padding:7px 10px;border:0;border-radius:6px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease}.strct-sm__section--collapsible .strct-sm__item{padding-inline-start:31px}.strct-sm__item:hover:not(:disabled){background:var(--bg-3)}.strct-sm__item--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-sm__item--active .strct-sm__item-icon{color:var(--acc)}.strct-sm__item:disabled{opacity:.5;cursor:not-allowed}.strct-sm__item-icon{color:var(--t2);flex-shrink:0}.strct-sm__item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__trailing{flex-shrink:0;color:var(--t3)}.strct-sm__badge{flex-shrink:0;min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;line-height:1;border-radius:9px;font-variant-numeric:tabular-nums}.strct-sm__badge--accent{background:var(--acc-m);color:var(--acc)}.strct-sm__badge--success{background:var(--success-bg);color:var(--success)}.strct-sm__badge--warning{background:var(--warning-bg);color:var(--warning)}.strct-sm__badge--critical{background:var(--critical-bg);color:var(--critical)}.strct-sm__dot{flex-shrink:0;width:7px;height:7px;border-radius:50%}.strct-sm__dot--accent{background:var(--acc)}.strct-sm__dot--success{background:var(--success)}.strct-sm__dot--warning{background:var(--warning)}.strct-sm__dot--critical{background:var(--critical)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7193
7706
  }
7194
7707
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctSectionMenu, decorators: [{
7195
7708
  type: Component,
7196
- args: [{ selector: 'strct-section-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
7709
+ args: [{ selector: 'strct-section-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [NgTemplateOutlet, RouterLink, RouterLinkActive, StrctIcon], template: `
7710
+ <!-- What every entry holds, whatever element carries it. -->
7711
+ <ng-template #rowContent let-item>
7712
+ @if (showIcons() && item.icon) {
7713
+ <strct-icon
7714
+ class="strct-sm__item-icon"
7715
+ [name]="item.icon"
7716
+ [size]="16"
7717
+ [strokeWidth]="1.4"
7718
+ />
7719
+ }
7720
+ <span class="strct-sm__item-label">{{ item.label }}</span>
7721
+ @if (item.trailingIcon) {
7722
+ <strct-icon
7723
+ class="strct-sm__trailing"
7724
+ [name]="item.trailingIcon"
7725
+ [size]="14"
7726
+ [strokeWidth]="1.5"
7727
+ />
7728
+ }
7729
+ @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {
7730
+ <span class="strct-sm__badge strct-sm__badge--{{ item.badgeStatus ?? 'accent' }}">
7731
+ {{ item.badge }}
7732
+ </span>
7733
+ } @else if (item.dot) {
7734
+ <span class="strct-sm__dot strct-sm__dot--{{ item.dotStatus ?? 'accent' }}"></span>
7735
+ }
7736
+ </ng-template>
7737
+
7738
+ <!-- A router link, a plain link, or a button (the default). A page with an
7739
+ address of its own has to be openable in a new tab, as the rail
7740
+ beside this menu already is. -->
7741
+ <ng-template #row let-item>
7742
+ @if (isRouted(item)) {
7743
+ <a
7744
+ class="strct-sm__item"
7745
+ [routerLink]="item.routerLink"
7746
+ routerLinkActive
7747
+ #rla="routerLinkActive"
7748
+ [class.strct-sm__item--active]="isActive(item, rla.isActive)"
7749
+ [attr.aria-current]="isActive(item, rla.isActive) ? 'page' : null"
7750
+ (click)="pick(item, $event)"
7751
+ >
7752
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7753
+ </a>
7754
+ } @else if (item.href && !item.disabled) {
7755
+ <a
7756
+ class="strct-sm__item"
7757
+ [href]="item.href"
7758
+ [class.strct-sm__item--active]="item.id === activeId()"
7759
+ [attr.aria-current]="item.id === activeId() ? 'page' : null"
7760
+ (click)="pick(item, $event)"
7761
+ >
7762
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7763
+ </a>
7764
+ } @else {
7765
+ <button
7766
+ type="button"
7767
+ class="strct-sm__item"
7768
+ [class.strct-sm__item--active]="item.id === activeId()"
7769
+ [disabled]="item.disabled"
7770
+ [attr.aria-current]="item.id === activeId() ? 'page' : null"
7771
+ (click)="pick(item)"
7772
+ >
7773
+ <ng-container *ngTemplateOutlet="rowContent; context: { $implicit: item }" />
7774
+ </button>
7775
+ }
7776
+ </ng-template>
7777
+
7197
7778
  <nav class="strct-sm" [attr.aria-label]="ariaLabel()">
7198
7779
  @for (sec of sections(); track sec.label) {
7199
7780
  <div class="strct-sm__section" [class.strct-sm__section--collapsible]="collapsible()">
@@ -7224,50 +7805,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7224
7805
  @if (isOpen(sec)) {
7225
7806
  <div class="strct-sm__items" role="group">
7226
7807
  @for (item of sec.items; track item.id) {
7227
- <button
7228
- type="button"
7229
- class="strct-sm__item"
7230
- [class.strct-sm__item--active]="item.id === activeId()"
7231
- [disabled]="item.disabled"
7232
- [attr.aria-current]="item.id === activeId() ? 'page' : null"
7233
- (click)="pick(item)"
7234
- >
7235
- @if (showIcons() && item.icon) {
7236
- <strct-icon
7237
- class="strct-sm__item-icon"
7238
- [name]="item.icon"
7239
- [size]="16"
7240
- [strokeWidth]="1.4"
7241
- />
7242
- }
7243
- <span class="strct-sm__item-label">{{ item.label }}</span>
7244
- @if (item.trailingIcon) {
7245
- <strct-icon
7246
- class="strct-sm__trailing"
7247
- [name]="item.trailingIcon"
7248
- [size]="14"
7249
- [strokeWidth]="1.5"
7250
- />
7251
- }
7252
- @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {
7253
- <span
7254
- class="strct-sm__badge strct-sm__badge--{{ item.badgeStatus ?? 'accent' }}"
7255
- >
7256
- {{ item.badge }}
7257
- </span>
7258
- } @else if (item.dot) {
7259
- <span
7260
- class="strct-sm__dot strct-sm__dot--{{ item.dotStatus ?? 'accent' }}"
7261
- ></span>
7262
- }
7263
- </button>
7808
+ <ng-container *ngTemplateOutlet="row; context: { $implicit: item }" />
7264
7809
  }
7265
7810
  </div>
7266
7811
  }
7267
7812
  </div>
7268
7813
  }
7269
7814
  </nav>
7270
- `, host: { class: 'strct-sm-host' }, styles: [".strct-sm{display:flex;flex-direction:column;gap:1px}.strct-sm__section--collapsible+.strct-sm__section--collapsible{margin-top:1px}.strct-sm__cat{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--t2);font-size:13px;font-weight:500;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease,color .13s ease}.strct-sm__cat:hover{background:var(--bg-3);color:var(--t1)}.strct-sm__cat:focus-visible,.strct-sm__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-sm__chevron{display:inline-flex;color:var(--t3);flex-shrink:0;transition:transform .15s ease}.strct-sm__chevron--open{transform:rotate(90deg)}.strct-sm__cat-icon{color:var(--t3);flex-shrink:0}.strct-sm__cat-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__cat-static{padding:11px 10px 5px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--t3)}.strct-sm__items{display:flex;flex-direction:column}.strct-sm__item{display:flex;align-items:center;gap:9px;width:100%;padding:7px 10px;border:0;border-radius:6px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease}.strct-sm__section--collapsible .strct-sm__item{padding-inline-start:31px}.strct-sm__item:hover:not(:disabled){background:var(--bg-3)}.strct-sm__item--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-sm__item--active .strct-sm__item-icon{color:var(--acc)}.strct-sm__item:disabled{opacity:.5;cursor:not-allowed}.strct-sm__item-icon{color:var(--t2);flex-shrink:0}.strct-sm__item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__trailing{flex-shrink:0;color:var(--t3)}.strct-sm__badge{flex-shrink:0;min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;line-height:1;border-radius:9px;font-variant-numeric:tabular-nums}.strct-sm__badge--accent{background:var(--acc-m);color:var(--acc)}.strct-sm__badge--success{background:var(--success-bg);color:var(--success)}.strct-sm__badge--warning{background:var(--warning-bg);color:var(--warning)}.strct-sm__badge--critical{background:var(--critical-bg);color:var(--critical)}.strct-sm__dot{flex-shrink:0;width:7px;height:7px;border-radius:50%}.strct-sm__dot--accent{background:var(--acc)}.strct-sm__dot--success{background:var(--success)}.strct-sm__dot--warning{background:var(--warning)}.strct-sm__dot--critical{background:var(--critical)}\n"] }]
7815
+ `, host: { class: 'strct-sm-host' }, styles: [".strct-sm{display:flex;flex-direction:column;gap:1px}.strct-sm__section--collapsible+.strct-sm__section--collapsible{margin-top:1px}.strct-sm__cat{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--t2);font-size:13px;font-weight:500;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease,color .13s ease}.strct-sm__cat:hover{background:var(--bg-3);color:var(--t1)}.strct-sm__cat:focus-visible,.strct-sm__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-sm__chevron{display:inline-flex;color:var(--t3);flex-shrink:0;transition:transform .15s ease}.strct-sm__chevron--open{transform:rotate(90deg)}.strct-sm__cat-icon{color:var(--t3);flex-shrink:0}.strct-sm__cat-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__cat-static{padding:11px 10px 5px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--t3)}.strct-sm__items{display:flex;flex-direction:column}.strct-sm__item{display:flex;align-items:center;gap:9px;width:100%;box-sizing:border-box;text-decoration:none;padding:7px 10px;border:0;border-radius:6px;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);cursor:pointer;text-align:start;transition:background .13s ease}.strct-sm__section--collapsible .strct-sm__item{padding-inline-start:31px}.strct-sm__item:hover:not(:disabled){background:var(--bg-3)}.strct-sm__item--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-sm__item--active .strct-sm__item-icon{color:var(--acc)}.strct-sm__item:disabled{opacity:.5;cursor:not-allowed}.strct-sm__item-icon{color:var(--t2);flex-shrink:0}.strct-sm__item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-sm__trailing{flex-shrink:0;color:var(--t3)}.strct-sm__badge{flex-shrink:0;min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;line-height:1;border-radius:9px;font-variant-numeric:tabular-nums}.strct-sm__badge--accent{background:var(--acc-m);color:var(--acc)}.strct-sm__badge--success{background:var(--success-bg);color:var(--success)}.strct-sm__badge--warning{background:var(--warning-bg);color:var(--warning)}.strct-sm__badge--critical{background:var(--critical-bg);color:var(--critical)}.strct-sm__dot{flex-shrink:0;width:7px;height:7px;border-radius:50%}.strct-sm__dot--accent{background:var(--acc)}.strct-sm__dot--success{background:var(--success)}.strct-sm__dot--warning{background:var(--warning)}.strct-sm__dot--critical{background:var(--critical)}\n"] }]
7271
7816
  }], propDecorators: { sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: true }] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }, { type: i0.Output, args: ["activeIdChange"] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], showIcons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcons", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
7272
7817
 
7273
7818
  /**
@@ -7324,6 +7869,12 @@ class StrctCommandPalette {
7324
7869
  * rendering.
7325
7870
  */
7326
7871
  filter = input(true, { ...(ngDevMode ? { debugName: "filter" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7872
+ /**
7873
+ * The order the sections are drawn in — the product you are in first, say.
7874
+ * Sections not named here follow, in the order their first result arrives.
7875
+ */
7876
+ sectionOrder = input([], /* @ts-ignore */
7877
+ ...(ngDevMode ? [{ debugName: "sectionOrder" }] : /* istanbul ignore next */ []));
7327
7878
  /** Show a "searching" row while server results are in flight. */
7328
7879
  loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7329
7880
  /** Localizable text of the loading row. */
@@ -7344,19 +7895,39 @@ class StrctCommandPalette {
7344
7895
  optionId(i) {
7345
7896
  return `${this.listId}-opt-${i}`;
7346
7897
  }
7898
+ /**
7899
+ * The results in their drawn order, chunked into sections. The flat index
7900
+ * each row carries is the one the keyboard and `aria-activedescendant`
7901
+ * use, so what the arrows do and what the eye sees stay the same list.
7902
+ */
7903
+ sections = computed(() => {
7904
+ const out = [];
7905
+ this.filtered().forEach((item, index) => {
7906
+ const label = item.section ?? '';
7907
+ const last = out[out.length - 1];
7908
+ if (last && last.label === label)
7909
+ last.rows.push({ item, index });
7910
+ else
7911
+ out.push({ label, id: `${this.listId}-sec-${out.length}`, rows: [{ item, index }] });
7912
+ });
7913
+ return out;
7914
+ }, /* @ts-ignore */
7915
+ ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
7347
7916
  /** Rank: label prefix > word-start > substring; group/keyword matches last. */
7348
7917
  filtered = computed(() => {
7349
7918
  const items = this.items();
7350
7919
  // Server-backed mode: the caller already filtered — render as given.
7351
7920
  if (!this.filter())
7352
- return items.slice(0, this.maxResults());
7921
+ return this.bySection(items.slice(0, this.maxResults()));
7353
7922
  const q = this.query().trim().toLowerCase();
7354
7923
  if (!q)
7355
- return items.slice(0, this.maxResults());
7924
+ return this.bySection(items.slice(0, this.maxResults()));
7356
7925
  const scored = [];
7357
7926
  for (const item of items) {
7358
7927
  const label = item.label.toLowerCase();
7359
- const hay = [item.group ?? '', ...(item.keywords ?? [])].join(' ').toLowerCase();
7928
+ const hay = [item.group ?? '', item.section ?? '', ...(item.keywords ?? [])]
7929
+ .join(' ')
7930
+ .toLowerCase();
7360
7931
  let score = -1;
7361
7932
  if (label.startsWith(q))
7362
7933
  score = 0;
@@ -7369,12 +7940,30 @@ class StrctCommandPalette {
7369
7940
  if (score >= 0)
7370
7941
  scored.push({ item, score });
7371
7942
  }
7372
- return scored
7943
+ return this.bySection(scored
7373
7944
  .sort((a, b) => a.score - b.score)
7374
7945
  .slice(0, this.maxResults())
7375
- .map((s) => s.item);
7946
+ .map((s) => s.item));
7376
7947
  }, /* @ts-ignore */
7377
7948
  ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
7949
+ /**
7950
+ * Keeps each section's results together, in `sectionOrder` where it is
7951
+ * given and otherwise in the order the sections first appear. The sort is
7952
+ * stable, so how well a result matched still orders it inside its section.
7953
+ */
7954
+ bySection(list) {
7955
+ const rank = new Map();
7956
+ this.sectionOrder().forEach((name, i) => rank.set(name, i));
7957
+ let next = rank.size;
7958
+ for (const item of list) {
7959
+ const key = item.section ?? '';
7960
+ if (!rank.has(key))
7961
+ rank.set(key, next++);
7962
+ }
7963
+ if (rank.size < 2)
7964
+ return list;
7965
+ return [...list].sort((a, b) => rank.get(a.section ?? '') - rank.get(b.section ?? ''));
7966
+ }
7378
7967
  constructor() {
7379
7968
  // Async results can shrink under the cursor — keep the active option in range.
7380
7969
  effect(() => {
@@ -7453,7 +8042,7 @@ class StrctCommandPalette {
7453
8042
  this.open.set(false);
7454
8043
  }
7455
8044
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctCommandPalette, deps: [], target: i0.ɵɵFactoryTarget.Component });
7456
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctCommandPalette, isStandalone: true, selector: "strct-command-palette", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", picked: "picked", query: "queryChange" }, host: { listeners: { "document:keydown": "onGlobalKey($event)" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
8045
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctCommandPalette, isStandalone: true, selector: "strct-command-palette", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, sectionOrder: { classPropertyName: "sectionOrder", publicName: "sectionOrder", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", picked: "picked", query: "queryChange" }, host: { listeners: { "document:keydown": "onGlobalKey($event)" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
7457
8046
  @if (open()) {
7458
8047
  <!-- Backdrop: pointer-only dismiss (Escape covers keyboard). -->
7459
8048
  <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
@@ -7489,34 +8078,61 @@ class StrctCommandPalette {
7489
8078
  <span class="strct-cmdp__spin" aria-hidden="true"></span>{{ loadingText() }}
7490
8079
  </div>
7491
8080
  }
7492
- @for (item of filtered(); track item.id; let i = $index) {
7493
- <button
7494
- type="button"
7495
- class="strct-cmdp__item"
7496
- role="option"
7497
- [id]="optionId(i)"
7498
- [attr.aria-selected]="i === activeIndex()"
7499
- [class.is-active]="i === activeIndex()"
7500
- tabindex="-1"
7501
- (click)="pick(item)"
7502
- (mousemove)="activeIndex.set(i)"
8081
+ <!-- A section is a group with a heading, not an option: the
8082
+ heading is never active and never picked, and the arrows
8083
+ move through every option across every section. -->
8084
+ @for (section of sections(); track section.id) {
8085
+ <div
8086
+ class="strct-cmdp__sec"
8087
+ role="group"
8088
+ [attr.aria-labelledby]="section.label ? section.id : null"
7503
8089
  >
7504
- @if (item.icon) {
7505
- <strct-icon
7506
- class="strct-cmdp__icon"
7507
- [name]="item.icon"
7508
- [size]="16"
7509
- [strokeWidth]="1.4"
7510
- />
8090
+ @if (section.label) {
8091
+ <div class="strct-cmdp__section" [id]="section.id">{{ section.label }}</div>
7511
8092
  }
7512
- <span class="strct-cmdp__label">{{ item.label }}</span>
7513
- @if (item.hint) {
7514
- <strct-kbd>{{ item.hint }}</strct-kbd>
7515
- }
7516
- @if (item.group) {
7517
- <span class="strct-cmdp__group">{{ item.group }}</span>
8093
+ @for (row of section.rows; track row.item.id) {
8094
+ <button
8095
+ type="button"
8096
+ class="strct-cmdp__item"
8097
+ role="option"
8098
+ [id]="optionId(row.index)"
8099
+ [attr.aria-selected]="row.index === activeIndex()"
8100
+ [class.is-active]="row.index === activeIndex()"
8101
+ tabindex="-1"
8102
+ (click)="pick(row.item)"
8103
+ (mousemove)="activeIndex.set(row.index)"
8104
+ >
8105
+ @if (row.item.icon) {
8106
+ <strct-icon
8107
+ class="strct-cmdp__icon"
8108
+ [name]="row.item.icon"
8109
+ [size]="16"
8110
+ [strokeWidth]="1.4"
8111
+ />
8112
+ }
8113
+ <span class="strct-cmdp__text">
8114
+ <span class="strct-cmdp__label">{{ row.item.label }}</span>
8115
+ @if (row.item.description) {
8116
+ <span class="strct-cmdp__desc">{{ row.item.description }}</span>
8117
+ }
8118
+ </span>
8119
+ @if (row.item.hint) {
8120
+ <strct-kbd>{{ row.item.hint }}</strct-kbd>
8121
+ }
8122
+ @if (row.item.badge; as badge) {
8123
+ <strct-badge
8124
+ class="strct-cmdp__badge"
8125
+ [status]="badge.status ?? 'neutral'"
8126
+ [tone]="badge.tone ?? ''"
8127
+ >{{ badge.label }}</strct-badge
8128
+ >
8129
+ }
8130
+ @if (row.item.group) {
8131
+ <span class="strct-cmdp__group">{{ row.item.group }}</span>
8132
+ }
8133
+ </button>
7518
8134
  }
7519
- </button>
8135
+ </div>
7520
8136
  } @empty {
7521
8137
  @if (!loading()) {
7522
8138
  <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
@@ -7526,11 +8142,11 @@ class StrctCommandPalette {
7526
8142
  </div>
7527
8143
  </div>
7528
8144
  }
7529
- `, isInline: true, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:var(--z-toast);display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
8145
+ `, isInline: true, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:var(--z-toast);display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex:0 20 auto;min-width:0;max-width:55%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cmdp__text{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}.strct-cmdp__desc{font-size:var(--text-sm);color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cmdp__badge{flex:none}.strct-cmdp__section{padding:8px 12px 4px;font-size:var(--text-xs, 11px);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--t3)}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel", "count", "countMax", "countStatus", "countLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }, { kind: "component", type: StrctBadge, selector: "strct-badge", inputs: ["status", "tone", "solid"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7530
8146
  }
7531
8147
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctCommandPalette, decorators: [{
7532
8148
  type: Component,
7533
- args: [{ selector: 'strct-command-palette', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctKbd], host: { '(document:keydown)': 'onGlobalKey($event)' }, template: `
8149
+ args: [{ selector: 'strct-command-palette', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctKbd, StrctBadge], host: { '(document:keydown)': 'onGlobalKey($event)' }, template: `
7534
8150
  @if (open()) {
7535
8151
  <!-- Backdrop: pointer-only dismiss (Escape covers keyboard). -->
7536
8152
  <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
@@ -7566,34 +8182,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7566
8182
  <span class="strct-cmdp__spin" aria-hidden="true"></span>{{ loadingText() }}
7567
8183
  </div>
7568
8184
  }
7569
- @for (item of filtered(); track item.id; let i = $index) {
7570
- <button
7571
- type="button"
7572
- class="strct-cmdp__item"
7573
- role="option"
7574
- [id]="optionId(i)"
7575
- [attr.aria-selected]="i === activeIndex()"
7576
- [class.is-active]="i === activeIndex()"
7577
- tabindex="-1"
7578
- (click)="pick(item)"
7579
- (mousemove)="activeIndex.set(i)"
8185
+ <!-- A section is a group with a heading, not an option: the
8186
+ heading is never active and never picked, and the arrows
8187
+ move through every option across every section. -->
8188
+ @for (section of sections(); track section.id) {
8189
+ <div
8190
+ class="strct-cmdp__sec"
8191
+ role="group"
8192
+ [attr.aria-labelledby]="section.label ? section.id : null"
7580
8193
  >
7581
- @if (item.icon) {
7582
- <strct-icon
7583
- class="strct-cmdp__icon"
7584
- [name]="item.icon"
7585
- [size]="16"
7586
- [strokeWidth]="1.4"
7587
- />
7588
- }
7589
- <span class="strct-cmdp__label">{{ item.label }}</span>
7590
- @if (item.hint) {
7591
- <strct-kbd>{{ item.hint }}</strct-kbd>
8194
+ @if (section.label) {
8195
+ <div class="strct-cmdp__section" [id]="section.id">{{ section.label }}</div>
7592
8196
  }
7593
- @if (item.group) {
7594
- <span class="strct-cmdp__group">{{ item.group }}</span>
8197
+ @for (row of section.rows; track row.item.id) {
8198
+ <button
8199
+ type="button"
8200
+ class="strct-cmdp__item"
8201
+ role="option"
8202
+ [id]="optionId(row.index)"
8203
+ [attr.aria-selected]="row.index === activeIndex()"
8204
+ [class.is-active]="row.index === activeIndex()"
8205
+ tabindex="-1"
8206
+ (click)="pick(row.item)"
8207
+ (mousemove)="activeIndex.set(row.index)"
8208
+ >
8209
+ @if (row.item.icon) {
8210
+ <strct-icon
8211
+ class="strct-cmdp__icon"
8212
+ [name]="row.item.icon"
8213
+ [size]="16"
8214
+ [strokeWidth]="1.4"
8215
+ />
8216
+ }
8217
+ <span class="strct-cmdp__text">
8218
+ <span class="strct-cmdp__label">{{ row.item.label }}</span>
8219
+ @if (row.item.description) {
8220
+ <span class="strct-cmdp__desc">{{ row.item.description }}</span>
8221
+ }
8222
+ </span>
8223
+ @if (row.item.hint) {
8224
+ <strct-kbd>{{ row.item.hint }}</strct-kbd>
8225
+ }
8226
+ @if (row.item.badge; as badge) {
8227
+ <strct-badge
8228
+ class="strct-cmdp__badge"
8229
+ [status]="badge.status ?? 'neutral'"
8230
+ [tone]="badge.tone ?? ''"
8231
+ >{{ badge.label }}</strct-badge
8232
+ >
8233
+ }
8234
+ @if (row.item.group) {
8235
+ <span class="strct-cmdp__group">{{ row.item.group }}</span>
8236
+ }
8237
+ </button>
7595
8238
  }
7596
- </button>
8239
+ </div>
7597
8240
  } @empty {
7598
8241
  @if (!loading()) {
7599
8242
  <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
@@ -7603,8 +8246,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
7603
8246
  </div>
7604
8247
  </div>
7605
8248
  }
7606
- `, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:var(--z-toast);display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"] }]
7607
- }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }] } });
8249
+ `, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:var(--z-toast);display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:var(--backdrop);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex:0 20 auto;min-width:0;max-width:55%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cmdp__text{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}.strct-cmdp__desc{font-size:var(--text-sm);color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cmdp__badge{flex:none}.strct-cmdp__section{padding:8px 12px 4px;font-size:var(--text-xs, 11px);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--t3)}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"] }]
8250
+ }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], sectionOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionOrder", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }] } });
7608
8251
 
7609
8252
  /**
7610
8253
  * Compact search pill — the pattern from the docs header: a leading search
@@ -29449,5 +30092,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
29449
30092
  * Generated bundle index. Do not edit.
29450
30093
  */
29451
30094
 
29452
- export { FOCUSABLE_SELECTOR, STRCT_DP_DOW, STRCT_DP_DOW_FULL, STRCT_DP_MONTHS, STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_MD_LIMITS, STRCT_NOTIFICATION_HISTORY_LIMIT, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAlertActions, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCardHeaderActions, StrctCardHeaderLeading, StrctCardHeaderMeta, StrctCardHeaderNote, StrctCascadeColumn, StrctCascadeHost, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChange, StrctChart, StrctChatAttachment, StrctChatComposer, StrctChatMessage, StrctChatMessageActions, StrctChatThread, StrctCheckbox, StrctChips, StrctCode, StrctCodeInline, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctConfirmOutlet, StrctConfirmService, StrctContextMenu, StrctContextMenuTrigger, StrctControlDescription, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatagridActionBarCaption, StrctDatepicker, StrctDatetimePicker, StrctDesc, StrctDescriptionList, StrctDetailPaneActions, StrctDetailPaneMeta, StrctDetailPaneTitle, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownItemAction, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFieldGroup, StrctFieldHint, StrctFieldPrefix, StrctFieldSuffix, StrctFieldValue, StrctFile, StrctFilterBar, StrctFlow, StrctFlowNodeTemplate, StrctFooter, StrctGauge, StrctGroupHeaderDef, StrctHeader, StrctHeatmap, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInlineEdit, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLegend, StrctList, StrctListItem, StrctListItemDescription, StrctListItemLeading, StrctListItemMeta, StrctListItemTrailing, StrctLiveIndicator, StrctLogViewer, StrctLogin, StrctMarkdown, StrctMarkdownToken, StrctMediaFrame, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctModalContent, StrctNav, StrctNavItem, StrctNotificationCenter, StrctNumber, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPageHeaderTitleMeta, StrctPagination, StrctPassword, StrctPopover, StrctPopoverTrigger, StrctProgress, StrctQr, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderGroup, StrctReorderHandle, StrctReorderItem, StrctResizeHandle, StrctRowDetailDef, StrctSearchbox, StrctSectionHeader, StrctSectionHeaderActions, StrctSectionHeaderMeta, StrctSectionMenu, StrctSegmented, StrctSelect, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSignpostTrigger, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStatusDot, StrctStep, StrctStepAction, StrctSteps, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctTagLeading, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctToolbar, StrctToolbarCaption, StrctToolbarSpacer, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctTreeSelect, StrctVerticalNav, StrctWatermark, StrctWindow, StrctWindowDock, StrctWindowDockItem, StrctWindowService, StrctWizard, StrctWizardAside, __qrInternals, focusFirstIn, keepTabInside, lockBodyScroll, parseAnsi, parseInlines, provideStrctWizardDefaults, registerStrctIcon, restoreFocus, saveFocusedElement, strctCheckHostDisplay, strctCheckHostInputs, strctClassifyHref, strctComputeDiff, strctDpPad, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctParseMarkdown, strctQrMatrix, strctValidationIcon, strctValidationTone, unlockBodyScroll };
30095
+ export { FOCUSABLE_SELECTOR, STRCT_DP_DOW, STRCT_DP_DOW_FULL, STRCT_DP_MONTHS, STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_MD_LIMITS, STRCT_NOTIFICATION_HISTORY_LIMIT, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAlertActions, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCardHeaderActions, StrctCardHeaderLeading, StrctCardHeaderMeta, StrctCardHeaderNote, StrctCascadeColumn, StrctCascadeHost, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChange, StrctChart, StrctChatAttachment, StrctChatComposer, StrctChatMessage, StrctChatMessageActions, StrctChatThread, StrctCheckbox, StrctChips, StrctCode, StrctCodeInline, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctConfirmOutlet, StrctConfirmService, StrctContextMenu, StrctContextMenuTrigger, StrctControlDescription, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatagridActionBarCaption, StrctDatepicker, StrctDatetimePicker, StrctDesc, StrctDescriptionList, StrctDetailPaneActions, StrctDetailPaneMeta, StrctDetailPaneTitle, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownGroup, StrctDropdownItem, StrctDropdownItemAction, StrctDropdownItemDescription, StrctDropdownItemIcon, StrctDropdownItemLabel, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFieldGroup, StrctFieldHint, StrctFieldPrefix, StrctFieldSuffix, StrctFieldValue, StrctFile, StrctFilterBar, StrctFlow, StrctFlowNodeTemplate, StrctFooter, StrctGauge, StrctGroupHeaderDef, StrctHeader, StrctHeaderAction, StrctHeatmap, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInlineEdit, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLegend, StrctList, StrctListItem, StrctListItemDescription, StrctListItemLeading, StrctListItemMeta, StrctListItemTrailing, StrctLiveIndicator, StrctLogViewer, StrctLogin, StrctMarkdown, StrctMarkdownToken, StrctMediaFrame, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctModalContent, StrctNav, StrctNavItem, StrctNotificationCenter, StrctNumber, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPageHeaderTitleMeta, StrctPagination, StrctPassword, StrctPopover, StrctPopoverTrigger, StrctProgress, StrctQr, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderGroup, StrctReorderHandle, StrctReorderItem, StrctResizeHandle, StrctRowDetailDef, StrctSearchbox, StrctSectionHeader, StrctSectionHeaderActions, StrctSectionHeaderMeta, StrctSectionMenu, StrctSegmented, StrctSelect, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSignpostTrigger, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStatusDot, StrctStep, StrctStepAction, StrctSteps, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctTagLeading, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctToolbar, StrctToolbarCaption, StrctToolbarSpacer, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctTreeSelect, StrctVerticalNav, StrctWatermark, StrctWindow, StrctWindowDock, StrctWindowDockItem, StrctWindowService, StrctWizard, StrctWizardAside, __qrInternals, focusFirstIn, keepTabInside, lockBodyScroll, parseAnsi, parseInlines, provideStrctWizardDefaults, registerStrctIcon, restoreFocus, saveFocusedElement, strctCheckHostDisplay, strctCheckHostInputs, strctClassifyHref, strctComputeDiff, strctDpPad, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctParseMarkdown, strctQrMatrix, strctValidationIcon, strctValidationTone, unlockBodyScroll };
29453
30096
  //# sourceMappingURL=akcelik-strct.mjs.map