@akcelik/strct 1.5.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, model, output, booleanAttribute, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, DestroyRef, contentChild, viewChild, viewChildren, untracked, TemplateRef, Renderer2 } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, afterNextRender, Directive, model, output, booleanAttribute, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, DestroyRef, contentChild, viewChild, viewChildren, untracked, TemplateRef, Renderer2, Pipe } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { NgTemplateOutlet, DOCUMENT as DOCUMENT$1 } from '@angular/common';
5
5
  import { RouterLink, RouterLinkActive } from '@angular/router';
@@ -135,7 +135,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
135
135
  * storage, VMs …). Object state — running / stopped / maintenance — is layered
136
136
  * on with the `badge` input rather than a separate icon per state.
137
137
  */
138
- const STRCT_ICONS = {
138
+ // Built-in stroke glyphs. Keys stay inferred so \`StrctIconName\` below is a
139
+ // compile-time union of every name.
140
+ const STRCT_ICON_DEFS = {
139
141
  // ── General UI ───────────────────────────────────────────────
140
142
  hexagon: '<path d="M8 1.6l5.5 3.2v6.4L8 14.4 2.5 11.2V4.8L8 1.6z"/>',
141
143
  chevronRight: '<path d="M6 3.5L10.5 8 6 12.5"/>',
@@ -254,6 +256,8 @@ const STRCT_ICONS = {
254
256
  megaphone: '<path d="M3 7.3v1.6l7 3V4.3zM10 5.7 12.5 4.2v7.6L10 10.3M4 9.6l.9 3.1h1.6l-.7-2.9"/>',
255
257
  flag: '<path d="M4 13.5V3M4 3.6h7.4L9.8 6.2l1.6 2.6H4"/>',
256
258
  shieldAlert: '<path d="M8 2.3l5.2 1.9v3.3c0 3.1-2.3 5.1-5.2 6.4C5.1 12.6 2.8 10.6 2.8 7.5V4.2z"/><path d="M8 5.5v3.1M8 10.6v.2"/>',
259
+ rocket: '<path d="M8 1.8c1.9 1.3 2.9 3.1 2.9 5.3 0 1.5-.4 2.9-1 4.1H6.1c-.6-1.2-1-2.6-1-4.1 0-2.2 1-4 2.9-5.3z"/><circle cx="8" cy="6.3" r="1.1"/><path d="M5.1 8.9 3.5 11l2-.2M10.9 8.9l1.6 2.1-2-.2M6.9 11.2 8 14.1l1.1-2.9"/>',
260
+ shieldCheck: '<path d="M8 2.3l5.2 1.9v3.3c0 3.1-2.3 5.1-5.2 6.4C5.1 12.6 2.8 10.6 2.8 7.5V4.2z"/><path d="M5.9 7.9l1.5 1.5 2.8-2.9"/>',
257
261
  // ── State / action (also usable as badges via the icon `badge` input) ─
258
262
  running: '<path d="M5.5 4l6 4-6 4z"/>',
259
263
  stopped: '<rect x="4.5" y="4.5" width="7" height="7" rx="1"/>',
@@ -365,15 +369,26 @@ const STRCT_ICONS = {
365
369
  // Printer — paper feed, body, output sheet and an LED.
366
370
  printer: '<path d="M5 6.4V3.2h6v3.2"/><rect x="2.8" y="6.4" width="10.4" height="4.8" rx="1"/><path d="M5 9.2h6v3.6H5z"/><circle cx="11" cy="8.4" r=".5" fill="currentColor" stroke="none"/>',
367
371
  };
368
- // Composite "off" variants — re-use base glyph + diagonal slash.
369
- STRCT_ICONS['eyeOff'] = `${STRCT_ICONS['eye']}<path d="M2.5 2.5l11 11"/>`;
370
- STRCT_ICONS['bellOff'] = `${STRCT_ICONS['bell']}<path d="M2.6 2.6l10.8 10.8"/>`;
371
- STRCT_ICONS['wifiOff'] = `${STRCT_ICONS['wifi']}<path d="M2.5 2.5l11 11"/>`;
372
- STRCT_ICONS['cloudOff'] = `${STRCT_ICONS['cloud']}<path d="M2.5 2.5l11 11"/>`;
373
- STRCT_ICONS['linkOff'] = `${STRCT_ICONS['link']}<path d="M2.5 2.5l11 11"/>`;
374
- // Semantic aliases.
375
- STRCT_ICONS['unlink'] = STRCT_ICONS['linkOff'];
376
- STRCT_ICONS['webhook'] = STRCT_ICONS['bolt'];
372
+ // Composite "off" variants — re-use base glyph + diagonal slash — and aliases.
373
+ const STRCT_ICON_DERIVED_DEFS = {
374
+ eyeOff: `${STRCT_ICON_DEFS.eye}<path d="M2.5 2.5l11 11"/>`,
375
+ bellOff: `${STRCT_ICON_DEFS.bell}<path d="M2.6 2.6l10.8 10.8"/>`,
376
+ wifiOff: `${STRCT_ICON_DEFS.wifi}<path d="M2.5 2.5l11 11"/>`,
377
+ cloudOff: `${STRCT_ICON_DEFS.cloud}<path d="M2.5 2.5l11 11"/>`,
378
+ linkOff: `${STRCT_ICON_DEFS.link}<path d="M2.5 2.5l11 11"/>`,
379
+ unlink: `${STRCT_ICON_DEFS.link}<path d="M2.5 2.5l11 11"/>`,
380
+ webhook: STRCT_ICON_DEFS.bolt,
381
+ };
382
+ /**
383
+ * Icon name → glyph markup. Open (`Record<string, string>`) on purpose so
384
+ * `registerStrctIcon` can add app-specific names at runtime.
385
+ */
386
+ const STRCT_ICONS = {
387
+ ...STRCT_ICON_DEFS,
388
+ ...STRCT_ICON_DERIVED_DEFS,
389
+ };
390
+ /** All built-in icon names — greppable, iterable (galleries, docs, tests). */
391
+ const STRCT_ICON_NAMES = Object.keys(STRCT_ICONS).sort();
377
392
  /** Icon names grouped for galleries / documentation. */
378
393
  const STRCT_ICON_GROUPS = [
379
394
  {
@@ -514,6 +529,7 @@ const STRCT_ICON_GROUPS = [
514
529
  'container',
515
530
  'firewall',
516
531
  'shield',
532
+ 'shieldCheck',
517
533
  'certificate',
518
534
  'key',
519
535
  'metrics',
@@ -524,6 +540,7 @@ const STRCT_ICON_GROUPS = [
524
540
  'api',
525
541
  'bolt',
526
542
  'queue',
543
+ 'rocket',
527
544
  ],
528
545
  },
529
546
  {
@@ -585,9 +602,15 @@ function registerStrctIcon(name, content, options = {}) {
585
602
  * glyph with a green status dot (a "running host"). Unknown names render
586
603
  * nothing rather than throwing.
587
604
  */
605
+ // One warning per unknown name per app run — enough to surface the bug
606
+ // without flooding the console from a list render.
607
+ const warnedUnknownIcons = new Set();
588
608
  class StrctIcon {
589
609
  sanitizer = inject(DomSanitizer);
590
- /** Icon name from the STRCT_ICONS registry. */
610
+ /**
611
+ * Icon name. Typed to autocomplete the built-in set while still accepting
612
+ * any string, since `registerStrctIcon` can add names at runtime.
613
+ */
591
614
  name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
592
615
  /** Icon size in pixels. */
593
616
  size = input(16, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
@@ -599,7 +622,17 @@ class StrctIcon {
599
622
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
600
623
  /** True when the named icon is a full-SVG (raw) icon registered by the app. */
601
624
  isRaw = computed(() => Object.prototype.hasOwnProperty.call(STRCT_RAW_ICONS, this.name()), ...(ngDevMode ? [{ debugName: "isRaw" }] : /* istanbul ignore next */ []));
602
- svg = computed(() => this.sanitizer.bypassSecurityTrustHtml(STRCT_ICONS[this.name()] ?? ''), ...(ngDevMode ? [{ debugName: "svg" }] : /* istanbul ignore next */ []));
625
+ svg = computed(() => {
626
+ const name = this.name();
627
+ const glyph = STRCT_ICONS[name];
628
+ if (glyph === undefined && !this.isRaw() && isDevMode() && !warnedUnknownIcons.has(name)) {
629
+ warnedUnknownIcons.add(name);
630
+ console.warn(`[strct-icon] Unknown icon name "${name}" — it will render empty. ` +
631
+ `Built-in names are exported as STRCT_ICON_NAMES (type StrctIconName); ` +
632
+ `custom icons must be added with registerStrctIcon().`);
633
+ }
634
+ return this.sanitizer.bypassSecurityTrustHtml(glyph ?? '');
635
+ }, ...(ngDevMode ? [{ debugName: "svg" }] : /* istanbul ignore next */ []));
603
636
  rawSvg = computed(() => this.sanitizer.bypassSecurityTrustHtml(STRCT_RAW_ICONS[this.name()] ?? ''), ...(ngDevMode ? [{ debugName: "rawSvg" }] : /* istanbul ignore next */ []));
604
637
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
605
638
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctIcon, isStandalone: true, selector: "strct-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-icon" }, ngImport: i0, template: `@if (isRaw()) {
@@ -3326,18 +3359,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
3326
3359
  * <strct-dropdown-item>Rename</strct-dropdown-item>
3327
3360
  * <strct-dropdown-item critical>Delete</strct-dropdown-item>
3328
3361
  * </strct-dropdown>
3362
+ *
3363
+ * With `popover`, the panel holds *form controls* instead of menu items: an
3364
+ * inner click no longer closes it (only outside click / Escape do), and the
3365
+ * semantics switch from `role="menu"` to a labeled `role="dialog"` — a panel
3366
+ * of selects is not a menu to a screen reader either:
3367
+ *
3368
+ * <strct-dropdown popover popoverLabel="Filters">
3369
+ * <button strct-button strctDropdownTrigger>Filters</button>
3370
+ * <strct-field label="Severity">…</strct-field>
3371
+ * </strct-dropdown>
3329
3372
  */
3330
3373
  class StrctDropdown {
3331
3374
  host = inject((ElementRef));
3332
3375
  /** Horizontal alignment of the menu. */
3333
3376
  align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
3377
+ /**
3378
+ * Popover mode for panels holding form controls (filter/settings panels):
3379
+ * inner clicks never close the panel — only outside click / Escape do —
3380
+ * and it renders as a labeled `role="dialog"` instead of a menu.
3381
+ */
3382
+ popover = input(false, { ...(ngDevMode ? { debugName: "popover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3383
+ /** Accessible name of the popover dialog (localizable). */
3384
+ popoverLabel = input('Filters', ...(ngDevMode ? [{ debugName: "popoverLabel" }] : /* istanbul ignore next */ []));
3334
3385
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
3386
+ constructor() {
3387
+ // The static `popover` attribute collides with the native HTML Popover
3388
+ // API: the UA styles [popover] hosts (Canvas background, medium border,
3389
+ // fixed positioning) — a white frame around the trigger in dark themes.
3390
+ // Angular has already read the input by now; the DOM attribute must go.
3391
+ this.host.nativeElement.removeAttribute('popover');
3392
+ }
3335
3393
  toggle() {
3336
3394
  this.open.update((v) => !v);
3337
3395
  }
3338
3396
  close() {
3339
3397
  this.open.set(false);
3340
3398
  }
3399
+ /** Menu items close on activation; popover form controls never do. */
3400
+ onInnerActivate() {
3401
+ if (!this.popover())
3402
+ this.close();
3403
+ }
3341
3404
  onDocClick(event) {
3342
3405
  if (this.open() && !this.host.nativeElement.contains(event.target)) {
3343
3406
  this.close();
@@ -3347,70 +3410,98 @@ class StrctDropdown {
3347
3410
  this.close();
3348
3411
  }
3349
3412
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
3350
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDropdown, isStandalone: true, selector: "strct-dropdown", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" }, classAttribute: "strct-dd" }, ngImport: i0, template: `
3351
- <div
3352
- #trigger
3353
- class="strct-dd__trigger"
3354
- role="button"
3355
- tabindex="0"
3356
- (click)="toggle()"
3357
- (keydown.enter)="toggle()"
3358
- (keydown.space)="toggle()"
3359
- >
3413
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDropdown, isStandalone: true, selector: "strct-dropdown", inputs: { align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, popover: { classPropertyName: "popover", publicName: "popover", isSignal: true, isRequired: false, transformFunction: null }, popoverLabel: { classPropertyName: "popoverLabel", publicName: "popoverLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" }, classAttribute: "strct-dd" }, ngImport: i0, template: `
3414
+ <!--
3415
+ The wrapper is intentionally inert: the projected trigger should be a
3416
+ real button (nesting interactives fails axe). Its native click —
3417
+ pointer or Enter/Space — bubbles here; the strctDropdownTrigger
3418
+ directive puts aria-haspopup/aria-expanded on the button itself.
3419
+ -->
3420
+ <div #trigger class="strct-dd__trigger" (click)="toggle()">
3360
3421
  <ng-content select="[strctDropdownTrigger]" />
3361
3422
  </div>
3423
+ <!--
3424
+ One panel for both modes (a default ng-content per @if branch would strand
3425
+ the projected content in the inactive branch) — mode picks the semantics.
3426
+ -->
3362
3427
  @if (open()) {
3363
3428
  <div
3364
3429
  class="strct-dd__menu"
3430
+ [class.strct-dd__menu--popover]="popover()"
3365
3431
  [strctOverlay]="trigger"
3366
3432
  [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
3367
- role="menu"
3368
- tabindex="0"
3369
- (click)="close()"
3370
- (keydown.enter)="close()"
3371
- (keydown.space)="close()"
3433
+ [attr.role]="popover() ? 'dialog' : 'menu'"
3434
+ [attr.aria-label]="popover() ? popoverLabel() : null"
3435
+ [attr.tabindex]="popover() ? -1 : 0"
3436
+ (click)="onInnerActivate()"
3437
+ (keydown.enter)="onInnerActivate()"
3438
+ (keydown.space)="onInnerActivate()"
3372
3439
  >
3373
3440
  <ng-content />
3374
3441
  </div>
3375
3442
  }
3376
- `, isInline: true, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:200;min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"], dependencies: [{ kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3443
+ `, isInline: true, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:200;min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"], dependencies: [{ kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3377
3444
  }
3378
3445
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdown, decorators: [{
3379
3446
  type: Component,
3380
3447
  args: [{ selector: 'strct-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctOverlay], template: `
3381
- <div
3382
- #trigger
3383
- class="strct-dd__trigger"
3384
- role="button"
3385
- tabindex="0"
3386
- (click)="toggle()"
3387
- (keydown.enter)="toggle()"
3388
- (keydown.space)="toggle()"
3389
- >
3448
+ <!--
3449
+ The wrapper is intentionally inert: the projected trigger should be a
3450
+ real button (nesting interactives fails axe). Its native click —
3451
+ pointer or Enter/Space — bubbles here; the strctDropdownTrigger
3452
+ directive puts aria-haspopup/aria-expanded on the button itself.
3453
+ -->
3454
+ <div #trigger class="strct-dd__trigger" (click)="toggle()">
3390
3455
  <ng-content select="[strctDropdownTrigger]" />
3391
3456
  </div>
3457
+ <!--
3458
+ One panel for both modes (a default ng-content per @if branch would strand
3459
+ the projected content in the inactive branch) — mode picks the semantics.
3460
+ -->
3392
3461
  @if (open()) {
3393
3462
  <div
3394
3463
  class="strct-dd__menu"
3464
+ [class.strct-dd__menu--popover]="popover()"
3395
3465
  [strctOverlay]="trigger"
3396
3466
  [strctOverlayPlacement]="align() === 'end' ? 'bottom-end' : 'bottom-start'"
3397
- role="menu"
3398
- tabindex="0"
3399
- (click)="close()"
3400
- (keydown.enter)="close()"
3401
- (keydown.space)="close()"
3467
+ [attr.role]="popover() ? 'dialog' : 'menu'"
3468
+ [attr.aria-label]="popover() ? popoverLabel() : null"
3469
+ [attr.tabindex]="popover() ? -1 : 0"
3470
+ (click)="onInnerActivate()"
3471
+ (keydown.enter)="onInnerActivate()"
3472
+ (keydown.space)="onInnerActivate()"
3402
3473
  >
3403
3474
  <ng-content />
3404
3475
  </div>
3405
3476
  }
3406
- `, host: { class: 'strct-dd' }, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:200;min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"] }]
3407
- }], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], onDocClick: [{
3477
+ `, host: { class: 'strct-dd' }, styles: [".strct-dd{position:relative;display:inline-block}.strct-dd__trigger{display:inline-flex}.strct-dd__menu{position:absolute;top:calc(100% + 4px);left:0;z-index:200;min-width:170px;max-width:calc(100vw - 24px);padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);animation:strct-dd-in .1s ease}.strct-dd__menu--end{left:auto;right:0}.strct-dd__menu--popover{min-width:240px;padding:12px 14px}@keyframes strct-dd-in{0%{opacity:0;transform:translateY(-4px)}}\n"] }]
3478
+ }], ctorParameters: () => [], propDecorators: { align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "popover", required: false }] }], popoverLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "popoverLabel", required: false }] }], onDocClick: [{
3408
3479
  type: HostListener,
3409
3480
  args: ['document:click', ['$event']]
3410
3481
  }], onEscape: [{
3411
3482
  type: HostListener,
3412
3483
  args: ['document:keydown.escape']
3413
3484
  }] } });
3485
+ /**
3486
+ * Marks (and upgrades) the dropdown's trigger element. The attribute alone is
3487
+ * enough for content projection; importing the directive additionally wires
3488
+ * `aria-haspopup` / `aria-expanded` onto the real button.
3489
+ */
3490
+ class StrctDropdownTrigger {
3491
+ dd = inject(StrctDropdown, { optional: true });
3492
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdownTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3493
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.16", type: StrctDropdownTrigger, isStandalone: true, selector: "[strctDropdownTrigger]", host: { properties: { "attr.aria-haspopup": "dd ? (dd.popover() ? 'dialog' : 'menu') : null", "attr.aria-expanded": "dd ? dd.open() : null" } }, ngImport: i0 });
3494
+ }
3495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdownTrigger, decorators: [{
3496
+ type: Directive,
3497
+ args: [{
3498
+ selector: '[strctDropdownTrigger]',
3499
+ host: {
3500
+ '[attr.aria-haspopup]': "dd ? (dd.popover() ? 'dialog' : 'menu') : null",
3501
+ '[attr.aria-expanded]': 'dd ? dd.open() : null',
3502
+ },
3503
+ }]
3504
+ }] });
3414
3505
  /** A selectable row inside a `<strct-dropdown>`. */
