@akcelik/strct 0.3.0 → 0.4.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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, booleanAttribute, output, HostListener, model, contentChildren, effect, DestroyRef, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, TemplateRef, contentChild, Renderer2 } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, booleanAttribute, output, HostListener, model, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, DestroyRef, forwardRef, TemplateRef, contentChild, Renderer2 } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { DOCUMENT as DOCUMENT$1, NgTemplateOutlet } from '@angular/common';
5
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
@@ -1130,6 +1130,336 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1130
1130
  `, host: { class: 'strct-tabs' }, styles: [".strct-tabs{display:block}.strct-tabs__bar{display:flex;gap:2px;border-bottom:1px solid var(--b2)}.strct-tabs__btn{appearance:none;border:0;background:transparent;cursor:pointer;font-family:var(--font);font-size:13px;font-weight:500;color:var(--t2);padding:9px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .14s ease,border-color .14s ease}.strct-tabs__btn:hover{color:var(--t1)}.strct-tabs__btn--active{color:var(--acc);border-bottom-color:var(--acc)}.strct-tabs__btn:disabled{color:var(--t4);cursor:not-allowed}.strct-tabs__panels{padding-top:16px}\n"] }]
1131
1131
  }], ctorParameters: () => [], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTab), { isSignal: true }] }] } });
1132
1132
 
1133
+ /**
1134
+ * Floating menu panel — portaled into `<body>` (so it escapes overflow /
1135
+ * transform clipping), positioned by its real measured size, with full keyboard
1136
+ * navigation and recursive submenus. Usually created by `[strctContextMenu]`,
1137
+ * but can be embedded directly with `submenu`.
1138
+ */
1139
+ class StrctMenuPanel {
1140
+ host = inject(ElementRef);
1141
+ items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1142
+ data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1143
+ x = input(0, ...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
1144
+ y = input(0, ...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
1145
+ submenu = input(false, { ...(ngDevMode ? { debugName: "submenu" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1146
+ select = output();
1147
+ close = output();
1148
+ /** ArrowLeft inside a submenu — asks the parent to close it. */
1149
+ back = output();
1150
+ posX = signal(0, ...(ngDevMode ? [{ debugName: "posX" }] : /* istanbul ignore next */ []));
1151
+ posY = signal(0, ...(ngDevMode ? [{ debugName: "posY" }] : /* istanbul ignore next */ []));
1152
+ flipLeft = signal(false, ...(ngDevMode ? [{ debugName: "flipLeft" }] : /* istanbul ignore next */ []));
1153
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1154
+ openSubIndex = signal(null, ...(ngDevMode ? [{ debugName: "openSubIndex" }] : /* istanbul ignore next */ []));
1155
+ navIndices = computed(() => this.items()
1156
+ .map((it, i) => (it.divider ? -1 : i))
1157
+ .filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "navIndices" }] : /* istanbul ignore next */ []));
1158
+ constructor() {
1159
+ this.posX.set(this.x());
1160
+ this.posY.set(this.y());
1161
+ afterNextRender(() => {
1162
+ this.activeIndex.set(this.navIndices()[0] ?? 0);
1163
+ if (!this.submenu())
1164
+ this.clampToViewport();
1165
+ this.focusItem(this.activeIndex());
1166
+ });
1167
+ }
1168
+ clampToViewport() {
1169
+ const host = this.host.nativeElement;
1170
+ const w = host.offsetWidth;
1171
+ const h = host.offsetHeight;
1172
+ const vw = window.innerWidth;
1173
+ const vh = window.innerHeight;
1174
+ const m = 6;
1175
+ let nx = this.x();
1176
+ let ny = this.y();
1177
+ if (nx + w > vw - m)
1178
+ nx = Math.max(m, Math.min(this.x() - w, vw - w - m));
1179
+ if (ny + h > vh - m)
1180
+ ny = Math.max(m, vh - h - m);
1181
+ this.posX.set(nx);
1182
+ this.posY.set(ny);
1183
+ // Submenus of a panel near the right edge open to the left.
1184
+ this.flipLeft.set(nx + w > vw - 220);
1185
+ }
1186
+ focusItem(i) {
1187
+ this.activeIndex.set(i);
1188
+ this.host.nativeElement
1189
+ .querySelector(`.strct-menu__item[data-idx="${i}"]`)
1190
+ ?.focus();
1191
+ }
1192
+ move(dir) {
1193
+ const nav = this.navIndices();
1194
+ if (!nav.length)
1195
+ return;
1196
+ const pos = nav.indexOf(this.activeIndex());
1197
+ const next = nav[(pos + dir + nav.length) % nav.length];
1198
+ this.openSubIndex.set(null);
1199
+ this.focusItem(next);
1200
+ }
1201
+ onHover(i) {
1202
+ this.activeIndex.set(i);
1203
+ const it = this.items()[i];
1204
+ this.openSubIndex.set(it?.children?.length ? i : null);
1205
+ }
1206
+ onLeave(i) {
1207
+ if (this.openSubIndex() === i)
1208
+ this.openSubIndex.set(null);
1209
+ }
1210
+ onItemClick(item, i, event) {
1211
+ event.stopPropagation();
1212
+ if (item.disabled)
1213
+ return;
1214
+ if (item.children?.length) {
1215
+ this.openSubIndex.set(this.openSubIndex() === i ? null : i);
1216
+ this.focusItem(i);
1217
+ }
1218
+ else {
1219
+ this.select.emit(item);
1220
+ }
1221
+ }
1222
+ closeSub() {
1223
+ this.openSubIndex.set(null);
1224
+ }
1225
+ onKeydown(event) {
1226
+ const key = event.key;
1227
+ const item = this.items()[this.activeIndex()];
1228
+ switch (key) {
1229
+ case 'ArrowDown':
1230
+ event.preventDefault();
1231
+ event.stopPropagation();
1232
+ this.move(1);
1233
+ break;
1234
+ case 'ArrowUp':
1235
+ event.preventDefault();
1236
+ event.stopPropagation();
1237
+ this.move(-1);
1238
+ break;
1239
+ case 'Home':
1240
+ event.preventDefault();
1241
+ event.stopPropagation();
1242
+ this.focusItem(this.navIndices()[0] ?? 0);
1243
+ break;
1244
+ case 'End':
1245
+ event.preventDefault();
1246
+ event.stopPropagation();
1247
+ this.focusItem(this.navIndices().at(-1) ?? 0);
1248
+ break;
1249
+ case 'ArrowRight':
1250
+ if (item?.children?.length) {
1251
+ event.preventDefault();
1252
+ event.stopPropagation();
1253
+ this.openSubIndex.set(this.activeIndex());
1254
+ }
1255
+ break;
1256
+ case 'ArrowLeft':
1257
+ event.preventDefault();
1258
+ event.stopPropagation();
1259
+ if (this.openSubIndex() != null)
1260
+ this.closeSub();
1261
+ else if (this.submenu())
1262
+ this.back.emit();
1263
+ break;
1264
+ case 'Enter':
1265
+ case ' ':
1266
+ event.preventDefault();
1267
+ event.stopPropagation();
1268
+ if (item && !item.disabled) {
1269
+ if (item.children?.length)
1270
+ this.openSubIndex.set(this.activeIndex());
1271
+ else
1272
+ this.select.emit(item);
1273
+ }
1274
+ break;
1275
+ case 'Escape':
1276
+ event.preventDefault();
1277
+ event.stopPropagation();
1278
+ this.close.emit();
1279
+ break;
1280
+ }
1281
+ }
1282
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
1283
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctMenuPanel, isStandalone: true, selector: "strct-menu-panel", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: false, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, submenu: { classPropertyName: "submenu", publicName: "submenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", close: "close", back: "back" }, host: { properties: { "style.position": "submenu() ? null : 'fixed'", "style.left.px": "submenu() ? null : posX()", "style.top.px": "submenu() ? null : posY()", "style.zIndex": "submenu() ? null : 1100" }, classAttribute: "strct-menu-host" }, ngImport: i0, template: `
1284
+ <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1285
+ @for (item of items(); track $index; let i = $index) {
1286
+ @if (item.divider) {
1287
+ <div class="strct-menu__sep" role="separator"></div>
1288
+ } @else {
1289
+ <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1290
+ <button
1291
+ type="button"
1292
+ class="strct-menu__item"
1293
+ [attr.data-idx]="i"
1294
+ [class.strct-menu__item--danger]="item.danger"
1295
+ [class.strct-menu__item--active]="i === activeIndex()"
1296
+ [disabled]="item.disabled"
1297
+ role="menuitem"
1298
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1299
+ [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1300
+ [attr.tabindex]="i === activeIndex() ? 0 : -1"
1301
+ (click)="onItemClick(item, i, $event)"
1302
+ >
1303
+ @if (item.icon) {
1304
+ <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1305
+ } @else {
1306
+ <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1307
+ }
1308
+ <span class="strct-menu__label">{{ item.label }}</span>
1309
+ @if (item.children?.length) {
1310
+ <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1311
+ }
1312
+ </button>
1313
+ @if (openSubIndex() === i && item.children?.length) {
1314
+ <strct-menu-panel
1315
+ submenu
1316
+ class="strct-menu__subpanel"
1317
+ [class.strct-menu__subpanel--flip]="flipLeft()"
1318
+ [items]="item.children!"
1319
+ [data]="data()"
1320
+ (select)="select.emit($event)"
1321
+ (close)="close.emit()"
1322
+ (back)="closeSub(); focusItem(i)"
1323
+ />
1324
+ }
1325
+ </div>
1326
+ }
1327
+ }
1328
+ </div>
1329
+ `, isInline: true, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"], dependencies: [{ kind: "component", type: StrctMenuPanel, selector: "strct-menu-panel", inputs: ["items", "data", "x", "y", "submenu"], outputs: ["select", "close", "back"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1330
+ }
1331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, decorators: [{
1332
+ type: Component,
1333
+ args: [{ selector: 'strct-menu-panel', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1334
+ <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1335
+ @for (item of items(); track $index; let i = $index) {
1336
+ @if (item.divider) {
1337
+ <div class="strct-menu__sep" role="separator"></div>
1338
+ } @else {
1339
+ <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1340
+ <button
1341
+ type="button"
1342
+ class="strct-menu__item"
1343
+ [attr.data-idx]="i"
1344
+ [class.strct-menu__item--danger]="item.danger"
1345
+ [class.strct-menu__item--active]="i === activeIndex()"
1346
+ [disabled]="item.disabled"
1347
+ role="menuitem"
1348
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1349
+ [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1350
+ [attr.tabindex]="i === activeIndex() ? 0 : -1"
1351
+ (click)="onItemClick(item, i, $event)"
1352
+ >
1353
+ @if (item.icon) {
1354
+ <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1355
+ } @else {
1356
+ <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1357
+ }
1358
+ <span class="strct-menu__label">{{ item.label }}</span>
1359
+ @if (item.children?.length) {
1360
+ <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1361
+ }
1362
+ </button>
1363
+ @if (openSubIndex() === i && item.children?.length) {
1364
+ <strct-menu-panel
1365
+ submenu
1366
+ class="strct-menu__subpanel"
1367
+ [class.strct-menu__subpanel--flip]="flipLeft()"
1368
+ [items]="item.children!"
1369
+ [data]="data()"
1370
+ (select)="select.emit($event)"
1371
+ (close)="close.emit()"
1372
+ (back)="closeSub(); focusItem(i)"
1373
+ />
1374
+ }
1375
+ </div>
1376
+ }
1377
+ }
1378
+ </div>
1379
+ `, host: {
1380
+ class: 'strct-menu-host',
1381
+ '[style.position]': "submenu() ? null : 'fixed'",
1382
+ '[style.left.px]': 'submenu() ? null : posX()',
1383
+ '[style.top.px]': 'submenu() ? null : posY()',
1384
+ '[style.zIndex]': 'submenu() ? null : 1100',
1385
+ }, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"] }]
1386
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: false }] }], y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], submenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenu", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], close: [{ type: i0.Output, args: ["close"] }], back: [{ type: i0.Output, args: ["back"] }] } });
1387
+ /**
1388
+ * Right-click (context) menu driven by a data array. Attach to any trigger; the
1389
+ * menu portals into `<body>` and runs each item's `action` on selection.
1390
+ * <div [strctContextMenu]="menuFor(host)" [strctContextMenuData]="host"
1391
+ * (menuSelect)="onPick($event)">…</div>
1392
+ */
1393
+ class StrctContextMenuTrigger {
1394
+ appRef = inject(ApplicationRef);
1395
+ envInjector = inject(EnvironmentInjector);
1396
+ zone = inject(NgZone);
1397
+ doc = inject(DOCUMENT$1);
1398
+ items = input.required({ ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), alias: 'strctContextMenu' });
1399
+ data = input(undefined, { ...(ngDevMode ? { debugName: "data" } : /* istanbul ignore next */ {}), alias: 'strctContextMenuData' });
1400
+ menuSelect = output();
1401
+ ref = null;
1402
+ onClose = () => this.zone.run(() => this.closeMenu());
1403
+ onContextMenu(event) {
1404
+ if (!this.items()?.length)
1405
+ return;
1406
+ event.preventDefault();
1407
+ this.openAt(event.clientX, event.clientY);
1408
+ }
1409
+ openAt(x, y) {
1410
+ this.closeMenu();
1411
+ const ref = createComponent(StrctMenuPanel, { environmentInjector: this.envInjector });
1412
+ ref.setInput('items', this.items());
1413
+ ref.setInput('data', this.data());
1414
+ ref.setInput('x', x);
1415
+ ref.setInput('y', y);
1416
+ ref.instance.select.subscribe((item) => {
1417
+ item.action?.(this.data());
1418
+ this.menuSelect.emit(item);
1419
+ this.closeMenu();
1420
+ });
1421
+ ref.instance.close.subscribe(() => this.closeMenu());
1422
+ this.appRef.attachView(ref.hostView);
1423
+ this.doc.body.appendChild(ref.location.nativeElement);
1424
+ this.ref = ref;
1425
+ // Defer global listeners so the opening right-click doesn't immediately close.
1426
+ setTimeout(() => {
1427
+ this.zone.runOutsideAngular(() => {
1428
+ this.doc.addEventListener('mousedown', this.onOutside, true);
1429
+ window.addEventListener('scroll', this.onClose, true);
1430
+ window.addEventListener('resize', this.onClose);
1431
+ });
1432
+ });
1433
+ }
1434
+ onOutside = (event) => {
1435
+ if (this.ref && !this.ref.location.nativeElement.contains(event.target)) {
1436
+ this.onClose();
1437
+ }
1438
+ };
1439
+ closeMenu() {
1440
+ if (!this.ref)
1441
+ return;
1442
+ this.doc.removeEventListener('mousedown', this.onOutside, true);
1443
+ window.removeEventListener('scroll', this.onClose, true);
1444
+ window.removeEventListener('resize', this.onClose);
1445
+ this.appRef.detachView(this.ref.hostView);
1446
+ this.ref.destroy();
1447
+ this.ref = null;
1448
+ }
1449
+ ngOnDestroy() {
1450
+ this.closeMenu();
1451
+ }
1452
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1453
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctContextMenuTrigger, isStandalone: true, selector: "[strctContextMenu]", inputs: { items: { classPropertyName: "items", publicName: "strctContextMenu", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "strctContextMenuData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuSelect: "menuSelect" }, host: { listeners: { "contextmenu": "onContextMenu($event)" } }, ngImport: i0 });
1454
+ }
1455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, decorators: [{
1456
+ type: Directive,
1457
+ args: [{ selector: '[strctContextMenu]' }]
1458
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenu", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenuData", required: false }] }], menuSelect: [{ type: i0.Output, args: ["menuSelect"] }], onContextMenu: [{
1459
+ type: HostListener,
1460
+ args: ['contextmenu', ['$event']]
1461
+ }] } });
1462
+
1133
1463
  /**
1134
1464
  * Tree node. Two modes:
1135
1465
  * - **Content:** nest `<strct-tree-node>` children manually.
@@ -1148,10 +1478,20 @@ class StrctTreeNode {
1148
1478
  badge = input('none', ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
1149
1479
  active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1150
1480
  expanded = model(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1481
+ /** Per-node menu resolver (data mode); bubbles down the recursion. */
1482
+ nodeMenu = input(null, ...(ngDevMode ? [{ debugName: "nodeMenu" }] : /* istanbul ignore next */ []));
1151
1483
  /** Content-mode click. */
