@akcelik/strct 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
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, 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, DestroyRef, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, TemplateRef, contentChild, Renderer2 } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
+ import { DOCUMENT as DOCUMENT$1, NgTemplateOutlet } from '@angular/common';
4
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
- import { NgTemplateOutlet } from '@angular/common';
6
6
 
7
7
  /** All palettes the token system ships with, in display order. */
8
8
  const STRCT_PALETTES = [
@@ -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 {
@@ -1116,45 +1130,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1116
1130
  `, host: { class: 'strct-tabs' }, styles: [".strct-tabs{display:block}.strct-tabs__bar{display:flex;gap:2px;border-bottom:1px solid var(--b2)}.strct-tabs__btn{appearance:none;border:0;background:transparent;cursor:pointer;font-family:var(--font);font-size:13px;font-weight:500;color:var(--t2);padding:9px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .14s ease,border-color .14s ease}.strct-tabs__btn:hover{color:var(--t1)}.strct-tabs__btn--active{color:var(--acc);border-bottom-color:var(--acc)}.strct-tabs__btn:disabled{color:var(--t4);cursor:not-allowed}.strct-tabs__panels{padding-top:16px}\n"] }]
1117
1131
  }], ctorParameters: () => [], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTab), { isSignal: true }] }] } });
1118
1132
 
1119
- /** Root container for a tree of `<strct-tree-node>` items. */
1120
- class StrctTree {
1121
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
1122
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.16", type: StrctTree, isStandalone: true, selector: "strct-tree", host: { attributes: { "role": "tree" }, classAttribute: "strct-tree" }, ngImport: i0, template: `<ng-content />`, isInline: true, styles: [".strct-tree{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1123
- }
1124
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, decorators: [{
1125
- type: Component,
1126
- args: [{ selector: 'strct-tree', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `<ng-content />`, host: { class: 'strct-tree', role: 'tree' }, styles: [".strct-tree{display:block}\n"] }]
1127
- }] });
1128
1133
  /**
1129
- * Tree node. Nest `<strct-tree-node>` children to build hierarchy:
1130
- * <strct-tree-node label="Group" [(expanded)]="open">
1131
- * <strct-tree-node label="Leaf" icon="grid" [active]="true" />
1134
+ * Tree node. Two modes:
1135
+ * - **Content:** nest `<strct-tree-node>` children manually.
1136
+ * - **Data:** pass a `[node]` object that recurses over its `children` —
1137
+ * used internally by `<strct-tree [nodes]>`.
1138
+ *
1139
+ * <strct-tree-node label="Group" icon="layers" badge="ok" [(expanded)]="open">
1140
+ * <strct-tree-node label="Leaf" icon="vm" [active]="true" />
1132
1141
  * </strct-tree-node>
1133
1142
  */
1134
1143
  class StrctTreeNode {
1135
- label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1144
+ /** Data-driven node; when set, label/icon/children come from it. */
1145
+ node = input(null, ...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
1146
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1136
1147
  icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1148
+ badge = input('none', ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
1137
1149
  active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1138
1150
  expanded = model(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1151
+ /** Content-mode click. */
1139
1152
  activated = output();
1153
+ /** Data-mode click — carries the activated node (bubbles to the tree). */
1154
+ nodeActivated = output();
1140
1155
  childNodes = contentChildren(StrctTreeNode, ...(ngDevMode ? [{ debugName: "childNodes" }] : /* istanbul ignore next */ []));
1141
- hasChildren = computed(() => this.childNodes().length > 0, ...(ngDevMode ? [{ debugName: "hasChildren" }] : /* istanbul ignore next */ []));
1156
+ /** Data-mode expansion (seeded from node.expanded on first toggle). */
1157
+ dataExpanded = signal(null, ...(ngDevMode ? [{ debugName: "dataExpanded" }] : /* istanbul ignore next */ []));
1158
+ displayLabel = computed(() => this.node()?.label ?? this.label(), ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
1159
+ displayIcon = computed(() => this.node()?.icon ?? this.icon(), ...(ngDevMode ? [{ debugName: "displayIcon" }] : /* istanbul ignore next */ []));
1160
+ displayBadge = computed(() => this.node()?.badge ?? this.badge(), ...(ngDevMode ? [{ debugName: "displayBadge" }] : /* istanbul ignore next */ []));
1161
+ displayActive = computed(() => this.node()?.active ?? this.active(), ...(ngDevMode ? [{ debugName: "displayActive" }] : /* istanbul ignore next */ []));
1162
+ hasChildren = computed(() => {
1163
+ const n = this.node();
1164
+ return n ? (n.children?.length ?? 0) > 0 : this.childNodes().length > 0;
1165
+ }, ...(ngDevMode ? [{ debugName: "hasChildren" }] : /* istanbul ignore next */ []));
1166
+ isOpen = computed(() => {
1167
+ if (this.node())
1168
+ return this.dataExpanded() ?? this.node().expanded ?? false;
1169
+ return this.expanded();
1170
+ }, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1142
1171
  toggle() {
1143
- this.expanded.update((v) => !v);
1172
+ if (this.node()) {
1173
+ this.dataExpanded.set(!this.isOpen());
1174
+ }
1175
+ else {
1176
+ this.expanded.update((v) => !v);
1177
+ }
1178
+ }
1179
+ onActivate() {
1180
+ const n = this.node();
1181
+ if (n)
1182
+ this.nodeActivated.emit(n);
1183
+ else
1184
+ this.activated.emit();
1144
1185
  }
1145
1186
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Component });
1146
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTreeNode, isStandalone: true, selector: "strct-tree-node", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", 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" }, host: { classAttribute: "strct-tnode" }, queries: [{ propertyName: "childNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
1187
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTreeNode, isStandalone: true, selector: "strct-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expanded: "expandedChange", activated: "activated", nodeActivated: "nodeActivated" }, host: { classAttribute: "strct-tnode" }, queries: [{ propertyName: "childNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
1147
1188
  <div
1148
1189
  class="strct-tnode__row"
1149
- [class.strct-tnode__row--active]="active()"
1190
+ [class.strct-tnode__row--active]="displayActive()"
1150
1191
  role="treeitem"
1151
- [attr.aria-expanded]="hasChildren() ? expanded() : null"
1152
- (click)="activated.emit()"
1192
+ [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1193
+ (click)="onActivate()"
1153
1194
  >
1154
1195
  @if (hasChildren()) {
1155
1196
  <span
1156
1197
  class="strct-tnode__chevron"
1157
- [class.strct-tnode__chevron--open]="expanded()"
1198
+ [class.strct-tnode__chevron--open]="isOpen()"
1158
1199
  (click)="$event.stopPropagation(); toggle()"
1159
1200
  >
1160
1201
  <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
@@ -1162,33 +1203,45 @@ class StrctTreeNode {
1162
1203
  } @else {
1163
1204
  <span class="strct-tnode__spacer"></span>
1164
1205
  }
1165
- @if (icon()) {
1166
- <strct-icon class="strct-tnode__icon" [name]="icon()!" [size]="14" [strokeWidth]="1.3" />
1206
+ @if (displayIcon()) {
1207
+ <strct-icon
1208
+ class="strct-tnode__icon"
1209
+ [name]="displayIcon()!"
1210
+ [size]="14"
1211
+ [strokeWidth]="1.3"
1212
+ [badge]="displayBadge()"
1213
+ />
1167
1214
  }
1168
- <span class="strct-tnode__label">{{ label() }}</span>
1215
+ <span class="strct-tnode__label">{{ displayLabel() }}</span>
1169
1216
  <ng-content select="[strctTreeTrailing]" />
1170
1217
  </div>
1171
- @if (hasChildren() && expanded()) {
1218
+ @if (hasChildren() && isOpen()) {
1172
1219
  <div class="strct-tnode__children" role="group">
1173
- <ng-content />
1220
+ @if (node()) {
1221
+ @for (child of node()!.children ?? []; track $index) {
1222
+ <strct-tree-node [node]="child" (nodeActivated)="nodeActivated.emit($event)" />
1223
+ }
1224
+ } @else {
1225
+ <ng-content />
1226
+ }
1174
1227
  </div>
1175
1228
  }
1176
- `, 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: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1229
+ `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:7px;padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;color:var(--t3);transition:transform .15s ease;width:14px;justify-content:center}.strct-tnode__chevron--open{transform:rotate(90deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-left:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded"], outputs: ["expandedChange", "activated", "nodeActivated"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1177
1230
  }
1178
1231
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTreeNode, decorators: [{
1179
1232
  type: Component,
1180
1233
  args: [{ selector: 'strct-tree-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1181
1234
  <div
1182
1235
  class="strct-tnode__row"
1183
- [class.strct-tnode__row--active]="active()"
1236
+ [class.strct-tnode__row--active]="displayActive()"
1184
1237
  role="treeitem"
1185
- [attr.aria-expanded]="hasChildren() ? expanded() : null"
1186
- (click)="activated.emit()"
1238
+ [attr.aria-expanded]="hasChildren() ? isOpen() : null"
1239
+ (click)="onActivate()"
1187
1240
  >
1188
1241
  @if (hasChildren()) {
1189
1242
  <span
1190
1243
  class="strct-tnode__chevron"
1191
- [class.strct-tnode__chevron--open]="expanded()"
1244
+ [class.strct-tnode__chevron--open]="isOpen()"
1192
1245
  (click)="$event.stopPropagation(); toggle()"
1193
1246
  >
1194
1247
  <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
@@ -1196,21 +1249,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1196
1249
  } @else {
1197
1250
  <span class="strct-tnode__spacer"></span>
1198
1251
  }
1199
- @if (icon()) {
1200
- <strct-icon class="strct-tnode__icon" [name]="icon()!" [size]="14" [strokeWidth]="1.3" />
1252
+ @if (displayIcon()) {
1253
+ <strct-icon
1254
+ class="strct-tnode__icon"
1255
+ [name]="displayIcon()!"
1256
+ [size]="14"
1257
+ [strokeWidth]="1.3"
1258
+ [badge]="displayBadge()"
1259
+ />
1201
1260
  }
1202
- <span class="strct-tnode__label">{{ label() }}</span>
1261
+ <span class="strct-tnode__label">{{ displayLabel() }}</span>
1203
1262
  <ng-content select="[strctTreeTrailing]" />
1204
1263
  </div>
1205
- @if (hasChildren() && expanded()) {
1264
+ @if (hasChildren() && isOpen()) {
1206
1265
  <div class="strct-tnode__children" role="group">
1207
- <ng-content />
1266
+ @if (node()) {
1267
+ @for (child of node()!.children ?? []; track $index) {
1268
+ <strct-tree-node [node]="child" (nodeActivated)="nodeActivated.emit($event)" />
1269
+ }
1270
+ } @else {
1271
+ <ng-content />
1272
+ }
1208
1273
  </div>
1209
1274
  }
1210
1275
  `, 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"] }]
1211
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", 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"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }] } });
1276
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], activated: [{ type: i0.Output, args: ["activated"] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }] } });
1277
+ /**
1278
+ * Root container for a tree. Either project `<strct-tree-node>` children, or
1279
+ * pass `[nodes]` for a fully data-driven, self-recursing tree:
1280
+ * <strct-tree [nodes]="roots" (nodeActivated)="select($event)" />
1281
+ */
1282
+ class StrctTree {
1283
+ /** Data-driven node list; when set, projected content is ignored. */
1284
+ nodes = input(null, ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
1285
+ /** Emitted when any data-driven node is clicked. */
1286
+ nodeActivated = output();
1287
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
1288
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeActivated: "nodeActivated" }, host: { attributes: { "role": "tree" }, classAttribute: "strct-tree" }, ngImport: i0, template: `
1289
+ @if (nodes(); as ns) {
1290
+ @for (n of ns; track $index) {
1291
+ <strct-tree-node [node]="n" (nodeActivated)="nodeActivated.emit($event)" />
1292
+ }
1293
+ } @else {
1294
+ <ng-content />
1295
+ }
1296
+ `, isInline: true, styles: [".strct-tree{display:block}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded"], outputs: ["expandedChange", "activated", "nodeActivated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1297
+ }
1298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTree, decorators: [{
1299
+ type: Component,
1300
+ args: [{ selector: 'strct-tree', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctTreeNode], template: `
1301
+ @if (nodes(); as ns) {
1302
+ @for (n of ns; track $index) {
1303
+ <strct-tree-node [node]="n" (nodeActivated)="nodeActivated.emit($event)" />
1304
+ }
1305
+ } @else {
1306
+ <ng-content />
1307
+ }
1308
+ `, host: { class: 'strct-tree', role: 'tree' }, styles: [".strct-tree{display:block}\n"] }]
1309
+ }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }] } });
1212
1310
 
