@akcelik/strct 0.2.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, 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';
@@ -124,6 +124,9 @@ const STRCT_ICONS = {
124
124
  code: '<path d="M5.5 5L2.5 8l3 3M10.5 5l3 3-3 3M9 3.5l-2 9"/>',
125
125
  book: '<path d="M3 3.2h6a1.5 1.5 0 0 1 1.5 1.5v8.1H4.5A1.5 1.5 0 0 1 3 11.3z"/><path d="M13 3.2H9.5A1.5 1.5 0 0 0 8 4.7v8.1h5z"/>',
126
126
  terminal: '<rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M4.8 6.5L7 8.2 4.8 9.9M8.2 10.2h3"/>',
127
+ folder: '<path d="M2 4.6a1 1 0 0 1 1-1h3.1l1.3 1.6H13a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/>',
128
+ template: '<path d="M4.3 2.5h4.6l3.1 3.1V13a.5.5 0 0 1-.5.5H4.3a.5.5 0 0 1-.5-.5V3a.5.5 0 0 1 .5-.5z"/><path d="M8.7 2.6v3.1h3.1"/><path d="M5.8 9h4.2M5.8 11h2.6"/>',
129
+ tag: '<path d="M2.6 7.7V3.2a.6.6 0 0 1 .6-.6h4.5l5.6 5.6a1 1 0 0 1 0 1.4l-3.5 3.5a1 1 0 0 1-1.4 0z"/><circle cx="5.4" cy="5.4" r=".9" fill="currentColor" stroke="none"/>',
127
130
  // ── Datacenter / infrastructure ─────────────────────────────
128
131
  datacenter: '<rect x="2.5" y="2.5" width="11" height="11" rx="1"/><path d="M5 5h6M5 7.3h6M5 9.6h3.5"/><circle cx="11" cy="9.7" r=".5" fill="currentColor" stroke="none"/>',
129
132
  rack: '<rect x="3.5" y="2" width="9" height="12" rx="1"/><path d="M3.5 5.2h9M3.5 8.4h9M3.5 11.6h9"/><circle cx="5.4" cy="3.6" r=".4" fill="currentColor" stroke="none"/><circle cx="5.4" cy="6.8" r=".4" fill="currentColor" stroke="none"/><circle cx="5.4" cy="10" r=".4" fill="currentColor" stroke="none"/>',
@@ -144,6 +147,10 @@ const STRCT_ICONS = {
144
147
  hba: '<rect x="2.3" y="3.4" width="11.4" height="6.4" rx="1"/><circle cx="5.1" cy="6.6" r="1.2"/><circle cx="8.1" cy="6.6" r="1.2"/><path d="M10.6 5.6h1.6M10.6 7.6h1.6M5 9.8v2.6M11 9.8v2.6"/>',
145
148
  // RJ45 ethernet port (switch / server) — keyed jack with contact pins.
146
149
  ethernet: '<rect x="2.5" y="4" width="11" height="7.4" rx="1"/><path d="M6 4V2.6h4V4"/><path d="M5.3 7.6v2M7.1 7.6v2M8.9 7.6v2M10.7 7.6v2"/>',
150
+ // Resource pool — a proportional allocation (pie with two radii).
151
+ resourcePool: '<circle cx="8" cy="8" r="5.6"/><path d="M8 8V2.4M8 8l4.9 2.7"/>',
152
+ // Port group — a grouped set of switch ports under a shared rail.
153
+ portGroup: '<path d="M1.8 4.4h12.4"/><rect x="2.2" y="6.2" width="3.4" height="4" rx=".5"/><rect x="6.3" y="6.2" width="3.4" height="4" rx=".5"/><rect x="10.4" y="6.2" width="3.4" height="4" rx=".5"/><path d="M3.9 10.2v1.6M8 10.2v1.6M12.1 10.2v1.6"/>',
147
154
  // ── Accessibility (original glyphs for generic concepts) ─────
148
155
  universalAccess: '<circle cx="8" cy="8" r="6"/><circle cx="8" cy="4.9" r=".9" fill="currentColor" stroke="none"/><path d="M4.9 6.3c2 .8 4.2 .8 6.2 0M8 6.4v3.1M6.3 11.9 8 9.4l1.7 2.5"/>',
149
156
  wheelchair: '<circle cx="6.5" cy="3.1" r="1.3"/><path d="M6.5 4.5v3.3h3.1l1.7 3.4"/><circle cx="6.7" cy="11.3" r="2.6"/><path d="M9.3 11.3h2.1l-.5 1.6"/>',
@@ -181,7 +188,7 @@ const STRCT_ICON_GROUPS = [
181
188
  'hexagon', 'search', 'menu', 'ellipsis', 'dots', 'close', 'check', 'calendar',
182
189
  'eye', 'eyeOff', 'upload', 'download', 'sun', 'moon', 'bell', 'heart', 'layers',
183
190
  'grid', 'form', 'chart', 'bars', 'gauge', 'palette', 'sidebar', 'compass',
184
- 'copy', 'code', 'book', 'terminal',
191
+ 'copy', 'code', 'book', 'terminal', 'folder', 'template', 'tag',
185
192
  ],
186
193
  },
187
194
  {
@@ -201,6 +208,7 @@ const STRCT_ICON_GROUPS = [
201
208
  names: [
202
209
  'datacenter', 'rack', 'cluster', 'host', 'vm', 'switch', 'storage',
203
210
  'network', 'cpu', 'memory', 'disk', 'port', 'nic', 'hba', 'ethernet', 'power',
211
+ 'resourcePool', 'portGroup',
204
212
  ],
205
213
  },
206
214
  {
@@ -436,10 +444,16 @@ class StrctOverlay {
436
444
  let left;
437
445
  if (p === 'right') {
438
446
  left = a.right + gap;
447
+ // Flip to the left of the anchor if it would overflow the right edge.
448
+ if (left + w > vw - margin && a.left - gap - w > margin)
449
+ left = a.left - gap - w;
439
450
  top = a.top;
440
451
  }
441
452
  else if (p === 'left') {
442
453
  left = a.left - gap - w;
454
+ // Flip to the right of the anchor if it would overflow the left edge.
455
+ if (left < margin && a.right + gap + w < vw - margin)
456
+ left = a.right + gap;
443
457
  top = a.top;
444
458
  }
445
459
  else {
@@ -1117,132 +1131,488 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1117
1131
  }], ctorParameters: () => [], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTab), { isSignal: true }] }] } });