1152
1484
  activated = output();
1153
1485
  /** Data-mode click — carries the activated node (bubbles to the tree). */
1154
1486
  nodeActivated = output();
1487
+ /** Data-mode right-click menu selection (bubbles to the tree). */
1488
+ nodeMenuSelect = output();
1489
+ /** Right-click menu items for this node ([] when no resolver / not data mode). */
1490
+ menuItems = computed(() => {
1491
+ const fn = this.nodeMenu();
1492
+ const n = this.node();
1493
+ return fn && n ? fn(n) : [];
1494
+ }, ...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
1155
1495
  childNodes = contentChildren(StrctTreeNode, ...(ngDevMode ? [{ debugName: "childNodes" }] : /* istanbul ignore next */ []));
1156
1496
  /** Data-mode expansion (seeded from node.expanded on first toggle). */
1157
1497
  dataExpanded = signal(null, ...(ngDevMode ? [{ debugName: "dataExpanded" }] : /* istanbul ignore next */ []));
@@ -1183,13 +1523,21 @@ class StrctTreeNode {
1183
1523
  else
1184
1524
  this.activated.emit();
1185
1525
  }
1526
+ onMenuSelect(item) {
1527
+ const n = this.node();
1528
+ if (n)
1529
+ this.nodeMenuSelect.emit({ node: n, item });
1530
+ }
1186
1531
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Component });
1187
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTreeNode, isStandalone: true, selector: "strct-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expanded: "expandedChange", activated: "activated", nodeActivated: "nodeActivated" }, host: { classAttribute: "strct-tnode" }, queries: [{ propertyName: "childNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
1532
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTreeNode, isStandalone: true, selector: "strct-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expanded: "expandedChange", activated: "activated", nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect" }, host: { classAttribute: "strct-tnode" }, queries: [{ propertyName: "childNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
1188
1533
  <div
1189
1534
  class="strct-tnode__row"
1190
1535
  [class.strct-tnode__row--active]="displayActive()"
1191
1536
  role="treeitem"
1192
1537
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1538
+ [strctContextMenu]="menuItems()"
1539
+ [strctContextMenuData]="node()"
1540
+ (menuSelect)="onMenuSelect($event)"
1193
1541
  (click)="onActivate()"
1194
1542
  >
1195
1543
  @if (hasChildren()) {
@@ -1219,23 +1567,31 @@ class StrctTreeNode {
1219
1567
  <div class="strct-tnode__children" role="group">
1220
1568
  @if (node()) {
1221
1569
  @for (child of node()!.children ?? []; track $index) {
1222
- <strct-tree-node [node]="child" (nodeActivated)="nodeActivated.emit($event)" />
1570
+ <strct-tree-node
1571
+ [node]="child"
1572
+ [nodeMenu]="nodeMenu()"
1573
+ (nodeActivated)="nodeActivated.emit($event)"
1574
+ (nodeMenuSelect)="nodeMenuSelect.emit($event)"
1575
+ />
1223
1576
  }
1224
1577
  } @else {
1225
1578
  <ng-content />
1226
1579
  }
1227
1580
  </div>
1228
1581
  }
1229
- `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:7px;padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;color:var(--t3);transition:transform .15s ease;width:14px;justify-content:center}.strct-tnode__chevron--open{transform:rotate(90deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-left:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded"], outputs: ["expandedChange", "activated", "nodeActivated"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1582
+ `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:7px;padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;color:var(--t3);transition:transform .15s ease;width:14px;justify-content:center}.strct-tnode__chevron--open{transform:rotate(90deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-left:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }, { kind: "directive", type: StrctContextMenuTrigger, selector: "[strctContextMenu]", inputs: ["strctContextMenu", "strctContextMenuData"], outputs: ["menuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1230
1583
  }
1231
1584
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, decorators: [{
1232
1585
  type: Component,
1233
- args: [{ selector: 'strct-tree-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1586
+ args: [{ selector: 'strct-tree-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctContextMenuTrigger], template: `
1234
1587
  <div
1235
1588
  class="strct-tnode__row"
1236
1589
  [class.strct-tnode__row--active]="displayActive()"
1237
1590
  role="treeitem"
1238
1591
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1592
+ [strctContextMenu]="menuItems()"
1593
+ [strctContextMenuData]="node()"
1594
+ (menuSelect)="onMenuSelect($event)"
1239
1595
  (click)="onActivate()"
1240
1596
  >
1241
1597
  @if (hasChildren()) {
@@ -1265,7 +1621,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1265
1621
  <div class="strct-tnode__children" role="group">
1266
1622
  @if (node()) {
1267
1623
  @for (child of node()!.children ?? []; track $index) {
1268
- <strct-tree-node [node]="child" (nodeActivated)="nodeActivated.emit($event)" />
1624
+ <strct-tree-node
1625
+ [node]="child"
1626
+ [nodeMenu]="nodeMenu()"
1627
+ (nodeActivated)="nodeActivated.emit($event)"
1628
+ (nodeMenuSelect)="nodeMenuSelect.emit($event)"
1629
+ />
1269
1630
  }
1270
1631
  } @else {
1271
1632
  <ng-content />
@@ -1273,7 +1634,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1273
1634
  </div>
1274
1635
  }
1275
1636
  `, host: { class: 'strct-tnode' }, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:7px;padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;color:var(--t3);transition:transform .15s ease;width:14px;justify-content:center}.strct-tnode__chevron--open{transform:rotate(90deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-left:16px}\n"] }]
1276
- }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], activated: [{ type: i0.Output, args: ["activated"] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }] } });
1637
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], activated: [{ type: i0.Output, args: ["activated"] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }] } });
1277
1638
  /**
1278
1639
  * Root container for a tree. Either project `<strct-tree-node>` children, or
1279
1640
  * pass `[nodes]` for a fully data-driven, self-recursing tree:
@@ -1282,31 +1643,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1282
1643
  class StrctTree {
1283
1644
  /** Data-driven node list; when set, projected content is ignored. */
1284
1645
  nodes = input(null, ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
1646
+ /** Per-node right-click menu resolver. */
1647
+ nodeMenu = input(null, ...(ngDevMode ? [{ debugName: "nodeMenu" }] : /* istanbul ignore next */ []));
1285
1648
  /** Emitted when any data-driven node is clicked. */
1286
1649
  nodeActivated = output();
1650
+ /** Emitted when a data-driven node's right-click menu item is chosen. */
1651
+ nodeMenuSelect = output();
1287
1652
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
1288
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeActivated: "nodeActivated" }, host: { attributes: { "role": "tree" }, classAttribute: "strct-tree" }, ngImport: i0, template: `
1653
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect" }, host: { attributes: { "role": "tree" }, classAttribute: "strct-tree" }, ngImport: i0, template: `
1289
1654
  @if (nodes(); as ns) {
1290
1655
  @for (n of ns; track $index) {
1291
- <strct-tree-node [node]="n" (nodeActivated)="nodeActivated.emit($event)" />
1656
+ <strct-tree-node
1657
+ [node]="n"
1658
+ [nodeMenu]="nodeMenu()"
1659
+ (nodeActivated)="nodeActivated.emit($event)"
1660
+ (nodeMenuSelect)="nodeMenuSelect.emit($event)"
1661
+ />
1292
1662
  }
1293
1663
  } @else {
1294
1664
  <ng-content />
1295
1665
  }
1296
- `, isInline: true, styles: [".strct-tree{display:block}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded"], outputs: ["expandedChange", "activated", "nodeActivated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1666
+ `, isInline: true, styles: [".strct-tree{display:block}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1297
1667
  }
1298
1668
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, decorators: [{
1299
1669
  type: Component,
1300
1670
  args: [{ selector: 'strct-tree', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctTreeNode], template: `
1301
1671
  @if (nodes(); as ns) {
1302
1672
  @for (n of ns; track $index) {
1303
- <strct-tree-node [node]="n" (nodeActivated)="nodeActivated.emit($event)" />
1673
+ <strct-tree-node
1674
+ [node]="n"
1675
+ [nodeMenu]="nodeMenu()"
1676
+ (nodeActivated)="nodeActivated.emit($event)"
1677
+ (nodeMenuSelect)="nodeMenuSelect.emit($event)"
1678
+ />
1304
1679
  }
1305
1680
  } @else {
1306
1681
  <ng-content />
1307
1682
  }
1308
1683
  `, host: { class: 'strct-tree', role: 'tree' }, styles: [".strct-tree{display:block}\n"] }]
1309
- }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }] } });
1684
+ }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }] } });
1310
1685
 