1213
1311
  let modalCounter = 0;
1312
+ // Body scroll-lock shared across any number of simultaneously open modals.
1313
+ let scrollLockCount = 0;
1314
+ let savedBodyOverflow = '';
1315
+ function lockBodyScroll(doc) {
1316
+ if (scrollLockCount === 0) {
1317
+ savedBodyOverflow = doc.body.style.overflow;
1318
+ doc.body.style.overflow = 'hidden';
1319
+ }
1320
+ scrollLockCount++;
1321
+ }
1322
+ function unlockBodyScroll(doc) {
1323
+ scrollLockCount = Math.max(0, scrollLockCount - 1);
1324
+ if (scrollLockCount === 0)
1325
+ doc.body.style.overflow = savedBodyOverflow;
1326
+ }
1214
1327
  /**
1215
1328
  * Overlay dialog with two-way `open`:
1216
1329
  * <strct-modal [(open)]="show" title="Confirm">
@@ -1233,16 +1346,32 @@ class StrctModal {
1233
1346
  titleId = `strct-modal-${++modalCounter}`;
1234
1347
  /** Element that had focus before the dialog opened, restored on close. */
1235
1348
  previousActive = null;
1349
+ /** Whether this instance currently holds a scroll lock. */
1350
+ locked = false;
1236
1351
  constructor() {
1237
1352
  effect(() => {
1238
- if (this.open()) {
1353
+ const open = this.open();
1354
+ if (open && !this.locked) {
1355
+ this.locked = true;
1356
+ lockBodyScroll(this.doc);
1239
1357
  this.previousActive = this.doc.activeElement;
1240
1358
  // Move focus into the dialog once it has rendered.
1241
1359
  setTimeout(() => this.focusInitial());
1242
1360
  }
1243
- else if (this.previousActive) {
1244
- this.previousActive.focus?.();
1245
- this.previousActive = null;
1361
+ else if (!open && this.locked) {
1362
+ this.locked = false;
1363
+ unlockBodyScroll(this.doc);
1364
+ if (this.previousActive) {
1365
+ this.previousActive.focus?.();
1366
+ this.previousActive = null;
1367
+ }
1368
+ }
1369
+ });
1370
+ // Release the lock if the modal is destroyed while still open.
1371
+ inject(DestroyRef).onDestroy(() => {
1372
+ if (this.locked) {
1373
+ this.locked = false;
1374
+ unlockBodyScroll(this.doc);
1246
1375
  }
1247
1376
  });
1248
1377
  }
@@ -1543,28 +1672,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1543
1672
  }] } });
1544
1673
 
1545
1674
  /**
1546
- * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Reuse
1547
- * `strct-dropdown-item` for the nested entries.
1675
+ * A nested fly-out inside a `strct-context-menu` or `strct-dropdown`. Opens on
1676
+ * hover, click/tap, or the keyboard (Enter / Space / →), and flips to the left
1677
+ * near the right edge of the viewport. Reuse `strct-dropdown-item` for entries.
1548
1678
  * <strct-submenu label="Power">
1549
1679
  * <strct-dropdown-item>Power on</strct-dropdown-item>
1550
1680
  * <strct-dropdown-item>Power off</strct-dropdown-item>
1551
1681
  * </strct-submenu>
1552
1682
  */
