@akcelik/strct 1.6.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, 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 } 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';
@@ -256,6 +256,7 @@ const STRCT_ICON_DEFS = {
256
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"/>',
257
257
  flag: '<path d="M4 13.5V3M4 3.6h7.4L9.8 6.2l1.6 2.6H4"/>',
258
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"/>',
259
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"/>',
260
261
  // ── State / action (also usable as badges via the icon `badge` input) ─
261
262
  running: '<path d="M5.5 4l6 4-6 4z"/>',
@@ -539,6 +540,7 @@ const STRCT_ICON_GROUPS = [
539
540
  'api',
540
541
  'bolt',
541
542
  'queue',
543
+ 'rocket',
542
544
  ],
543
545
  },
544
546
  {
@@ -3381,6 +3383,13 @@ class StrctDropdown {
3381
3383
  /** Accessible name of the popover dialog (localizable). */
3382
3384
  popoverLabel = input('Filters', ...(ngDevMode ? [{ debugName: "popoverLabel" }] : /* istanbul ignore next */ []));
3383
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
+ }
3384
3393
  toggle() {
3385
3394
  this.open.update((v) => !v);
3386
3395
  }
@@ -3402,17 +3411,13 @@ class StrctDropdown {
3402
3411
  }
3403
3412
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
3404
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: `
3405
- <div
3406
- #trigger
3407
- class="strct-dd__trigger"
3408
- role="button"
3409
- tabindex="0"
3410
- [attr.aria-haspopup]="popover() ? 'dialog' : 'menu'"
3411
- [attr.aria-expanded]="open()"
3412
- (click)="toggle()"
3413
- (keydown.enter)="toggle()"
3414
- (keydown.space)="toggle()"
3415
- >
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()">
3416
3421
  <ng-content select="[strctDropdownTrigger]" />
3417
3422
  </div>
3418
3423
  <!--
@@ -3440,17 +3445,13 @@ class StrctDropdown {
3440
3445
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDropdown, decorators: [{
3441
3446
  type: Component,
3442
3447
  args: [{ selector: 'strct-dropdown', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctOverlay], template: `
3443
- <div
3444
- #trigger
3445
- class="strct-dd__trigger"
3446
- role="button"
3447
- tabindex="0"
3448
- [attr.aria-haspopup]="popover() ? 'dialog' : 'menu'"
3449
- [attr.aria-expanded]="open()"
3450
- (click)="toggle()"
3451
- (keydown.enter)="toggle()"
3452
- (keydown.space)="toggle()"
3453
- >
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()">
3454
3455
  <ng-content select="[strctDropdownTrigger]" />
3455
3456
  </div>
3456
3457
  <!--
@@ -3474,13 +3475,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
3474
3475
  </div>
3475
3476
  }
3476
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"] }]
3477
- }], 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: [{
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: [{
3478
3479
  type: HostListener,
3479
3480
  args: ['document:click', ['$event']]
3480
3481
  }], onEscape: [{
3481
3482
  type: HostListener,
3482
3483
  args: ['document:keydown.escape']
3483
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
+ }] });
3484
3505
  /** A selectable row inside a `<strct-dropdown>`. */