1118
1132
 
1119
1133
  /**
1120
- * Tree node. Two modes:
1121
- * - **Content:** nest `<strct-tree-node>` children manually.
1122
- * - **Data:** pass a `[node]` object that recurses over its `children` —
1123
- * used internally by `<strct-tree [nodes]>`.
1124
- *
1125
- * <strct-tree-node label="Group" icon="layers" badge="ok" [(expanded)]="open">
1126
- * <strct-tree-node label="Leaf" icon="vm" [active]="true" />
1127
- * </strct-tree-node>
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`.
1128
1138
  */
1129
- class StrctTreeNode {
1130
- /** Data-driven node; when set, label/icon/children come from it. */
1131
- node = input(null, ...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
1132
- label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1133
- icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1134
- badge = input('none', ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
1135
- active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1136
- expanded = model(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1137
- /** Content-mode click. */
1138
- activated = output();
1139
- /** Data-mode click — carries the activated node (bubbles to the tree). */
1140
- nodeActivated = output();
1141
- childNodes = contentChildren(StrctTreeNode, ...(ngDevMode ? [{ debugName: "childNodes" }] : /* istanbul ignore next */ []));
1142
- /** Data-mode expansion (seeded from node.expanded on first toggle). */
1143
- dataExpanded = signal(null, ...(ngDevMode ? [{ debugName: "dataExpanded" }] : /* istanbul ignore next */ []));
1144
- displayLabel = computed(() => this.node()?.label ?? this.label(), ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
1145
- displayIcon = computed(() => this.node()?.icon ?? this.icon(), ...(ngDevMode ? [{ debugName: "displayIcon" }] : /* istanbul ignore next */ []));
1146
- displayBadge = computed(() => this.node()?.badge ?? this.badge(), ...(ngDevMode ? [{ debugName: "displayBadge" }] : /* istanbul ignore next */ []));
1147
- displayActive = computed(() => this.node()?.active ?? this.active(), ...(ngDevMode ? [{ debugName: "displayActive" }] : /* istanbul ignore next */ []));
1148
- hasChildren = computed(() => {
1149
- const n = this.node();
1150
- return n ? (n.children?.length ?? 0) > 0 : this.childNodes().length > 0;
1151
- }, ...(ngDevMode ? [{ debugName: "hasChildren" }] : /* istanbul ignore next */ []));
1152
- isOpen = computed(() => {
1153
- if (this.node())
1154
- return this.dataExpanded() ?? this.node().expanded ?? false;
1155
- return this.expanded();
1156
- }, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1157
- toggle() {
1158
- if (this.node()) {
1159
- this.dataExpanded.set(!this.isOpen());
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);
1160
1217
  }
1161
1218
  else {
1162
- this.expanded.update((v) => !v);
1219
+ this.select.emit(item);
1163
1220
  }
1164
1221
  }
1165
- onActivate() {
1166
- const n = this.node();
1167
- if (n)
1168
- this.nodeActivated.emit(n);
1169
- else
1170
- this.activated.emit();
1222
+ closeSub() {
1223
+ this.openSubIndex.set(null);
1171
1224
  }
1172
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Component });
1173
- 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: `
1174
- <div
1175
- class="strct-tnode__row"
1176
- [class.strct-tnode__row--active]="displayActive()"
1177
- role="treeitem"
1178
- [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1179
- (click)="onActivate()"
1180
- >
1181
- @if (hasChildren()) {
1182
- <span
1183
- class="strct-tnode__chevron"
1184
- [class.strct-tnode__chevron--open]="isOpen()"
1185
- (click)="$event.stopPropagation(); toggle()"
1186
- >
1187
- <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
1188
- </span>
1189
- } @else {
1190
- <span class="strct-tnode__spacer"></span>
1191
- }
1192
- @if (displayIcon()) {
1193
- <strct-icon
1194
- class="strct-tnode__icon"
1195
- [name]="displayIcon()!"
1196
- [size]="14"
1197
- [strokeWidth]="1.3"
1198
- [badge]="displayBadge()"
1199
- />
1200
- }
1201
- <span class="strct-tnode__label">{{ displayLabel() }}</span>
1202
- <ng-content select="[strctTreeTrailing]" />
1203
- </div>
1204
- @if (hasChildren() && isOpen()) {
1205
- <div class="strct-tnode__children" role="group">
1206
- @if (node()) {
1207
- @for (child of node()!.children ?? []; track $index) {
1208
- <strct-tree-node [node]="child" (nodeActivated)="nodeActivated.emit($event)" />
1209
- }
1210
- } @else {
1211
- <ng-content />
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;
1212
1280
  }
1213
- </div>
1214
1281
  }
1215
- `, 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 });
1216
- }
1217
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, decorators: [{
1218
- type: Component,
1219
- args: [{ selector: 'strct-tree-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1220
- <div
1221
- class="strct-tnode__row"
1222
- [class.strct-tnode__row--active]="displayActive()"
1223
- role="treeitem"
1224
- [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1225
- (click)="onActivate()"
1226
- >
1227
- @if (hasChildren()) {
1228
- <span
1229
- class="strct-tnode__chevron"
1230
- [class.strct-tnode__chevron--open]="isOpen()"
1231
- (click)="$event.stopPropagation(); toggle()"
1232
- >
1233
- <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
1234
- </span>
1235
- } @else {
1236
- <span class="strct-tnode__spacer"></span>
1237
- }
1238
- @if (displayIcon()) {
1239
- <strct-icon
1240
- class="strct-tnode__icon"
1241
- [name]="displayIcon()!"
1242
- [size]="14"
1243
- [strokeWidth]="1.3"
1244
- [badge]="displayBadge()"
1245
- />
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
+
1463
+ /**
1464
+ * Tree node. Two modes:
1465
+ * - **Content:** nest `<strct-tree-node>` children manually.
1466
+ * - **Data:** pass a `[node]` object that recurses over its `children` —
1467
+ * used internally by `<strct-tree [nodes]>`.
1468
+ *
1469
+ * <strct-tree-node label="Group" icon="layers" badge="ok" [(expanded)]="open">
1470
+ * <strct-tree-node label="Leaf" icon="vm" [active]="true" />
1471
+ * </strct-tree-node>
1472
+ */
1473
+ class StrctTreeNode {
1474
+ /** Data-driven node; when set, label/icon/children come from it. */
1475
+ node = input(null, ...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
1476
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1477
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1478
+ badge = input('none', ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
1479
+ active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
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 */ []));
1483
+ /** Content-mode click. */
1484
+ activated = output();
1485
+ /** Data-mode click — carries the activated node (bubbles to the tree). */
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 */ []));
1495
+ childNodes = contentChildren(StrctTreeNode, ...(ngDevMode ? [{ debugName: "childNodes" }] : /* istanbul ignore next */ []));
1496
+ /** Data-mode expansion (seeded from node.expanded on first toggle). */
1497
+ dataExpanded = signal(null, ...(ngDevMode ? [{ debugName: "dataExpanded" }] : /* istanbul ignore next */ []));
1498
+ displayLabel = computed(() => this.node()?.label ?? this.label(), ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
1499
+ displayIcon = computed(() => this.node()?.icon ?? this.icon(), ...(ngDevMode ? [{ debugName: "displayIcon" }] : /* istanbul ignore next */ []));
1500
+ displayBadge = computed(() => this.node()?.badge ?? this.badge(), ...(ngDevMode ? [{ debugName: "displayBadge" }] : /* istanbul ignore next */ []));
1501
+ displayActive = computed(() => this.node()?.active ?? this.active(), ...(ngDevMode ? [{ debugName: "displayActive" }] : /* istanbul ignore next */ []));
1502
+ hasChildren = computed(() => {
1503
+ const n = this.node();
1504
+ return n ? (n.children?.length ?? 0) > 0 : this.childNodes().length > 0;
1505
+ }, ...(ngDevMode ? [{ debugName: "hasChildren" }] : /* istanbul ignore next */ []));
1506
+ isOpen = computed(() => {
1507
+ if (this.node())
1508
+ return this.dataExpanded() ?? this.node().expanded ?? false;
1509
+ return this.expanded();
1510
+ }, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1511
+ toggle() {
1512
+ if (this.node()) {
1513
+ this.dataExpanded.set(!this.isOpen());
1514
+ }
1515
+ else {
1516
+ this.expanded.update((v) => !v);
1517
+ }
1518
+ }
1519
+ onActivate() {
1520
+ const n = this.node();
1521
+ if (n)
1522
+ this.nodeActivated.emit(n);
1523
+ else
1524
+ this.activated.emit();
1525
+ }
1526
+ onMenuSelect(item) {
1527
+ const n = this.node();
1528
+ if (n)
1529
+ this.nodeMenuSelect.emit({ node: n, item });
1530
+ }
1531
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Component });
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: `
1533
+ <div
1534
+ class="strct-tnode__row"
1535
+ [class.strct-tnode__row--active]="displayActive()"
1536
+ role="treeitem"
1537
+ [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1538
+ [strctContextMenu]="menuItems()"
1539
+ [strctContextMenuData]="node()"
1540
+ (menuSelect)="onMenuSelect($event)"
1541
+ (click)="onActivate()"
1542
+ >
1543
+ @if (hasChildren()) {
1544
+ <span
1545
+ class="strct-tnode__chevron"
1546
+ [class.strct-tnode__chevron--open]="isOpen()"
1547
+ (click)="$event.stopPropagation(); toggle()"
1548
+ >
1549
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
1550
+ </span>
1551
+ } @else {
1552
+ <span class="strct-tnode__spacer"></span>
1553
+ }
1554
+ @if (displayIcon()) {
1555
+ <strct-icon
1556
+ class="strct-tnode__icon"
1557
+ [name]="displayIcon()!"
1558
+ [size]="14"
1559
+ [strokeWidth]="1.3"
1560
+ [badge]="displayBadge()"
1561
+ />
1562
+ }
1563
+ <span class="strct-tnode__label">{{ displayLabel() }}</span>
1564
+ <ng-content select="[strctTreeTrailing]" />
1565
+ </div>
1566
+ @if (hasChildren() && isOpen()) {
1567
+ <div class="strct-tnode__children" role="group">
1568
+ @if (node()) {
1569
+ @for (child of node()!.children ?? []; track $index) {
1570
+ <strct-tree-node
1571
+ [node]="child"
1572
+ [nodeMenu]="nodeMenu()"
1573
+ (nodeActivated)="nodeActivated.emit($event)"
1574
+ (nodeMenuSelect)="nodeMenuSelect.emit($event)"
1575
+ />
1576
+ }
1577
+ } @else {
1578
+ <ng-content />
1579
+ }
1580
+ </div>
1581
+ }
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 });
1583
+ }
1584
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, decorators: [{
1585
+ type: Component,
1586
+ args: [{ selector: 'strct-tree-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctContextMenuTrigger], template: `
1587
+ <div
1588
+ class="strct-tnode__row"
1589
+ [class.strct-tnode__row--active]="displayActive()"
1590
+ role="treeitem"
1591
+ [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1592
+ [strctContextMenu]="menuItems()"
1593
+ [strctContextMenuData]="node()"
1594
+ (menuSelect)="onMenuSelect($event)"
1595
+ (click)="onActivate()"
1596
+ >
1597
+ @if (hasChildren()) {
1598
+ <span
1599
+ class="strct-tnode__chevron"
1600
+ [class.strct-tnode__chevron--open]="isOpen()"
1601
+ (click)="$event.stopPropagation(); toggle()"
1602
+ >
1603
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
1604
+ </span>
1605
+ } @else {
1606
+ <span class="strct-tnode__spacer"></span>
1607
+ }
1608
+ @if (displayIcon()) {
1609
+ <strct-icon
1610
+ class="strct-tnode__icon"
1611
+ [name]="displayIcon()!"
1612
+ [size]="14"
1613
+ [strokeWidth]="1.3"
1614
+ [badge]="displayBadge()"
1615
+ />
1246
1616
  }
1247
1617
  <span class="strct-tnode__label">{{ displayLabel() }}</span>
1248
1618
  <ng-content select="[strctTreeTrailing]" />
@@ -1251,7 +1621,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1251
1621
  <div class="strct-tnode__children" role="group">
1252
1622
  @if (node()) {
1253
1623
  @for (child of node()!.children ?? []; track $index) {
1254
- <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
+ />
1255
1630
  }
1256
1631
  } @else {
1257
1632
  <ng-content />
@@ -1259,7 +1634,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1259
1634
  </div>
1260
1635
  }
1261
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"] }]
1262
- }], 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 }] }] } });
1263
1638
  /**
1264
1639
  * Root container for a tree. Either project `<strct-tree-node>` children, or
1265
1640
  * pass `[nodes]` for a fully data-driven, self-recursing tree:
@@ -1268,33 +1643,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1268
1643
  class StrctTree {
1269
1644
  /** Data-driven node list; when set, projected content is ignored. */