1553
1683
  class StrctSubmenu {
1684
+ host = inject(ElementRef);
1554
1685
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1555
1686
  /** Optional leading icon; when omitted the icon column is still reserved so
1556
1687
  * the label stays aligned with sibling items that do have icons. */
1557
1688
  icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
1558
1689
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1690
+ /** Open to the left when the fly-out would overflow the right edge. */
1691
+ flip = signal(false, ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
1692
+ setOpen(value) {
1693
+ if (value) {
1694
+ const rect = this.host.nativeElement.getBoundingClientRect();
1695
+ this.flip.set(rect.right + 190 > window.innerWidth);
1696
+ }
1697
+ this.open.set(value);
1698
+ }
1559
1699
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
1560
1700
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSubmenu, isStandalone: true, selector: "strct-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-submenu-host" }, ngImport: i0, template: `
1561
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1701
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1562
1702
  <div
1563
1703
  class="strct-submenu__trigger"
1564
1704
  role="menuitem"
1705
+ tabindex="0"
1565
1706
  aria-haspopup="menu"
1566
1707
  [attr.aria-expanded]="open()"
1567
- (click)="$event.stopPropagation()"
1708
+ (click)="$event.stopPropagation(); setOpen(!open())"
1709
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1710
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1711
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1712
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1713
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
1568
1714
  >
1569
1715
  @if (icon()) {
1570
1716
  <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
@@ -1575,21 +1721,29 @@ class StrctSubmenu {
1575
1721
  <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1576
1722
  </div>
1577
1723
  @if (open()) {
1578
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1724
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1725
+ <ng-content />
1726
+ </div>
1579
1727
  }
1580
1728
  </div>
1581
- `, 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 });
1729
+ `, isInline: true, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1582
1730
  }
1583
1731
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSubmenu, decorators: [{
1584
1732
  type: Component,
1585
1733
  args: [{ selector: 'strct-submenu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1586
- <div class="strct-submenu" (mouseenter)="open.set(true)" (mouseleave)="open.set(false)">
1734
+ <div class="strct-submenu" (mouseenter)="setOpen(true)" (mouseleave)="open.set(false)">
1587
1735
  <div
1588
1736
  class="strct-submenu__trigger"
1589
1737
  role="menuitem"
1738
+ tabindex="0"
1590
1739
  aria-haspopup="menu"
1591
1740
  [attr.aria-expanded]="open()"
1592
- (click)="$event.stopPropagation()"
1741
+ (click)="$event.stopPropagation(); setOpen(!open())"
1742
+ (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1743
+ (keydown.space)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1744
+ (keydown.arrowright)="$event.preventDefault(); $event.stopPropagation(); setOpen(true)"
1745
+ (keydown.arrowleft)="$event.stopPropagation(); open.set(false)"
1746
+ (keydown.escape)="$event.stopPropagation(); open.set(false)"
1593
1747
  >
1594
1748
  @if (icon()) {
1595
1749
  <strct-icon class="strct-submenu__icon" [name]="icon()" [size]="14" [strokeWidth]="1.3" />
@@ -1600,15 +1754,349 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1600
1754
  <strct-icon class="strct-submenu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1601
1755
  </div>
1602
1756
  @if (open()) {
1603
- <div class="strct-submenu__panel" role="menu"><ng-content /></div>
1757
+ <div class="strct-submenu__panel" [class.strct-submenu__panel--flip]="flip()" role="menu">
1758
+ <ng-content />
1759
+ </div>
1604
1760
  }
1605
1761
  </div>
1606
- `, 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"] }]
1762
+ `, host: { class: 'strct-submenu-host' }, styles: [".strct-submenu{position:relative}.strct-submenu__trigger{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 10px;border-radius:5px;cursor:default;font-size:13px;color:var(--t1)}.strct-submenu__trigger:hover{background:var(--bg-3)}.strct-submenu__trigger:focus-visible{outline:none;background:var(--bg-3)}.strct-submenu__icon{color:var(--t2);flex-shrink:0}.strct-submenu__icon-spacer{width:14px;flex-shrink:0}.strct-submenu__label{flex:1;display:inline-flex;align-items:center;gap:8px}.strct-submenu__arrow{color:var(--t3)}.strct-submenu__panel{position:absolute;top:-5px;left:100%;z-index:1;min-width:170px;margin-left:2px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-submenu-in .1s ease}.strct-submenu__panel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-submenu-in{0%{opacity:0;transform:translate(-4px)}}\n"] }]
1607
1763
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } });
1608
1764
 
1765
+ /**
1766
+ * Floating menu panel — portaled into `<body>` (so it escapes overflow /
1767
+ * transform clipping), positioned by its real measured size, with full keyboard
1768
+ * navigation and recursive submenus. Usually created by `[strctContextMenu]`,
1769
+ * but can be embedded directly with `submenu`.
1770
+ */
1771
+ class StrctMenuPanel {
1772
+ host = inject(ElementRef);
1773
+ items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1774
+ data = input(undefined, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1775
+ x = input(0, ...(ngDevMode ? [{ debugName: "x" }] : /* istanbul ignore next */ []));
1776
+ y = input(0, ...(ngDevMode ? [{ debugName: "y" }] : /* istanbul ignore next */ []));
1777
+ submenu = input(false, { ...(ngDevMode ? { debugName: "submenu" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1778
+ select = output();
1779
+ close = output();
1780
+ /** ArrowLeft inside a submenu — asks the parent to close it. */
1781
+ back = output();
1782
+ posX = signal(0, ...(ngDevMode ? [{ debugName: "posX" }] : /* istanbul ignore next */ []));
1783
+ posY = signal(0, ...(ngDevMode ? [{ debugName: "posY" }] : /* istanbul ignore next */ []));
1784
+ flipLeft = signal(false, ...(ngDevMode ? [{ debugName: "flipLeft" }] : /* istanbul ignore next */ []));
1785
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1786
+ openSubIndex = signal(null, ...(ngDevMode ? [{ debugName: "openSubIndex" }] : /* istanbul ignore next */ []));
1787
+ navIndices = computed(() => this.items()
1788
+ .map((it, i) => (it.divider ? -1 : i))
1789
+ .filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "navIndices" }] : /* istanbul ignore next */ []));
1790
+ constructor() {
1791
+ this.posX.set(this.x());
1792
+ this.posY.set(this.y());
1793
+ afterNextRender(() => {
1794
+ this.activeIndex.set(this.navIndices()[0] ?? 0);
1795
+ if (!this.submenu())
1796
+ this.clampToViewport();
1797
+ this.focusItem(this.activeIndex());
1798
+ });
1799
+ }
1800
+ clampToViewport() {
1801
+ const host = this.host.nativeElement;
1802
+ const w = host.offsetWidth;
1803
+ const h = host.offsetHeight;
1804
+ const vw = window.innerWidth;
1805
+ const vh = window.innerHeight;
1806
+ const m = 6;
1807
+ let nx = this.x();
1808
+ let ny = this.y();
1809
+ if (nx + w > vw - m)
1810
+ nx = Math.max(m, Math.min(this.x() - w, vw - w - m));
1811
+ if (ny + h > vh - m)
1812
+ ny = Math.max(m, vh - h - m);
1813
+ this.posX.set(nx);
1814
+ this.posY.set(ny);
1815
+ // Submenus of a panel near the right edge open to the left.
1816
+ this.flipLeft.set(nx + w > vw - 220);
1817
+ }
1818
+ focusItem(i) {
1819
+ this.activeIndex.set(i);
1820
+ this.host.nativeElement
1821
+ .querySelector(`.strct-menu__item[data-idx="${i}"]`)
1822
+ ?.focus();
1823
+ }
1824
+ move(dir) {
1825
+ const nav = this.navIndices();
1826
+ if (!nav.length)
1827
+ return;
1828
+ const pos = nav.indexOf(this.activeIndex());
1829
+ const next = nav[(pos + dir + nav.length) % nav.length];
1830
+ this.openSubIndex.set(null);
1831
+ this.focusItem(next);
1832
+ }
1833
+ onHover(i) {
1834
+ this.activeIndex.set(i);
1835
+ const it = this.items()[i];
1836
+ this.openSubIndex.set(it?.children?.length ? i : null);
1837
+ }
1838
+ onLeave(i) {
1839
+ if (this.openSubIndex() === i)
1840
+ this.openSubIndex.set(null);
1841
+ }
1842
+ onItemClick(item, i, event) {
1843
+ event.stopPropagation();
1844
+ if (item.disabled)
1845
+ return;
1846
+ if (item.children?.length) {
1847
+ this.openSubIndex.set(this.openSubIndex() === i ? null : i);
1848
+ this.focusItem(i);
1849
+ }
1850
+ else {
1851
+ this.select.emit(item);
1852
+ }
1853
+ }
1854
+ closeSub() {
1855
+ this.openSubIndex.set(null);
1856
+ }
1857
+ onKeydown(event) {
1858
+ const key = event.key;
1859
+ const item = this.items()[this.activeIndex()];
1860
+ switch (key) {
1861
+ case 'ArrowDown':
1862
+ event.preventDefault();
1863
+ event.stopPropagation();
1864
+ this.move(1);
1865
+ break;
1866
+ case 'ArrowUp':
1867
+ event.preventDefault();
1868
+ event.stopPropagation();
1869
+ this.move(-1);
1870
+ break;
1871
+ case 'Home':
1872
+ event.preventDefault();
1873
+ event.stopPropagation();
1874
+ this.focusItem(this.navIndices()[0] ?? 0);
1875
+ break;
1876
+ case 'End':
1877
+ event.preventDefault();
1878
+ event.stopPropagation();
1879
+ this.focusItem(this.navIndices().at(-1) ?? 0);
1880
+ break;
1881
+ case 'ArrowRight':
1882
+ if (item?.children?.length) {
1883
+ event.preventDefault();
1884
+ event.stopPropagation();
1885
+ this.openSubIndex.set(this.activeIndex());
1886
+ }
1887
+ break;
1888
+ case 'ArrowLeft':
1889
+ event.preventDefault();
1890
+ event.stopPropagation();
1891
+ if (this.openSubIndex() != null)
1892
+ this.closeSub();
1893
+ else if (this.submenu())
1894
+ this.back.emit();
1895
+ break;
1896
+ case 'Enter':
1897
+ case ' ':
1898
+ event.preventDefault();
1899
+ event.stopPropagation();
1900
+ if (item && !item.disabled) {
1901
+ if (item.children?.length)
1902
+ this.openSubIndex.set(this.activeIndex());
1903
+ else
1904
+ this.select.emit(item);
1905
+ }
1906
+ break;
1907
+ case 'Escape':
1908
+ event.preventDefault();
1909
+ event.stopPropagation();
1910
+ this.close.emit();
1911
+ break;
1912
+ }
1913
+ }
1914
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
1915
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctMenuPanel, isStandalone: true, selector: "strct-menu-panel", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: false, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: false, transformFunction: null }, submenu: { classPropertyName: "submenu", publicName: "submenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", close: "close", back: "back" }, host: { properties: { "style.position": "submenu() ? null : 'fixed'", "style.left.px": "submenu() ? null : posX()", "style.top.px": "submenu() ? null : posY()", "style.zIndex": "submenu() ? null : 1100" }, classAttribute: "strct-menu-host" }, ngImport: i0, template: `
1916
+ <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1917
+ @for (item of items(); track $index; let i = $index) {
1918
+ @if (item.divider) {
1919
+ <div class="strct-menu__sep" role="separator"></div>
1920
+ } @else {
1921
+ <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1922
+ <button
1923
+ type="button"
1924
+ class="strct-menu__item"
1925
+ [attr.data-idx]="i"
1926
+ [class.strct-menu__item--danger]="item.danger"
1927
+ [class.strct-menu__item--active]="i === activeIndex()"
1928
+ [disabled]="item.disabled"
1929
+ role="menuitem"
1930
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1931
+ [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1932
+ [attr.tabindex]="i === activeIndex() ? 0 : -1"
1933
+ (click)="onItemClick(item, i, $event)"
1934
+ >
1935
+ @if (item.icon) {
1936
+ <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1937
+ } @else {
1938
+ <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1939
+ }
1940
+ <span class="strct-menu__label">{{ item.label }}</span>
1941
+ @if (item.children?.length) {
1942
+ <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1943
+ }
1944
+ </button>
1945
+ @if (openSubIndex() === i && item.children?.length) {
1946
+ <strct-menu-panel
1947
+ submenu
1948
+ class="strct-menu__subpanel"
1949
+ [class.strct-menu__subpanel--flip]="flipLeft()"
1950
+ [items]="item.children!"
1951
+ [data]="data()"
1952
+ (select)="select.emit($event)"
1953
+ (close)="close.emit()"
1954
+ (back)="closeSub(); focusItem(i)"
1955
+ />
1956
+ }
1957
+ </div>
1958
+ }
1959
+ }
1960
+ </div>
1961
+ `, isInline: true, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"], dependencies: [{ kind: "component", type: StrctMenuPanel, selector: "strct-menu-panel", inputs: ["items", "data", "x", "y", "submenu"], outputs: ["select", "close", "back"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1962
+ }
1963
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenuPanel, decorators: [{
1964
+ type: Component,
1965
+ args: [{ selector: 'strct-menu-panel', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
1966
+ <div class="strct-menu" role="menu" tabindex="-1" (keydown)="onKeydown($event)">
1967
+ @for (item of items(); track $index; let i = $index) {
1968
+ @if (item.divider) {
1969
+ <div class="strct-menu__sep" role="separator"></div>
1970
+ } @else {
1971
+ <div class="strct-menu__wrap" (mouseenter)="onHover(i)" (mouseleave)="onLeave(i)">
1972
+ <button
1973
+ type="button"
1974
+ class="strct-menu__item"
1975
+ [attr.data-idx]="i"
1976
+ [class.strct-menu__item--danger]="item.danger"
1977
+ [class.strct-menu__item--active]="i === activeIndex()"
1978
+ [disabled]="item.disabled"
1979
+ role="menuitem"
1980
+ [attr.aria-haspopup]="item.children?.length ? 'menu' : null"
1981
+ [attr.aria-expanded]="item.children?.length ? openSubIndex() === i : null"
1982
+ [attr.tabindex]="i === activeIndex() ? 0 : -1"
1983
+ (click)="onItemClick(item, i, $event)"
1984
+ >
1985
+ @if (item.icon) {
1986
+ <strct-icon class="strct-menu__icon" [name]="item.icon" [size]="14" [strokeWidth]="1.3" />
1987
+ } @else {
1988
+ <span class="strct-menu__icon-spacer" aria-hidden="true"></span>
1989
+ }
1990
+ <span class="strct-menu__label">{{ item.label }}</span>
1991
+ @if (item.children?.length) {
1992
+ <strct-icon class="strct-menu__arrow" name="chevronRight" [size]="12" [strokeWidth]="1.6" />
1993
+ }
1994
+ </button>
1995
+ @if (openSubIndex() === i && item.children?.length) {
1996
+ <strct-menu-panel
1997
+ submenu
1998
+ class="strct-menu__subpanel"
1999
+ [class.strct-menu__subpanel--flip]="flipLeft()"
2000
+ [items]="item.children!"
2001
+ [data]="data()"
2002
+ (select)="select.emit($event)"
2003
+ (close)="close.emit()"
2004
+ (back)="closeSub(); focusItem(i)"
2005
+ />
2006
+ }
2007
+ </div>
2008
+ }
2009
+ }
2010
+ </div>
2011
+ `, host: {
2012
+ class: 'strct-menu-host',
2013
+ '[style.position]': "submenu() ? null : 'fixed'",
2014
+ '[style.left.px]': 'submenu() ? null : posX()',
2015
+ '[style.top.px]': 'submenu() ? null : posY()',
2016
+ '[style.zIndex]': 'submenu() ? null : 1100',
2017
+ }, styles: [".strct-menu-host{display:block}.strct-menu{min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-menu-in .1s ease}.strct-menu:focus{outline:none}.strct-menu__wrap{position:relative}.strct-menu__item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 10px;border:0;border-radius:5px;cursor:pointer;background:transparent;color:var(--t1);font-size:13px;font-family:var(--font);text-align:left}.strct-menu__item:hover:not(:disabled),.strct-menu__item--active:not(:disabled){background:var(--bg-3)}.strct-menu__item:focus-visible{outline:none;background:var(--bg-3)}.strct-menu__item--danger{color:var(--crt)}.strct-menu__item--danger:hover:not(:disabled),.strct-menu__item--danger.strct-menu__item--active:not(:disabled){background:var(--crt-bg)}.strct-menu__item:disabled{opacity:.45;cursor:not-allowed}.strct-menu__icon{color:var(--t2);flex-shrink:0}.strct-menu__item--danger .strct-menu__icon{color:var(--crt)}.strct-menu__icon-spacer{width:14px;flex-shrink:0}.strct-menu__label{flex:1;white-space:nowrap}.strct-menu__arrow{color:var(--t3);flex-shrink:0}.strct-menu__sep{height:1px;margin:4px 6px;background:var(--b1)}.strct-menu__subpanel{position:absolute;top:-5px;left:100%;margin-left:2px;z-index:1}.strct-menu__subpanel--flip{left:auto;right:100%;margin-left:0;margin-right:2px}@keyframes strct-menu-in{0%{opacity:0;transform:scale(.97)}}\n"] }]
2018
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], x: [{ type: i0.Input, args: [{ isSignal: true, alias: "x", required: false }] }], y: [{ type: i0.Input, args: [{ isSignal: true, alias: "y", required: false }] }], submenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenu", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], close: [{ type: i0.Output, args: ["close"] }], back: [{ type: i0.Output, args: ["back"] }] } });
2019
+ /**
2020
+ * Right-click (context) menu driven by a data array. Attach to any trigger; the
2021
+ * menu portals into `<body>` and runs each item's `action` on selection.
2022
+ * <div [strctContextMenu]="menuFor(host)" [strctContextMenuData]="host"
2023
+ * (menuSelect)="onPick($event)">…</div>
2024
+ */
2025
+ class StrctContextMenuTrigger {
2026
+ appRef = inject(ApplicationRef);
2027
+ envInjector = inject(EnvironmentInjector);
2028
+ zone = inject(NgZone);
2029
+ doc = inject(DOCUMENT$1);
2030
+ items = input.required({ ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), alias: 'strctContextMenu' });
2031
+ data = input(undefined, { ...(ngDevMode ? { debugName: "data" } : /* istanbul ignore next */ {}), alias: 'strctContextMenuData' });
2032
+ menuSelect = output();
2033
+ ref = null;
2034
+ onClose = () => this.zone.run(() => this.closeMenu());
2035
+ onContextMenu(event) {
2036
+ if (!this.items()?.length)
2037
+ return;
2038
+ event.preventDefault();
2039
+ this.openAt(event.clientX, event.clientY);
2040
+ }
2041
+ openAt(x, y) {
2042
+ this.closeMenu();
2043
+ const ref = createComponent(StrctMenuPanel, { environmentInjector: this.envInjector });
2044
+ ref.setInput('items', this.items());
2045
+ ref.setInput('data', this.data());
2046
+ ref.setInput('x', x);
2047
+ ref.setInput('y', y);
2048
+ ref.instance.select.subscribe((item) => {
2049
+ item.action?.(this.data());
2050
+ this.menuSelect.emit(item);
2051
+ this.closeMenu();
2052
+ });
2053
+ ref.instance.close.subscribe(() => this.closeMenu());
2054
+ this.appRef.attachView(ref.hostView);
2055
+ this.doc.body.appendChild(ref.location.nativeElement);
2056
+ this.ref = ref;
2057
+ // Defer global listeners so the opening right-click doesn't immediately close.
2058
+ setTimeout(() => {
2059
+ this.zone.runOutsideAngular(() => {
2060
+ this.doc.addEventListener('mousedown', this.onOutside, true);
2061
+ window.addEventListener('scroll', this.onClose, true);
2062
+ window.addEventListener('resize', this.onClose);
2063
+ });
2064
+ });
2065
+ }
2066
+ onOutside = (event) => {
2067
+ if (this.ref && !this.ref.location.nativeElement.contains(event.target)) {
2068
+ this.onClose();
2069
+ }
2070
+ };
2071
+ closeMenu() {
2072
+ if (!this.ref)
2073
+ return;
2074
+ this.doc.removeEventListener('mousedown', this.onOutside, true);
2075
+ window.removeEventListener('scroll', this.onClose, true);
2076
+ window.removeEventListener('resize', this.onClose);
2077
+ this.appRef.detachView(this.ref.hostView);
2078
+ this.ref.destroy();
2079
+ this.ref = null;
2080
+ }
2081
+ ngOnDestroy() {
2082
+ this.closeMenu();
2083
+ }
2084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2085
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctContextMenuTrigger, isStandalone: true, selector: "[strctContextMenu]", inputs: { items: { classPropertyName: "items", publicName: "strctContextMenu", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "strctContextMenuData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuSelect: "menuSelect" }, host: { listeners: { "contextmenu": "onContextMenu($event)" } }, ngImport: i0 });
2086
+ }
2087
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctContextMenuTrigger, decorators: [{
2088
+ type: Directive,
2089
+ args: [{ selector: '[strctContextMenu]' }]
2090
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenu", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctContextMenuData", required: false }] }], menuSelect: [{ type: i0.Output, args: ["menuSelect"] }], onContextMenu: [{
2091
+ type: HostListener,
2092
+ args: ['contextmenu', ['$event']]
2093
+ }] } });
2094
+
1609
2095
  /** A single wizard step. `label` names it in the step header. */
