@akcelik/strct 1.6.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DOCUMENT, signal, computed, Injectable, input, 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,11 @@ 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',
8265
8291
  };
8266
8292
  /** Utility-column widths used when sticky columns are active (px). */
8267
8293
  const UTIL_W = { detail: 36, expand: 36, sel: 40 };
@@ -8389,6 +8415,19 @@ class StrctDatagrid {
8389
8415
  * groups. Paging is bypassed while grouped; not combinable with `virtual`.
8390
8416
  */
8391
8417
  groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
8418
+ /**
8419
+ * Two-way per-column filter state: `key → contains-text` (text filter) or
8420
+ * `key → unknown[]` (checked value set). Applied client-side; in `lazy`
8421
+ * mode it is passed through on `lazyLoad` instead.
8422
+ */
8423
+ filters = model({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
8424
+ /**
8425
+ * Tree-grid: the row property holding a row's children array. Rows render
8426
+ * hierarchically with indent + carets; sorting applies per sibling level;
8427
+ * an active filter shows matches with their ancestors, force-expanded.
8428
+ * Not combinable with `groupBy` or `lazy`.
8429
+ */
8430
+ childrenKey = input(null, ...(ngDevMode ? [{ debugName: "childrenKey" }] : /* istanbul ignore next */ []));
8392
8431
  /** Emitted when the selection changes. */
8393
8432
  selectionChange = output();
8394
8433
  /** Emitted when the refresh button is clicked. */
@@ -8397,6 +8436,9 @@ class StrctDatagrid {
8397
8436
  rowAction = output();
8398
8437
  /** Server-side data request (lazy mode): load this page / sort and set `rows`. */
8399
8438
  lazyLoad = output();
8439
+ /** An editable cell was committed (Enter / blur). The grid does not mutate
8440
+ * rows — apply `value` to your data and pass the updated array back in. */
8441
+ cellEdit = output();
8400
8442
  menu = inject(StrctMenuService);
8401
8443
  detailDef = contentChild(StrctRowDetailDef, ...(ngDevMode ? [{ debugName: "detailDef" }] : /* istanbul ignore next */ []));
8402
8444
  actionBarDef = contentChild(StrctDatagridActionBar, ...(ngDevMode ? [{ debugName: "actionBarDef" }] : /* istanbul ignore next */ []));
@@ -8493,6 +8535,11 @@ class StrctDatagrid {
8493
8535
  dragKey = signal(null, ...(ngDevMode ? [{ debugName: "dragKey" }] : /* istanbul ignore next */ []));
8494
8536
  dropKey = signal(null, ...(ngDevMode ? [{ debugName: "dropKey" }] : /* istanbul ignore next */ []));
8495
8537
  onColDragStart(key, event) {
8538
+ // Text selection inside the filter panel must not start a column drag.
8539
+ if (event.target?.closest?.('.strct-dg__filterpanel')) {
8540
+ event.preventDefault();
8541
+ return;
8542
+ }
8496
8543
  if (!this.reorderable())
8497
8544
  return;
8498
8545
  this.dragKey.set(key);
@@ -8562,12 +8609,154 @@ class StrctDatagrid {
8562
8609
  itemKey(it) {
8563
8610
  return it.group ? `strct-group:${String(it.group.key)}` : this.rowKey(it.row);
8564
8611
  }
8612
+ // ── Column filter popover ──────────────────────────────────────
8613
+ filterOpenKey = signal(null, ...(ngDevMode ? [{ debugName: "filterOpenKey" }] : /* istanbul ignore next */ []));
8614
+ toggleFilterPanel(key, event) {
8615
+ event.stopPropagation();
8616
+ this.filterOpenKey.set(this.filterOpenKey() === key ? null : key);
8617
+ }
8618
+ hasFilter(key) {
8619
+ const v = this.filters()[key];
8620
+ return Array.isArray(v) ? v.length > 0 : v != null && String(v).trim() !== '';
8621
+ }
8622
+ textFilter(key) {
8623
+ const v = this.filters()[key];
8624
+ return typeof v === 'string' ? v : '';
8625
+ }
8626
+ setTextFilter(key, value) {
8627
+ this.filters.update((f) => ({ ...f, [key]: value }));
8628
+ }
8629
+ optionChecked(key, opt) {
8630
+ const v = this.filters()[key];
8631
+ return Array.isArray(v) && v.includes(opt);
8632
+ }
8633
+ toggleFilterOption(key, opt) {
8634
+ this.filters.update((f) => {
8635
+ const cur = Array.isArray(f[key]) ? [...f[key]] : [];
8636
+ const i = cur.indexOf(opt);
8637
+ if (i >= 0)
8638
+ cur.splice(i, 1);
8639
+ else
8640
+ cur.push(opt);
8641
+ return { ...f, [key]: cur };
8642
+ });
8643
+ }
8644
+ clearFilter(key) {
8645
+ this.filters.update((f) => {
8646
+ const next = { ...f };
8647
+ delete next[key];
8648
+ return next;
8649
+ });
8650
+ }
8651
+ onDocClickFilter(event) {
8652
+ if (!this.filterOpenKey())
8653
+ return;
8654
+ const t = event.target;
8655
+ if (t.closest('.strct-dg__filterpanel') || t.closest('.strct-dg__filterbtn'))
8656
+ return;
8657
+ this.filterOpenKey.set(null);
8658
+ }
8659
+ onEscapeFilter() {
8660
+ this.filterOpenKey.set(null);
8661
+ }
8662
+ // ── Tree-grid state ────────────────────────────────────────────
8663
+ expandedTree = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedTree" }] : /* istanbul ignore next */ []));
8664
+ treeDepth(row) {
8665
+ return this.treeFlat().meta.get(this.rowKey(row))?.depth ?? 0;
8666
+ }
8667
+ treeHasChildren(row) {
8668
+ return this.treeFlat().meta.get(this.rowKey(row))?.hasChildren ?? false;
8669
+ }
8670
+ treeExpanded(row) {
8671
+ return this.treeFlat().meta.get(this.rowKey(row))?.expanded ?? false;
8672
+ }
8673
+ /** Expand / collapse one tree row. */
8674
+ toggleTreeRow(row) {
8675
+ const key = this.rowKey(row);
8676
+ const next = new Set(this.expandedTree());
8677
+ if (next.has(key))
8678
+ next.delete(key);
8679
+ else
8680
+ next.add(key);
8681
+ this.expandedTree.set(next);
8682
+ }
8683
+ // ── Inline cell editing ────────────────────────────────────────
8684
+ hostRef = inject(ElementRef);
8685
+ editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
8686
+ isEditingCell(row, col) {
8687
+ const e = this.editing();
8688
+ return !!e && e.rowKey === this.rowKey(row) && e.colKey === col.key;
8689
+ }
8690
+ startEdit(row, col) {
8691
+ this.editing.set({ rowKey: this.rowKey(row), colKey: col.key });
8692
+ setTimeout(() => this.hostRef.nativeElement.querySelector('.strct-dg__editinput')?.focus());
8693
+ }
8694
+ commitEdit(row, col, event) {
8695
+ if (!this.isEditingCell(row, col))
8696
+ return; // Escape/Enter already closed it
8697
+ const value = event.target.value;
8698
+ this.editing.set(null);
8699
+ const previous = row[col.key];
8700
+ if (String(previous ?? '') !== value)
8701
+ this.cellEdit.emit({ row, column: col, value, previous });
8702
+ }
8703
+ cancelEdit(event) {
8704
+ event?.stopPropagation();
8705
+ this.editing.set(null);
8706
+ }
8707
+ /** Active (non-empty) filter entries. */
8708
+ activeFilters = computed(() => Object.entries(this.filters()).filter(([, v]) => Array.isArray(v) ? v.length > 0 : String(v).trim() !== ''), ...(ngDevMode ? [{ debugName: "activeFilters" }] : /* istanbul ignore next */ []));
8709
+ matchesFilters(row) {
8710
+ return this.activeFilters().every(([key, v]) => Array.isArray(v)
8711
+ ? v.some((opt) => opt === row[key])
8712
+ : String(row[key] ?? '')
8713
+ .toLowerCase()
8714
+ .includes(String(v).trim().toLowerCase()));
8715
+ }
8716
+ /** Rows surviving the per-column filters (client-side modes only). */
8717
+ filteredRows = computed(() => {
8718
+ if (this.lazy() || !this.activeFilters().length)
8719
+ return this.rows();
8720
+ return this.rows().filter((r) => this.matchesFilters(r));
8721
+ }, ...(ngDevMode ? [{ debugName: "filteredRows" }] : /* istanbul ignore next */ []));
8722
+ /** Tree mode: flattened visible rows + per-row depth/children/expanded meta. */
8723
+ treeFlat = computed(() => {
8724
+ const ck = this.childrenKey();
8725
+ const { key, dir } = this.sort();
8726
+ const sign = dir === 'asc' ? 1 : -1;
8727
+ const sortRows = (rs) => {
8728
+ const c = [...rs];
8729
+ if (key)
8730
+ c.sort((a, b) => sign * this.compare(a[key], b[key]));
8731
+ return c;
8732
+ };
8733
+ const filtering = this.activeFilters().length > 0;
8734
+ const kidsOf = (r) => r[ck] ?? [];
8735
+ const survives = (r) => this.matchesFilters(r) || kidsOf(r).some(survives);
8736
+ const meta = new Map();
8737
+ const rowsOut = [];
8738
+ const walk = (rs, depth) => {
8739
+ for (const r of sortRows(rs)) {
8740
+ const kids = filtering ? kidsOf(r).filter(survives) : kidsOf(r);
8741
+ // Filtering force-expands so matches are never hidden under a fold.
8742
+ const expanded = filtering ? true : this.expandedTree().has(this.rowKey(r));
8743
+ meta.set(this.rowKey(r), { depth, hasChildren: kids.length > 0, expanded });
8744
+ rowsOut.push(r);
8745
+ if (kids.length && expanded)
8746
+ walk(kids, depth + 1);
8747
+ }
8748
+ };
8749
+ walk(filtering ? this.rows().filter(survives) : this.rows(), 0);
8750
+ return { rows: rowsOut, meta };
8751
+ }, ...(ngDevMode ? [{ debugName: "treeFlat" }] : /* istanbul ignore next */ []));
8565
8752
  sorted = computed(() => {
8566
8753
  // Server-side mode: rows arrive already ordered / sliced — never touch them.
8567
8754
  if (this.lazy())
8568
8755
  return this.rows();
8756
+ if (this.childrenKey())
8757
+ return this.treeFlat().rows;
8569
8758
  const { key, dir } = this.sort();
8570
- const data = [...this.rows()];
8759
+ const data = [...this.filteredRows()];
8571
8760
  if (!key)
8572
8761
  return data;
8573
8762
  const sign = dir === 'asc' ? 1 : -1;
@@ -8689,6 +8878,11 @@ class StrctDatagrid {
8689
8878
  if (this.page() > pageCount)
8690
8879
  this.page.set(pageCount);
8691
8880
  });
8881
+ // A changed filter always restarts from page 1.
8882
+ effect(() => {
8883
+ this.filters();
8884
+ untracked(() => this.page.set(1));
8885
+ });
8692
8886
  // Seed the selection from initialSelection (re-runs only when the input changes,
8693
8887
  // so a user's own toggles afterward are untouched).
8694
8888
  effect(() => {
@@ -8707,6 +8901,7 @@ class StrctDatagrid {
8707
8901
  pageSize: this.pageSize(),
8708
8902
  sortKey: this.sort().key,
8709
8903
  sortDir: this.sort().dir,
8904
+ filters: this.filters(),
8710
8905
  };
8711
8906
  const key = JSON.stringify(state);
8712
8907
  untracked(() => {
@@ -8953,7 +9148,7 @@ class StrctDatagrid {
8953
9148
  return String(a ?? '').localeCompare(String(b ?? ''), undefined, { numeric: true });
8954
9149
  }
8955
9150
  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: `
9151
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDatagrid, isStandalone: true, selector: "strct-datagrid", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, detailPane: { classPropertyName: "detailPane", publicName: "detailPane", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, singleLine: { classPropertyName: "singleLine", publicName: "singleLine", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, columnChooser: { classPropertyName: "columnChooser", publicName: "columnChooser", isSignal: true, isRequired: false, transformFunction: null }, sync: { classPropertyName: "sync", publicName: "sync", isSignal: true, isRequired: false, transformFunction: null }, footerActionsDisabled: { classPropertyName: "footerActionsDisabled", publicName: "footerActionsDisabled", isSignal: true, isRequired: false, transformFunction: null }, rowId: { classPropertyName: "rowId", publicName: "rowId", isSignal: true, isRequired: false, transformFunction: null }, rowActions: { classPropertyName: "rowActions", publicName: "rowActions", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, virtual: { classPropertyName: "virtual", publicName: "virtual", isSignal: true, isRequired: false, transformFunction: null }, viewportHeight: { classPropertyName: "viewportHeight", publicName: "viewportHeight", isSignal: true, isRequired: false, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: false, transformFunction: null }, stateKey: { classPropertyName: "stateKey", publicName: "stateKey", isSignal: true, isRequired: false, transformFunction: null }, columnState: { classPropertyName: "columnState", publicName: "columnState", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, childrenKey: { classPropertyName: "childrenKey", publicName: "childrenKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { columnState: "columnStateChange", filters: "filtersChange", selectionChange: "selectionChange", syncChange: "syncChange", rowAction: "rowAction", lazyLoad: "lazyLoad", cellEdit: "cellEdit" }, host: { listeners: { "document:click": "onDocClickFilter($event)", "document:keydown.escape": "onEscapeFilter()" }, properties: { "class.strct-dg-host--compact": "compact()", "class.strct-dg-host--singleline": "singleLine()", "class.strct-dg-host--virtual": "virtual()", "class.strct-dg-host--sticky": "stickyActive()" }, classAttribute: "strct-dg-host" }, queries: [{ propertyName: "detailDef", first: true, predicate: StrctRowDetailDef, descendants: true, isSignal: true }, { propertyName: "actionBarDef", first: true, predicate: StrctDatagridActionBar, descendants: true, isSignal: true }, { propertyName: "cellDefs", predicate: StrctCellDef, isSignal: true }], ngImport: i0, template: `
8957
9152
  @if (actionBarDef()) {
8958
9153
  <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
8959
9154
  }
@@ -8967,7 +9162,7 @@ class StrctDatagrid {
8967
9162
  [style.max-height.px]="virtual() ? viewportHeight() : null"
8968
9163
  (scroll)="virtual() && onScroll($event)"
8969
9164
  >
8970
- <table class="strct-dg">
9165
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
8971
9166
  <thead>
8972
9167
  <tr>
8973
9168
  @if (canDetail()) {
@@ -9027,6 +9222,62 @@ class StrctDatagrid {
9027
9222
  [size]="13"
9028
9223
  />
9029
9224
  }
9225
+ @if (col.filterable || col.filterOptions) {
9226
+ <button
9227
+ #fbtn
9228
+ type="button"
9229
+ class="strct-dg__filterbtn"
9230
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9231
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9232
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9233
+ (click)="toggleFilterPanel(col.key, $event)"
9234
+ (keydown.enter)="$event.stopPropagation()"
9235
+ (keydown.space)="$event.stopPropagation()"
9236
+ >
9237
+ <strct-icon name="filter" [size]="12" />
9238
+ </button>
9239
+ @if (filterOpenKey() === col.key) {
9240
+ <div
9241
+ class="strct-dg__filterpanel"
9242
+ role="dialog"
9243
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9244
+ [strctOverlay]="fbtn"
9245
+ strctOverlayPlacement="bottom-start"
9246
+ (click)="$event.stopPropagation()"
9247
+ (keydown.enter)="$event.stopPropagation()"
9248
+ (keydown.space)="$event.stopPropagation()"
9249
+ >
9250
+ @if (col.filterOptions; as opts) {
9251
+ @for (opt of opts; track $index) {
9252
+ <label class="strct-dg__filteropt">
9253
+ <input
9254
+ type="checkbox"
9255
+ [checked]="optionChecked(col.key, opt)"
9256
+ (change)="toggleFilterOption(col.key, opt)"
9257
+ />
9258
+ <span>{{ opt }}</span>
9259
+ </label>
9260
+ }
9261
+ } @else {
9262
+ <input
9263
+ class="strct-dg__filterinput"
9264
+ type="text"
9265
+ [attr.placeholder]="L().filterPlaceholder"
9266
+ [value]="textFilter(col.key)"
9267
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9268
+ />
9269
+ }
9270
+ <button
9271
+ type="button"
9272
+ class="strct-dg__filterclear"
9273
+ [disabled]="!hasFilter(col.key)"
9274
+ (click)="clearFilter(col.key)"
9275
+ >
9276
+ {{ L().clearFilter }}
9277
+ </button>
9278
+ </div>
9279
+ }
9280
+ }
9030
9281
  </span>
9031
9282
  @if (resizable() && col.resizable !== false) {
9032
9283
  <div
@@ -9097,6 +9348,10 @@ class StrctDatagrid {
9097
9348
  <tr
9098
9349
  [class.strct-dg__row--selected]="isSelected(row)"
9099
9350
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9351
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9352
+ [attr.aria-expanded]="
9353
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9354
+ "
9100
9355
  >
9101
9356
  @if (canDetail()) {
9102
9357
  <td
@@ -9147,14 +9402,51 @@ class StrctDatagrid {
9147
9402
  />
9148
9403
  </td>
9149
9404
  }
9150
- @for (col of visibleColumns(); track col.key) {
9405
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9151
9406
  <td
9152
9407
  [style.text-align]="col.align ?? 'start'"
9153
9408
  [class.strct-dg__cell--sticky]="isSticky(col)"
9154
9409
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9410
+ [class.strct-dg__cell--editable]="col.editable"
9155
9411
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9412
+ (dblclick)="col.editable && startEdit(row, col)"
9156
9413
  >
9157
- @if (cellTemplate(col.key); as tpl) {
9414
+ @if (childrenKey() && colIdx === 0) {
9415
+ <span
9416
+ class="strct-dg__treepad"
9417
+ [style.width.px]="treeDepth(row) * 18"
9418
+ aria-hidden="true"
9419
+ ></span>
9420
+ @if (treeHasChildren(row)) {
9421
+ <button
9422
+ type="button"
9423
+ class="strct-dg__treebtn"
9424
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9425
+ [attr.aria-expanded]="treeExpanded(row)"
9426
+ [attr.aria-label]="L().toggleChildren"
9427
+ (click)="toggleTreeRow(row)"
9428
+ >
9429
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9430
+ </button>
9431
+ } @else {
9432
+ <span
9433
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9434
+ aria-hidden="true"
9435
+ ></span>
9436
+ }
9437
+ }
9438
+ @if (isEditingCell(row, col)) {
9439
+ <input
9440
+ class="strct-dg__editinput"
9441
+ type="text"
9442
+ [value]="row[col.key] ?? ''"
9443
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9444
+ (keydown.enter)="commitEdit(row, col, $event)"
9445
+ (keydown.escape)="cancelEdit($event)"
9446
+ (blur)="commitEdit(row, col, $event)"
9447
+ (click)="$event.stopPropagation()"
9448
+ />
9449
+ } @else if (cellTemplate(col.key); as tpl) {
9158
9450
  <ng-container
9159
9451
  [ngTemplateOutlet]="tpl"
9160
9452
  [ngTemplateOutletContext]="{
@@ -9292,7 +9584,7 @@ class StrctDatagrid {
9292
9584
  </div>
9293
9585
  </div>
9294
9586
  }
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 });
9587
+ `, isInline: true, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;right:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:210;display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctPagination, selector: "strct-pagination", inputs: ["prevLabel", "nextLabel", "regionLabel", "total", "pageSize", "page"], outputs: ["pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "isDisabled", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange", "isDisabledChange"] }, { kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctButtonGroup, selector: "strct-button-group" }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9296
9588
  }
9297
9589
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDatagrid, decorators: [{
9298
9590
  type: Component,
@@ -9303,6 +9595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9303
9595
  StrctCheckbox,
9304
9596
  StrctButton,
9305
9597
  StrctButtonGroup,
9598
+ StrctOverlay,
9306
9599
  ], template: `
9307
9600
  @if (actionBarDef()) {
9308
9601
  <div class="strct-dg__toolbar"><ng-content select="[strctDatagridActionBar]" /></div>
@@ -9317,7 +9610,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9317
9610
  [style.max-height.px]="virtual() ? viewportHeight() : null"
9318
9611
  (scroll)="virtual() && onScroll($event)"
9319
9612
  >
9320
- <table class="strct-dg">
9613
+ <table class="strct-dg" [attr.role]="childrenKey() ? 'treegrid' : null">
9321
9614
  <thead>
9322
9615
  <tr>
9323
9616
  @if (canDetail()) {
@@ -9377,6 +9670,62 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9377
9670
  [size]="13"
9378
9671
  />
9379
9672
  }
9673
+ @if (col.filterable || col.filterOptions) {
9674
+ <button
9675
+ #fbtn
9676
+ type="button"
9677
+ class="strct-dg__filterbtn"
9678
+ [class.strct-dg__filterbtn--active]="hasFilter(col.key)"
9679
+ [attr.aria-expanded]="filterOpenKey() === col.key"
9680
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9681
+ (click)="toggleFilterPanel(col.key, $event)"
9682
+ (keydown.enter)="$event.stopPropagation()"
9683
+ (keydown.space)="$event.stopPropagation()"
9684
+ >
9685
+ <strct-icon name="filter" [size]="12" />
9686
+ </button>
9687
+ @if (filterOpenKey() === col.key) {
9688
+ <div
9689
+ class="strct-dg__filterpanel"
9690
+ role="dialog"
9691
+ [attr.aria-label]="L().filterColumn + ': ' + col.label"
9692
+ [strctOverlay]="fbtn"
9693
+ strctOverlayPlacement="bottom-start"
9694
+ (click)="$event.stopPropagation()"
9695
+ (keydown.enter)="$event.stopPropagation()"
9696
+ (keydown.space)="$event.stopPropagation()"
9697
+ >
9698
+ @if (col.filterOptions; as opts) {
9699
+ @for (opt of opts; track $index) {
9700
+ <label class="strct-dg__filteropt">
9701
+ <input
9702
+ type="checkbox"
9703
+ [checked]="optionChecked(col.key, opt)"
9704
+ (change)="toggleFilterOption(col.key, opt)"
9705
+ />
9706
+ <span>{{ opt }}</span>
9707
+ </label>
9708
+ }
9709
+ } @else {
9710
+ <input
9711
+ class="strct-dg__filterinput"
9712
+ type="text"
9713
+ [attr.placeholder]="L().filterPlaceholder"
9714
+ [value]="textFilter(col.key)"
9715
+ (input)="setTextFilter(col.key, $any($event.target).value)"
9716
+ />
9717
+ }
9718
+ <button
9719
+ type="button"
9720
+ class="strct-dg__filterclear"
9721
+ [disabled]="!hasFilter(col.key)"
9722
+ (click)="clearFilter(col.key)"
9723
+ >
9724
+ {{ L().clearFilter }}
9725
+ </button>
9726
+ </div>
9727
+ }
9728
+ }
9380
9729
  </span>
9381
9730
  @if (resizable() && col.resizable !== false) {
9382
9731
  <div
@@ -9447,6 +9796,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9447
9796
  <tr
9448
9797
  [class.strct-dg__row--selected]="isSelected(row)"
9449
9798
  [class.strct-dg__row--active]="paneOpen() && row === activeRow()"
9799
+ [attr.aria-level]="childrenKey() ? treeDepth(row) + 1 : null"
9800
+ [attr.aria-expanded]="
9801
+ childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null
9802
+ "
9450
9803
  >
9451
9804
  @if (canDetail()) {
9452
9805
  <td
@@ -9497,14 +9850,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9497
9850
  />
9498
9851
  </td>
9499
9852
  }
9500
- @for (col of visibleColumns(); track col.key) {
9853
+ @for (col of visibleColumns(); track col.key; let colIdx = $index) {
9501
9854
  <td
9502
9855
  [style.text-align]="col.align ?? 'start'"
9503
9856
  [class.strct-dg__cell--sticky]="isSticky(col)"
9504
9857
  [class.strct-dg__cell--sticky-last]="col.key === lastStickyKey()"
9858
+ [class.strct-dg__cell--editable]="col.editable"
9505
9859
  [style.insetInlineStart.px]="stickyLeft(col.key)"
9860
+ (dblclick)="col.editable && startEdit(row, col)"
9506
9861
  >
9507
- @if (cellTemplate(col.key); as tpl) {
9862
+ @if (childrenKey() && colIdx === 0) {
9863
+ <span
9864
+ class="strct-dg__treepad"
9865
+ [style.width.px]="treeDepth(row) * 18"
9866
+ aria-hidden="true"
9867
+ ></span>
9868
+ @if (treeHasChildren(row)) {
9869
+ <button
9870
+ type="button"
9871
+ class="strct-dg__treebtn"
9872
+ [class.strct-dg__treebtn--open]="treeExpanded(row)"
9873
+ [attr.aria-expanded]="treeExpanded(row)"
9874
+ [attr.aria-label]="L().toggleChildren"
9875
+ (click)="toggleTreeRow(row)"
9876
+ >
9877
+ <strct-icon name="chevronRight" [size]="12" [strokeWidth]="1.7" />
9878
+ </button>
9879
+ } @else {
9880
+ <span
9881
+ class="strct-dg__treebtn strct-dg__treebtn--leaf"
9882
+ aria-hidden="true"
9883
+ ></span>
9884
+ }
9885
+ }
9886
+ @if (isEditingCell(row, col)) {
9887
+ <input
9888
+ class="strct-dg__editinput"
9889
+ type="text"
9890
+ [value]="row[col.key] ?? ''"
9891
+ [attr.aria-label]="L().editCell + ': ' + col.label"
9892
+ (keydown.enter)="commitEdit(row, col, $event)"
9893
+ (keydown.escape)="cancelEdit($event)"
9894
+ (blur)="commitEdit(row, col, $event)"
9895
+ (click)="$event.stopPropagation()"
9896
+ />
9897
+ } @else if (cellTemplate(col.key); as tpl) {
9508
9898
  <ng-container
9509
9899
  [ngTemplateOutlet]="tpl"
9510
9900
  [ngTemplateOutletContext]="{
@@ -9648,8 +10038,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
9648
10038
  '[class.strct-dg-host--singleline]': 'singleLine()',
9649
10039
  '[class.strct-dg-host--virtual]': 'virtual()',
9650
10040
  '[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 }] }] } });
10041
+ }, styles: [".strct-dg-host{display:block;border:1px solid var(--b2);border-radius:10px;background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th{position:sticky;top:0;z-index:5}.strct-dg__vspacer td{padding:0;border:0;background:transparent}.strct-dg th[draggable]{cursor:grab}.strct-dg__th--drop{box-shadow:inset 3px 0 0 var(--acc)}.strct-dg__grouprow td{background:var(--bg-2)!important;padding:0}.strct-dg__groupbtn{display:flex;align-items:center;gap:8px;width:100%;padding:7px 13px;border:0;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;text-align:start}.strct-dg__groupbtn:hover{background:var(--bg-3)}.strct-dg__groupbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__groupchev{display:inline-flex;color:var(--t3);transition:transform .15s ease}.strct-dg__groupchev--open{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.strct-dg__groupchev{transition:none}}.strct-dg__groupcount{min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;border-radius:9px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg{border-collapse:separate;border-spacing:0;overflow:visible;border-radius:0}.strct-dg-host--sticky .strct-dg{width:max-content;min-width:100%}.strct-dg .strct-dg__cell--sticky{position:sticky;z-index:3}.strct-dg thead th.strct-dg__cell--sticky{z-index:6}.strct-dg .strct-dg__cell--sticky-last:after{content:\"\";position:absolute;top:0;bottom:0;inset-inline-end:-1px;width:7px;pointer-events:none;background:linear-gradient(to right,rgba(0,0,0,.14),transparent)}[dir=rtl] .strct-dg .strct-dg__cell--sticky-last:after{background:linear-gradient(to left,rgba(0,0,0,.14),transparent)}.strct-dg-host--sticky .strct-dg__expandcol,.strct-dg-host--sticky .strct-dg__expandcell{width:36px;min-width:36px;max-width:36px;box-sizing:border-box}.strct-dg-host--sticky .strct-dg__sel{width:40px;min-width:40px;max-width:40px;box-sizing:border-box}.strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__cell--sticky{background:var(--bg-3)}.strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody tr:hover .strct-dg__cell--sticky{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg-host--sticky tbody .strct-dg__row--selected .strct-dg__cell--sticky{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg{width:100%;border-collapse:collapse;font-size:13px;overflow:hidden;background:var(--bg-2)}.strct-dg-host:not(:has(.strct-dg__toolbar)) .strct-dg{border-start-start-radius:9px;border-start-end-radius:9px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:9px;border-end-end-radius:9px}.strct-dg th,.strct-dg td{padding:9px 13px;text-align:start;border-bottom:1px solid var(--b1)}.strct-dg tbody td{background:var(--bg-1)}[data-theme=dark] .strct-dg tbody td{background:var(--bg-3)}.strct-dg-host--compact .strct-dg th,.strct-dg-host--compact .strct-dg td{padding:5px 11px}.strct-dg-host--singleline .strct-dg tbody tr:not(.strct-dg__detailrow)>td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.strct-dg th{position:relative;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--t2);background:var(--bg-2);white-space:nowrap;-webkit-user-select:none;user-select:none}.strct-dg__th--sortable{cursor:pointer}.strct-dg__th--sortable:hover{color:var(--t1)}.strct-dg__th--sortable:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-dg__hd{display:inline-flex;align-items:center;gap:5px}.strct-dg__sorticon{color:var(--t3)}.strct-dg__th--sortable:hover .strct-dg__sorticon{color:var(--acc)}.strct-dg__resize{position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;cursor:col-resize;background:transparent;z-index:2}.strct-dg__resize:hover{background:var(--acc)}.strct-dg td{color:var(--t1)}.strct-dg tbody tr:last-child td{border-bottom:0}.strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:var(--acc-s)}.strct-dg__row--selected td{background:var(--acc-m)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel input{accent-color:var(--acc);width:15px;height:15px;cursor:pointer}.strct-dg__expandcol,.strct-dg__expandcell{width:1%;white-space:nowrap}.strct-dg__actioncol,.strct-dg__actioncell{width:1%;white-space:nowrap;text-align:end}.strct-dg__kebab{display:inline-flex;padding:4px;border:0;border-radius:5px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__kebab:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:transform .15s ease,color .15s ease}.strct-dg__expandbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__expandbtn--open{transform:rotate(90deg);color:var(--acc)}.strct-dg__detailbtn{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;transition:color .14s ease,background .14s ease}.strct-dg__detailbtn:hover{color:var(--acc);background:var(--bg-3)}.strct-dg__detailbtn--active{color:var(--acc);background:var(--acc-m)}.strct-dg__detailrow td{background:var(--bg-2);padding:0}.strct-dg__detail{padding:14px 16px;font-size:13px;color:var(--t2)}.strct-dg__layout{display:flex;align-items:flex-start;min-width:0}.strct-dg__layout--paned{gap:0}.strct-dg__layout--paned .strct-dg__scroll{flex:0 0 auto;width:260px;min-width:260px;max-width:260px}.strct-dg__layout--paned .strct-dg{width:260px;min-width:260px;max-width:260px;flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0}.strct-dg__row--clickable{cursor:pointer}.strct-dg__row--active td{background:var(--acc-m)}.strct-dg__layout--paned .strct-dg__row--active td:last-child{position:relative;padding-inline-end:26px}.strct-dg__layout--paned .strct-dg__row--active td:last-child:after{content:\"\";position:absolute;right:11px;top:50%;width:6px;height:6px;border-top:1.6px solid var(--acc);border-inline-end:1.6px solid var(--acc);transform:translateY(-50%) rotate(45deg)}.strct-dg__pane{flex:1;min-width:0;align-self:stretch;background:var(--bg-1);border:1px solid var(--b2);border-inline-start:2px solid var(--acc);border-radius:0 8px 8px 0;overflow:hidden;animation:strct-dg-pane-in .14s ease}.strct-dg__pane-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--b1);font-size:13px;font-weight:600;color:var(--t1)}.strct-dg__pane-close{display:inline-flex;padding:3px;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer}.strct-dg__pane-close:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__pane-body{padding:14px 16px;font-size:13px;color:var(--t2)}@keyframes strct-dg-pane-in{0%{opacity:0;transform:translate(8px)}}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-dg__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t3);padding:22px}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);left:0;z-index:10;min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:8px;box-shadow:var(--shadow-pop);padding:6px;display:flex;flex-direction:column;gap:2px}.strct-dg__chooser-menu--right{inset-inline-start:auto;inset-inline-end:0}.strct-dg__chooser-item{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s ease;-webkit-user-select:none;user-select:none}.strct-dg__chooser-item:hover{background:var(--bg-3)}.strct-dg__count{font-size:12px;color:var(--t2)}.strct-dg__count-sep{margin:0 8px;color:var(--b2)}.strct-dg__count-sel{color:var(--acc)}.strct-dg__filterbtn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__filterbtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__filterbtn--active{color:var(--acc)}.strct-dg__filterbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filterpanel{position:absolute;z-index:210;display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);font-weight:400;text-transform:none;letter-spacing:normal;white-space:nowrap}.strct-dg__filterinput{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-dg__filterinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__filteropt{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--t1);cursor:pointer}.strct-dg__filterclear{align-self:flex-start;padding:3px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:transparent;color:var(--t2);font-family:var(--font);font-size:11.5px;cursor:pointer}.strct-dg__filterclear:hover:not(:disabled){color:var(--t1);background:var(--bg-3)}.strct-dg__filterclear:disabled{color:var(--t4);cursor:default}.strct-dg__treepad{display:inline-block;height:1px;vertical-align:middle}.strct-dg__treebtn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-inline-end:4px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--t3);cursor:pointer;vertical-align:middle}.strct-dg__treebtn:hover{color:var(--t1);background:var(--bg-3)}.strct-dg__treebtn:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-dg__treebtn strct-icon{transition:transform .15s ease}.strct-dg__treebtn--open strct-icon{transform:rotate(90deg)}.strct-dg__treebtn--leaf{cursor:default;pointer-events:none}@media(prefers-reduced-motion:reduce){.strct-dg__treebtn strct-icon{transition:none}}.strct-dg__cell--editable{cursor:text}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__editinput{width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid var(--acc);border-radius:var(--radius-sm);background:var(--bg-1);color:var(--t1);font:inherit}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"] }]
10042
+ }], ctorParameters: () => [], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], detailPane: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailPane", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], singleLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleLine", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], columnChooser: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnChooser", required: false }] }], sync: [{ type: i0.Input, args: [{ isSignal: true, alias: "sync", required: false }] }], footerActionsDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerActionsDisabled", required: false }] }], rowId: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowId", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], virtual: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtual", required: false }] }], viewportHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewportHeight", required: false }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], stateKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateKey", required: false }] }], columnState: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnState", required: false }] }, { type: i0.Output, args: ["columnStateChange"] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }], childrenKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "childrenKey", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], syncChange: [{ type: i0.Output, args: ["syncChange"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], cellEdit: [{ type: i0.Output, args: ["cellEdit"] }], detailDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctRowDetailDef), { isSignal: true }] }], actionBarDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => StrctDatagridActionBar), { isSignal: true }] }], cellDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctCellDef), { isSignal: true }] }], onDocClickFilter: [{
10043
+ type: HostListener,
10044
+ args: ['document:click', ['$event']]
10045
+ }], onEscapeFilter: [{
10046
+ type: HostListener,
10047
+ args: ['document:keydown.escape']
10048
+ }] } });
9653
10049
 