1270
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 */ []));
1271
1648
  /** Emitted when any data-driven node is clicked. */
1272
1649
  nodeActivated = output();
1650
+ /** Emitted when a data-driven node's right-click menu item is chosen. */
1651
+ nodeMenuSelect = output();
1273
1652
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
1274
- 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: `
1275
1654
  @if (nodes(); as ns) {
1276
1655
  @for (n of ns; track $index) {
1277
- <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
+ />
1278
1662
  }
1279
1663
  } @else {
1280
1664
  <ng-content />
1281
1665
  }
1282
- `, 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 });
1283
1667
  }
1284
1668
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, decorators: [{
1285
1669
  type: Component,
1286
1670
  args: [{ selector: 'strct-tree', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctTreeNode], template: `
1287
1671
  @if (nodes(); as ns) {
1288
1672
  @for (n of ns; track $index) {
1289
- <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
+ />
1290
1679
  }
1291
1680
  } @else {
1292
1681
  <ng-content />
1293
1682
  }
1294
1683
  `, host: { class: 'strct-tree', role: 'tree' }, styles: [".strct-tree{display:block}\n"] }]
1295
- }], 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"] }] } });
1296
1685
 
1297
1686
  let modalCounter = 0;
1687
+ // Body scroll-lock shared across any number of simultaneously open modals.
1688
+ let scrollLockCount = 0;
1689
+ let savedBodyOverflow = '';
1690
+ function lockBodyScroll(doc) {
1691
+ if (scrollLockCount === 0) {
1692
+ savedBodyOverflow = doc.body.style.overflow;
1693
+ doc.body.style.overflow = 'hidden';
1694
+ }
1695
+ scrollLockCount++;
1696
+ }
1697
+ function unlockBodyScroll(doc) {
1698
+ scrollLockCount = Math.max(0, scrollLockCount - 1);
1699
+ if (scrollLockCount === 0)
1700
+ doc.body.style.overflow = savedBodyOverflow;
1701
+ }
1298
1702
  /**
1299
1703
  * Overlay dialog with two-way `open`:
1300
1704
  * <strct-modal [(open)]="show" title="Confirm">
@@ -1317,16 +1721,32 @@ class StrctModal {
1317
1721
  titleId = `strct-modal-${++modalCounter}`;
1318
1722
  /** Element that had focus before the dialog opened, restored on close. */