3485
3506
  class StrctDropdownItem {
3486
3507
  /** Danger. */
@@ -8262,6 +8283,12 @@ const DG_LABELS = {
8262
8283
  chooseColumns: 'Choose columns',
8263
8284
  refresh: 'Refresh data',
8264
8285
  actions: 'Actions',
8286
+ filterColumn: 'Filter',
8287
+ filterPlaceholder: 'Filter…',
8288
+ clearFilter: 'Clear filter',
8289
+ editCell: 'Edit',
8290
+ toggleChildren: 'Toggle children',
8291
+ quickFilter: 'Quick filter',
8265
8292
  };
8266
8293
  /** Utility-column widths used when sticky columns are active (px). */
8267
8294
  const UTIL_W = { detail: 36, expand: 36, sel: 40 };
@@ -8389,6 +8416,30 @@ class StrctDatagrid {
8389
8416
  * groups. Paging is bypassed while grouped; not combinable with `virtual`.
8390
8417
  */
8391
8418
  groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
8419
+ /**
8420
+ * Two-way per-column filter state: `key → contains-text` (text filter) or
8421
+ * `key → unknown[]` (checked value set). Applied client-side; in `lazy`
8422
+ * mode it is passed through on `lazyLoad` instead.
8423
+ */
8424
+ filters = model({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
8425
+ /**
8426
+ * Global quick filter (two-way): one term substring-matched (OR) across
8427
+ * `quickFilterFields` — the console-standard "filter this list fast" box.
8428
+ * Complementary to the per-column `filters` (which AND). Applied before
8429
+ * paging client-side; passed through on `lazyLoad` in server mode.
8430
+ */
8431
+ quickFilter = model('', ...(ngDevMode ? [{ debugName: "quickFilter" }] : /* istanbul ignore next */ []));
8432
+ /** Columns the quick term scans. Defaults to every column's `key`. */
8433
+ quickFilterFields = input(null, ...(ngDevMode ? [{ debugName: "quickFilterFields" }] : /* istanbul ignore next */ []));
8434
+ /** Render the built-in quick-filter searchbox in the toolbar. */
8435
+ quickFilterable = input(false, { ...(ngDevMode ? { debugName: "quickFilterable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
8436
+ /**
8437
+ * Tree-grid: the row property holding a row's children array. Rows render
8438
+ * hierarchically with indent + carets; sorting applies per sibling level;
8439
+ * an active filter shows matches with their ancestors, force-expanded.
8440
+ * Not combinable with `groupBy` or `lazy`.
8441
+ */
8442
+ childrenKey = input(null, ...(ngDevMode ? [{ debugName: "childrenKey" }] : /* istanbul ignore next */ []));
8392
8443
  /** Emitted when the selection changes. */
8393
8444
  selectionChange = output();
8394
8445
  /** Emitted when the refresh button is clicked. */
@@ -8397,6 +8448,9 @@ class StrctDatagrid {
8397
8448
  rowAction = output();
8398
8449
  /** Server-side data request (lazy mode): load this page / sort and set `rows`. */
8399
8450
  lazyLoad = output();
8451
+ /** An editable cell was committed (Enter / blur). The grid does not mutate
8452
+ * rows — apply `value` to your data and pass the updated array back in. */
8453
+ cellEdit = output();
8400
8454
  menu = inject(StrctMenuService);
8401
8455
  detailDef = contentChild(StrctRowDetailDef, ...(ngDevMode ? [{ debugName: "detailDef" }] : /* istanbul ignore next */ []));
8402
8456
  actionBarDef = contentChild(StrctDatagridActionBar, ...(ngDevMode ? [{ debugName: "actionBarDef" }] : /* istanbul ignore next */ []));
@@ -8493,6 +8547,11 @@ class StrctDatagrid {
8493
8547
  dragKey = signal(null, ...(ngDevMode ? [{ debugName: "dragKey" }] : /* istanbul ignore next */ []));
8494
8548
  dropKey = signal(null, ...(ngDevMode ? [{ debugName: "dropKey" }] : /* istanbul ignore next */ []));
8495
8549
  onColDragStart(key, event) {
8550
+ // Text selection inside the filter panel must not start a column drag.
8551
+ if (event.target?.closest?.('.strct-dg__filterpanel')) {
8552
+ event.preventDefault();
8553
+ return;
8554
+ }
8496
8555
  if (!this.reorderable())
8497
8556
  return;
8498
8557
  this.dragKey.set(key);
@@ -8562,12 +8621,171 @@ class StrctDatagrid {
8562
8621
  itemKey(it) {
8563
8622
  return it.group ? `strct-group:${String(it.group.key)}` : this.rowKey(it.row);
8564
8623
  }
8624
+ // ── Column filter popover ──────────────────────────────────────
8625
+ filterOpenKey = signal(null, ...(ngDevMode ? [{ debugName: "filterOpenKey" }] : /* istanbul ignore next */ []));
8626
+ toggleFilterPanel(key, event) {
8627
+ event.stopPropagation();
8628
+ this.filterOpenKey.set(this.filterOpenKey() === key ? null : key);
8629
+ }
8630
+ hasFilter(key) {
8631
+ const v = this.filters()[key];
8632
+ return Array.isArray(v) ? v.length > 0 : v != null && String(v).trim() !== '';
8633
+ }
8634
+ textFilter(key) {
8635
+ const v = this.filters()[key];
8636
+ return typeof v === 'string' ? v : '';
8637
+ }
8638
+ setTextFilter(key, value) {
8639
+ this.filters.update((f) => ({ ...f, [key]: value }));
8640
+ }
8641
+ optionChecked(key, opt) {
8642
+ const v = this.filters()[key];
8643
+ return Array.isArray(v) && v.includes(opt);
8644
+ }
8645
+ toggleFilterOption(key, opt) {
8646
+ this.filters.update((f) => {
8647
+ const cur = Array.isArray(f[key]) ? [...f[key]] : [];
8648
+ const i = cur.indexOf(opt);
8649
+ if (i >= 0)
8650
+ cur.splice(i, 1);
8651
+ else
8652
+ cur.push(opt);
8653
+ return { ...f, [key]: cur };
8654
+ });
8655
+ }
8656
+ clearFilter(key) {
8657
+ this.filters.update((f) => {
8658
+ const next = { ...f };
8659
+ delete next[key];
8660
+ return next;
8661
+ });
8662
+ }
8663
+ onDocClickFilter(event) {
8664
+ if (!this.filterOpenKey())
8665
+ return;
8666
+ const t = event.target;
8667
+ if (t.closest('.strct-dg__filterpanel') || t.closest('.strct-dg__filterbtn'))
8668
+ return;
8669
+ this.filterOpenKey.set(null);
8670
+ }
8671
+ onEscapeFilter() {
8672
+ this.filterOpenKey.set(null);
8673
+ }
8674
+ // ── Tree-grid state ────────────────────────────────────────────
8675
+ expandedTree = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedTree" }] : /* istanbul ignore next */ []));
8676
+ treeDepth(row) {
8677
+ return this.treeFlat().meta.get(this.rowKey(row))?.depth ?? 0;
8678
+ }
8679
+ treeHasChildren(row) {
8680
+ return this.treeFlat().meta.get(this.rowKey(row))?.hasChildren ?? false;
8681
+ }
8682
+ treeExpanded(row) {
8683
+ return this.treeFlat().meta.get(this.rowKey(row))?.expanded ?? false;
8684
+ }
8685
+ /** Expand / collapse one tree row. */
8686
+ toggleTreeRow(row) {
8687
+ const key = this.rowKey(row);
8688
+ const next = new Set(this.expandedTree());
8689
+ if (next.has(key))
8690
+ next.delete(key);
8691
+ else
8692
+ next.add(key);
8693
+ this.expandedTree.set(next);
8694
+ }
8695
+ // ── Inline cell editing ────────────────────────────────────────
8696
+ hostRef = inject(ElementRef);
8697
+ editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
8698
+ isEditingCell(row, col) {
8699
+ const e = this.editing();
8700
+ return !!e && e.rowKey === this.rowKey(row) && e.colKey === col.key;
8701
+ }
8702
+ startEdit(row, col) {
8703
+ this.editing.set({ rowKey: this.rowKey(row), colKey: col.key });
8704
+ setTimeout(() => this.hostRef.nativeElement.querySelector('.strct-dg__editinput')?.focus());
8705
+ }
8706
+ commitEdit(row, col, event) {
8707
+ if (!this.isEditingCell(row, col))
8708
+ return; // Escape/Enter already closed it
8709
+ const value = event.target.value;
8710
+ this.editing.set(null);
8711
+ const previous = row[col.key];
8712
+ if (String(previous ?? '') !== value)
8713
+ this.cellEdit.emit({ row, column: col, value, previous });
8714
+ }
8715
+ cancelEdit(event) {
8716
+ event?.stopPropagation();
8717
+ this.editing.set(null);
8718
+ }
8719
+ /** Active (non-empty) filter entries. */
8720
+ activeFilters = computed(() => Object.entries(this.filters()).filter(([, v]) => Array.isArray(v) ? v.length > 0 : String(v).trim() !== ''), ...(ngDevMode ? [{ debugName: "activeFilters" }] : /* istanbul ignore next */ []));
8721
+ quickTerm = computed(() => this.quickFilter().trim().toLowerCase(), ...(ngDevMode ? [{ debugName: "quickTerm" }] : /* istanbul ignore next */ []));
8722
+ matchesQuick(row) {
8723
+ const term = this.quickTerm();
8724
+ if (!term)
8725
+ return true;
8726
+ const fields = this.quickFilterFields() ?? this.columns().map((c) => c.key);
8727
+ return fields.some((key) => String(row[key] ?? '')
8728
+ .toLowerCase()
8729
+ .includes(term));
8730
+ }
8731
+ matchesFilters(row) {
8732
+ return (this.matchesQuick(row) &&
8733
+ this.activeFilters().every(([key, v]) => Array.isArray(v)
8734
+ ? v.some((opt) => opt === row[key])
8735
+ : String(row[key] ?? '')
8736
+ .toLowerCase()
8737
+ .includes(String(v).trim().toLowerCase())));
8738
+ }
8739
+ /** Rows surviving the quick + per-column filters (client-side modes only). */
8740
+ filteredRows = computed(() => {
8741
+ if (this.lazy() || (!this.activeFilters().length && !this.quickTerm()))
8742
+ return this.rows();
8743
+ return this.rows().filter((r) => this.matchesFilters(r));
8744
+ }, ...(ngDevMode ? [{ debugName: "filteredRows" }] : /* istanbul ignore next */ []));
8745
+ /** "12 / 480" — shown while any client-side filtering narrows the set. */
8746
+ filterNote = computed(() => {
8747
+ if (this.lazy() || (!this.activeFilters().length && !this.quickTerm()))
8748
+ return null;
8749
+ return `${this.sorted().length} / ${this.rows().length}`;
8750
+ }, ...(ngDevMode ? [{ debugName: "filterNote" }] : /* istanbul ignore next */ []));
8751
+ /** Tree mode: flattened visible rows + per-row depth/children/expanded meta. */
8752
+ treeFlat = computed(() => {
8753
+ const ck = this.childrenKey();
8754
+ const { key, dir } = this.sort();
8755
+ const sign = dir === 'asc' ? 1 : -1;
8756
+ const sortRows = (rs) => {
8757
+ const c = [...rs];
8758
+ if (key)
8759
+ c.sort((a, b) => sign * this.compare(a[key], b[key]));
8760
+ return c;
8761
+ };
8762
+ const filtering = this.activeFilters().length > 0 || this.quickTerm() !== '';
8763
+ const kidsOf = (r) => r[ck] ?? [];
8764
+ const survives = (r) => this.matchesFilters(r) || kidsOf(r).some(survives);
8765
+ const meta = new Map();
8766
+ const rowsOut = [];
8767
+ const walk = (rs, depth) => {
8768
+ for (const r of sortRows(rs)) {
8769
+ const kids = filtering ? kidsOf(r).filter(survives) : kidsOf(r);
8770
+ // Filtering force-expands so matches are never hidden under a fold.
8771
+ const expanded = filtering ? true : this.expandedTree().has(this.rowKey(r));
8772
+ meta.set(this.rowKey(r), { depth, hasChildren: kids.length > 0, expanded });
8773
+ rowsOut.push(r);
8774
+ if (kids.length && expanded)
8775
+ walk(kids, depth + 1);
8776
+ }
8777
+ };
8778
+ walk(filtering ? this.rows().filter(survives) : this.rows(), 0);
8779
+ return { rows: rowsOut, meta };
8780
+ }, ...(ngDevMode ? [{ debugName: "treeFlat" }] : /* istanbul ignore next */ []));
8565
8781
  sorted = computed(() => {
8566
8782
  // Server-side mode: rows arrive already ordered / sliced — never touch them.
8567
8783
  if (this.lazy())
8568
8784
  return this.rows();
8785
+ if (this.childrenKey())
8786
+ return this.treeFlat().rows;
8569
8787
  const { key, dir } = this.sort();
8570
- const data = [...this.rows()];
8788
+ const data = [...this.filteredRows()];
8571
8789
  if (!key)
8572
8790
  return data;
8573
8791
  const sign = dir === 'asc' ? 1 : -1;
@@ -8689,6 +8907,12 @@ class StrctDatagrid {
8689
8907
  if (this.page() > pageCount)
8690
8908
  this.page.set(pageCount);
8691
8909
  });
8910
+ // A changed filter always restarts from page 1.
8911
+ effect(() => {
8912
+ this.filters();
8913
+ this.quickTerm();
8914
+ untracked(() => this.page.set(1));
8915
+ });
8692
8916
  // Seed the selection from initialSelection (re-runs only when the input changes,
8693
8917
  // so a user's own toggles afterward are untouched).
8694
8918
  effect(() => {
@@ -8707,6 +8931,8 @@ class StrctDatagrid {
8707
8931
  pageSize: this.pageSize(),
8708
8932
  sortKey: this.sort().key,
8709
8933
  sortDir: this.sort().dir,
8934
+ filters: this.filters(),
8935
+ quickFilter: this.quickTerm(),
8710
8936
  };
8711
8937
  const key = JSON.stringify(state);
8712
8938
  untracked(() => {
@@ -8953,9 +9179,23 @@ class StrctDatagrid {
8953
9179
  return String(a ?? '').localeCompare(String(b ?? ''), undefined, { numeric: true });
8954
9180
  }
8955
9181
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
8956
- 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: `
8957
- @if (actionBarDef()) {
8958
- <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
9182
+ 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 }, quickFilter: { classPropertyName: "quickFilter", publicName: "quickFilter", isSignal: true, isRequired: false, transformFunction: null }, quickFilterFields: { classPropertyName: "quickFilterFields", publicName: "quickFilterFields", isSignal: true, isRequired: false, transformFunction: null }, quickFilterable: { classPropertyName: "quickFilterable", publicName: "quickFilterable", isSignal: true, isRequired: false, transformFunction: null }, childrenKey: { classPropertyName: "childrenKey", publicName: "childrenKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnState: "columnStateChange", filters: "filtersChange", quickFilter: "quickFilterChange", 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: `
9183
+ @if (actionBarDef() || quickFilterable()) {
9184
+ <div class="strct-dg__toolbar">
9185
+ @if (quickFilterable()) {
9186
+ <strct-searchbox
9187
+ class="strct-dg__quickfilter"
9188
+ [placeholder]="L().quickFilter"
9189
+ [ariaLabel]="L().quickFilter"
9190
+ [value]="quickFilter()"
9191
+ (valueChange)="quickFilter.set($event)"
9192
+ />
9193
+ @if (filterNote(); as note) {
9194
+ <span class="strct-dg__filternote">{{ note }}</span>
9195
+ }
9196
+ }
9197
+ <ng-content select="[strctDatagridActionBar]" />
9198
+ </div>
8959
9199
  }
8960
9200
 
8961
9201
  <div class="strct-dg__layout" [class.strct-dg__layout--paned]="paneOpen()">
@@ -8967,7 +9207,7 @@ class StrctDatagrid {
8967
9207
  [style.max-height.px]="virtual() ? viewportHeight() : null"
8968
9208
  (scroll)="virtual() && onScroll($event)"
8969
9209
  >
8970
- <table class="strct-dg">
9210
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
8971
9211
  <thead>
8972
9212
  <tr>
8973
9213
  @if (canDetail()) {
@@ -9027,6 +9267,62 @@ class StrctDatagrid {
9027
9267
  [size]="13"
9028
9268
  />
9029
9269
  }
9270
+ @if (col.filterable || col.filterOptions) {
9271
+ <button
9272
+ #fbtn
9273
+ type="button"
9274
+ class="strct-dg__filterbtn"
9275
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9276
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9277
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9278
+ (click)="toggleFilterPanel(col.key, $event)"
9279
+ (keydown.enter)="$event.stopPropagation()"
9280
+ (keydown.space)="$event.stopPropagation()"
9281
+ >
9282
+ <strct-icon name="filter" [size]="12" />
9283
+ </button>
9284
+ @if (filterOpenKey() === col.key) {
9285
+ <div
9286
+ class="strct-dg__filterpanel"
9287
+ role="dialog"
9288
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9289
+ [strctOverlay]="fbtn"
9290
+ strctOverlayPlacement="bottom-start"
9291
+ (click)="$event.stopPropagation()"
9292
+ (keydown.enter)="$event.stopPropagation()"
9293
+ (keydown.space)="$event.stopPropagation()"
9294
+ >
9295
+ @if (col.filterOptions; as opts) {
9296
+ @for (opt of opts; track $index) {
9297
+ <label class="strct-dg__filteropt">
9298
+ <input
9299
+ type="checkbox"
9300
+ [checked]="optionChecked(col.key, opt)"
9301
+ (change)="toggleFilterOption(col.key, opt)"
9302
+ />
9303
+ <span>{{ opt }}</span>
9304
+ </label>
9305
+ }
9306
+ } @else {
9307
+ <input
9308
+ class="strct-dg__filterinput"
9309
+ type="text"
9310
+ [attr.placeholder]="L().filterPlaceholder"
9311
+ [value]="textFilter(col.key)"
9312
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9313
+ />
9314
+ }
9315
+ <button
9316
+ type="button"
9317
+ class="strct-dg__filterclear"
9318
+ [disabled]="!hasFilter(col.key)"
9319
+ (click)="clearFilter(col.key)"
9320
+ >
9321
+ {{ L().clearFilter }}
9322
+ </button>
9323
+ </div>
9324
+ }
9325
+ }
9030
9326
  </span>
9031
9327
  @if (resizable() && col.resizable !== false) {
9032
9328
  <div
@@ -9097,6 +9393,10 @@ class StrctDatagrid {
9097
9393
  <tr
9098
9394
  [class.strct-dg__row--selected]="isSelected(row)"
9099
9395
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9396
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9397
+ [attr.aria-expanded]="
9398
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9399
+ "
9100
9400
  >
9101
9401
  @if (canDetail()) {
9102
9402
  <td
@@ -9147,14 +9447,51 @@ class StrctDatagrid {
9147
9447
  />
9148
9448
  </td>
9149
9449
  }
9150
- @for (col of visibleColumns(); track col.key) {
9450
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9151
9451
  <td
9152
9452
  [style.text-align]="col.align ?? 'start'"
9153
9453
  [class.strct-dg__cell--sticky]="isSticky(col)"
9154
9454
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9455
+ [class.strct-dg__cell--editable]="col.editable"
9155
9456
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9457
+ (dblclick)="col.editable && startEdit(row, col)"
9156
9458
  >
9157
- @if (cellTemplate(col.key); as tpl) {
9459
+ @if (childrenKey() && colIdx === 0) {
9460
+ <span
9461
+ class="strct-dg__treepad"
9462
+ [style.width.px]="treeDepth(row) * 18"
9463
+ aria-hidden="true"
9464
+ ></span>
9465
+ @if (treeHasChildren(row)) {
9466
+ <button
9467
+ type="button"
9468
+ class="strct-dg__treebtn"
9469
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9470
+ [attr.aria-expanded]="treeExpanded(row)"
9471
+ [attr.aria-label]="L().toggleChildren"
9472
+ (click)="toggleTreeRow(row)"
9473
+ >
9474
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9475
+ </button>
9476
+ } @else {
9477
+ <span
9478
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9479
+ aria-hidden="true"
9480
+ ></span>
9481
+ }
9482
+ }
9483
+ @if (isEditingCell(row, col)) {
9484
+ <input
9485
+ class="strct-dg__editinput"
9486
+ type="text"
9487
+ [value]="row[col.key] ?? ''"
9488
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9489
+ (keydown.enter)="commitEdit(row, col, $event)"
9490
+ (keydown.escape)="cancelEdit($event)"
9491
+ (blur)="commitEdit(row, col, $event)"
9492
+ (click)="$event.stopPropagation()"
9493
+ />
9494
+ } @else if (cellTemplate(col.key); as tpl) {
9158
9495
  <ng-container
9159
9496
  [ngTemplateOutlet]="tpl"
9160
9497
  [ngTemplateOutletContext]="{
@@ -9292,7 +9629,7 @@ class StrctDatagrid {
9292
9629
  </div>
9293
9630
  </div>
9294
9631
  }
9295
- `, 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 });
9632
+ `, 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__quickfilter{min-width:180px;max-width:260px}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.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"] }, { 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 });
9296
9633
  }
9297
9634
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, decorators: [{
9298
9635
  type: Component,
@@ -9303,9 +9640,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9303
9640
  StrctCheckbox,
9304
9641
  StrctButton,
9305
9642
  StrctButtonGroup,
9643
+ StrctOverlay,
9644
+ StrctSearchbox,
9306
9645
  ], template: `
9307
- @if (actionBarDef()) {
9308
- <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
9646
+ @if (actionBarDef() || quickFilterable()) {
9647
+ <div class="strct-dg__toolbar">
9648
+ @if (quickFilterable()) {
9649
+ <strct-searchbox
9650
+ class="strct-dg__quickfilter"
9651
+ [placeholder]="L().quickFilter"
9652
+ [ariaLabel]="L().quickFilter"
9653
+ [value]="quickFilter()"
9654
+ (valueChange)="quickFilter.set($event)"
9655
+ />
9656
+ @if (filterNote(); as note) {
9657
+ <span class="strct-dg__filternote">{{ note }}</span>
9658
+ }
9659
+ }
9660
+ <ng-content select="[strctDatagridActionBar]" />
9661
+ </div>
9309
9662
  }
9310
9663
 
9311
9664
  <div class="strct-dg__layout" [class.strct-dg__layout--paned]="paneOpen()">
@@ -9317,7 +9670,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9317
9670
  [style.max-height.px]="virtual() ? viewportHeight() : null"
9318
9671
  (scroll)="virtual() && onScroll($event)"
9319
9672
  >
9320
- <table class="strct-dg">
9673
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
9321
9674
  <thead>
9322
9675
  <tr>
9323
9676
  @if (canDetail()) {
@@ -9377,6 +9730,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9377
9730
  [size]="13"
9378
9731
  />
9379
9732
  }
9733
+ @if (col.filterable || col.filterOptions) {
9734
+ <button
9735
+ #fbtn
9736
+ type="button"
9737
+ class="strct-dg__filterbtn"
9738
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9739
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9740
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9741
+ (click)="toggleFilterPanel(col.key, $event)"
9742
+ (keydown.enter)="$event.stopPropagation()"
9743
+ (keydown.space)="$event.stopPropagation()"
9744
+ >
9745
+ <strct-icon name="filter" [size]="12" />
9746
+ </button>
9747
+ @if (filterOpenKey() === col.key) {
9748
+ <div
9749
+ class="strct-dg__filterpanel"
9750
+ role="dialog"
9751
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9752
+ [strctOverlay]="fbtn"
9753
+ strctOverlayPlacement="bottom-start"
9754
+ (click)="$event.stopPropagation()"
9755
+ (keydown.enter)="$event.stopPropagation()"
9756
+ (keydown.space)="$event.stopPropagation()"
9757
+ >
9758
+ @if (col.filterOptions; as opts) {
9759
+ @for (opt of opts; track $index) {
9760
+ <label class="strct-dg__filteropt">
9761
+ <input
9762
+ type="checkbox"
9763
+ [checked]="optionChecked(col.key, opt)"
9764
+ (change)="toggleFilterOption(col.key, opt)"
9765
+ />
9766
+ <span>{{ opt }}</span>
9767
+ </label>
9768
+ }
9769
+ } @else {
9770
+ <input
9771
+ class="strct-dg__filterinput"
9772
+ type="text"
9773
+ [attr.placeholder]="L().filterPlaceholder"
9774
+ [value]="textFilter(col.key)"
9775
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9776
+ />
9777
+ }
9778
+ <button
9779
+ type="button"
9780
+ class="strct-dg__filterclear"
9781
+ [disabled]="!hasFilter(col.key)"
9782
+ (click)="clearFilter(col.key)"
9783
+ >
9784
+ {{ L().clearFilter }}
9785
+ </button>
9786
+ </div>
9787
+ }
9788
+ }
9380
9789
  </span>
9381
9790
  @if (resizable() && col.resizable !== false) {
9382
9791
  <div
@@ -9447,6 +9856,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9447
9856
  <tr
9448
9857
  [class.strct-dg__row--selected]="isSelected(row)"
9449
9858
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9859
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9860
+ [attr.aria-expanded]="
9861
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9862
+ "
9450
9863
  >
9451
9864
  @if (canDetail()) {
9452
9865
  <td
@@ -9497,14 +9910,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9497
9910
  />
9498
9911
  </td>
9499
9912
  }
9500
- @for (col of visibleColumns(); track col.key) {
9913
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9501
9914
  <td
9502
9915
  [style.text-align]="col.align ?? 'start'"
9503
9916
  [class.strct-dg__cell--sticky]="isSticky(col)"
9504
9917
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9918
+ [class.strct-dg__cell--editable]="col.editable"
9505
9919
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9920
+ (dblclick)="col.editable && startEdit(row, col)"
9506
9921
  >
9507
- @if (cellTemplate(col.key); as tpl) {
9922
+ @if (childrenKey() && colIdx === 0) {
9923
+ <span
9924
+ class="strct-dg__treepad"
9925
+ [style.width.px]="treeDepth(row) * 18"
9926
+ aria-hidden="true"
9927
+ ></span>
9928
+ @if (treeHasChildren(row)) {
9929
+ <button
9930
+ type="button"
9931
+ class="strct-dg__treebtn"
9932
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9933
+ [attr.aria-expanded]="treeExpanded(row)"
9934
+ [attr.aria-label]="L().toggleChildren"
9935
+ (click)="toggleTreeRow(row)"
9936
+ >
9937
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9938
+ </button>
9939
+ } @else {
9940
+ <span
9941
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9942
+ aria-hidden="true"
9943
+ ></span>
9944
+ }
9945
+ }
9946
+ @if (isEditingCell(row, col)) {
9947
+ <input
9948
+ class="strct-dg__editinput"
9949
+ type="text"
9950
+ [value]="row[col.key] ?? ''"
9951
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9952
+ (keydown.enter)="commitEdit(row, col, $event)"
9953
+ (keydown.escape)="cancelEdit($event)"
9954
+ (blur)="commitEdit(row, col, $event)"
9955
+ (click)="$event.stopPropagation()"
9956
+ />
9957
+ } @else if (cellTemplate(col.key); as tpl) {
9508
9958
  <ng-container
9509
9959
  [ngTemplateOutlet]="tpl"
9510
9960
  [ngTemplateOutletContext]="{
@@ -9648,8 +10098,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9648
10098
  '[class.strct-dg-host--singleline]': 'singleLine()',
9649
10099
  '[class.strct-dg-host--virtual]': 'virtual()',
9650
10100
  '[class.strct-dg-host--sticky]': 'stickyActive()',
9651
- }, 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"] }]
9652
- }], 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 }] }] } });
10101
+ }, 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__quickfilter{min-width:180px;max-width:260px}.strct-dg__filternote{font-size:11.5px;color:var(--t3);font-variant-numeric:tabular-nums;white-space:nowrap}.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"] }]
10102
+ }], 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"] }], quickFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilter", required: false }] }, { type: i0.Output, args: ["quickFilterChange"] }], quickFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilterFields", required: false }] }], quickFilterable: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickFilterable", required: false }] }], 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: [{
10103
+ type: HostListener,
10104
+ args: ['document:click', ['$event']]
10105
+ }], onEscapeFilter: [{
10106
+ type: HostListener,
10107
+ args: ['document:keydown.escape']
10108
+ }] } });
9653
10109
 
9654
10110
  /** Vertical timeline container. Wraps `<strct-timeline-item>` children. */
9655
10111
  class StrctTimeline {
@@ -12759,6 +13215,2340 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12759
13215
  `, 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"] }]
12760
13216
  }], 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 }] }] } });
12761
13217
 
13218
+ /**
13219
+ * Unit formatting for infrastructure consoles — bytes, rates, durations and
13220
+ * SI magnitudes, as pure functions plus template pipes. The functions are the
13221
+ * API charts / grids can call from TS; the pipes are the template sugar:
13222
+ *
13223
+ * {{ vm.memBytes | strctBytes }} → 1.5 GiB
13224
+ * {{ nic.rxBps | strctRate }} → 2.4 Gbit/s
13225
+ * {{ task.elapsedMs | strctDuration }} → 2h 14m
13226
+ * {{ cluster.iops | strctSi : 'IOPS' }} → 12.4k IOPS
13227
+ */
13228
+ const BINARY_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
13229
+ const DECIMAL_UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB'];
13230
+ const SI_SUFFIXES = ['', 'k', 'M', 'G', 'T', 'P'];
13231
+ function trimZeros(v) {
13232
+ return v.includes('.') ? v.replace(/\.?0+$/, '') : v;
13233
+ }
13234
+ /**
13235
+ * Format a byte count. Binary (KiB, 1024) by default — the storage/memory
13236
+ * convention consoles actually mean; pass `binary: false` for decimal (kB).
13237
+ */
13238
+ function strctFormatBytes(bytes, options = {}) {
13239
+ if (!Number.isFinite(bytes))
13240
+ return '—';
13241
+ const { binary = true, digits = 1 } = options;
13242
+ const base = binary ? 1024 : 1000;
13243
+ const units = binary ? BINARY_UNITS : DECIMAL_UNITS;
13244
+ const neg = bytes < 0 ? '-' : '';
13245
+ let v = Math.abs(bytes);
13246
+ let i = 0;
13247
+ while (v >= base && i < units.length - 1) {
13248
+ v /= base;
13249
+ i++;
13250
+ }
13251
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13252
+ }
13253
+ /**
13254
+ * Format a data rate from bits per second (network convention). Pass bytes/s
13255
+ * through `strctFormatBytes(...) + '/s'` instead if you mean throughput.
13256
+ */
13257
+ function strctFormatRate(bitsPerSecond, options = {}) {
13258
+ if (!Number.isFinite(bitsPerSecond))
13259
+ return '—';
13260
+ const { digits = 1 } = options;
13261
+ const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s', 'Tbit/s'];
13262
+ const neg = bitsPerSecond < 0 ? '-' : '';
13263
+ let v = Math.abs(bitsPerSecond);
13264
+ let i = 0;
13265
+ while (v >= 1000 && i < units.length - 1) {
13266
+ v /= 1000;
13267
+ i++;
13268
+ }
13269
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13270
+ }
13271
+ /**
13272
+ * Format a duration in ms as the two most significant units: "2h 14m",
13273
+ * "3d 6h", "45s", "820ms". Sub-second stays in ms; `maxUnits` widens it.
13274
+ */
13275
+ function strctFormatDuration(ms, options = {}) {
13276
+ if (!Number.isFinite(ms))
13277
+ return '—';
13278
+ const { maxUnits = 2 } = options;
13279
+ const neg = ms < 0 ? '-' : '';
13280
+ let v = Math.abs(ms);
13281
+ if (v < 1000)
13282
+ return `${neg}${Math.round(v)}ms`;
13283
+ const units = [
13284
+ ['d', 86_400_000],
13285
+ ['h', 3_600_000],
13286
+ ['m', 60_000],
13287
+ ['s', 1000],
13288
+ ];
13289
+ const parts = [];
13290
+ for (const [label, size] of units) {
13291
+ if (parts.length >= maxUnits)
13292
+ break;
13293
+ const n = Math.floor(v / size);
13294
+ if (n > 0 || parts.length > 0) {
13295
+ if (n > 0)
13296
+ parts.push(`${n}${label}`);
13297
+ v -= n * size;
13298
+ }
13299
+ }
13300
+ return neg + (parts.length ? parts.join(' ') : '0s');
13301
+ }
13302
+ /** Format a count with SI magnitude suffixes: 12400 → "12.4k". */
13303
+ function strctFormatSi(value, options = {}) {
13304
+ if (!Number.isFinite(value))
13305
+ return '—';
13306
+ const { digits = 1, unit = '' } = options;
13307
+ const neg = value < 0 ? '-' : '';
13308
+ let v = Math.abs(value);
13309
+ let i = 0;
13310
+ while (v >= 1000 && i < SI_SUFFIXES.length - 1) {
13311
+ v /= 1000;
13312
+ i++;
13313
+ }
13314
+ const num = `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))}${SI_SUFFIXES[i]}`;
13315
+ return unit ? `${num} ${unit}` : num;
13316
+ }
13317
+ /** `{{ bytes | strctBytes }}` / `{{ bytes | strctBytes : false }}` (decimal). */
13318
+ class StrctBytesPipe {
13319
+ transform(value, binary = true, digits = 1) {
13320
+ return strctFormatBytes(value, { binary, digits });
13321
+ }
13322
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13323
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, isStandalone: true, name: "strctBytes" });
13324
+ }
13325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, decorators: [{
13326
+ type: Pipe,
13327
+ args: [{ name: 'strctBytes' }]
13328
+ }] });
13329
+ /** `{{ bps | strctRate }}` — bits per second with kbit/Mbit/Gbit steps. */
13330
+ class StrctRatePipe {
13331
+ transform(value, digits = 1) {
13332
+ return strctFormatRate(value, { digits });
13333
+ }
13334
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13335
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, isStandalone: true, name: "strctRate" });
13336
+ }
13337
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, decorators: [{
13338
+ type: Pipe,
13339
+ args: [{ name: 'strctRate' }]
13340
+ }] });
13341
+ /** `{{ ms | strctDuration }}` — "2h 14m"; optional max unit count. */
13342
+ class StrctDurationPipe {
13343
+ transform(value, maxUnits = 2) {
13344
+ return strctFormatDuration(value, { maxUnits });
13345
+ }
13346
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13347
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, isStandalone: true, name: "strctDuration" });
13348
+ }
13349
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, decorators: [{
13350
+ type: Pipe,
13351
+ args: [{ name: 'strctDuration' }]
13352
+ }] });
13353
+ /** `{{ n | strctSi : 'IOPS' }}` — SI magnitude with an optional unit label. */
13354
+ class StrctSiPipe {
13355
+ transform(value, unit = '', digits = 1) {
13356
+ return strctFormatSi(value, { unit, digits });
13357
+ }
13358
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13359
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, isStandalone: true, name: "strctSi" });
13360
+ }
13361
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, decorators: [{
13362
+ type: Pipe,
13363
+ args: [{ name: 'strctSi' }]
13364
+ }] });
13365
+
13366
+ /** The Grafana-conventional quick ranges consoles expect. */
13367
+ const STRCT_TIME_RANGE_PRESETS = [
13368
+ { id: '15m', label: 'Last 15 minutes', ms: 15 * 60_000 },
13369
+ { id: '1h', label: 'Last 1 hour', ms: 3_600_000 },
13370
+ { id: '6h', label: 'Last 6 hours', ms: 6 * 3_600_000 },
13371
+ { id: '24h', label: 'Last 24 hours', ms: 24 * 3_600_000 },
13372
+ { id: '7d', label: 'Last 7 days', ms: 7 * 86_400_000 },
13373
+ { id: '30d', label: 'Last 30 days', ms: 30 * 86_400_000 },
13374
+ ];
13375
+ /**
13376
+ * Monitoring time-range picker — the "Last 1 hour ▾" control charts hang off:
13377
+ * quick relative ranges plus an absolute from/to editor, in one popover.
13378
+ *
13379
+ * <strct-time-range [(range)]="range" />
13380
+ *
13381
+ * Picking a preset resolves it against "now" immediately and stamps
13382
+ * `presetId`, so a consumer can re-resolve the same preset on its own refresh
13383
+ * tick. The popover reuses `strct-dropdown`'s dialog semantics: inner clicks
13384
+ * keep it open, outside click / Escape dismiss.
13385
+ */
13386
+ class StrctTimeRangePicker {
13387
+ /** The selected window, two-way. Null until the user (or you) picks one. */
13388
+ range = model(null, ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
13389
+ /** Quick ranges shown in the left column. */
13390
+ presets = input(STRCT_TIME_RANGE_PRESETS, ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
13391
+ /** Popover alignment, as in strct-dropdown. */
13392
+ align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
13393
+ /** Trigger size — match it to the toolbar's buttons (sm strips). */
13394
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
13395
+ /** Localizable strings. */
13396
+ dialogLabel = input('Time range', ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
13397
+ quickLabel = input('Quick ranges', ...(ngDevMode ? [{ debugName: "quickLabel" }] : /* istanbul ignore next */ []));
13398
+ absoluteLabel = input('Absolute range', ...(ngDevMode ? [{ debugName: "absoluteLabel" }] : /* istanbul ignore next */ []));
13399
+ fromLabel = input('From', ...(ngDevMode ? [{ debugName: "fromLabel" }] : /* istanbul ignore next */ []));
13400
+ toLabel = input('To', ...(ngDevMode ? [{ debugName: "toLabel" }] : /* istanbul ignore next */ []));
13401
+ applyLabel = input('Apply', ...(ngDevMode ? [{ debugName: "applyLabel" }] : /* istanbul ignore next */ []));
13402
+ invalidLabel = input('"From" must be before "To".', ...(ngDevMode ? [{ debugName: "invalidLabel" }] : /* istanbul ignore next */ []));
13403
+ placeholderLabel = input('Select time range', ...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
13404
+ /** Fires on every commit (preset pick or absolute apply). */
13405
+ applied = output();
13406
+ dd = viewChild.required(StrctDropdown);
13407
+ // Absolute-editor draft (datetime-local strings), seeded from the range.
13408
+ draftFrom = signal('', ...(ngDevMode ? [{ debugName: "draftFrom" }] : /* istanbul ignore next */ []));
13409
+ draftTo = signal('', ...(ngDevMode ? [{ debugName: "draftTo" }] : /* istanbul ignore next */ []));
13410
+ draftError = computed(() => {
13411
+ const f = this.draftFrom();
13412
+ const t = this.draftTo();
13413
+ if (!f || !t)
13414
+ return false;
13415
+ return new Date(f).getTime() >= new Date(t).getTime();
13416
+ }, ...(ngDevMode ? [{ debugName: "draftError" }] : /* istanbul ignore next */ []));
13417
+ triggerLabel = computed(() => {
13418
+ const r = this.range();
13419
+ if (!r)
13420
+ return this.placeholderLabel();
13421
+ const preset = this.presets().find((p) => p.id === r.presetId);
13422
+ if (preset)
13423
+ return preset.label;
13424
+ return `${this.formatStamp(r.from)} → ${this.formatStamp(r.to)}`;
13425
+ }, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
13426
+ pickPreset(p) {
13427
+ const to = new Date();
13428
+ const next = { from: new Date(to.getTime() - p.ms), to, presetId: p.id };
13429
+ this.range.set(next);
13430
+ this.applied.emit(next);
13431
+ this.seedDraft(next);
13432
+ }
13433
+ applyAbsolute() {
13434
+ const f = this.draftFrom();
13435
+ const t = this.draftTo();
13436
+ if (!f || !t || this.draftError())
13437
+ return false;
13438
+ const next = { from: new Date(f), to: new Date(t) };
13439
+ this.range.set(next);
13440
+ this.applied.emit(next);
13441
+ return true;
13442
+ }
13443
+ /** Re-resolve a preset range against "now" — call from your refresh tick. */
13444
+ refresh() {
13445
+ const r = this.range();
13446
+ const preset = r && this.presets().find((p) => p.id === r.presetId);
13447
+ if (!preset)
13448
+ return;
13449
+ this.pickPreset(preset);
13450
+ }
13451
+ seedDraft(r) {
13452
+ this.draftFrom.set(this.toLocal(r.from));
13453
+ this.draftTo.set(this.toLocal(r.to));
13454
+ }
13455
+ toLocal(d) {
13456
+ const pad = (n) => String(n).padStart(2, '0');
13457
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
13458
+ }
13459
+ formatStamp(d) {
13460
+ const pad = (n) => String(n).padStart(2, '0');
13461
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
13462
+ }
13463
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
13464
+ 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 }, size: { classPropertyName: "size", publicName: "size", 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: `
13465
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13466
+ <button
13467
+ strct-button
13468
+ variant="neutral"
13469
+ [size]="size()"
13470
+ strctDropdownTrigger
13471
+ class="strct-tr__trigger"
13472
+ >
13473
+ <strct-icon name="clock" [size]="13" />
13474
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13475
+ <strct-icon name="chevronDown" [size]="12" />
13476
+ </button>
13477
+
13478
+ <div class="strct-tr__panel">
13479
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13480
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13481
+ @for (p of presets(); track p.id) {
13482
+ <button
13483
+ type="button"
13484
+ class="strct-tr__preset"
13485
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13486
+ [attr.aria-pressed]="range()?.presetId === p.id"
13487
+ (click)="pickPreset(p); dd.close()"
13488
+ >
13489
+ {{ p.label }}
13490
+ </button>
13491
+ }
13492
+ </div>
13493
+
13494
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13495
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13496
+ <label class="strct-tr__field">
13497
+ <span>{{ fromLabel() }}</span>
13498
+ <input
13499
+ type="datetime-local"
13500
+ class="strct-tr__input"
13501
+ [value]="draftFrom()"
13502
+ (input)="draftFrom.set($any($event.target).value)"
13503
+ />
13504
+ </label>
13505
+ <label class="strct-tr__field">
13506
+ <span>{{ toLabel() }}</span>
13507
+ <input
13508
+ type="datetime-local"
13509
+ class="strct-tr__input"
13510
+ [value]="draftTo()"
13511
+ (input)="draftTo.set($any($event.target).value)"
13512
+ />
13513
+ </label>
13514
+ @if (draftError()) {
13515
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13516
+ }
13517
+ <button
13518
+ strct-button
13519
+ variant="primary"
13520
+ size="sm"
13521
+ class="strct-tr__apply"
13522
+ [disabled]="draftError()"
13523
+ (click)="applyAbsolute() && dd.close()"
13524
+ >
13525
+ {{ applyLabel() }}
13526
+ </button>
13527
+ </div>
13528
+ </div>
13529
+ </strct-dropdown>
13530
+ `, 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 });
13531
+ }
13532
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, decorators: [{
13533
+ type: Component,
13534
+ args: [{ selector: 'strct-time-range', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctDropdown, StrctDropdownTrigger, StrctIcon], template: `
13535
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13536
+ <button
13537
+ strct-button
13538
+ variant="neutral"
13539
+ [size]="size()"
13540
+ strctDropdownTrigger
13541
+ class="strct-tr__trigger"
13542
+ >
13543
+ <strct-icon name="clock" [size]="13" />
13544
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13545
+ <strct-icon name="chevronDown" [size]="12" />
13546
+ </button>
13547
+
13548
+ <div class="strct-tr__panel">
13549
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13550
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13551
+ @for (p of presets(); track p.id) {
13552
+ <button
13553
+ type="button"
13554
+ class="strct-tr__preset"
13555
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13556
+ [attr.aria-pressed]="range()?.presetId === p.id"
13557
+ (click)="pickPreset(p); dd.close()"
13558
+ >
13559
+ {{ p.label }}
13560
+ </button>
13561
+ }
13562
+ </div>
13563
+
13564
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13565
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13566
+ <label class="strct-tr__field">
13567
+ <span>{{ fromLabel() }}</span>
13568
+ <input
13569
+ type="datetime-local"
13570
+ class="strct-tr__input"
13571
+ [value]="draftFrom()"
13572
+ (input)="draftFrom.set($any($event.target).value)"
13573
+ />
13574
+ </label>
13575
+ <label class="strct-tr__field">
13576
+ <span>{{ toLabel() }}</span>
13577
+ <input
13578
+ type="datetime-local"
13579
+ class="strct-tr__input"
13580
+ [value]="draftTo()"
13581
+ (input)="draftTo.set($any($event.target).value)"
13582
+ />
13583
+ </label>
13584
+ @if (draftError()) {
13585
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13586
+ }
13587
+ <button
13588
+ strct-button
13589
+ variant="primary"
13590
+ size="sm"
13591
+ class="strct-tr__apply"
13592
+ [disabled]="draftError()"
13593
+ (click)="applyAbsolute() && dd.close()"
13594
+ >
13595
+ {{ applyLabel() }}
13596
+ </button>
13597
+ </div>
13598
+ </div>
13599
+ </strct-dropdown>
13600
+ `, 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"] }]
13601
+ }], 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 }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", 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 }] }] } });
13602
+
13603
+ // SGR code → segment class. Colors map onto chart/status tokens, not raw hues.
13604
+ const ANSI_CLASSES = {
13605
+ 1: 'b',
13606
+ 30: 'c30',
13607
+ 31: 'c31',
13608
+ 32: 'c32',
13609
+ 33: 'c33',
13610
+ 34: 'c34',
13611
+ 35: 'c35',
13612
+ 36: 'c36',
13613
+ 37: 'c37',
13614
+ 90: 'c90',
13615
+ 91: 'c31',
13616
+ 92: 'c32',
13617
+ 93: 'c33',
13618
+ 94: 'c34',
13619
+ 95: 'c35',
13620
+ 96: 'c36',
13621
+ 97: 'c37',
13622
+ };
13623
+ const LEVEL_RE = /\b(ERROR|ERR|FATAL|WARN(?:ING)?|INFO|DEBUG|TRACE)\b/;
13624
+ /**
13625
+ * Virtualized log tail — the `kubectl logs -f` surface as a component:
13626
+ *
13627
+ * <strct-log-viewer [lines]="lines()" [(follow)]="follow" [height]="360" />
13628
+ *
13629
+ * - **Virtualized**: only the visible window plus overscan is in the DOM, so
13630
+ * 100k lines scroll flat.
13631
+ * - **Follow mode** (two-way): sticks to the tail as lines stream in;
13632
+ * scrolling up pauses it, the button (or scrolling back to the bottom)
13633
+ * resumes it — the Grafana/Loki convention.
13634
+ * - **ANSI SGR colors** (16-color + bold) parsed into safe spans — no
13635
+ * innerHTML — and severity tinting from `StrctLogLine.level` or, for plain
13636
+ * strings, auto-detected ERROR/WARN/INFO/DEBUG tokens.
13637
+ */
13638
+ class StrctLogViewer {
13639
+ /** Log lines — plain strings or `{ text, level }` objects. */
13640
+ lines = input([], ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
13641
+ /** Viewport height in px. */
13642
+ height = input(320, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
13643
+ /** Follow the tail as new lines arrive (two-way; pauses on scroll-up). */
13644
+ follow = model(true, ...(ngDevMode ? [{ debugName: "follow" }] : /* istanbul ignore next */ []));
13645
+ /** Soft-wrap long lines instead of horizontal scrolling (two-way). */
13646
+ wrapMode = model(false, ...(ngDevMode ? [{ debugName: "wrapMode" }] : /* istanbul ignore next */ []));
13647
+ /** Auto-detect ERROR/WARN/INFO/DEBUG tokens on plain-string lines. */
13648
+ autoLevel = input(true, { ...(ngDevMode ? { debugName: "autoLevel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13649
+ /** Localizable strings. */
13650
+ title = input('Logs', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13651
+ followLabel = input('Follow', ...(ngDevMode ? [{ debugName: "followLabel" }] : /* istanbul ignore next */ []));
13652
+ wrapLabel = input('Wrap', ...(ngDevMode ? [{ debugName: "wrapLabel" }] : /* istanbul ignore next */ []));
13653
+ linesLabel = input('lines', ...(ngDevMode ? [{ debugName: "linesLabel" }] : /* istanbul ignore next */ []));
13654
+ emptyLabel = input('No log lines.', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
13655
+ scroller = viewChild.required('scroller');
13656
+ scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
13657
+ suppressScrollEvent = false;
13658
+ static LINE_H = 20;
13659
+ static OVERSCAN = 10;
13660
+ normalized = computed(() => this.lines().map((l) => (typeof l === 'string' ? { text: l } : l)), ...(ngDevMode ? [{ debugName: "normalized" }] : /* istanbul ignore next */ []));
13661
+ totalHeight = computed(() => this.normalized().length * StrctLogViewer.LINE_H || StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "totalHeight" }] : /* istanbul ignore next */ []));
13662
+ windowStart = computed(() => {
13663
+ const start = Math.floor(this.scrollTop() / StrctLogViewer.LINE_H) - StrctLogViewer.OVERSCAN;
13664
+ return Math.max(0, start);
13665
+ }, ...(ngDevMode ? [{ debugName: "windowStart" }] : /* istanbul ignore next */ []));
13666
+ offsetY = computed(() => this.windowStart() * StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "offsetY" }] : /* istanbul ignore next */ []));
13667
+ window = computed(() => {
13668
+ const all = this.normalized();
13669
+ const start = this.windowStart();
13670
+ const visible = Math.ceil(this.height() / StrctLogViewer.LINE_H) + StrctLogViewer.OVERSCAN * 2;
13671
+ return all.slice(start, start + visible).map((line, i) => ({
13672
+ index: start + i,
13673
+ level: line.level ?? (this.autoLevel() ? detectLevel(line.text) : 'none'),
13674
+ segments: parseAnsi(line.text),
13675
+ }));
13676
+ }, ...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
13677
+ constructor() {
13678
+ // Stick to the tail whenever content grows while following.
13679
+ effect(() => {
13680
+ this.totalHeight();
13681
+ if (!this.follow())
13682
+ return;
13683
+ const el = this.scroller().nativeElement;
13684
+ // After the DOM height settles.
13685
+ requestAnimationFrame(() => {
13686
+ this.suppressScrollEvent = true;
13687
+ el.scrollTop = el.scrollHeight;
13688
+ this.scrollTop.set(el.scrollTop);
13689
+ });
13690
+ });
13691
+ }
13692
+ onScroll() {
13693
+ const el = this.scroller().nativeElement;
13694
+ this.scrollTop.set(el.scrollTop);
13695
+ if (this.suppressScrollEvent) {
13696
+ this.suppressScrollEvent = false;
13697
+ return;
13698
+ }
13699
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - StrctLogViewer.LINE_H;
13700
+ // Manual scroll: leaving the tail pauses follow, reaching it resumes.
13701
+ if (this.follow() && !atBottom)
13702
+ this.follow.set(false);
13703
+ else if (!this.follow() && atBottom)
13704
+ this.follow.set(true);
13705
+ }
13706
+ toggleFollow() {
13707
+ const next = !this.follow();
13708
+ this.follow.set(next);
13709
+ if (next) {
13710
+ const el = this.scroller().nativeElement;
13711
+ this.suppressScrollEvent = true;
13712
+ el.scrollTop = el.scrollHeight;
13713
+ this.scrollTop.set(el.scrollTop);
13714
+ }
13715
+ }
13716
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
13717
+ 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: `
13718
+ <div class="strct-lv__bar">
13719
+ <span class="strct-lv__title">{{ title() }}</span>
13720
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13721
+ <span class="strct-lv__grow"></span>
13722
+ <button
13723
+ strct-button
13724
+ size="sm"
13725
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13726
+ [attr.aria-pressed]="wrapMode()"
13727
+ (click)="wrapMode.set(!wrapMode())"
13728
+ >
13729
+ {{ wrapLabel() }}
13730
+ </button>
13731
+ <button
13732
+ strct-button
13733
+ size="sm"
13734
+ [variant]="follow() ? 'primary' : 'flat'"
13735
+ [attr.aria-pressed]="follow()"
13736
+ (click)="toggleFollow()"
13737
+ >
13738
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13739
+ {{ followLabel() }}
13740
+ </button>
13741
+ </div>
13742
+ <div
13743
+ #scroller
13744
+ class="strct-lv__scroll"
13745
+ role="log"
13746
+ tabindex="0"
13747
+ [attr.aria-label]="title()"
13748
+ [style.height.px]="height()"
13749
+ (scroll)="onScroll()"
13750
+ >
13751
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13752
+ <div
13753
+ class="strct-lv__window"
13754
+ [class.strct-lv__window--wrap]="wrapMode()"
13755
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13756
+ >
13757
+ @for (line of window(); track line.index) {
13758
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13759
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13760
+ <span class="strct-lv__text">
13761
+ @for (seg of line.segments; track $index) {
13762
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13763
+ }
13764
+ </span>
13765
+ </div>
13766
+ } @empty {
13767
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13768
+ }
13769
+ </div>
13770
+ </div>
13771
+ </div>
13772
+ `, 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 });
13773
+ }
13774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, decorators: [{
13775
+ type: Component,
13776
+ args: [{ selector: 'strct-log-viewer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctIcon], template: `
13777
+ <div class="strct-lv__bar">
13778
+ <span class="strct-lv__title">{{ title() }}</span>
13779
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13780
+ <span class="strct-lv__grow"></span>
13781
+ <button
13782
+ strct-button
13783
+ size="sm"
13784
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13785
+ [attr.aria-pressed]="wrapMode()"
13786
+ (click)="wrapMode.set(!wrapMode())"
13787
+ >
13788
+ {{ wrapLabel() }}
13789
+ </button>
13790
+ <button
13791
+ strct-button
13792
+ size="sm"
13793
+ [variant]="follow() ? 'primary' : 'flat'"
13794
+ [attr.aria-pressed]="follow()"
13795
+ (click)="toggleFollow()"
13796
+ >
13797
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13798
+ {{ followLabel() }}
13799
+ </button>
13800
+ </div>
13801
+ <div
13802
+ #scroller
13803
+ class="strct-lv__scroll"
13804
+ role="log"
13805
+ tabindex="0"
13806
+ [attr.aria-label]="title()"
13807
+ [style.height.px]="height()"
13808
+ (scroll)="onScroll()"
13809
+ >
13810
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13811
+ <div
13812
+ class="strct-lv__window"
13813
+ [class.strct-lv__window--wrap]="wrapMode()"
13814
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13815
+ >
13816
+ @for (line of window(); track line.index) {
13817
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13818
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13819
+ <span class="strct-lv__text">
13820
+ @for (seg of line.segments; track $index) {
13821
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13822
+ }
13823
+ </span>
13824
+ </div>
13825
+ } @empty {
13826
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13827
+ }
13828
+ </div>
13829
+ </div>
13830
+ </div>
13831
+ `, 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"] }]
13832
+ }], 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 }] }] } });
13833
+ function detectLevel(text) {
13834
+ const m = LEVEL_RE.exec(text);
13835
+ if (!m)
13836
+ return 'none';
13837
+ const tok = m[1];
13838
+ if (tok.startsWith('ERR') || tok === 'FATAL')
13839
+ return 'error';
13840
+ if (tok.startsWith('WARN'))
13841
+ return 'warn';
13842
+ if (tok === 'DEBUG' || tok === 'TRACE')
13843
+ return 'debug';
13844
+ return 'info';
13845
+ }
13846
+ /** Parse ANSI SGR sequences into class-tagged segments (16-color + bold). */
13847
+ function parseAnsi(text) {
13848
+ if (!text.includes('\u001b['))
13849
+ return [{ text, cls: 'c37' }];
13850
+ const segments = [];
13851
+ const re = /\u001b\[([0-9;]*)m/g;
13852
+ let last = 0;
13853
+ let current = new Set();
13854
+ let m;
13855
+ while ((m = re.exec(text))) {
13856
+ if (m.index > last) {
13857
+ segments.push({ text: text.slice(last, m.index), cls: clsOf(current) });
13858
+ }
13859
+ for (const raw of (m[1] || '0').split(';')) {
13860
+ const code = Number(raw || '0');
13861
+ if (code === 0)
13862
+ current = new Set();
13863
+ else if (ANSI_CLASSES[code]) {
13864
+ // A new foreground color replaces the previous one; bold stacks.
13865
+ if (code !== 1)
13866
+ for (const c of [...current])
13867
+ if (c !== 'b')
13868
+ current.delete(c);
13869
+ current.add(ANSI_CLASSES[code]);
13870
+ }
13871
+ }
13872
+ last = re.lastIndex;
13873
+ }
13874
+ if (last < text.length)
13875
+ segments.push({ text: text.slice(last), cls: clsOf(current) });
13876
+ return segments.length ? segments : [{ text: '', cls: 'c37' }];
13877
+ }
13878
+ function clsOf(set) {
13879
+ return set.size ? [...set].join(' strct-lv__seg-') : 'c37';
13880
+ }
13881
+
13882
+ /**
13883
+ * Compute a line diff (LCS-based). Exported for programmatic use — change
13884
+ * counts, gating "anything changed?" logic, custom renderings.
13885
+ */
13886
+ function strctComputeDiff(before, after) {
13887
+ const a = before.split('\n');
13888
+ const b = after.split('\n');
13889
+ // Trim common prefix/suffix first — configs mostly match, keeping DP small.
13890
+ let start = 0;
13891
+ while (start < a.length && start < b.length && a[start] === b[start])
13892
+ start++;
13893
+ let endA = a.length;
13894
+ let endB = b.length;
13895
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
13896
+ endA--;
13897
+ endB--;
13898
+ }
13899
+ const midA = a.slice(start, endA);
13900
+ const midB = b.slice(start, endB);
13901
+ const rows = [];
13902
+ for (let i = 0; i < start; i++)
13903
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: i + 1, text: a[i] });
13904
+ // LCS DP on the changed middle; fall back to replace-all when it is huge.
13905
+ if (midA.length * midB.length <= 4_000_000) {
13906
+ const n = midA.length;
13907
+ const m = midB.length;
13908
+ const dp = new Uint32Array((n + 1) * (m + 1));
13909
+ for (let i = n - 1; i >= 0; i--) {
13910
+ for (let j = m - 1; j >= 0; j--) {
13911
+ dp[i * (m + 1) + j] =
13912
+ midA[i] === midB[j]
13913
+ ? dp[(i + 1) * (m + 1) + j + 1] + 1
13914
+ : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + j + 1]);
13915
+ }
13916
+ }
13917
+ let i = 0;
13918
+ let j = 0;
13919
+ while (i < n && j < m) {
13920
+ if (midA[i] === midB[j]) {
13921
+ rows.push({ type: 'ctx', oldNo: start + i + 1, newNo: start + j + 1, text: midA[i] });
13922
+ i++;
13923
+ j++;
13924
+ }
13925
+ else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + j + 1]) {
13926
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13927
+ i++;
13928
+ }
13929
+ else {
13930
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13931
+ j++;
13932
+ }
13933
+ }
13934
+ while (i < n) {
13935
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13936
+ i++;
13937
+ }
13938
+ while (j < m) {
13939
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13940
+ j++;
13941
+ }
13942
+ }
13943
+ else {
13944
+ for (let i = 0; i < midA.length; i++)
13945
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13946
+ for (let j = 0; j < midB.length; j++)
13947
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13948
+ }
13949
+ for (let i = endA, j = endB; i < a.length; i++, j++) {
13950
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: j + 1, text: a[i] });
13951
+ }
13952
+ return rows;
13953
+ }
13954
+ /**
13955
+ * Config / YAML diff viewer — change-approval screens without a hand-rolled
13956
+ * diff. Unified by default, `split` for side-by-side; long unchanged runs
13957
+ * collapse to an expandable "· N unchanged lines ·" row.
13958
+ *
13959
+ * <strct-diff [before]="saved" [after]="draft" beforeLabel="Running" afterLabel="Draft" />
13960
+ *
13961
+ * Rows are marked by symbol (+/−) as well as tint, so the diff reads without
13962
+ * color — the same CVD rule the icon badges follow.
13963
+ */
13964
+ class StrctDiff {
13965
+ /** The two texts to compare. */
13966
+ before = input.required(...(ngDevMode ? [{ debugName: "before" }] : /* istanbul ignore next */ []));
13967
+ after = input.required(...(ngDevMode ? [{ debugName: "after" }] : /* istanbul ignore next */ []));
13968
+ /** Unified (default) or side-by-side split view. */
13969
+ mode = input('unified', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
13970
+ /** Context lines kept around changes before collapsing (0 disables collapsing). */
13971
+ context = input(3, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
13972
+ /** Header bits. */
13973
+ title = input('Diff', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13974
+ language = input('', ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
13975
+ beforeLabel = input('Before', ...(ngDevMode ? [{ debugName: "beforeLabel" }] : /* istanbul ignore next */ []));
13976
+ afterLabel = input('After', ...(ngDevMode ? [{ debugName: "afterLabel" }] : /* istanbul ignore next */ []));
13977
+ copyable = input(true, { ...(ngDevMode ? { debugName: "copyable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13978
+ copyAfterLabel = input('Copy new version', ...(ngDevMode ? [{ debugName: "copyAfterLabel" }] : /* istanbul ignore next */ []));
13979
+ unchangedLabel = input('unchanged lines', ...(ngDevMode ? [{ debugName: "unchangedLabel" }] : /* istanbul ignore next */ []));
13980
+ /** Scroll the body past this height (px); null grows freely. */
13981
+ maxHeight = input(null, ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
13982
+ // Indices of collapsed runs the user has expanded.
13983
+ expanded = signal(new Set(), ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
13984
+ rows = computed(() => strctComputeDiff(this.before(), this.after()), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
13985
+ addCount = computed(() => this.rows().filter((r) => r.type === 'add').length, ...(ngDevMode ? [{ debugName: "addCount" }] : /* istanbul ignore next */ []));
13986
+ delCount = computed(() => this.rows().filter((r) => r.type === 'del').length, ...(ngDevMode ? [{ debugName: "delCount" }] : /* istanbul ignore next */ []));
13987
+ unifiedRows = computed(() => {
13988
+ const out = [];
13989
+ for (const chunk of this.chunks()) {
13990
+ if (chunk.skip)
13991
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
13992
+ else
13993
+ for (const r of chunk.rows)
13994
+ out.push({ kind: 'row', ...r });
13995
+ }
13996
+ return out;
13997
+ }, ...(ngDevMode ? [{ debugName: "unifiedRows" }] : /* istanbul ignore next */ []));
13998
+ splitRows = computed(() => {
13999
+ const out = [];
14000
+ for (const chunk of this.chunks()) {
14001
+ if (chunk.skip) {
14002
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
14003
+ continue;
14004
+ }
14005
+ // Pair del-runs with the add-run that follows them (classic split view).
14006
+ let i = 0;
14007
+ const rows = chunk.rows;
14008
+ while (i < rows.length) {
14009
+ const r = rows[i];
14010
+ if (r.type === 'ctx') {
14011
+ out.push({
14012
+ kind: 'row',
14013
+ left: { type: 'ctx', no: r.oldNo, text: r.text },
14014
+ right: { type: 'ctx', no: r.newNo, text: r.text },
14015
+ });
14016
+ i++;
14017
+ continue;
14018
+ }
14019
+ const dels = [];
14020
+ const adds = [];
14021
+ while (i < rows.length && rows[i].type === 'del')
14022
+ dels.push(rows[i++]);
14023
+ while (i < rows.length && rows[i].type === 'add')
14024
+ adds.push(rows[i++]);
14025
+ const len = Math.max(dels.length, adds.length);
14026
+ for (let k = 0; k < len; k++) {
14027
+ const d = dels[k];
14028
+ const a = adds[k];
14029
+ out.push({
14030
+ kind: 'row',
14031
+ left: d
14032
+ ? { type: 'del', no: d.oldNo, text: d.text }
14033
+ : { type: 'empty', no: null, text: '' },
14034
+ right: a
14035
+ ? { type: 'add', no: a.newNo, text: a.text }
14036
+ : { type: 'empty', no: null, text: '' },
14037
+ });
14038
+ }
14039
+ }
14040
+ }
14041
+ return out;
14042
+ }, ...(ngDevMode ? [{ debugName: "splitRows" }] : /* istanbul ignore next */ []));
14043
+ /** Group rows into visible chunks and collapsible unchanged runs. */
14044
+ chunks = computed(() => {
14045
+ const rows = this.rows();
14046
+ const ctx = this.context();
14047
+ const expanded = this.expanded();
14048
+ if (ctx <= 0)
14049
+ return [{ skip: false, start: 0, rows }];
14050
+ // Mark rows within `ctx` of any change as visible.
14051
+ const visible = new Array(rows.length).fill(false);
14052
+ rows.forEach((r, i) => {
14053
+ if (r.type !== 'ctx') {
14054
+ for (let k = Math.max(0, i - ctx); k <= Math.min(rows.length - 1, i + ctx); k++) {
14055
+ visible[k] = true;
14056
+ }
14057
+ }
14058
+ });
14059
+ const out = [];
14060
+ let i = 0;
14061
+ while (i < rows.length) {
14062
+ const vis = visible[i] || expanded.has(startOfRun(visible, i));
14063
+ const start = i;
14064
+ while (i < rows.length && (visible[i] || expanded.has(startOfRun(visible, i))) === vis)
14065
+ i++;
14066
+ const slice = rows.slice(start, i);
14067
+ // Tiny hidden runs are not worth a fold row.
14068
+ if (!vis && slice.length <= 2)
14069
+ out.push({ skip: false, start, rows: slice });
14070
+ else
14071
+ out.push({ skip: !vis, start, rows: slice });
14072
+ }
14073
+ return out;
14074
+ }, ...(ngDevMode ? [{ debugName: "chunks" }] : /* istanbul ignore next */ []));
14075
+ expand(startIndex) {
14076
+ const next = new Set(this.expanded());
14077
+ next.add(startIndex);
14078
+ this.expanded.set(next);
14079
+ }
14080
+ sign(type) {
14081
+ return type === 'add' ? '+' : type === 'del' ? '−' : '';
14082
+ }
14083
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, deps: [], target: i0.ɵɵFactoryTarget.Component });
14084
+ 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: `
14085
+ <div class="strct-diff__head">
14086
+ <span class="strct-diff__title">{{ title() }}</span>
14087
+ @if (language()) {
14088
+ <span class="strct-diff__lang">{{ language() }}</span>
14089
+ }
14090
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14091
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14092
+ <span class="strct-diff__grow"></span>
14093
+ @if (copyable()) {
14094
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14095
+ }
14096
+ </div>
14097
+ @if (mode() === 'split') {
14098
+ <div class="strct-diff__cols" aria-hidden="true">
14099
+ <span>{{ beforeLabel() }}</span>
14100
+ <span>{{ afterLabel() }}</span>
14101
+ </div>
14102
+ }
14103
+ <div
14104
+ class="strct-diff__scroll"
14105
+ tabindex="0"
14106
+ role="region"
14107
+ [attr.aria-label]="title()"
14108
+ [style.max-height.px]="maxHeight()"
14109
+ >
14110
+ @if (mode() === 'unified') {
14111
+ <table class="strct-diff__table">
14112
+ <tbody>
14113
+ @for (row of unifiedRows(); track $index) {
14114
+ @if (row.kind === 'skip') {
14115
+ <tr class="strct-diff__skip-row">
14116
+ <td colspan="4">
14117
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14118
+ · {{ row.count }} {{ unchangedLabel() }} ·
14119
+ </button>
14120
+ </td>
14121
+ </tr>
14122
+ } @else {
14123
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14124
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14125
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14126
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14127
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14128
+ </tr>
14129
+ }
14130
+ }
14131
+ </tbody>
14132
+ </table>
14133
+ } @else {
14134
+ <table class="strct-diff__table strct-diff__table--split">
14135
+ <tbody>
14136
+ @for (row of splitRows(); track $index) {
14137
+ @if (row.kind === 'skip') {
14138
+ <tr class="strct-diff__skip-row">
14139
+ <td colspan="6">
14140
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14141
+ · {{ row.count }} {{ unchangedLabel() }} ·
14142
+ </button>
14143
+ </td>
14144
+ </tr>
14145
+ } @else {
14146
+ <tr>
14147
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14148
+ {{ row.left.no ?? '' }}
14149
+ </td>
14150
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14151
+ {{ sign(row.left.type) }}
14152
+ </td>
14153
+ <td
14154
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14155
+ [textContent]="row.left.text"
14156
+ ></td>
14157
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14158
+ {{ row.right.no ?? '' }}
14159
+ </td>
14160
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14161
+ {{ sign(row.right.type) }}
14162
+ </td>
14163
+ <td
14164
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14165
+ [textContent]="row.right.text"
14166
+ ></td>
14167
+ </tr>
14168
+ }
14169
+ }
14170
+ </tbody>
14171
+ </table>
14172
+ }
14173
+ </div>
14174
+ `, 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 });
14175
+ }
14176
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, decorators: [{
14177
+ type: Component,
14178
+ args: [{ selector: 'strct-diff', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctCopy], template: `
14179
+ <div class="strct-diff__head">
14180
+ <span class="strct-diff__title">{{ title() }}</span>
14181
+ @if (language()) {
14182
+ <span class="strct-diff__lang">{{ language() }}</span>
14183
+ }
14184
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14185
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14186
+ <span class="strct-diff__grow"></span>
14187
+ @if (copyable()) {
14188
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14189
+ }
14190
+ </div>
14191
+ @if (mode() === 'split') {
14192
+ <div class="strct-diff__cols" aria-hidden="true">
14193
+ <span>{{ beforeLabel() }}</span>
14194
+ <span>{{ afterLabel() }}</span>
14195
+ </div>
14196
+ }
14197
+ <div
14198
+ class="strct-diff__scroll"
14199
+ tabindex="0"
14200
+ role="region"
14201
+ [attr.aria-label]="title()"
14202
+ [style.max-height.px]="maxHeight()"
14203
+ >
14204
+ @if (mode() === 'unified') {
14205
+ <table class="strct-diff__table">
14206
+ <tbody>
14207
+ @for (row of unifiedRows(); track $index) {
14208
+ @if (row.kind === 'skip') {
14209
+ <tr class="strct-diff__skip-row">
14210
+ <td colspan="4">
14211
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14212
+ · {{ row.count }} {{ unchangedLabel() }} ·
14213
+ </button>
14214
+ </td>
14215
+ </tr>
14216
+ } @else {
14217
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14218
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14219
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14220
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14221
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14222
+ </tr>
14223
+ }
14224
+ }
14225
+ </tbody>
14226
+ </table>
14227
+ } @else {
14228
+ <table class="strct-diff__table strct-diff__table--split">
14229
+ <tbody>
14230
+ @for (row of splitRows(); track $index) {
14231
+ @if (row.kind === 'skip') {
14232
+ <tr class="strct-diff__skip-row">
14233
+ <td colspan="6">
14234
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14235
+ · {{ row.count }} {{ unchangedLabel() }} ·
14236
+ </button>
14237
+ </td>
14238
+ </tr>
14239
+ } @else {
14240
+ <tr>
14241
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14242
+ {{ row.left.no ?? '' }}
14243
+ </td>
14244
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14245
+ {{ sign(row.left.type) }}
14246
+ </td>
14247
+ <td
14248
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14249
+ [textContent]="row.left.text"
14250
+ ></td>
14251
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14252
+ {{ row.right.no ?? '' }}
14253
+ </td>
14254
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14255
+ {{ sign(row.right.type) }}
14256
+ </td>
14257
+ <td
14258
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14259
+ [textContent]="row.right.text"
14260
+ ></td>
14261
+ </tr>
14262
+ }
14263
+ }
14264
+ </tbody>
14265
+ </table>
14266
+ }
14267
+ </div>
14268
+ `, 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"] }]
14269
+ }], 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 }] }] } });
14270
+ function startOfRun(visible, i) {
14271
+ let s = i;
14272
+ while (s > 0 && visible[s - 1] === visible[i])
14273
+ s--;
14274
+ return s;
14275
+ }
14276
+
14277
+ /**
14278
+ * Two resizable panes with a draggable gutter — master/detail layouts where
14279
+ * the split is the user's to own:
14280
+ *
14281
+ * <strct-splitter [(split)]="pct" [min]="20" [max]="80">
14282
+ * <div strctPaneStart>…list…</div>
14283
+ * <div strctPaneEnd>…detail…</div>
14284
+ * </strct-splitter>
14285
+ *
14286
+ * `split` is the start pane's share in percent (two-way, persistable).
14287
+ * The gutter is a keyboard separator: arrows nudge, Home/End jump to min/max.
14288
+ */
14289
+ class StrctSplitter {
14290
+ host = inject(ElementRef);
14291
+ /** Start pane share in percent (two-way). */
14292
+ split = model(50, ...(ngDevMode ? [{ debugName: "split" }] : /* istanbul ignore next */ []));
14293
+ /** Clamp bounds for the split (percent). */
14294
+ min = input(15, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
14295
+ max = input(85, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
14296
+ /** Stack panes vertically (gutter drags up/down). */
14297
+ vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : /* istanbul ignore next */ []));
14298
+ /** Accessible name of the separator (localizable). */
14299
+ gutterLabel = input('Resize panes', ...(ngDevMode ? [{ debugName: "gutterLabel" }] : /* istanbul ignore next */ []));
14300
+ /** Keyboard nudge step in percent. */
14301
+ step = input(3, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
14302
+ clamped = computed(() => Math.min(this.max(), Math.max(this.min(), this.split())), ...(ngDevMode ? [{ debugName: "clamped" }] : /* istanbul ignore next */ []));
14303
+ dragging = false;
14304
+ moveHandler = (e) => this.onDragMove(e);
14305
+ upHandler = () => this.onDragEnd();
14306
+ onDragStart(event) {
14307
+ event.preventDefault();
14308
+ this.dragging = true;
14309
+ document.addEventListener('mousemove', this.moveHandler);
14310
+ document.addEventListener('mouseup', this.upHandler);
14311
+ }
14312
+ onDragMove(event) {
14313
+ const rect = this.host.nativeElement.getBoundingClientRect();
14314
+ const ratio = this.vertical()
14315
+ ? (event.clientY - rect.top) / rect.height
14316
+ : (event.clientX - rect.left) / rect.width;
14317
+ this.split.set(Math.min(this.max(), Math.max(this.min(), Math.round(ratio * 100))));
14318
+ }
14319
+ onDragEnd() {
14320
+ this.dragging = false;
14321
+ document.removeEventListener('mousemove', this.moveHandler);
14322
+ document.removeEventListener('mouseup', this.upHandler);
14323
+ }
14324
+ onKeydown(event) {
14325
+ const dec = this.vertical() ? 'ArrowUp' : 'ArrowLeft';
14326
+ const inc = this.vertical() ? 'ArrowDown' : 'ArrowRight';
14327
+ let next = null;
14328
+ if (event.key === dec)
14329
+ next = this.clamped() - this.step();
14330
+ else if (event.key === inc)
14331
+ next = this.clamped() + this.step();
14332
+ else if (event.key === 'Home')
14333
+ next = this.min();
14334
+ else if (event.key === 'End')
14335
+ next = this.max();
14336
+ if (next == null)
14337
+ return;
14338
+ event.preventDefault();
14339
+ this.split.set(Math.min(this.max(), Math.max(this.min(), next)));
14340
+ }
14341
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, deps: [], target: i0.ɵɵFactoryTarget.Component });
14342
+ 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: `
14343
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14344
+ <ng-content select="[strctPaneStart]" />
14345
+ </div>
14346
+ <div
14347
+ class="strct-split__gutter"
14348
+ role="separator"
14349
+ tabindex="0"
14350
+ [attr.aria-label]="gutterLabel()"
14351
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14352
+ [attr.aria-valuenow]="clamped()"
14353
+ [attr.aria-valuemin]="min()"
14354
+ [attr.aria-valuemax]="max()"
14355
+ (mousedown)="onDragStart($event)"
14356
+ (keydown)="onKeydown($event)"
14357
+ >
14358
+ <span class="strct-split__grip" aria-hidden="true"></span>
14359
+ </div>
14360
+ <div class="strct-split__pane strct-split__pane--end">
14361
+ <ng-content select="[strctPaneEnd]" />
14362
+ </div>
14363
+ `, 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 });
14364
+ }
14365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, decorators: [{
14366
+ type: Component,
14367
+ args: [{ selector: 'strct-splitter', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14368
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14369
+ <ng-content select="[strctPaneStart]" />
14370
+ </div>
14371
+ <div
14372
+ class="strct-split__gutter"
14373
+ role="separator"
14374
+ tabindex="0"
14375
+ [attr.aria-label]="gutterLabel()"
14376
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14377
+ [attr.aria-valuenow]="clamped()"
14378
+ [attr.aria-valuemin]="min()"
14379
+ [attr.aria-valuemax]="max()"
14380
+ (mousedown)="onDragStart($event)"
14381
+ (keydown)="onKeydown($event)"
14382
+ >
14383
+ <span class="strct-split__grip" aria-hidden="true"></span>
14384
+ </div>
14385
+ <div class="strct-split__pane strct-split__pane--end">
14386
+ <ng-content select="[strctPaneEnd]" />
14387
+ </div>
14388
+ `, host: {
14389
+ class: 'strct-split',
14390
+ '[class.strct-split--vertical]': 'vertical()',
14391
+ '[class.strct-split--dragging]': 'dragging',
14392
+ }, 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"] }]
14393
+ }], 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 }] }] } });
14394
+
14395
+ /**
14396
+ * Dual-list transfer (picklist) — "assign hosts to the cluster": all items on
14397
+ * the left, the chosen set on the right, checkbox multi-select and move
14398
+ * buttons in between, with a searchbox per side.
14399
+ *
14400
+ * <strct-transfer [items]="hosts" [(assigned)]="clusterHosts" />
14401
+ *
14402
+ * `assigned` is the two-way id set (string[]); `items` stays the single
14403
+ * source-of-truth list.
14404
+ */
14405
+ class StrctTransfer {
14406
+ /** The full item universe. */
14407
+ items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14408
+ /** Assigned item ids (two-way). */
14409
+ assigned = model([], ...(ngDevMode ? [{ debugName: "assigned" }] : /* istanbul ignore next */ []));
14410
+ /** Localizable strings. */
14411
+ sourceLabel = input('Available', ...(ngDevMode ? [{ debugName: "sourceLabel" }] : /* istanbul ignore next */ []));
14412
+ targetLabel = input('Assigned', ...(ngDevMode ? [{ debugName: "targetLabel" }] : /* istanbul ignore next */ []));
14413
+ searchPlaceholder = input('Search', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
14414
+ emptyLabel = input('No items', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
14415
+ assignLabel = input('Assign selected', ...(ngDevMode ? [{ debugName: "assignLabel" }] : /* istanbul ignore next */ []));
14416
+ unassignLabel = input('Unassign selected', ...(ngDevMode ? [{ debugName: "unassignLabel" }] : /* istanbul ignore next */ []));
14417
+ /** A move happened (either direction) with the moved ids. */
14418
+ moved = output();
14419
+ sourceQuery = signal('', ...(ngDevMode ? [{ debugName: "sourceQuery" }] : /* istanbul ignore next */ []));
14420
+ targetQuery = signal('', ...(ngDevMode ? [{ debugName: "targetQuery" }] : /* istanbul ignore next */ []));
14421
+ sourceChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "sourceChecked" }] : /* istanbul ignore next */ []));
14422
+ targetChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "targetChecked" }] : /* istanbul ignore next */ []));
14423
+ assignedSet = computed(() => new Set(this.assigned()), ...(ngDevMode ? [{ debugName: "assignedSet" }] : /* istanbul ignore next */ []));
14424
+ sourceItems = computed(() => this.items().filter((i) => !this.assignedSet().has(i.id) && this.matches(i, this.sourceQuery())), ...(ngDevMode ? [{ debugName: "sourceItems" }] : /* istanbul ignore next */ []));
14425
+ targetItems = computed(() => this.items().filter((i) => this.assignedSet().has(i.id) && this.matches(i, this.targetQuery())), ...(ngDevMode ? [{ debugName: "targetItems" }] : /* istanbul ignore next */ []));
14426
+ matches(item, q) {
14427
+ return !q.trim() || item.label.toLowerCase().includes(q.trim().toLowerCase());
14428
+ }
14429
+ toggle(which, id) {
14430
+ const next = new Set(which());
14431
+ if (next.has(id))
14432
+ next.delete(id);
14433
+ else
14434
+ next.add(id);
14435
+ which.set(next);
14436
+ }
14437
+ assign() {
14438
+ const ids = [...this.sourceChecked()];
14439
+ if (!ids.length)
14440
+ return;
14441
+ this.assigned.set([...this.assigned(), ...ids]);
14442
+ this.sourceChecked.set(new Set());
14443
+ this.moved.emit({ direction: 'assign', ids });
14444
+ }
14445
+ unassign() {
14446
+ const ids = new Set(this.targetChecked());
14447
+ if (!ids.size)
14448
+ return;
14449
+ this.assigned.set(this.assigned().filter((id) => !ids.has(id)));
14450
+ this.targetChecked.set(new Set());
14451
+ this.moved.emit({ direction: 'unassign', ids: [...ids] });
14452
+ }
14453
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, deps: [], target: i0.ɵɵFactoryTarget.Component });
14454
+ 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: `
14455
+ <div class="strct-tf__col">
14456
+ <div class="strct-tf__head">
14457
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14458
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14459
+ </div>
14460
+ <strct-searchbox
14461
+ class="strct-tf__search"
14462
+ [placeholder]="searchPlaceholder()"
14463
+ [value]="sourceQuery()"
14464
+ (valueChange)="sourceQuery.set($event)"
14465
+ />
14466
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14467
+ @for (item of sourceItems(); track item.id) {
14468
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14469
+ <strct-checkbox
14470
+ [checked]="sourceChecked().has(item.id)"
14471
+ [disabled]="item.disabled ?? false"
14472
+ (checkedChange)="toggle(sourceChecked, item.id)"
14473
+ >
14474
+ @if (item.icon) {
14475
+ <strct-icon [name]="item.icon" [size]="14" />
14476
+ }
14477
+ <span class="strct-tf__label">{{ item.label }}</span>
14478
+ </strct-checkbox>
14479
+ </li>
14480
+ } @empty {
14481
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14482
+ }
14483
+ </ul>
14484
+ </div>
14485
+
14486
+ <div class="strct-tf__mid">
14487
+ <button
14488
+ strct-button
14489
+ size="sm"
14490
+ variant="neutral"
14491
+ [disabled]="!sourceChecked().size"
14492
+ [attr.aria-label]="assignLabel()"
14493
+ (click)="assign()"
14494
+ >
14495
+ <strct-icon name="chevronRight" [size]="14" />
14496
+ </button>
14497
+ <button
14498
+ strct-button
14499
+ size="sm"
14500
+ variant="neutral"
14501
+ [disabled]="!targetChecked().size"
14502
+ [attr.aria-label]="unassignLabel()"
14503
+ (click)="unassign()"
14504
+ >
14505
+ <strct-icon name="chevronLeft" [size]="14" />
14506
+ </button>
14507
+ </div>
14508
+
14509
+ <div class="strct-tf__col">
14510
+ <div class="strct-tf__head">
14511
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14512
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14513
+ </div>
14514
+ <strct-searchbox
14515
+ class="strct-tf__search"
14516
+ [placeholder]="searchPlaceholder()"
14517
+ [value]="targetQuery()"
14518
+ (valueChange)="targetQuery.set($event)"
14519
+ />
14520
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14521
+ @for (item of targetItems(); track item.id) {
14522
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14523
+ <strct-checkbox
14524
+ [checked]="targetChecked().has(item.id)"
14525
+ [disabled]="item.disabled ?? false"
14526
+ (checkedChange)="toggle(targetChecked, item.id)"
14527
+ >
14528
+ @if (item.icon) {
14529
+ <strct-icon [name]="item.icon" [size]="14" />
14530
+ }
14531
+ <span class="strct-tf__label">{{ item.label }}</span>
14532
+ </strct-checkbox>
14533
+ </li>
14534
+ } @empty {
14535
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14536
+ }
14537
+ </ul>
14538
+ </div>
14539
+ `, 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 });
14540
+ }
14541
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, decorators: [{
14542
+ type: Component,
14543
+ args: [{ selector: 'strct-transfer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctCheckbox, StrctIcon, StrctSearchbox], template: `
14544
+ <div class="strct-tf__col">
14545
+ <div class="strct-tf__head">
14546
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14547
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14548
+ </div>
14549
+ <strct-searchbox
14550
+ class="strct-tf__search"
14551
+ [placeholder]="searchPlaceholder()"
14552
+ [value]="sourceQuery()"
14553
+ (valueChange)="sourceQuery.set($event)"
14554
+ />
14555
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14556
+ @for (item of sourceItems(); track item.id) {
14557
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14558
+ <strct-checkbox
14559
+ [checked]="sourceChecked().has(item.id)"
14560
+ [disabled]="item.disabled ?? false"
14561
+ (checkedChange)="toggle(sourceChecked, item.id)"
14562
+ >
14563
+ @if (item.icon) {
14564
+ <strct-icon [name]="item.icon" [size]="14" />
14565
+ }
14566
+ <span class="strct-tf__label">{{ item.label }}</span>
14567
+ </strct-checkbox>
14568
+ </li>
14569
+ } @empty {
14570
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14571
+ }
14572
+ </ul>
14573
+ </div>
14574
+
14575
+ <div class="strct-tf__mid">
14576
+ <button
14577
+ strct-button
14578
+ size="sm"
14579
+ variant="neutral"
14580
+ [disabled]="!sourceChecked().size"
14581
+ [attr.aria-label]="assignLabel()"
14582
+ (click)="assign()"
14583
+ >
14584
+ <strct-icon name="chevronRight" [size]="14" />
14585
+ </button>
14586
+ <button
14587
+ strct-button
14588
+ size="sm"
14589
+ variant="neutral"
14590
+ [disabled]="!targetChecked().size"
14591
+ [attr.aria-label]="unassignLabel()"
14592
+ (click)="unassign()"
14593
+ >
14594
+ <strct-icon name="chevronLeft" [size]="14" />
14595
+ </button>
14596
+ </div>
14597
+
14598
+ <div class="strct-tf__col">
14599
+ <div class="strct-tf__head">
14600
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14601
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14602
+ </div>
14603
+ <strct-searchbox
14604
+ class="strct-tf__search"
14605
+ [placeholder]="searchPlaceholder()"
14606
+ [value]="targetQuery()"
14607
+ (valueChange)="targetQuery.set($event)"
14608
+ />
14609
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14610
+ @for (item of targetItems(); track item.id) {
14611
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14612
+ <strct-checkbox
14613
+ [checked]="targetChecked().has(item.id)"
14614
+ [disabled]="item.disabled ?? false"
14615
+ (checkedChange)="toggle(targetChecked, item.id)"
14616
+ >
14617
+ @if (item.icon) {
14618
+ <strct-icon [name]="item.icon" [size]="14" />
14619
+ }
14620
+ <span class="strct-tf__label">{{ item.label }}</span>
14621
+ </strct-checkbox>
14622
+ </li>
14623
+ } @empty {
14624
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14625
+ }
14626
+ </ul>
14627
+ </div>
14628
+ `, 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"] }]
14629
+ }], 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"] }] } });
14630
+
14631
+ /**
14632
+ * Split button — a primary action plus a chevron opening its variants
14633
+ * (Fluent's pattern): "Deploy ▾ → Deploy with snapshot / Deploy paused".
14634
+ *
14635
+ * <strct-split-button label="Deploy" [items]="variants"
14636
+ * (action)="deploy()" (picked)="run($event)" />
14637
+ *
14638
+ * The main segment fires `(action)`; menu entries fire `(picked)`.
14639
+ */
14640
+ class StrctSplitButton {
14641
+ /** Main action label. */
14642
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
14643
+ /** Menu entries (StrctMenuItem: id, label, icon?, critical?, disabled?). */
14644
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14645
+ /** Optional leading icon of the main segment. */
14646
+ icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
14647
+ /** Filled (solid accent) look. */
14648
+ solid = input(false, { ...(ngDevMode ? { debugName: "solid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14649
+ /** Disable both segments. */
14650
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14651
+ /** Accessible name of the chevron (localizable). */
14652
+ menuLabel = input('More actions', ...(ngDevMode ? [{ debugName: "menuLabel" }] : /* istanbul ignore next */ []));
14653
+ /** The main segment was clicked. */
14654
+ action = output();
14655
+ /** A menu entry was chosen. */
14656
+ picked = output();
14657
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
14658
+ 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: `
14659
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14660
+ <button
14661
+ type="button"
14662
+ class="strct-sbt__main"
14663
+ [disabled]="disabled() || null"
14664
+ (click)="action.emit()"
14665
+ >
14666
+ @if (icon()) {
14667
+ <strct-icon [name]="icon()" [size]="13" />
14668
+ }
14669
+ {{ label() }}
14670
+ </button>
14671
+ <strct-dropdown align="end">
14672
+ <button
14673
+ type="button"
14674
+ class="strct-sbt__chev"
14675
+ strctDropdownTrigger
14676
+ [disabled]="disabled() || null"
14677
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14678
+ >
14679
+ <strct-icon name="chevronDown" [size]="12" />
14680
+ </button>
14681
+ @for (item of items(); track $index) {
14682
+ @if (item.divider) {
14683
+ <strct-dropdown-divider />
14684
+ } @else {
14685
+ <strct-dropdown-item
14686
+ [critical]="item.critical ?? false"
14687
+ [disabled]="item.disabled ?? false"
14688
+ (click)="!item.disabled && picked.emit(item)"
14689
+ >
14690
+ @if (item.icon) {
14691
+ <strct-icon [name]="item.icon" [size]="13" />
14692
+ }
14693
+ {{ item.label }}
14694
+ </strct-dropdown-item>
14695
+ }
14696
+ }
14697
+ </strct-dropdown>
14698
+ </div>
14699
+ `, 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 });
14700
+ }
14701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, decorators: [{
14702
+ type: Component,
14703
+ args: [{ selector: 'strct-split-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
14704
+ StrctDropdown,
14705
+ StrctDropdownDivider,
14706
+ StrctDropdownItem,
14707
+ StrctDropdownTrigger,
14708
+ StrctIcon,
14709
+ ], template: `
14710
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14711
+ <button
14712
+ type="button"
14713
+ class="strct-sbt__main"
14714
+ [disabled]="disabled() || null"
14715
+ (click)="action.emit()"
14716
+ >
14717
+ @if (icon()) {
14718
+ <strct-icon [name]="icon()" [size]="13" />
14719
+ }
14720
+ {{ label() }}
14721
+ </button>
14722
+ <strct-dropdown align="end">
14723
+ <button
14724
+ type="button"
14725
+ class="strct-sbt__chev"
14726
+ strctDropdownTrigger
14727
+ [disabled]="disabled() || null"
14728
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14729
+ >
14730
+ <strct-icon name="chevronDown" [size]="12" />
14731
+ </button>
14732
+ @for (item of items(); track $index) {
14733
+ @if (item.divider) {
14734
+ <strct-dropdown-divider />
14735
+ } @else {
14736
+ <strct-dropdown-item
14737
+ [critical]="item.critical ?? false"
14738
+ [disabled]="item.disabled ?? false"
14739
+ (click)="!item.disabled && picked.emit(item)"
14740
+ >
14741
+ @if (item.icon) {
14742
+ <strct-icon [name]="item.icon" [size]="13" />
14743
+ }
14744
+ {{ item.label }}
14745
+ </strct-dropdown-item>
14746
+ }
14747
+ }
14748
+ </strct-dropdown>
14749
+ </div>
14750
+ `, 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"] }]
14751
+ }], 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"] }] } });
14752
+
14753
+ /**
14754
+ * Repeating diagonal text watermark over its content — compliance overlays
14755
+ * for classified/internal consoles ("CONFIDENTIAL", "user@corp · 2026-07-21"):
14756
+ *
14757
+ * <strct-watermark text="CONFIDENTIAL">…page…</strct-watermark>
14758
+ *
14759
+ * Rendered as a pointer-transparent tiled SVG layer, so the content below
14760
+ * stays fully interactive and selectable. Purely visual — `aria-hidden`.
14761
+ */
14762
+ class StrctWatermark {
14763
+ sanitizer = inject(DomSanitizer);
14764
+ /** The repeated watermark text. */
14765
+ text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
14766
+ /** Layer opacity (0–1). */
14767
+ opacity = input(0.06, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
14768
+ /** Rotation in degrees (negative = up-slope). */
14769
+ angle = input(-24, ...(ngDevMode ? [{ debugName: "angle" }] : /* istanbul ignore next */ []));
14770
+ /** Tile size in px — larger spreads the marks out. */
14771
+ gap = input(220, ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
14772
+ /** Font size in px. */
14773
+ fontSize = input(15, ...(ngDevMode ? [{ debugName: "fontSize" }] : /* istanbul ignore next */ []));
14774
+ tile = computed(() => {
14775
+ const size = this.gap();
14776
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
14777
+ `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" ` +
14778
+ `transform="rotate(${this.angle()} ${size / 2} ${size / 2})" ` +
14779
+ `font-family="sans-serif" font-size="${this.fontSize()}" font-weight="600" ` +
14780
+ `fill="currentColor" fill-opacity="${this.opacity()}">${escapeXml(this.text())}</text></svg>`;
14781
+ return this.sanitizer.bypassSecurityTrustStyle(`url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`);
14782
+ }, ...(ngDevMode ? [{ debugName: "tile" }] : /* istanbul ignore next */ []));
14783
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, deps: [], target: i0.ɵɵFactoryTarget.Component });
14784
+ 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: `
14785
+ <ng-content />
14786
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14787
+ `, 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 });
14788
+ }
14789
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, decorators: [{
14790
+ type: Component,
14791
+ args: [{ selector: 'strct-watermark', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14792
+ <ng-content />
14793
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14794
+ `, 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"] }]
14795
+ }], 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 }] }] } });
14796
+ function escapeXml(s) {
14797
+ return s
14798
+ .replace(/&/g, '&amp;')
14799
+ .replace(/</g, '&lt;')
14800
+ .replace(/>/g, '&gt;')
14801
+ .replace(/"/g, '&quot;');
14802
+ }
14803
+
14804
+ /**
14805
+ * Feature tour (coach marks) — "what's new" onboarding over live UI:
14806
+ *
14807
+ * <strct-tour [(open)]="tourOpen" [steps]="steps" (finished)="done()" />
14808
+ *
14809
+ * Each step spotlights its `target` selector with a ring cut out of a dimmed
14810
+ * backdrop and anchors a card next to it. Arrows / buttons step, Escape
14811
+ * closes, and the card is a labeled dialog. Purely DOM-driven: targets are
14812
+ * looked up when the step shows, so it works over any page.
14813
+ */
14814
+ class StrctTour {
14815
+ /** Whether the tour is showing (two-way). */
14816
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
14817
+ /** The steps, in order. */
14818
+ steps = input.required(...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
14819
+ /** Localizable strings. */
14820
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
14821
+ backLabel = input('Back', ...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
14822
+ skipLabel = input('Skip', ...(ngDevMode ? [{ debugName: "skipLabel" }] : /* istanbul ignore next */ []));
14823
+ doneLabel = input('Done', ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
14824
+ /** The last step's Done was clicked (Escape / Skip emit `dismissed`). */
14825
+ finished = output();
14826
+ dismissed = output();
14827
+ index = signal(0, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
14828
+ /** Bumped on open/step/resize to re-measure the target. */
14829
+ measureTick = signal(0, ...(ngDevMode ? [{ debugName: "measureTick" }] : /* istanbul ignore next */ []));
14830
+ current = computed(() => this.steps()[this.index()] ?? null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
14831
+ spot = computed(() => {
14832
+ this.measureTick();
14833
+ const step = this.current();
14834
+ if (!this.open() || !step?.target)
14835
+ return null;
14836
+ const el = document.querySelector(step.target);
14837
+ if (!el)
14838
+ return null;
14839
+ const r = el.getBoundingClientRect();
14840
+ return { top: r.top, left: r.left, width: r.width, height: r.height };
14841
+ }, ...(ngDevMode ? [{ debugName: "spot" }] : /* istanbul ignore next */ []));
14842
+ cardPos = computed(() => {
14843
+ const r = this.spot();
14844
+ const w = 300;
14845
+ const h = 170;
14846
+ if (!r) {
14847
+ return {
14848
+ top: Math.max(20, window.innerHeight / 2 - h / 2),
14849
+ left: Math.max(20, window.innerWidth / 2 - w / 2),
14850
+ };
14851
+ }
14852
+ // Below the target if it fits, else above; clamped into the viewport.
14853
+ let top = r.top + r.height + 14;
14854
+ if (top + h > window.innerHeight - 12)
14855
+ top = Math.max(12, r.top - h - 14);
14856
+ const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
14857
+ return { top, left };
14858
+ }, ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
14859
+ constructor() {
14860
+ // Restart from step 0 on open; scroll the first target into view.
14861
+ effect(() => {
14862
+ if (this.open()) {
14863
+ this.index.set(0);
14864
+ this.scrollToTarget();
14865
+ }
14866
+ });
14867
+ }
14868
+ next() {
14869
+ if (this.index() >= this.steps().length - 1) {
14870
+ this.open.set(false);
14871
+ this.finished.emit();
14872
+ return;
14873
+ }
14874
+ this.index.update((i) => i + 1);
14875
+ this.scrollToTarget();
14876
+ }
14877
+ prev() {
14878
+ this.index.update((i) => Math.max(0, i - 1));
14879
+ this.scrollToTarget();
14880
+ }
14881
+ close() {
14882
+ this.open.set(false);
14883
+ this.dismissed.emit();
14884
+ }
14885
+ scrollToTarget() {
14886
+ setTimeout(() => {
14887
+ const sel = this.steps()[this.index()]?.target;
14888
+ if (sel)
14889
+ document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
14890
+ this.measureTick.update((n) => n + 1);
14891
+ });
14892
+ }
14893
+ onEscape() {
14894
+ if (this.open())
14895
+ this.close();
14896
+ }
14897
+ onReflow() {
14898
+ if (this.open())
14899
+ this.measureTick.update((n) => n + 1);
14900
+ }
14901
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
14902
+ 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: `
14903
+ @if (open() && current(); as step) {
14904
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14905
+ <div
14906
+ class="strct-tour__backdrop"
14907
+ [class.strct-tour__backdrop--dim]="!spot()"
14908
+ (click)="close()"
14909
+ ></div>
14910
+ @if (spot(); as r) {
14911
+ <div
14912
+ class="strct-tour__ring"
14913
+ [style.top.px]="r.top - 6"
14914
+ [style.left.px]="r.left - 6"
14915
+ [style.width.px]="r.width + 12"
14916
+ [style.height.px]="r.height + 12"
14917
+ ></div>
14918
+ }
14919
+ <div
14920
+ class="strct-tour__card"
14921
+ role="dialog"
14922
+ [attr.aria-label]="step.title"
14923
+ [style.top.px]="cardPos().top"
14924
+ [style.left.px]="cardPos().left"
14925
+ >
14926
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14927
+ <div class="strct-tour__title">{{ step.title }}</div>
14928
+ <div class="strct-tour__body">{{ step.body }}</div>
14929
+ <div class="strct-tour__nav">
14930
+ <button strct-button size="sm" variant="flat" (click)="close()">
14931
+ {{ skipLabel() }}
14932
+ </button>
14933
+ <span class="strct-tour__grow"></span>
14934
+ @if (index() > 0) {
14935
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14936
+ {{ backLabel() }}
14937
+ </button>
14938
+ }
14939
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14940
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14941
+ </button>
14942
+ </div>
14943
+ </div>
14944
+ }
14945
+ `, 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 });
14946
+ }
14947
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, decorators: [{
14948
+ type: Component,
14949
+ args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
14950
+ @if (open() && current(); as step) {
14951
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14952
+ <div
14953
+ class="strct-tour__backdrop"
14954
+ [class.strct-tour__backdrop--dim]="!spot()"
14955
+ (click)="close()"
14956
+ ></div>
14957
+ @if (spot(); as r) {
14958
+ <div
14959
+ class="strct-tour__ring"
14960
+ [style.top.px]="r.top - 6"
14961
+ [style.left.px]="r.left - 6"
14962
+ [style.width.px]="r.width + 12"
14963
+ [style.height.px]="r.height + 12"
14964
+ ></div>
14965
+ }
14966
+ <div
14967
+ class="strct-tour__card"
14968
+ role="dialog"
14969
+ [attr.aria-label]="step.title"
14970
+ [style.top.px]="cardPos().top"
14971
+ [style.left.px]="cardPos().left"
14972
+ >
14973
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14974
+ <div class="strct-tour__title">{{ step.title }}</div>
14975
+ <div class="strct-tour__body">{{ step.body }}</div>
14976
+ <div class="strct-tour__nav">
14977
+ <button strct-button size="sm" variant="flat" (click)="close()">
14978
+ {{ skipLabel() }}
14979
+ </button>
14980
+ <span class="strct-tour__grow"></span>
14981
+ @if (index() > 0) {
14982
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14983
+ {{ backLabel() }}
14984
+ </button>
14985
+ }
14986
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14987
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14988
+ </button>
14989
+ </div>
14990
+ </div>
14991
+ }
14992
+ `, 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"] }]
14993
+ }], 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: [{
14994
+ type: HostListener,
14995
+ args: ['document:keydown.escape']
14996
+ }], onReflow: [{
14997
+ type: HostListener,
14998
+ args: ['window:resize']
14999
+ }, {
15000
+ type: HostListener,
15001
+ args: ['window:scroll']
15002
+ }] } });
15003
+
15004
+ /**
15005
+ * Horizontal menubar — the application-menu strip ("File · Edit · View") for
15006
+ * dense tool-style consoles:
15007
+ *
15008
+ * <strct-menubar [menus]="menus" (picked)="run($event)" />
15009
+ *
15010
+ * Click (or Enter/Down) opens a menu; Left/Right move across the bar and,
15011
+ * while open, switch the open menu (the APG menubar convention). Escape and
15012
+ * outside click close. `(picked)` carries `{ menu, item }`.
15013
+ */
15014
+ class StrctMenubar {
15015
+ host = inject(ElementRef);
15016
+ /** Top-level menus. */
15017
+ menus = input.required(...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
15018
+ /** Accessible name of the bar (localizable). */
15019
+ ariaLabel = input('Application menu', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
15020
+ /** A menu entry was chosen. */
15021
+ picked = output();
15022
+ openId = signal(null, ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
15023
+ focusIdx = signal(0, ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
15024
+ toggleMenu(id, i) {
15025
+ this.focusIdx.set(i);
15026
+ this.openId.set(this.openId() === id ? null : id);
15027
+ }
15028
+ pick(menu, item) {
15029
+ if (item.disabled)
15030
+ return;
15031
+ this.openId.set(null);
15032
+ this.picked.emit({ menu, item });
15033
+ }
15034
+ onTopKey(event, i) {
15035
+ const menus = this.menus();
15036
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
15037
+ event.preventDefault();
15038
+ const dir = event.key === 'ArrowRight' ? 1 : -1;
15039
+ const next = (i + dir + menus.length) % menus.length;
15040
+ this.focusIdx.set(next);
15041
+ // While a menu is open, moving across the bar switches the open menu.
15042
+ if (this.openId())
15043
+ this.openId.set(menus[next].id);
15044
+ setTimeout(() => {
15045
+ const tops = this.host.nativeElement.querySelectorAll('.strct-mb__top');
15046
+ tops[next]?.focus();
15047
+ });
15048
+ }
15049
+ else if (event.key === 'ArrowDown' || event.key === 'Enter') {
15050
+ event.preventDefault();
15051
+ this.openId.set(menus[i].id);
15052
+ }
15053
+ }
15054
+ onDocClick(event) {
15055
+ if (this.openId() && !this.host.nativeElement.contains(event.target)) {
15056
+ this.openId.set(null);
15057
+ }
15058
+ }
15059
+ onEscape() {
15060
+ this.openId.set(null);
15061
+ }
15062
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
15063
+ 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: `
15064
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
15065
+ @for (menu of menus(); track menu.id; let i = $index) {
15066
+ <div class="strct-mb__wrap">
15067
+ <button
15068
+ type="button"
15069
+ class="strct-mb__top"
15070
+ role="menuitem"
15071
+ [class.strct-mb__top--open]="openId() === menu.id"
15072
+ [attr.aria-haspopup]="'menu'"
15073
+ [attr.aria-expanded]="openId() === menu.id"
15074
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15075
+ (click)="toggleMenu(menu.id, i)"
15076
+ (keydown)="onTopKey($event, i)"
15077
+ >
15078
+ {{ menu.label }}
15079
+ </button>
15080
+ @if (openId() === menu.id) {
15081
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15082
+ @for (item of menu.items; track $index) {
15083
+ @if (item.divider) {
15084
+ <div class="strct-mb__divider" role="separator"></div>
15085
+ } @else {
15086
+ <button
15087
+ type="button"
15088
+ class="strct-mb__item"
15089
+ role="menuitem"
15090
+ [class.strct-mb__item--critical]="item.critical"
15091
+ [disabled]="item.disabled || null"
15092
+ (click)="pick(menu, item)"
15093
+ >
15094
+ @if (item.icon) {
15095
+ <strct-icon [name]="item.icon" [size]="13" />
15096
+ }
15097
+ {{ item.label }}
15098
+ </button>
15099
+ }
15100
+ }
15101
+ </div>
15102
+ }
15103
+ </div>
15104
+ }
15105
+ </div>
15106
+ `, 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 });
15107
+ }
15108
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, decorators: [{
15109
+ type: Component,
15110
+ args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
15111
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
15112
+ @for (menu of menus(); track menu.id; let i = $index) {
15113
+ <div class="strct-mb__wrap">
15114
+ <button
15115
+ type="button"
15116
+ class="strct-mb__top"
15117
+ role="menuitem"
15118
+ [class.strct-mb__top--open]="openId() === menu.id"
15119
+ [attr.aria-haspopup]="'menu'"
15120
+ [attr.aria-expanded]="openId() === menu.id"
15121
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15122
+ (click)="toggleMenu(menu.id, i)"
15123
+ (keydown)="onTopKey($event, i)"
15124
+ >
15125
+ {{ menu.label }}
15126
+ </button>
15127
+ @if (openId() === menu.id) {
15128
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15129
+ @for (item of menu.items; track $index) {
15130
+ @if (item.divider) {
15131
+ <div class="strct-mb__divider" role="separator"></div>
15132
+ } @else {
15133
+ <button
15134
+ type="button"
15135
+ class="strct-mb__item"
15136
+ role="menuitem"
15137
+ [class.strct-mb__item--critical]="item.critical"
15138
+ [disabled]="item.disabled || null"
15139
+ (click)="pick(menu, item)"
15140
+ >
15141
+ @if (item.icon) {
15142
+ <strct-icon [name]="item.icon" [size]="13" />
15143
+ }
15144
+ {{ item.label }}
15145
+ </button>
15146
+ }
15147
+ }
15148
+ </div>
15149
+ }
15150
+ </div>
15151
+ }
15152
+ </div>
15153
+ `, 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"] }]
15154
+ }], 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: [{
15155
+ type: HostListener,
15156
+ args: ['document:click', ['$event']]
15157
+ }], onEscape: [{
15158
+ type: HostListener,
15159
+ args: ['document:keydown.escape']
15160
+ }] } });
15161
+
15162
+ /**
15163
+ * Screen-reader announcements for state changes that have no visible text —
15164
+ * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
15165
+ *
15166
+ * private announcer = inject(StrctAnnouncer);
15167
+ * this.announcer.announce('12 rows loaded');
15168
+ *
15169
+ * Maintains one visually-hidden live region per politeness level; repeated
15170
+ * identical messages are re-announced (the region is cleared first).
15171
+ */
15172
+ class StrctAnnouncer {
15173
+ regions = new Map();
15174
+ clearTimer;
15175
+ announce(message, politeness = 'polite') {
15176
+ const region = this.regionFor(politeness);
15177
+ // Clear first so identical consecutive messages still fire.
15178
+ region.textContent = '';
15179
+ setTimeout(() => (region.textContent = message));
15180
+ clearTimeout(this.clearTimer);
15181
+ // Stale announcements should not linger for the next SR user to stumble on.
15182
+ this.clearTimer = setTimeout(() => (region.textContent = ''), 10_000);
15183
+ }
15184
+ regionFor(politeness) {
15185
+ let region = this.regions.get(politeness);
15186
+ if (!region) {
15187
+ region = document.createElement('div');
15188
+ region.setAttribute('aria-live', politeness);
15189
+ region.setAttribute('aria-atomic', 'true');
15190
+ region.className = 'strct-announcer';
15191
+ region.style.cssText =
15192
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
15193
+ document.body.appendChild(region);
15194
+ this.regions.set(politeness, region);
15195
+ }
15196
+ return region;
15197
+ }
15198
+ ngOnDestroy() {
15199
+ clearTimeout(this.clearTimer);
15200
+ for (const region of this.regions.values())
15201
+ region.remove();
15202
+ this.regions.clear();
15203
+ }
15204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15205
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
15206
+ }
15207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, decorators: [{
15208
+ type: Injectable,
15209
+ args: [{ providedIn: 'root' }]
15210
+ }] });
15211
+
15212
+ function normalize(combo) {
15213
+ return combo
15214
+ .toLowerCase()
15215
+ .split('+')
15216
+ .map((p) => p.trim())
15217
+ .sort()
15218
+ .join('+');
15219
+ }
15220
+ function comboOf(event) {
15221
+ const parts = [];
15222
+ if (event.ctrlKey)
15223
+ parts.push('ctrl');
15224
+ if (event.metaKey)
15225
+ parts.push('meta');
15226
+ if (event.altKey)
15227
+ parts.push('alt');
15228
+ if (event.shiftKey && event.key.length > 1)
15229
+ parts.push('shift');
15230
+ parts.push(event.key.toLowerCase());
15231
+ return parts.sort().join('+');
15232
+ }
15233
+ /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
15234
+ function expand(combo) {
15235
+ const n = combo.toLowerCase();
15236
+ if (!n.includes('mod'))
15237
+ return [normalize(n)];
15238
+ return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
15239
+ }
15240
+ /**
15241
+ * Application hotkeys, centrally registered so they are discoverable — the
15242
+ * Blueprint pattern:
15243
+ *
15244
+ * private hotkeys = inject(StrctHotkeysService);
15245
+ * ngOnInit() { this.dispose = this.hotkeys.register(
15246
+ * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
15247
+ * handler: () => this.palette.open() }); }
15248
+ *
15249
+ * Combos: `'a'`, `'?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
15250
+ * Plain-key combos are suppressed while typing in inputs / textareas /
15251
+ * contenteditable; modifier combos always fire. Pair with
15252
+ * `<strct-hotkeys-help/>` for the ⌘/ ("?") cheatsheet overlay.
15253
+ */
15254
+ class StrctHotkeysService {
15255
+ registry = signal([], ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
15256
+ /** All registered hotkeys (for the help overlay). */
15257
+ hotkeys = this.registry.asReadonly();
15258
+ listener = (event) => this.onKeydown(event);
15259
+ constructor() {
15260
+ document.addEventListener('keydown', this.listener);
15261
+ }
15262
+ /** Register a hotkey; the returned function unregisters it. */
15263
+ register(hotkey) {
15264
+ this.registry.update((list) => [...list, hotkey]);
15265
+ return () => this.registry.update((list) => list.filter((h) => h !== hotkey));
15266
+ }
15267
+ onKeydown(event) {
15268
+ const combo = comboOf(event);
15269
+ const typing = isTyping(event);
15270
+ for (const hk of this.registry()) {
15271
+ if (!expand(hk.combo).includes(combo))
15272
+ continue;
15273
+ // While typing, only modifier combos may fire.
15274
+ if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
15275
+ continue;
15276
+ event.preventDefault();
15277
+ hk.handler(event);
15278
+ return;
15279
+ }
15280
+ }
15281
+ ngOnDestroy() {
15282
+ document.removeEventListener('keydown', this.listener);
15283
+ }
15284
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15285
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
15286
+ }
15287
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, decorators: [{
15288
+ type: Injectable,
15289
+ args: [{ providedIn: 'root' }]
15290
+ }], ctorParameters: () => [] });
15291
+ function isTyping(event) {
15292
+ const el = event.target;
15293
+ if (!el)
15294
+ return false;
15295
+ return (el.tagName === 'INPUT' ||
15296
+ el.tagName === 'TEXTAREA' ||
15297
+ el.tagName === 'SELECT' ||
15298
+ el.isContentEditable);
15299
+ }
15300
+ /**
15301
+ * The hotkey cheatsheet overlay. Mount once near the app root; it registers
15302
+ * `?` itself and lists everything in the service, grouped:
15303
+ *
15304
+ * <strct-hotkeys-help />
15305
+ */
15306
+ class StrctHotkeysHelp {
15307
+ service = inject(StrctHotkeysService);
15308
+ /** Overlay visibility (two-way; `?` toggles it too). */
15309
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
15310
+ /** Localizable strings. */
15311
+ title = input('Keyboard shortcuts', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
15312
+ emptyText = input('No shortcuts registered.', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
15313
+ grouped = computed(() => {
15314
+ const groups = new Map();
15315
+ for (const hk of this.service.hotkeys()) {
15316
+ const name = hk.group ?? 'General';
15317
+ const list = groups.get(name);
15318
+ if (list)
15319
+ list.push(hk);
15320
+ else
15321
+ groups.set(name, [hk]);
15322
+ }
15323
+ return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
15324
+ }, ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
15325
+ constructor() {
15326
+ const dispose = this.service.register({
15327
+ combo: '?',
15328
+ description: 'Show this help',
15329
+ group: 'General',
15330
+ handler: () => this.open.update((v) => !v),
15331
+ });
15332
+ inject(DestroyRef).onDestroy(dispose);
15333
+ }
15334
+ // Escape is handled locally — registering it in the service would swallow
15335
+ // every Escape in the app (the service preventDefaults on match).
15336
+ onEscape() {
15337
+ if (this.open())
15338
+ this.open.set(false);
15339
+ }
15340
+ parts(combo) {
15341
+ return combo.split('+').map((p) => {
15342
+ const map = {
15343
+ mod: '⌘/Ctrl',
15344
+ meta: '⌘',
15345
+ ctrl: 'Ctrl',
15346
+ alt: 'Alt',
15347
+ shift: 'Shift',
15348
+ escape: 'Esc',
15349
+ };
15350
+ return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
15351
+ });
15352
+ }
15353
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
15354
+ 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: `
15355
+ @if (open()) {
15356
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15357
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15358
+ <div class="strct-hkh__head">
15359
+ <span class="strct-hkh__title">{{ title() }}</span>
15360
+ <strct-kbd>?</strct-kbd>
15361
+ </div>
15362
+ @for (group of grouped(); track group.name) {
15363
+ <div class="strct-hkh__group">
15364
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15365
+ @for (hk of group.keys; track hk.combo + hk.description) {
15366
+ <div class="strct-hkh__row">
15367
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15368
+ <span class="strct-hkh__keys">
15369
+ @for (part of parts(hk.combo); track $index) {
15370
+ <strct-kbd>{{ part }}</strct-kbd>
15371
+ }
15372
+ </span>
15373
+ </div>
15374
+ }
15375
+ </div>
15376
+ } @empty {
15377
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15378
+ }
15379
+ </div>
15380
+ }
15381
+ `, 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 });
15382
+ }
15383
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, decorators: [{
15384
+ type: Component,
15385
+ args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctKbd], template: `
15386
+ @if (open()) {
15387
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15388
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15389
+ <div class="strct-hkh__head">
15390
+ <span class="strct-hkh__title">{{ title() }}</span>
15391
+ <strct-kbd>?</strct-kbd>
15392
+ </div>
15393
+ @for (group of grouped(); track group.name) {
15394
+ <div class="strct-hkh__group">
15395
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15396
+ @for (hk of group.keys; track hk.combo + hk.description) {
15397
+ <div class="strct-hkh__row">
15398
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15399
+ <span class="strct-hkh__keys">
15400
+ @for (part of parts(hk.combo); track $index) {
15401
+ <strct-kbd>{{ part }}</strct-kbd>
15402
+ }
15403
+ </span>
15404
+ </div>
15405
+ }
15406
+ </div>
15407
+ } @empty {
15408
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15409
+ }
15410
+ </div>
15411
+ }
15412
+ `, 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"] }]
15413
+ }], 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: [{
15414
+ type: HostListener,
15415
+ args: ['document:keydown.escape']
15416
+ }] } });
15417
+
15418
+ /**
15419
+ * List drag-reorder primitive — the consumer owns the array:
15420
+ *
15421
+ * <ul strctReorder (reordered)="move($event)">
15422
+ * @for (s of steps(); track s.id) {
15423
+ * <li strctReorderItem>{{ s.label }}</li>
15424
+ * }
15425
+ * </ul>
15426
+ *
15427
+ * move({ from, to }: StrctReorderEvent) {
15428
+ * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
15429
+ * }
15430
+ *
15431
+ * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
15432
+ * on the focused item (items get `tabindex="0"` unless they already manage
15433
+ * focus). Indexes are positions among the `strctReorderItem` siblings.
15434
+ */
15435
+ class StrctReorder {
15436
+ host = inject(ElementRef);
15437
+ /** Emits when a drag or keyboard move completes. */
15438
+ reordered = output();
15439
+ /** Disable all reordering (display-only mode). */
15440
+ reorderDisabled = input(false, ...(ngDevMode ? [{ debugName: "reorderDisabled" }] : /* istanbul ignore next */ []));
15441
+ dragIndex = signal(null, ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
15442
+ overIndex = signal(null, ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
15443
+ items() {
15444
+ return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
15445
+ }
15446
+ indexOf(el) {
15447
+ return this.items().indexOf(el);
15448
+ }
15449
+ commit(from, to) {
15450
+ this.dragIndex.set(null);
15451
+ this.overIndex.set(null);
15452
+ if (from < 0 || to < 0 || from === to)
15453
+ return;
15454
+ this.reordered.emit({ from, to });
15455
+ }
15456
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15457
+ 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 });
15458
+ }
15459
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, decorators: [{
15460
+ type: Directive,
15461
+ args: [{ selector: '[strctReorder]' }]
15462
+ }], propDecorators: { reordered: [{ type: i0.Output, args: ["reordered"] }], reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }] } });
15463
+ /** One draggable row inside a `[strctReorder]` container. */
15464
+ class StrctReorderItem {
15465
+ list = inject(StrctReorder);
15466
+ el = inject(ElementRef);
15467
+ // Captured before our own binding ever writes tabindex, so it never flips.
15468
+ hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
15469
+ hostTabindex() {
15470
+ return this.hadOwnTabindex ? null : 0;
15471
+ }
15472
+ isDragging() {
15473
+ return this.list.dragIndex() === this.index();
15474
+ }
15475
+ isOver() {
15476
+ return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
15477
+ }
15478
+ index() {
15479
+ return this.list.indexOf(this.el.nativeElement);
15480
+ }
15481
+ onDragStart(event) {
15482
+ if (this.list.reorderDisabled())
15483
+ return;
15484
+ this.list.dragIndex.set(this.index());
15485
+ event.dataTransfer?.setData('text/plain', String(this.index()));
15486
+ if (event.dataTransfer)
15487
+ event.dataTransfer.effectAllowed = 'move';
15488
+ }
15489
+ onDragOver(event) {
15490
+ if (this.list.dragIndex() == null)
15491
+ return;
15492
+ event.preventDefault();
15493
+ this.list.overIndex.set(this.index());
15494
+ }
15495
+ onDrop(event) {
15496
+ event.preventDefault();
15497
+ const from = this.list.dragIndex();
15498
+ if (from == null)
15499
+ return;
15500
+ this.list.commit(from, this.index());
15501
+ }
15502
+ onDragEnd() {
15503
+ this.list.dragIndex.set(null);
15504
+ this.list.overIndex.set(null);
15505
+ }
15506
+ onKeydown(event) {
15507
+ if (this.list.reorderDisabled() || !event.altKey)
15508
+ return;
15509
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
15510
+ return;
15511
+ event.preventDefault();
15512
+ const from = this.index();
15513
+ const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
15514
+ if (to < 0 || to >= this.list.items().length)
15515
+ return;
15516
+ this.list.commit(from, to);
15517
+ // Keep focus on the moved item after the consumer re-renders.
15518
+ setTimeout(() => this.list.items()[to]?.focus());
15519
+ }
15520
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15521
+ 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 });
15522
+ }
15523
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, decorators: [{
15524
+ type: Directive,
15525
+ args: [{
15526
+ selector: '[strctReorderItem]',
15527
+ host: {
15528
+ '[attr.draggable]': '!list.reorderDisabled()',
15529
+ '[attr.tabindex]': 'hostTabindex()',
15530
+ '[class.strct-reorder--dragging]': 'isDragging()',
15531
+ '[class.strct-reorder--over]': 'isOver()',
15532
+ '[attr.aria-roledescription]': "'sortable'",
15533
+ },
15534
+ }]
15535
+ }], propDecorators: { onDragStart: [{
15536
+ type: HostListener,
15537
+ args: ['dragstart', ['$event']]
15538
+ }], onDragOver: [{
15539
+ type: HostListener,
15540
+ args: ['dragover', ['$event']]
15541
+ }], onDrop: [{
15542
+ type: HostListener,
15543
+ args: ['drop', ['$event']]
15544
+ }], onDragEnd: [{
15545
+ type: HostListener,
15546
+ args: ['dragend']
15547
+ }], onKeydown: [{
15548
+ type: HostListener,
15549
+ args: ['keydown', ['$event']]
15550
+ }] } });
15551
+
12762
15552
  /**
12763
15553
  * Public API surface of the strct UI library.
12764
15554
  */
@@ -12768,5 +15558,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12768
15558
  * Generated bundle index. Do not edit.
12769
15559
  */
12770
15560
 
12771
- export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, 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 };
15561
+ 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 };
12772
15562
  //# sourceMappingURL=akcelik-strct.mjs.map