3415
3506
  class StrctDropdownItem {
3416
3507
  /** Danger. */
@@ -4209,9 +4300,24 @@ class StrctCommandPalette {
4209
4300
  emptyText = input('No matches for', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
4210
4301
  /** Maximum rendered results. */
4211
4302
  maxResults = input(50, ...(ngDevMode ? [{ debugName: "maxResults" }] : /* istanbul ignore next */ []));
4303
+ /**
4304
+ * Internal ranked filtering. Set `false` for a **server-backed** palette:
4305
+ * items render in the order given (you already filtered them) and the query
4306
+ * reaches you through the two-way `query` model. `maxResults` still caps
4307
+ * rendering.
4308
+ */
4309
+ filter = input(true, { ...(ngDevMode ? { debugName: "filter" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4310
+ /** Show a "searching" row while server results are in flight. */
4311
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4312
+ /** Localizable text of the loading row. */
4313
+ loadingText = input('Searching…', ...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
4212
4314
  /** Emitted when a command is chosen (Enter or click); the palette closes. */
4213
4315
  picked = output();
4214
- query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
4316
+ /**
4317
+ * The typed query (two-way). The palette still owns typing and keyboard
4318
+ * behaviour; consumers observe it to serve results asynchronously.
4319
+ */
4320
+ query = model('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
4215
4321
  activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
4216
4322
  listId = `strct-cmdp-list-${++paletteCounter}`;
4217
4323
  previousActive = null;
@@ -4220,8 +4326,11 @@ class StrctCommandPalette {
4220
4326
  }
4221
4327
  /** Rank: label prefix > word-start > substring; group/keyword matches last. */
4222
4328
  filtered = computed(() => {
4223
- const q = this.query().trim().toLowerCase();
4224
4329
  const items = this.items();
4330
+ // Server-backed mode: the caller already filtered — render as given.
4331
+ if (!this.filter())
4332
+ return items.slice(0, this.maxResults());
4333
+ const q = this.query().trim().toLowerCase();
4225
4334
  if (!q)
4226
4335
  return items.slice(0, this.maxResults());
4227
4336
  const scored = [];
@@ -4246,6 +4355,12 @@ class StrctCommandPalette {
4246
4355
  .map((s) => s.item);
4247
4356
  }, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
4248
4357
  constructor() {
4358
+ // Async results can shrink under the cursor — keep the active option in range.
4359
+ effect(() => {
4360
+ const len = this.filtered().length;
4361
+ if (this.activeIndex() >= len)
4362
+ this.activeIndex.set(Math.max(0, len - 1));
4363
+ });
4249
4364
  // Focus the input on open; restore focus on close.
4250
4365
  effect(() => {
4251
4366
  if (this.open()) {
@@ -4317,7 +4432,7 @@ class StrctCommandPalette {
4317
4432
  this.open.set(false);
4318
4433
  }
4319
4434
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCommandPalette, deps: [], target: i0.ɵɵFactoryTarget.Component });
4320
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCommandPalette, isStandalone: true, selector: "strct-command-palette", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", picked: "picked" }, host: { listeners: { "document:keydown": "onGlobalKey($event)" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
4435
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCommandPalette, isStandalone: true, selector: "strct-command-palette", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", picked: "picked", query: "queryChange" }, host: { listeners: { "document:keydown": "onGlobalKey($event)" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
4321
4436
  @if (open()) {
4322
4437
  <!-- Backdrop: pointer-only dismiss (Escape covers keyboard). -->
4323
4438
  <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
@@ -4348,6 +4463,11 @@ class StrctCommandPalette {
4348
4463
  <strct-kbd>Esc</strct-kbd>
4349
4464
  </div>
4350
4465
  <div class="strct-cmdp__list" role="listbox" [id]="listId">
4466
+ @if (loading()) {
4467
+ <div class="strct-cmdp__loading" aria-live="polite">
4468
+ <span class="strct-cmdp__spin" aria-hidden="true"></span>{{ loadingText() }}
4469
+ </div>
4470
+ }
4351
4471
  @for (item of filtered(); track item.id; let i = $index) {
4352
4472
  <button
4353
4473
  type="button"
@@ -4377,13 +4497,15 @@ class StrctCommandPalette {
4377
4497
  }
4378
4498
  </button>
4379
4499
  } @empty {
4380
- <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
4500
+ @if (!loading()) {
4501
+ <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
4502
+ }
4381
4503
  }
4382
4504
  </div>
4383
4505
  </div>
4384
4506
  </div>
4385
4507
  }
4386
- `, isInline: true, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:1200;display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4508
+ `, isInline: true, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:1200;display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4387
4509
  }
4388
4510
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCommandPalette, decorators: [{
4389
4511
  type: Component,
@@ -4418,6 +4540,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4418
4540
  <strct-kbd>Esc</strct-kbd>
4419
4541
  </div>
4420
4542
  <div class="strct-cmdp__list" role="listbox" [id]="listId">
4543
+ @if (loading()) {
4544
+ <div class="strct-cmdp__loading" aria-live="polite">
4545
+ <span class="strct-cmdp__spin" aria-hidden="true"></span>{{ loadingText() }}
4546
+ </div>
4547
+ }
4421
4548
  @for (item of filtered(); track item.id; let i = $index) {
4422
4549
  <button
4423
4550
  type="button"
@@ -4447,14 +4574,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4447
4574
  }
4448
4575
  </button>
4449
4576
  } @empty {
4450
- <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
4577
+ @if (!loading()) {
4578
+ <div class="strct-cmdp__empty">{{ emptyText() }} “{{ query() }}”</div>
4579
+ }
4451
4580
  }
4452
4581
  </div>
4453
4582
  </div>
4454
4583
  </div>
4455
4584
  }
4456
- `, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:1200;display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"] }]
4457
- }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
4585
+ `, styles: [".strct-cmdp__backdrop{position:fixed;inset:0;z-index:1200;display:flex;justify-content:center;align-items:flex-start;padding:14vh 16px 16px;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:strct-cmdp-fade .12s ease}.strct-cmdp{width:100%;max-width:560px;max-height:60vh;display:flex;flex-direction:column;background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-xl);box-shadow:var(--shadow-floating);overflow:hidden;animation:strct-cmdp-rise .14s ease}.strct-cmdp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--b1);color:var(--t3)}.strct-cmdp__input{flex:1;min-width:0;border:0;background:transparent;color:var(--t1);font-size:15px;font-family:var(--font);outline:none}.strct-cmdp__list{overflow-y:auto;padding:6px}.strct-cmdp__item{display:flex;align-items:center;gap:11px;width:100%;padding:9px 11px;border:0;border-radius:var(--radius-md);background:transparent;cursor:pointer;text-align:start;font-family:var(--font)}.strct-cmdp__item.is-active{background:var(--acc-m)}.strct-cmdp__icon{color:var(--t2);flex-shrink:0}.strct-cmdp__item.is-active .strct-cmdp__icon{color:var(--acc)}.strct-cmdp__label{flex:1;min-width:0;font-size:var(--text-lg);color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-cmdp__group{font-size:var(--text-sm);color:var(--t3);flex-shrink:0}.strct-cmdp__loading{display:flex;align-items:center;gap:9px;padding:10px 12px;font-size:var(--text-md);color:var(--t3)}.strct-cmdp__spin{width:13px;height:13px;border:2px solid var(--acc30);border-top-color:var(--acc);border-radius:50%;flex-shrink:0}@media(prefers-reduced-motion:no-preference){.strct-cmdp__spin{animation:strct-cmdp-spin .8s linear infinite}}@keyframes strct-cmdp-spin{to{transform:rotate(360deg)}}.strct-cmdp__empty{padding:20px;text-align:center;font-size:var(--text-md);color:var(--t3)}@keyframes strct-cmdp-fade{0%{opacity:0}}@keyframes strct-cmdp-rise{0%{opacity:0;transform:translateY(-8px)}}@media(prefers-reduced-motion:reduce){.strct-cmdp__backdrop,.strct-cmdp{animation:none}}\n"] }]
4586
+ }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }] } });
4458
4587
 
4459
4588
  /**
4460
4589
  * Compact search pill — the pattern from the docs header: a leading search
@@ -4727,6 +4856,12 @@ class StrctCode {
4727
4856
  collapsed = model(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
4728
4857
  /** Show a line-number gutter (numbers are never copied — `code` is). */
4729
4858
  lineNumbers = input(false, { ...(ngDevMode ? { debugName: "lineNumbers" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4859
+ /**
4860
+ * Soft-wrap long unbroken text (PEM/CSR blocks, base64, long commands) so a
4861
+ * dialog never scrolls horizontally. Wrapping breaks the gutter's visual
4862
+ * alignment, so `wrap` takes precedence and hides `lineNumbers`.
4863
+ */
4864
+ wrap = input(false, { ...(ngDevMode ? { debugName: "wrap" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
4730
4865
  /** Scroll the body past this height (px); null grows freely. */
4731
4866
  maxHeight = input(null, ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
4732
4867
  /** Localizable labels for the collapse toggle. */
@@ -4737,7 +4872,7 @@ class StrctCode {
4737
4872
  return Array.from({ length: n }, (_, i) => i + 1);
4738
4873
  }, ...(ngDevMode ? [{ debugName: "lineNos" }] : /* istanbul ignore next */ []));
4739
4874
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCode, deps: [], target: i0.ɵɵFactoryTarget.Component });
4740
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCode, isStandalone: true, selector: "strct-code", inputs: { code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, lineNumbers: { classPropertyName: "lineNumbers", publicName: "lineNumbers", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, collapseLabel: { classPropertyName: "collapseLabel", publicName: "collapseLabel", isSignal: true, isRequired: false, transformFunction: null }, expandLabel: { classPropertyName: "expandLabel", publicName: "expandLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "class.strct-code--collapsed": "collapsible() && collapsed()" }, classAttribute: "strct-code" }, ngImport: i0, template: `
4875
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCode, isStandalone: true, selector: "strct-code", inputs: { code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, lineNumbers: { classPropertyName: "lineNumbers", publicName: "lineNumbers", isSignal: true, isRequired: false, transformFunction: null }, wrap: { classPropertyName: "wrap", publicName: "wrap", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, collapseLabel: { classPropertyName: "collapseLabel", publicName: "collapseLabel", isSignal: true, isRequired: false, transformFunction: null }, expandLabel: { classPropertyName: "expandLabel", publicName: "expandLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "class.strct-code--collapsed": "collapsible() && collapsed()" }, classAttribute: "strct-code" }, ngImport: i0, template: `
4741
4876
  @if (title() || language() || copyable() || collapsible()) {
4742
4877
  <div class="strct-code__head">
4743
4878
  @if (collapsible()) {
@@ -4775,19 +4910,20 @@ class StrctCode {
4775
4910
  @if (!collapsible() || !collapsed()) {
4776
4911
  <div
4777
4912
  class="strct-code__scroll"
4913
+ [class.strct-code__scroll--wrap]="wrap()"
4778
4914
  tabindex="0"
4779
4915
  role="region"
4780
4916
  [attr.aria-label]="title() || language() || 'code'"
4781
4917
  [style.max-height.px]="maxHeight()"
4782
4918
  >
4783
4919
  <pre class="strct-code__pre"><!--
4784
- -->@if (lineNumbers()) {<!--
4920
+ -->@if (lineNumbers() && !wrap()) {<!--
4785
4921
  --><span class="strct-code__gutter" aria-hidden="true">@for (n of lineNos(); track n) {<span class="strct-code__ln">{{ n }}</span>}</span><!--
4786
4922
  -->}<!--
4787
4923
  --><code class="strct-code__code">{{ code() }}</code></pre>
4788
4924
  </div>
4789
4925
  }
4790
- `, isInline: true, styles: [".strct-code{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-code__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-code--collapsed .strct-code__head{border-bottom:0}.strct-code__toggle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin:-3px 0 -3px -4px;padding:0;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-code__toggle:hover{color:var(--t1);background:var(--bg-3)}.strct-code__toggle:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-code__toggle strct-icon{transition:transform .15s ease}.strct-code__chev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-code__toggle strct-icon{transition:none}}.strct-code__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-code__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-code__grow{flex:1}.strct-code__scroll{overflow:auto}.strct-code__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-code__pre{display:flex;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--t1)}.strct-code__gutter{display:flex;flex-direction:column;flex-shrink:0;margin-inline-end:12px;padding-inline-end:10px;border-inline-end:1px solid var(--b1);color:var(--t3);text-align:end;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-code__code{display:block;white-space:pre}\n"], dependencies: [{ kind: "component", type: StrctCopy, selector: "strct-copy", inputs: ["text", "label", "ariaLabel", "copyLabel", "copiedLabel"], outputs: ["copied"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4926
+ `, isInline: true, styles: [".strct-code{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-code__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-code--collapsed .strct-code__head{border-bottom:0}.strct-code__toggle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin:-3px 0 -3px -4px;padding:0;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-code__toggle:hover{color:var(--t1);background:var(--bg-3)}.strct-code__toggle:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-code__toggle strct-icon{transition:transform .15s ease}.strct-code__chev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-code__toggle strct-icon{transition:none}}.strct-code__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-code__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-code__grow{flex:1}.strct-code__scroll{overflow:auto}.strct-code__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-code__pre{display:flex;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--t1)}.strct-code__gutter{display:flex;flex-direction:column;flex-shrink:0;margin-inline-end:12px;padding-inline-end:10px;border-inline-end:1px solid var(--b1);color:var(--t3);text-align:end;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-code__code{display:block;white-space:pre}.strct-code__scroll--wrap .strct-code__code{white-space:pre-wrap;overflow-wrap:anywhere}\n"], dependencies: [{ kind: "component", type: StrctCopy, selector: "strct-copy", inputs: ["text", "label", "ariaLabel", "copyLabel", "copiedLabel"], outputs: ["copied"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
4791
4927
  }
4792
4928
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCode, decorators: [{
4793
4929
  type: Component,
@@ -4829,13 +4965,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4829
4965
  @if (!collapsible() || !collapsed()) {
4830
4966
  <div
4831
4967
  class="strct-code__scroll"
4968
+ [class.strct-code__scroll--wrap]="wrap()"
4832
4969
  tabindex="0"
4833
4970
  role="region"
4834
4971
  [attr.aria-label]="title() || language() || 'code'"
4835
4972
  [style.max-height.px]="maxHeight()"
4836
4973
  >
4837
4974
  <pre class="strct-code__pre"><!--
4838
- -->@if (lineNumbers()) {<!--
4975
+ -->@if (lineNumbers() && !wrap()) {<!--
4839
4976
  --><span class="strct-code__gutter" aria-hidden="true">@for (n of lineNos(); track n) {<span class="strct-code__ln">{{ n }}</span>}</span><!--
4840
4977
  -->}<!--
4841
4978
  --><code class="strct-code__code">{{ code() }}</code></pre>
@@ -4844,8 +4981,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
4844
4981
  `, host: {
4845
4982
  class: 'strct-code',
4846
4983
  '[class.strct-code--collapsed]': 'collapsible() && collapsed()',
4847
- }, styles: [".strct-code{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-code__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-code--collapsed .strct-code__head{border-bottom:0}.strct-code__toggle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin:-3px 0 -3px -4px;padding:0;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-code__toggle:hover{color:var(--t1);background:var(--bg-3)}.strct-code__toggle:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-code__toggle strct-icon{transition:transform .15s ease}.strct-code__chev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-code__toggle strct-icon{transition:none}}.strct-code__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-code__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-code__grow{flex:1}.strct-code__scroll{overflow:auto}.strct-code__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-code__pre{display:flex;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--t1)}.strct-code__gutter{display:flex;flex-direction:column;flex-shrink:0;margin-inline-end:12px;padding-inline-end:10px;border-inline-end:1px solid var(--b1);color:var(--t3);text-align:end;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-code__code{display:block;white-space:pre}\n"] }]
4848
- }], propDecorators: { code: [{ type: i0.Input, args: [{ isSignal: true, alias: "code", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], lineNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineNumbers", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], collapseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseLabel", required: false }] }], expandLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandLabel", required: false }] }] } });
4984
+ }, styles: [".strct-code{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-code__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-code--collapsed .strct-code__head{border-bottom:0}.strct-code__toggle{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin:-3px 0 -3px -4px;padding:0;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer}.strct-code__toggle:hover{color:var(--t1);background:var(--bg-3)}.strct-code__toggle:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-code__toggle strct-icon{transition:transform .15s ease}.strct-code__chev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-code__toggle strct-icon{transition:none}}.strct-code__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-code__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-code__grow{flex:1}.strct-code__scroll{overflow:auto}.strct-code__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-code__pre{display:flex;margin:0;padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--t1)}.strct-code__gutter{display:flex;flex-direction:column;flex-shrink:0;margin-inline-end:12px;padding-inline-end:10px;border-inline-end:1px solid var(--b1);color:var(--t3);text-align:end;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-code__code{display:block;white-space:pre}.strct-code__scroll--wrap .strct-code__code{white-space:pre-wrap;overflow-wrap:anywhere}\n"] }]
4985
+ }], propDecorators: { code: [{ type: i0.Input, args: [{ isSignal: true, alias: "code", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], lineNumbers: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineNumbers", required: false }] }], wrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrap", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], collapseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseLabel", required: false }] }], expandLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandLabel", required: false }] }] } });
4849
4986
 
4850
4987
  /**
4851
4988
  * The standard strip above a data grid: a search field, the active filter
@@ -8146,6 +8283,11 @@ const DG_LABELS = {
8146
8283
  chooseColumns: 'Choose columns',
8147
8284
  refresh: 'Refresh data',
8148
8285
  actions: 'Actions',
8286
+ filterColumn: 'Filter',
8287
+ filterPlaceholder: 'Filter…',
8288
+ clearFilter: 'Clear filter',
8289
+ editCell: 'Edit',
8290
+ toggleChildren: 'Toggle children',
8149
8291
  };
8150
8292
  /** Utility-column widths used when sticky columns are active (px). */
8151
8293
  const UTIL_W = { detail: 36, expand: 36, sel: 40 };
@@ -8273,6 +8415,19 @@ class StrctDatagrid {
8273
8415
  * groups. Paging is bypassed while grouped; not combinable with `virtual`.
8274
8416
  */
8275
8417
  groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
8418
+ /**
8419
+ * Two-way per-column filter state: `key → contains-text` (text filter) or
8420
+ * `key → unknown[]` (checked value set). Applied client-side; in `lazy`
8421
+ * mode it is passed through on `lazyLoad` instead.
8422
+ */
8423
+ filters = model({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
8424
+ /**
8425
+ * Tree-grid: the row property holding a row's children array. Rows render
8426
+ * hierarchically with indent + carets; sorting applies per sibling level;
8427
+ * an active filter shows matches with their ancestors, force-expanded.
8428
+ * Not combinable with `groupBy` or `lazy`.
8429
+ */
8430
+ childrenKey = input(null, ...(ngDevMode ? [{ debugName: "childrenKey" }] : /* istanbul ignore next */ []));
8276
8431
  /** Emitted when the selection changes. */
8277
8432
  selectionChange = output();
8278
8433
  /** Emitted when the refresh button is clicked. */
@@ -8281,6 +8436,9 @@ class StrctDatagrid {
8281
8436
  rowAction = output();
8282
8437
  /** Server-side data request (lazy mode): load this page / sort and set `rows`. */
8283
8438
  lazyLoad = output();
8439
+ /** An editable cell was committed (Enter / blur). The grid does not mutate
8440
+ * rows — apply `value` to your data and pass the updated array back in. */
8441
+ cellEdit = output();
8284
8442
  menu = inject(StrctMenuService);
8285
8443
  detailDef = contentChild(StrctRowDetailDef, ...(ngDevMode ? [{ debugName: "detailDef" }] : /* istanbul ignore next */ []));
8286
8444
  actionBarDef = contentChild(StrctDatagridActionBar, ...(ngDevMode ? [{ debugName: "actionBarDef" }] : /* istanbul ignore next */ []));
@@ -8377,6 +8535,11 @@ class StrctDatagrid {
8377
8535
  dragKey = signal(null, ...(ngDevMode ? [{ debugName: "dragKey" }] : /* istanbul ignore next */ []));
8378
8536
  dropKey = signal(null, ...(ngDevMode ? [{ debugName: "dropKey" }] : /* istanbul ignore next */ []));
8379
8537
  onColDragStart(key, event) {
8538
+ // Text selection inside the filter panel must not start a column drag.
8539
+ if (event.target?.closest?.('.strct-dg__filterpanel')) {
8540
+ event.preventDefault();
8541
+ return;
8542
+ }
8380
8543
  if (!this.reorderable())
8381
8544
  return;
8382
8545
  this.dragKey.set(key);
@@ -8446,12 +8609,154 @@ class StrctDatagrid {
8446
8609
  itemKey(it) {
8447
8610
  return it.group ? `strct-group:${String(it.group.key)}` : this.rowKey(it.row);
8448
8611
  }
8612
+ // ── Column filter popover ──────────────────────────────────────
8613
+ filterOpenKey = signal(null, ...(ngDevMode ? [{ debugName: "filterOpenKey" }] : /* istanbul ignore next */ []));
8614
+ toggleFilterPanel(key, event) {
8615
+ event.stopPropagation();
8616
+ this.filterOpenKey.set(this.filterOpenKey() === key ? null : key);
8617
+ }
8618
+ hasFilter(key) {
8619
+ const v = this.filters()[key];
8620
+ return Array.isArray(v) ? v.length > 0 : v != null && String(v).trim() !== '';
8621
+ }
8622
+ textFilter(key) {
8623
+ const v = this.filters()[key];
8624
+ return typeof v === 'string' ? v : '';
8625
+ }
8626
+ setTextFilter(key, value) {
8627
+ this.filters.update((f) => ({ ...f, [key]: value }));
8628
+ }
8629
+ optionChecked(key, opt) {
8630
+ const v = this.filters()[key];
8631
+ return Array.isArray(v) && v.includes(opt);
8632
+ }
8633
+ toggleFilterOption(key, opt) {
8634
+ this.filters.update((f) => {
8635
+ const cur = Array.isArray(f[key]) ? [...f[key]] : [];
8636
+ const i = cur.indexOf(opt);
8637
+ if (i >= 0)
8638
+ cur.splice(i, 1);
8639
+ else
8640
+ cur.push(opt);
8641
+ return { ...f, [key]: cur };
8642
+ });
8643
+ }
8644
+ clearFilter(key) {
8645
+ this.filters.update((f) => {
8646
+ const next = { ...f };
8647
+ delete next[key];
8648
+ return next;
8649
+ });
8650
+ }
8651
+ onDocClickFilter(event) {
8652
+ if (!this.filterOpenKey())
8653
+ return;
8654
+ const t = event.target;
8655
+ if (t.closest('.strct-dg__filterpanel') || t.closest('.strct-dg__filterbtn'))
8656
+ return;
8657
+ this.filterOpenKey.set(null);
8658
+ }
8659
+ onEscapeFilter() {
8660
+ this.filterOpenKey.set(null);
8661
+ }
8662
+ // ── Tree-grid state ────────────────────────────────────────────
8663
+ expandedTree = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedTree" }] : /* istanbul ignore next */ []));
8664
+ treeDepth(row) {
8665
+ return this.treeFlat().meta.get(this.rowKey(row))?.depth ?? 0;
8666
+ }
8667
+ treeHasChildren(row) {
8668
+ return this.treeFlat().meta.get(this.rowKey(row))?.hasChildren ?? false;
8669
+ }
8670
+ treeExpanded(row) {
8671
+ return this.treeFlat().meta.get(this.rowKey(row))?.expanded ?? false;
8672
+ }
8673
+ /** Expand / collapse one tree row. */
8674
+ toggleTreeRow(row) {
8675
+ const key = this.rowKey(row);
8676
+ const next = new Set(this.expandedTree());
8677
+ if (next.has(key))
8678
+ next.delete(key);
8679
+ else
8680
+ next.add(key);
8681
+ this.expandedTree.set(next);
8682
+ }
8683
+ // ── Inline cell editing ────────────────────────────────────────
8684
+ hostRef = inject(ElementRef);
8685
+ editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
8686
+ isEditingCell(row, col) {
8687
+ const e = this.editing();
8688
+ return !!e && e.rowKey === this.rowKey(row) && e.colKey === col.key;
8689
+ }
8690
+ startEdit(row, col) {
8691
+ this.editing.set({ rowKey: this.rowKey(row), colKey: col.key });
8692
+ setTimeout(() => this.hostRef.nativeElement.querySelector('.strct-dg__editinput')?.focus());
8693
+ }
8694
+ commitEdit(row, col, event) {
8695
+ if (!this.isEditingCell(row, col))
8696
+ return; // Escape/Enter already closed it
8697
+ const value = event.target.value;
8698
+ this.editing.set(null);
8699
+ const previous = row[col.key];
8700
+ if (String(previous ?? '') !== value)
8701
+ this.cellEdit.emit({ row, column: col, value, previous });
8702
+ }
8703
+ cancelEdit(event) {
8704
+ event?.stopPropagation();
8705
+ this.editing.set(null);
8706
+ }
8707
+ /** Active (non-empty) filter entries. */
8708
+ activeFilters = computed(() => Object.entries(this.filters()).filter(([, v]) => Array.isArray(v) ? v.length > 0 : String(v).trim() !== ''), ...(ngDevMode ? [{ debugName: "activeFilters" }] : /* istanbul ignore next */ []));
8709
+ matchesFilters(row) {
8710
+ return this.activeFilters().every(([key, v]) => Array.isArray(v)
8711
+ ? v.some((opt) => opt === row[key])
8712
+ : String(row[key] ?? '')
8713
+ .toLowerCase()
8714
+ .includes(String(v).trim().toLowerCase()));
8715
+ }
8716
+ /** Rows surviving the per-column filters (client-side modes only). */
8717
+ filteredRows = computed(() => {
8718
+ if (this.lazy() || !this.activeFilters().length)
8719
+ return this.rows();
8720
+ return this.rows().filter((r) => this.matchesFilters(r));
8721
+ }, ...(ngDevMode ? [{ debugName: "filteredRows" }] : /* istanbul ignore next */ []));
8722
+ /** Tree mode: flattened visible rows + per-row depth/children/expanded meta. */
8723
+ treeFlat = computed(() => {
8724
+ const ck = this.childrenKey();
8725
+ const { key, dir } = this.sort();
8726
+ const sign = dir === 'asc' ? 1 : -1;
8727
+ const sortRows = (rs) => {
8728
+ const c = [...rs];
8729
+ if (key)
8730
+ c.sort((a, b) => sign * this.compare(a[key], b[key]));
8731
+ return c;
8732
+ };
8733
+ const filtering = this.activeFilters().length > 0;
8734
+ const kidsOf = (r) => r[ck] ?? [];
8735
+ const survives = (r) => this.matchesFilters(r) || kidsOf(r).some(survives);
8736
+ const meta = new Map();
8737
+ const rowsOut = [];
8738
+ const walk = (rs, depth) => {
8739
+ for (const r of sortRows(rs)) {
8740
+ const kids = filtering ? kidsOf(r).filter(survives) : kidsOf(r);
8741
+ // Filtering force-expands so matches are never hidden under a fold.
8742
+ const expanded = filtering ? true : this.expandedTree().has(this.rowKey(r));
8743
+ meta.set(this.rowKey(r), { depth, hasChildren: kids.length > 0, expanded });
8744
+ rowsOut.push(r);
8745
+ if (kids.length && expanded)
8746
+ walk(kids, depth + 1);
8747
+ }
8748
+ };
8749
+ walk(filtering ? this.rows().filter(survives) : this.rows(), 0);
8750
+ return { rows: rowsOut, meta };
8751
+ }, ...(ngDevMode ? [{ debugName: "treeFlat" }] : /* istanbul ignore next */ []));
8449
8752
  sorted = computed(() => {
8450
8753
  // Server-side mode: rows arrive already ordered / sliced — never touch them.
8451
8754
  if (this.lazy())
8452
8755
  return this.rows();
8756
+ if (this.childrenKey())
8757
+ return this.treeFlat().rows;
8453
8758
  const { key, dir } = this.sort();
8454
- const data = [...this.rows()];
8759
+ const data = [...this.filteredRows()];
8455
8760
  if (!key)
8456
8761
  return data;
8457
8762
  const sign = dir === 'asc' ? 1 : -1;
@@ -8573,6 +8878,11 @@ class StrctDatagrid {
8573
8878
  if (this.page() > pageCount)
8574
8879
  this.page.set(pageCount);
8575
8880
  });
8881
+ // A changed filter always restarts from page 1.
8882
+ effect(() => {
8883
+ this.filters();
8884
+ untracked(() => this.page.set(1));
8885
+ });
8576
8886
  // Seed the selection from initialSelection (re-runs only when the input changes,
8577
8887
  // so a user's own toggles afterward are untouched).
8578
8888
  effect(() => {
@@ -8591,6 +8901,7 @@ class StrctDatagrid {
8591
8901
  pageSize: this.pageSize(),
8592
8902
  sortKey: this.sort().key,
8593
8903
  sortDir: this.sort().dir,
8904
+ filters: this.filters(),
8594
8905
  };
8595
8906
  const key = JSON.stringify(state);
8596
8907
  untracked(() => {
@@ -8837,7 +9148,7 @@ class StrctDatagrid {
8837
9148
  return String(a ?? '').localeCompare(String(b ?? ''), undefined, { numeric: true });
8838
9149
  }
8839
9150
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
8840
- 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 }, singleLine: { classPropertyName: "singleLine", publicName: "singleLine", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, columnChooser: { classPropertyName: "columnChooser", publicName: "columnChooser", isSignal: true, isRequired: false, transformFunction: null }, sync: { classPropertyName: "sync", publicName: "sync", isSignal: true, isRequired: false, transformFunction: null }, footerActionsDisabled: { classPropertyName: "footerActionsDisabled", publicName: "footerActionsDisabled", isSignal: true, isRequired: false, transformFunction: null }, rowId: { classPropertyName: "rowId", publicName: "rowId", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, virtual: { classPropertyName: "virtual", publicName: "virtual", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, stateKey: { classPropertyName: "stateKey", publicName: "stateKey", isSignal: true, isRequired: false, transformFunction: null }, columnState: { classPropertyName: "columnState", publicName: "columnState", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnState: "columnStateChange", selectionChange: "selectionChange", syncChange: "syncChange", rowAction: "rowAction", lazyLoad: "lazyLoad" }, host: { properties: { "class.strct-dg-host--compact": "compact()", "class.strct-dg-host--singleline": "singleLine()", "class.strct-dg-host--virtual": "virtual()", "class.strct-dg-host--sticky": "stickyActive()" }, 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: `
9151
+ 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 }, singleLine: { classPropertyName: "singleLine", publicName: "singleLine", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, columnChooser: { classPropertyName: "columnChooser", publicName: "columnChooser", isSignal: true, isRequired: false, transformFunction: null }, sync: { classPropertyName: "sync", publicName: "sync", isSignal: true, isRequired: false, transformFunction: null }, footerActionsDisabled: { classPropertyName: "footerActionsDisabled", publicName: "footerActionsDisabled", isSignal: true, isRequired: false, transformFunction: null }, rowId: { classPropertyName: "rowId", publicName: "rowId", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, virtual: { classPropertyName: "virtual", publicName: "virtual", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, stateKey: { classPropertyName: "stateKey", publicName: "stateKey", isSignal: true, isRequired: false, transformFunction: null }, columnState: { classPropertyName: "columnState", publicName: "columnState", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, childrenKey: { classPropertyName: "childrenKey", publicName: "childrenKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnState: "columnStateChange", filters: "filtersChange", selectionChange: "selectionChange", syncChange: "syncChange", rowAction: "rowAction", lazyLoad: "lazyLoad", cellEdit: "cellEdit" }, host: { listeners: { "document:click": "onDocClickFilter($event)", "document:keydown.escape": "onEscapeFilter()" }, properties: { "class.strct-dg-host--compact": "compact()", "class.strct-dg-host--singleline": "singleLine()", "class.strct-dg-host--virtual": "virtual()", "class.strct-dg-host--sticky": "stickyActive()" }, 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: `
8841
9152
  @if (actionBarDef()) {
8842
9153
  <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
8843
9154
  }
@@ -8851,7 +9162,7 @@ class StrctDatagrid {
8851
9162
  [style.max-height.px]="virtual() ? viewportHeight() : null"
8852
9163
  (scroll)="virtual() && onScroll($event)"
8853
9164
  >
8854
- <table class="strct-dg">
9165
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
8855
9166
  <thead>
8856
9167
  <tr>
8857
9168
  @if (canDetail()) {
@@ -8911,6 +9222,62 @@ class StrctDatagrid {
8911
9222
  [size]="13"
8912
9223
  />
8913
9224
  }
9225
+ @if (col.filterable || col.filterOptions) {
9226
+ <button
9227
+ #fbtn
9228
+ type="button"
9229
+ class="strct-dg__filterbtn"
9230
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9231
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9232
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9233
+ (click)="toggleFilterPanel(col.key, $event)"
9234
+ (keydown.enter)="$event.stopPropagation()"
9235
+ (keydown.space)="$event.stopPropagation()"
9236
+ >
9237
+ <strct-icon name="filter" [size]="12" />
9238
+ </button>
9239
+ @if (filterOpenKey() === col.key) {
9240
+ <div
9241
+ class="strct-dg__filterpanel"
9242
+ role="dialog"
9243
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9244
+ [strctOverlay]="fbtn"
9245
+ strctOverlayPlacement="bottom-start"
9246
+ (click)="$event.stopPropagation()"
9247
+ (keydown.enter)="$event.stopPropagation()"
9248
+ (keydown.space)="$event.stopPropagation()"
9249
+ >
9250
+ @if (col.filterOptions; as opts) {
9251
+ @for (opt of opts; track $index) {
9252
+ <label class="strct-dg__filteropt">
9253
+ <input
9254
+ type="checkbox"
9255
+ [checked]="optionChecked(col.key, opt)"
9256
+ (change)="toggleFilterOption(col.key, opt)"
9257
+ />
9258
+ <span>{{ opt }}</span>
9259
+ </label>
9260
+ }
9261
+ } @else {
9262
+ <input
9263
+ class="strct-dg__filterinput"
9264
+ type="text"
9265
+ [attr.placeholder]="L().filterPlaceholder"
9266
+ [value]="textFilter(col.key)"
9267
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9268
+ />
9269
+ }
9270
+ <button
9271
+ type="button"
9272
+ class="strct-dg__filterclear"
9273
+ [disabled]="!hasFilter(col.key)"
9274
+ (click)="clearFilter(col.key)"
9275
+ >
9276
+ {{ L().clearFilter }}
9277
+ </button>
9278
+ </div>
9279
+ }
9280
+ }
8914
9281
  </span>
8915
9282
  @if (resizable() && col.resizable !== false) {
8916
9283
  <div
@@ -8981,6 +9348,10 @@ class StrctDatagrid {
8981
9348
  <tr
8982
9349
  [class.strct-dg__row--selected]="isSelected(row)"
8983
9350
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9351
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9352
+ [attr.aria-expanded]="
9353
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9354
+ "
8984
9355
  >
8985
9356
  @if (canDetail()) {
8986
9357
  <td
@@ -9031,14 +9402,51 @@ class StrctDatagrid {
9031
9402
  />
9032
9403
  </td>
9033
9404
  }
9034
- @for (col of visibleColumns(); track col.key) {
9405
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9035
9406
  <td
9036
9407
  [style.text-align]="col.align ?? 'start'"
9037
9408
  [class.strct-dg__cell--sticky]="isSticky(col)"
9038
9409
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9410
+ [class.strct-dg__cell--editable]="col.editable"
9039
9411
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9412
+ (dblclick)="col.editable && startEdit(row, col)"
9040
9413
  >
9041
- @if (cellTemplate(col.key); as tpl) {
9414
+ @if (childrenKey() && colIdx === 0) {
9415
+ <span
9416
+ class="strct-dg__treepad"
9417
+ [style.width.px]="treeDepth(row) * 18"
9418
+ aria-hidden="true"
9419
+ ></span>
9420
+ @if (treeHasChildren(row)) {
9421
+ <button
9422
+ type="button"
9423
+ class="strct-dg__treebtn"
9424
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9425
+ [attr.aria-expanded]="treeExpanded(row)"
9426
+ [attr.aria-label]="L().toggleChildren"
9427
+ (click)="toggleTreeRow(row)"
9428
+ >
9429
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9430
+ </button>
9431
+ } @else {
9432
+ <span
9433
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9434
+ aria-hidden="true"
9435
+ ></span>
9436
+ }
9437
+ }
9438
+ @if (isEditingCell(row, col)) {
9439
+ <input
9440
+ class="strct-dg__editinput"
9441
+ type="text"
9442
+ [value]="row[col.key] ?? ''"
9443
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9444
+ (keydown.enter)="commitEdit(row, col, $event)"
9445
+ (keydown.escape)="cancelEdit($event)"
9446
+ (blur)="commitEdit(row, col, $event)"
9447
+ (click)="$event.stopPropagation()"
9448
+ />
9449
+ } @else if (cellTemplate(col.key); as tpl) {
9042
9450
  <ng-container
9043
9451
  [ngTemplateOutlet]="tpl"
9044
9452
  [ngTemplateOutletContext]="{
@@ -9176,7 +9584,7 @@ class StrctDatagrid {
9176
9584
  </div>
9177
9585
  </div>
9178
9586
  }
9179
- `, isInline: true, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;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__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background: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__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.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;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius: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-inline-end: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-inline-end: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-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;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)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.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:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctPagination, selector: "strct-pagination", inputs: ["prevLabel", "nextLabel", "regionLabel", "total", "pageSize", "page"], outputs: ["pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "isDisabled", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange", "isDisabledChange"] }, { kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctButtonGroup, selector: "strct-button-group" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9587
+ `, isInline: true, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;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__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background: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__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.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;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius: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-inline-end: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-inline-end: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-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;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)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.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:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:210;display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctPagination, selector: "strct-pagination", inputs: ["prevLabel", "nextLabel", "regionLabel", "total", "pageSize", "page"], outputs: ["pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "isDisabled", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange", "isDisabledChange"] }, { kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctButtonGroup, selector: "strct-button-group" }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9180
9588
  }
9181
9589
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, decorators: [{
9182
9590
  type: Component,
@@ -9187,6 +9595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9187
9595
  StrctCheckbox,
9188
9596
  StrctButton,
9189
9597
  StrctButtonGroup,
9598
+ StrctOverlay,
9190
9599
  ], template: `
9191
9600
  @if (actionBarDef()) {
9192
9601
  <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
@@ -9201,7 +9610,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9201
9610
  [style.max-height.px]="virtual() ? viewportHeight() : null"
9202
9611
  (scroll)="virtual() && onScroll($event)"
9203
9612
  >
9204
- <table class="strct-dg">
9613
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
9205
9614
  <thead>
9206
9615
  <tr>
9207
9616
  @if (canDetail()) {
@@ -9261,6 +9670,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9261
9670
  [size]="13"
9262
9671
  />
9263
9672
  }
9673
+ @if (col.filterable || col.filterOptions) {
9674
+ <button
9675
+ #fbtn
9676
+ type="button"
9677
+ class="strct-dg__filterbtn"
9678
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9679
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9680
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9681
+ (click)="toggleFilterPanel(col.key, $event)"
9682
+ (keydown.enter)="$event.stopPropagation()"
9683
+ (keydown.space)="$event.stopPropagation()"
9684
+ >
9685
+ <strct-icon name="filter" [size]="12" />
9686
+ </button>
9687
+ @if (filterOpenKey() === col.key) {
9688
+ <div
9689
+ class="strct-dg__filterpanel"
9690
+ role="dialog"
9691
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9692
+ [strctOverlay]="fbtn"
9693
+ strctOverlayPlacement="bottom-start"
9694
+ (click)="$event.stopPropagation()"
9695
+ (keydown.enter)="$event.stopPropagation()"
9696
+ (keydown.space)="$event.stopPropagation()"
9697
+ >
9698
+ @if (col.filterOptions; as opts) {
9699
+ @for (opt of opts; track $index) {
9700
+ <label class="strct-dg__filteropt">
9701
+ <input
9702
+ type="checkbox"
9703
+ [checked]="optionChecked(col.key, opt)"
9704
+ (change)="toggleFilterOption(col.key, opt)"
9705
+ />
9706
+ <span>{{ opt }}</span>
9707
+ </label>
9708
+ }
9709
+ } @else {
9710
+ <input
9711
+ class="strct-dg__filterinput"
9712
+ type="text"
9713
+ [attr.placeholder]="L().filterPlaceholder"
9714
+ [value]="textFilter(col.key)"
9715
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9716
+ />
9717
+ }
9718
+ <button
9719
+ type="button"
9720
+ class="strct-dg__filterclear"
9721
+ [disabled]="!hasFilter(col.key)"
9722
+ (click)="clearFilter(col.key)"
9723
+ >
9724
+ {{ L().clearFilter }}
9725
+ </button>
9726
+ </div>
9727
+ }
9728
+ }
9264
9729
  </span>
9265
9730
  @if (resizable() && col.resizable !== false) {
9266
9731
  <div
@@ -9331,6 +9796,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9331
9796
  <tr
9332
9797
  [class.strct-dg__row--selected]="isSelected(row)"
9333
9798
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9799
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9800
+ [attr.aria-expanded]="
9801
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9802
+ "
9334
9803
  >
9335
9804
  @if (canDetail()) {
9336
9805
  <td
@@ -9381,14 +9850,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9381
9850
  />
9382
9851
  </td>
9383
9852
  }
9384
- @for (col of visibleColumns(); track col.key) {
9853
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9385
9854
  <td
9386
9855
  [style.text-align]="col.align ?? 'start'"
9387
9856
  [class.strct-dg__cell--sticky]="isSticky(col)"
9388
9857
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9858
+ [class.strct-dg__cell--editable]="col.editable"
9389
9859
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9860
+ (dblclick)="col.editable && startEdit(row, col)"
9390
9861
  >
9391
- @if (cellTemplate(col.key); as tpl) {
9862
+ @if (childrenKey() && colIdx === 0) {
9863
+ <span
9864
+ class="strct-dg__treepad"
9865
+ [style.width.px]="treeDepth(row) * 18"
9866
+ aria-hidden="true"
9867
+ ></span>
9868
+ @if (treeHasChildren(row)) {
9869
+ <button
9870
+ type="button"
9871
+ class="strct-dg__treebtn"
9872
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9873
+ [attr.aria-expanded]="treeExpanded(row)"
9874
+ [attr.aria-label]="L().toggleChildren"
9875
+ (click)="toggleTreeRow(row)"
9876
+ >
9877
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9878
+ </button>
9879
+ } @else {
9880
+ <span
9881
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9882
+ aria-hidden="true"
9883
+ ></span>
9884
+ }
9885
+ }
9886
+ @if (isEditingCell(row, col)) {
9887
+ <input
9888
+ class="strct-dg__editinput"
9889
+ type="text"
9890
+ [value]="row[col.key] ?? ''"
9891
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9892
+ (keydown.enter)="commitEdit(row, col, $event)"
9893
+ (keydown.escape)="cancelEdit($event)"
9894
+ (blur)="commitEdit(row, col, $event)"
9895
+ (click)="$event.stopPropagation()"
9896
+ />
9897
+ } @else if (cellTemplate(col.key); as tpl) {
9392
9898
  <ng-container
9393
9899
  [ngTemplateOutlet]="tpl"
9394
9900
  [ngTemplateOutletContext]="{
@@ -9532,8 +10038,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9532
10038
  '[class.strct-dg-host--singleline]': 'singleLine()',
9533
10039
  '[class.strct-dg-host--virtual]': 'virtual()',
9534
10040
  '[class.strct-dg-host--sticky]': 'stickyActive()',
9535
- }, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;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__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background: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__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.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;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius: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-inline-end: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-inline-end: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-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;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)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.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:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}\n"] }]
9536
- }], 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 }] }], singleLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleLine", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], columnChooser: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnChooser", required: false }] }], sync: [{ type: i0.Input, args: [{ isSignal: true, alias: "sync", required: false }] }], footerActionsDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerActionsDisabled", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], stateKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateKey", required: false }] }], columnState: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnState", required: false }] }, { type: i0.Output, args: ["columnStateChange"] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], syncChange: [{ type: i0.Output, args: ["syncChange"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], 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 }] }] } });
10041
+ }, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;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__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background: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__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.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;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius: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-inline-end: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-inline-end: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-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;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)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.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:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:210;display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"] }]
10042
+ }], 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 }] }], singleLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleLine", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], columnChooser: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnChooser", required: false }] }], sync: [{ type: i0.Input, args: [{ isSignal: true, alias: "sync", required: false }] }], footerActionsDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerActionsDisabled", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], stateKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateKey", required: false }] }], columnState: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnState", required: false }] }, { type: i0.Output, args: ["columnStateChange"] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }], childrenKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "childrenKey", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], syncChange: [{ type: i0.Output, args: ["syncChange"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], cellEdit: [{ type: i0.Output, args: ["cellEdit"] }], 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 }] }], onDocClickFilter: [{
10043
+ type: HostListener,
10044
+ args: ['document:click', ['$event']]
10045
+ }], onEscapeFilter: [{
10046
+ type: HostListener,
10047
+ args: ['document:keydown.escape']
10048
+ }] } });
9537
10049
 
9538
10050
  /** Vertical timeline container. Wraps `<strct-timeline-item>` children. */
9539
10051
  class StrctTimeline {
@@ -12643,6 +13155,2326 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12643
13155
  `, host: { class: 'strct-empty' }, styles: [".strct-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;padding:40px 24px}.strct-empty__icon{display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:14px;margin-bottom:2px}.strct-empty__icon--neutral{background:var(--bg-3);color:var(--t2)}.strct-empty__icon--warning{background:var(--warning-bg);color:var(--warning)}.strct-empty__icon--critical{background:var(--critical-bg);color:var(--critical)}.strct-empty__icon--accent{background:var(--acc-m);color:var(--acc)}.strct-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--t1)}.strct-empty__desc{margin:0;max-width:360px;font-size:13px;line-height:1.5;color:var(--t3)}.strct-empty__actions{display:flex;align-items:center;gap:8px;margin-top:6px}.strct-empty__actions:empty{display:none}\n"] }]
12644
13156
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }] } });
12645
13157
 
13158
+ /**
13159
+ * Unit formatting for infrastructure consoles — bytes, rates, durations and
13160
+ * SI magnitudes, as pure functions plus template pipes. The functions are the
13161
+ * API charts / grids can call from TS; the pipes are the template sugar:
13162
+ *
13163
+ * {{ vm.memBytes | strctBytes }} → 1.5 GiB
13164
+ * {{ nic.rxBps | strctRate }} → 2.4 Gbit/s
13165
+ * {{ task.elapsedMs | strctDuration }} → 2h 14m
13166
+ * {{ cluster.iops | strctSi : 'IOPS' }} → 12.4k IOPS
13167
+ */
13168
+ const BINARY_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
13169
+ const DECIMAL_UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB'];
13170
+ const SI_SUFFIXES = ['', 'k', 'M', 'G', 'T', 'P'];
13171
+ function trimZeros(v) {
13172
+ return v.includes('.') ? v.replace(/\.?0+$/, '') : v;
13173
+ }
13174
+ /**
13175
+ * Format a byte count. Binary (KiB, 1024) by default — the storage/memory
13176
+ * convention consoles actually mean; pass `binary: false` for decimal (kB).
13177
+ */
13178
+ function strctFormatBytes(bytes, options = {}) {
13179
+ if (!Number.isFinite(bytes))
13180
+ return '—';
13181
+ const { binary = true, digits = 1 } = options;
13182
+ const base = binary ? 1024 : 1000;
13183
+ const units = binary ? BINARY_UNITS : DECIMAL_UNITS;
13184
+ const neg = bytes < 0 ? '-' : '';
13185
+ let v = Math.abs(bytes);
13186
+ let i = 0;
13187
+ while (v >= base && i < units.length - 1) {
13188
+ v /= base;
13189
+ i++;
13190
+ }
13191
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13192
+ }
13193
+ /**
13194
+ * Format a data rate from bits per second (network convention). Pass bytes/s
13195
+ * through `strctFormatBytes(...) + '/s'` instead if you mean throughput.
13196
+ */
13197
+ function strctFormatRate(bitsPerSecond, options = {}) {
13198
+ if (!Number.isFinite(bitsPerSecond))
13199
+ return '—';
13200
+ const { digits = 1 } = options;
13201
+ const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s', 'Tbit/s'];
13202
+ const neg = bitsPerSecond < 0 ? '-' : '';
13203
+ let v = Math.abs(bitsPerSecond);
13204
+ let i = 0;
13205
+ while (v >= 1000 && i < units.length - 1) {
13206
+ v /= 1000;
13207
+ i++;
13208
+ }
13209
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13210
+ }
13211
+ /**
13212
+ * Format a duration in ms as the two most significant units: "2h 14m",
13213
+ * "3d 6h", "45s", "820ms". Sub-second stays in ms; `maxUnits` widens it.
13214
+ */
13215
+ function strctFormatDuration(ms, options = {}) {
13216
+ if (!Number.isFinite(ms))
13217
+ return '—';
13218
+ const { maxUnits = 2 } = options;
13219
+ const neg = ms < 0 ? '-' : '';
13220
+ let v = Math.abs(ms);
13221
+ if (v < 1000)
13222
+ return `${neg}${Math.round(v)}ms`;
13223
+ const units = [
13224
+ ['d', 86_400_000],
13225
+ ['h', 3_600_000],
13226
+ ['m', 60_000],
13227
+ ['s', 1000],
13228
+ ];
13229
+ const parts = [];
13230
+ for (const [label, size] of units) {
13231
+ if (parts.length >= maxUnits)
13232
+ break;
13233
+ const n = Math.floor(v / size);
13234
+ if (n > 0 || parts.length > 0) {
13235
+ if (n > 0)
13236
+ parts.push(`${n}${label}`);
13237
+ v -= n * size;
13238
+ }
13239
+ }
13240
+ return neg + (parts.length ? parts.join(' ') : '0s');
13241
+ }
13242
+ /** Format a count with SI magnitude suffixes: 12400 → "12.4k". */
13243
+ function strctFormatSi(value, options = {}) {
13244
+ if (!Number.isFinite(value))
13245
+ return '—';
13246
+ const { digits = 1, unit = '' } = options;
13247
+ const neg = value < 0 ? '-' : '';
13248
+ let v = Math.abs(value);
13249
+ let i = 0;
13250
+ while (v >= 1000 && i < SI_SUFFIXES.length - 1) {
13251
+ v /= 1000;
13252
+ i++;
13253
+ }
13254
+ const num = `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))}${SI_SUFFIXES[i]}`;
13255
+ return unit ? `${num} ${unit}` : num;
13256
+ }
13257
+ /** `{{ bytes | strctBytes }}` / `{{ bytes | strctBytes : false }}` (decimal). */
13258
+ class StrctBytesPipe {
13259
+ transform(value, binary = true, digits = 1) {
13260
+ return strctFormatBytes(value, { binary, digits });
13261
+ }
13262
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13263
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, isStandalone: true, name: "strctBytes" });
13264
+ }
13265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, decorators: [{
13266
+ type: Pipe,
13267
+ args: [{ name: 'strctBytes' }]
13268
+ }] });
13269
+ /** `{{ bps | strctRate }}` — bits per second with kbit/Mbit/Gbit steps. */
13270
+ class StrctRatePipe {
13271
+ transform(value, digits = 1) {
13272
+ return strctFormatRate(value, { digits });
13273
+ }
13274
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13275
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, isStandalone: true, name: "strctRate" });
13276
+ }
13277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, decorators: [{
13278
+ type: Pipe,
13279
+ args: [{ name: 'strctRate' }]
13280
+ }] });
13281
+ /** `{{ ms | strctDuration }}` — "2h 14m"; optional max unit count. */
13282
+ class StrctDurationPipe {
13283
+ transform(value, maxUnits = 2) {
13284
+ return strctFormatDuration(value, { maxUnits });
13285
+ }
13286
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13287
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, isStandalone: true, name: "strctDuration" });
13288
+ }
13289
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, decorators: [{
13290
+ type: Pipe,
13291
+ args: [{ name: 'strctDuration' }]
13292
+ }] });
13293
+ /** `{{ n | strctSi : 'IOPS' }}` — SI magnitude with an optional unit label. */
13294
+ class StrctSiPipe {
13295
+ transform(value, unit = '', digits = 1) {
13296
+ return strctFormatSi(value, { unit, digits });
13297
+ }
13298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13299
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, isStandalone: true, name: "strctSi" });
13300
+ }
13301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, decorators: [{
13302
+ type: Pipe,
13303
+ args: [{ name: 'strctSi' }]
13304
+ }] });
13305
+
13306
+ /** The Grafana-conventional quick ranges consoles expect. */
13307
+ const STRCT_TIME_RANGE_PRESETS = [
13308
+ { id: '15m', label: 'Last 15 minutes', ms: 15 * 60_000 },
13309
+ { id: '1h', label: 'Last 1 hour', ms: 3_600_000 },
13310
+ { id: '6h', label: 'Last 6 hours', ms: 6 * 3_600_000 },
13311
+ { id: '24h', label: 'Last 24 hours', ms: 24 * 3_600_000 },
13312
+ { id: '7d', label: 'Last 7 days', ms: 7 * 86_400_000 },
13313
+ { id: '30d', label: 'Last 30 days', ms: 30 * 86_400_000 },
13314
+ ];
13315
+ /**
13316
+ * Monitoring time-range picker — the "Last 1 hour ▾" control charts hang off:
13317
+ * quick relative ranges plus an absolute from/to editor, in one popover.
13318
+ *
13319
+ * <strct-time-range [(range)]="range" />
13320
+ *
13321
+ * Picking a preset resolves it against "now" immediately and stamps
13322
+ * `presetId`, so a consumer can re-resolve the same preset on its own refresh
13323
+ * tick. The popover reuses `strct-dropdown`'s dialog semantics: inner clicks
13324
+ * keep it open, outside click / Escape dismiss.
13325
+ */
13326
+ class StrctTimeRangePicker {
13327
+ /** The selected window, two-way. Null until the user (or you) picks one. */
13328
+ range = model(null, ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
13329
+ /** Quick ranges shown in the left column. */
13330
+ presets = input(STRCT_TIME_RANGE_PRESETS, ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
13331
+ /** Popover alignment, as in strct-dropdown. */
13332
+ align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
13333
+ /** Localizable strings. */
13334
+ dialogLabel = input('Time range', ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
13335
+ quickLabel = input('Quick ranges', ...(ngDevMode ? [{ debugName: "quickLabel" }] : /* istanbul ignore next */ []));
13336
+ absoluteLabel = input('Absolute range', ...(ngDevMode ? [{ debugName: "absoluteLabel" }] : /* istanbul ignore next */ []));
13337
+ fromLabel = input('From', ...(ngDevMode ? [{ debugName: "fromLabel" }] : /* istanbul ignore next */ []));
13338
+ toLabel = input('To', ...(ngDevMode ? [{ debugName: "toLabel" }] : /* istanbul ignore next */ []));
13339
+ applyLabel = input('Apply', ...(ngDevMode ? [{ debugName: "applyLabel" }] : /* istanbul ignore next */ []));
13340
+ invalidLabel = input('"From" must be before "To".', ...(ngDevMode ? [{ debugName: "invalidLabel" }] : /* istanbul ignore next */ []));
13341
+ placeholderLabel = input('Select time range', ...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
13342
+ /** Fires on every commit (preset pick or absolute apply). */
13343
+ applied = output();
13344
+ dd = viewChild.required(StrctDropdown);
13345
+ // Absolute-editor draft (datetime-local strings), seeded from the range.
13346
+ draftFrom = signal('', ...(ngDevMode ? [{ debugName: "draftFrom" }] : /* istanbul ignore next */ []));
13347
+ draftTo = signal('', ...(ngDevMode ? [{ debugName: "draftTo" }] : /* istanbul ignore next */ []));
13348
+ draftError = computed(() => {
13349
+ const f = this.draftFrom();
13350
+ const t = this.draftTo();
13351
+ if (!f || !t)
13352
+ return false;
13353
+ return new Date(f).getTime() >= new Date(t).getTime();
13354
+ }, ...(ngDevMode ? [{ debugName: "draftError" }] : /* istanbul ignore next */ []));
13355
+ triggerLabel = computed(() => {
13356
+ const r = this.range();
13357
+ if (!r)
13358
+ return this.placeholderLabel();
13359
+ const preset = this.presets().find((p) => p.id === r.presetId);
13360
+ if (preset)
13361
+ return preset.label;
13362
+ return `${this.formatStamp(r.from)} → ${this.formatStamp(r.to)}`;
13363
+ }, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
13364
+ pickPreset(p) {
13365
+ const to = new Date();
13366
+ const next = { from: new Date(to.getTime() - p.ms), to, presetId: p.id };
13367
+ this.range.set(next);
13368
+ this.applied.emit(next);
13369
+ this.seedDraft(next);
13370
+ }
13371
+ applyAbsolute() {
13372
+ const f = this.draftFrom();
13373
+ const t = this.draftTo();
13374
+ if (!f || !t || this.draftError())
13375
+ return false;
13376
+ const next = { from: new Date(f), to: new Date(t) };
13377
+ this.range.set(next);
13378
+ this.applied.emit(next);
13379
+ return true;
13380
+ }
13381
+ /** Re-resolve a preset range against "now" — call from your refresh tick. */
13382
+ refresh() {
13383
+ const r = this.range();
13384
+ const preset = r && this.presets().find((p) => p.id === r.presetId);
13385
+ if (!preset)
13386
+ return;
13387
+ this.pickPreset(preset);
13388
+ }
13389
+ seedDraft(r) {
13390
+ this.draftFrom.set(this.toLocal(r.from));
13391
+ this.draftTo.set(this.toLocal(r.to));
13392
+ }
13393
+ toLocal(d) {
13394
+ const pad = (n) => String(n).padStart(2, '0');
13395
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
13396
+ }
13397
+ formatStamp(d) {
13398
+ const pad = (n) => String(n).padStart(2, '0');
13399
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
13400
+ }
13401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
13402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTimeRangePicker, isStandalone: true, selector: "strct-time-range", inputs: { range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, dialogLabel: { classPropertyName: "dialogLabel", publicName: "dialogLabel", isSignal: true, isRequired: false, transformFunction: null }, quickLabel: { classPropertyName: "quickLabel", publicName: "quickLabel", isSignal: true, isRequired: false, transformFunction: null }, absoluteLabel: { classPropertyName: "absoluteLabel", publicName: "absoluteLabel", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, applyLabel: { classPropertyName: "applyLabel", publicName: "applyLabel", isSignal: true, isRequired: false, transformFunction: null }, invalidLabel: { classPropertyName: "invalidLabel", publicName: "invalidLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholderLabel: { classPropertyName: "placeholderLabel", publicName: "placeholderLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { range: "rangeChange", applied: "applied" }, host: { classAttribute: "strct-tr" }, viewQueries: [{ propertyName: "dd", first: true, predicate: StrctDropdown, descendants: true, isSignal: true }], ngImport: i0, template: `
13403
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13404
+ <button strct-button variant="neutral" strctDropdownTrigger class="strct-tr__trigger">
13405
+ <strct-icon name="clock" [size]="13" />
13406
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13407
+ <strct-icon name="chevronDown" [size]="12" />
13408
+ </button>
13409
+
13410
+ <div class="strct-tr__panel">
13411
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13412
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13413
+ @for (p of presets(); track p.id) {
13414
+ <button
13415
+ type="button"
13416
+ class="strct-tr__preset"
13417
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13418
+ [attr.aria-pressed]="range()?.presetId === p.id"
13419
+ (click)="pickPreset(p); dd.close()"
13420
+ >
13421
+ {{ p.label }}
13422
+ </button>
13423
+ }
13424
+ </div>
13425
+
13426
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13427
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13428
+ <label class="strct-tr__field">
13429
+ <span>{{ fromLabel() }}</span>
13430
+ <input
13431
+ type="datetime-local"
13432
+ class="strct-tr__input"
13433
+ [value]="draftFrom()"
13434
+ (input)="draftFrom.set($any($event.target).value)"
13435
+ />
13436
+ </label>
13437
+ <label class="strct-tr__field">
13438
+ <span>{{ toLabel() }}</span>
13439
+ <input
13440
+ type="datetime-local"
13441
+ class="strct-tr__input"
13442
+ [value]="draftTo()"
13443
+ (input)="draftTo.set($any($event.target).value)"
13444
+ />
13445
+ </label>
13446
+ @if (draftError()) {
13447
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13448
+ }
13449
+ <button
13450
+ strct-button
13451
+ variant="primary"
13452
+ size="sm"
13453
+ class="strct-tr__apply"
13454
+ [disabled]="draftError()"
13455
+ (click)="applyAbsolute() && dd.close()"
13456
+ >
13457
+ {{ applyLabel() }}
13458
+ </button>
13459
+ </div>
13460
+ </div>
13461
+ </strct-dropdown>
13462
+ `, isInline: true, styles: [".strct-tr{display:inline-block}.strct-tr__trigger .strct-tr__label{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tr__panel{display:flex;gap:16px}.strct-tr__group{display:flex;flex-direction:column;gap:2px;min-width:150px}.strct-tr__head{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-tr__preset{text-align:start;padding:6px 9px;border:0;border-radius:var(--radius-sm);background:transparent;font-family:var(--font);font-size:12.5px;color:var(--t1);cursor:pointer}.strct-tr__preset:hover{background:var(--bg-3)}.strct-tr__preset--active{background:var(--acc18);color:var(--acc);font-weight:600}.strct-tr__preset:focus-visible,.strct-tr__input:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-tr__field{display:flex;flex-direction:column;gap:3px;font-size:11.5px;color:var(--t2);margin-bottom:7px}.strct-tr__input{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-tr__error{font-size:11.5px;color:var(--critical);margin-bottom:6px}.strct-tr__apply{align-self:flex-start}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctDropdown, selector: "strct-dropdown", inputs: ["align", "popover", "popoverLabel"] }, { kind: "directive", type: StrctDropdownTrigger, selector: "[strctDropdownTrigger]" }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13463
+ }
13464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, decorators: [{
13465
+ type: Component,
13466
+ args: [{ selector: 'strct-time-range', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctDropdown, StrctDropdownTrigger, StrctIcon], template: `
13467
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13468
+ <button strct-button variant="neutral" strctDropdownTrigger class="strct-tr__trigger">
13469
+ <strct-icon name="clock" [size]="13" />
13470
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13471
+ <strct-icon name="chevronDown" [size]="12" />
13472
+ </button>
13473
+
13474
+ <div class="strct-tr__panel">
13475
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13476
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13477
+ @for (p of presets(); track p.id) {
13478
+ <button
13479
+ type="button"
13480
+ class="strct-tr__preset"
13481
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13482
+ [attr.aria-pressed]="range()?.presetId === p.id"
13483
+ (click)="pickPreset(p); dd.close()"
13484
+ >
13485
+ {{ p.label }}
13486
+ </button>
13487
+ }
13488
+ </div>
13489
+
13490
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13491
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13492
+ <label class="strct-tr__field">
13493
+ <span>{{ fromLabel() }}</span>
13494
+ <input
13495
+ type="datetime-local"
13496
+ class="strct-tr__input"
13497
+ [value]="draftFrom()"
13498
+ (input)="draftFrom.set($any($event.target).value)"
13499
+ />
13500
+ </label>
13501
+ <label class="strct-tr__field">
13502
+ <span>{{ toLabel() }}</span>
13503
+ <input
13504
+ type="datetime-local"
13505
+ class="strct-tr__input"
13506
+ [value]="draftTo()"
13507
+ (input)="draftTo.set($any($event.target).value)"
13508
+ />
13509
+ </label>
13510
+ @if (draftError()) {
13511
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13512
+ }
13513
+ <button
13514
+ strct-button
13515
+ variant="primary"
13516
+ size="sm"
13517
+ class="strct-tr__apply"
13518
+ [disabled]="draftError()"
13519
+ (click)="applyAbsolute() && dd.close()"
13520
+ >
13521
+ {{ applyLabel() }}
13522
+ </button>
13523
+ </div>
13524
+ </div>
13525
+ </strct-dropdown>
13526
+ `, host: { class: 'strct-tr' }, styles: [".strct-tr{display:inline-block}.strct-tr__trigger .strct-tr__label{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tr__panel{display:flex;gap:16px}.strct-tr__group{display:flex;flex-direction:column;gap:2px;min-width:150px}.strct-tr__head{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-tr__preset{text-align:start;padding:6px 9px;border:0;border-radius:var(--radius-sm);background:transparent;font-family:var(--font);font-size:12.5px;color:var(--t1);cursor:pointer}.strct-tr__preset:hover{background:var(--bg-3)}.strct-tr__preset--active{background:var(--acc18);color:var(--acc);font-weight:600}.strct-tr__preset:focus-visible,.strct-tr__input:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-tr__field{display:flex;flex-direction:column;gap:3px;font-size:11.5px;color:var(--t2);margin-bottom:7px}.strct-tr__input{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-tr__error{font-size:11.5px;color:var(--critical);margin-bottom:6px}.strct-tr__apply{align-self:flex-start}\n"] }]
13527
+ }], propDecorators: { range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }, { type: i0.Output, args: ["rangeChange"] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], dialogLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dialogLabel", required: false }] }], quickLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickLabel", required: false }] }], absoluteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "absoluteLabel", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], applyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "applyLabel", required: false }] }], invalidLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidLabel", required: false }] }], placeholderLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderLabel", required: false }] }], applied: [{ type: i0.Output, args: ["applied"] }], dd: [{ type: i0.ViewChild, args: [i0.forwardRef(() => StrctDropdown), { isSignal: true }] }] } });
13528
+
13529
+ // SGR code → segment class. Colors map onto chart/status tokens, not raw hues.
13530
+ const ANSI_CLASSES = {
13531
+ 1: 'b',
13532
+ 30: 'c30',
13533
+ 31: 'c31',
13534
+ 32: 'c32',
13535
+ 33: 'c33',
13536
+ 34: 'c34',
13537
+ 35: 'c35',
13538
+ 36: 'c36',
13539
+ 37: 'c37',
13540
+ 90: 'c90',
13541
+ 91: 'c31',
13542
+ 92: 'c32',
13543
+ 93: 'c33',
13544
+ 94: 'c34',
13545
+ 95: 'c35',
13546
+ 96: 'c36',
13547
+ 97: 'c37',
13548
+ };
13549
+ const LEVEL_RE = /\b(ERROR|ERR|FATAL|WARN(?:ING)?|INFO|DEBUG|TRACE)\b/;
13550
+ /**
13551
+ * Virtualized log tail — the `kubectl logs -f` surface as a component:
13552
+ *
13553
+ * <strct-log-viewer [lines]="lines()" [(follow)]="follow" [height]="360" />
13554
+ *
13555
+ * - **Virtualized**: only the visible window plus overscan is in the DOM, so
13556
+ * 100k lines scroll flat.
13557
+ * - **Follow mode** (two-way): sticks to the tail as lines stream in;
13558
+ * scrolling up pauses it, the button (or scrolling back to the bottom)
13559
+ * resumes it — the Grafana/Loki convention.
13560
+ * - **ANSI SGR colors** (16-color + bold) parsed into safe spans — no
13561
+ * innerHTML — and severity tinting from `StrctLogLine.level` or, for plain
13562
+ * strings, auto-detected ERROR/WARN/INFO/DEBUG tokens.
13563
+ */
13564
+ class StrctLogViewer {
13565
+ /** Log lines — plain strings or `{ text, level }` objects. */
13566
+ lines = input([], ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
13567
+ /** Viewport height in px. */
13568
+ height = input(320, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
13569
+ /** Follow the tail as new lines arrive (two-way; pauses on scroll-up). */
13570
+ follow = model(true, ...(ngDevMode ? [{ debugName: "follow" }] : /* istanbul ignore next */ []));
13571
+ /** Soft-wrap long lines instead of horizontal scrolling (two-way). */
13572
+ wrapMode = model(false, ...(ngDevMode ? [{ debugName: "wrapMode" }] : /* istanbul ignore next */ []));
13573
+ /** Auto-detect ERROR/WARN/INFO/DEBUG tokens on plain-string lines. */
13574
+ autoLevel = input(true, { ...(ngDevMode ? { debugName: "autoLevel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13575
+ /** Localizable strings. */
13576
+ title = input('Logs', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13577
+ followLabel = input('Follow', ...(ngDevMode ? [{ debugName: "followLabel" }] : /* istanbul ignore next */ []));
13578
+ wrapLabel = input('Wrap', ...(ngDevMode ? [{ debugName: "wrapLabel" }] : /* istanbul ignore next */ []));
13579
+ linesLabel = input('lines', ...(ngDevMode ? [{ debugName: "linesLabel" }] : /* istanbul ignore next */ []));
13580
+ emptyLabel = input('No log lines.', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
13581
+ scroller = viewChild.required('scroller');
13582
+ scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
13583
+ suppressScrollEvent = false;
13584
+ static LINE_H = 20;
13585
+ static OVERSCAN = 10;
13586
+ normalized = computed(() => this.lines().map((l) => (typeof l === 'string' ? { text: l } : l)), ...(ngDevMode ? [{ debugName: "normalized" }] : /* istanbul ignore next */ []));
13587
+ totalHeight = computed(() => this.normalized().length * StrctLogViewer.LINE_H || StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "totalHeight" }] : /* istanbul ignore next */ []));
13588
+ windowStart = computed(() => {
13589
+ const start = Math.floor(this.scrollTop() / StrctLogViewer.LINE_H) - StrctLogViewer.OVERSCAN;
13590
+ return Math.max(0, start);
13591
+ }, ...(ngDevMode ? [{ debugName: "windowStart" }] : /* istanbul ignore next */ []));
13592
+ offsetY = computed(() => this.windowStart() * StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "offsetY" }] : /* istanbul ignore next */ []));
13593
+ window = computed(() => {
13594
+ const all = this.normalized();
13595
+ const start = this.windowStart();
13596
+ const visible = Math.ceil(this.height() / StrctLogViewer.LINE_H) + StrctLogViewer.OVERSCAN * 2;
13597
+ return all.slice(start, start + visible).map((line, i) => ({
13598
+ index: start + i,
13599
+ level: line.level ?? (this.autoLevel() ? detectLevel(line.text) : 'none'),
13600
+ segments: parseAnsi(line.text),
13601
+ }));
13602
+ }, ...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
13603
+ constructor() {
13604
+ // Stick to the tail whenever content grows while following.
13605
+ effect(() => {
13606
+ this.totalHeight();
13607
+ if (!this.follow())
13608
+ return;
13609
+ const el = this.scroller().nativeElement;
13610
+ // After the DOM height settles.
13611
+ requestAnimationFrame(() => {
13612
+ this.suppressScrollEvent = true;
13613
+ el.scrollTop = el.scrollHeight;
13614
+ this.scrollTop.set(el.scrollTop);
13615
+ });
13616
+ });
13617
+ }
13618
+ onScroll() {
13619
+ const el = this.scroller().nativeElement;
13620
+ this.scrollTop.set(el.scrollTop);
13621
+ if (this.suppressScrollEvent) {
13622
+ this.suppressScrollEvent = false;
13623
+ return;
13624
+ }
13625
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - StrctLogViewer.LINE_H;
13626
+ // Manual scroll: leaving the tail pauses follow, reaching it resumes.
13627
+ if (this.follow() && !atBottom)
13628
+ this.follow.set(false);
13629
+ else if (!this.follow() && atBottom)
13630
+ this.follow.set(true);
13631
+ }
13632
+ toggleFollow() {
13633
+ const next = !this.follow();
13634
+ this.follow.set(next);
13635
+ if (next) {
13636
+ const el = this.scroller().nativeElement;
13637
+ this.suppressScrollEvent = true;
13638
+ el.scrollTop = el.scrollHeight;
13639
+ this.scrollTop.set(el.scrollTop);
13640
+ }
13641
+ }
13642
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
13643
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctLogViewer, isStandalone: true, selector: "strct-log-viewer", inputs: { lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, follow: { classPropertyName: "follow", publicName: "follow", isSignal: true, isRequired: false, transformFunction: null }, wrapMode: { classPropertyName: "wrapMode", publicName: "wrapMode", isSignal: true, isRequired: false, transformFunction: null }, autoLevel: { classPropertyName: "autoLevel", publicName: "autoLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, followLabel: { classPropertyName: "followLabel", publicName: "followLabel", isSignal: true, isRequired: false, transformFunction: null }, wrapLabel: { classPropertyName: "wrapLabel", publicName: "wrapLabel", isSignal: true, isRequired: false, transformFunction: null }, linesLabel: { classPropertyName: "linesLabel", publicName: "linesLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { follow: "followChange", wrapMode: "wrapModeChange" }, host: { classAttribute: "strct-lv" }, viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }], ngImport: i0, template: `
13644
+ <div class="strct-lv__bar">
13645
+ <span class="strct-lv__title">{{ title() }}</span>
13646
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13647
+ <span class="strct-lv__grow"></span>
13648
+ <button
13649
+ strct-button
13650
+ size="sm"
13651
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13652
+ [attr.aria-pressed]="wrapMode()"
13653
+ (click)="wrapMode.set(!wrapMode())"
13654
+ >
13655
+ {{ wrapLabel() }}
13656
+ </button>
13657
+ <button
13658
+ strct-button
13659
+ size="sm"
13660
+ [variant]="follow() ? 'primary' : 'flat'"
13661
+ [attr.aria-pressed]="follow()"
13662
+ (click)="toggleFollow()"
13663
+ >
13664
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13665
+ {{ followLabel() }}
13666
+ </button>
13667
+ </div>
13668
+ <div
13669
+ #scroller
13670
+ class="strct-lv__scroll"
13671
+ role="log"
13672
+ tabindex="0"
13673
+ [attr.aria-label]="title()"
13674
+ [style.height.px]="height()"
13675
+ (scroll)="onScroll()"
13676
+ >
13677
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13678
+ <div
13679
+ class="strct-lv__window"
13680
+ [class.strct-lv__window--wrap]="wrapMode()"
13681
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13682
+ >
13683
+ @for (line of window(); track line.index) {
13684
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13685
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13686
+ <span class="strct-lv__text">
13687
+ @for (seg of line.segments; track $index) {
13688
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13689
+ }
13690
+ </span>
13691
+ </div>
13692
+ } @empty {
13693
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13694
+ }
13695
+ </div>
13696
+ </div>
13697
+ </div>
13698
+ `, isInline: true, styles: [".strct-lv{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-lv__bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-lv__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-lv__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-lv__grow{flex:1}.strct-lv__scroll{overflow:auto;overscroll-behavior:contain}.strct-lv__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-lv__spacer{position:relative}.strct-lv__window{position:absolute;inset-inline:0;top:0}.strct-lv__line{display:flex;gap:10px;height:var(--strct-lv-line-height, 20px);line-height:var(--strct-lv-line-height, 20px);padding-inline:10px;font-family:var(--mono);font-size:12px;color:var(--t1);white-space:pre}.strct-lv__window--wrap .strct-lv__line{height:auto;min-height:var(--strct-lv-line-height, 20px)}.strct-lv__window--wrap .strct-lv__text{white-space:pre-wrap;overflow-wrap:anywhere}.strct-lv__no{flex-shrink:0;min-width:4ch;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-lv__line--error{background:var(--critical-bg)}.strct-lv__line--error .strct-lv__text{color:var(--critical)}.strct-lv__line--warn{background:var(--warning-bg)}.strct-lv__line--warn .strct-lv__text{color:var(--warning)}.strct-lv__line--debug .strct-lv__text{color:var(--t3)}.strct-lv__empty{padding:14px 12px;font-size:12.5px;color:var(--t3)}.strct-lv__seg-b{font-weight:700}.strct-lv__seg-c31{color:var(--critical)}.strct-lv__seg-c32{color:var(--success)}.strct-lv__seg-c33{color:var(--warning)}.strct-lv__seg-c34{color:var(--chart-1, var(--acc))}.strct-lv__seg-c35{color:var(--chart-4, var(--acc))}.strct-lv__seg-c36{color:var(--chart-5, var(--acc))}.strct-lv__seg-c37{color:var(--t1)}.strct-lv__seg-c30,.strct-lv__seg-c90{color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13699
+ }
13700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, decorators: [{
13701
+ type: Component,
13702
+ args: [{ selector: 'strct-log-viewer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctIcon], template: `
13703
+ <div class="strct-lv__bar">
13704
+ <span class="strct-lv__title">{{ title() }}</span>
13705
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13706
+ <span class="strct-lv__grow"></span>
13707
+ <button
13708
+ strct-button
13709
+ size="sm"
13710
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13711
+ [attr.aria-pressed]="wrapMode()"
13712
+ (click)="wrapMode.set(!wrapMode())"
13713
+ >
13714
+ {{ wrapLabel() }}
13715
+ </button>
13716
+ <button
13717
+ strct-button
13718
+ size="sm"
13719
+ [variant]="follow() ? 'primary' : 'flat'"
13720
+ [attr.aria-pressed]="follow()"
13721
+ (click)="toggleFollow()"
13722
+ >
13723
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13724
+ {{ followLabel() }}
13725
+ </button>
13726
+ </div>
13727
+ <div
13728
+ #scroller
13729
+ class="strct-lv__scroll"
13730
+ role="log"
13731
+ tabindex="0"
13732
+ [attr.aria-label]="title()"
13733
+ [style.height.px]="height()"
13734
+ (scroll)="onScroll()"
13735
+ >
13736
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13737
+ <div
13738
+ class="strct-lv__window"
13739
+ [class.strct-lv__window--wrap]="wrapMode()"
13740
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13741
+ >
13742
+ @for (line of window(); track line.index) {
13743
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13744
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13745
+ <span class="strct-lv__text">
13746
+ @for (seg of line.segments; track $index) {
13747
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13748
+ }
13749
+ </span>
13750
+ </div>
13751
+ } @empty {
13752
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13753
+ }
13754
+ </div>
13755
+ </div>
13756
+ </div>
13757
+ `, host: { class: 'strct-lv' }, styles: [".strct-lv{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-lv__bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-lv__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-lv__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-lv__grow{flex:1}.strct-lv__scroll{overflow:auto;overscroll-behavior:contain}.strct-lv__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-lv__spacer{position:relative}.strct-lv__window{position:absolute;inset-inline:0;top:0}.strct-lv__line{display:flex;gap:10px;height:var(--strct-lv-line-height, 20px);line-height:var(--strct-lv-line-height, 20px);padding-inline:10px;font-family:var(--mono);font-size:12px;color:var(--t1);white-space:pre}.strct-lv__window--wrap .strct-lv__line{height:auto;min-height:var(--strct-lv-line-height, 20px)}.strct-lv__window--wrap .strct-lv__text{white-space:pre-wrap;overflow-wrap:anywhere}.strct-lv__no{flex-shrink:0;min-width:4ch;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-lv__line--error{background:var(--critical-bg)}.strct-lv__line--error .strct-lv__text{color:var(--critical)}.strct-lv__line--warn{background:var(--warning-bg)}.strct-lv__line--warn .strct-lv__text{color:var(--warning)}.strct-lv__line--debug .strct-lv__text{color:var(--t3)}.strct-lv__empty{padding:14px 12px;font-size:12.5px;color:var(--t3)}.strct-lv__seg-b{font-weight:700}.strct-lv__seg-c31{color:var(--critical)}.strct-lv__seg-c32{color:var(--success)}.strct-lv__seg-c33{color:var(--warning)}.strct-lv__seg-c34{color:var(--chart-1, var(--acc))}.strct-lv__seg-c35{color:var(--chart-4, var(--acc))}.strct-lv__seg-c36{color:var(--chart-5, var(--acc))}.strct-lv__seg-c37{color:var(--t1)}.strct-lv__seg-c30,.strct-lv__seg-c90{color:var(--t3)}\n"] }]
13758
+ }], ctorParameters: () => [], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], follow: [{ type: i0.Input, args: [{ isSignal: true, alias: "follow", required: false }] }, { type: i0.Output, args: ["followChange"] }], wrapMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapMode", required: false }] }, { type: i0.Output, args: ["wrapModeChange"] }], autoLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoLevel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], followLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "followLabel", required: false }] }], wrapLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapLabel", required: false }] }], linesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "linesLabel", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }] } });
13759
+ function detectLevel(text) {
13760
+ const m = LEVEL_RE.exec(text);
13761
+ if (!m)
13762
+ return 'none';
13763
+ const tok = m[1];
13764
+ if (tok.startsWith('ERR') || tok === 'FATAL')
13765
+ return 'error';
13766
+ if (tok.startsWith('WARN'))
13767
+ return 'warn';
13768
+ if (tok === 'DEBUG' || tok === 'TRACE')
13769
+ return 'debug';
13770
+ return 'info';
13771
+ }
13772
+ /** Parse ANSI SGR sequences into class-tagged segments (16-color + bold). */
13773
+ function parseAnsi(text) {
13774
+ if (!text.includes('\u001b['))
13775
+ return [{ text, cls: 'c37' }];
13776
+ const segments = [];
13777
+ const re = /\u001b\[([0-9;]*)m/g;
13778
+ let last = 0;
13779
+ let current = new Set();
13780
+ let m;
13781
+ while ((m = re.exec(text))) {
13782
+ if (m.index > last) {
13783
+ segments.push({ text: text.slice(last, m.index), cls: clsOf(current) });
13784
+ }
13785
+ for (const raw of (m[1] || '0').split(';')) {
13786
+ const code = Number(raw || '0');
13787
+ if (code === 0)
13788
+ current = new Set();
13789
+ else if (ANSI_CLASSES[code]) {
13790
+ // A new foreground color replaces the previous one; bold stacks.
13791
+ if (code !== 1)
13792
+ for (const c of [...current])
13793
+ if (c !== 'b')
13794
+ current.delete(c);
13795
+ current.add(ANSI_CLASSES[code]);
13796
+ }
13797
+ }
13798
+ last = re.lastIndex;
13799
+ }
13800
+ if (last < text.length)
13801
+ segments.push({ text: text.slice(last), cls: clsOf(current) });
13802
+ return segments.length ? segments : [{ text: '', cls: 'c37' }];
13803
+ }
13804
+ function clsOf(set) {
13805
+ return set.size ? [...set].join(' strct-lv__seg-') : 'c37';
13806
+ }
13807
+
13808
+ /**
13809
+ * Compute a line diff (LCS-based). Exported for programmatic use — change
13810
+ * counts, gating "anything changed?" logic, custom renderings.
13811
+ */
13812
+ function strctComputeDiff(before, after) {
13813
+ const a = before.split('\n');
13814
+ const b = after.split('\n');
13815
+ // Trim common prefix/suffix first — configs mostly match, keeping DP small.
13816
+ let start = 0;
13817
+ while (start < a.length && start < b.length && a[start] === b[start])
13818
+ start++;
13819
+ let endA = a.length;
13820
+ let endB = b.length;
13821
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
13822
+ endA--;
13823
+ endB--;
13824
+ }
13825
+ const midA = a.slice(start, endA);
13826
+ const midB = b.slice(start, endB);
13827
+ const rows = [];
13828
+ for (let i = 0; i < start; i++)
13829
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: i + 1, text: a[i] });
13830
+ // LCS DP on the changed middle; fall back to replace-all when it is huge.
13831
+ if (midA.length * midB.length <= 4_000_000) {
13832
+ const n = midA.length;
13833
+ const m = midB.length;
13834
+ const dp = new Uint32Array((n + 1) * (m + 1));
13835
+ for (let i = n - 1; i >= 0; i--) {
13836
+ for (let j = m - 1; j >= 0; j--) {
13837
+ dp[i * (m + 1) + j] =
13838
+ midA[i] === midB[j]
13839
+ ? dp[(i + 1) * (m + 1) + j + 1] + 1
13840
+ : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + j + 1]);
13841
+ }
13842
+ }
13843
+ let i = 0;
13844
+ let j = 0;
13845
+ while (i < n && j < m) {
13846
+ if (midA[i] === midB[j]) {
13847
+ rows.push({ type: 'ctx', oldNo: start + i + 1, newNo: start + j + 1, text: midA[i] });
13848
+ i++;
13849
+ j++;
13850
+ }
13851
+ else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + j + 1]) {
13852
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13853
+ i++;
13854
+ }
13855
+ else {
13856
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13857
+ j++;
13858
+ }
13859
+ }
13860
+ while (i < n) {
13861
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13862
+ i++;
13863
+ }
13864
+ while (j < m) {
13865
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13866
+ j++;
13867
+ }
13868
+ }
13869
+ else {
13870
+ for (let i = 0; i < midA.length; i++)
13871
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13872
+ for (let j = 0; j < midB.length; j++)
13873
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13874
+ }
13875
+ for (let i = endA, j = endB; i < a.length; i++, j++) {
13876
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: j + 1, text: a[i] });
13877
+ }
13878
+ return rows;
13879
+ }
13880
+ /**
13881
+ * Config / YAML diff viewer — change-approval screens without a hand-rolled
13882
+ * diff. Unified by default, `split` for side-by-side; long unchanged runs
13883
+ * collapse to an expandable "· N unchanged lines ·" row.
13884
+ *
13885
+ * <strct-diff [before]="saved" [after]="draft" beforeLabel="Running" afterLabel="Draft" />
13886
+ *
13887
+ * Rows are marked by symbol (+/−) as well as tint, so the diff reads without
13888
+ * color — the same CVD rule the icon badges follow.
13889
+ */
13890
+ class StrctDiff {
13891
+ /** The two texts to compare. */
13892
+ before = input.required(...(ngDevMode ? [{ debugName: "before" }] : /* istanbul ignore next */ []));
13893
+ after = input.required(...(ngDevMode ? [{ debugName: "after" }] : /* istanbul ignore next */ []));
13894
+ /** Unified (default) or side-by-side split view. */
13895
+ mode = input('unified', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
13896
+ /** Context lines kept around changes before collapsing (0 disables collapsing). */
13897
+ context = input(3, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
13898
+ /** Header bits. */
13899
+ title = input('Diff', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13900
+ language = input('', ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
13901
+ beforeLabel = input('Before', ...(ngDevMode ? [{ debugName: "beforeLabel" }] : /* istanbul ignore next */ []));
13902
+ afterLabel = input('After', ...(ngDevMode ? [{ debugName: "afterLabel" }] : /* istanbul ignore next */ []));
13903
+ copyable = input(true, { ...(ngDevMode ? { debugName: "copyable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13904
+ copyAfterLabel = input('Copy new version', ...(ngDevMode ? [{ debugName: "copyAfterLabel" }] : /* istanbul ignore next */ []));
13905
+ unchangedLabel = input('unchanged lines', ...(ngDevMode ? [{ debugName: "unchangedLabel" }] : /* istanbul ignore next */ []));
13906
+ /** Scroll the body past this height (px); null grows freely. */
13907
+ maxHeight = input(null, ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
13908
+ // Indices of collapsed runs the user has expanded.
13909
+ expanded = signal(new Set(), ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
13910
+ rows = computed(() => strctComputeDiff(this.before(), this.after()), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
13911
+ addCount = computed(() => this.rows().filter((r) => r.type === 'add').length, ...(ngDevMode ? [{ debugName: "addCount" }] : /* istanbul ignore next */ []));
13912
+ delCount = computed(() => this.rows().filter((r) => r.type === 'del').length, ...(ngDevMode ? [{ debugName: "delCount" }] : /* istanbul ignore next */ []));
13913
+ unifiedRows = computed(() => {
13914
+ const out = [];
13915
+ for (const chunk of this.chunks()) {
13916
+ if (chunk.skip)
13917
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
13918
+ else
13919
+ for (const r of chunk.rows)
13920
+ out.push({ kind: 'row', ...r });
13921
+ }
13922
+ return out;
13923
+ }, ...(ngDevMode ? [{ debugName: "unifiedRows" }] : /* istanbul ignore next */ []));
13924
+ splitRows = computed(() => {
13925
+ const out = [];
13926
+ for (const chunk of this.chunks()) {
13927
+ if (chunk.skip) {
13928
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
13929
+ continue;
13930
+ }
13931
+ // Pair del-runs with the add-run that follows them (classic split view).
13932
+ let i = 0;
13933
+ const rows = chunk.rows;
13934
+ while (i < rows.length) {
13935
+ const r = rows[i];
13936
+ if (r.type === 'ctx') {
13937
+ out.push({
13938
+ kind: 'row',
13939
+ left: { type: 'ctx', no: r.oldNo, text: r.text },
13940
+ right: { type: 'ctx', no: r.newNo, text: r.text },
13941
+ });
13942
+ i++;
13943
+ continue;
13944
+ }
13945
+ const dels = [];
13946
+ const adds = [];
13947
+ while (i < rows.length && rows[i].type === 'del')
13948
+ dels.push(rows[i++]);
13949
+ while (i < rows.length && rows[i].type === 'add')
13950
+ adds.push(rows[i++]);
13951
+ const len = Math.max(dels.length, adds.length);
13952
+ for (let k = 0; k < len; k++) {
13953
+ const d = dels[k];
13954
+ const a = adds[k];
13955
+ out.push({
13956
+ kind: 'row',
13957
+ left: d
13958
+ ? { type: 'del', no: d.oldNo, text: d.text }
13959
+ : { type: 'empty', no: null, text: '' },
13960
+ right: a
13961
+ ? { type: 'add', no: a.newNo, text: a.text }
13962
+ : { type: 'empty', no: null, text: '' },
13963
+ });
13964
+ }
13965
+ }
13966
+ }
13967
+ return out;
13968
+ }, ...(ngDevMode ? [{ debugName: "splitRows" }] : /* istanbul ignore next */ []));
13969
+ /** Group rows into visible chunks and collapsible unchanged runs. */
13970
+ chunks = computed(() => {
13971
+ const rows = this.rows();
13972
+ const ctx = this.context();
13973
+ const expanded = this.expanded();
13974
+ if (ctx <= 0)
13975
+ return [{ skip: false, start: 0, rows }];
13976
+ // Mark rows within `ctx` of any change as visible.
13977
+ const visible = new Array(rows.length).fill(false);
13978
+ rows.forEach((r, i) => {
13979
+ if (r.type !== 'ctx') {
13980
+ for (let k = Math.max(0, i - ctx); k <= Math.min(rows.length - 1, i + ctx); k++) {
13981
+ visible[k] = true;
13982
+ }
13983
+ }
13984
+ });
13985
+ const out = [];
13986
+ let i = 0;
13987
+ while (i < rows.length) {
13988
+ const vis = visible[i] || expanded.has(startOfRun(visible, i));
13989
+ const start = i;
13990
+ while (i < rows.length && (visible[i] || expanded.has(startOfRun(visible, i))) === vis)
13991
+ i++;
13992
+ const slice = rows.slice(start, i);
13993
+ // Tiny hidden runs are not worth a fold row.
13994
+ if (!vis && slice.length <= 2)
13995
+ out.push({ skip: false, start, rows: slice });
13996
+ else
13997
+ out.push({ skip: !vis, start, rows: slice });
13998
+ }
13999
+ return out;
14000
+ }, ...(ngDevMode ? [{ debugName: "chunks" }] : /* istanbul ignore next */ []));
14001
+ expand(startIndex) {
14002
+ const next = new Set(this.expanded());
14003
+ next.add(startIndex);
14004
+ this.expanded.set(next);
14005
+ }
14006
+ sign(type) {
14007
+ return type === 'add' ? '+' : type === 'del' ? '−' : '';
14008
+ }
14009
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, deps: [], target: i0.ɵɵFactoryTarget.Component });
14010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDiff, isStandalone: true, selector: "strct-diff", inputs: { before: { classPropertyName: "before", publicName: "before", isSignal: true, isRequired: true, transformFunction: null }, after: { classPropertyName: "after", publicName: "after", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, beforeLabel: { classPropertyName: "beforeLabel", publicName: "beforeLabel", isSignal: true, isRequired: false, transformFunction: null }, afterLabel: { classPropertyName: "afterLabel", publicName: "afterLabel", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, copyAfterLabel: { classPropertyName: "copyAfterLabel", publicName: "copyAfterLabel", isSignal: true, isRequired: false, transformFunction: null }, unchangedLabel: { classPropertyName: "unchangedLabel", publicName: "unchangedLabel", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-diff" }, ngImport: i0, template: `
14011
+ <div class="strct-diff__head">
14012
+ <span class="strct-diff__title">{{ title() }}</span>
14013
+ @if (language()) {
14014
+ <span class="strct-diff__lang">{{ language() }}</span>
14015
+ }
14016
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14017
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14018
+ <span class="strct-diff__grow"></span>
14019
+ @if (copyable()) {
14020
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14021
+ }
14022
+ </div>
14023
+ @if (mode() === 'split') {
14024
+ <div class="strct-diff__cols" aria-hidden="true">
14025
+ <span>{{ beforeLabel() }}</span>
14026
+ <span>{{ afterLabel() }}</span>
14027
+ </div>
14028
+ }
14029
+ <div
14030
+ class="strct-diff__scroll"
14031
+ tabindex="0"
14032
+ role="region"
14033
+ [attr.aria-label]="title()"
14034
+ [style.max-height.px]="maxHeight()"
14035
+ >
14036
+ @if (mode() === 'unified') {
14037
+ <table class="strct-diff__table">
14038
+ <tbody>
14039
+ @for (row of unifiedRows(); track $index) {
14040
+ @if (row.kind === 'skip') {
14041
+ <tr class="strct-diff__skip-row">
14042
+ <td colspan="4">
14043
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14044
+ · {{ row.count }} {{ unchangedLabel() }} ·
14045
+ </button>
14046
+ </td>
14047
+ </tr>
14048
+ } @else {
14049
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14050
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14051
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14052
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14053
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14054
+ </tr>
14055
+ }
14056
+ }
14057
+ </tbody>
14058
+ </table>
14059
+ } @else {
14060
+ <table class="strct-diff__table strct-diff__table--split">
14061
+ <tbody>
14062
+ @for (row of splitRows(); track $index) {
14063
+ @if (row.kind === 'skip') {
14064
+ <tr class="strct-diff__skip-row">
14065
+ <td colspan="6">
14066
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14067
+ · {{ row.count }} {{ unchangedLabel() }} ·
14068
+ </button>
14069
+ </td>
14070
+ </tr>
14071
+ } @else {
14072
+ <tr>
14073
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14074
+ {{ row.left.no ?? '' }}
14075
+ </td>
14076
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14077
+ {{ sign(row.left.type) }}
14078
+ </td>
14079
+ <td
14080
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14081
+ [textContent]="row.left.text"
14082
+ ></td>
14083
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14084
+ {{ row.right.no ?? '' }}
14085
+ </td>
14086
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14087
+ {{ sign(row.right.type) }}
14088
+ </td>
14089
+ <td
14090
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14091
+ [textContent]="row.right.text"
14092
+ ></td>
14093
+ </tr>
14094
+ }
14095
+ }
14096
+ </tbody>
14097
+ </table>
14098
+ }
14099
+ </div>
14100
+ `, isInline: true, styles: [".strct-diff{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-diff__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-diff__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-diff__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-diff__stat{font-family:var(--mono);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.strct-diff__stat--add{color:var(--success)}.strct-diff__stat--del{color:var(--critical)}.strct-diff__grow{flex:1}.strct-diff__cols{display:grid;grid-template-columns:1fr 1fr;padding:4px 10px;border-bottom:1px solid var(--b1);font-size:11px;font-weight:600;color:var(--t3)}.strct-diff__scroll{overflow:auto}.strct-diff__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-diff__table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;line-height:1.55}.strct-diff__no{width:1%;min-width:3.5ch;padding:0 6px;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums;vertical-align:top}.strct-diff__sign{width:1%;padding:0 2px;text-align:center;-webkit-user-select:none;user-select:none;vertical-align:top}.strct-diff__text{padding:0 10px 0 4px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--t1)}.strct-diff__row--add,.strct-diff__cell--add{background:var(--success-bg)}.strct-diff__row--add .strct-diff__sign,.strct-diff__cell--add.strct-diff__sign{color:var(--success);font-weight:700}.strct-diff__row--del,.strct-diff__cell--del{background:var(--critical-bg)}.strct-diff__row--del .strct-diff__sign,.strct-diff__cell--del.strct-diff__sign{color:var(--critical);font-weight:700}.strct-diff__table--split .strct-diff__text{width:50%}.strct-diff__skip-row td{padding:0}.strct-diff__skip{display:block;width:100%;padding:3px 10px;border:0;border-block:1px dashed var(--b2);background:var(--bg-3);color:var(--t3);font-family:var(--mono);font-size:11px;cursor:pointer}.strct-diff__skip:hover{color:var(--t1)}.strct-diff__skip:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}\n"], dependencies: [{ kind: "component", type: StrctCopy, selector: "strct-copy", inputs: ["text", "label", "ariaLabel", "copyLabel", "copiedLabel"], outputs: ["copied"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14101
+ }
14102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, decorators: [{
14103
+ type: Component,
14104
+ args: [{ selector: 'strct-diff', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctCopy], template: `
14105
+ <div class="strct-diff__head">
14106
+ <span class="strct-diff__title">{{ title() }}</span>
14107
+ @if (language()) {
14108
+ <span class="strct-diff__lang">{{ language() }}</span>
14109
+ }
14110
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14111
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14112
+ <span class="strct-diff__grow"></span>
14113
+ @if (copyable()) {
14114
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14115
+ }
14116
+ </div>
14117
+ @if (mode() === 'split') {
14118
+ <div class="strct-diff__cols" aria-hidden="true">
14119
+ <span>{{ beforeLabel() }}</span>
14120
+ <span>{{ afterLabel() }}</span>
14121
+ </div>
14122
+ }
14123
+ <div
14124
+ class="strct-diff__scroll"
14125
+ tabindex="0"
14126
+ role="region"
14127
+ [attr.aria-label]="title()"
14128
+ [style.max-height.px]="maxHeight()"
14129
+ >
14130
+ @if (mode() === 'unified') {
14131
+ <table class="strct-diff__table">
14132
+ <tbody>
14133
+ @for (row of unifiedRows(); track $index) {
14134
+ @if (row.kind === 'skip') {
14135
+ <tr class="strct-diff__skip-row">
14136
+ <td colspan="4">
14137
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14138
+ · {{ row.count }} {{ unchangedLabel() }} ·
14139
+ </button>
14140
+ </td>
14141
+ </tr>
14142
+ } @else {
14143
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14144
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14145
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14146
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14147
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14148
+ </tr>
14149
+ }
14150
+ }
14151
+ </tbody>
14152
+ </table>
14153
+ } @else {
14154
+ <table class="strct-diff__table strct-diff__table--split">
14155
+ <tbody>
14156
+ @for (row of splitRows(); track $index) {
14157
+ @if (row.kind === 'skip') {
14158
+ <tr class="strct-diff__skip-row">
14159
+ <td colspan="6">
14160
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14161
+ · {{ row.count }} {{ unchangedLabel() }} ·
14162
+ </button>
14163
+ </td>
14164
+ </tr>
14165
+ } @else {
14166
+ <tr>
14167
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14168
+ {{ row.left.no ?? '' }}
14169
+ </td>
14170
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14171
+ {{ sign(row.left.type) }}
14172
+ </td>
14173
+ <td
14174
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14175
+ [textContent]="row.left.text"
14176
+ ></td>
14177
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14178
+ {{ row.right.no ?? '' }}
14179
+ </td>
14180
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14181
+ {{ sign(row.right.type) }}
14182
+ </td>
14183
+ <td
14184
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14185
+ [textContent]="row.right.text"
14186
+ ></td>
14187
+ </tr>
14188
+ }
14189
+ }
14190
+ </tbody>
14191
+ </table>
14192
+ }
14193
+ </div>
14194
+ `, host: { class: 'strct-diff' }, styles: [".strct-diff{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-diff__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-diff__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-diff__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-diff__stat{font-family:var(--mono);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.strct-diff__stat--add{color:var(--success)}.strct-diff__stat--del{color:var(--critical)}.strct-diff__grow{flex:1}.strct-diff__cols{display:grid;grid-template-columns:1fr 1fr;padding:4px 10px;border-bottom:1px solid var(--b1);font-size:11px;font-weight:600;color:var(--t3)}.strct-diff__scroll{overflow:auto}.strct-diff__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-diff__table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;line-height:1.55}.strct-diff__no{width:1%;min-width:3.5ch;padding:0 6px;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums;vertical-align:top}.strct-diff__sign{width:1%;padding:0 2px;text-align:center;-webkit-user-select:none;user-select:none;vertical-align:top}.strct-diff__text{padding:0 10px 0 4px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--t1)}.strct-diff__row--add,.strct-diff__cell--add{background:var(--success-bg)}.strct-diff__row--add .strct-diff__sign,.strct-diff__cell--add.strct-diff__sign{color:var(--success);font-weight:700}.strct-diff__row--del,.strct-diff__cell--del{background:var(--critical-bg)}.strct-diff__row--del .strct-diff__sign,.strct-diff__cell--del.strct-diff__sign{color:var(--critical);font-weight:700}.strct-diff__table--split .strct-diff__text{width:50%}.strct-diff__skip-row td{padding:0}.strct-diff__skip{display:block;width:100%;padding:3px 10px;border:0;border-block:1px dashed var(--b2);background:var(--bg-3);color:var(--t3);font-family:var(--mono);font-size:11px;cursor:pointer}.strct-diff__skip:hover{color:var(--t1)}.strct-diff__skip:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}\n"] }]
14195
+ }], propDecorators: { before: [{ type: i0.Input, args: [{ isSignal: true, alias: "before", required: true }] }], after: [{ type: i0.Input, args: [{ isSignal: true, alias: "after", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], beforeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "beforeLabel", required: false }] }], afterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "afterLabel", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], copyAfterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyAfterLabel", required: false }] }], unchangedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unchangedLabel", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }] } });
14196
+ function startOfRun(visible, i) {
14197
+ let s = i;
14198
+ while (s > 0 && visible[s - 1] === visible[i])
14199
+ s--;
14200
+ return s;
14201
+ }
14202
+
14203
+ /**
14204
+ * Two resizable panes with a draggable gutter — master/detail layouts where
14205
+ * the split is the user's to own:
14206
+ *
14207
+ * <strct-splitter [(split)]="pct" [min]="20" [max]="80">
14208
+ * <div strctPaneStart>…list…</div>
14209
+ * <div strctPaneEnd>…detail…</div>
14210
+ * </strct-splitter>
14211
+ *
14212
+ * `split` is the start pane's share in percent (two-way, persistable).
14213
+ * The gutter is a keyboard separator: arrows nudge, Home/End jump to min/max.
14214
+ */
14215
+ class StrctSplitter {
14216
+ host = inject(ElementRef);
14217
+ /** Start pane share in percent (two-way). */
14218
+ split = model(50, ...(ngDevMode ? [{ debugName: "split" }] : /* istanbul ignore next */ []));
14219
+ /** Clamp bounds for the split (percent). */
14220
+ min = input(15, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
14221
+ max = input(85, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
14222
+ /** Stack panes vertically (gutter drags up/down). */
14223
+ vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : /* istanbul ignore next */ []));
14224
+ /** Accessible name of the separator (localizable). */
14225
+ gutterLabel = input('Resize panes', ...(ngDevMode ? [{ debugName: "gutterLabel" }] : /* istanbul ignore next */ []));
14226
+ /** Keyboard nudge step in percent. */
14227
+ step = input(3, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
14228
+ clamped = computed(() => Math.min(this.max(), Math.max(this.min(), this.split())), ...(ngDevMode ? [{ debugName: "clamped" }] : /* istanbul ignore next */ []));
14229
+ dragging = false;
14230
+ moveHandler = (e) => this.onDragMove(e);
14231
+ upHandler = () => this.onDragEnd();
14232
+ onDragStart(event) {
14233
+ event.preventDefault();
14234
+ this.dragging = true;
14235
+ document.addEventListener('mousemove', this.moveHandler);
14236
+ document.addEventListener('mouseup', this.upHandler);
14237
+ }
14238
+ onDragMove(event) {
14239
+ const rect = this.host.nativeElement.getBoundingClientRect();
14240
+ const ratio = this.vertical()
14241
+ ? (event.clientY - rect.top) / rect.height
14242
+ : (event.clientX - rect.left) / rect.width;
14243
+ this.split.set(Math.min(this.max(), Math.max(this.min(), Math.round(ratio * 100))));
14244
+ }
14245
+ onDragEnd() {
14246
+ this.dragging = false;
14247
+ document.removeEventListener('mousemove', this.moveHandler);
14248
+ document.removeEventListener('mouseup', this.upHandler);
14249
+ }
14250
+ onKeydown(event) {
14251
+ const dec = this.vertical() ? 'ArrowUp' : 'ArrowLeft';
14252
+ const inc = this.vertical() ? 'ArrowDown' : 'ArrowRight';
14253
+ let next = null;
14254
+ if (event.key === dec)
14255
+ next = this.clamped() - this.step();
14256
+ else if (event.key === inc)
14257
+ next = this.clamped() + this.step();
14258
+ else if (event.key === 'Home')
14259
+ next = this.min();
14260
+ else if (event.key === 'End')
14261
+ next = this.max();
14262
+ if (next == null)
14263
+ return;
14264
+ event.preventDefault();
14265
+ this.split.set(Math.min(this.max(), Math.max(this.min(), next)));
14266
+ }
14267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, deps: [], target: i0.ɵɵFactoryTarget.Component });
14268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: StrctSplitter, isStandalone: true, selector: "strct-splitter", inputs: { split: { classPropertyName: "split", publicName: "split", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null }, gutterLabel: { classPropertyName: "gutterLabel", publicName: "gutterLabel", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { split: "splitChange" }, host: { properties: { "class.strct-split--vertical": "vertical()", "class.strct-split--dragging": "dragging" }, classAttribute: "strct-split" }, ngImport: i0, template: `
14269
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14270
+ <ng-content select="[strctPaneStart]" />
14271
+ </div>
14272
+ <div
14273
+ class="strct-split__gutter"
14274
+ role="separator"
14275
+ tabindex="0"
14276
+ [attr.aria-label]="gutterLabel()"
14277
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14278
+ [attr.aria-valuenow]="clamped()"
14279
+ [attr.aria-valuemin]="min()"
14280
+ [attr.aria-valuemax]="max()"
14281
+ (mousedown)="onDragStart($event)"
14282
+ (keydown)="onKeydown($event)"
14283
+ >
14284
+ <span class="strct-split__grip" aria-hidden="true"></span>
14285
+ </div>
14286
+ <div class="strct-split__pane strct-split__pane--end">
14287
+ <ng-content select="[strctPaneEnd]" />
14288
+ </div>
14289
+ `, isInline: true, styles: [".strct-split{display:flex;width:100%;min-height:0}.strct-split--vertical{flex-direction:column}.strct-split__pane{flex-grow:0;flex-shrink:0;overflow:auto;min-width:0;min-height:0}.strct-split__pane--end{flex:1 1 0}.strct-split__gutter{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:7px;cursor:col-resize;background:transparent;touch-action:none}.strct-split--vertical>.strct-split__gutter{width:auto;height:7px;cursor:row-resize}.strct-split__gutter:hover,.strct-split--dragging>.strct-split__gutter{background:var(--acc18)}.strct-split__gutter:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-split__grip{width:1.5px;height:26px;border-radius:1px;background:var(--b2)}.strct-split--vertical .strct-split__grip{width:26px;height:1.5px}.strct-split--dragging{-webkit-user-select:none;user-select:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14290
+ }
14291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, decorators: [{
14292
+ type: Component,
14293
+ args: [{ selector: 'strct-splitter', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14294
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14295
+ <ng-content select="[strctPaneStart]" />
14296
+ </div>
14297
+ <div
14298
+ class="strct-split__gutter"
14299
+ role="separator"
14300
+ tabindex="0"
14301
+ [attr.aria-label]="gutterLabel()"
14302
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14303
+ [attr.aria-valuenow]="clamped()"
14304
+ [attr.aria-valuemin]="min()"
14305
+ [attr.aria-valuemax]="max()"
14306
+ (mousedown)="onDragStart($event)"
14307
+ (keydown)="onKeydown($event)"
14308
+ >
14309
+ <span class="strct-split__grip" aria-hidden="true"></span>
14310
+ </div>
14311
+ <div class="strct-split__pane strct-split__pane--end">
14312
+ <ng-content select="[strctPaneEnd]" />
14313
+ </div>
14314
+ `, host: {
14315
+ class: 'strct-split',
14316
+ '[class.strct-split--vertical]': 'vertical()',
14317
+ '[class.strct-split--dragging]': 'dragging',
14318
+ }, styles: [".strct-split{display:flex;width:100%;min-height:0}.strct-split--vertical{flex-direction:column}.strct-split__pane{flex-grow:0;flex-shrink:0;overflow:auto;min-width:0;min-height:0}.strct-split__pane--end{flex:1 1 0}.strct-split__gutter{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:7px;cursor:col-resize;background:transparent;touch-action:none}.strct-split--vertical>.strct-split__gutter{width:auto;height:7px;cursor:row-resize}.strct-split__gutter:hover,.strct-split--dragging>.strct-split__gutter{background:var(--acc18)}.strct-split__gutter:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-split__grip{width:1.5px;height:26px;border-radius:1px;background:var(--b2)}.strct-split--vertical .strct-split__grip{width:26px;height:1.5px}.strct-split--dragging{-webkit-user-select:none;user-select:none}\n"] }]
14319
+ }], propDecorators: { split: [{ type: i0.Input, args: [{ isSignal: true, alias: "split", required: false }] }, { type: i0.Output, args: ["splitChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], gutterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutterLabel", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }] } });
14320
+
14321
+ /**
14322
+ * Dual-list transfer (picklist) — "assign hosts to the cluster": all items on
14323
+ * the left, the chosen set on the right, checkbox multi-select and move
14324
+ * buttons in between, with a searchbox per side.
14325
+ *
14326
+ * <strct-transfer [items]="hosts" [(assigned)]="clusterHosts" />
14327
+ *
14328
+ * `assigned` is the two-way id set (string[]); `items` stays the single
14329
+ * source-of-truth list.
14330
+ */
14331
+ class StrctTransfer {
14332
+ /** The full item universe. */
14333
+ items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14334
+ /** Assigned item ids (two-way). */
14335
+ assigned = model([], ...(ngDevMode ? [{ debugName: "assigned" }] : /* istanbul ignore next */ []));
14336
+ /** Localizable strings. */
14337
+ sourceLabel = input('Available', ...(ngDevMode ? [{ debugName: "sourceLabel" }] : /* istanbul ignore next */ []));
14338
+ targetLabel = input('Assigned', ...(ngDevMode ? [{ debugName: "targetLabel" }] : /* istanbul ignore next */ []));
14339
+ searchPlaceholder = input('Search', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
14340
+ emptyLabel = input('No items', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
14341
+ assignLabel = input('Assign selected', ...(ngDevMode ? [{ debugName: "assignLabel" }] : /* istanbul ignore next */ []));
14342
+ unassignLabel = input('Unassign selected', ...(ngDevMode ? [{ debugName: "unassignLabel" }] : /* istanbul ignore next */ []));
14343
+ /** A move happened (either direction) with the moved ids. */
14344
+ moved = output();
14345
+ sourceQuery = signal('', ...(ngDevMode ? [{ debugName: "sourceQuery" }] : /* istanbul ignore next */ []));
14346
+ targetQuery = signal('', ...(ngDevMode ? [{ debugName: "targetQuery" }] : /* istanbul ignore next */ []));
14347
+ sourceChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "sourceChecked" }] : /* istanbul ignore next */ []));
14348
+ targetChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "targetChecked" }] : /* istanbul ignore next */ []));
14349
+ assignedSet = computed(() => new Set(this.assigned()), ...(ngDevMode ? [{ debugName: "assignedSet" }] : /* istanbul ignore next */ []));
14350
+ sourceItems = computed(() => this.items().filter((i) => !this.assignedSet().has(i.id) && this.matches(i, this.sourceQuery())), ...(ngDevMode ? [{ debugName: "sourceItems" }] : /* istanbul ignore next */ []));
14351
+ targetItems = computed(() => this.items().filter((i) => this.assignedSet().has(i.id) && this.matches(i, this.targetQuery())), ...(ngDevMode ? [{ debugName: "targetItems" }] : /* istanbul ignore next */ []));
14352
+ matches(item, q) {
14353
+ return !q.trim() || item.label.toLowerCase().includes(q.trim().toLowerCase());
14354
+ }
14355
+ toggle(which, id) {
14356
+ const next = new Set(which());
14357
+ if (next.has(id))
14358
+ next.delete(id);
14359
+ else
14360
+ next.add(id);
14361
+ which.set(next);
14362
+ }
14363
+ assign() {
14364
+ const ids = [...this.sourceChecked()];
14365
+ if (!ids.length)
14366
+ return;
14367
+ this.assigned.set([...this.assigned(), ...ids]);
14368
+ this.sourceChecked.set(new Set());
14369
+ this.moved.emit({ direction: 'assign', ids });
14370
+ }
14371
+ unassign() {
14372
+ const ids = new Set(this.targetChecked());
14373
+ if (!ids.size)
14374
+ return;
14375
+ this.assigned.set(this.assigned().filter((id) => !ids.has(id)));
14376
+ this.targetChecked.set(new Set());
14377
+ this.moved.emit({ direction: 'unassign', ids: [...ids] });
14378
+ }
14379
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, deps: [], target: i0.ɵɵFactoryTarget.Component });
14380
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTransfer, isStandalone: true, selector: "strct-transfer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, assigned: { classPropertyName: "assigned", publicName: "assigned", isSignal: true, isRequired: false, transformFunction: null }, sourceLabel: { classPropertyName: "sourceLabel", publicName: "sourceLabel", isSignal: true, isRequired: false, transformFunction: null }, targetLabel: { classPropertyName: "targetLabel", publicName: "targetLabel", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, assignLabel: { classPropertyName: "assignLabel", publicName: "assignLabel", isSignal: true, isRequired: false, transformFunction: null }, unassignLabel: { classPropertyName: "unassignLabel", publicName: "unassignLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { assigned: "assignedChange", moved: "moved" }, host: { classAttribute: "strct-tf" }, ngImport: i0, template: `
14381
+ <div class="strct-tf__col">
14382
+ <div class="strct-tf__head">
14383
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14384
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14385
+ </div>
14386
+ <strct-searchbox
14387
+ class="strct-tf__search"
14388
+ [placeholder]="searchPlaceholder()"
14389
+ [value]="sourceQuery()"
14390
+ (valueChange)="sourceQuery.set($event)"
14391
+ />
14392
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14393
+ @for (item of sourceItems(); track item.id) {
14394
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14395
+ <strct-checkbox
14396
+ [checked]="sourceChecked().has(item.id)"
14397
+ [disabled]="item.disabled ?? false"
14398
+ (checkedChange)="toggle(sourceChecked, item.id)"
14399
+ >
14400
+ @if (item.icon) {
14401
+ <strct-icon [name]="item.icon" [size]="14" />
14402
+ }
14403
+ <span class="strct-tf__label">{{ item.label }}</span>
14404
+ </strct-checkbox>
14405
+ </li>
14406
+ } @empty {
14407
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14408
+ }
14409
+ </ul>
14410
+ </div>
14411
+
14412
+ <div class="strct-tf__mid">
14413
+ <button
14414
+ strct-button
14415
+ size="sm"
14416
+ variant="neutral"
14417
+ [disabled]="!sourceChecked().size"
14418
+ [attr.aria-label]="assignLabel()"
14419
+ (click)="assign()"
14420
+ >
14421
+ <strct-icon name="chevronRight" [size]="14" />
14422
+ </button>
14423
+ <button
14424
+ strct-button
14425
+ size="sm"
14426
+ variant="neutral"
14427
+ [disabled]="!targetChecked().size"
14428
+ [attr.aria-label]="unassignLabel()"
14429
+ (click)="unassign()"
14430
+ >
14431
+ <strct-icon name="chevronLeft" [size]="14" />
14432
+ </button>
14433
+ </div>
14434
+
14435
+ <div class="strct-tf__col">
14436
+ <div class="strct-tf__head">
14437
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14438
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14439
+ </div>
14440
+ <strct-searchbox
14441
+ class="strct-tf__search"
14442
+ [placeholder]="searchPlaceholder()"
14443
+ [value]="targetQuery()"
14444
+ (valueChange)="targetQuery.set($event)"
14445
+ />
14446
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14447
+ @for (item of targetItems(); track item.id) {
14448
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14449
+ <strct-checkbox
14450
+ [checked]="targetChecked().has(item.id)"
14451
+ [disabled]="item.disabled ?? false"
14452
+ (checkedChange)="toggle(targetChecked, item.id)"
14453
+ >
14454
+ @if (item.icon) {
14455
+ <strct-icon [name]="item.icon" [size]="14" />
14456
+ }
14457
+ <span class="strct-tf__label">{{ item.label }}</span>
14458
+ </strct-checkbox>
14459
+ </li>
14460
+ } @empty {
14461
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14462
+ }
14463
+ </ul>
14464
+ </div>
14465
+ `, isInline: true, styles: [".strct-tf{display:flex;align-items:stretch;gap:10px;width:100%}.strct-tf__col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:10px;background:var(--bg-2);border:1px solid var(--b2);border-radius:9px}.strct-tf__head{display:flex;align-items:baseline;gap:8px}.strct-tf__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-tf__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-tf__search{width:100%}.strct-tf__list{list-style:none;margin:0;padding:0;min-height:120px;max-height:240px;overflow:auto;display:flex;flex-direction:column;gap:1px}.strct-tf__item{display:flex;align-items:center;padding:2px 4px;border-radius:5px;color:var(--t1);font-size:12.5px}.strct-tf__item strct-icon{margin-inline-end:6px}.strct-tf__item:hover{background:var(--bg-3)}.strct-tf__item--disabled{color:var(--t4);cursor:default}.strct-tf__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tf__empty{padding:12px 8px;font-size:12px;color:var(--t3)}.strct-tf__mid{display:flex;flex-direction:column;justify-content:center;gap:8px}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "isDisabled", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange", "isDisabledChange"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctSearchbox, selector: "strct-searchbox", inputs: ["placeholder", "hint", "trigger", "clearable", "ariaLabel", "clearLabel", "value"], outputs: ["valueChange", "search", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14466
+ }
14467
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, decorators: [{
14468
+ type: Component,
14469
+ args: [{ selector: 'strct-transfer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctCheckbox, StrctIcon, StrctSearchbox], template: `
14470
+ <div class="strct-tf__col">
14471
+ <div class="strct-tf__head">
14472
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14473
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14474
+ </div>
14475
+ <strct-searchbox
14476
+ class="strct-tf__search"
14477
+ [placeholder]="searchPlaceholder()"
14478
+ [value]="sourceQuery()"
14479
+ (valueChange)="sourceQuery.set($event)"
14480
+ />
14481
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14482
+ @for (item of sourceItems(); track item.id) {
14483
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14484
+ <strct-checkbox
14485
+ [checked]="sourceChecked().has(item.id)"
14486
+ [disabled]="item.disabled ?? false"
14487
+ (checkedChange)="toggle(sourceChecked, item.id)"
14488
+ >
14489
+ @if (item.icon) {
14490
+ <strct-icon [name]="item.icon" [size]="14" />
14491
+ }
14492
+ <span class="strct-tf__label">{{ item.label }}</span>
14493
+ </strct-checkbox>
14494
+ </li>
14495
+ } @empty {
14496
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14497
+ }
14498
+ </ul>
14499
+ </div>
14500
+
14501
+ <div class="strct-tf__mid">
14502
+ <button
14503
+ strct-button
14504
+ size="sm"
14505
+ variant="neutral"
14506
+ [disabled]="!sourceChecked().size"
14507
+ [attr.aria-label]="assignLabel()"
14508
+ (click)="assign()"
14509
+ >
14510
+ <strct-icon name="chevronRight" [size]="14" />
14511
+ </button>
14512
+ <button
14513
+ strct-button
14514
+ size="sm"
14515
+ variant="neutral"
14516
+ [disabled]="!targetChecked().size"
14517
+ [attr.aria-label]="unassignLabel()"
14518
+ (click)="unassign()"
14519
+ >
14520
+ <strct-icon name="chevronLeft" [size]="14" />
14521
+ </button>
14522
+ </div>
14523
+
14524
+ <div class="strct-tf__col">
14525
+ <div class="strct-tf__head">
14526
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14527
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14528
+ </div>
14529
+ <strct-searchbox
14530
+ class="strct-tf__search"
14531
+ [placeholder]="searchPlaceholder()"
14532
+ [value]="targetQuery()"
14533
+ (valueChange)="targetQuery.set($event)"
14534
+ />
14535
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14536
+ @for (item of targetItems(); track item.id) {
14537
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14538
+ <strct-checkbox
14539
+ [checked]="targetChecked().has(item.id)"
14540
+ [disabled]="item.disabled ?? false"
14541
+ (checkedChange)="toggle(targetChecked, item.id)"
14542
+ >
14543
+ @if (item.icon) {
14544
+ <strct-icon [name]="item.icon" [size]="14" />
14545
+ }
14546
+ <span class="strct-tf__label">{{ item.label }}</span>
14547
+ </strct-checkbox>
14548
+ </li>
14549
+ } @empty {
14550
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14551
+ }
14552
+ </ul>
14553
+ </div>
14554
+ `, host: { class: 'strct-tf' }, styles: [".strct-tf{display:flex;align-items:stretch;gap:10px;width:100%}.strct-tf__col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:10px;background:var(--bg-2);border:1px solid var(--b2);border-radius:9px}.strct-tf__head{display:flex;align-items:baseline;gap:8px}.strct-tf__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-tf__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-tf__search{width:100%}.strct-tf__list{list-style:none;margin:0;padding:0;min-height:120px;max-height:240px;overflow:auto;display:flex;flex-direction:column;gap:1px}.strct-tf__item{display:flex;align-items:center;padding:2px 4px;border-radius:5px;color:var(--t1);font-size:12.5px}.strct-tf__item strct-icon{margin-inline-end:6px}.strct-tf__item:hover{background:var(--bg-3)}.strct-tf__item--disabled{color:var(--t4);cursor:default}.strct-tf__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tf__empty{padding:12px 8px;font-size:12px;color:var(--t3)}.strct-tf__mid{display:flex;flex-direction:column;justify-content:center;gap:8px}\n"] }]
14555
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], assigned: [{ type: i0.Input, args: [{ isSignal: true, alias: "assigned", required: false }] }, { type: i0.Output, args: ["assignedChange"] }], sourceLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceLabel", required: false }] }], targetLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "targetLabel", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], assignLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "assignLabel", required: false }] }], unassignLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unassignLabel", required: false }] }], moved: [{ type: i0.Output, args: ["moved"] }] } });
14556
+
14557
+ /**
14558
+ * Split button — a primary action plus a chevron opening its variants
14559
+ * (Fluent's pattern): "Deploy ▾ → Deploy with snapshot / Deploy paused".
14560
+ *
14561
+ * <strct-split-button label="Deploy" [items]="variants"
14562
+ * (action)="deploy()" (picked)="run($event)" />
14563
+ *
14564
+ * The main segment fires `(action)`; menu entries fire `(picked)`.
14565
+ */
14566
+ class StrctSplitButton {
14567
+ /** Main action label. */
14568
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
14569
+ /** Menu entries (StrctMenuItem: id, label, icon?, critical?, disabled?). */
14570
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14571
+ /** Optional leading icon of the main segment. */
14572
+ icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
14573
+ /** Filled (solid accent) look. */
14574
+ solid = input(false, { ...(ngDevMode ? { debugName: "solid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14575
+ /** Disable both segments. */
14576
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14577
+ /** Accessible name of the chevron (localizable). */
14578
+ menuLabel = input('More actions', ...(ngDevMode ? [{ debugName: "menuLabel" }] : /* istanbul ignore next */ []));
14579
+ /** The main segment was clicked. */
14580
+ action = output();
14581
+ /** A menu entry was chosen. */
14582
+ picked = output();
14583
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
14584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSplitButton, isStandalone: true, selector: "strct-split-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, menuLabel: { classPropertyName: "menuLabel", publicName: "menuLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action", picked: "picked" }, ngImport: i0, template: `
14585
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14586
+ <button
14587
+ type="button"
14588
+ class="strct-sbt__main"
14589
+ [disabled]="disabled() || null"
14590
+ (click)="action.emit()"
14591
+ >
14592
+ @if (icon()) {
14593
+ <strct-icon [name]="icon()" [size]="13" />
14594
+ }
14595
+ {{ label() }}
14596
+ </button>
14597
+ <strct-dropdown align="end">
14598
+ <button
14599
+ type="button"
14600
+ class="strct-sbt__chev"
14601
+ strctDropdownTrigger
14602
+ [disabled]="disabled() || null"
14603
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14604
+ >
14605
+ <strct-icon name="chevronDown" [size]="12" />
14606
+ </button>
14607
+ @for (item of items(); track $index) {
14608
+ @if (item.divider) {
14609
+ <strct-dropdown-divider />
14610
+ } @else {
14611
+ <strct-dropdown-item
14612
+ [critical]="item.critical ?? false"
14613
+ [disabled]="item.disabled ?? false"
14614
+ (click)="!item.disabled && picked.emit(item)"
14615
+ >
14616
+ @if (item.icon) {
14617
+ <strct-icon [name]="item.icon" [size]="13" />
14618
+ }
14619
+ {{ item.label }}
14620
+ </strct-dropdown-item>
14621
+ }
14622
+ }
14623
+ </strct-dropdown>
14624
+ </div>
14625
+ `, isInline: true, styles: [".strct-sbt{display:inline-flex;align-items:stretch}.strct-sbt .strct-dd{display:inline-flex;align-self:stretch}.strct-sbt .strct-dd__trigger{display:inline-flex;align-items:stretch}.strct-sbt__chev{align-self:stretch;height:100%}.strct-sbt__main,.strct-sbt__chev{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--acc);background:transparent;color:var(--acc);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;padding:5px 12px}.strct-sbt__main{border-start-start-radius:7px;border-end-start-radius:7px;border-inline-end:0}.strct-sbt__chev{padding:5px 6px;border-start-end-radius:7px;border-end-end-radius:7px;border-inline-start:1px solid var(--acc50)}.strct-sbt--solid .strct-sbt__main,.strct-sbt--solid .strct-sbt__chev{background:var(--acc);color:var(--inv)}.strct-sbt--solid .strct-sbt__chev{border-inline-start-color:color-mix(in srgb,var(--inv) 30%,var(--acc))}.strct-sbt__main:hover:not(:disabled),.strct-sbt__chev:hover:not(:disabled){background:var(--acc18)}.strct-sbt--solid .strct-sbt__main:hover:not(:disabled),.strct-sbt--solid .strct-sbt__chev:hover:not(:disabled){filter:brightness(1.08);background:var(--acc)}.strct-sbt__main:disabled,.strct-sbt__chev:disabled{opacity:.5;cursor:default}.strct-sbt__main:focus-visible,.strct-sbt__chev:focus-visible{outline:2px solid var(--acc50);outline-offset:1px;position:relative;z-index:1}\n"], dependencies: [{ kind: "component", type: StrctDropdown, selector: "strct-dropdown", inputs: ["align", "popover", "popoverLabel"] }, { kind: "component", type: StrctDropdownDivider, selector: "strct-dropdown-divider" }, { kind: "component", type: StrctDropdownItem, selector: "strct-dropdown-item", inputs: ["critical", "disabled"] }, { kind: "directive", type: StrctDropdownTrigger, selector: "[strctDropdownTrigger]" }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14626
+ }
14627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, decorators: [{
14628
+ type: Component,
14629
+ args: [{ selector: 'strct-split-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
14630
+ StrctDropdown,
14631
+ StrctDropdownDivider,
14632
+ StrctDropdownItem,
14633
+ StrctDropdownTrigger,
14634
+ StrctIcon,
14635
+ ], template: `
14636
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14637
+ <button
14638
+ type="button"
14639
+ class="strct-sbt__main"
14640
+ [disabled]="disabled() || null"
14641
+ (click)="action.emit()"
14642
+ >
14643
+ @if (icon()) {
14644
+ <strct-icon [name]="icon()" [size]="13" />
14645
+ }
14646
+ {{ label() }}
14647
+ </button>
14648
+ <strct-dropdown align="end">
14649
+ <button
14650
+ type="button"
14651
+ class="strct-sbt__chev"
14652
+ strctDropdownTrigger
14653
+ [disabled]="disabled() || null"
14654
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14655
+ >
14656
+ <strct-icon name="chevronDown" [size]="12" />
14657
+ </button>
14658
+ @for (item of items(); track $index) {
14659
+ @if (item.divider) {
14660
+ <strct-dropdown-divider />
14661
+ } @else {
14662
+ <strct-dropdown-item
14663
+ [critical]="item.critical ?? false"
14664
+ [disabled]="item.disabled ?? false"
14665
+ (click)="!item.disabled && picked.emit(item)"
14666
+ >
14667
+ @if (item.icon) {
14668
+ <strct-icon [name]="item.icon" [size]="13" />
14669
+ }
14670
+ {{ item.label }}
14671
+ </strct-dropdown-item>
14672
+ }
14673
+ }
14674
+ </strct-dropdown>
14675
+ </div>
14676
+ `, styles: [".strct-sbt{display:inline-flex;align-items:stretch}.strct-sbt .strct-dd{display:inline-flex;align-self:stretch}.strct-sbt .strct-dd__trigger{display:inline-flex;align-items:stretch}.strct-sbt__chev{align-self:stretch;height:100%}.strct-sbt__main,.strct-sbt__chev{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--acc);background:transparent;color:var(--acc);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;padding:5px 12px}.strct-sbt__main{border-start-start-radius:7px;border-end-start-radius:7px;border-inline-end:0}.strct-sbt__chev{padding:5px 6px;border-start-end-radius:7px;border-end-end-radius:7px;border-inline-start:1px solid var(--acc50)}.strct-sbt--solid .strct-sbt__main,.strct-sbt--solid .strct-sbt__chev{background:var(--acc);color:var(--inv)}.strct-sbt--solid .strct-sbt__chev{border-inline-start-color:color-mix(in srgb,var(--inv) 30%,var(--acc))}.strct-sbt__main:hover:not(:disabled),.strct-sbt__chev:hover:not(:disabled){background:var(--acc18)}.strct-sbt--solid .strct-sbt__main:hover:not(:disabled),.strct-sbt--solid .strct-sbt__chev:hover:not(:disabled){filter:brightness(1.08);background:var(--acc)}.strct-sbt__main:disabled,.strct-sbt__chev:disabled{opacity:.5;cursor:default}.strct-sbt__main:focus-visible,.strct-sbt__chev:focus-visible{outline:2px solid var(--acc50);outline-offset:1px;position:relative;z-index:1}\n"] }]
14677
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
14678
+
14679
+ /**
14680
+ * Repeating diagonal text watermark over its content — compliance overlays
14681
+ * for classified/internal consoles ("CONFIDENTIAL", "user@corp · 2026-07-21"):
14682
+ *
14683
+ * <strct-watermark text="CONFIDENTIAL">…page…</strct-watermark>
14684
+ *
14685
+ * Rendered as a pointer-transparent tiled SVG layer, so the content below
14686
+ * stays fully interactive and selectable. Purely visual — `aria-hidden`.
14687
+ */
14688
+ class StrctWatermark {
14689
+ sanitizer = inject(DomSanitizer);
14690
+ /** The repeated watermark text. */
14691
+ text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
14692
+ /** Layer opacity (0–1). */
14693
+ opacity = input(0.06, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
14694
+ /** Rotation in degrees (negative = up-slope). */
14695
+ angle = input(-24, ...(ngDevMode ? [{ debugName: "angle" }] : /* istanbul ignore next */ []));
14696
+ /** Tile size in px — larger spreads the marks out. */
14697
+ gap = input(220, ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
14698
+ /** Font size in px. */
14699
+ fontSize = input(15, ...(ngDevMode ? [{ debugName: "fontSize" }] : /* istanbul ignore next */ []));
14700
+ tile = computed(() => {
14701
+ const size = this.gap();
14702
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
14703
+ `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" ` +
14704
+ `transform="rotate(${this.angle()} ${size / 2} ${size / 2})" ` +
14705
+ `font-family="sans-serif" font-size="${this.fontSize()}" font-weight="600" ` +
14706
+ `fill="currentColor" fill-opacity="${this.opacity()}">${escapeXml(this.text())}</text></svg>`;
14707
+ return this.sanitizer.bypassSecurityTrustStyle(`url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`);
14708
+ }, ...(ngDevMode ? [{ debugName: "tile" }] : /* istanbul ignore next */ []));
14709
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, deps: [], target: i0.ɵɵFactoryTarget.Component });
14710
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: StrctWatermark, isStandalone: true, selector: "strct-watermark", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, angle: { classPropertyName: "angle", publicName: "angle", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, fontSize: { classPropertyName: "fontSize", publicName: "fontSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-wm" }, ngImport: i0, template: `
14711
+ <ng-content />
14712
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14713
+ `, isInline: true, styles: [".strct-wm{position:relative;display:block}.strct-wm__layer{position:absolute;inset:0;pointer-events:none;background-repeat:repeat;z-index:5}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14714
+ }
14715
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, decorators: [{
14716
+ type: Component,
14717
+ args: [{ selector: 'strct-watermark', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14718
+ <ng-content />
14719
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14720
+ `, host: { class: 'strct-wm' }, styles: [".strct-wm{position:relative;display:block}.strct-wm__layer{position:absolute;inset:0;pointer-events:none;background-repeat:repeat;z-index:5}\n"] }]
14721
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], angle: [{ type: i0.Input, args: [{ isSignal: true, alias: "angle", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], fontSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fontSize", required: false }] }] } });
14722
+ function escapeXml(s) {
14723
+ return s
14724
+ .replace(/&/g, '&amp;')
14725
+ .replace(/</g, '&lt;')
14726
+ .replace(/>/g, '&gt;')
14727
+ .replace(/"/g, '&quot;');
14728
+ }
14729
+
14730
+ /**
14731
+ * Feature tour (coach marks) — "what's new" onboarding over live UI:
14732
+ *
14733
+ * <strct-tour [(open)]="tourOpen" [steps]="steps" (finished)="done()" />
14734
+ *
14735
+ * Each step spotlights its `target` selector with a ring cut out of a dimmed
14736
+ * backdrop and anchors a card next to it. Arrows / buttons step, Escape
14737
+ * closes, and the card is a labeled dialog. Purely DOM-driven: targets are
14738
+ * looked up when the step shows, so it works over any page.
14739
+ */
14740
+ class StrctTour {
14741
+ /** Whether the tour is showing (two-way). */
14742
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
14743
+ /** The steps, in order. */
14744
+ steps = input.required(...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
14745
+ /** Localizable strings. */
14746
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
14747
+ backLabel = input('Back', ...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
14748
+ skipLabel = input('Skip', ...(ngDevMode ? [{ debugName: "skipLabel" }] : /* istanbul ignore next */ []));
14749
+ doneLabel = input('Done', ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
14750
+ /** The last step's Done was clicked (Escape / Skip emit `dismissed`). */
14751
+ finished = output();
14752
+ dismissed = output();
14753
+ index = signal(0, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
14754
+ /** Bumped on open/step/resize to re-measure the target. */
14755
+ measureTick = signal(0, ...(ngDevMode ? [{ debugName: "measureTick" }] : /* istanbul ignore next */ []));
14756
+ current = computed(() => this.steps()[this.index()] ?? null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
14757
+ spot = computed(() => {
14758
+ this.measureTick();
14759
+ const step = this.current();
14760
+ if (!this.open() || !step?.target)
14761
+ return null;
14762
+ const el = document.querySelector(step.target);
14763
+ if (!el)
14764
+ return null;
14765
+ const r = el.getBoundingClientRect();
14766
+ return { top: r.top, left: r.left, width: r.width, height: r.height };
14767
+ }, ...(ngDevMode ? [{ debugName: "spot" }] : /* istanbul ignore next */ []));
14768
+ cardPos = computed(() => {
14769
+ const r = this.spot();
14770
+ const w = 300;
14771
+ const h = 170;
14772
+ if (!r) {
14773
+ return {
14774
+ top: Math.max(20, window.innerHeight / 2 - h / 2),
14775
+ left: Math.max(20, window.innerWidth / 2 - w / 2),
14776
+ };
14777
+ }
14778
+ // Below the target if it fits, else above; clamped into the viewport.
14779
+ let top = r.top + r.height + 14;
14780
+ if (top + h > window.innerHeight - 12)
14781
+ top = Math.max(12, r.top - h - 14);
14782
+ const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
14783
+ return { top, left };
14784
+ }, ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
14785
+ constructor() {
14786
+ // Restart from step 0 on open; scroll the first target into view.
14787
+ effect(() => {
14788
+ if (this.open()) {
14789
+ this.index.set(0);
14790
+ this.scrollToTarget();
14791
+ }
14792
+ });
14793
+ }
14794
+ next() {
14795
+ if (this.index() >= this.steps().length - 1) {
14796
+ this.open.set(false);
14797
+ this.finished.emit();
14798
+ return;
14799
+ }
14800
+ this.index.update((i) => i + 1);
14801
+ this.scrollToTarget();
14802
+ }
14803
+ prev() {
14804
+ this.index.update((i) => Math.max(0, i - 1));
14805
+ this.scrollToTarget();
14806
+ }
14807
+ close() {
14808
+ this.open.set(false);
14809
+ this.dismissed.emit();
14810
+ }
14811
+ scrollToTarget() {
14812
+ setTimeout(() => {
14813
+ const sel = this.steps()[this.index()]?.target;
14814
+ if (sel)
14815
+ document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
14816
+ this.measureTick.update((n) => n + 1);
14817
+ });
14818
+ }
14819
+ onEscape() {
14820
+ if (this.open())
14821
+ this.close();
14822
+ }
14823
+ onReflow() {
14824
+ if (this.open())
14825
+ this.measureTick.update((n) => n + 1);
14826
+ }
14827
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
14828
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTour, isStandalone: true, selector: "strct-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", finished: "finished", dismissed: "dismissed" }, host: { listeners: { "document:keydown.escape": "onEscape()", "window:resize": "onReflow()", "window:scroll": "onReflow()" } }, ngImport: i0, template: `
14829
+ @if (open() && current(); as step) {
14830
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14831
+ <div
14832
+ class="strct-tour__backdrop"
14833
+ [class.strct-tour__backdrop--dim]="!spot()"
14834
+ (click)="close()"
14835
+ ></div>
14836
+ @if (spot(); as r) {
14837
+ <div
14838
+ class="strct-tour__ring"
14839
+ [style.top.px]="r.top - 6"
14840
+ [style.left.px]="r.left - 6"
14841
+ [style.width.px]="r.width + 12"
14842
+ [style.height.px]="r.height + 12"
14843
+ ></div>
14844
+ }
14845
+ <div
14846
+ class="strct-tour__card"
14847
+ role="dialog"
14848
+ [attr.aria-label]="step.title"
14849
+ [style.top.px]="cardPos().top"
14850
+ [style.left.px]="cardPos().left"
14851
+ >
14852
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14853
+ <div class="strct-tour__title">{{ step.title }}</div>
14854
+ <div class="strct-tour__body">{{ step.body }}</div>
14855
+ <div class="strct-tour__nav">
14856
+ <button strct-button size="sm" variant="flat" (click)="close()">
14857
+ {{ skipLabel() }}
14858
+ </button>
14859
+ <span class="strct-tour__grow"></span>
14860
+ @if (index() > 0) {
14861
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14862
+ {{ backLabel() }}
14863
+ </button>
14864
+ }
14865
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14866
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14867
+ </button>
14868
+ </div>
14869
+ </div>
14870
+ }
14871
+ `, isInline: true, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:400;background:transparent}.strct-tour__backdrop--dim{background:#00000073}.strct-tour__ring{position:fixed;z-index:401;border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px #00000073;pointer-events:none}.strct-tour__card{position:fixed;z-index:402;width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\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 });
14872
+ }
14873
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, decorators: [{
14874
+ type: Component,
14875
+ args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
14876
+ @if (open() && current(); as step) {
14877
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14878
+ <div
14879
+ class="strct-tour__backdrop"
14880
+ [class.strct-tour__backdrop--dim]="!spot()"
14881
+ (click)="close()"
14882
+ ></div>
14883
+ @if (spot(); as r) {
14884
+ <div
14885
+ class="strct-tour__ring"
14886
+ [style.top.px]="r.top - 6"
14887
+ [style.left.px]="r.left - 6"
14888
+ [style.width.px]="r.width + 12"
14889
+ [style.height.px]="r.height + 12"
14890
+ ></div>
14891
+ }
14892
+ <div
14893
+ class="strct-tour__card"
14894
+ role="dialog"
14895
+ [attr.aria-label]="step.title"
14896
+ [style.top.px]="cardPos().top"
14897
+ [style.left.px]="cardPos().left"
14898
+ >
14899
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14900
+ <div class="strct-tour__title">{{ step.title }}</div>
14901
+ <div class="strct-tour__body">{{ step.body }}</div>
14902
+ <div class="strct-tour__nav">
14903
+ <button strct-button size="sm" variant="flat" (click)="close()">
14904
+ {{ skipLabel() }}
14905
+ </button>
14906
+ <span class="strct-tour__grow"></span>
14907
+ @if (index() > 0) {
14908
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14909
+ {{ backLabel() }}
14910
+ </button>
14911
+ }
14912
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14913
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14914
+ </button>
14915
+ </div>
14916
+ </div>
14917
+ }
14918
+ `, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:400;background:transparent}.strct-tour__backdrop--dim{background:#00000073}.strct-tour__ring{position:fixed;z-index:401;border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px #00000073;pointer-events:none}.strct-tour__card{position:fixed;z-index:402;width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"] }]
14919
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], finished: [{ type: i0.Output, args: ["finished"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], onEscape: [{
14920
+ type: HostListener,
14921
+ args: ['document:keydown.escape']
14922
+ }], onReflow: [{
14923
+ type: HostListener,
14924
+ args: ['window:resize']
14925
+ }, {
14926
+ type: HostListener,
14927
+ args: ['window:scroll']
14928
+ }] } });
14929
+
14930
+ /**
14931
+ * Horizontal menubar — the application-menu strip ("File · Edit · View") for
14932
+ * dense tool-style consoles:
14933
+ *
14934
+ * <strct-menubar [menus]="menus" (picked)="run($event)" />
14935
+ *
14936
+ * Click (or Enter/Down) opens a menu; Left/Right move across the bar and,
14937
+ * while open, switch the open menu (the APG menubar convention). Escape and
14938
+ * outside click close. `(picked)` carries `{ menu, item }`.
14939
+ */
14940
+ class StrctMenubar {
14941
+ host = inject(ElementRef);
14942
+ /** Top-level menus. */
14943
+ menus = input.required(...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
14944
+ /** Accessible name of the bar (localizable). */
14945
+ ariaLabel = input('Application menu', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
14946
+ /** A menu entry was chosen. */
14947
+ picked = output();
14948
+ openId = signal(null, ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
14949
+ focusIdx = signal(0, ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
14950
+ toggleMenu(id, i) {
14951
+ this.focusIdx.set(i);
14952
+ this.openId.set(this.openId() === id ? null : id);
14953
+ }
14954
+ pick(menu, item) {
14955
+ if (item.disabled)
14956
+ return;
14957
+ this.openId.set(null);
14958
+ this.picked.emit({ menu, item });
14959
+ }
14960
+ onTopKey(event, i) {
14961
+ const menus = this.menus();
14962
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
14963
+ event.preventDefault();
14964
+ const dir = event.key === 'ArrowRight' ? 1 : -1;
14965
+ const next = (i + dir + menus.length) % menus.length;
14966
+ this.focusIdx.set(next);
14967
+ // While a menu is open, moving across the bar switches the open menu.
14968
+ if (this.openId())
14969
+ this.openId.set(menus[next].id);
14970
+ setTimeout(() => {
14971
+ const tops = this.host.nativeElement.querySelectorAll('.strct-mb__top');
14972
+ tops[next]?.focus();
14973
+ });
14974
+ }
14975
+ else if (event.key === 'ArrowDown' || event.key === 'Enter') {
14976
+ event.preventDefault();
14977
+ this.openId.set(menus[i].id);
14978
+ }
14979
+ }
14980
+ onDocClick(event) {
14981
+ if (this.openId() && !this.host.nativeElement.contains(event.target)) {
14982
+ this.openId.set(null);
14983
+ }
14984
+ }
14985
+ onEscape() {
14986
+ this.openId.set(null);
14987
+ }
14988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
14989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctMenubar, isStandalone: true, selector: "strct-menubar", inputs: { menus: { classPropertyName: "menus", publicName: "menus", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
14990
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
14991
+ @for (menu of menus(); track menu.id; let i = $index) {
14992
+ <div class="strct-mb__wrap">
14993
+ <button
14994
+ type="button"
14995
+ class="strct-mb__top"
14996
+ role="menuitem"
14997
+ [class.strct-mb__top--open]="openId() === menu.id"
14998
+ [attr.aria-haspopup]="'menu'"
14999
+ [attr.aria-expanded]="openId() === menu.id"
15000
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15001
+ (click)="toggleMenu(menu.id, i)"
15002
+ (keydown)="onTopKey($event, i)"
15003
+ >
15004
+ {{ menu.label }}
15005
+ </button>
15006
+ @if (openId() === menu.id) {
15007
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15008
+ @for (item of menu.items; track $index) {
15009
+ @if (item.divider) {
15010
+ <div class="strct-mb__divider" role="separator"></div>
15011
+ } @else {
15012
+ <button
15013
+ type="button"
15014
+ class="strct-mb__item"
15015
+ role="menuitem"
15016
+ [class.strct-mb__item--critical]="item.critical"
15017
+ [disabled]="item.disabled || null"
15018
+ (click)="pick(menu, item)"
15019
+ >
15020
+ @if (item.icon) {
15021
+ <strct-icon [name]="item.icon" [size]="13" />
15022
+ }
15023
+ {{ item.label }}
15024
+ </button>
15025
+ }
15026
+ }
15027
+ </div>
15028
+ }
15029
+ </div>
15030
+ }
15031
+ </div>
15032
+ `, isInline: true, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:7px}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:200;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not(:disabled){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not(:disabled){background:var(--critical-bg)}.strct-mb__item:disabled{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15033
+ }
15034
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, decorators: [{
15035
+ type: Component,
15036
+ args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
15037
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
15038
+ @for (menu of menus(); track menu.id; let i = $index) {
15039
+ <div class="strct-mb__wrap">
15040
+ <button
15041
+ type="button"
15042
+ class="strct-mb__top"
15043
+ role="menuitem"
15044
+ [class.strct-mb__top--open]="openId() === menu.id"
15045
+ [attr.aria-haspopup]="'menu'"
15046
+ [attr.aria-expanded]="openId() === menu.id"
15047
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15048
+ (click)="toggleMenu(menu.id, i)"
15049
+ (keydown)="onTopKey($event, i)"
15050
+ >
15051
+ {{ menu.label }}
15052
+ </button>
15053
+ @if (openId() === menu.id) {
15054
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15055
+ @for (item of menu.items; track $index) {
15056
+ @if (item.divider) {
15057
+ <div class="strct-mb__divider" role="separator"></div>
15058
+ } @else {
15059
+ <button
15060
+ type="button"
15061
+ class="strct-mb__item"
15062
+ role="menuitem"
15063
+ [class.strct-mb__item--critical]="item.critical"
15064
+ [disabled]="item.disabled || null"
15065
+ (click)="pick(menu, item)"
15066
+ >
15067
+ @if (item.icon) {
15068
+ <strct-icon [name]="item.icon" [size]="13" />
15069
+ }
15070
+ {{ item.label }}
15071
+ </button>
15072
+ }
15073
+ }
15074
+ </div>
15075
+ }
15076
+ </div>
15077
+ }
15078
+ </div>
15079
+ `, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:7px}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:200;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not(:disabled){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not(:disabled){background:var(--critical-bg)}.strct-mb__item:disabled{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}\n"] }]
15080
+ }], propDecorators: { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], onDocClick: [{
15081
+ type: HostListener,
15082
+ args: ['document:click', ['$event']]
15083
+ }], onEscape: [{
15084
+ type: HostListener,
15085
+ args: ['document:keydown.escape']
15086
+ }] } });
15087
+
15088
+ /**
15089
+ * Screen-reader announcements for state changes that have no visible text —
15090
+ * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
15091
+ *
15092
+ * private announcer = inject(StrctAnnouncer);
15093
+ * this.announcer.announce('12 rows loaded');
15094
+ *
15095
+ * Maintains one visually-hidden live region per politeness level; repeated
15096
+ * identical messages are re-announced (the region is cleared first).
15097
+ */
15098
+ class StrctAnnouncer {
15099
+ regions = new Map();
15100
+ clearTimer;
15101
+ announce(message, politeness = 'polite') {
15102
+ const region = this.regionFor(politeness);
15103
+ // Clear first so identical consecutive messages still fire.
15104
+ region.textContent = '';
15105
+ setTimeout(() => (region.textContent = message));
15106
+ clearTimeout(this.clearTimer);
15107
+ // Stale announcements should not linger for the next SR user to stumble on.
15108
+ this.clearTimer = setTimeout(() => (region.textContent = ''), 10_000);
15109
+ }
15110
+ regionFor(politeness) {
15111
+ let region = this.regions.get(politeness);
15112
+ if (!region) {
15113
+ region = document.createElement('div');
15114
+ region.setAttribute('aria-live', politeness);
15115
+ region.setAttribute('aria-atomic', 'true');
15116
+ region.className = 'strct-announcer';
15117
+ region.style.cssText =
15118
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
15119
+ document.body.appendChild(region);
15120
+ this.regions.set(politeness, region);
15121
+ }
15122
+ return region;
15123
+ }
15124
+ ngOnDestroy() {
15125
+ clearTimeout(this.clearTimer);
15126
+ for (const region of this.regions.values())
15127
+ region.remove();
15128
+ this.regions.clear();
15129
+ }
15130
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15131
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
15132
+ }
15133
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, decorators: [{
15134
+ type: Injectable,
15135
+ args: [{ providedIn: 'root' }]
15136
+ }] });
15137
+
15138
+ function normalize(combo) {
15139
+ return combo
15140
+ .toLowerCase()
15141
+ .split('+')
15142
+ .map((p) => p.trim())
15143
+ .sort()
15144
+ .join('+');
15145
+ }
15146
+ function comboOf(event) {
15147
+ const parts = [];
15148
+ if (event.ctrlKey)
15149
+ parts.push('ctrl');
15150
+ if (event.metaKey)
15151
+ parts.push('meta');
15152
+ if (event.altKey)
15153
+ parts.push('alt');
15154
+ if (event.shiftKey && event.key.length > 1)
15155
+ parts.push('shift');
15156
+ parts.push(event.key.toLowerCase());
15157
+ return parts.sort().join('+');
15158
+ }
15159
+ /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
15160
+ function expand(combo) {
15161
+ const n = combo.toLowerCase();
15162
+ if (!n.includes('mod'))
15163
+ return [normalize(n)];
15164
+ return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
15165
+ }
15166
+ /**
15167
+ * Application hotkeys, centrally registered so they are discoverable — the
15168
+ * Blueprint pattern:
15169
+ *
15170
+ * private hotkeys = inject(StrctHotkeysService);
15171
+ * ngOnInit() { this.dispose = this.hotkeys.register(
15172
+ * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
15173
+ * handler: () => this.palette.open() }); }
15174
+ *
15175
+ * Combos: `'a'`, `'?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
15176
+ * Plain-key combos are suppressed while typing in inputs / textareas /
15177
+ * contenteditable; modifier combos always fire. Pair with
15178
+ * `<strct-hotkeys-help/>` for the ⌘/ ("?") cheatsheet overlay.
15179
+ */
15180
+ class StrctHotkeysService {
15181
+ registry = signal([], ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
15182
+ /** All registered hotkeys (for the help overlay). */
15183
+ hotkeys = this.registry.asReadonly();
15184
+ listener = (event) => this.onKeydown(event);
15185
+ constructor() {
15186
+ document.addEventListener('keydown', this.listener);
15187
+ }
15188
+ /** Register a hotkey; the returned function unregisters it. */
15189
+ register(hotkey) {
15190
+ this.registry.update((list) => [...list, hotkey]);
15191
+ return () => this.registry.update((list) => list.filter((h) => h !== hotkey));
15192
+ }
15193
+ onKeydown(event) {
15194
+ const combo = comboOf(event);
15195
+ const typing = isTyping(event);
15196
+ for (const hk of this.registry()) {
15197
+ if (!expand(hk.combo).includes(combo))
15198
+ continue;
15199
+ // While typing, only modifier combos may fire.
15200
+ if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
15201
+ continue;
15202
+ event.preventDefault();
15203
+ hk.handler(event);
15204
+ return;
15205
+ }
15206
+ }
15207
+ ngOnDestroy() {
15208
+ document.removeEventListener('keydown', this.listener);
15209
+ }
15210
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15211
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
15212
+ }
15213
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, decorators: [{
15214
+ type: Injectable,
15215
+ args: [{ providedIn: 'root' }]
15216
+ }], ctorParameters: () => [] });
15217
+ function isTyping(event) {
15218
+ const el = event.target;
15219
+ if (!el)
15220
+ return false;
15221
+ return (el.tagName === 'INPUT' ||
15222
+ el.tagName === 'TEXTAREA' ||
15223
+ el.tagName === 'SELECT' ||
15224
+ el.isContentEditable);
15225
+ }
15226
+ /**
15227
+ * The hotkey cheatsheet overlay. Mount once near the app root; it registers
15228
+ * `?` itself and lists everything in the service, grouped:
15229
+ *
15230
+ * <strct-hotkeys-help />
15231
+ */
15232
+ class StrctHotkeysHelp {
15233
+ service = inject(StrctHotkeysService);
15234
+ /** Overlay visibility (two-way; `?` toggles it too). */
15235
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
15236
+ /** Localizable strings. */
15237
+ title = input('Keyboard shortcuts', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
15238
+ emptyText = input('No shortcuts registered.', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
15239
+ grouped = computed(() => {
15240
+ const groups = new Map();
15241
+ for (const hk of this.service.hotkeys()) {
15242
+ const name = hk.group ?? 'General';
15243
+ const list = groups.get(name);
15244
+ if (list)
15245
+ list.push(hk);
15246
+ else
15247
+ groups.set(name, [hk]);
15248
+ }
15249
+ return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
15250
+ }, ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
15251
+ constructor() {
15252
+ const dispose = this.service.register({
15253
+ combo: '?',
15254
+ description: 'Show this help',
15255
+ group: 'General',
15256
+ handler: () => this.open.update((v) => !v),
15257
+ });
15258
+ inject(DestroyRef).onDestroy(dispose);
15259
+ }
15260
+ // Escape is handled locally — registering it in the service would swallow
15261
+ // every Escape in the app (the service preventDefaults on match).
15262
+ onEscape() {
15263
+ if (this.open())
15264
+ this.open.set(false);
15265
+ }
15266
+ parts(combo) {
15267
+ return combo.split('+').map((p) => {
15268
+ const map = {
15269
+ mod: '⌘/Ctrl',
15270
+ meta: '⌘',
15271
+ ctrl: 'Ctrl',
15272
+ alt: 'Alt',
15273
+ shift: 'Shift',
15274
+ escape: 'Esc',
15275
+ };
15276
+ return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
15277
+ });
15278
+ }
15279
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
15280
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctHotkeysHelp, isStandalone: true, selector: "strct-hotkeys-help", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
15281
+ @if (open()) {
15282
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15283
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15284
+ <div class="strct-hkh__head">
15285
+ <span class="strct-hkh__title">{{ title() }}</span>
15286
+ <strct-kbd>?</strct-kbd>
15287
+ </div>
15288
+ @for (group of grouped(); track group.name) {
15289
+ <div class="strct-hkh__group">
15290
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15291
+ @for (hk of group.keys; track hk.combo + hk.description) {
15292
+ <div class="strct-hkh__row">
15293
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15294
+ <span class="strct-hkh__keys">
15295
+ @for (part of parts(hk.combo); track $index) {
15296
+ <strct-kbd>{{ part }}</strct-kbd>
15297
+ }
15298
+ </span>
15299
+ </div>
15300
+ }
15301
+ </div>
15302
+ } @empty {
15303
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15304
+ }
15305
+ </div>
15306
+ }
15307
+ `, isInline: true, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:400;background:#00000073}.strct-hkh{position:fixed;z-index:401;top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15308
+ }
15309
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, decorators: [{
15310
+ type: Component,
15311
+ args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctKbd], template: `
15312
+ @if (open()) {
15313
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15314
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15315
+ <div class="strct-hkh__head">
15316
+ <span class="strct-hkh__title">{{ title() }}</span>
15317
+ <strct-kbd>?</strct-kbd>
15318
+ </div>
15319
+ @for (group of grouped(); track group.name) {
15320
+ <div class="strct-hkh__group">
15321
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15322
+ @for (hk of group.keys; track hk.combo + hk.description) {
15323
+ <div class="strct-hkh__row">
15324
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15325
+ <span class="strct-hkh__keys">
15326
+ @for (part of parts(hk.combo); track $index) {
15327
+ <strct-kbd>{{ part }}</strct-kbd>
15328
+ }
15329
+ </span>
15330
+ </div>
15331
+ }
15332
+ </div>
15333
+ } @empty {
15334
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15335
+ }
15336
+ </div>
15337
+ }
15338
+ `, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:400;background:#00000073}.strct-hkh{position:fixed;z-index:401;top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"] }]
15339
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], onEscape: [{
15340
+ type: HostListener,
15341
+ args: ['document:keydown.escape']
15342
+ }] } });
15343
+
15344
+ /**
15345
+ * List drag-reorder primitive — the consumer owns the array:
15346
+ *
15347
+ * <ul strctReorder (reordered)="move($event)">
15348
+ * @for (s of steps(); track s.id) {
15349
+ * <li strctReorderItem>{{ s.label }}</li>
15350
+ * }
15351
+ * </ul>
15352
+ *
15353
+ * move({ from, to }: StrctReorderEvent) {
15354
+ * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
15355
+ * }
15356
+ *
15357
+ * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
15358
+ * on the focused item (items get `tabindex="0"` unless they already manage
15359
+ * focus). Indexes are positions among the `strctReorderItem` siblings.
15360
+ */
15361
+ class StrctReorder {
15362
+ host = inject(ElementRef);
15363
+ /** Emits when a drag or keyboard move completes. */
15364
+ reordered = output();
15365
+ /** Disable all reordering (display-only mode). */
15366
+ reorderDisabled = input(false, ...(ngDevMode ? [{ debugName: "reorderDisabled" }] : /* istanbul ignore next */ []));
15367
+ dragIndex = signal(null, ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
15368
+ overIndex = signal(null, ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
15369
+ items() {
15370
+ return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
15371
+ }
15372
+ indexOf(el) {
15373
+ return this.items().indexOf(el);
15374
+ }
15375
+ commit(from, to) {
15376
+ this.dragIndex.set(null);
15377
+ this.overIndex.set(null);
15378
+ if (from < 0 || to < 0 || from === to)
15379
+ return;
15380
+ this.reordered.emit({ from, to });
15381
+ }
15382
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15383
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctReorder, isStandalone: true, selector: "[strctReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { reordered: "reordered" }, ngImport: i0 });
15384
+ }
15385
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, decorators: [{
15386
+ type: Directive,
15387
+ args: [{ selector: '[strctReorder]' }]
15388
+ }], propDecorators: { reordered: [{ type: i0.Output, args: ["reordered"] }], reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }] } });
15389
+ /** One draggable row inside a `[strctReorder]` container. */
15390
+ class StrctReorderItem {
15391
+ list = inject(StrctReorder);
15392
+ el = inject(ElementRef);
15393
+ // Captured before our own binding ever writes tabindex, so it never flips.
15394
+ hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
15395
+ hostTabindex() {
15396
+ return this.hadOwnTabindex ? null : 0;
15397
+ }
15398
+ isDragging() {
15399
+ return this.list.dragIndex() === this.index();
15400
+ }
15401
+ isOver() {
15402
+ return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
15403
+ }
15404
+ index() {
15405
+ return this.list.indexOf(this.el.nativeElement);
15406
+ }
15407
+ onDragStart(event) {
15408
+ if (this.list.reorderDisabled())
15409
+ return;
15410
+ this.list.dragIndex.set(this.index());
15411
+ event.dataTransfer?.setData('text/plain', String(this.index()));
15412
+ if (event.dataTransfer)
15413
+ event.dataTransfer.effectAllowed = 'move';
15414
+ }
15415
+ onDragOver(event) {
15416
+ if (this.list.dragIndex() == null)
15417
+ return;
15418
+ event.preventDefault();
15419
+ this.list.overIndex.set(this.index());
15420
+ }
15421
+ onDrop(event) {
15422
+ event.preventDefault();
15423
+ const from = this.list.dragIndex();
15424
+ if (from == null)
15425
+ return;
15426
+ this.list.commit(from, this.index());
15427
+ }
15428
+ onDragEnd() {
15429
+ this.list.dragIndex.set(null);
15430
+ this.list.overIndex.set(null);
15431
+ }
15432
+ onKeydown(event) {
15433
+ if (this.list.reorderDisabled() || !event.altKey)
15434
+ return;
15435
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
15436
+ return;
15437
+ event.preventDefault();
15438
+ const from = this.index();
15439
+ const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
15440
+ if (to < 0 || to >= this.list.items().length)
15441
+ return;
15442
+ this.list.commit(from, to);
15443
+ // Keep focus on the moved item after the consumer re-renders.
15444
+ setTimeout(() => this.list.items()[to]?.focus());
15445
+ }
15446
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15447
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.16", type: StrctReorderItem, isStandalone: true, selector: "[strctReorderItem]", host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd()", "keydown": "onKeydown($event)" }, properties: { "attr.draggable": "!list.reorderDisabled()", "attr.tabindex": "hostTabindex()", "class.strct-reorder--dragging": "isDragging()", "class.strct-reorder--over": "isOver()", "attr.aria-roledescription": "'sortable'" } }, ngImport: i0 });
15448
+ }
15449
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, decorators: [{
15450
+ type: Directive,
15451
+ args: [{
15452
+ selector: '[strctReorderItem]',
15453
+ host: {
15454
+ '[attr.draggable]': '!list.reorderDisabled()',
15455
+ '[attr.tabindex]': 'hostTabindex()',
15456
+ '[class.strct-reorder--dragging]': 'isDragging()',
15457
+ '[class.strct-reorder--over]': 'isOver()',
15458
+ '[attr.aria-roledescription]': "'sortable'",
15459
+ },
15460
+ }]
15461
+ }], propDecorators: { onDragStart: [{
15462
+ type: HostListener,
15463
+ args: ['dragstart', ['$event']]
15464
+ }], onDragOver: [{
15465
+ type: HostListener,
15466
+ args: ['dragover', ['$event']]
15467
+ }], onDrop: [{
15468
+ type: HostListener,
15469
+ args: ['drop', ['$event']]
15470
+ }], onDragEnd: [{
15471
+ type: HostListener,
15472
+ args: ['dragend']
15473
+ }], onKeydown: [{
15474
+ type: HostListener,
15475
+ args: ['keydown', ['$event']]
15476
+ }] } });
15477
+
12646
15478
  /**
12647
15479
  * Public API surface of the strct UI library.
12648
15480
  */
@@ -12652,5 +15484,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12652
15484
  * Generated bundle index. Do not edit.
12653
15485
  */
12654
15486
 
12655
- 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, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDesc, StrctDescriptionList, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHero, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMetricTile, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRating, StrctRowDetailDef, StrctSearchbox, StrctSectionMenu, StrctSegmented, StrctShell, StrctShellService, 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, strctValidationIcon, strctValidationTone };
15487
+ export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionMenu, StrctSegmented, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWatermark, StrctWizard, parseAnsi, registerStrctIcon, strctComputeDiff, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone };
12656
15488
  //# sourceMappingURL=akcelik-strct.mjs.map