1610
2096
  class StrctStep {
1611
2097
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2098
+ /** When false, the wizard's Next / Finish is disabled on this step. */
2099
+ canAdvance = input(true, { ...(ngDevMode ? { debugName: "canAdvance" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1612
2100
  _active = signal(false, ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
1613
2101
  active = this._active.asReadonly();
1614
2102
  /** @internal */
@@ -1616,7 +2104,7 @@ class StrctStep {
1616
2104
  this._active.set(value);
1617
2105
  }
1618
2106
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctStep, deps: [], target: i0.ɵɵFactoryTarget.Component });
1619
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctStep, isStandalone: true, selector: "strct-step", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "hidden": "!active()" }, classAttribute: "strct-step" }, ngImport: i0, template: `@if (active()) {
2107
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctStep, isStandalone: true, selector: "strct-step", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, canAdvance: { classPropertyName: "canAdvance", publicName: "canAdvance", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "hidden": "!active()" }, classAttribute: "strct-step" }, ngImport: i0, template: `@if (active()) {
1620
2108
  <ng-content />
1621
2109
  }`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1622
2110
  }
@@ -1630,13 +2118,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1630
2118
  }`,
1631
2119
  host: { class: 'strct-step', '[hidden]': '!active()' },
1632
2120
  }]