1319
1723
  previousActive = null;
1724
+ /** Whether this instance currently holds a scroll lock. */
1725
+ locked = false;
1320
1726
  constructor() {
1321
1727
  effect(() => {
1322
- if (this.open()) {
1728
+ const open = this.open();
1729
+ if (open && !this.locked) {
1730
+ this.locked = true;
1731
+ lockBodyScroll(this.doc);
1323
1732
  this.previousActive = this.doc.activeElement;
1324
1733
  // Move focus into the dialog once it has rendered.
1325
1734
  setTimeout(() => this.focusInitial());
1326
1735
  }
1327
- else if (this.previousActive) {
1328
- this.previousActive.focus?.();
1329
- this.previousActive = null;
1736
+ else if (!open && this.locked) {
1737
+ this.locked = false;
1738
+ unlockBodyScroll(this.doc);
1739
+ if (this.previousActive) {
1740
+ this.previousActive.focus?.();
1741
+ this.previousActive = null;
1742
+ }
1743
+ }
1744
+ });
1745
+ // Release the lock if the modal is destroyed while still open.
1746
+ inject(DestroyRef).onDestroy(() => {
1747
+ if (this.locked) {
1748
+ this.locked = false;
1749
+ unlockBodyScroll(this.doc);
1330
1750
  }
1331
1751
  });
1332
1752
  }
@@ -1611,414 +2031,111 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1611
2031
  (click)="close()"
1612
2032
  (contextmenu)="$event.preventDefault(); close()"
1613
2033
  >
1614
- <ng-content select="[strctContextMenuItems]" />
1615
- </div>
1616
- }
1617
- `, host: { class: 'strct-ctx' }, styles: [".strct-ctx{display:block}.strct-ctx__menu{position:fixed;z-index:1100;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-ctx-in .1s ease}@keyframes strct-ctx-in{0%{opacity:0;transform:scale(.97)}}\n"] }]
1618
- }], propDecorators: { onDocClick: [{
1619
- type: HostListener,
1620
- args: ['document:click']
1621
- }], onEscape: [{
1622
- type: HostListener,
1623
- args: ['document:keydown.escape']
1624
- }], onResize: [{
1625
- type: HostListener,
1626
- args: ['window:resize']
1627
- }] } });
1628
-
1629
- /**
1630
- * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Reuse
1631
- * `strct-dropdown-item` for the nested entries.
1632
- * <strct-submenu label="Power">
1633
- * <strct-dropdown-item>Power on</strct-dropdown-item>
1634
- * <strct-dropdown-item>Power off</strct-dropdown-item>
1635
- * </strct-submenu>
1636
- */
1637
- class StrctSubmenu {
1638
- label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1639
- /** Optional leading icon; when omitted the icon column is still reserved so
1640
- * the label stays aligned with sibling items that do have icons. */
1641
- icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1642
- open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1643
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1644
- 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: `
1645
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1646
- <div
1647
- class="strct-submenu__trigger"
1648
- role="menuitem"
1649
- aria-haspopup="menu"
1650
- [attr.aria-expanded]="open()"
1651
- (click)="$event.stopPropagation()"
1652
- >
1653
- @if (icon()) {
1654
- <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1655
- } @else {
1656
- <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1657
- }
1658
- <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
1659
- <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1660
- </div>
1661
- @if (open()) {
1662
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1663
- }
1664
- </div>
1665
- `, 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__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}@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 });
1666
- }
1667
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, decorators: [{
1668
- type: Component,
1669
- args: [{ selector: 'strct-submenu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1670
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1671
- <div
1672
- class="strct-submenu__trigger"
1673
- role="menuitem"
1674
- aria-haspopup="menu"
1675
- [attr.aria-expanded]="open()"
1676
- (click)="$event.stopPropagation()"
1677
- >
1678
- @if (icon()) {
1679
- <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
1680
- } @else {
1681
- <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1682
- }
1683
- <span class="strct-submenu__label">{{ label() }}<ng-content select="[strctSubmenuLabel]" /></span>
1684
- <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1685
- </div>
1686
- @if (open()) {
1687
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1688
- }
1689
- </div>
1690
- `, 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__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}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
1691
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
1692
-
1693
- /**
1694
- * Floating menu panel — portaled into `<body>` (so it escapes overflow /
1695
- * transform clipping), positioned by its real measured size, with full keyboard
1696
- * navigation and recursive submenus. Usually created by `[strctContextMenu]`,
1697
- * but can be embedded directly with `submenu`.
1698
- */
1699
- class StrctMenuPanel {
1700
- host = inject(ElementRef);
1701
- items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1702
- data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1703
- x = input(0, ...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
1704
- y = input(0, ...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
1705
- submenu = input(false, { ...(ngDevMode ? { debugName: "submenu" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1706
- select = output();
1707
- close = output();
1708
- /** ArrowLeft inside a submenu — asks the parent to close it. */
1709
- back = output();
1710
- posX = signal(0, ...(ngDevMode ? [{ debugName: "posX" }] : /* istanbul ignore next */ []));
1711
- posY = signal(0, ...(ngDevMode ? [{ debugName: "posY" }] : /* istanbul ignore next */ []));
1712
- flipLeft = signal(false, ...(ngDevMode ? [{ debugName: "flipLeft" }] : /* istanbul ignore next */ []));
1713
- activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1714
- openSubIndex = signal(null, ...(ngDevMode ? [{ debugName: "openSubIndex" }] : /* istanbul ignore next */ []));
1715
- navIndices = computed(() => this.items()
1716
- .map((it, i) => (it.divider ? -1 : i))
1717
- .filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "navIndices" }] : /* istanbul ignore next */ []));
1718
- constructor() {
1719
- this.posX.set(this.x());
1720
- this.posY.set(this.y());
1721
- afterNextRender(() => {
1722
- this.activeIndex.set(this.navIndices()[0] ?? 0);
1723
- if (!this.submenu())
1724
- this.clampToViewport();
1725
- this.focusItem(this.activeIndex());
1726
- });
1727
- }
1728
- clampToViewport() {
1729
- const host = this.host.nativeElement;
1730
- const w = host.offsetWidth;
1731
- const h = host.offsetHeight;
1732
- const vw = window.innerWidth;
1733
- const vh = window.innerHeight;
1734
- const m = 6;
1735
- let nx = this.x();
1736
- let ny = this.y();
1737
- if (nx + w > vw - m)
1738
- nx = Math.max(m, Math.min(this.x() - w, vw - w - m));
1739
- if (ny + h > vh - m)
1740
- ny = Math.max(m, vh - h - m);
1741
- this.posX.set(nx);
1742
- this.posY.set(ny);
1743
- // Submenus of a panel near the right edge open to the left.
1744
- this.flipLeft.set(nx + w > vw - 220);
1745
- }
1746
- focusItem(i) {
1747
- this.activeIndex.set(i);
1748
- this.host.nativeElement
1749
- .querySelector(`.strct-menu__item[data-idx="${i}"]`)
1750
- ?.focus();
1751
- }
1752
- move(dir) {
1753
- const nav = this.navIndices();
1754
- if (!nav.length)
1755
- return;
1756
- const pos = nav.indexOf(this.activeIndex());
1757
- const next = nav[(pos + dir + nav.length) % nav.length];
1758
- this.openSubIndex.set(null);
1759
- this.focusItem(next);
1760
- }
1761
- onHover(i) {
1762
- this.activeIndex.set(i);
1763
- const it = this.items()[i];
1764
- this.openSubIndex.set(it?.children?.length ? i : null);
1765
- }
1766
- onLeave(i) {
1767
- if (this.openSubIndex() === i)
1768
- this.openSubIndex.set(null);
1769
- }
1770
- onItemClick(item, i, event) {
1771
- event.stopPropagation();
1772
- if (item.disabled)
1773
- return;
1774
- if (item.children?.length) {
1775
- this.openSubIndex.set(this.openSubIndex() === i ? null : i);
1776
- this.focusItem(i);
1777
- }
1778
- else {
1779
- this.select.emit(item);
1780
- }
1781
- }
1782
- closeSub() {
1783
- this.openSubIndex.set(null);
1784
- }
1785
- onKeydown(event) {
1786
- const key = event.key;
1787
- const item = this.items()[this.activeIndex()];
1788
- switch (key) {
1789
- case 'ArrowDown':
1790
- event.preventDefault();
1791
- event.stopPropagation();
1792
- this.move(1);
1793
- break;
1794
- case 'ArrowUp':
1795
- event.preventDefault();
1796
- event.stopPropagation();
1797
- this.move(-1);
1798
- break;
1799
- case 'Home':
1800
- event.preventDefault();
1801
- event.stopPropagation();
1802
- this.focusItem(this.navIndices()[0] ?? 0);
1803
- break;
1804
- case 'End':
1805
- event.preventDefault();
1806
- event.stopPropagation();
1807
- this.focusItem(this.navIndices().at(-1) ?? 0);
1808
- break;
1809
- case 'ArrowRight':
1810
- if (item?.children?.length) {
1811
- event.preventDefault();
1812
- event.stopPropagation();
1813
- this.openSubIndex.set(this.activeIndex());
1814
- }
1815
- break;
1816
- case 'ArrowLeft':
1817
- event.preventDefault();
1818
- event.stopPropagation();
1819
- if (this.openSubIndex() != null)
1820
- this.closeSub();
1821
- else if (this.submenu())
1822
- this.back.emit();
1823
- break;
1824
- case 'Enter':
1825
- case ' ':
1826
- event.preventDefault();
1827
- event.stopPropagation();
1828
- if (item && !item.disabled) {
1829
- if (item.children?.length)
1830
- this.openSubIndex.set(this.activeIndex());
1831
- else
1832
- this.select.emit(item);
1833
- }
1834
- break;
1835
- case 'Escape':
1836
- event.preventDefault();
1837
- event.stopPropagation();
1838
- this.close.emit();
1839
- break;
2034
+ <ng-content select="[strctContextMenuItems]" />
2035
+ </div>
2036
+ }
2037
+ `, host: { class: 'strct-ctx' }, styles: [".strct-ctx{display:block}.strct-ctx__menu{position:fixed;z-index:1100;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-ctx-in .1s ease}@keyframes strct-ctx-in{0%{opacity:0;transform:scale(.97)}}\n"] }]
2038
+ }], propDecorators: { onDocClick: [{
2039
+ type: HostListener,
2040
+ args: ['document:click']
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);
1840
2071
  }
2072
+ this.open.set(value);
1841
2073
  }
1842
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
1843
- 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: `
1844
- <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1845
- @for (item of items(); track $index; let i = $index) {
1846
- @if (item.divider) {
1847
- <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" />
1848
2092
  } @else {
1849
- <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1850
- <button
1851
- type="button"
1852
- class="strct-menu__item"
1853
- [attr.data-idx]="i"
1854
- [class.strct-menu__item--danger]="item.danger"
1855
- [class.strct-menu__item--active]="i === activeIndex()"
1856
- [disabled]="item.disabled"
1857
- role="menuitem"
1858
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1859
- [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1860
- [attr.tabindex]="i === activeIndex() ? 0 : -1"
1861
- (click)="onItemClick(item, i, $event)"
1862
- >
1863
- @if (item.icon) {
1864
- <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1865
- } @else {
1866
- <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1867
- }
1868
- <span class="strct-menu__label">{{ item.label }}</span>
1869
- @if (item.children?.length) {
1870
- <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1871
- }
1872
- </button>
1873
- @if (openSubIndex() === i && item.children?.length) {
1874
- <strct-menu-panel
1875
- submenu
1876
- class="strct-menu__subpanel"
1877
- [class.strct-menu__subpanel--flip]="flipLeft()"
1878
- [items]="item.children!"
1879
- [data]="data()"
1880
- (select)="select.emit($event)"
1881
- (close)="close.emit()"
1882
- (back)="closeSub(); focusItem(i)"
1883
- />
1884
- }
1885
- </div>
2093
+ <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1886
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>
1887
2102
  }
1888
2103
  </div>
1889
- `, 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 });
1890
2105
  }
1891
- 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: [{
1892
2107
  type: Component,
1893
- args: [{ selector: 'strct-menu-panel', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1894
- <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1895
- @for (item of items(); track $index; let i = $index) {
1896
- @if (item.divider) {
1897
- <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" />
1898
2125
  } @else {
1899
- <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1900
- <button
1901
- type="button"
1902
- class="strct-menu__item"
1903
- [attr.data-idx]="i"
1904
- [class.strct-menu__item--danger]="item.danger"
1905
- [class.strct-menu__item--active]="i === activeIndex()"
1906
- [disabled]="item.disabled"
1907
- role="menuitem"
1908
- [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1909
- [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1910
- [attr.tabindex]="i === activeIndex() ? 0 : -1"
1911
- (click)="onItemClick(item, i, $event)"
1912
- >
1913
- @if (item.icon) {
1914
- <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1915
- } @else {
1916
- <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1917
- }
1918
- <span class="strct-menu__label">{{ item.label }}</span>
1919
- @if (item.children?.length) {
1920
- <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1921
- }
1922
- </button>
1923
- @if (openSubIndex() === i && item.children?.length) {
1924
- <strct-menu-panel
1925
- submenu
1926
- class="strct-menu__subpanel"
1927
- [class.strct-menu__subpanel--flip]="flipLeft()"
1928
- [items]="item.children!"
1929
- [data]="data()"
1930
- (select)="select.emit($event)"
1931
- (close)="close.emit()"
1932
- (back)="closeSub(); focusItem(i)"
1933
- />
1934
- }
1935
- </div>
2126
+ <span class="strct-submenu__icon-spacer" aria-hidden="true"></span>
1936
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>
1937
2135
  }
1938
2136
  </div>
1939
- `, host: {
1940
- class: 'strct-menu-host',
1941
- '[style.position]': "submenu() ? null : 'fixed'",
1942
- '[style.left.px]': 'submenu() ? null : posX()',
1943
- '[style.top.px]': 'submenu() ? null : posY()',
1944
- '[style.zIndex]': 'submenu() ? null : 1100',
1945
- }, 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"] }]
1946
- }], 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"] }] } });
1947
- /**
1948
- * Right-click (context) menu driven by a data array. Attach to any trigger; the
1949
- * menu portals into `<body>` and runs each item's `action` on selection.
1950
- * <div [strctContextMenu]="menuFor(host)" [strctContextMenuData]="host"
1951
- * (menuSelect)="onPick($event)">…</div>
1952
- */
1953
- class StrctContextMenuTrigger {
1954
- appRef = inject(ApplicationRef);
1955
- envInjector = inject(EnvironmentInjector);
1956
- zone = inject(NgZone);
1957
- doc = inject(DOCUMENT$1);
1958
- items = input.required({ ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), alias: 'strctContextMenu' });
1959
- data = input(undefined, { ...(ngDevMode ? { debugName: "data" } : /* istanbul ignore next */ {}), alias: 'strctContextMenuData' });
1960
- menuSelect = output();
1961
- ref = null;
1962
- onClose = () => this.zone.run(() => this.closeMenu());
1963
- onContextMenu(event) {
1964
- if (!this.items()?.length)
1965
- return;
1966
- event.preventDefault();
1967
- this.openAt(event.clientX, event.clientY);
1968
- }
1969
- openAt(x, y) {
1970
- this.closeMenu();
1971
- const ref = createComponent(StrctMenuPanel, { environmentInjector: this.envInjector });
1972
- ref.setInput('items', this.items());
1973
- ref.setInput('data', this.data());
1974
- ref.setInput('x', x);
1975
- ref.setInput('y', y);
1976
- ref.instance.select.subscribe((item) => {
1977
- item.action?.(this.data());
1978
- this.menuSelect.emit(item);
1979
- this.closeMenu();
1980
- });
1981
- ref.instance.close.subscribe(() => this.closeMenu());
1982
- this.appRef.attachView(ref.hostView);
1983
- this.doc.body.appendChild(ref.location.nativeElement);
1984
- this.ref = ref;
1985
- // Defer global listeners so the opening right-click doesn't immediately close.
1986
- setTimeout(() => {
1987
- this.zone.runOutsideAngular(() => {
1988
- this.doc.addEventListener('mousedown', this.onOutside, true);
1989
- window.addEventListener('scroll', this.onClose, true);
1990
- window.addEventListener('resize', this.onClose);
1991
- });
1992
- });
1993
- }
1994
- onOutside = (event) => {
1995
- if (this.ref && !this.ref.location.nativeElement.contains(event.target)) {
1996
- this.onClose();
1997
- }
1998
- };
1999
- closeMenu() {
2000
- if (!this.ref)
2001
- return;
2002
- this.doc.removeEventListener('mousedown', this.onOutside, true);
2003
- window.removeEventListener('scroll', this.onClose, true);
2004
- window.removeEventListener('resize', this.onClose);
2005
- this.appRef.detachView(this.ref.hostView);
2006
- this.ref.destroy();
2007
- this.ref = null;
2008
- }
2009
- ngOnDestroy() {
2010
- this.closeMenu();
2011
- }
2012
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2013
- 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 });
2014
- }
2015
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, decorators: [{
2016
- type: Directive,
2017
- args: [{ selector: '[strctContextMenu]' }]
2018
- }], 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: [{
2019
- type: HostListener,
2020
- args: ['contextmenu', ['$event']]
2021
- }] } });
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 }] }] } });
2022
2139
 