9654
10050
  /** Vertical timeline container. Wraps `<strct-timeline-item>` children. */
9655
10051
  class StrctTimeline {
@@ -12759,6 +13155,2326 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12759
13155
  `, host: { class: 'strct-empty' }, styles: [".strct-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;padding:40px 24px}.strct-empty__icon{display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:14px;margin-bottom:2px}.strct-empty__icon--neutral{background:var(--bg-3);color:var(--t2)}.strct-empty__icon--warning{background:var(--warning-bg);color:var(--warning)}.strct-empty__icon--critical{background:var(--critical-bg);color:var(--critical)}.strct-empty__icon--accent{background:var(--acc-m);color:var(--acc)}.strct-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--t1)}.strct-empty__desc{margin:0;max-width:360px;font-size:13px;line-height:1.5;color:var(--t3)}.strct-empty__actions{display:flex;align-items:center;gap:8px;margin-top:6px}.strct-empty__actions:empty{display:none}\n"] }]
12760
13156
  }], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }] } });
12761
13157
 
13158
+ /**
13159
+ * Unit formatting for infrastructure consoles — bytes, rates, durations and
13160
+ * SI magnitudes, as pure functions plus template pipes. The functions are the
13161
+ * API charts / grids can call from TS; the pipes are the template sugar:
13162
+ *
13163
+ * {{ vm.memBytes | strctBytes }} → 1.5 GiB
13164
+ * {{ nic.rxBps | strctRate }} → 2.4 Gbit/s
13165
+ * {{ task.elapsedMs | strctDuration }} → 2h 14m
13166
+ * {{ cluster.iops | strctSi : 'IOPS' }} → 12.4k IOPS
13167
+ */
13168
+ const BINARY_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
13169
+ const DECIMAL_UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB'];
13170
+ const SI_SUFFIXES = ['', 'k', 'M', 'G', 'T', 'P'];
13171
+ function trimZeros(v) {
13172
+ return v.includes('.') ? v.replace(/\.?0+$/, '') : v;
13173
+ }
13174
+ /**
13175
+ * Format a byte count. Binary (KiB, 1024) by default — the storage/memory
13176
+ * convention consoles actually mean; pass `binary: false` for decimal (kB).
13177
+ */
13178
+ function strctFormatBytes(bytes, options = {}) {
13179
+ if (!Number.isFinite(bytes))
13180
+ return '—';
13181
+ const { binary = true, digits = 1 } = options;
13182
+ const base = binary ? 1024 : 1000;
13183
+ const units = binary ? BINARY_UNITS : DECIMAL_UNITS;
13184
+ const neg = bytes < 0 ? '-' : '';
13185
+ let v = Math.abs(bytes);
13186
+ let i = 0;
13187
+ while (v >= base && i < units.length - 1) {
13188
+ v /= base;
13189
+ i++;
13190
+ }
13191
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13192
+ }
13193
+ /**
13194
+ * Format a data rate from bits per second (network convention). Pass bytes/s
13195
+ * through `strctFormatBytes(...) + '/s'` instead if you mean throughput.
13196
+ */
13197
+ function strctFormatRate(bitsPerSecond, options = {}) {
13198
+ if (!Number.isFinite(bitsPerSecond))
13199
+ return '—';
13200
+ const { digits = 1 } = options;
13201
+ const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s', 'Tbit/s'];
13202
+ const neg = bitsPerSecond < 0 ? '-' : '';
13203
+ let v = Math.abs(bitsPerSecond);
13204
+ let i = 0;
13205
+ while (v >= 1000 && i < units.length - 1) {
13206
+ v /= 1000;
13207
+ i++;
13208
+ }
13209
+ return `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))} ${units[i]}`;
13210
+ }
13211
+ /**
13212
+ * Format a duration in ms as the two most significant units: "2h 14m",
13213
+ * "3d 6h", "45s", "820ms". Sub-second stays in ms; `maxUnits` widens it.
13214
+ */
13215
+ function strctFormatDuration(ms, options = {}) {
13216
+ if (!Number.isFinite(ms))
13217
+ return '—';
13218
+ const { maxUnits = 2 } = options;
13219
+ const neg = ms < 0 ? '-' : '';
13220
+ let v = Math.abs(ms);
13221
+ if (v < 1000)
13222
+ return `${neg}${Math.round(v)}ms`;
13223
+ const units = [
13224
+ ['d', 86_400_000],
13225
+ ['h', 3_600_000],
13226
+ ['m', 60_000],
13227
+ ['s', 1000],
13228
+ ];
13229
+ const parts = [];
13230
+ for (const [label, size] of units) {
13231
+ if (parts.length >= maxUnits)
13232
+ break;
13233
+ const n = Math.floor(v / size);
13234
+ if (n > 0 || parts.length > 0) {
13235
+ if (n > 0)
13236
+ parts.push(`${n}${label}`);
13237
+ v -= n * size;
13238
+ }
13239
+ }
13240
+ return neg + (parts.length ? parts.join(' ') : '0s');
13241
+ }
13242
+ /** Format a count with SI magnitude suffixes: 12400 → "12.4k". */
13243
+ function strctFormatSi(value, options = {}) {
13244
+ if (!Number.isFinite(value))
13245
+ return '—';
13246
+ const { digits = 1, unit = '' } = options;
13247
+ const neg = value < 0 ? '-' : '';
13248
+ let v = Math.abs(value);
13249
+ let i = 0;
13250
+ while (v >= 1000 && i < SI_SUFFIXES.length - 1) {
13251
+ v /= 1000;
13252
+ i++;
13253
+ }
13254
+ const num = `${neg}${trimZeros(v.toFixed(i === 0 ? 0 : digits))}${SI_SUFFIXES[i]}`;
13255
+ return unit ? `${num} ${unit}` : num;
13256
+ }
13257
+ /** `{{ bytes | strctBytes }}` / `{{ bytes | strctBytes : false }}` (decimal). */
13258
+ class StrctBytesPipe {
13259
+ transform(value, binary = true, digits = 1) {
13260
+ return strctFormatBytes(value, { binary, digits });
13261
+ }
13262
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13263
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, isStandalone: true, name: "strctBytes" });
13264
+ }
13265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctBytesPipe, decorators: [{
13266
+ type: Pipe,
13267
+ args: [{ name: 'strctBytes' }]
13268
+ }] });
13269
+ /** `{{ bps | strctRate }}` — bits per second with kbit/Mbit/Gbit steps. */
13270
+ class StrctRatePipe {
13271
+ transform(value, digits = 1) {
13272
+ return strctFormatRate(value, { digits });
13273
+ }
13274
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13275
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, isStandalone: true, name: "strctRate" });
13276
+ }
13277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctRatePipe, decorators: [{
13278
+ type: Pipe,
13279
+ args: [{ name: 'strctRate' }]
13280
+ }] });
13281
+ /** `{{ ms | strctDuration }}` — "2h 14m"; optional max unit count. */
13282
+ class StrctDurationPipe {
13283
+ transform(value, maxUnits = 2) {
13284
+ return strctFormatDuration(value, { maxUnits });
13285
+ }
13286
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13287
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, isStandalone: true, name: "strctDuration" });
13288
+ }
13289
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDurationPipe, decorators: [{
13290
+ type: Pipe,
13291
+ args: [{ name: 'strctDuration' }]
13292
+ }] });
13293
+ /** `{{ n | strctSi : 'IOPS' }}` — SI magnitude with an optional unit label. */
13294
+ class StrctSiPipe {
13295
+ transform(value, unit = '', digits = 1) {
13296
+ return strctFormatSi(value, { unit, digits });
13297
+ }
13298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
13299
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, isStandalone: true, name: "strctSi" });
13300
+ }
13301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSiPipe, decorators: [{
13302
+ type: Pipe,
13303
+ args: [{ name: 'strctSi' }]
13304
+ }] });
13305
+
13306
+ /** The Grafana-conventional quick ranges consoles expect. */
13307
+ const STRCT_TIME_RANGE_PRESETS = [
13308
+ { id: '15m', label: 'Last 15 minutes', ms: 15 * 60_000 },
13309
+ { id: '1h', label: 'Last 1 hour', ms: 3_600_000 },
13310
+ { id: '6h', label: 'Last 6 hours', ms: 6 * 3_600_000 },
13311
+ { id: '24h', label: 'Last 24 hours', ms: 24 * 3_600_000 },
13312
+ { id: '7d', label: 'Last 7 days', ms: 7 * 86_400_000 },
13313
+ { id: '30d', label: 'Last 30 days', ms: 30 * 86_400_000 },
13314
+ ];
13315
+ /**
13316
+ * Monitoring time-range picker — the "Last 1 hour ▾" control charts hang off:
13317
+ * quick relative ranges plus an absolute from/to editor, in one popover.
13318
+ *
13319
+ * <strct-time-range [(range)]="range" />
13320
+ *
13321
+ * Picking a preset resolves it against "now" immediately and stamps
13322
+ * `presetId`, so a consumer can re-resolve the same preset on its own refresh
13323
+ * tick. The popover reuses `strct-dropdown`'s dialog semantics: inner clicks
13324
+ * keep it open, outside click / Escape dismiss.
13325
+ */
13326
+ class StrctTimeRangePicker {
13327
+ /** The selected window, two-way. Null until the user (or you) picks one. */
13328
+ range = model(null, ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
13329
+ /** Quick ranges shown in the left column. */
13330
+ presets = input(STRCT_TIME_RANGE_PRESETS, ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
13331
+ /** Popover alignment, as in strct-dropdown. */
13332
+ align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
13333
+ /** Localizable strings. */
13334
+ dialogLabel = input('Time range', ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
13335
+ quickLabel = input('Quick ranges', ...(ngDevMode ? [{ debugName: "quickLabel" }] : /* istanbul ignore next */ []));
13336
+ absoluteLabel = input('Absolute range', ...(ngDevMode ? [{ debugName: "absoluteLabel" }] : /* istanbul ignore next */ []));
13337
+ fromLabel = input('From', ...(ngDevMode ? [{ debugName: "fromLabel" }] : /* istanbul ignore next */ []));
13338
+ toLabel = input('To', ...(ngDevMode ? [{ debugName: "toLabel" }] : /* istanbul ignore next */ []));
13339
+ applyLabel = input('Apply', ...(ngDevMode ? [{ debugName: "applyLabel" }] : /* istanbul ignore next */ []));
13340
+ invalidLabel = input('"From" must be before "To".', ...(ngDevMode ? [{ debugName: "invalidLabel" }] : /* istanbul ignore next */ []));
13341
+ placeholderLabel = input('Select time range', ...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
13342
+ /** Fires on every commit (preset pick or absolute apply). */
13343
+ applied = output();
13344
+ dd = viewChild.required(StrctDropdown);
13345
+ // Absolute-editor draft (datetime-local strings), seeded from the range.
13346
+ draftFrom = signal('', ...(ngDevMode ? [{ debugName: "draftFrom" }] : /* istanbul ignore next */ []));
13347
+ draftTo = signal('', ...(ngDevMode ? [{ debugName: "draftTo" }] : /* istanbul ignore next */ []));
13348
+ draftError = computed(() => {
13349
+ const f = this.draftFrom();
13350
+ const t = this.draftTo();
13351
+ if (!f || !t)
13352
+ return false;
13353
+ return new Date(f).getTime() >= new Date(t).getTime();
13354
+ }, ...(ngDevMode ? [{ debugName: "draftError" }] : /* istanbul ignore next */ []));
13355
+ triggerLabel = computed(() => {
13356
+ const r = this.range();
13357
+ if (!r)
13358
+ return this.placeholderLabel();
13359
+ const preset = this.presets().find((p) => p.id === r.presetId);
13360
+ if (preset)
13361
+ return preset.label;
13362
+ return `${this.formatStamp(r.from)} → ${this.formatStamp(r.to)}`;
13363
+ }, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
13364
+ pickPreset(p) {
13365
+ const to = new Date();
13366
+ const next = { from: new Date(to.getTime() - p.ms), to, presetId: p.id };
13367
+ this.range.set(next);
13368
+ this.applied.emit(next);
13369
+ this.seedDraft(next);
13370
+ }
13371
+ applyAbsolute() {
13372
+ const f = this.draftFrom();
13373
+ const t = this.draftTo();
13374
+ if (!f || !t || this.draftError())
13375
+ return false;
13376
+ const next = { from: new Date(f), to: new Date(t) };
13377
+ this.range.set(next);
13378
+ this.applied.emit(next);
13379
+ return true;
13380
+ }
13381
+ /** Re-resolve a preset range against "now" — call from your refresh tick. */
13382
+ refresh() {
13383
+ const r = this.range();
13384
+ const preset = r && this.presets().find((p) => p.id === r.presetId);
13385
+ if (!preset)
13386
+ return;
13387
+ this.pickPreset(preset);
13388
+ }
13389
+ seedDraft(r) {
13390
+ this.draftFrom.set(this.toLocal(r.from));
13391
+ this.draftTo.set(this.toLocal(r.to));
13392
+ }
13393
+ toLocal(d) {
13394
+ const pad = (n) => String(n).padStart(2, '0');
13395
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
13396
+ }
13397
+ formatStamp(d) {
13398
+ const pad = (n) => String(n).padStart(2, '0');
13399
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
13400
+ }
13401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
13402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTimeRangePicker, isStandalone: true, selector: "strct-time-range", inputs: { range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, dialogLabel: { classPropertyName: "dialogLabel", publicName: "dialogLabel", isSignal: true, isRequired: false, transformFunction: null }, quickLabel: { classPropertyName: "quickLabel", publicName: "quickLabel", isSignal: true, isRequired: false, transformFunction: null }, absoluteLabel: { classPropertyName: "absoluteLabel", publicName: "absoluteLabel", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, applyLabel: { classPropertyName: "applyLabel", publicName: "applyLabel", isSignal: true, isRequired: false, transformFunction: null }, invalidLabel: { classPropertyName: "invalidLabel", publicName: "invalidLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholderLabel: { classPropertyName: "placeholderLabel", publicName: "placeholderLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { range: "rangeChange", applied: "applied" }, host: { classAttribute: "strct-tr" }, viewQueries: [{ propertyName: "dd", first: true, predicate: StrctDropdown, descendants: true, isSignal: true }], ngImport: i0, template: `
13403
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13404
+ <button strct-button variant="neutral" strctDropdownTrigger class="strct-tr__trigger">
13405
+ <strct-icon name="clock" [size]="13" />
13406
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13407
+ <strct-icon name="chevronDown" [size]="12" />
13408
+ </button>
13409
+
13410
+ <div class="strct-tr__panel">
13411
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13412
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13413
+ @for (p of presets(); track p.id) {
13414
+ <button
13415
+ type="button"
13416
+ class="strct-tr__preset"
13417
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13418
+ [attr.aria-pressed]="range()?.presetId === p.id"
13419
+ (click)="pickPreset(p); dd.close()"
13420
+ >
13421
+ {{ p.label }}
13422
+ </button>
13423
+ }
13424
+ </div>
13425
+
13426
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13427
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13428
+ <label class="strct-tr__field">
13429
+ <span>{{ fromLabel() }}</span>
13430
+ <input
13431
+ type="datetime-local"
13432
+ class="strct-tr__input"
13433
+ [value]="draftFrom()"
13434
+ (input)="draftFrom.set($any($event.target).value)"
13435
+ />
13436
+ </label>
13437
+ <label class="strct-tr__field">
13438
+ <span>{{ toLabel() }}</span>
13439
+ <input
13440
+ type="datetime-local"
13441
+ class="strct-tr__input"
13442
+ [value]="draftTo()"
13443
+ (input)="draftTo.set($any($event.target).value)"
13444
+ />
13445
+ </label>
13446
+ @if (draftError()) {
13447
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13448
+ }
13449
+ <button
13450
+ strct-button
13451
+ variant="primary"
13452
+ size="sm"
13453
+ class="strct-tr__apply"
13454
+ [disabled]="draftError()"
13455
+ (click)="applyAbsolute() && dd.close()"
13456
+ >
13457
+ {{ applyLabel() }}
13458
+ </button>
13459
+ </div>
13460
+ </div>
13461
+ </strct-dropdown>
13462
+ `, isInline: true, styles: [".strct-tr{display:inline-block}.strct-tr__trigger .strct-tr__label{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tr__panel{display:flex;gap:16px}.strct-tr__group{display:flex;flex-direction:column;gap:2px;min-width:150px}.strct-tr__head{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-tr__preset{text-align:start;padding:6px 9px;border:0;border-radius:var(--radius-sm);background:transparent;font-family:var(--font);font-size:12.5px;color:var(--t1);cursor:pointer}.strct-tr__preset:hover{background:var(--bg-3)}.strct-tr__preset--active{background:var(--acc18);color:var(--acc);font-weight:600}.strct-tr__preset:focus-visible,.strct-tr__input:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-tr__field{display:flex;flex-direction:column;gap:3px;font-size:11.5px;color:var(--t2);margin-bottom:7px}.strct-tr__input{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-tr__error{font-size:11.5px;color:var(--critical);margin-bottom:6px}.strct-tr__apply{align-self:flex-start}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctDropdown, selector: "strct-dropdown", inputs: ["align", "popover", "popoverLabel"] }, { kind: "directive", type: StrctDropdownTrigger, selector: "[strctDropdownTrigger]" }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13463
+ }
13464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTimeRangePicker, decorators: [{
13465
+ type: Component,
13466
+ args: [{ selector: 'strct-time-range', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctDropdown, StrctDropdownTrigger, StrctIcon], template: `
13467
+ <strct-dropdown #dd popover [popoverLabel]="dialogLabel()" [align]="align()">
13468
+ <button strct-button variant="neutral" strctDropdownTrigger class="strct-tr__trigger">
13469
+ <strct-icon name="clock" [size]="13" />
13470
+ <span class="strct-tr__label">{{ triggerLabel() }}</span>
13471
+ <strct-icon name="chevronDown" [size]="12" />
13472
+ </button>
13473
+
13474
+ <div class="strct-tr__panel">
13475
+ <div class="strct-tr__group" role="group" [attr.aria-label]="quickLabel()">
13476
+ <div class="strct-tr__head">{{ quickLabel() }}</div>
13477
+ @for (p of presets(); track p.id) {
13478
+ <button
13479
+ type="button"
13480
+ class="strct-tr__preset"
13481
+ [class.strct-tr__preset--active]="range()?.presetId === p.id"
13482
+ [attr.aria-pressed]="range()?.presetId === p.id"
13483
+ (click)="pickPreset(p); dd.close()"
13484
+ >
13485
+ {{ p.label }}
13486
+ </button>
13487
+ }
13488
+ </div>
13489
+
13490
+ <div class="strct-tr__group" role="group" [attr.aria-label]="absoluteLabel()">
13491
+ <div class="strct-tr__head">{{ absoluteLabel() }}</div>
13492
+ <label class="strct-tr__field">
13493
+ <span>{{ fromLabel() }}</span>
13494
+ <input
13495
+ type="datetime-local"
13496
+ class="strct-tr__input"
13497
+ [value]="draftFrom()"
13498
+ (input)="draftFrom.set($any($event.target).value)"
13499
+ />
13500
+ </label>
13501
+ <label class="strct-tr__field">
13502
+ <span>{{ toLabel() }}</span>
13503
+ <input
13504
+ type="datetime-local"
13505
+ class="strct-tr__input"
13506
+ [value]="draftTo()"
13507
+ (input)="draftTo.set($any($event.target).value)"
13508
+ />
13509
+ </label>
13510
+ @if (draftError()) {
13511
+ <div class="strct-tr__error" role="alert">{{ invalidLabel() }}</div>
13512
+ }
13513
+ <button
13514
+ strct-button
13515
+ variant="primary"
13516
+ size="sm"
13517
+ class="strct-tr__apply"
13518
+ [disabled]="draftError()"
13519
+ (click)="applyAbsolute() && dd.close()"
13520
+ >
13521
+ {{ applyLabel() }}
13522
+ </button>
13523
+ </div>
13524
+ </div>
13525
+ </strct-dropdown>
13526
+ `, host: { class: 'strct-tr' }, styles: [".strct-tr{display:inline-block}.strct-tr__trigger .strct-tr__label{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tr__panel{display:flex;gap:16px}.strct-tr__group{display:flex;flex-direction:column;gap:2px;min-width:150px}.strct-tr__head{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-tr__preset{text-align:start;padding:6px 9px;border:0;border-radius:var(--radius-sm);background:transparent;font-family:var(--font);font-size:12.5px;color:var(--t1);cursor:pointer}.strct-tr__preset:hover{background:var(--bg-3)}.strct-tr__preset--active{background:var(--acc18);color:var(--acc);font-weight:600}.strct-tr__preset:focus-visible,.strct-tr__input:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-tr__field{display:flex;flex-direction:column;gap:3px;font-size:11.5px;color:var(--t2);margin-bottom:7px}.strct-tr__input{padding:5px 8px;border:1px solid var(--b2);border-radius:var(--radius-sm);background:var(--bg-2);color:var(--t1);font-family:var(--font);font-size:12px}.strct-tr__error{font-size:11.5px;color:var(--critical);margin-bottom:6px}.strct-tr__apply{align-self:flex-start}\n"] }]
13527
+ }], propDecorators: { range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }, { type: i0.Output, args: ["rangeChange"] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], dialogLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dialogLabel", required: false }] }], quickLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "quickLabel", required: false }] }], absoluteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "absoluteLabel", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], applyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "applyLabel", required: false }] }], invalidLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidLabel", required: false }] }], placeholderLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderLabel", required: false }] }], applied: [{ type: i0.Output, args: ["applied"] }], dd: [{ type: i0.ViewChild, args: [i0.forwardRef(() => StrctDropdown), { isSignal: true }] }] } });
13528
+
13529
+ // SGR code → segment class. Colors map onto chart/status tokens, not raw hues.
13530
+ const ANSI_CLASSES = {
13531
+ 1: 'b',
13532
+ 30: 'c30',
13533
+ 31: 'c31',
13534
+ 32: 'c32',
13535
+ 33: 'c33',
13536
+ 34: 'c34',
13537
+ 35: 'c35',
13538
+ 36: 'c36',
13539
+ 37: 'c37',
13540
+ 90: 'c90',
13541
+ 91: 'c31',
13542
+ 92: 'c32',
13543
+ 93: 'c33',
13544
+ 94: 'c34',
13545
+ 95: 'c35',
13546
+ 96: 'c36',
13547
+ 97: 'c37',
13548
+ };
13549
+ const LEVEL_RE = /\b(ERROR|ERR|FATAL|WARN(?:ING)?|INFO|DEBUG|TRACE)\b/;
13550
+ /**
13551
+ * Virtualized log tail — the `kubectl logs -f` surface as a component:
13552
+ *
13553
+ * <strct-log-viewer [lines]="lines()" [(follow)]="follow" [height]="360" />
13554
+ *
13555
+ * - **Virtualized**: only the visible window plus overscan is in the DOM, so
13556
+ * 100k lines scroll flat.
13557
+ * - **Follow mode** (two-way): sticks to the tail as lines stream in;
13558
+ * scrolling up pauses it, the button (or scrolling back to the bottom)
13559
+ * resumes it — the Grafana/Loki convention.
13560
+ * - **ANSI SGR colors** (16-color + bold) parsed into safe spans — no
13561
+ * innerHTML — and severity tinting from `StrctLogLine.level` or, for plain
13562
+ * strings, auto-detected ERROR/WARN/INFO/DEBUG tokens.
13563
+ */
13564
+ class StrctLogViewer {
13565
+ /** Log lines — plain strings or `{ text, level }` objects. */
13566
+ lines = input([], ...(ngDevMode ? [{ debugName: "lines" }] : /* istanbul ignore next */ []));
13567
+ /** Viewport height in px. */
13568
+ height = input(320, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
13569
+ /** Follow the tail as new lines arrive (two-way; pauses on scroll-up). */
13570
+ follow = model(true, ...(ngDevMode ? [{ debugName: "follow" }] : /* istanbul ignore next */ []));
13571
+ /** Soft-wrap long lines instead of horizontal scrolling (two-way). */
13572
+ wrapMode = model(false, ...(ngDevMode ? [{ debugName: "wrapMode" }] : /* istanbul ignore next */ []));
13573
+ /** Auto-detect ERROR/WARN/INFO/DEBUG tokens on plain-string lines. */
13574
+ autoLevel = input(true, { ...(ngDevMode ? { debugName: "autoLevel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13575
+ /** Localizable strings. */
13576
+ title = input('Logs', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13577
+ followLabel = input('Follow', ...(ngDevMode ? [{ debugName: "followLabel" }] : /* istanbul ignore next */ []));
13578
+ wrapLabel = input('Wrap', ...(ngDevMode ? [{ debugName: "wrapLabel" }] : /* istanbul ignore next */ []));
13579
+ linesLabel = input('lines', ...(ngDevMode ? [{ debugName: "linesLabel" }] : /* istanbul ignore next */ []));
13580
+ emptyLabel = input('No log lines.', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
13581
+ scroller = viewChild.required('scroller');
13582
+ scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
13583
+ suppressScrollEvent = false;
13584
+ static LINE_H = 20;
13585
+ static OVERSCAN = 10;
13586
+ normalized = computed(() => this.lines().map((l) => (typeof l === 'string' ? { text: l } : l)), ...(ngDevMode ? [{ debugName: "normalized" }] : /* istanbul ignore next */ []));
13587
+ totalHeight = computed(() => this.normalized().length * StrctLogViewer.LINE_H || StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "totalHeight" }] : /* istanbul ignore next */ []));
13588
+ windowStart = computed(() => {
13589
+ const start = Math.floor(this.scrollTop() / StrctLogViewer.LINE_H) - StrctLogViewer.OVERSCAN;
13590
+ return Math.max(0, start);
13591
+ }, ...(ngDevMode ? [{ debugName: "windowStart" }] : /* istanbul ignore next */ []));
13592
+ offsetY = computed(() => this.windowStart() * StrctLogViewer.LINE_H, ...(ngDevMode ? [{ debugName: "offsetY" }] : /* istanbul ignore next */ []));
13593
+ window = computed(() => {
13594
+ const all = this.normalized();
13595
+ const start = this.windowStart();
13596
+ const visible = Math.ceil(this.height() / StrctLogViewer.LINE_H) + StrctLogViewer.OVERSCAN * 2;
13597
+ return all.slice(start, start + visible).map((line, i) => ({
13598
+ index: start + i,
13599
+ level: line.level ?? (this.autoLevel() ? detectLevel(line.text) : 'none'),
13600
+ segments: parseAnsi(line.text),
13601
+ }));
13602
+ }, ...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
13603
+ constructor() {
13604
+ // Stick to the tail whenever content grows while following.
13605
+ effect(() => {
13606
+ this.totalHeight();
13607
+ if (!this.follow())
13608
+ return;
13609
+ const el = this.scroller().nativeElement;
13610
+ // After the DOM height settles.
13611
+ requestAnimationFrame(() => {
13612
+ this.suppressScrollEvent = true;
13613
+ el.scrollTop = el.scrollHeight;
13614
+ this.scrollTop.set(el.scrollTop);
13615
+ });
13616
+ });
13617
+ }
13618
+ onScroll() {
13619
+ const el = this.scroller().nativeElement;
13620
+ this.scrollTop.set(el.scrollTop);
13621
+ if (this.suppressScrollEvent) {
13622
+ this.suppressScrollEvent = false;
13623
+ return;
13624
+ }
13625
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - StrctLogViewer.LINE_H;
13626
+ // Manual scroll: leaving the tail pauses follow, reaching it resumes.
13627
+ if (this.follow() && !atBottom)
13628
+ this.follow.set(false);
13629
+ else if (!this.follow() && atBottom)
13630
+ this.follow.set(true);
13631
+ }
13632
+ toggleFollow() {
13633
+ const next = !this.follow();
13634
+ this.follow.set(next);
13635
+ if (next) {
13636
+ const el = this.scroller().nativeElement;
13637
+ this.suppressScrollEvent = true;
13638
+ el.scrollTop = el.scrollHeight;
13639
+ this.scrollTop.set(el.scrollTop);
13640
+ }
13641
+ }
13642
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
13643
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctLogViewer, isStandalone: true, selector: "strct-log-viewer", inputs: { lines: { classPropertyName: "lines", publicName: "lines", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, follow: { classPropertyName: "follow", publicName: "follow", isSignal: true, isRequired: false, transformFunction: null }, wrapMode: { classPropertyName: "wrapMode", publicName: "wrapMode", isSignal: true, isRequired: false, transformFunction: null }, autoLevel: { classPropertyName: "autoLevel", publicName: "autoLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, followLabel: { classPropertyName: "followLabel", publicName: "followLabel", isSignal: true, isRequired: false, transformFunction: null }, wrapLabel: { classPropertyName: "wrapLabel", publicName: "wrapLabel", isSignal: true, isRequired: false, transformFunction: null }, linesLabel: { classPropertyName: "linesLabel", publicName: "linesLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { follow: "followChange", wrapMode: "wrapModeChange" }, host: { classAttribute: "strct-lv" }, viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }], ngImport: i0, template: `
13644
+ <div class="strct-lv__bar">
13645
+ <span class="strct-lv__title">{{ title() }}</span>
13646
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13647
+ <span class="strct-lv__grow"></span>
13648
+ <button
13649
+ strct-button
13650
+ size="sm"
13651
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13652
+ [attr.aria-pressed]="wrapMode()"
13653
+ (click)="wrapMode.set(!wrapMode())"
13654
+ >
13655
+ {{ wrapLabel() }}
13656
+ </button>
13657
+ <button
13658
+ strct-button
13659
+ size="sm"
13660
+ [variant]="follow() ? 'primary' : 'flat'"
13661
+ [attr.aria-pressed]="follow()"
13662
+ (click)="toggleFollow()"
13663
+ >
13664
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13665
+ {{ followLabel() }}
13666
+ </button>
13667
+ </div>
13668
+ <div
13669
+ #scroller
13670
+ class="strct-lv__scroll"
13671
+ role="log"
13672
+ tabindex="0"
13673
+ [attr.aria-label]="title()"
13674
+ [style.height.px]="height()"
13675
+ (scroll)="onScroll()"
13676
+ >
13677
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13678
+ <div
13679
+ class="strct-lv__window"
13680
+ [class.strct-lv__window--wrap]="wrapMode()"
13681
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13682
+ >
13683
+ @for (line of window(); track line.index) {
13684
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13685
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13686
+ <span class="strct-lv__text">
13687
+ @for (seg of line.segments; track $index) {
13688
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13689
+ }
13690
+ </span>
13691
+ </div>
13692
+ } @empty {
13693
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13694
+ }
13695
+ </div>
13696
+ </div>
13697
+ </div>
13698
+ `, isInline: true, styles: [".strct-lv{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-lv__bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-lv__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-lv__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-lv__grow{flex:1}.strct-lv__scroll{overflow:auto;overscroll-behavior:contain}.strct-lv__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-lv__spacer{position:relative}.strct-lv__window{position:absolute;inset-inline:0;top:0}.strct-lv__line{display:flex;gap:10px;height:var(--strct-lv-line-height, 20px);line-height:var(--strct-lv-line-height, 20px);padding-inline:10px;font-family:var(--mono);font-size:12px;color:var(--t1);white-space:pre}.strct-lv__window--wrap .strct-lv__line{height:auto;min-height:var(--strct-lv-line-height, 20px)}.strct-lv__window--wrap .strct-lv__text{white-space:pre-wrap;overflow-wrap:anywhere}.strct-lv__no{flex-shrink:0;min-width:4ch;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-lv__line--error{background:var(--critical-bg)}.strct-lv__line--error .strct-lv__text{color:var(--critical)}.strct-lv__line--warn{background:var(--warning-bg)}.strct-lv__line--warn .strct-lv__text{color:var(--warning)}.strct-lv__line--debug .strct-lv__text{color:var(--t3)}.strct-lv__empty{padding:14px 12px;font-size:12.5px;color:var(--t3)}.strct-lv__seg-b{font-weight:700}.strct-lv__seg-c31{color:var(--critical)}.strct-lv__seg-c32{color:var(--success)}.strct-lv__seg-c33{color:var(--warning)}.strct-lv__seg-c34{color:var(--chart-1, var(--acc))}.strct-lv__seg-c35{color:var(--chart-4, var(--acc))}.strct-lv__seg-c36{color:var(--chart-5, var(--acc))}.strct-lv__seg-c37{color:var(--t1)}.strct-lv__seg-c30,.strct-lv__seg-c90{color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
13699
+ }
13700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctLogViewer, decorators: [{
13701
+ type: Component,
13702
+ args: [{ selector: 'strct-log-viewer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctIcon], template: `
13703
+ <div class="strct-lv__bar">
13704
+ <span class="strct-lv__title">{{ title() }}</span>
13705
+ <span class="strct-lv__count">{{ normalized().length }} {{ linesLabel() }}</span>
13706
+ <span class="strct-lv__grow"></span>
13707
+ <button
13708
+ strct-button
13709
+ size="sm"
13710
+ [variant]="wrapMode() ? 'primary' : 'flat'"
13711
+ [attr.aria-pressed]="wrapMode()"
13712
+ (click)="wrapMode.set(!wrapMode())"
13713
+ >
13714
+ {{ wrapLabel() }}
13715
+ </button>
13716
+ <button
13717
+ strct-button
13718
+ size="sm"
13719
+ [variant]="follow() ? 'primary' : 'flat'"
13720
+ [attr.aria-pressed]="follow()"
13721
+ (click)="toggleFollow()"
13722
+ >
13723
+ <strct-icon name="chevronDoubleDown" [size]="12" />
13724
+ {{ followLabel() }}
13725
+ </button>
13726
+ </div>
13727
+ <div
13728
+ #scroller
13729
+ class="strct-lv__scroll"
13730
+ role="log"
13731
+ tabindex="0"
13732
+ [attr.aria-label]="title()"
13733
+ [style.height.px]="height()"
13734
+ (scroll)="onScroll()"
13735
+ >
13736
+ <div class="strct-lv__spacer" [style.height.px]="totalHeight()">
13737
+ <div
13738
+ class="strct-lv__window"
13739
+ [class.strct-lv__window--wrap]="wrapMode()"
13740
+ [style.transform]="'translateY(' + offsetY() + 'px)'"
13741
+ >
13742
+ @for (line of window(); track line.index) {
13743
+ <div class="strct-lv__line strct-lv__line--{{ line.level }}">
13744
+ <span class="strct-lv__no" aria-hidden="true">{{ line.index + 1 }}</span>
13745
+ <span class="strct-lv__text">
13746
+ @for (seg of line.segments; track $index) {
13747
+ <span class="strct-lv__seg-{{ seg.cls }}">{{ seg.text }}</span>
13748
+ }
13749
+ </span>
13750
+ </div>
13751
+ } @empty {
13752
+ <div class="strct-lv__empty">{{ emptyLabel() }}</div>
13753
+ }
13754
+ </div>
13755
+ </div>
13756
+ </div>
13757
+ `, host: { class: 'strct-lv' }, styles: [".strct-lv{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-lv__bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-lv__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-lv__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-lv__grow{flex:1}.strct-lv__scroll{overflow:auto;overscroll-behavior:contain}.strct-lv__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-lv__spacer{position:relative}.strct-lv__window{position:absolute;inset-inline:0;top:0}.strct-lv__line{display:flex;gap:10px;height:var(--strct-lv-line-height, 20px);line-height:var(--strct-lv-line-height, 20px);padding-inline:10px;font-family:var(--mono);font-size:12px;color:var(--t1);white-space:pre}.strct-lv__window--wrap .strct-lv__line{height:auto;min-height:var(--strct-lv-line-height, 20px)}.strct-lv__window--wrap .strct-lv__text{white-space:pre-wrap;overflow-wrap:anywhere}.strct-lv__no{flex-shrink:0;min-width:4ch;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.strct-lv__line--error{background:var(--critical-bg)}.strct-lv__line--error .strct-lv__text{color:var(--critical)}.strct-lv__line--warn{background:var(--warning-bg)}.strct-lv__line--warn .strct-lv__text{color:var(--warning)}.strct-lv__line--debug .strct-lv__text{color:var(--t3)}.strct-lv__empty{padding:14px 12px;font-size:12.5px;color:var(--t3)}.strct-lv__seg-b{font-weight:700}.strct-lv__seg-c31{color:var(--critical)}.strct-lv__seg-c32{color:var(--success)}.strct-lv__seg-c33{color:var(--warning)}.strct-lv__seg-c34{color:var(--chart-1, var(--acc))}.strct-lv__seg-c35{color:var(--chart-4, var(--acc))}.strct-lv__seg-c36{color:var(--chart-5, var(--acc))}.strct-lv__seg-c37{color:var(--t1)}.strct-lv__seg-c30,.strct-lv__seg-c90{color:var(--t3)}\n"] }]
13758
+ }], ctorParameters: () => [], propDecorators: { lines: [{ type: i0.Input, args: [{ isSignal: true, alias: "lines", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], follow: [{ type: i0.Input, args: [{ isSignal: true, alias: "follow", required: false }] }, { type: i0.Output, args: ["followChange"] }], wrapMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapMode", required: false }] }, { type: i0.Output, args: ["wrapModeChange"] }], autoLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoLevel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], followLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "followLabel", required: false }] }], wrapLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapLabel", required: false }] }], linesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "linesLabel", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }] } });
13759
+ function detectLevel(text) {
13760
+ const m = LEVEL_RE.exec(text);
13761
+ if (!m)
13762
+ return 'none';
13763
+ const tok = m[1];
13764
+ if (tok.startsWith('ERR') || tok === 'FATAL')
13765
+ return 'error';
13766
+ if (tok.startsWith('WARN'))
13767
+ return 'warn';
13768
+ if (tok === 'DEBUG' || tok === 'TRACE')
13769
+ return 'debug';
13770
+ return 'info';
13771
+ }
13772
+ /** Parse ANSI SGR sequences into class-tagged segments (16-color + bold). */
13773
+ function parseAnsi(text) {
13774
+ if (!text.includes('\u001b['))
13775
+ return [{ text, cls: 'c37' }];
13776
+ const segments = [];
13777
+ const re = /\u001b\[([0-9;]*)m/g;
13778
+ let last = 0;
13779
+ let current = new Set();
13780
+ let m;
13781
+ while ((m = re.exec(text))) {
13782
+ if (m.index > last) {
13783
+ segments.push({ text: text.slice(last, m.index), cls: clsOf(current) });
13784
+ }
13785
+ for (const raw of (m[1] || '0').split(';')) {
13786
+ const code = Number(raw || '0');
13787
+ if (code === 0)
13788
+ current = new Set();
13789
+ else if (ANSI_CLASSES[code]) {
13790
+ // A new foreground color replaces the previous one; bold stacks.
13791
+ if (code !== 1)
13792
+ for (const c of [...current])
13793
+ if (c !== 'b')
13794
+ current.delete(c);
13795
+ current.add(ANSI_CLASSES[code]);
13796
+ }
13797
+ }
13798
+ last = re.lastIndex;
13799
+ }
13800
+ if (last < text.length)
13801
+ segments.push({ text: text.slice(last), cls: clsOf(current) });
13802
+ return segments.length ? segments : [{ text: '', cls: 'c37' }];
13803
+ }
13804
+ function clsOf(set) {
13805
+ return set.size ? [...set].join(' strct-lv__seg-') : 'c37';
13806
+ }
13807
+
13808
+ /**
13809
+ * Compute a line diff (LCS-based). Exported for programmatic use — change
13810
+ * counts, gating "anything changed?" logic, custom renderings.
13811
+ */
13812
+ function strctComputeDiff(before, after) {
13813
+ const a = before.split('\n');
13814
+ const b = after.split('\n');
13815
+ // Trim common prefix/suffix first — configs mostly match, keeping DP small.
13816
+ let start = 0;
13817
+ while (start < a.length && start < b.length && a[start] === b[start])
13818
+ start++;
13819
+ let endA = a.length;
13820
+ let endB = b.length;
13821
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
13822
+ endA--;
13823
+ endB--;
13824
+ }
13825
+ const midA = a.slice(start, endA);
13826
+ const midB = b.slice(start, endB);
13827
+ const rows = [];
13828
+ for (let i = 0; i < start; i++)
13829
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: i + 1, text: a[i] });
13830
+ // LCS DP on the changed middle; fall back to replace-all when it is huge.
13831
+ if (midA.length * midB.length <= 4_000_000) {
13832
+ const n = midA.length;
13833
+ const m = midB.length;
13834
+ const dp = new Uint32Array((n + 1) * (m + 1));
13835
+ for (let i = n - 1; i >= 0; i--) {
13836
+ for (let j = m - 1; j >= 0; j--) {
13837
+ dp[i * (m + 1) + j] =
13838
+ midA[i] === midB[j]
13839
+ ? dp[(i + 1) * (m + 1) + j + 1] + 1
13840
+ : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + j + 1]);
13841
+ }
13842
+ }
13843
+ let i = 0;
13844
+ let j = 0;
13845
+ while (i < n && j < m) {
13846
+ if (midA[i] === midB[j]) {
13847
+ rows.push({ type: 'ctx', oldNo: start + i + 1, newNo: start + j + 1, text: midA[i] });
13848
+ i++;
13849
+ j++;
13850
+ }
13851
+ else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + j + 1]) {
13852
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13853
+ i++;
13854
+ }
13855
+ else {
13856
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13857
+ j++;
13858
+ }
13859
+ }
13860
+ while (i < n) {
13861
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13862
+ i++;
13863
+ }
13864
+ while (j < m) {
13865
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13866
+ j++;
13867
+ }
13868
+ }
13869
+ else {
13870
+ for (let i = 0; i < midA.length; i++)
13871
+ rows.push({ type: 'del', oldNo: start + i + 1, newNo: null, text: midA[i] });
13872
+ for (let j = 0; j < midB.length; j++)
13873
+ rows.push({ type: 'add', oldNo: null, newNo: start + j + 1, text: midB[j] });
13874
+ }
13875
+ for (let i = endA, j = endB; i < a.length; i++, j++) {
13876
+ rows.push({ type: 'ctx', oldNo: i + 1, newNo: j + 1, text: a[i] });
13877
+ }
13878
+ return rows;
13879
+ }
13880
+ /**
13881
+ * Config / YAML diff viewer — change-approval screens without a hand-rolled
13882
+ * diff. Unified by default, `split` for side-by-side; long unchanged runs
13883
+ * collapse to an expandable "· N unchanged lines ·" row.
13884
+ *
13885
+ * <strct-diff [before]="saved" [after]="draft" beforeLabel="Running" afterLabel="Draft" />
13886
+ *
13887
+ * Rows are marked by symbol (+/−) as well as tint, so the diff reads without
13888
+ * color — the same CVD rule the icon badges follow.
13889
+ */
13890
+ class StrctDiff {
13891
+ /** The two texts to compare. */
13892
+ before = input.required(...(ngDevMode ? [{ debugName: "before" }] : /* istanbul ignore next */ []));
13893
+ after = input.required(...(ngDevMode ? [{ debugName: "after" }] : /* istanbul ignore next */ []));
13894
+ /** Unified (default) or side-by-side split view. */
13895
+ mode = input('unified', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
13896
+ /** Context lines kept around changes before collapsing (0 disables collapsing). */
13897
+ context = input(3, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
13898
+ /** Header bits. */
13899
+ title = input('Diff', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13900
+ language = input('', ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
13901
+ beforeLabel = input('Before', ...(ngDevMode ? [{ debugName: "beforeLabel" }] : /* istanbul ignore next */ []));
13902
+ afterLabel = input('After', ...(ngDevMode ? [{ debugName: "afterLabel" }] : /* istanbul ignore next */ []));
13903
+ copyable = input(true, { ...(ngDevMode ? { debugName: "copyable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13904
+ copyAfterLabel = input('Copy new version', ...(ngDevMode ? [{ debugName: "copyAfterLabel" }] : /* istanbul ignore next */ []));
13905
+ unchangedLabel = input('unchanged lines', ...(ngDevMode ? [{ debugName: "unchangedLabel" }] : /* istanbul ignore next */ []));
13906
+ /** Scroll the body past this height (px); null grows freely. */
13907
+ maxHeight = input(null, ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
13908
+ // Indices of collapsed runs the user has expanded.
13909
+ expanded = signal(new Set(), ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
13910
+ rows = computed(() => strctComputeDiff(this.before(), this.after()), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
13911
+ addCount = computed(() => this.rows().filter((r) => r.type === 'add').length, ...(ngDevMode ? [{ debugName: "addCount" }] : /* istanbul ignore next */ []));
13912
+ delCount = computed(() => this.rows().filter((r) => r.type === 'del').length, ...(ngDevMode ? [{ debugName: "delCount" }] : /* istanbul ignore next */ []));
13913
+ unifiedRows = computed(() => {
13914
+ const out = [];
13915
+ for (const chunk of this.chunks()) {
13916
+ if (chunk.skip)
13917
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
13918
+ else
13919
+ for (const r of chunk.rows)
13920
+ out.push({ kind: 'row', ...r });
13921
+ }
13922
+ return out;
13923
+ }, ...(ngDevMode ? [{ debugName: "unifiedRows" }] : /* istanbul ignore next */ []));
13924
+ splitRows = computed(() => {
13925
+ const out = [];
13926
+ for (const chunk of this.chunks()) {
13927
+ if (chunk.skip) {
13928
+ out.push({ kind: 'skip', count: chunk.rows.length, startIndex: chunk.start });
13929
+ continue;
13930
+ }
13931
+ // Pair del-runs with the add-run that follows them (classic split view).
13932
+ let i = 0;
13933
+ const rows = chunk.rows;
13934
+ while (i < rows.length) {
13935
+ const r = rows[i];
13936
+ if (r.type === 'ctx') {
13937
+ out.push({
13938
+ kind: 'row',
13939
+ left: { type: 'ctx', no: r.oldNo, text: r.text },
13940
+ right: { type: 'ctx', no: r.newNo, text: r.text },
13941
+ });
13942
+ i++;
13943
+ continue;
13944
+ }
13945
+ const dels = [];
13946
+ const adds = [];
13947
+ while (i < rows.length && rows[i].type === 'del')
13948
+ dels.push(rows[i++]);
13949
+ while (i < rows.length && rows[i].type === 'add')
13950
+ adds.push(rows[i++]);
13951
+ const len = Math.max(dels.length, adds.length);
13952
+ for (let k = 0; k < len; k++) {
13953
+ const d = dels[k];
13954
+ const a = adds[k];
13955
+ out.push({
13956
+ kind: 'row',
13957
+ left: d
13958
+ ? { type: 'del', no: d.oldNo, text: d.text }
13959
+ : { type: 'empty', no: null, text: '' },
13960
+ right: a
13961
+ ? { type: 'add', no: a.newNo, text: a.text }
13962
+ : { type: 'empty', no: null, text: '' },
13963
+ });
13964
+ }
13965
+ }
13966
+ }
13967
+ return out;
13968
+ }, ...(ngDevMode ? [{ debugName: "splitRows" }] : /* istanbul ignore next */ []));
13969
+ /** Group rows into visible chunks and collapsible unchanged runs. */
13970
+ chunks = computed(() => {
13971
+ const rows = this.rows();
13972
+ const ctx = this.context();
13973
+ const expanded = this.expanded();
13974
+ if (ctx <= 0)
13975
+ return [{ skip: false, start: 0, rows }];
13976
+ // Mark rows within `ctx` of any change as visible.
13977
+ const visible = new Array(rows.length).fill(false);
13978
+ rows.forEach((r, i) => {
13979
+ if (r.type !== 'ctx') {
13980
+ for (let k = Math.max(0, i - ctx); k <= Math.min(rows.length - 1, i + ctx); k++) {
13981
+ visible[k] = true;
13982
+ }
13983
+ }
13984
+ });
13985
+ const out = [];
13986
+ let i = 0;
13987
+ while (i < rows.length) {
13988
+ const vis = visible[i] || expanded.has(startOfRun(visible, i));
13989
+ const start = i;
13990
+ while (i < rows.length && (visible[i] || expanded.has(startOfRun(visible, i))) === vis)
13991
+ i++;
13992
+ const slice = rows.slice(start, i);
13993
+ // Tiny hidden runs are not worth a fold row.
13994
+ if (!vis && slice.length <= 2)
13995
+ out.push({ skip: false, start, rows: slice });
13996
+ else
13997
+ out.push({ skip: !vis, start, rows: slice });
13998
+ }
13999
+ return out;
14000
+ }, ...(ngDevMode ? [{ debugName: "chunks" }] : /* istanbul ignore next */ []));
14001
+ expand(startIndex) {
14002
+ const next = new Set(this.expanded());
14003
+ next.add(startIndex);
14004
+ this.expanded.set(next);
14005
+ }
14006
+ sign(type) {
14007
+ return type === 'add' ? '+' : type === 'del' ? '−' : '';
14008
+ }
14009
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, deps: [], target: i0.ɵɵFactoryTarget.Component });
14010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctDiff, isStandalone: true, selector: "strct-diff", inputs: { before: { classPropertyName: "before", publicName: "before", isSignal: true, isRequired: true, transformFunction: null }, after: { classPropertyName: "after", publicName: "after", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, beforeLabel: { classPropertyName: "beforeLabel", publicName: "beforeLabel", isSignal: true, isRequired: false, transformFunction: null }, afterLabel: { classPropertyName: "afterLabel", publicName: "afterLabel", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null }, copyAfterLabel: { classPropertyName: "copyAfterLabel", publicName: "copyAfterLabel", isSignal: true, isRequired: false, transformFunction: null }, unchangedLabel: { classPropertyName: "unchangedLabel", publicName: "unchangedLabel", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-diff" }, ngImport: i0, template: `
14011
+ <div class="strct-diff__head">
14012
+ <span class="strct-diff__title">{{ title() }}</span>
14013
+ @if (language()) {
14014
+ <span class="strct-diff__lang">{{ language() }}</span>
14015
+ }
14016
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14017
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14018
+ <span class="strct-diff__grow"></span>
14019
+ @if (copyable()) {
14020
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14021
+ }
14022
+ </div>
14023
+ @if (mode() === 'split') {
14024
+ <div class="strct-diff__cols" aria-hidden="true">
14025
+ <span>{{ beforeLabel() }}</span>
14026
+ <span>{{ afterLabel() }}</span>
14027
+ </div>
14028
+ }
14029
+ <div
14030
+ class="strct-diff__scroll"
14031
+ tabindex="0"
14032
+ role="region"
14033
+ [attr.aria-label]="title()"
14034
+ [style.max-height.px]="maxHeight()"
14035
+ >
14036
+ @if (mode() === 'unified') {
14037
+ <table class="strct-diff__table">
14038
+ <tbody>
14039
+ @for (row of unifiedRows(); track $index) {
14040
+ @if (row.kind === 'skip') {
14041
+ <tr class="strct-diff__skip-row">
14042
+ <td colspan="4">
14043
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14044
+ · {{ row.count }} {{ unchangedLabel() }} ·
14045
+ </button>
14046
+ </td>
14047
+ </tr>
14048
+ } @else {
14049
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14050
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14051
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14052
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14053
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14054
+ </tr>
14055
+ }
14056
+ }
14057
+ </tbody>
14058
+ </table>
14059
+ } @else {
14060
+ <table class="strct-diff__table strct-diff__table--split">
14061
+ <tbody>
14062
+ @for (row of splitRows(); track $index) {
14063
+ @if (row.kind === 'skip') {
14064
+ <tr class="strct-diff__skip-row">
14065
+ <td colspan="6">
14066
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14067
+ · {{ row.count }} {{ unchangedLabel() }} ·
14068
+ </button>
14069
+ </td>
14070
+ </tr>
14071
+ } @else {
14072
+ <tr>
14073
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14074
+ {{ row.left.no ?? '' }}
14075
+ </td>
14076
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14077
+ {{ sign(row.left.type) }}
14078
+ </td>
14079
+ <td
14080
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14081
+ [textContent]="row.left.text"
14082
+ ></td>
14083
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14084
+ {{ row.right.no ?? '' }}
14085
+ </td>
14086
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14087
+ {{ sign(row.right.type) }}
14088
+ </td>
14089
+ <td
14090
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14091
+ [textContent]="row.right.text"
14092
+ ></td>
14093
+ </tr>
14094
+ }
14095
+ }
14096
+ </tbody>
14097
+ </table>
14098
+ }
14099
+ </div>
14100
+ `, isInline: true, styles: [".strct-diff{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-diff__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-diff__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-diff__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-diff__stat{font-family:var(--mono);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.strct-diff__stat--add{color:var(--success)}.strct-diff__stat--del{color:var(--critical)}.strct-diff__grow{flex:1}.strct-diff__cols{display:grid;grid-template-columns:1fr 1fr;padding:4px 10px;border-bottom:1px solid var(--b1);font-size:11px;font-weight:600;color:var(--t3)}.strct-diff__scroll{overflow:auto}.strct-diff__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-diff__table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;line-height:1.55}.strct-diff__no{width:1%;min-width:3.5ch;padding:0 6px;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums;vertical-align:top}.strct-diff__sign{width:1%;padding:0 2px;text-align:center;-webkit-user-select:none;user-select:none;vertical-align:top}.strct-diff__text{padding:0 10px 0 4px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--t1)}.strct-diff__row--add,.strct-diff__cell--add{background:var(--success-bg)}.strct-diff__row--add .strct-diff__sign,.strct-diff__cell--add.strct-diff__sign{color:var(--success);font-weight:700}.strct-diff__row--del,.strct-diff__cell--del{background:var(--critical-bg)}.strct-diff__row--del .strct-diff__sign,.strct-diff__cell--del.strct-diff__sign{color:var(--critical);font-weight:700}.strct-diff__table--split .strct-diff__text{width:50%}.strct-diff__skip-row td{padding:0}.strct-diff__skip{display:block;width:100%;padding:3px 10px;border:0;border-block:1px dashed var(--b2);background:var(--bg-3);color:var(--t3);font-family:var(--mono);font-size:11px;cursor:pointer}.strct-diff__skip:hover{color:var(--t1)}.strct-diff__skip:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}\n"], dependencies: [{ kind: "component", type: StrctCopy, selector: "strct-copy", inputs: ["text", "label", "ariaLabel", "copyLabel", "copiedLabel"], outputs: ["copied"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14101
+ }
14102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctDiff, decorators: [{
14103
+ type: Component,
14104
+ args: [{ selector: 'strct-diff', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctCopy], template: `
14105
+ <div class="strct-diff__head">
14106
+ <span class="strct-diff__title">{{ title() }}</span>
14107
+ @if (language()) {
14108
+ <span class="strct-diff__lang">{{ language() }}</span>
14109
+ }
14110
+ <span class="strct-diff__stat strct-diff__stat--add">+{{ addCount() }}</span>
14111
+ <span class="strct-diff__stat strct-diff__stat--del">−{{ delCount() }}</span>
14112
+ <span class="strct-diff__grow"></span>
14113
+ @if (copyable()) {
14114
+ <strct-copy [text]="after()" [copyLabel]="copyAfterLabel()" />
14115
+ }
14116
+ </div>
14117
+ @if (mode() === 'split') {
14118
+ <div class="strct-diff__cols" aria-hidden="true">
14119
+ <span>{{ beforeLabel() }}</span>
14120
+ <span>{{ afterLabel() }}</span>
14121
+ </div>
14122
+ }
14123
+ <div
14124
+ class="strct-diff__scroll"
14125
+ tabindex="0"
14126
+ role="region"
14127
+ [attr.aria-label]="title()"
14128
+ [style.max-height.px]="maxHeight()"
14129
+ >
14130
+ @if (mode() === 'unified') {
14131
+ <table class="strct-diff__table">
14132
+ <tbody>
14133
+ @for (row of unifiedRows(); track $index) {
14134
+ @if (row.kind === 'skip') {
14135
+ <tr class="strct-diff__skip-row">
14136
+ <td colspan="4">
14137
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14138
+ · {{ row.count }} {{ unchangedLabel() }} ·
14139
+ </button>
14140
+ </td>
14141
+ </tr>
14142
+ } @else {
14143
+ <tr class="strct-diff__row strct-diff__row--{{ row.type }}">
14144
+ <td class="strct-diff__no">{{ row.oldNo ?? '' }}</td>
14145
+ <td class="strct-diff__no">{{ row.newNo ?? '' }}</td>
14146
+ <td class="strct-diff__sign">{{ sign(row.type) }}</td>
14147
+ <td class="strct-diff__text" [textContent]="row.text"></td>
14148
+ </tr>
14149
+ }
14150
+ }
14151
+ </tbody>
14152
+ </table>
14153
+ } @else {
14154
+ <table class="strct-diff__table strct-diff__table--split">
14155
+ <tbody>
14156
+ @for (row of splitRows(); track $index) {
14157
+ @if (row.kind === 'skip') {
14158
+ <tr class="strct-diff__skip-row">
14159
+ <td colspan="6">
14160
+ <button type="button" class="strct-diff__skip" (click)="expand(row.startIndex)">
14161
+ · {{ row.count }} {{ unchangedLabel() }} ·
14162
+ </button>
14163
+ </td>
14164
+ </tr>
14165
+ } @else {
14166
+ <tr>
14167
+ <td class="strct-diff__no strct-diff__cell--{{ row.left.type }}">
14168
+ {{ row.left.no ?? '' }}
14169
+ </td>
14170
+ <td class="strct-diff__sign strct-diff__cell--{{ row.left.type }}">
14171
+ {{ sign(row.left.type) }}
14172
+ </td>
14173
+ <td
14174
+ class="strct-diff__text strct-diff__cell--{{ row.left.type }}"
14175
+ [textContent]="row.left.text"
14176
+ ></td>
14177
+ <td class="strct-diff__no strct-diff__cell--{{ row.right.type }}">
14178
+ {{ row.right.no ?? '' }}
14179
+ </td>
14180
+ <td class="strct-diff__sign strct-diff__cell--{{ row.right.type }}">
14181
+ {{ sign(row.right.type) }}
14182
+ </td>
14183
+ <td
14184
+ class="strct-diff__text strct-diff__cell--{{ row.right.type }}"
14185
+ [textContent]="row.right.text"
14186
+ ></td>
14187
+ </tr>
14188
+ }
14189
+ }
14190
+ </tbody>
14191
+ </table>
14192
+ }
14193
+ </div>
14194
+ `, host: { class: 'strct-diff' }, styles: [".strct-diff{display:block;border:1px solid var(--b2);border-radius:9px;background:var(--bg-2);overflow:hidden}.strct-diff__head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--b1)}.strct-diff__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-diff__lang{font-family:var(--mono);font-size:10px;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);border:1px solid var(--b2);border-radius:4px;padding:2px 6px}.strct-diff__stat{font-family:var(--mono);font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}.strct-diff__stat--add{color:var(--success)}.strct-diff__stat--del{color:var(--critical)}.strct-diff__grow{flex:1}.strct-diff__cols{display:grid;grid-template-columns:1fr 1fr;padding:4px 10px;border-bottom:1px solid var(--b1);font-size:11px;font-weight:600;color:var(--t3)}.strct-diff__scroll{overflow:auto}.strct-diff__scroll:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-diff__table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;line-height:1.55}.strct-diff__no{width:1%;min-width:3.5ch;padding:0 6px;text-align:end;color:var(--t4);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums;vertical-align:top}.strct-diff__sign{width:1%;padding:0 2px;text-align:center;-webkit-user-select:none;user-select:none;vertical-align:top}.strct-diff__text{padding:0 10px 0 4px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--t1)}.strct-diff__row--add,.strct-diff__cell--add{background:var(--success-bg)}.strct-diff__row--add .strct-diff__sign,.strct-diff__cell--add.strct-diff__sign{color:var(--success);font-weight:700}.strct-diff__row--del,.strct-diff__cell--del{background:var(--critical-bg)}.strct-diff__row--del .strct-diff__sign,.strct-diff__cell--del.strct-diff__sign{color:var(--critical);font-weight:700}.strct-diff__table--split .strct-diff__text{width:50%}.strct-diff__skip-row td{padding:0}.strct-diff__skip{display:block;width:100%;padding:3px 10px;border:0;border-block:1px dashed var(--b2);background:var(--bg-3);color:var(--t3);font-family:var(--mono);font-size:11px;cursor:pointer}.strct-diff__skip:hover{color:var(--t1)}.strct-diff__skip:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}\n"] }]
14195
+ }], propDecorators: { before: [{ type: i0.Input, args: [{ isSignal: true, alias: "before", required: true }] }], after: [{ type: i0.Input, args: [{ isSignal: true, alias: "after", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], beforeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "beforeLabel", required: false }] }], afterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "afterLabel", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], copyAfterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyAfterLabel", required: false }] }], unchangedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unchangedLabel", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }] } });
14196
+ function startOfRun(visible, i) {
14197
+ let s = i;
14198
+ while (s > 0 && visible[s - 1] === visible[i])
14199
+ s--;
14200
+ return s;
14201
+ }
14202
+
14203
+ /**
14204
+ * Two resizable panes with a draggable gutter — master/detail layouts where
14205
+ * the split is the user's to own:
14206
+ *
14207
+ * <strct-splitter [(split)]="pct" [min]="20" [max]="80">
14208
+ * <div strctPaneStart>…list…</div>
14209
+ * <div strctPaneEnd>…detail…</div>
14210
+ * </strct-splitter>
14211
+ *
14212
+ * `split` is the start pane's share in percent (two-way, persistable).
14213
+ * The gutter is a keyboard separator: arrows nudge, Home/End jump to min/max.
14214
+ */
14215
+ class StrctSplitter {
14216
+ host = inject(ElementRef);
14217
+ /** Start pane share in percent (two-way). */
14218
+ split = model(50, ...(ngDevMode ? [{ debugName: "split" }] : /* istanbul ignore next */ []));
14219
+ /** Clamp bounds for the split (percent). */
14220
+ min = input(15, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
14221
+ max = input(85, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
14222
+ /** Stack panes vertically (gutter drags up/down). */
14223
+ vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : /* istanbul ignore next */ []));
14224
+ /** Accessible name of the separator (localizable). */
14225
+ gutterLabel = input('Resize panes', ...(ngDevMode ? [{ debugName: "gutterLabel" }] : /* istanbul ignore next */ []));
14226
+ /** Keyboard nudge step in percent. */
14227
+ step = input(3, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
14228
+ clamped = computed(() => Math.min(this.max(), Math.max(this.min(), this.split())), ...(ngDevMode ? [{ debugName: "clamped" }] : /* istanbul ignore next */ []));
14229
+ dragging = false;
14230
+ moveHandler = (e) => this.onDragMove(e);
14231
+ upHandler = () => this.onDragEnd();
14232
+ onDragStart(event) {
14233
+ event.preventDefault();
14234
+ this.dragging = true;
14235
+ document.addEventListener('mousemove', this.moveHandler);
14236
+ document.addEventListener('mouseup', this.upHandler);
14237
+ }
14238
+ onDragMove(event) {
14239
+ const rect = this.host.nativeElement.getBoundingClientRect();
14240
+ const ratio = this.vertical()
14241
+ ? (event.clientY - rect.top) / rect.height
14242
+ : (event.clientX - rect.left) / rect.width;
14243
+ this.split.set(Math.min(this.max(), Math.max(this.min(), Math.round(ratio * 100))));
14244
+ }
14245
+ onDragEnd() {
14246
+ this.dragging = false;
14247
+ document.removeEventListener('mousemove', this.moveHandler);
14248
+ document.removeEventListener('mouseup', this.upHandler);
14249
+ }
14250
+ onKeydown(event) {
14251
+ const dec = this.vertical() ? 'ArrowUp' : 'ArrowLeft';
14252
+ const inc = this.vertical() ? 'ArrowDown' : 'ArrowRight';
14253
+ let next = null;
14254
+ if (event.key === dec)
14255
+ next = this.clamped() - this.step();
14256
+ else if (event.key === inc)
14257
+ next = this.clamped() + this.step();
14258
+ else if (event.key === 'Home')
14259
+ next = this.min();
14260
+ else if (event.key === 'End')
14261
+ next = this.max();
14262
+ if (next == null)
14263
+ return;
14264
+ event.preventDefault();
14265
+ this.split.set(Math.min(this.max(), Math.max(this.min(), next)));
14266
+ }
14267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, deps: [], target: i0.ɵɵFactoryTarget.Component });
14268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: StrctSplitter, isStandalone: true, selector: "strct-splitter", inputs: { split: { classPropertyName: "split", publicName: "split", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null }, gutterLabel: { classPropertyName: "gutterLabel", publicName: "gutterLabel", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { split: "splitChange" }, host: { properties: { "class.strct-split--vertical": "vertical()", "class.strct-split--dragging": "dragging" }, classAttribute: "strct-split" }, ngImport: i0, template: `
14269
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14270
+ <ng-content select="[strctPaneStart]" />
14271
+ </div>
14272
+ <div
14273
+ class="strct-split__gutter"
14274
+ role="separator"
14275
+ tabindex="0"
14276
+ [attr.aria-label]="gutterLabel()"
14277
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14278
+ [attr.aria-valuenow]="clamped()"
14279
+ [attr.aria-valuemin]="min()"
14280
+ [attr.aria-valuemax]="max()"
14281
+ (mousedown)="onDragStart($event)"
14282
+ (keydown)="onKeydown($event)"
14283
+ >
14284
+ <span class="strct-split__grip" aria-hidden="true"></span>
14285
+ </div>
14286
+ <div class="strct-split__pane strct-split__pane--end">
14287
+ <ng-content select="[strctPaneEnd]" />
14288
+ </div>
14289
+ `, isInline: true, styles: [".strct-split{display:flex;width:100%;min-height:0}.strct-split--vertical{flex-direction:column}.strct-split__pane{flex-grow:0;flex-shrink:0;overflow:auto;min-width:0;min-height:0}.strct-split__pane--end{flex:1 1 0}.strct-split__gutter{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:7px;cursor:col-resize;background:transparent;touch-action:none}.strct-split--vertical>.strct-split__gutter{width:auto;height:7px;cursor:row-resize}.strct-split__gutter:hover,.strct-split--dragging>.strct-split__gutter{background:var(--acc18)}.strct-split__gutter:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-split__grip{width:1.5px;height:26px;border-radius:1px;background:var(--b2)}.strct-split--vertical .strct-split__grip{width:26px;height:1.5px}.strct-split--dragging{-webkit-user-select:none;user-select:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14290
+ }
14291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitter, decorators: [{
14292
+ type: Component,
14293
+ args: [{ selector: 'strct-splitter', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14294
+ <div class="strct-split__pane" [style.flex-basis.%]="clamped()">
14295
+ <ng-content select="[strctPaneStart]" />
14296
+ </div>
14297
+ <div
14298
+ class="strct-split__gutter"
14299
+ role="separator"
14300
+ tabindex="0"
14301
+ [attr.aria-label]="gutterLabel()"
14302
+ [attr.aria-orientation]="vertical() ? 'horizontal' : 'vertical'"
14303
+ [attr.aria-valuenow]="clamped()"
14304
+ [attr.aria-valuemin]="min()"
14305
+ [attr.aria-valuemax]="max()"
14306
+ (mousedown)="onDragStart($event)"
14307
+ (keydown)="onKeydown($event)"
14308
+ >
14309
+ <span class="strct-split__grip" aria-hidden="true"></span>
14310
+ </div>
14311
+ <div class="strct-split__pane strct-split__pane--end">
14312
+ <ng-content select="[strctPaneEnd]" />
14313
+ </div>
14314
+ `, host: {
14315
+ class: 'strct-split',
14316
+ '[class.strct-split--vertical]': 'vertical()',
14317
+ '[class.strct-split--dragging]': 'dragging',
14318
+ }, styles: [".strct-split{display:flex;width:100%;min-height:0}.strct-split--vertical{flex-direction:column}.strct-split__pane{flex-grow:0;flex-shrink:0;overflow:auto;min-width:0;min-height:0}.strct-split__pane--end{flex:1 1 0}.strct-split__gutter{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:7px;cursor:col-resize;background:transparent;touch-action:none}.strct-split--vertical>.strct-split__gutter{width:auto;height:7px;cursor:row-resize}.strct-split__gutter:hover,.strct-split--dragging>.strct-split__gutter{background:var(--acc18)}.strct-split__gutter:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-split__grip{width:1.5px;height:26px;border-radius:1px;background:var(--b2)}.strct-split--vertical .strct-split__grip{width:26px;height:1.5px}.strct-split--dragging{-webkit-user-select:none;user-select:none}\n"] }]
14319
+ }], propDecorators: { split: [{ type: i0.Input, args: [{ isSignal: true, alias: "split", required: false }] }, { type: i0.Output, args: ["splitChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], gutterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutterLabel", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }] } });
14320
+
14321
+ /**
14322
+ * Dual-list transfer (picklist) — "assign hosts to the cluster": all items on
14323
+ * the left, the chosen set on the right, checkbox multi-select and move
14324
+ * buttons in between, with a searchbox per side.
14325
+ *
14326
+ * <strct-transfer [items]="hosts" [(assigned)]="clusterHosts" />
14327
+ *
14328
+ * `assigned` is the two-way id set (string[]); `items` stays the single
14329
+ * source-of-truth list.
14330
+ */
14331
+ class StrctTransfer {
14332
+ /** The full item universe. */
14333
+ items = input.required(...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14334
+ /** Assigned item ids (two-way). */
14335
+ assigned = model([], ...(ngDevMode ? [{ debugName: "assigned" }] : /* istanbul ignore next */ []));
14336
+ /** Localizable strings. */
14337
+ sourceLabel = input('Available', ...(ngDevMode ? [{ debugName: "sourceLabel" }] : /* istanbul ignore next */ []));
14338
+ targetLabel = input('Assigned', ...(ngDevMode ? [{ debugName: "targetLabel" }] : /* istanbul ignore next */ []));
14339
+ searchPlaceholder = input('Search', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
14340
+ emptyLabel = input('No items', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : /* istanbul ignore next */ []));
14341
+ assignLabel = input('Assign selected', ...(ngDevMode ? [{ debugName: "assignLabel" }] : /* istanbul ignore next */ []));
14342
+ unassignLabel = input('Unassign selected', ...(ngDevMode ? [{ debugName: "unassignLabel" }] : /* istanbul ignore next */ []));
14343
+ /** A move happened (either direction) with the moved ids. */
14344
+ moved = output();
14345
+ sourceQuery = signal('', ...(ngDevMode ? [{ debugName: "sourceQuery" }] : /* istanbul ignore next */ []));
14346
+ targetQuery = signal('', ...(ngDevMode ? [{ debugName: "targetQuery" }] : /* istanbul ignore next */ []));
14347
+ sourceChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "sourceChecked" }] : /* istanbul ignore next */ []));
14348
+ targetChecked = signal(new Set(), ...(ngDevMode ? [{ debugName: "targetChecked" }] : /* istanbul ignore next */ []));
14349
+ assignedSet = computed(() => new Set(this.assigned()), ...(ngDevMode ? [{ debugName: "assignedSet" }] : /* istanbul ignore next */ []));
14350
+ sourceItems = computed(() => this.items().filter((i) => !this.assignedSet().has(i.id) && this.matches(i, this.sourceQuery())), ...(ngDevMode ? [{ debugName: "sourceItems" }] : /* istanbul ignore next */ []));
14351
+ targetItems = computed(() => this.items().filter((i) => this.assignedSet().has(i.id) && this.matches(i, this.targetQuery())), ...(ngDevMode ? [{ debugName: "targetItems" }] : /* istanbul ignore next */ []));
14352
+ matches(item, q) {
14353
+ return !q.trim() || item.label.toLowerCase().includes(q.trim().toLowerCase());
14354
+ }
14355
+ toggle(which, id) {
14356
+ const next = new Set(which());
14357
+ if (next.has(id))
14358
+ next.delete(id);
14359
+ else
14360
+ next.add(id);
14361
+ which.set(next);
14362
+ }
14363
+ assign() {
14364
+ const ids = [...this.sourceChecked()];
14365
+ if (!ids.length)
14366
+ return;
14367
+ this.assigned.set([...this.assigned(), ...ids]);
14368
+ this.sourceChecked.set(new Set());
14369
+ this.moved.emit({ direction: 'assign', ids });
14370
+ }
14371
+ unassign() {
14372
+ const ids = new Set(this.targetChecked());
14373
+ if (!ids.size)
14374
+ return;
14375
+ this.assigned.set(this.assigned().filter((id) => !ids.has(id)));
14376
+ this.targetChecked.set(new Set());
14377
+ this.moved.emit({ direction: 'unassign', ids: [...ids] });
14378
+ }
14379
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, deps: [], target: i0.ɵɵFactoryTarget.Component });
14380
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTransfer, isStandalone: true, selector: "strct-transfer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, assigned: { classPropertyName: "assigned", publicName: "assigned", isSignal: true, isRequired: false, transformFunction: null }, sourceLabel: { classPropertyName: "sourceLabel", publicName: "sourceLabel", isSignal: true, isRequired: false, transformFunction: null }, targetLabel: { classPropertyName: "targetLabel", publicName: "targetLabel", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, assignLabel: { classPropertyName: "assignLabel", publicName: "assignLabel", isSignal: true, isRequired: false, transformFunction: null }, unassignLabel: { classPropertyName: "unassignLabel", publicName: "unassignLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { assigned: "assignedChange", moved: "moved" }, host: { classAttribute: "strct-tf" }, ngImport: i0, template: `
14381
+ <div class="strct-tf__col">
14382
+ <div class="strct-tf__head">
14383
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14384
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14385
+ </div>
14386
+ <strct-searchbox
14387
+ class="strct-tf__search"
14388
+ [placeholder]="searchPlaceholder()"
14389
+ [value]="sourceQuery()"
14390
+ (valueChange)="sourceQuery.set($event)"
14391
+ />
14392
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14393
+ @for (item of sourceItems(); track item.id) {
14394
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14395
+ <strct-checkbox
14396
+ [checked]="sourceChecked().has(item.id)"
14397
+ [disabled]="item.disabled ?? false"
14398
+ (checkedChange)="toggle(sourceChecked, item.id)"
14399
+ >
14400
+ @if (item.icon) {
14401
+ <strct-icon [name]="item.icon" [size]="14" />
14402
+ }
14403
+ <span class="strct-tf__label">{{ item.label }}</span>
14404
+ </strct-checkbox>
14405
+ </li>
14406
+ } @empty {
14407
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14408
+ }
14409
+ </ul>
14410
+ </div>
14411
+
14412
+ <div class="strct-tf__mid">
14413
+ <button
14414
+ strct-button
14415
+ size="sm"
14416
+ variant="neutral"
14417
+ [disabled]="!sourceChecked().size"
14418
+ [attr.aria-label]="assignLabel()"
14419
+ (click)="assign()"
14420
+ >
14421
+ <strct-icon name="chevronRight" [size]="14" />
14422
+ </button>
14423
+ <button
14424
+ strct-button
14425
+ size="sm"
14426
+ variant="neutral"
14427
+ [disabled]="!targetChecked().size"
14428
+ [attr.aria-label]="unassignLabel()"
14429
+ (click)="unassign()"
14430
+ >
14431
+ <strct-icon name="chevronLeft" [size]="14" />
14432
+ </button>
14433
+ </div>
14434
+
14435
+ <div class="strct-tf__col">
14436
+ <div class="strct-tf__head">
14437
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14438
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14439
+ </div>
14440
+ <strct-searchbox
14441
+ class="strct-tf__search"
14442
+ [placeholder]="searchPlaceholder()"
14443
+ [value]="targetQuery()"
14444
+ (valueChange)="targetQuery.set($event)"
14445
+ />
14446
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14447
+ @for (item of targetItems(); track item.id) {
14448
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14449
+ <strct-checkbox
14450
+ [checked]="targetChecked().has(item.id)"
14451
+ [disabled]="item.disabled ?? false"
14452
+ (checkedChange)="toggle(targetChecked, item.id)"
14453
+ >
14454
+ @if (item.icon) {
14455
+ <strct-icon [name]="item.icon" [size]="14" />
14456
+ }
14457
+ <span class="strct-tf__label">{{ item.label }}</span>
14458
+ </strct-checkbox>
14459
+ </li>
14460
+ } @empty {
14461
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14462
+ }
14463
+ </ul>
14464
+ </div>
14465
+ `, isInline: true, styles: [".strct-tf{display:flex;align-items:stretch;gap:10px;width:100%}.strct-tf__col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:10px;background:var(--bg-2);border:1px solid var(--b2);border-radius:9px}.strct-tf__head{display:flex;align-items:baseline;gap:8px}.strct-tf__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-tf__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-tf__search{width:100%}.strct-tf__list{list-style:none;margin:0;padding:0;min-height:120px;max-height:240px;overflow:auto;display:flex;flex-direction:column;gap:1px}.strct-tf__item{display:flex;align-items:center;padding:2px 4px;border-radius:5px;color:var(--t1);font-size:12.5px}.strct-tf__item strct-icon{margin-inline-end:6px}.strct-tf__item:hover{background:var(--bg-3)}.strct-tf__item--disabled{color:var(--t4);cursor:default}.strct-tf__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tf__empty{padding:12px 8px;font-size:12px;color:var(--t3)}.strct-tf__mid{display:flex;flex-direction:column;justify-content:center;gap:8px}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }, { kind: "component", type: StrctCheckbox, selector: "strct-checkbox", inputs: ["checked", "isDisabled", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange", "isDisabledChange"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "component", type: StrctSearchbox, selector: "strct-searchbox", inputs: ["placeholder", "hint", "trigger", "clearable", "ariaLabel", "clearLabel", "value"], outputs: ["valueChange", "search", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14466
+ }
14467
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTransfer, decorators: [{
14468
+ type: Component,
14469
+ args: [{ selector: 'strct-transfer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton, StrctCheckbox, StrctIcon, StrctSearchbox], template: `
14470
+ <div class="strct-tf__col">
14471
+ <div class="strct-tf__head">
14472
+ <span class="strct-tf__title">{{ sourceLabel() }}</span>
14473
+ <span class="strct-tf__count">{{ sourceItems().length }}</span>
14474
+ </div>
14475
+ <strct-searchbox
14476
+ class="strct-tf__search"
14477
+ [placeholder]="searchPlaceholder()"
14478
+ [value]="sourceQuery()"
14479
+ (valueChange)="sourceQuery.set($event)"
14480
+ />
14481
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="sourceLabel()">
14482
+ @for (item of sourceItems(); track item.id) {
14483
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14484
+ <strct-checkbox
14485
+ [checked]="sourceChecked().has(item.id)"
14486
+ [disabled]="item.disabled ?? false"
14487
+ (checkedChange)="toggle(sourceChecked, item.id)"
14488
+ >
14489
+ @if (item.icon) {
14490
+ <strct-icon [name]="item.icon" [size]="14" />
14491
+ }
14492
+ <span class="strct-tf__label">{{ item.label }}</span>
14493
+ </strct-checkbox>
14494
+ </li>
14495
+ } @empty {
14496
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14497
+ }
14498
+ </ul>
14499
+ </div>
14500
+
14501
+ <div class="strct-tf__mid">
14502
+ <button
14503
+ strct-button
14504
+ size="sm"
14505
+ variant="neutral"
14506
+ [disabled]="!sourceChecked().size"
14507
+ [attr.aria-label]="assignLabel()"
14508
+ (click)="assign()"
14509
+ >
14510
+ <strct-icon name="chevronRight" [size]="14" />
14511
+ </button>
14512
+ <button
14513
+ strct-button
14514
+ size="sm"
14515
+ variant="neutral"
14516
+ [disabled]="!targetChecked().size"
14517
+ [attr.aria-label]="unassignLabel()"
14518
+ (click)="unassign()"
14519
+ >
14520
+ <strct-icon name="chevronLeft" [size]="14" />
14521
+ </button>
14522
+ </div>
14523
+
14524
+ <div class="strct-tf__col">
14525
+ <div class="strct-tf__head">
14526
+ <span class="strct-tf__title">{{ targetLabel() }}</span>
14527
+ <span class="strct-tf__count">{{ targetItems().length }}</span>
14528
+ </div>
14529
+ <strct-searchbox
14530
+ class="strct-tf__search"
14531
+ [placeholder]="searchPlaceholder()"
14532
+ [value]="targetQuery()"
14533
+ (valueChange)="targetQuery.set($event)"
14534
+ />
14535
+ <ul class="strct-tf__list" role="listbox" [attr.aria-label]="targetLabel()">
14536
+ @for (item of targetItems(); track item.id) {
14537
+ <li class="strct-tf__item" [class.strct-tf__item--disabled]="item.disabled">
14538
+ <strct-checkbox
14539
+ [checked]="targetChecked().has(item.id)"
14540
+ [disabled]="item.disabled ?? false"
14541
+ (checkedChange)="toggle(targetChecked, item.id)"
14542
+ >
14543
+ @if (item.icon) {
14544
+ <strct-icon [name]="item.icon" [size]="14" />
14545
+ }
14546
+ <span class="strct-tf__label">{{ item.label }}</span>
14547
+ </strct-checkbox>
14548
+ </li>
14549
+ } @empty {
14550
+ <li class="strct-tf__empty">{{ emptyLabel() }}</li>
14551
+ }
14552
+ </ul>
14553
+ </div>
14554
+ `, host: { class: 'strct-tf' }, styles: [".strct-tf{display:flex;align-items:stretch;gap:10px;width:100%}.strct-tf__col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:10px;background:var(--bg-2);border:1px solid var(--b2);border-radius:9px}.strct-tf__head{display:flex;align-items:baseline;gap:8px}.strct-tf__title{font-size:12.5px;font-weight:600;color:var(--t1)}.strct-tf__count{font-size:11px;color:var(--t3);font-variant-numeric:tabular-nums}.strct-tf__search{width:100%}.strct-tf__list{list-style:none;margin:0;padding:0;min-height:120px;max-height:240px;overflow:auto;display:flex;flex-direction:column;gap:1px}.strct-tf__item{display:flex;align-items:center;padding:2px 4px;border-radius:5px;color:var(--t1);font-size:12.5px}.strct-tf__item strct-icon{margin-inline-end:6px}.strct-tf__item:hover{background:var(--bg-3)}.strct-tf__item--disabled{color:var(--t4);cursor:default}.strct-tf__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-tf__empty{padding:12px 8px;font-size:12px;color:var(--t3)}.strct-tf__mid{display:flex;flex-direction:column;justify-content:center;gap:8px}\n"] }]
14555
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], assigned: [{ type: i0.Input, args: [{ isSignal: true, alias: "assigned", required: false }] }, { type: i0.Output, args: ["assignedChange"] }], sourceLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceLabel", required: false }] }], targetLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "targetLabel", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], assignLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "assignLabel", required: false }] }], unassignLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unassignLabel", required: false }] }], moved: [{ type: i0.Output, args: ["moved"] }] } });
14556
+
14557
+ /**
14558
+ * Split button — a primary action plus a chevron opening its variants
14559
+ * (Fluent's pattern): "Deploy ▾ → Deploy with snapshot / Deploy paused".
14560
+ *
14561
+ * <strct-split-button label="Deploy" [items]="variants"
14562
+ * (action)="deploy()" (picked)="run($event)" />
14563
+ *
14564
+ * The main segment fires `(action)`; menu entries fire `(picked)`.
14565
+ */
14566
+ class StrctSplitButton {
14567
+ /** Main action label. */
14568
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
14569
+ /** Menu entries (StrctMenuItem: id, label, icon?, critical?, disabled?). */
14570
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
14571
+ /** Optional leading icon of the main segment. */
14572
+ icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
14573
+ /** Filled (solid accent) look. */
14574
+ solid = input(false, { ...(ngDevMode ? { debugName: "solid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14575
+ /** Disable both segments. */
14576
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
14577
+ /** Accessible name of the chevron (localizable). */
14578
+ menuLabel = input('More actions', ...(ngDevMode ? [{ debugName: "menuLabel" }] : /* istanbul ignore next */ []));
14579
+ /** The main segment was clicked. */
14580
+ action = output();
14581
+ /** A menu entry was chosen. */
14582
+ picked = output();
14583
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
14584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSplitButton, isStandalone: true, selector: "strct-split-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, menuLabel: { classPropertyName: "menuLabel", publicName: "menuLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action", picked: "picked" }, ngImport: i0, template: `
14585
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14586
+ <button
14587
+ type="button"
14588
+ class="strct-sbt__main"
14589
+ [disabled]="disabled() || null"
14590
+ (click)="action.emit()"
14591
+ >
14592
+ @if (icon()) {
14593
+ <strct-icon [name]="icon()" [size]="13" />
14594
+ }
14595
+ {{ label() }}
14596
+ </button>
14597
+ <strct-dropdown align="end">
14598
+ <button
14599
+ type="button"
14600
+ class="strct-sbt__chev"
14601
+ strctDropdownTrigger
14602
+ [disabled]="disabled() || null"
14603
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14604
+ >
14605
+ <strct-icon name="chevronDown" [size]="12" />
14606
+ </button>
14607
+ @for (item of items(); track $index) {
14608
+ @if (item.divider) {
14609
+ <strct-dropdown-divider />
14610
+ } @else {
14611
+ <strct-dropdown-item
14612
+ [critical]="item.critical ?? false"
14613
+ [disabled]="item.disabled ?? false"
14614
+ (click)="!item.disabled && picked.emit(item)"
14615
+ >
14616
+ @if (item.icon) {
14617
+ <strct-icon [name]="item.icon" [size]="13" />
14618
+ }
14619
+ {{ item.label }}
14620
+ </strct-dropdown-item>
14621
+ }
14622
+ }
14623
+ </strct-dropdown>
14624
+ </div>
14625
+ `, isInline: true, styles: [".strct-sbt{display:inline-flex;align-items:stretch}.strct-sbt .strct-dd{display:inline-flex;align-self:stretch}.strct-sbt .strct-dd__trigger{display:inline-flex;align-items:stretch}.strct-sbt__chev{align-self:stretch;height:100%}.strct-sbt__main,.strct-sbt__chev{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--acc);background:transparent;color:var(--acc);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;padding:5px 12px}.strct-sbt__main{border-start-start-radius:7px;border-end-start-radius:7px;border-inline-end:0}.strct-sbt__chev{padding:5px 6px;border-start-end-radius:7px;border-end-end-radius:7px;border-inline-start:1px solid var(--acc50)}.strct-sbt--solid .strct-sbt__main,.strct-sbt--solid .strct-sbt__chev{background:var(--acc);color:var(--inv)}.strct-sbt--solid .strct-sbt__chev{border-inline-start-color:color-mix(in srgb,var(--inv) 30%,var(--acc))}.strct-sbt__main:hover:not(:disabled),.strct-sbt__chev:hover:not(:disabled){background:var(--acc18)}.strct-sbt--solid .strct-sbt__main:hover:not(:disabled),.strct-sbt--solid .strct-sbt__chev:hover:not(:disabled){filter:brightness(1.08);background:var(--acc)}.strct-sbt__main:disabled,.strct-sbt__chev:disabled{opacity:.5;cursor:default}.strct-sbt__main:focus-visible,.strct-sbt__chev:focus-visible{outline:2px solid var(--acc50);outline-offset:1px;position:relative;z-index:1}\n"], dependencies: [{ kind: "component", type: StrctDropdown, selector: "strct-dropdown", inputs: ["align", "popover", "popoverLabel"] }, { kind: "component", type: StrctDropdownDivider, selector: "strct-dropdown-divider" }, { kind: "component", type: StrctDropdownItem, selector: "strct-dropdown-item", inputs: ["critical", "disabled"] }, { kind: "directive", type: StrctDropdownTrigger, selector: "[strctDropdownTrigger]" }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14626
+ }
14627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSplitButton, decorators: [{
14628
+ type: Component,
14629
+ args: [{ selector: 'strct-split-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [
14630
+ StrctDropdown,
14631
+ StrctDropdownDivider,
14632
+ StrctDropdownItem,
14633
+ StrctDropdownTrigger,
14634
+ StrctIcon,
14635
+ ], template: `
14636
+ <div class="strct-sbt" [class.strct-sbt--solid]="solid()">
14637
+ <button
14638
+ type="button"
14639
+ class="strct-sbt__main"
14640
+ [disabled]="disabled() || null"
14641
+ (click)="action.emit()"
14642
+ >
14643
+ @if (icon()) {
14644
+ <strct-icon [name]="icon()" [size]="13" />
14645
+ }
14646
+ {{ label() }}
14647
+ </button>
14648
+ <strct-dropdown align="end">
14649
+ <button
14650
+ type="button"
14651
+ class="strct-sbt__chev"
14652
+ strctDropdownTrigger
14653
+ [disabled]="disabled() || null"
14654
+ [attr.aria-label]="menuLabel() + ': ' + label()"
14655
+ >
14656
+ <strct-icon name="chevronDown" [size]="12" />
14657
+ </button>
14658
+ @for (item of items(); track $index) {
14659
+ @if (item.divider) {
14660
+ <strct-dropdown-divider />
14661
+ } @else {
14662
+ <strct-dropdown-item
14663
+ [critical]="item.critical ?? false"
14664
+ [disabled]="item.disabled ?? false"
14665
+ (click)="!item.disabled && picked.emit(item)"
14666
+ >
14667
+ @if (item.icon) {
14668
+ <strct-icon [name]="item.icon" [size]="13" />
14669
+ }
14670
+ {{ item.label }}
14671
+ </strct-dropdown-item>
14672
+ }
14673
+ }
14674
+ </strct-dropdown>
14675
+ </div>
14676
+ `, styles: [".strct-sbt{display:inline-flex;align-items:stretch}.strct-sbt .strct-dd{display:inline-flex;align-self:stretch}.strct-sbt .strct-dd__trigger{display:inline-flex;align-items:stretch}.strct-sbt__chev{align-self:stretch;height:100%}.strct-sbt__main,.strct-sbt__chev{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--acc);background:transparent;color:var(--acc);font-family:var(--font);font-size:12.5px;font-weight:600;cursor:pointer;padding:5px 12px}.strct-sbt__main{border-start-start-radius:7px;border-end-start-radius:7px;border-inline-end:0}.strct-sbt__chev{padding:5px 6px;border-start-end-radius:7px;border-end-end-radius:7px;border-inline-start:1px solid var(--acc50)}.strct-sbt--solid .strct-sbt__main,.strct-sbt--solid .strct-sbt__chev{background:var(--acc);color:var(--inv)}.strct-sbt--solid .strct-sbt__chev{border-inline-start-color:color-mix(in srgb,var(--inv) 30%,var(--acc))}.strct-sbt__main:hover:not(:disabled),.strct-sbt__chev:hover:not(:disabled){background:var(--acc18)}.strct-sbt--solid .strct-sbt__main:hover:not(:disabled),.strct-sbt--solid .strct-sbt__chev:hover:not(:disabled){filter:brightness(1.08);background:var(--acc)}.strct-sbt__main:disabled,.strct-sbt__chev:disabled{opacity:.5;cursor:default}.strct-sbt__main:focus-visible,.strct-sbt__chev:focus-visible{outline:2px solid var(--acc50);outline-offset:1px;position:relative;z-index:1}\n"] }]
14677
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], picked: [{ type: i0.Output, args: ["picked"] }] } });
14678
+
14679
+ /**
14680
+ * Repeating diagonal text watermark over its content — compliance overlays
14681
+ * for classified/internal consoles ("CONFIDENTIAL", "user@corp · 2026-07-21"):
14682
+ *
14683
+ * <strct-watermark text="CONFIDENTIAL">…page…</strct-watermark>
14684
+ *
14685
+ * Rendered as a pointer-transparent tiled SVG layer, so the content below
14686
+ * stays fully interactive and selectable. Purely visual — `aria-hidden`.
14687
+ */
14688
+ class StrctWatermark {
14689
+ sanitizer = inject(DomSanitizer);
14690
+ /** The repeated watermark text. */
14691
+ text = input.required(...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
14692
+ /** Layer opacity (0–1). */
14693
+ opacity = input(0.06, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
14694
+ /** Rotation in degrees (negative = up-slope). */
14695
+ angle = input(-24, ...(ngDevMode ? [{ debugName: "angle" }] : /* istanbul ignore next */ []));
14696
+ /** Tile size in px — larger spreads the marks out. */
14697
+ gap = input(220, ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
14698
+ /** Font size in px. */
14699
+ fontSize = input(15, ...(ngDevMode ? [{ debugName: "fontSize" }] : /* istanbul ignore next */ []));
14700
+ tile = computed(() => {
14701
+ const size = this.gap();
14702
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">` +
14703
+ `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" ` +
14704
+ `transform="rotate(${this.angle()} ${size / 2} ${size / 2})" ` +
14705
+ `font-family="sans-serif" font-size="${this.fontSize()}" font-weight="600" ` +
14706
+ `fill="currentColor" fill-opacity="${this.opacity()}">${escapeXml(this.text())}</text></svg>`;
14707
+ return this.sanitizer.bypassSecurityTrustStyle(`url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`);
14708
+ }, ...(ngDevMode ? [{ debugName: "tile" }] : /* istanbul ignore next */ []));
14709
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, deps: [], target: i0.ɵɵFactoryTarget.Component });
14710
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: StrctWatermark, isStandalone: true, selector: "strct-watermark", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: true, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, angle: { classPropertyName: "angle", publicName: "angle", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, fontSize: { classPropertyName: "fontSize", publicName: "fontSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "strct-wm" }, ngImport: i0, template: `
14711
+ <ng-content />
14712
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14713
+ `, isInline: true, styles: [".strct-wm{position:relative;display:block}.strct-wm__layer{position:absolute;inset:0;pointer-events:none;background-repeat:repeat;z-index:5}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14714
+ }
14715
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctWatermark, decorators: [{
14716
+ type: Component,
14717
+ args: [{ selector: 'strct-watermark', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
14718
+ <ng-content />
14719
+ <div class="strct-wm__layer" aria-hidden="true" [style.background-image]="tile()"></div>
14720
+ `, host: { class: 'strct-wm' }, styles: [".strct-wm{position:relative;display:block}.strct-wm__layer{position:absolute;inset:0;pointer-events:none;background-repeat:repeat;z-index:5}\n"] }]
14721
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: true }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], angle: [{ type: i0.Input, args: [{ isSignal: true, alias: "angle", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], fontSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fontSize", required: false }] }] } });
14722
+ function escapeXml(s) {
14723
+ return s
14724
+ .replace(/&/g, '&amp;')
14725
+ .replace(/</g, '&lt;')
14726
+ .replace(/>/g, '&gt;')
14727
+ .replace(/"/g, '&quot;');
14728
+ }
14729
+
14730
+ /**
14731
+ * Feature tour (coach marks) — "what's new" onboarding over live UI:
14732
+ *
14733
+ * <strct-tour [(open)]="tourOpen" [steps]="steps" (finished)="done()" />
14734
+ *
14735
+ * Each step spotlights its `target` selector with a ring cut out of a dimmed
14736
+ * backdrop and anchors a card next to it. Arrows / buttons step, Escape
14737
+ * closes, and the card is a labeled dialog. Purely DOM-driven: targets are
14738
+ * looked up when the step shows, so it works over any page.
14739
+ */
14740
+ class StrctTour {
14741
+ /** Whether the tour is showing (two-way). */
14742
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
14743
+ /** The steps, in order. */
14744
+ steps = input.required(...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
14745
+ /** Localizable strings. */
14746
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
14747
+ backLabel = input('Back', ...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
14748
+ skipLabel = input('Skip', ...(ngDevMode ? [{ debugName: "skipLabel" }] : /* istanbul ignore next */ []));
14749
+ doneLabel = input('Done', ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
14750
+ /** The last step's Done was clicked (Escape / Skip emit `dismissed`). */
14751
+ finished = output();
14752
+ dismissed = output();
14753
+ index = signal(0, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
14754
+ /** Bumped on open/step/resize to re-measure the target. */
14755
+ measureTick = signal(0, ...(ngDevMode ? [{ debugName: "measureTick" }] : /* istanbul ignore next */ []));
14756
+ current = computed(() => this.steps()[this.index()] ?? null, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
14757
+ spot = computed(() => {
14758
+ this.measureTick();
14759
+ const step = this.current();
14760
+ if (!this.open() || !step?.target)
14761
+ return null;
14762
+ const el = document.querySelector(step.target);
14763
+ if (!el)
14764
+ return null;
14765
+ const r = el.getBoundingClientRect();
14766
+ return { top: r.top, left: r.left, width: r.width, height: r.height };
14767
+ }, ...(ngDevMode ? [{ debugName: "spot" }] : /* istanbul ignore next */ []));
14768
+ cardPos = computed(() => {
14769
+ const r = this.spot();
14770
+ const w = 300;
14771
+ const h = 170;
14772
+ if (!r) {
14773
+ return {
14774
+ top: Math.max(20, window.innerHeight / 2 - h / 2),
14775
+ left: Math.max(20, window.innerWidth / 2 - w / 2),
14776
+ };
14777
+ }
14778
+ // Below the target if it fits, else above; clamped into the viewport.
14779
+ let top = r.top + r.height + 14;
14780
+ if (top + h > window.innerHeight - 12)
14781
+ top = Math.max(12, r.top - h - 14);
14782
+ const left = Math.min(Math.max(12, r.left), window.innerWidth - w - 12);
14783
+ return { top, left };
14784
+ }, ...(ngDevMode ? [{ debugName: "cardPos" }] : /* istanbul ignore next */ []));
14785
+ constructor() {
14786
+ // Restart from step 0 on open; scroll the first target into view.
14787
+ effect(() => {
14788
+ if (this.open()) {
14789
+ this.index.set(0);
14790
+ this.scrollToTarget();
14791
+ }
14792
+ });
14793
+ }
14794
+ next() {
14795
+ if (this.index() >= this.steps().length - 1) {
14796
+ this.open.set(false);
14797
+ this.finished.emit();
14798
+ return;
14799
+ }
14800
+ this.index.update((i) => i + 1);
14801
+ this.scrollToTarget();
14802
+ }
14803
+ prev() {
14804
+ this.index.update((i) => Math.max(0, i - 1));
14805
+ this.scrollToTarget();
14806
+ }
14807
+ close() {
14808
+ this.open.set(false);
14809
+ this.dismissed.emit();
14810
+ }
14811
+ scrollToTarget() {
14812
+ setTimeout(() => {
14813
+ const sel = this.steps()[this.index()]?.target;
14814
+ if (sel)
14815
+ document.querySelector(sel)?.scrollIntoView?.({ block: 'center' });
14816
+ this.measureTick.update((n) => n + 1);
14817
+ });
14818
+ }
14819
+ onEscape() {
14820
+ if (this.open())
14821
+ this.close();
14822
+ }
14823
+ onReflow() {
14824
+ if (this.open())
14825
+ this.measureTick.update((n) => n + 1);
14826
+ }
14827
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, deps: [], target: i0.ɵɵFactoryTarget.Component });
14828
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctTour, isStandalone: true, selector: "strct-tour", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", finished: "finished", dismissed: "dismissed" }, host: { listeners: { "document:keydown.escape": "onEscape()", "window:resize": "onReflow()", "window:scroll": "onReflow()" } }, ngImport: i0, template: `
14829
+ @if (open() && current(); as step) {
14830
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14831
+ <div
14832
+ class="strct-tour__backdrop"
14833
+ [class.strct-tour__backdrop--dim]="!spot()"
14834
+ (click)="close()"
14835
+ ></div>
14836
+ @if (spot(); as r) {
14837
+ <div
14838
+ class="strct-tour__ring"
14839
+ [style.top.px]="r.top - 6"
14840
+ [style.left.px]="r.left - 6"
14841
+ [style.width.px]="r.width + 12"
14842
+ [style.height.px]="r.height + 12"
14843
+ ></div>
14844
+ }
14845
+ <div
14846
+ class="strct-tour__card"
14847
+ role="dialog"
14848
+ [attr.aria-label]="step.title"
14849
+ [style.top.px]="cardPos().top"
14850
+ [style.left.px]="cardPos().left"
14851
+ >
14852
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14853
+ <div class="strct-tour__title">{{ step.title }}</div>
14854
+ <div class="strct-tour__body">{{ step.body }}</div>
14855
+ <div class="strct-tour__nav">
14856
+ <button strct-button size="sm" variant="flat" (click)="close()">
14857
+ {{ skipLabel() }}
14858
+ </button>
14859
+ <span class="strct-tour__grow"></span>
14860
+ @if (index() > 0) {
14861
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14862
+ {{ backLabel() }}
14863
+ </button>
14864
+ }
14865
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14866
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14867
+ </button>
14868
+ </div>
14869
+ </div>
14870
+ }
14871
+ `, isInline: true, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:400;background:transparent}.strct-tour__backdrop--dim{background:#00000073}.strct-tour__ring{position:fixed;z-index:401;border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px #00000073;pointer-events:none}.strct-tour__card{position:fixed;z-index:402;width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"], dependencies: [{ kind: "component", type: StrctButton, selector: "button[strct-button], a[strct-button]", inputs: ["variant", "size", "solid", "block", "iconOnly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
14872
+ }
14873
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctTour, decorators: [{
14874
+ type: Component,
14875
+ args: [{ selector: 'strct-tour', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctButton], template: `
14876
+ @if (open() && current(); as step) {
14877
+ <!-- Always the click-catcher; dims only when no ring (its shadow dims). -->
14878
+ <div
14879
+ class="strct-tour__backdrop"
14880
+ [class.strct-tour__backdrop--dim]="!spot()"
14881
+ (click)="close()"
14882
+ ></div>
14883
+ @if (spot(); as r) {
14884
+ <div
14885
+ class="strct-tour__ring"
14886
+ [style.top.px]="r.top - 6"
14887
+ [style.left.px]="r.left - 6"
14888
+ [style.width.px]="r.width + 12"
14889
+ [style.height.px]="r.height + 12"
14890
+ ></div>
14891
+ }
14892
+ <div
14893
+ class="strct-tour__card"
14894
+ role="dialog"
14895
+ [attr.aria-label]="step.title"
14896
+ [style.top.px]="cardPos().top"
14897
+ [style.left.px]="cardPos().left"
14898
+ >
14899
+ <div class="strct-tour__step">{{ index() + 1 }} / {{ steps().length }}</div>
14900
+ <div class="strct-tour__title">{{ step.title }}</div>
14901
+ <div class="strct-tour__body">{{ step.body }}</div>
14902
+ <div class="strct-tour__nav">
14903
+ <button strct-button size="sm" variant="flat" (click)="close()">
14904
+ {{ skipLabel() }}
14905
+ </button>
14906
+ <span class="strct-tour__grow"></span>
14907
+ @if (index() > 0) {
14908
+ <button strct-button size="sm" variant="neutral" (click)="prev()">
14909
+ {{ backLabel() }}
14910
+ </button>
14911
+ }
14912
+ <button strct-button size="sm" variant="primary" solid (click)="next()">
14913
+ {{ index() === steps().length - 1 ? doneLabel() : nextLabel() }}
14914
+ </button>
14915
+ </div>
14916
+ </div>
14917
+ }
14918
+ `, styles: [".strct-tour__backdrop{position:fixed;inset:0;z-index:400;background:transparent}.strct-tour__backdrop--dim{background:#00000073}.strct-tour__ring{position:fixed;z-index:401;border:2px solid var(--acc);border-radius:9px;box-shadow:0 0 0 4px var(--acc30),0 0 0 9999px #00000073;pointer-events:none}.strct-tour__card{position:fixed;z-index:402;width:300px;padding:14px 16px;background:var(--bg-1);border:1px solid var(--b2);border-radius:9px;box-shadow:var(--shh)}.strct-tour__step{font-size:10.5px;font-weight:600;letter-spacing:.6px;color:var(--t3);margin-bottom:4px;font-variant-numeric:tabular-nums}.strct-tour__title{font-size:13.5px;font-weight:650;color:var(--t1);margin-bottom:6px}.strct-tour__body{font-size:12.5px;line-height:1.5;color:var(--t2);margin-bottom:12px}.strct-tour__nav{display:flex;align-items:center;gap:8px}.strct-tour__grow{flex:1}\n"] }]
14919
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], finished: [{ type: i0.Output, args: ["finished"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], onEscape: [{
14920
+ type: HostListener,
14921
+ args: ['document:keydown.escape']
14922
+ }], onReflow: [{
14923
+ type: HostListener,
14924
+ args: ['window:resize']
14925
+ }, {
14926
+ type: HostListener,
14927
+ args: ['window:scroll']
14928
+ }] } });
14929
+
14930
+ /**
14931
+ * Horizontal menubar — the application-menu strip ("File · Edit · View") for
14932
+ * dense tool-style consoles:
14933
+ *
14934
+ * <strct-menubar [menus]="menus" (picked)="run($event)" />
14935
+ *
14936
+ * Click (or Enter/Down) opens a menu; Left/Right move across the bar and,
14937
+ * while open, switch the open menu (the APG menubar convention). Escape and
14938
+ * outside click close. `(picked)` carries `{ menu, item }`.
14939
+ */
14940
+ class StrctMenubar {
14941
+ host = inject(ElementRef);
14942
+ /** Top-level menus. */
14943
+ menus = input.required(...(ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []));
14944
+ /** Accessible name of the bar (localizable). */
14945
+ ariaLabel = input('Application menu', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
14946
+ /** A menu entry was chosen. */
14947
+ picked = output();
14948
+ openId = signal(null, ...(ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []));
14949
+ focusIdx = signal(0, ...(ngDevMode ? [{ debugName: "focusIdx" }] : /* istanbul ignore next */ []));
14950
+ toggleMenu(id, i) {
14951
+ this.focusIdx.set(i);
14952
+ this.openId.set(this.openId() === id ? null : id);
14953
+ }
14954
+ pick(menu, item) {
14955
+ if (item.disabled)
14956
+ return;
14957
+ this.openId.set(null);
14958
+ this.picked.emit({ menu, item });
14959
+ }
14960
+ onTopKey(event, i) {
14961
+ const menus = this.menus();
14962
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
14963
+ event.preventDefault();
14964
+ const dir = event.key === 'ArrowRight' ? 1 : -1;
14965
+ const next = (i + dir + menus.length) % menus.length;
14966
+ this.focusIdx.set(next);
14967
+ // While a menu is open, moving across the bar switches the open menu.
14968
+ if (this.openId())
14969
+ this.openId.set(menus[next].id);
14970
+ setTimeout(() => {
14971
+ const tops = this.host.nativeElement.querySelectorAll('.strct-mb__top');
14972
+ tops[next]?.focus();
14973
+ });
14974
+ }
14975
+ else if (event.key === 'ArrowDown' || event.key === 'Enter') {
14976
+ event.preventDefault();
14977
+ this.openId.set(menus[i].id);
14978
+ }
14979
+ }
14980
+ onDocClick(event) {
14981
+ if (this.openId() && !this.host.nativeElement.contains(event.target)) {
14982
+ this.openId.set(null);
14983
+ }
14984
+ }
14985
+ onEscape() {
14986
+ this.openId.set(null);
14987
+ }
14988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, deps: [], target: i0.ɵɵFactoryTarget.Component });
14989
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctMenubar, isStandalone: true, selector: "strct-menubar", inputs: { menus: { classPropertyName: "menus", publicName: "menus", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { picked: "picked" }, host: { listeners: { "document:click": "onDocClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
14990
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
14991
+ @for (menu of menus(); track menu.id; let i = $index) {
14992
+ <div class="strct-mb__wrap">
14993
+ <button
14994
+ type="button"
14995
+ class="strct-mb__top"
14996
+ role="menuitem"
14997
+ [class.strct-mb__top--open]="openId() === menu.id"
14998
+ [attr.aria-haspopup]="'menu'"
14999
+ [attr.aria-expanded]="openId() === menu.id"
15000
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15001
+ (click)="toggleMenu(menu.id, i)"
15002
+ (keydown)="onTopKey($event, i)"
15003
+ >
15004
+ {{ menu.label }}
15005
+ </button>
15006
+ @if (openId() === menu.id) {
15007
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15008
+ @for (item of menu.items; track $index) {
15009
+ @if (item.divider) {
15010
+ <div class="strct-mb__divider" role="separator"></div>
15011
+ } @else {
15012
+ <button
15013
+ type="button"
15014
+ class="strct-mb__item"
15015
+ role="menuitem"
15016
+ [class.strct-mb__item--critical]="item.critical"
15017
+ [disabled]="item.disabled || null"
15018
+ (click)="pick(menu, item)"
15019
+ >
15020
+ @if (item.icon) {
15021
+ <strct-icon [name]="item.icon" [size]="13" />
15022
+ }
15023
+ {{ item.label }}
15024
+ </button>
15025
+ }
15026
+ }
15027
+ </div>
15028
+ }
15029
+ </div>
15030
+ }
15031
+ </div>
15032
+ `, isInline: true, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:7px}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:200;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not(:disabled){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not(:disabled){background:var(--critical-bg)}.strct-mb__item:disabled{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "size", "strokeWidth", "badge", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15033
+ }
15034
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctMenubar, decorators: [{
15035
+ type: Component,
15036
+ args: [{ selector: 'strct-menubar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon], template: `
15037
+ <div class="strct-mb" role="menubar" [attr.aria-label]="ariaLabel()">
15038
+ @for (menu of menus(); track menu.id; let i = $index) {
15039
+ <div class="strct-mb__wrap">
15040
+ <button
15041
+ type="button"
15042
+ class="strct-mb__top"
15043
+ role="menuitem"
15044
+ [class.strct-mb__top--open]="openId() === menu.id"
15045
+ [attr.aria-haspopup]="'menu'"
15046
+ [attr.aria-expanded]="openId() === menu.id"
15047
+ [attr.tabindex]="i === focusIdx() ? 0 : -1"
15048
+ (click)="toggleMenu(menu.id, i)"
15049
+ (keydown)="onTopKey($event, i)"
15050
+ >
15051
+ {{ menu.label }}
15052
+ </button>
15053
+ @if (openId() === menu.id) {
15054
+ <div class="strct-mb__menu" role="menu" [attr.aria-label]="menu.label">
15055
+ @for (item of menu.items; track $index) {
15056
+ @if (item.divider) {
15057
+ <div class="strct-mb__divider" role="separator"></div>
15058
+ } @else {
15059
+ <button
15060
+ type="button"
15061
+ class="strct-mb__item"
15062
+ role="menuitem"
15063
+ [class.strct-mb__item--critical]="item.critical"
15064
+ [disabled]="item.disabled || null"
15065
+ (click)="pick(menu, item)"
15066
+ >
15067
+ @if (item.icon) {
15068
+ <strct-icon [name]="item.icon" [size]="13" />
15069
+ }
15070
+ {{ item.label }}
15071
+ </button>
15072
+ }
15073
+ }
15074
+ </div>
15075
+ }
15076
+ </div>
15077
+ }
15078
+ </div>
15079
+ `, styles: [".strct-mb{display:inline-flex;gap:2px;padding:2px;background:var(--bg-2);border:1px solid var(--b1);border-radius:7px}.strct-mb__wrap{position:relative}.strct-mb__top{padding:4px 11px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;cursor:pointer;white-space:nowrap}.strct-mb__top:hover,.strct-mb__top--open{background:var(--bg-3)}.strct-mb__top:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}.strct-mb__menu{position:absolute;top:calc(100% + 4px);inset-inline-start:0;z-index:200;min-width:180px;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh);display:flex;flex-direction:column}.strct-mb__item{display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;border:0;border-radius:5px;background:transparent;color:var(--t1);font-family:var(--font);font-size:12.5px;text-align:start;cursor:pointer;white-space:nowrap}.strct-mb__item:hover:not(:disabled){background:var(--bg-3)}.strct-mb__item--critical{color:var(--critical)}.strct-mb__item--critical:hover:not(:disabled){background:var(--critical-bg)}.strct-mb__item:disabled{color:var(--t4);cursor:default}.strct-mb__item:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.strct-mb__divider{height:1px;margin:4px 6px;background:var(--b2)}\n"] }]
15080
+ }], propDecorators: { menus: [{ type: i0.Input, args: [{ isSignal: true, alias: "menus", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], onDocClick: [{
15081
+ type: HostListener,
15082
+ args: ['document:click', ['$event']]
15083
+ }], onEscape: [{
15084
+ type: HostListener,
15085
+ args: ['document:keydown.escape']
15086
+ }] } });
15087
+
15088
+ /**
15089
+ * Screen-reader announcements for state changes that have no visible text —
15090
+ * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
15091
+ *
15092
+ * private announcer = inject(StrctAnnouncer);
15093
+ * this.announcer.announce('12 rows loaded');
15094
+ *
15095
+ * Maintains one visually-hidden live region per politeness level; repeated
15096
+ * identical messages are re-announced (the region is cleared first).
15097
+ */
15098
+ class StrctAnnouncer {
15099
+ regions = new Map();
15100
+ clearTimer;
15101
+ announce(message, politeness = 'polite') {
15102
+ const region = this.regionFor(politeness);
15103
+ // Clear first so identical consecutive messages still fire.
15104
+ region.textContent = '';
15105
+ setTimeout(() => (region.textContent = message));
15106
+ clearTimeout(this.clearTimer);
15107
+ // Stale announcements should not linger for the next SR user to stumble on.
15108
+ this.clearTimer = setTimeout(() => (region.textContent = ''), 10_000);
15109
+ }
15110
+ regionFor(politeness) {
15111
+ let region = this.regions.get(politeness);
15112
+ if (!region) {
15113
+ region = document.createElement('div');
15114
+ region.setAttribute('aria-live', politeness);
15115
+ region.setAttribute('aria-atomic', 'true');
15116
+ region.className = 'strct-announcer';
15117
+ region.style.cssText =
15118
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
15119
+ document.body.appendChild(region);
15120
+ this.regions.set(politeness, region);
15121
+ }
15122
+ return region;
15123
+ }
15124
+ ngOnDestroy() {
15125
+ clearTimeout(this.clearTimer);
15126
+ for (const region of this.regions.values())
15127
+ region.remove();
15128
+ this.regions.clear();
15129
+ }
15130
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15131
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
15132
+ }
15133
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctAnnouncer, decorators: [{
15134
+ type: Injectable,
15135
+ args: [{ providedIn: 'root' }]
15136
+ }] });
15137
+
15138
+ function normalize(combo) {
15139
+ return combo
15140
+ .toLowerCase()
15141
+ .split('+')
15142
+ .map((p) => p.trim())
15143
+ .sort()
15144
+ .join('+');
15145
+ }
15146
+ function comboOf(event) {
15147
+ const parts = [];
15148
+ if (event.ctrlKey)
15149
+ parts.push('ctrl');
15150
+ if (event.metaKey)
15151
+ parts.push('meta');
15152
+ if (event.altKey)
15153
+ parts.push('alt');
15154
+ if (event.shiftKey && event.key.length > 1)
15155
+ parts.push('shift');
15156
+ parts.push(event.key.toLowerCase());
15157
+ return parts.sort().join('+');
15158
+ }
15159
+ /** `mod` matches Ctrl (and ⌘ on mac keyboards) — expand to both. */
15160
+ function expand(combo) {
15161
+ const n = combo.toLowerCase();
15162
+ if (!n.includes('mod'))
15163
+ return [normalize(n)];
15164
+ return [normalize(n.replace('mod', 'ctrl')), normalize(n.replace('mod', 'meta'))];
15165
+ }
15166
+ /**
15167
+ * Application hotkeys, centrally registered so they are discoverable — the
15168
+ * Blueprint pattern:
15169
+ *
15170
+ * private hotkeys = inject(StrctHotkeysService);
15171
+ * ngOnInit() { this.dispose = this.hotkeys.register(
15172
+ * { combo: 'mod+k', description: 'Open command palette', group: 'Global',
15173
+ * handler: () => this.palette.open() }); }
15174
+ *
15175
+ * Combos: `'a'`, `'?'`, `'ctrl+k'`, `'meta+shift+p'`, `'mod+k'` (mod = Ctrl/⌘).
15176
+ * Plain-key combos are suppressed while typing in inputs / textareas /
15177
+ * contenteditable; modifier combos always fire. Pair with
15178
+ * `<strct-hotkeys-help/>` for the ⌘/ ("?") cheatsheet overlay.
15179
+ */
15180
+ class StrctHotkeysService {
15181
+ registry = signal([], ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
15182
+ /** All registered hotkeys (for the help overlay). */
15183
+ hotkeys = this.registry.asReadonly();
15184
+ listener = (event) => this.onKeydown(event);
15185
+ constructor() {
15186
+ document.addEventListener('keydown', this.listener);
15187
+ }
15188
+ /** Register a hotkey; the returned function unregisters it. */
15189
+ register(hotkey) {
15190
+ this.registry.update((list) => [...list, hotkey]);
15191
+ return () => this.registry.update((list) => list.filter((h) => h !== hotkey));
15192
+ }
15193
+ onKeydown(event) {
15194
+ const combo = comboOf(event);
15195
+ const typing = isTyping(event);
15196
+ for (const hk of this.registry()) {
15197
+ if (!expand(hk.combo).includes(combo))
15198
+ continue;
15199
+ // While typing, only modifier combos may fire.
15200
+ if (typing && !(event.ctrlKey || event.metaKey || event.altKey))
15201
+ continue;
15202
+ event.preventDefault();
15203
+ hk.handler(event);
15204
+ return;
15205
+ }
15206
+ }
15207
+ ngOnDestroy() {
15208
+ document.removeEventListener('keydown', this.listener);
15209
+ }
15210
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15211
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, providedIn: 'root' });
15212
+ }
15213
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysService, decorators: [{
15214
+ type: Injectable,
15215
+ args: [{ providedIn: 'root' }]
15216
+ }], ctorParameters: () => [] });
15217
+ function isTyping(event) {
15218
+ const el = event.target;
15219
+ if (!el)
15220
+ return false;
15221
+ return (el.tagName === 'INPUT' ||
15222
+ el.tagName === 'TEXTAREA' ||
15223
+ el.tagName === 'SELECT' ||
15224
+ el.isContentEditable);
15225
+ }
15226
+ /**
15227
+ * The hotkey cheatsheet overlay. Mount once near the app root; it registers
15228
+ * `?` itself and lists everything in the service, grouped:
15229
+ *
15230
+ * <strct-hotkeys-help />
15231
+ */
15232
+ class StrctHotkeysHelp {
15233
+ service = inject(StrctHotkeysService);
15234
+ /** Overlay visibility (two-way; `?` toggles it too). */
15235
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
15236
+ /** Localizable strings. */
15237
+ title = input('Keyboard shortcuts', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
15238
+ emptyText = input('No shortcuts registered.', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
15239
+ grouped = computed(() => {
15240
+ const groups = new Map();
15241
+ for (const hk of this.service.hotkeys()) {
15242
+ const name = hk.group ?? 'General';
15243
+ const list = groups.get(name);
15244
+ if (list)
15245
+ list.push(hk);
15246
+ else
15247
+ groups.set(name, [hk]);
15248
+ }
15249
+ return [...groups.entries()].map(([name, keys]) => ({ name, keys }));
15250
+ }, ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
15251
+ constructor() {
15252
+ const dispose = this.service.register({
15253
+ combo: '?',
15254
+ description: 'Show this help',
15255
+ group: 'General',
15256
+ handler: () => this.open.update((v) => !v),
15257
+ });
15258
+ inject(DestroyRef).onDestroy(dispose);
15259
+ }
15260
+ // Escape is handled locally — registering it in the service would swallow
15261
+ // every Escape in the app (the service preventDefaults on match).
15262
+ onEscape() {
15263
+ if (this.open())
15264
+ this.open.set(false);
15265
+ }
15266
+ parts(combo) {
15267
+ return combo.split('+').map((p) => {
15268
+ const map = {
15269
+ mod: '⌘/Ctrl',
15270
+ meta: '⌘',
15271
+ ctrl: 'Ctrl',
15272
+ alt: 'Alt',
15273
+ shift: 'Shift',
15274
+ escape: 'Esc',
15275
+ };
15276
+ return map[p.trim().toLowerCase()] ?? p.trim().toUpperCase();
15277
+ });
15278
+ }
15279
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, deps: [], target: i0.ɵɵFactoryTarget.Component });
15280
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctHotkeysHelp, isStandalone: true, selector: "strct-hotkeys-help", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: `
15281
+ @if (open()) {
15282
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15283
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15284
+ <div class="strct-hkh__head">
15285
+ <span class="strct-hkh__title">{{ title() }}</span>
15286
+ <strct-kbd>?</strct-kbd>
15287
+ </div>
15288
+ @for (group of grouped(); track group.name) {
15289
+ <div class="strct-hkh__group">
15290
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15291
+ @for (hk of group.keys; track hk.combo + hk.description) {
15292
+ <div class="strct-hkh__row">
15293
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15294
+ <span class="strct-hkh__keys">
15295
+ @for (part of parts(hk.combo); track $index) {
15296
+ <strct-kbd>{{ part }}</strct-kbd>
15297
+ }
15298
+ </span>
15299
+ </div>
15300
+ }
15301
+ </div>
15302
+ } @empty {
15303
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15304
+ }
15305
+ </div>
15306
+ }
15307
+ `, isInline: true, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:400;background:#00000073}.strct-hkh{position:fixed;z-index:401;top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctKbd, selector: "strct-kbd" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15308
+ }
15309
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctHotkeysHelp, decorators: [{
15310
+ type: Component,
15311
+ args: [{ selector: 'strct-hotkeys-help', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctKbd], template: `
15312
+ @if (open()) {
15313
+ <div class="strct-hkh__backdrop" (click)="open.set(false)"></div>
15314
+ <div class="strct-hkh" role="dialog" [attr.aria-label]="title()">
15315
+ <div class="strct-hkh__head">
15316
+ <span class="strct-hkh__title">{{ title() }}</span>
15317
+ <strct-kbd>?</strct-kbd>
15318
+ </div>
15319
+ @for (group of grouped(); track group.name) {
15320
+ <div class="strct-hkh__group">
15321
+ <div class="strct-hkh__groupname">{{ group.name }}</div>
15322
+ @for (hk of group.keys; track hk.combo + hk.description) {
15323
+ <div class="strct-hkh__row">
15324
+ <span class="strct-hkh__desc">{{ hk.description }}</span>
15325
+ <span class="strct-hkh__keys">
15326
+ @for (part of parts(hk.combo); track $index) {
15327
+ <strct-kbd>{{ part }}</strct-kbd>
15328
+ }
15329
+ </span>
15330
+ </div>
15331
+ }
15332
+ </div>
15333
+ } @empty {
15334
+ <p class="strct-hkh__empty">{{ emptyText() }}</p>
15335
+ }
15336
+ </div>
15337
+ }
15338
+ `, styles: [".strct-hkh__backdrop{position:fixed;inset:0;z-index:400;background:#00000073}.strct-hkh{position:fixed;z-index:401;top:50%;left:50%;transform:translate(-50%,-50%);width:min(420px,calc(100vw - 32px));max-height:min(70vh,520px);overflow:auto;padding:16px 18px;background:var(--bg-1);border:1px solid var(--b2);border-radius:10px;box-shadow:var(--shh)}.strct-hkh__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.strct-hkh__title{font-size:13.5px;font-weight:650;color:var(--t1)}.strct-hkh__group{margin-bottom:12px}.strct-hkh__groupname{font-size:10.5px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:5px}.strct-hkh__row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}.strct-hkh__desc{font-size:12.5px;color:var(--t1)}.strct-hkh__keys{display:inline-flex;gap:4px}.strct-hkh__empty{font-size:12.5px;color:var(--t3)}\n"] }]
15339
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], onEscape: [{
15340
+ type: HostListener,
15341
+ args: ['document:keydown.escape']
15342
+ }] } });
15343
+
15344
+ /**
15345
+ * List drag-reorder primitive — the consumer owns the array:
15346
+ *
15347
+ * <ul strctReorder (reordered)="move($event)">
15348
+ * @for (s of steps(); track s.id) {
15349
+ * <li strctReorderItem>{{ s.label }}</li>
15350
+ * }
15351
+ * </ul>
15352
+ *
15353
+ * move({ from, to }: StrctReorderEvent) {
15354
+ * this.steps.update((s) => { const c = [...s]; c.splice(to, 0, ...c.splice(from, 1)); return c; });
15355
+ * }
15356
+ *
15357
+ * Items are HTML5-draggable; keyboard reorder is Alt+ArrowUp / Alt+ArrowDown
15358
+ * on the focused item (items get `tabindex="0"` unless they already manage
15359
+ * focus). Indexes are positions among the `strctReorderItem` siblings.
15360
+ */
15361
+ class StrctReorder {
15362
+ host = inject(ElementRef);
15363
+ /** Emits when a drag or keyboard move completes. */
15364
+ reordered = output();
15365
+ /** Disable all reordering (display-only mode). */
15366
+ reorderDisabled = input(false, ...(ngDevMode ? [{ debugName: "reorderDisabled" }] : /* istanbul ignore next */ []));
15367
+ dragIndex = signal(null, ...(ngDevMode ? [{ debugName: "dragIndex" }] : /* istanbul ignore next */ []));
15368
+ overIndex = signal(null, ...(ngDevMode ? [{ debugName: "overIndex" }] : /* istanbul ignore next */ []));
15369
+ items() {
15370
+ return [...this.host.nativeElement.querySelectorAll('[strctReorderItem]')];
15371
+ }
15372
+ indexOf(el) {
15373
+ return this.items().indexOf(el);
15374
+ }
15375
+ commit(from, to) {
15376
+ this.dragIndex.set(null);
15377
+ this.overIndex.set(null);
15378
+ if (from < 0 || to < 0 || from === to)
15379
+ return;
15380
+ this.reordered.emit({ from, to });
15381
+ }
15382
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15383
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: StrctReorder, isStandalone: true, selector: "[strctReorder]", inputs: { reorderDisabled: { classPropertyName: "reorderDisabled", publicName: "reorderDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { reordered: "reordered" }, ngImport: i0 });
15384
+ }
15385
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorder, decorators: [{
15386
+ type: Directive,
15387
+ args: [{ selector: '[strctReorder]' }]
15388
+ }], propDecorators: { reordered: [{ type: i0.Output, args: ["reordered"] }], reorderDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderDisabled", required: false }] }] } });
15389
+ /** One draggable row inside a `[strctReorder]` container. */
15390
+ class StrctReorderItem {
15391
+ list = inject(StrctReorder);
15392
+ el = inject(ElementRef);
15393
+ // Captured before our own binding ever writes tabindex, so it never flips.
15394
+ hadOwnTabindex = this.el.nativeElement.hasAttribute('tabindex');
15395
+ hostTabindex() {
15396
+ return this.hadOwnTabindex ? null : 0;
15397
+ }
15398
+ isDragging() {
15399
+ return this.list.dragIndex() === this.index();
15400
+ }
15401
+ isOver() {
15402
+ return this.list.overIndex() === this.index() && this.list.dragIndex() !== this.index();
15403
+ }
15404
+ index() {
15405
+ return this.list.indexOf(this.el.nativeElement);
15406
+ }
15407
+ onDragStart(event) {
15408
+ if (this.list.reorderDisabled())
15409
+ return;
15410
+ this.list.dragIndex.set(this.index());
15411
+ event.dataTransfer?.setData('text/plain', String(this.index()));
15412
+ if (event.dataTransfer)
15413
+ event.dataTransfer.effectAllowed = 'move';
15414
+ }
15415
+ onDragOver(event) {
15416
+ if (this.list.dragIndex() == null)
15417
+ return;
15418
+ event.preventDefault();
15419
+ this.list.overIndex.set(this.index());
15420
+ }
15421
+ onDrop(event) {
15422
+ event.preventDefault();
15423
+ const from = this.list.dragIndex();
15424
+ if (from == null)
15425
+ return;
15426
+ this.list.commit(from, this.index());
15427
+ }
15428
+ onDragEnd() {
15429
+ this.list.dragIndex.set(null);
15430
+ this.list.overIndex.set(null);
15431
+ }
15432
+ onKeydown(event) {
15433
+ if (this.list.reorderDisabled() || !event.altKey)
15434
+ return;
15435
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
15436
+ return;
15437
+ event.preventDefault();
15438
+ const from = this.index();
15439
+ const to = event.key === 'ArrowUp' ? from - 1 : from + 1;
15440
+ if (to < 0 || to >= this.list.items().length)
15441
+ return;
15442
+ this.list.commit(from, to);
15443
+ // Keep focus on the moved item after the consumer re-renders.
15444
+ setTimeout(() => this.list.items()[to]?.focus());
15445
+ }
15446
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
15447
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.16", type: StrctReorderItem, isStandalone: true, selector: "[strctReorderItem]", host: { listeners: { "dragstart": "onDragStart($event)", "dragover": "onDragOver($event)", "drop": "onDrop($event)", "dragend": "onDragEnd()", "keydown": "onKeydown($event)" }, properties: { "attr.draggable": "!list.reorderDisabled()", "attr.tabindex": "hostTabindex()", "class.strct-reorder--dragging": "isDragging()", "class.strct-reorder--over": "isOver()", "attr.aria-roledescription": "'sortable'" } }, ngImport: i0 });
15448
+ }
15449
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctReorderItem, decorators: [{
15450
+ type: Directive,
15451
+ args: [{
15452
+ selector: '[strctReorderItem]',
15453
+ host: {
15454
+ '[attr.draggable]': '!list.reorderDisabled()',
15455
+ '[attr.tabindex]': 'hostTabindex()',
15456
+ '[class.strct-reorder--dragging]': 'isDragging()',
15457
+ '[class.strct-reorder--over]': 'isOver()',
15458
+ '[attr.aria-roledescription]': "'sortable'",
15459
+ },
15460
+ }]
15461
+ }], propDecorators: { onDragStart: [{
15462
+ type: HostListener,
15463
+ args: ['dragstart', ['$event']]
15464
+ }], onDragOver: [{
15465
+ type: HostListener,
15466
+ args: ['dragover', ['$event']]
15467
+ }], onDrop: [{
15468
+ type: HostListener,
15469
+ args: ['drop', ['$event']]
15470
+ }], onDragEnd: [{
15471
+ type: HostListener,
15472
+ args: ['dragend']
15473
+ }], onKeydown: [{
15474
+ type: HostListener,
15475
+ args: ['keydown', ['$event']]
15476
+ }] } });
15477
+
12762
15478
  /**
12763
15479
  * Public API surface of the strct UI library.
12764
15480
  */
@@ -12768,5 +15484,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
12768
15484
  * Generated bundle index. Do not edit.
12769
15485
  */
12770
15486
 
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 };
15487
+ export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionMenu, StrctSegmented, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWatermark, StrctWizard, parseAnsi, registerStrctIcon, strctComputeDiff, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone };
12772
15488
  //# sourceMappingURL=akcelik-strct.mjs.map