1633
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }] } });
2121
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], canAdvance: [{ type: i0.Input, args: [{ isSignal: true, alias: "canAdvance", required: false }] }] } });
1634
2122
  /** Multi-step flow with a numbered header and Back / Next / Finish controls. */
1635
2123
  class StrctWizard {
1636
2124
  steps = contentChildren(StrctStep, ...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
1637
2125
  current = signal(0, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
2126
+ /** Label for the final-step button (default "Finish"). */
2127
+ finishLabel = input('Finish', ...(ngDevMode ? [{ debugName: "finishLabel" }] : /* istanbul ignore next */ []));
2128
+ /** Disable Finish and show a busy label while an async submit is in flight. */
2129
+ submitting = input(false, { ...(ngDevMode ? { debugName: "submitting" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2130
+ /** Show a Cancel button on the left. */
2131
+ cancelable = input(false, { ...(ngDevMode ? { debugName: "cancelable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1638
2132
  finished = output();
2133
+ cancelled = output();
2134
+ /** Emits the new step index after a Back / Next move. */
2135
+ stepChange = output();
1639
2136
  isLast = computed(() => this.current() >= this.steps().length - 1, ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
2137
+ /** Whether the current step permits advancing (its `canAdvance`). */
2138
+ canAdvance = computed(() => this.steps()[this.current()]?.canAdvance() ?? true, ...(ngDevMode ? [{ debugName: "canAdvance" }] : /* istanbul ignore next */ []));
1640
2139
  constructor() {
1641
2140
  effect(() => {
1642
2141
  const idx = this.current();
@@ -1644,18 +2143,23 @@ class StrctWizard {
1644
2143
  });
1645
2144
  }
1646
2145
  next() {
1647
- if (!this.isLast())
2146
+ if (!this.isLast() && this.canAdvance()) {
1648
2147
  this.current.update((i) => i + 1);
2148
+ this.stepChange.emit(this.current());
2149
+ }
1649
2150
  }
1650
2151
  back() {
1651
- if (this.current() > 0)
2152
+ if (this.current() > 0) {
1652
2153
  this.current.update((i) => i - 1);
2154
+ this.stepChange.emit(this.current());
2155
+ }
1653
2156
  }
1654
2157
  finish() {
1655
- this.finished.emit();
2158
+ if (this.canAdvance() && !this.submitting())
2159
+ this.finished.emit();
1656
2160
  }
1657
2161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWizard, deps: [], target: i0.ɵɵFactoryTarget.Component });
1658
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctWizard, isStandalone: true, selector: "strct-wizard", outputs: { finished: "finished" }, host: { classAttribute: "strct-wiz" }, queries: [{ propertyName: "steps", predicate: StrctStep, isSignal: true }], ngImport: i0, template: `
2162
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctWizard, isStandalone: true, selector: "strct-wizard", inputs: { finishLabel: { classPropertyName: "finishLabel", publicName: "finishLabel", isSignal: true, isRequired: false, transformFunction: null }, submitting: { classPropertyName: "submitting", publicName: "submitting", isSignal: true, isRequired: false, transformFunction: null }, cancelable: { classPropertyName: "cancelable", publicName: "cancelable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { finished: "finished", cancelled: "cancelled", stepChange: "stepChange" }, host: { classAttribute: "strct-wiz" }, queries: [{ propertyName: "steps", predicate: StrctStep, isSignal: true }], ngImport: i0, template: `
1659
2163
  <div class="strct-wiz__steps">
1660
2164
  @for (step of steps(); track step; let i = $index; let last = $last) {
1661
2165
  <div
@@ -1675,14 +2179,26 @@ class StrctWizard {
1675
2179
  <div class="strct-wiz__content"><ng-content /></div>
1676
2180
 
1677
2181
  <div class="strct-wiz__foot">
2182
+ @if (cancelable()) {
2183
+ <button strct-button variant="flat" class="strct-wiz__cancel" (click)="cancelled.emit()">
2184
+ Cancel
2185
+ </button>
2186
+ }
1678
2187
  <button strct-button variant="flat" [disabled]="current() === 0" (click)="back()">Back</button>
1679
2188
  @if (isLast()) {
1680
- <button strct-button variant="primary" (click)="finish()">Finish</button>
2189
+ <button
2190
+ strct-button
2191
+ variant="primary"
2192
+ [disabled]="submitting() || !canAdvance()"
2193
+ (click)="finish()"
2194
+ >
2195
+ {{ submitting() ? 'Submitting…' : finishLabel() }}
2196
+ </button>
1681
2197
  } @else {
1682
- <button strct-button variant="primary" (click)="next()">Next</button>
2198
+ <button strct-button variant="primary" [disabled]="!canAdvance()" (click)="next()">Next</button>
1683
2199
  }
1684
2200
  </div>
1685
- `, isInline: true, styles: [".strct-wiz{display:block}.strct-wiz__steps{display:flex;align-items:center;gap:6px}.strct-wiz__step{display:flex;align-items:center;gap:8px}.strct-wiz__dot{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:600;color:var(--t2);background:var(--bg-3);border:1px solid var(--b2)}.strct-wiz__label{font-size:12px;color:var(--t2)}.strct-wiz__step--active .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc)}.strct-wiz__step--active .strct-wiz__label{color:var(--t1);font-weight:600}.strct-wiz__step--done .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc30)}.strct-wiz__sep{flex:1;height:1px;background:var(--b2);min-width:18px}.strct-wiz__content{margin:18px 0;padding:16px;min-height:80px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:13px}.strct-wiz__foot{display:flex;justify-content:flex-end;gap:8px}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2201
+ `, isInline: true, styles: [".strct-wiz{display:block}.strct-wiz__steps{display:flex;align-items:center;gap:6px}.strct-wiz__step{display:flex;align-items:center;gap:8px}.strct-wiz__dot{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:600;color:var(--t2);background:var(--bg-3);border:1px solid var(--b2)}.strct-wiz__label{font-size:12px;color:var(--t2)}.strct-wiz__step--active .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc)}.strct-wiz__step--active .strct-wiz__label{color:var(--t1);font-weight:600}.strct-wiz__step--done .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc30)}.strct-wiz__sep{flex:1;height:1px;background:var(--b2);min-width:18px}.strct-wiz__content{margin:18px 0;padding:16px;min-height:80px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:13px}.strct-wiz__foot{display:flex;justify-content:flex-end;gap:8px}.strct-wiz__cancel{margin-right:auto}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1686
2202
  }
1687
2203
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWizard, decorators: [{
1688
2204
  type: Component,
@@ -1706,15 +2222,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1706
2222
  <div class="strct-wiz__content"><ng-content /></div>
1707
2223
 
1708
2224
  <div class="strct-wiz__foot">
2225
+ @if (cancelable()) {
2226
+ <button strct-button variant="flat" class="strct-wiz__cancel" (click)="cancelled.emit()">
2227
+ Cancel
2228
+ </button>
2229
+ }
1709
2230
  <button strct-button variant="flat" [disabled]="current() === 0" (click)="back()">Back</button>
1710
2231
  @if (isLast()) {
1711
- <button strct-button variant="primary" (click)="finish()">Finish</button>
2232
+ <button
2233
+ strct-button
2234
+ variant="primary"
2235
+ [disabled]="submitting() || !canAdvance()"
2236
+ (click)="finish()"
2237
+ >
2238
+ {{ submitting() ? 'Submitting…' : finishLabel() }}
2239
+ </button>
1712
2240
  } @else {
1713
- <button strct-button variant="primary" (click)="next()">Next</button>
2241
+ <button strct-button variant="primary" [disabled]="!canAdvance()" (click)="next()">Next</button>
1714
2242
  }
1715
2243
  </div>
1716
- `, host: { class: 'strct-wiz' }, styles: [".strct-wiz{display:block}.strct-wiz__steps{display:flex;align-items:center;gap:6px}.strct-wiz__step{display:flex;align-items:center;gap:8px}.strct-wiz__dot{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:600;color:var(--t2);background:var(--bg-3);border:1px solid var(--b2)}.strct-wiz__label{font-size:12px;color:var(--t2)}.strct-wiz__step--active .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc)}.strct-wiz__step--active .strct-wiz__label{color:var(--t1);font-weight:600}.strct-wiz__step--done .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc30)}.strct-wiz__sep{flex:1;height:1px;background:var(--b2);min-width:18px}.strct-wiz__content{margin:18px 0;padding:16px;min-height:80px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:13px}.strct-wiz__foot{display:flex;justify-content:flex-end;gap:8px}\n"] }]
1717
- }], ctorParameters: () => [], propDecorators: { steps: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctStep), { isSignal: true }] }], finished: [{ type: i0.Output, args: ["finished"] }] } });
2244
+ `, host: { class: 'strct-wiz' }, styles: [".strct-wiz{display:block}.strct-wiz__steps{display:flex;align-items:center;gap:6px}.strct-wiz__step{display:flex;align-items:center;gap:8px}.strct-wiz__dot{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:600;color:var(--t2);background:var(--bg-3);border:1px solid var(--b2)}.strct-wiz__label{font-size:12px;color:var(--t2)}.strct-wiz__step--active .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc)}.strct-wiz__step--active .strct-wiz__label{color:var(--t1);font-weight:600}.strct-wiz__step--done .strct-wiz__dot{background:var(--acc-m);color:var(--acc);border-color:var(--acc30)}.strct-wiz__sep{flex:1;height:1px;background:var(--b2);min-width:18px}.strct-wiz__content{margin:18px 0;padding:16px;min-height:80px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;color:var(--t2);font-size:13px}.strct-wiz__foot{display:flex;justify-content:flex-end;gap:8px}.strct-wiz__cancel{margin-right:auto}\n"] }]
2245
+ }], ctorParameters: () => [], propDecorators: { steps: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctStep), { isSignal: true }] }], finishLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "finishLabel", required: false }] }], submitting: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitting", required: false }] }], cancelable: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelable", required: false }] }], finished: [{ type: i0.Output, args: ["finished"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }], stepChange: [{ type: i0.Output, args: ["stepChange"] }] } });
1718
2246
 