1311
1686
  let modalCounter = 0;
1312
1687
  // Body scroll-lock shared across any number of simultaneously open modals.
@@ -1663,434 +2038,104 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1663
2038
  }], propDecorators: { onDocClick: [{
1664
2039
  type: HostListener,
1665
2040
  args: ['document:click']
1666
- }], onEscape: [{
1667
- type: HostListener,
1668
- args: ['document:keydown.escape']
1669
- }], onResize: [{
1670
- type: HostListener,
1671
- args: ['window:resize']
1672
- }] } });
1673
-
1674
- /**
1675
- * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Opens on
1676
- * hover, click/tap, or the keyboard (Enter / Space / →), and flips to the left
1677
- * near the right edge of the viewport. Reuse `strct-dropdown-item` for entries.
1678
- * <strct-submenu label="Power">
1679
- * <strct-dropdown-item>Power on</strct-dropdown-item>
1680
- * <strct-dropdown-item>Power off</strct-dropdown-item>
1681
- * </strct-submenu>
1682
- */
1683
- class StrctSubmenu {
1684
- host = inject(ElementRef);
1685
- label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1686
- /** Optional leading icon; when omitted the icon column is still reserved so
1687
- * the label stays aligned with sibling items that do have icons. */
1688
- icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1689
- open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1690
- /** Open to the left when the fly-out would overflow the right edge. */
1691
- flip = signal(false, ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
1692
- setOpen(value) {
1693
- if (value) {
1694
- const rect = this.host.nativeElement.getBoundingClientRect();
1695
- this.flip.set(rect.right + 190 > window.innerWidth);
1696
- }
1697
- this.open.set(value);
1698
- }
1699
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1700
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSubmenu, isStandalone: true, selector: "strct-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-submenu-host" }, ngImport: i0, template: `
1701
- <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1702
- <div
1703
- class="strct-submenu__trigger"
1704
- role="menuitem"
1705
- tabindex="0"
1706
- aria-haspopup="menu"
1707
- [attr.aria-expanded]="open()"
1708
- (click)="$event.stopPropagation(); setOpen(!open())"
1709
- (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1710
- (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1711
- (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1712
- (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1713
- (keydown.escape)="$event.stopPropagation(); open.set(false)"
1714
- >
1715
- @if (icon()) {
1716
- <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1717
- } @else {
1718
- <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1719
- }
1720
- <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
1721
- <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1722
- </div>
1723
- @if (open()) {
1724
- <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1725
- <ng-content />
1726
- </div>
1727
- }
1728
- </div>
1729
- `, isInline: true, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1730
- }
1731
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, decorators: [{
1732
- type: Component,
1733
- args: [{ selector: 'strct-submenu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1734
- <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1735
- <div
1736
- class="strct-submenu__trigger"
1737
- role="menuitem"
1738
- tabindex="0"
1739
- aria-haspopup="menu"
1740
- [attr.aria-expanded]="open()"
1741
- (click)="$event.stopPropagation(); setOpen(!open())"
1742
- (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1743
- (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1744
- (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1745
- (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1746
- (keydown.escape)="$event.stopPropagation(); open.set(false)"
1747
- >
1748
- @if (icon()) {
1749
- <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1750
- } @else {
1751
- <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1752
- }
1753
- <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
1754
- <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1755
- </div>
1756
- @if (open()) {
1757
- <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1758
- <ng-content />
1759
- </div>
1760
- }
1761
- </div>
1762
- `, host: { class: 'strct-submenu-host' }, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
1763
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
1764
-
1765
- /**
1766
- * Floating menu panel — portaled into `<body>` (so it escapes overflow /
1767
- * transform clipping), positioned by its real measured size, with full keyboard
1768
- * navigation and recursive submenus. Usually created by `[strctContextMenu]`,
1769
- * but can be embedded directly with `submenu`.
1770
- */
1771
- class StrctMenuPanel {
1772
- host = inject(ElementRef);
1773
- items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1774
- data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1775
- x = input(0, ...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
1776
- y = input(0, ...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
1777
- submenu = input(false, { ...(ngDevMode ? { debugName: "submenu" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1778
- select = output();
1779
- close = output();
1780
- /** ArrowLeft inside a submenu — asks the parent to close it. */
1781
- back = output();
1782
- posX = signal(0, ...(ngDevMode ? [{ debugName: "posX" }] : /* istanbul ignore next */ []));
1783
- posY = signal(0, ...(ngDevMode ? [{ debugName: "posY" }] : /* istanbul ignore next */ []));
1784
- flipLeft = signal(false, ...(ngDevMode ? [{ debugName: "flipLeft" }] : /* istanbul ignore next */ []));
1785
- activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1786
- openSubIndex = signal(null, ...(ngDevMode ? [{ debugName: "openSubIndex" }] : /* istanbul ignore next */ []));
1787
- navIndices = computed(() => this.items()
1788
- .map((it, i) => (it.divider ? -1 : i))
1789
- .filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "navIndices" }] : /* istanbul ignore next */ []));
1790
- constructor() {
1791
- this.posX.set(this.x());
1792
- this.posY.set(this.y());
1793
- afterNextRender(() => {
1794
- this.activeIndex.set(this.navIndices()[0] ?? 0);
1795
- if (!this.submenu())
1796
- this.clampToViewport();
1797
- this.focusItem(this.activeIndex());
1798
- });
1799
- }
1800
- clampToViewport() {
1801
- const host = this.host.nativeElement;
1802
- const w = host.offsetWidth;
1803
- const h = host.offsetHeight;
1804
- const vw = window.innerWidth;
1805
- const vh = window.innerHeight;
1806
- const m = 6;
1807
- let nx = this.x();
1808
- let ny = this.y();
1809
- if (nx + w > vw - m)
1810
- nx = Math.max(m, Math.min(this.x() - w, vw - w - m));
1811
- if (ny + h > vh - m)
1812
- ny = Math.max(m, vh - h - m);
1813
- this.posX.set(nx);
1814
- this.posY.set(ny);
1815
- // Submenus of a panel near the right edge open to the left.
1816
- this.flipLeft.set(nx + w > vw - 220);
1817
- }
1818
- focusItem(i) {
1819
- this.activeIndex.set(i);
1820
- this.host.nativeElement
1821
- .querySelector(`.strct-menu__item[data-idx="${i}"]`)
1822
- ?.focus();
1823
- }
1824
- move(dir) {
1825
- const nav = this.navIndices();
1826
- if (!nav.length)
1827
- return;
1828
- const pos = nav.indexOf(this.activeIndex());
1829
- const next = nav[(pos + dir + nav.length) % nav.length];
1830
- this.openSubIndex.set(null);
1831
- this.focusItem(next);
1832
- }
1833
- onHover(i) {
1834
- this.activeIndex.set(i);
1835
- const it = this.items()[i];
1836
- this.openSubIndex.set(it?.children?.length ? i : null);
1837
- }
1838
- onLeave(i) {
1839
- if (this.openSubIndex() === i)
1840
- this.openSubIndex.set(null);
1841
- }
1842
- onItemClick(item, i, event) {
1843
- event.stopPropagation();
1844
- if (item.disabled)
1845
- return;
1846
- if (item.children?.length) {
1847
- this.openSubIndex.set(this.openSubIndex() === i ? null : i);
1848
- this.focusItem(i);
1849
- }
1850
- else {
1851
- this.select.emit(item);
1852
- }
1853
- }
1854
- closeSub() {
1855
- this.openSubIndex.set(null);
1856
- }
1857
- onKeydown(event) {
1858
- const key = event.key;
1859
- const item = this.items()[this.activeIndex()];
1860
- switch (key) {
1861
- case 'ArrowDown':
1862
- event.preventDefault();
1863
- event.stopPropagation();
1864
- this.move(1);
1865
- break;
1866
- case 'ArrowUp':
1867
- event.preventDefault();
1868
- event.stopPropagation();
1869
- this.move(-1);
1870
- break;
1871
- case 'Home':
1872
- event.preventDefault();
1873
- event.stopPropagation();
1874
- this.focusItem(this.navIndices()[0] ?? 0);
1875
- break;
1876
- case 'End':
1877
- event.preventDefault();
1878
- event.stopPropagation();
1879
- this.focusItem(this.navIndices().at(-1) ?? 0);
1880
- break;
1881
- case 'ArrowRight':
1882
- if (item?.children?.length) {
1883
- event.preventDefault();
1884
- event.stopPropagation();
1885
- this.openSubIndex.set(this.activeIndex());
1886
- }
1887
- break;
1888
- case 'ArrowLeft':
1889
- event.preventDefault();
1890
- event.stopPropagation();
1891
- if (this.openSubIndex() != null)
1892
- this.closeSub();
1893
- else if (this.submenu())
1894
- this.back.emit();
1895
- break;
1896
- case 'Enter':
1897
- case ' ':
1898
- event.preventDefault();
1899
- event.stopPropagation();
1900
- if (item && !item.disabled) {
1901
- if (item.children?.length)
1902
- this.openSubIndex.set(this.activeIndex());
1903
- else
1904
- this.select.emit(item);
1905
- }
1906
- break;
1907
- case 'Escape':
1908
- event.preventDefault();
1909
- event.stopPropagation();
1910
- this.close.emit();
1911
- break;
2041
+ }], onEscape: [{
2042
+ type: HostListener,
2043
+ args: ['document:keydown.escape']
2044
+ }], onResize: [{
2045
+ type: HostListener,
2046
+ args: ['window:resize']
2047
+ }] } });
2048
+
2049
+ /**
2050
+ * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Opens on
2051
+ * hover, click/tap, or the keyboard (Enter / Space / →), and flips to the left
2052
+ * near the right edge of the viewport. Reuse `strct-dropdown-item` for entries.
2053
+ * <strct-submenu label="Power">
2054
+ * <strct-dropdown-item>Power on</strct-dropdown-item>
2055
+ * <strct-dropdown-item>Power off</strct-dropdown-item>
2056
+ * </strct-submenu>
2057
+ */
2058
+ class StrctSubmenu {
2059
+ host = inject(ElementRef);
2060
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2061
+ /** Optional leading icon; when omitted the icon column is still reserved so
2062
+ * the label stays aligned with sibling items that do have icons. */
2063
+ icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
2064
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
2065
+ /** Open to the left when the fly-out would overflow the right edge. */
2066
+ flip = signal(false, ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
2067
+ setOpen(value) {
2068
+ if (value) {
2069
+ const rect = this.host.nativeElement.getBoundingClientRect();
2070
+ this.flip.set(rect.right + 190 > window.innerWidth);
1912
2071
  }
2072
+ this.open.set(value);
1913
2073
  }
1914
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
1915
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctMenuPanel, isStandalone: true, selector: "strct-menu-panel", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: false, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, submenu: { classPropertyName: "submenu", publicName: "submenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", close: "close", back: "back" }, host: { properties: { "style.position": "submenu() ? null : 'fixed'", "style.left.px": "submenu() ? null : posX()", "style.top.px": "submenu() ? null : posY()", "style.zIndex": "submenu() ? null : 1100" }, classAttribute: "strct-menu-host" }, ngImport: i0, template: `
1916
- <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1917
- @for (item of items(); track $index; let i = $index) {
1918
- @if (item.divider) {
1919
- <div class="strct-menu__sep" role="separator"></div>
2074
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
2075
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSubmenu, isStandalone: true, selector: "strct-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-submenu-host" }, ngImport: i0, template: `
2076
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
2077
+ <div
2078
+ class="strct-submenu__trigger"
2079
+ role="menuitem"
2080
+ tabindex="0"
2081
+ aria-haspopup="menu"
2082
+ [attr.aria-expanded]="open()"
2083
+ (click)="$event.stopPropagation(); setOpen(!open())"
2084
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2085
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2086
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2087
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
2088
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
2089
+ >
2090
+ @if (icon()) {
2091
+ <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1920
2092
  } @else {
1921
- <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1922
- <button
1923
- type="button"
1924
- class="strct-menu__item"
1925
- [attr.data-idx]="i"
1926
- [class.strct-menu__item--danger]="item.danger"
1927
- [class.strct-menu__item--active]="i === activeIndex()"
1928
- [disabled]="item.disabled"
1929
- role="menuitem"
1930
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1931
- [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1932
- [attr.tabindex]="i === activeIndex() ? 0 : -1"
1933
- (click)="onItemClick(item, i, $event)"
1934
- >
1935
- @if (item.icon) {
1936
- <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1937
- } @else {
1938
- <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1939
- }
1940
- <span class="strct-menu__label">{{ item.label }}</span>
1941
- @if (item.children?.length) {
1942
- <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1943
- }
1944
- </button>
1945
- @if (openSubIndex() === i && item.children?.length) {
1946
- <strct-menu-panel
1947
- submenu
1948
- class="strct-menu__subpanel"
1949
- [class.strct-menu__subpanel--flip]="flipLeft()"
1950
- [items]="item.children!"
1951
- [data]="data()"
1952
- (select)="select.emit($event)"
1953
- (close)="close.emit()"
1954
- (back)="closeSub(); focusItem(i)"
1955
- />
1956
- }
1957
- </div>
2093
+ <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1958
2094
  }
2095
+ <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
2096
+ <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
2097
+ </div>
2098
+ @if (open()) {
2099
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
2100
+ <ng-content />
2101
+ </div>
1959
2102
  }
1960
2103
  </div>
1961
- `, isInline: true, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"], dependencies: [{ kind: "component", type: StrctMenuPanel, selector: "strct-menu-panel", inputs: ["items", "data", "x", "y", "submenu"], outputs: ["select", "close", "back"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2104
+ `, isInline: true, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1962
2105
  }
1963
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, decorators: [{
2106
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, decorators: [{
1964
2107
  type: Component,
1965
- args: [{ selector: 'strct-menu-panel', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1966
- <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1967
- @for (item of items(); track $index; let i = $index) {
1968
- @if (item.divider) {
1969
- <div class="strct-menu__sep" role="separator"></div>
2108
+ args: [{ selector: 'strct-submenu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
2109
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
2110
+ <div
2111
+ class="strct-submenu__trigger"
2112
+ role="menuitem"
2113
+ tabindex="0"
2114
+ aria-haspopup="menu"
2115
+ [attr.aria-expanded]="open()"
2116
+ (click)="$event.stopPropagation(); setOpen(!open())"
2117
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2118
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2119
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
2120
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
2121
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
2122
+ >
2123
+ @if (icon()) {
2124
+ <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1970
2125
  } @else {
1971
- <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1972
- <button
1973
- type="button"
1974
- class="strct-menu__item"
1975
- [attr.data-idx]="i"
1976
- [class.strct-menu__item--danger]="item.danger"
1977
- [class.strct-menu__item--active]="i === activeIndex()"
1978
- [disabled]="item.disabled"
1979
- role="menuitem"
1980
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1981
- [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1982
- [attr.tabindex]="i === activeIndex() ? 0 : -1"
1983
- (click)="onItemClick(item, i, $event)"
1984
- >
1985
- @if (item.icon) {
1986
- <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1987
- } @else {
1988
- <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1989
- }
1990
- <span class="strct-menu__label">{{ item.label }}</span>
1991
- @if (item.children?.length) {
1992
- <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1993
- }
1994
- </button>
1995
- @if (openSubIndex() === i && item.children?.length) {
1996
- <strct-menu-panel
1997
- submenu
1998
- class="strct-menu__subpanel"
1999
- [class.strct-menu__subpanel--flip]="flipLeft()"
2000
- [items]="item.children!"
2001
- [data]="data()"
2002
- (select)="select.emit($event)"
2003
- (close)="close.emit()"
2004
- (back)="closeSub(); focusItem(i)"
2005
- />
2006
- }
2007
- </div>
2126
+ <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
2008
2127
  }
2128
+ <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
2129
+ <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
2130
+ </div>
2131
+ @if (open()) {
2132
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
2133
+ <ng-content />
2134
+ </div>
2009
2135
  }
2010
2136
  </div>
2011
- `, host: {
2012
- class: 'strct-menu-host',
2013
- '[style.position]': "submenu() ? null : 'fixed'",
2014
- '[style.left.px]': 'submenu() ? null : posX()',
2015
- '[style.top.px]': 'submenu() ? null : posY()',
2016
- '[style.zIndex]': 'submenu() ? null : 1100',
2017
- }, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"] }]
2018
- }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: false }] }], y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], submenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenu", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], close: [{ type: i0.Output, args: ["close"] }], back: [{ type: i0.Output, args: ["back"] }] } });
2019
- /**
2020
- * Right-click (context) menu driven by a data array. Attach to any trigger; the
2021
- * menu portals into `<body>` and runs each item's `action` on selection.
2022
- * <div [strctContextMenu]="menuFor(host)" [strctContextMenuData]="host"
2023
- * (menuSelect)="onPick($event)">…</div>
2024
- */
2025
- class StrctContextMenuTrigger {
2026
- appRef = inject(ApplicationRef);
2027
- envInjector = inject(EnvironmentInjector);
2028
- zone = inject(NgZone);
2029
- doc = inject(DOCUMENT$1);
2030
- items = input.required({ ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), alias: 'strctContextMenu' });
2031
- data = input(undefined, { ...(ngDevMode ? { debugName: "data" } : /* istanbul ignore next */ {}), alias: 'strctContextMenuData' });
2032
- menuSelect = output();
2033
- ref = null;
2034
- onClose = () => this.zone.run(() => this.closeMenu());
2035
- onContextMenu(event) {
2036
- if (!this.items()?.length)
2037
- return;
2038
- event.preventDefault();
2039
- this.openAt(event.clientX, event.clientY);
2040
- }
2041
- openAt(x, y) {
2042
- this.closeMenu();
2043
- const ref = createComponent(StrctMenuPanel, { environmentInjector: this.envInjector });
2044
- ref.setInput('items', this.items());
2045
- ref.setInput('data', this.data());
2046
- ref.setInput('x', x);
2047
- ref.setInput('y', y);
2048
- ref.instance.select.subscribe((item) => {
2049
- item.action?.(this.data());
2050
- this.menuSelect.emit(item);
2051
- this.closeMenu();
2052
- });
2053
- ref.instance.close.subscribe(() => this.closeMenu());
2054
- this.appRef.attachView(ref.hostView);
2055
- this.doc.body.appendChild(ref.location.nativeElement);
2056
- this.ref = ref;
2057
- // Defer global listeners so the opening right-click doesn't immediately close.
2058
- setTimeout(() => {
2059
- this.zone.runOutsideAngular(() => {
2060
- this.doc.addEventListener('mousedown', this.onOutside, true);
2061
- window.addEventListener('scroll', this.onClose, true);
2062
- window.addEventListener('resize', this.onClose);
2063
- });
2064
- });
2065
- }
2066
- onOutside = (event) => {
2067
- if (this.ref && !this.ref.location.nativeElement.contains(event.target)) {
2068
- this.onClose();
2069
- }
2070
- };
2071
- closeMenu() {
2072
- if (!this.ref)
2073
- return;
2074
- this.doc.removeEventListener('mousedown', this.onOutside, true);
2075
- window.removeEventListener('scroll', this.onClose, true);
2076
- window.removeEventListener('resize', this.onClose);
2077
- this.appRef.detachView(this.ref.hostView);
2078
- this.ref.destroy();
2079
- this.ref = null;
2080
- }
2081
- ngOnDestroy() {
2082
- this.closeMenu();
2083
- }
2084
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2085
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctContextMenuTrigger, isStandalone: true, selector: "[strctContextMenu]", inputs: { items: { classPropertyName: "items", publicName: "strctContextMenu", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "strctContextMenuData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuSelect: "menuSelect" }, host: { listeners: { "contextmenu": "onContextMenu($event)" } }, ngImport: i0 });
2086
- }
2087
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, decorators: [{
2088
- type: Directive,
2089
- args: [{ selector: '[strctContextMenu]' }]
2090
- }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenu", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenuData", required: false }] }], menuSelect: [{ type: i0.Output, args: ["menuSelect"] }], onContextMenu: [{
2091
- type: HostListener,
2092
- args: ['contextmenu', ['$event']]
2093
- }] } });
2137
+ `, host: { class: 'strct-submenu-host' }, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
2138
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
2094
2139
 
2095
2140
  /** A single wizard step. `label` names it in the step header. */
2096
2141
  class StrctStep {
@@ -3945,7 +3990,7 @@ class StrctCombobox {
3945
3990
  }
3946
3991
  </div>
3947
3992
  }
3948
- `, isInline: true, styles: [".strct-cbx{position:relative;display:inline-block;width:100%;max-width:280px}.strct-cbx__field{position:relative}.strct-cbx__input{padding-right:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3993
+ `, isInline: true, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-right:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3949
3994
  }
3950
3995
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCombobox, decorators: [{
3951
3996
  type: Component,
@@ -3999,7 +4044,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
3999
4044
  }
4000
4045
  </div>
4001
4046
  }
4002
- `, host: { class: 'strct-cbx' }, styles: [".strct-cbx{position:relative;display:inline-block;width:100%;max-width:280px}.strct-cbx__field{position:relative}.strct-cbx__input{padding-right:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"] }]
4047
+ `, host: { class: 'strct-cbx' }, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-right:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"] }]
4003
4048
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], onDocClick: [{
4004
4049
  type: HostListener,
4005
4050
  args: ['document:click', ['$event']]