2023
2140
  /** A single wizard step. `label` names it in the step header. */
2024
2141
  class StrctStep {
@@ -2329,6 +2446,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
2329
2446
  `, host: { class: 'strct-pg', role: 'navigation', 'aria-label': 'Pagination' }, styles: [".strct-pg{display:inline-flex;align-items:center;gap:4px}.strct-pg__btn{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:30px;padding:0 7px;border-radius:6px;font-family:var(--font);font-size:13px;cursor:pointer;color:var(--t1);background:transparent;border:1px solid transparent;transition:background .14s ease,border-color .14s ease,color .14s ease}.strct-pg__btn:hover{background:var(--bg-3)}.strct-pg__btn--active{color:var(--acc);border-color:var(--acc30);background:var(--acc-m)}.strct-pg__btn:disabled{color:var(--t4);cursor:not-allowed;background:transparent}.strct-pg__nav{color:var(--t2)}.strct-pg__dots{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:30px;color:var(--t3)}\n"] }]
2330
2447
  }], propDecorators: { total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }, { type: i0.Output, args: ["pageChange"] }] } });
2331
2448
 
2449
+ let fieldCounter = 0;
2450
+ /**
2451
+ * Form-field wrapper: a label (with optional required marker), the projected
2452
+ * control, and a hint or error message. It auto-links the control via
2453
+ * `aria-describedby` and sets `aria-invalid` when an error is present.
2454
+ *
2455
+ * <strct-field label="Email" required hint="We never share it." [error]="emailError()">
2456
+ * <input strctInput type="email" [(ngModel)]="email" />
2457
+ * </strct-field>
2458
+ */
2459
+ class StrctField {
2460
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2461
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2462
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2463
+ /** Error message (string or first-of array); falsy clears the error state. */
2464
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2465
+ host = inject(ElementRef);
2466
+ n = ++fieldCounter;
2467
+ hintId = `strct-field-hint-${this.n}`;
2468
+ errorId = `strct-field-err-${this.n}`;
2469
+ controlId = signal('', ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
2470
+ errorText = computed(() => {
2471
+ const e = this.error();
2472
+ return (Array.isArray(e) ? e[0] : e) ?? '';
2473
+ }, ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
2474
+ constructor() {
2475
+ afterNextRender(() => this.link());
2476
+ // Keep aria in sync as the error / hint change.
2477
+ effect(() => {
2478
+ this.errorText();
2479
+ this.hint();
2480
+ this.applyAria();
2481
+ });
2482
+ }
2483
+ control() {
2484
+ return this.host.nativeElement.querySelector('input, select, textarea, [strctInput], [strctField]');
2485
+ }
2486
+ link() {
2487
+ const el = this.control();
2488
+ if (!el)
2489
+ return;
2490
+ if (!el.id)
2491
+ el.id = `strct-field-ctrl-${this.n}`;
2492
+ this.controlId.set(el.id);
2493
+ this.applyAria();
2494
+ }
2495
+ applyAria() {
2496
+ const el = this.control();
2497
+ if (!el)
2498
+ return;
2499
+ const describedBy = this.errorText() ? this.errorId : this.hint() ? this.hintId : '';
2500
+ if (describedBy)
2501
+ el.setAttribute('aria-describedby', describedBy);
2502
+ else
2503
+ el.removeAttribute('aria-describedby');
2504
+ if (this.errorText())
2505
+ el.setAttribute('aria-invalid', 'true');
2506
+ else
2507
+ el.removeAttribute('aria-invalid');
2508
+ }
2509
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, deps: [], target: i0.ɵɵFactoryTarget.Component });
2510
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctField, isStandalone: true, selector: "strct-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-field--invalid": "!!errorText()" }, classAttribute: "strct-field" }, ngImport: i0, template: `
2511
+ @if (label()) {
2512
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2513
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2514
+ </label>
2515
+ }
2516
+ <div class="strct-field__control"><ng-content /></div>
2517
+ @if (errorText()) {
2518
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2519
+ {{ errorText() }}
2520
+ </div>
2521
+ } @else if (hint()) {
2522
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2523
+ }
2524
+ `, isInline: true, styles: [".strct-field{display:flex;flex-direction:column;gap:6px}.strct-field__label{font-size:12px;font-weight:600;color:var(--t2)}.strct-field__req{color:var(--crt);margin-left:2px}.strct-field__control{display:flex;flex-direction:column}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint{color:var(--t3)}.strct-field__msg--error{color:var(--crt)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2525
+ }
2526
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, decorators: [{
2527
+ type: Component,
2528
+ args: [{ selector: 'strct-field', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2529
+ @if (label()) {
2530
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2531
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2532
+ </label>
2533
+ }
2534
+ <div class="strct-field__control"><ng-content /></div>
2535
+ @if (errorText()) {
2536
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2537
+ {{ errorText() }}
2538
+ </div>
2539
+ } @else if (hint()) {
2540
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2541
+ }
2542
+ `, host: {
2543
+ class: 'strct-field',
2544
+ '[class.strct-field--invalid]': '!!errorText()',
2545
+ }, styles: [".strct-field{display:flex;flex-direction:column;gap:6px}.strct-field__label{font-size:12px;font-weight:600;color:var(--t2)}.strct-field__req{color:var(--crt);margin-left:2px}.strct-field__control{display:flex;flex-direction:column}.strct-field__msg{font-size:12px;line-height:1.4}.strct-field__msg--hint{color:var(--t3)}.strct-field__msg--error{color:var(--crt)}\n"] }]
2546
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
2547
+
2332
2548
  /**
2333
2549
  * Applies the shared `.strct-control` look to a native input / textarea / select.
2334
2550
  * <input strctInput placeholder="Name" />
@@ -3774,7 +3990,7 @@ class StrctCombobox {
3774
3990
  }
3775
3991
  </div>
3776
3992
  }
3777
- `, 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 });
3778
3994
  }
3779
3995
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCombobox, decorators: [{
3780
3996
  type: Component,
@@ -3828,7 +4044,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
3828
4044
  }
3829
4045
  </div>
3830
4046
  }
3831
- `, 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"] }]
3832
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: [{
3833
4049
  type: HostListener,
3834
4050
  args: ['document:click', ['$event']]
@@ -5930,5 +6146,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
5930
6146
  * Generated bundle index. Do not edit.
5931
6147
  */
5932
6148
 
5933
- export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctChart, StrctCheckbox, StrctChips, StrctColorPicker, StrctCombobox, StrctContextMenu, StrctContextMenuTrigger, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDivider, StrctDonut, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctFile, StrctFooter, StrctGauge, StrctHeader, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKnob, StrctLogin, StrctMenuPanel, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRange, StrctRating, StrctRowDetailDef, StrctShell, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWizard, registerStrctIcon };
6149
+ export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctChart, StrctCheckbox, StrctChips, StrctColorPicker, StrctCombobox, StrctContextMenu, StrctContextMenuTrigger, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDivider, StrctDonut, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctField, StrctFile, StrctFooter, StrctGauge, StrctHeader, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKnob, StrctLogin, StrctMenuPanel, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRange, StrctRating, StrctRowDetailDef, StrctShell, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWizard, registerStrctIcon };
5934
6150
  //# sourceMappingURL=akcelik-strct.mjs.map