1719
2247
  /**
1720
2248
  * Separator rule. Horizontal by default; pass `vertical` for an inline rule.
@@ -1873,6 +2401,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
1873
2401
  `, 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"] }]
1874
2402
  }], 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"] }] } });
1875
2403
 
2404
+ let fieldCounter = 0;
2405
+ /**
2406
+ * Form-field wrapper: a label (with optional required marker), the projected
2407
+ * control, and a hint or error message. It auto-links the control via
2408
+ * `aria-describedby` and sets `aria-invalid` when an error is present.
2409
+ *
2410
+ * <strct-field label="Email" required hint="We never share it." [error]="emailError()">
2411
+ * <input strctInput type="email" [(ngModel)]="email" />
2412
+ * </strct-field>
2413
+ */
2414
+ class StrctField {
2415
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2416
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2417
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2418
+ /** Error message (string or first-of array); falsy clears the error state. */
2419
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2420
+ host = inject(ElementRef);
2421
+ n = ++fieldCounter;
2422
+ hintId = `strct-field-hint-${this.n}`;
2423
+ errorId = `strct-field-err-${this.n}`;
2424
+ controlId = signal('', ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
2425
+ errorText = computed(() => {
2426
+ const e = this.error();
2427
+ return (Array.isArray(e) ? e[0] : e) ?? '';
2428
+ }, ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
2429
+ constructor() {
2430
+ afterNextRender(() => this.link());
2431
+ // Keep aria in sync as the error / hint change.
2432
+ effect(() => {
2433
+ this.errorText();
2434
+ this.hint();
2435
+ this.applyAria();
2436
+ });
2437
+ }
2438
+ control() {
2439
+ return this.host.nativeElement.querySelector('input, select, textarea, [strctInput], [strctField]');
2440
+ }
2441
+ link() {
2442
+ const el = this.control();
2443
+ if (!el)
2444
+ return;
2445
+ if (!el.id)
2446
+ el.id = `strct-field-ctrl-${this.n}`;
2447
+ this.controlId.set(el.id);
2448
+ this.applyAria();
2449
+ }
2450
+ applyAria() {
2451
+ const el = this.control();
2452
+ if (!el)
2453
+ return;
2454
+ const describedBy = this.errorText() ? this.errorId : this.hint() ? this.hintId : '';
2455
+ if (describedBy)
2456
+ el.setAttribute('aria-describedby', describedBy);
2457
+ else
2458
+ el.removeAttribute('aria-describedby');
2459
+ if (this.errorText())
2460
+ el.setAttribute('aria-invalid', 'true');
2461
+ else
2462
+ el.removeAttribute('aria-invalid');
2463
+ }
2464
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, deps: [], target: i0.ɵɵFactoryTarget.Component });
2465
+ 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: `
2466
+ @if (label()) {
2467
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2468
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2469
+ </label>
2470
+ }
2471
+ <div class="strct-field__control"><ng-content /></div>
2472
+ @if (errorText()) {
2473
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2474
+ {{ errorText() }}
2475
+ </div>
2476
+ } @else if (hint()) {
2477
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2478
+ }
2479
+ `, 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 });
2480
+ }
2481
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctField, decorators: [{
2482
+ type: Component,
2483
+ args: [{ selector: 'strct-field', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2484
+ @if (label()) {
2485
+ <label class="strct-field__label" [attr.for]="controlId() || null">
2486
+ {{ label() }}@if (required()) {<span class="strct-field__req" aria-hidden="true">*</span>}
2487
+ </label>
2488
+ }
2489
+ <div class="strct-field__control"><ng-content /></div>
2490
+ @if (errorText()) {
2491
+ <div class="strct-field__msg strct-field__msg--error" [id]="errorId" role="alert">
2492
+ {{ errorText() }}
2493
+ </div>
2494
+ } @else if (hint()) {
2495
+ <div class="strct-field__msg strct-field__msg--hint" [id]="hintId">{{ hint() }}</div>
2496
+ }
2497
+ `, host: {
2498
+ class: 'strct-field',
2499
+ '[class.strct-field--invalid]': '!!errorText()',
2500
+ }, 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"] }]
2501
+ }], 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 }] }] } });
2502
+
1876
2503
  /**
1877
2504
  * Applies the shared `.strct-control` look to a native input / textarea / select.
1878
2505
  * <input strctInput placeholder="Name" />
@@ -4014,7 +4641,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4014
4641
  }] } });
4015
4642
 
4016
4643
  /**
4017
- * Declarative data table.
4644
+ * Per-column cell template for `strct-table` / `strct-datagrid`. The column key
4645
+ * is the directive value; the row, value and column are the template context:
4646
+ *
4647
+ * <ng-template strctCell="status" let-row let-value="value">
4648
+ * <strct-badge [status]="row['ok'] ? 'success' : 'danger'">{{ value }}</strct-badge>
4649
+ * </ng-template>
4650
+ */
4651
+ class StrctCellDef {
4652
+ key = input.required({ ...(ngDevMode ? { debugName: "key" } : /* istanbul ignore next */ {}), alias: 'strctCell' });
4653
+ template = inject(TemplateRef);
4654
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCellDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4655
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctCellDef, isStandalone: true, selector: "[strctCell]", inputs: { key: { classPropertyName: "key", publicName: "strctCell", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
4656
+ }
4657
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCellDef, decorators: [{
4658
+ type: Directive,
4659
+ args: [{ selector: '[strctCell]' }]
4660
+ }], propDecorators: { key: [{ type: i0.Input, args: [{ isSignal: true, alias: "strctCell", required: true }] }] } });
4661
+ /**
4662
+ * Declarative data table. Cells render `row[col.key]` as text by default; supply
4663
+ * a `*strctCell` template per column for custom content.
4018
4664
  * <strct-table [columns]="cols" [rows]="data" hover />
4019
4665
  */
4020
4666
  class StrctTable {
@@ -4023,8 +4669,18 @@ class StrctTable {
4023
4669
  striped = input(false, { ...(ngDevMode ? { debugName: "striped" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4024
4670
  hover = input(false, { ...(ngDevMode ? { debugName: "hover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4025
4671
  emptyText = input('No data', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
4672
+ cellDefs = contentChildren(StrctCellDef, ...(ngDevMode ? [{ debugName: "cellDefs" }] : /* istanbul ignore next */ []));
4673
+ cellMap = computed(() => {
4674
+ const m = new Map();
4675
+ for (const d of this.cellDefs())
4676
+ m.set(d.key(), d.template);
4677
+ return m;
4678
+ }, ...(ngDevMode ? [{ debugName: "cellMap" }] : /* istanbul ignore next */ []));
4679
+ cellTemplate(key) {
4680
+ return this.cellMap().get(key) ?? null;
4681
+ }
4026
4682
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
4027
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTable, isStandalone: true, selector: "strct-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, hover: { classPropertyName: "hover", publicName: "hover", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-table-host--striped": "striped()", "class.strct-table-host--hover": "hover()" }, classAttribute: "strct-table-host" }, ngImport: i0, template: `
4683
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTable, isStandalone: true, selector: "strct-table", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, hover: { classPropertyName: "hover", publicName: "hover", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.strct-table-host--striped": "striped()", "class.strct-table-host--hover": "hover()" }, classAttribute: "strct-table-host" }, queries: [{ propertyName: "cellDefs", predicate: StrctCellDef, isSignal: true }], ngImport: i0, template: `
4028
4684
  <table class="strct-table">
4029
4685
  <thead>
4030
4686
  <tr>
@@ -4037,7 +4693,16 @@ class StrctTable {
4037
4693
  @for (row of rows(); track $index) {
4038
4694
  <tr>
4039
4695
  @for (col of columns(); track col.key) {
4040
- <td [style.text-align]="col.align ?? 'start'">{{ row[col.key] }}</td>
4696
+ <td [style.text-align]="col.align ?? 'start'">
4697
+ @if (cellTemplate(col.key); as tpl) {
4698
+ <ng-container
4699
+ [ngTemplateOutlet]="tpl"
4700
+ [ngTemplateOutletContext]="{ $implicit: row, value: row[col.key], column: col }"
4701
+ />
4702
+ } @else {
4703
+ {{ row[col.key] }}
4704
+ }
4705
+ </td>
4041
4706
  }
4042
4707
  </tr>
4043
4708
  } @empty {
@@ -4047,11 +4712,11 @@ class StrctTable {
4047
4712
  }
4048
4713
  </tbody>
4049
4714
  </table>
4050
- `, isInline: true, styles: [".strct-table-host{display:block;overflow-x:auto}.strct-table{width:100%;border-collapse:collapse;font-size:13px;border:1px solid var(--b2);border-radius:8px;overflow:hidden}.strct-table th,.strct-table td{padding:9px 13px;text-align:left;border-bottom:1px solid var(--b1)}.strct-table th{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2)}.strct-table td{color:var(--t1)}.strct-table tbody tr:last-child td{border-bottom:0}.strct-table-host--striped tbody tr:nth-child(2n) td{background:var(--bg-2)}.strct-table-host--hover tbody tr:hover td{background:var(--acc-s)}.strct-table__empty{text-align:center;color:var(--t3);padding:22px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4715
+ `, isInline: true, styles: [".strct-table-host{display:block;overflow-x:auto}.strct-table{width:100%;border-collapse:collapse;font-size:13px;border:1px solid var(--b2);border-radius:8px;overflow:hidden}.strct-table th,.strct-table td{padding:9px 13px;text-align:left;border-bottom:1px solid var(--b1)}.strct-table th{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2)}.strct-table td{color:var(--t1)}.strct-table tbody tr:last-child td{border-bottom:0}.strct-table-host--striped tbody tr:nth-child(2n) td{background:var(--bg-2)}.strct-table-host--hover tbody tr:hover td{background:var(--acc-s)}.strct-table__empty{text-align:center;color:var(--t3);padding:22px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4051
4716
  }
4052
4717
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTable, decorators: [{
4053
4718
  type: Component,
4054
- args: [{ selector: 'strct-table', changeDetection: ChangeDetectionStrategy.OnPush, template: `
4719
+ args: [{ selector: 'strct-table', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: `
4055
4720
  <table class="strct-table">
4056
4721
  <thead>
4057
4722
  <tr>
@@ -4064,7 +4729,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4064
4729
  @for (row of rows(); track $index) {
4065
4730
  <tr>
4066
4731
  @for (col of columns(); track col.key) {
4067
- <td [style.text-align]="col.align ?? 'start'">{{ row[col.key] }}</td>
4732
+ <td [style.text-align]="col.align ?? 'start'">
4733
+ @if (cellTemplate(col.key); as tpl) {
4734
+ <ng-container
4735
+ [ngTemplateOutlet]="tpl"
4736
+ [ngTemplateOutletContext]="{ $implicit: row, value: row[col.key], column: col }"
4737
+ />
4738
+ } @else {
4739
+ {{ row[col.key] }}
4740
+ }
4741
+ </td>
4068
4742
  }
4069
4743
  </tr>
4070
4744
  } @empty {
@@ -4079,7 +4753,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4079
4753
  '[class.strct-table-host--striped]': 'striped()',
4080
4754
  '[class.strct-table-host--hover]': 'hover()',
4081
4755
  }, styles: [".strct-table-host{display:block;overflow-x:auto}.strct-table{width:100%;border-collapse:collapse;font-size:13px;border:1px solid var(--b2);border-radius:8px;overflow:hidden}.strct-table th,.strct-table td{padding:9px 13px;text-align:left;border-bottom:1px solid var(--b1)}.strct-table th{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2)}.strct-table td{color:var(--t1)}.strct-table tbody tr:last-child td{border-bottom:0}.strct-table-host--striped tbody tr:nth-child(2n) td{background:var(--bg-2)}.strct-table-host--hover tbody tr:hover td{background:var(--acc-s)}.strct-table__empty{text-align:center;color:var(--t3);padding:22px}\n"] }]
4082
- }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], hover: [{ type: i0.Input, args: [{ isSignal: true, alias: "hover", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }] } });
4756
+ }], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], hover: [{ type: i0.Input, args: [{ isSignal: true, alias: "hover", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctCellDef), { isSignal: true }] }] } });
4083
4757
 
4084
4758
  /**
4085
4759
  * Marks the expandable-row detail template. The row is the template's implicit
@@ -4123,14 +4797,34 @@ class StrctDatagrid {
4123
4797
  detailPane = input(false, { ...(ngDevMode ? { debugName: "detailPane" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4124
4798
  compact = input(false, { ...(ngDevMode ? { debugName: "compact" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4125
4799
  emptyText = input('No data', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
4800
+ /**
4801
+ * Stable row identity (property key or function). Set this for live-refreshing
4802
+ * data so selection, expansion and the active detail row survive re-fetches
4803
+ * that replace the row objects. Defaults to object identity.
4804
+ */
4805
+ rowId = input(null, ...(ngDevMode ? [{ debugName: "rowId" }] : /* istanbul ignore next */ []));
4126
4806
  selectionChange = output();
4127
4807
  detailDef = contentChild(StrctRowDetailDef, ...(ngDevMode ? [{ debugName: "detailDef" }] : /* istanbul ignore next */ []));
4128
4808
  actionBarDef = contentChild(StrctDatagridActionBar, ...(ngDevMode ? [{ debugName: "actionBarDef" }] : /* istanbul ignore next */ []));
4809
+ cellDefs = contentChildren(StrctCellDef, ...(ngDevMode ? [{ debugName: "cellDefs" }] : /* istanbul ignore next */ []));
4810
+ cellMap = computed(() => {
4811
+ const m = new Map();
4812
+ for (const d of this.cellDefs())
4813
+ m.set(d.key(), d.template);
4814
+ return m;
4815
+ }, ...(ngDevMode ? [{ debugName: "cellMap" }] : /* istanbul ignore next */ []));
4129
4816
  page = signal(1, ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
4130
4817
  sort = signal({ key: null, dir: 'asc' }, ...(ngDevMode ? [{ debugName: "sort" }] : /* istanbul ignore next */ []));
4818
+ /** Selection / expansion are tracked by row id (see {@link rowId}). */
4131
4819
  selected = signal(new Set(), ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
4132
4820
  expandedRows = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedRows" }] : /* istanbul ignore next */ []));
4133
- activeRow = signal(null, ...(ngDevMode ? [{ debugName: "activeRow" }] : /* istanbul ignore next */ []));
4821
+ activeId = signal(null, ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
4822
+ activeRow = computed(() => {
4823
+ const id = this.activeId();
4824
+ if (id == null)
4825
+ return null;
4826
+ return this.rows().find((r) => this.idOf(r) === id) ?? null;
4827
+ }, ...(ngDevMode ? [{ debugName: "activeRow" }] : /* istanbul ignore next */ []));
4134
4828
  canExpand = computed(() => this.expandable() && !this.detailPane() && !!this.detailDef(), ...(ngDevMode ? [{ debugName: "canExpand" }] : /* istanbul ignore next */ []));
4135
4829
  canDetail = computed(() => this.detailPane() && !!this.detailDef(), ...(ngDevMode ? [{ debugName: "canDetail" }] : /* istanbul ignore next */ []));
4136
4830
  paneOpen = computed(() => this.detailPane() && !!this.detailDef() && !!this.activeRow(), ...(ngDevMode ? [{ debugName: "paneOpen" }] : /* istanbul ignore next */ []));
@@ -4154,9 +4848,22 @@ class StrctDatagrid {
4154
4848
  selectedCount = computed(() => this.selected().size, ...(ngDevMode ? [{ debugName: "selectedCount" }] : /* istanbul ignore next */ []));
4155
4849
  allPageSelected = computed(() => {
4156
4850
  const rows = this.paged();
4157
- return rows.length > 0 && rows.every((r) => this.selected().has(r));
4851
+ return rows.length > 0 && rows.every((r) => this.selected().has(this.idOf(r)));
4158
4852
  }, ...(ngDevMode ? [{ debugName: "allPageSelected" }] : /* istanbul ignore next */ []));
4159
- somePageSelected = computed(() => !this.allPageSelected() && this.paged().some((r) => this.selected().has(r)), ...(ngDevMode ? [{ debugName: "somePageSelected" }] : /* istanbul ignore next */ []));
4853
+ somePageSelected = computed(() => !this.allPageSelected() && this.paged().some((r) => this.selected().has(this.idOf(r))), ...(ngDevMode ? [{ debugName: "somePageSelected" }] : /* istanbul ignore next */ []));
4854
+ /** Resolve a row's stable identity (defaults to the row object itself). */
4855
+ idOf(row) {
4856
+ const id = this.rowId();
4857
+ if (id == null)
4858
+ return row;
4859
+ return typeof id === 'function' ? id(row) : row[id];
4860
+ }
4861
+ rowKey(row) {
4862
+ return this.idOf(row);
4863
+ }
4864
+ cellTemplate(key) {
4865
+ return this.cellMap().get(key) ?? null;
4866
+ }
4160
4867
  constructor() {
4161
4868
  // Keep the page in range when the data set shrinks.
4162
4869
  effect(() => {
@@ -4177,10 +4884,11 @@ class StrctDatagrid {
4177
4884
  /** Toggle the detail pane for a row (triggered by its » button, not the row,
4178
4885
  * so row cells remain selectable for copy). */
4179
4886
  openDetail(row) {
4180
- this.activeRow.set(this.activeRow() === row ? null : row);
4887
+ const id = this.idOf(row);
4888
+ this.activeId.set(this.activeId() === id ? null : id);
4181
4889
  }
4182
4890
  closePane() {
4183
- this.activeRow.set(null);
4891
+ this.activeId.set(null);
4184
4892
  }
4185
4893
  sortBy(key) {
4186
4894
  const current = this.sort();
@@ -4211,38 +4919,42 @@ class StrctDatagrid {
4211
4919
  this.sortBy(key);
4212
4920
  }
4213
4921
  isSelected(row) {
4214
- return this.selected().has(row);
4922
+ return this.selected().has(this.idOf(row));
4215
4923
  }
4216
4924
  isExpanded(row) {
4217
- return this.expandedRows().has(row);
4925
+ return this.expandedRows().has(this.idOf(row));
4218
4926
  }
4219
4927
  toggleExpand(row) {
4928
+ const id = this.idOf(row);
4220
4929
  const next = new Set(this.expandedRows());
4221
- next.has(row) ? next.delete(row) : next.add(row);
4930
+ next.has(id) ? next.delete(id) : next.add(id);
4222
4931
  this.expandedRows.set(next);
4223
4932
  }
4224
4933
  toggleRow(row) {
4934
+ const id = this.idOf(row);
4225
4935
  const next = new Set(this.selected());
4226
- next.has(row) ? next.delete(row) : next.add(row);
4936
+ next.has(id) ? next.delete(id) : next.add(id);
4227
4937
  this.commitSelection(next);
4228
4938
  }
4229
4939
  toggleAll() {
4230
4940
  const next = new Set(this.selected());
4231
4941
  const rows = this.paged();
4232
4942
  if (this.allPageSelected()) {
4233
- rows.forEach((r) => next.delete(r));
4943
+ rows.forEach((r) => next.delete(this.idOf(r)));
4234
4944
  }
4235
4945
  else {
4236
- rows.forEach((r) => next.add(r));
4946
+ rows.forEach((r) => next.add(this.idOf(r)));
4237
4947
  }
4238
4948
  this.commitSelection(next);
4239
4949
  }
4240
4950
  clearSelection() {
4241
4951
  this.commitSelection(new Set());
4242
4952
  }
4953
+ /** Emit the current row objects whose id is selected (resolved against the
4954
+ * latest data, so consumers always get fresh references). */
4243
4955
  commitSelection(next) {
4244
4956
  this.selected.set(next);
4245
- this.selectionChange.emit([...next]);
4957
+ this.selectionChange.emit(this.rows().filter((r) => next.has(this.idOf(r))));
4246
4958
  }
4247
4959
  compare(a, b) {
4248
4960
  if (typeof a === 'number' && typeof b === 'number')
@@ -4250,7 +4962,7 @@ class StrctDatagrid {
4250
4962
  return String(a ?? '').localeCompare(String(b ?? ''), undefined, { numeric: true });
4251
4963
  }
4252
4964
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
4253
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDatagrid, isStandalone: true, selector: "strct-datagrid", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, detailPane: { classPropertyName: "detailPane", publicName: "detailPane", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { properties: { "class.strct-dg-host--compact": "compact()" }, classAttribute: "strct-dg-host" }, queries: [{ propertyName: "detailDef", first: true, predicate: StrctRowDetailDef, descendants: true, isSignal: true }, { propertyName: "actionBarDef", first: true, predicate: StrctDatagridActionBar, descendants: true, isSignal: true }], ngImport: i0, template: `
4965
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDatagrid, isStandalone: true, selector: "strct-datagrid", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, detailPane: { classPropertyName: "detailPane", publicName: "detailPane", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, rowId: { classPropertyName: "rowId", publicName: "rowId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { properties: { "class.strct-dg-host--compact": "compact()" }, classAttribute: "strct-dg-host" }, queries: [{ propertyName: "detailDef", first: true, predicate: StrctRowDetailDef, descendants: true, isSignal: true }, { propertyName: "actionBarDef", first: true, predicate: StrctDatagridActionBar, descendants: true, isSignal: true }, { propertyName: "cellDefs", predicate: StrctCellDef, isSignal: true }], ngImport: i0, template: `
4254
4966
  @if (actionBarDef()) {
4255
4967
  <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
4256
4968
  }
@@ -4306,7 +5018,7 @@ class StrctDatagrid {
4306
5018
  </tr>
4307
5019
  </thead>
4308
5020
  <tbody>
4309
- @for (row of paged(); track $index) {
5021
+ @for (row of paged(); track rowKey(row)) {
4310
5022
  <tr
4311
5023
  [class.strct-dg__row--selected]="isSelected(row)"
4312
5024
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
@@ -4350,7 +5062,16 @@ class StrctDatagrid {
4350
5062
  </td>
4351
5063
  }
4352
5064
  @for (col of visibleColumns(); track col.key) {
4353
- <td [style.text-align]="col.align ?? 'start'">{{ row[col.key] }}</td>
5065
+ <td [style.text-align]="col.align ?? 'start'">
5066
+ @if (cellTemplate(col.key); as tpl) {
5067
+ <ng-container
5068
+ [ngTemplateOutlet]="tpl"
5069
+ [ngTemplateOutletContext]="{ $implicit: row, value: row[col.key], column: col }"
5070
+ />
5071
+ } @else {
5072
+ {{ row[col.key] }}
5073
+ }
5074
+ </td>
4354
5075
  }
4355
5076
  </tr>
4356
5077
  @if (canExpand() && isExpanded(row)) {
@@ -4462,7 +5183,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4462
5183
  </tr>
4463
5184
  </thead>
4464
5185
  <tbody>
4465
- @for (row of paged(); track $index) {
5186
+ @for (row of paged(); track rowKey(row)) {
4466
5187
  <tr
4467
5188
  [class.strct-dg__row--selected]="isSelected(row)"
4468
5189
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
@@ -4506,7 +5227,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4506
5227
  </td>
4507
5228
  }
4508
5229
  @for (col of visibleColumns(); track col.key) {
4509
- <td [style.text-align]="col.align ?? 'start'">{{ row[col.key] }}</td>
5230
+ <td [style.text-align]="col.align ?? 'start'">
5231
+ @if (cellTemplate(col.key); as tpl) {
5232
+ <ng-container
5233
+ [ngTemplateOutlet]="tpl"
5234
+ [ngTemplateOutletContext]="{ $implicit: row, value: row[col.key], column: col }"
5235
+ />
5236
+ } @else {
5237
+ {{ row[col.key] }}
5238
+ }
5239
+ </td>
4510
5240
  }
4511
5241
  </tr>
4512
5242
  @if (canExpand() && isExpanded(row)) {
@@ -4562,7 +5292,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4562
5292
  class: 'strct-dg-host',
4563
5293
  '[class.strct-dg-host--compact]': 'compact()',
4564
5294
  }, styles: [".strct-dg-host{display:block}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;border:1px solid var(--b2);border-radius:8px;overflow:hidden}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:left;border-bottom:1px solid var(--b1)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg th{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;gap:14px;align-items:flex-start}.strct-dg__layout--paned .strct-dg{width:auto;min-width:180px;max-width:260px;flex-shrink:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-right:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;right:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-right:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-left:2px solid var(--acc);border-radius:8px;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;margin-bottom:10px;background:var(--bg-2);border:1px solid var(--b2);border-radius:8px}.strct-dg__actionbar{display:flex;align-items:center;gap:14px;padding:8px 12px;margin-bottom:10px;background:var(--acc-m);border:1px solid var(--acc30);border-radius:7px;font-size:13px;animation:strct-dg-bar-in .12s ease}.strct-dg__actionbar-count{color:var(--acc);font-weight:600}.strct-dg__actionbar-clear{border:0;background:transparent;color:var(--t2);cursor:pointer;font-size:12px;padding:2px 4px}.strct-dg__actionbar-clear:hover{color:var(--t1)}.strct-dg__actionbar-actions{display:flex;gap:8px;margin-left:auto}@keyframes strct-dg-bar-in{0%{opacity:0;transform:translateY(-4px)}}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:12px;flex-wrap:wrap}.strct-dg__count{font-size:12px;color:var(--t2)}\n"] }]
4565
- }], ctorParameters: () => [], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], detailPane: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailPane", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], detailDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctRowDetailDef), { isSignal: true }] }], actionBarDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDatagridActionBar), { isSignal: true }] }] } });
5295
+ }], ctorParameters: () => [], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], detailPane: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailPane", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], detailDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctRowDetailDef), { isSignal: true }] }], actionBarDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDatagridActionBar), { isSignal: true }] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctCellDef), { isSignal: true }] }] } });
4566
5296
 
4567
5297
  /** Vertical timeline container. Wraps `<strct-timeline-item>` children. */
4568
5298
  class StrctTimeline {
@@ -5371,5 +6101,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
5371
6101
  * Generated bundle index. Do not edit.
5372
6102
  */
5373
6103
 
5374
- 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, StrctChart, StrctCheckbox, StrctChips, StrctColorPicker, StrctCombobox, StrctContextMenu, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDivider, StrctDonut, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctFile, StrctFooter, StrctGauge, StrctHeader, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKnob, StrctLogin, 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 };
6104
+ 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 };
5375
6105
  //# sourceMappingURL=akcelik-strct.mjs.map