@akcelik/strct 6.0.1 → 6.0.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.
@@ -2486,6 +2486,7 @@ var StrctHeader = class StrctHeader {
2486
2486
  }
2487
2487
  },
2488
2488
  host: {
2489
+ attributes: { "role": "banner" },
2489
2490
  properties: {
2490
2491
  "class.strct-header--accent": "accent()",
2491
2492
  "style.--strct-header-accent": "accent() || null"
@@ -2734,6 +2735,7 @@ i0.ɵɵngDeclareClassMetadata({
2734
2735
  `,
2735
2736
  host: {
2736
2737
  class: "strct-header",
2738
+ role: "banner",
2737
2739
  "[class.strct-header--accent]": "accent()",
2738
2740
  "[style.--strct-header-accent]": "accent() || null"
2739
2741
  },
@@ -2799,7 +2801,10 @@ var StrctFooter = class StrctFooter {
2799
2801
  type: StrctFooter,
2800
2802
  isStandalone: true,
2801
2803
  selector: "strct-footer",
2802
- host: { classAttribute: "strct-footer" },
2804
+ host: {
2805
+ attributes: { "role": "contentinfo" },
2806
+ classAttribute: "strct-footer"
2807
+ },
2803
2808
  ngImport: i0,
2804
2809
  template: `<ng-content />`,
2805
2810
  isInline: true,
@@ -2820,7 +2825,10 @@ i0.ɵɵngDeclareClassMetadata({
2820
2825
  changeDetection: ChangeDetectionStrategy.OnPush,
2821
2826
  encapsulation: ViewEncapsulation.None,
2822
2827
  template: `<ng-content />`,
2823
- host: { class: "strct-footer" },
2828
+ host: {
2829
+ class: "strct-footer",
2830
+ role: "contentinfo"
2831
+ },
2824
2832
  styles: [".strct-footer{display:flex;align-items:center;gap:12px;height:32px;padding:0 16px;background:var(--bg-1);border-top:1px solid var(--b2);font-size:12px;color:var(--t2)}\n"]
2825
2833
  }]
2826
2834
  }]
@@ -14470,6 +14478,12 @@ var StrctSearchbox = class StrctSearchbox {
14470
14478
  transform: booleanAttribute
14471
14479
  });
14472
14480
  ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
14481
+ triggerName = computed(() => {
14482
+ const label = this.placeholder().trim();
14483
+ const extra = this.ariaLabel().trim();
14484
+ if (extra && label && extra.toLowerCase().startsWith(label.toLowerCase())) return extra;
14485
+ return [[label, this.hint().trim()].filter(Boolean).join(" "), extra].filter(Boolean).join(", ");
14486
+ }, ...ngDevMode ? [{ debugName: "triggerName" }] : /* istanbul ignore next */ []);
14473
14487
  clearLabel = input("Clear search", ...ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []);
14474
14488
  value = model("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
14475
14489
  search = output();
@@ -14587,11 +14601,14 @@ var StrctSearchbox = class StrctSearchbox {
14587
14601
  ngImport: i0,
14588
14602
  template: `
14589
14603
  @if (trigger()) {
14604
+ <!-- The accessible name must contain everything the button shows (WCAG
14605
+ 2.5.3, label in name): a voice user says what they see — "Search"
14606
+ and the "⌘K" chip. See triggerName(). -->
14590
14607
  <button
14591
14608
  type="button"
14592
14609
  class="strct-sb strct-sb--trigger"
14593
14610
  [disabled]="isDisabled()"
14594
- [attr.aria-label]="ariaLabel() || placeholder()"
14611
+ [attr.aria-label]="triggerName()"
14595
14612
  (click)="activated.emit()"
14596
14613
  >
14597
14614
  <strct-icon class="strct-sb__icon" name="search" [size]="14" [strokeWidth]="1.6" />
@@ -14677,11 +14694,14 @@ i0.ɵɵngDeclareClassMetadata({
14677
14694
  }],
14678
14695
  template: `
14679
14696
  @if (trigger()) {
14697
+ <!-- The accessible name must contain everything the button shows (WCAG
14698
+ 2.5.3, label in name): a voice user says what they see — "Search"
14699
+ and the "⌘K" chip. See triggerName(). -->
14680
14700
  <button
14681
14701
  type="button"
14682
14702
  class="strct-sb strct-sb--trigger"
14683
14703
  [disabled]="isDisabled()"
14684
- [attr.aria-label]="ariaLabel() || placeholder()"
14704
+ [attr.aria-label]="triggerName()"
14685
14705
  (click)="activated.emit()"
14686
14706
  >
14687
14707
  <strct-icon class="strct-sb__icon" name="search" [size]="14" [strokeWidth]="1.6" />
@@ -14900,7 +14920,7 @@ var StrctCopy = class StrctCopy {
14900
14920
  [class.strct-copy--done]="done()"
14901
14921
  [class.strct-copy--error]="error()"
14902
14922
  [attr.aria-label]="
14903
- done() ? copiedLabel() : error() ? failedLabel() : ariaLabel() || copyLabel()
14923
+ done() ? copiedLabel() : error() ? failedLabel() : ariaLabel() || label() || copyLabel()
14904
14924
  "
14905
14925
  (click)="copy()"
14906
14926
  >
@@ -14959,7 +14979,7 @@ i0.ɵɵngDeclareClassMetadata({
14959
14979
  [class.strct-copy--done]="done()"
14960
14980
  [class.strct-copy--error]="error()"
14961
14981
  [attr.aria-label]="
14962
- done() ? copiedLabel() : error() ? failedLabel() : ariaLabel() || copyLabel()
14982
+ done() ? copiedLabel() : error() ? failedLabel() : ariaLabel() || label() || copyLabel()
14963
14983
  "
14964
14984
  (click)="copy()"
14965
14985
  >
@@ -19026,6 +19046,8 @@ i0.ɵɵngDeclareClassMetadata({
19026
19046
  });
19027
19047
  var StrctChips = class StrctChips {
19028
19048
  placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
19049
+ ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
19050
+ inField = !!inject(StrctField, { optional: true });
19029
19051
  allowDuplicates = input(false, {
19030
19052
  ...ngDevMode ? { debugName: "allowDuplicates" } : /* istanbul ignore next */ {},
19031
19053
  transform: booleanAttribute
@@ -19094,6 +19116,13 @@ var StrctChips = class StrctChips {
19094
19116
  isRequired: false,
19095
19117
  transformFunction: null
19096
19118
  },
19119
+ ariaLabel: {
19120
+ classPropertyName: "ariaLabel",
19121
+ publicName: "ariaLabel",
19122
+ isSignal: true,
19123
+ isRequired: false,
19124
+ transformFunction: null
19125
+ },
19097
19126
  allowDuplicates: {
19098
19127
  classPropertyName: "allowDuplicates",
19099
19128
  publicName: "allowDuplicates",
@@ -19109,14 +19138,13 @@ var StrctChips = class StrctChips {
19109
19138
  }],
19110
19139
  ngImport: i0,
19111
19140
  template: `
19112
- <div
19113
- class="strct-chips"
19114
- role="button"
19115
- tabindex="0"
19116
- (click)="input.focus()"
19117
- (keydown.enter)="input.focus()"
19118
- (keydown.space)="input.focus()"
19119
- >
19141
+ <!-- Pointer convenience only: a click on the field's padding focuses the
19142
+ real <input>, which is the tab stop and owns the keyboard path. It was
19143
+ a role="button" tab stop wrapping the input and the chips' remove
19144
+ buttons — an interactive inside an interactive (axe nested-interactive)
19145
+ and one Tab press that did nothing. -->
19146
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
19147
+ <div class="strct-chips" (click)="input.focus()">
19120
19148
  @for (chip of value(); track $index) {
19121
19149
  <strct-tag status="accent" removable [disabled]="isDisabled()" (removed)="remove($index)">{{
19122
19150
  chip
@@ -19126,6 +19154,7 @@ var StrctChips = class StrctChips {
19126
19154
  #input
19127
19155
  type="text"
19128
19156
  class="strct-chips__input"
19157
+ [attr.aria-label]="ariaLabel() || (inField ? null : placeholder() || null)"
19129
19158
  [value]="draft()"
19130
19159
  [placeholder]="value().length ? '' : placeholder()"
19131
19160
  [disabled]="isDisabled()"
@@ -19177,14 +19206,13 @@ i0.ɵɵngDeclareClassMetadata({
19177
19206
  multi: true
19178
19207
  }],
19179
19208
  template: `
19180
- <div
19181
- class="strct-chips"
19182
- role="button"
19183
- tabindex="0"
19184
- (click)="input.focus()"
19185
- (keydown.enter)="input.focus()"
19186
- (keydown.space)="input.focus()"
19187
- >
19209
+ <!-- Pointer convenience only: a click on the field's padding focuses the
19210
+ real <input>, which is the tab stop and owns the keyboard path. It was
19211
+ a role="button" tab stop wrapping the input and the chips' remove
19212
+ buttons — an interactive inside an interactive (axe nested-interactive)
19213
+ and one Tab press that did nothing. -->
19214
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
19215
+ <div class="strct-chips" (click)="input.focus()">
19188
19216
  @for (chip of value(); track $index) {
19189
19217
  <strct-tag status="accent" removable [disabled]="isDisabled()" (removed)="remove($index)">{{
19190
19218
  chip
@@ -19194,6 +19222,7 @@ i0.ɵɵngDeclareClassMetadata({
19194
19222
  #input
19195
19223
  type="text"
19196
19224
  class="strct-chips__input"
19225
+ [attr.aria-label]="ariaLabel() || (inField ? null : placeholder() || null)"
19197
19226
  [value]="draft()"
19198
19227
  [placeholder]="value().length ? '' : placeholder()"
19199
19228
  [disabled]="isDisabled()"
@@ -19215,6 +19244,14 @@ i0.ɵɵngDeclareClassMetadata({
19215
19244
  required: false
19216
19245
  }]
19217
19246
  }],
19247
+ ariaLabel: [{
19248
+ type: i0.Input,
19249
+ args: [{
19250
+ isSignal: true,
19251
+ alias: "ariaLabel",
19252
+ required: false
19253
+ }]
19254
+ }],
19218
19255
  allowDuplicates: [{
19219
19256
  type: i0.Input,
19220
19257
  args: [{
@@ -24844,6 +24881,7 @@ const DG_LABELS = {
24844
24881
  showing: (shown, total) => total === void 0 ? `Showing ${shown}` : `Showing the latest ${shown} of ${total}`,
24845
24882
  row: "row",
24846
24883
  rows: "rows",
24884
+ pagination: "Pagination",
24847
24885
  selected: "selected",
24848
24886
  selectAll: "Select all rows on this page",
24849
24887
  selectRow: "Select row",
@@ -25137,6 +25175,10 @@ var StrctDatagrid = class StrctDatagrid {
25137
25175
  ...DG_LABELS,
25138
25176
  ...this.labels()
25139
25177
  }), ...ngDevMode ? [{ debugName: "L" }] : /* istanbul ignore next */ []);
25178
+ regionName(base) {
25179
+ const caption = this.caption().trim();
25180
+ return caption ? `${caption} ${base}` : base;
25181
+ }
25140
25182
  editHint = input(false, {
25141
25183
  ...ngDevMode ? { debugName: "editHint" } : /* istanbul ignore next */ {},
25142
25184
  transform: booleanAttribute
@@ -26890,7 +26932,7 @@ var StrctDatagrid = class StrctDatagrid {
26890
26932
  }
26891
26933
  ],
26892
26934
  ngImport: i0,
26893
- template: "<!-- One polite region for everything the grid does to itself: sorting,\n paging and filtering all change the rows without saying so. -->\n<span class=\"strct-dg__announcer\" role=\"status\" aria-live=\"polite\">{{ announcement() }}</span>\n@if (actionBarDef() || quickFilterable() || actionBarCaptionDef()) {\n <div class=\"strct-dg__toolbar\">\n @if (quickFilterable()) {\n <strct-searchbox\n class=\"strct-dg__quickfilter\"\n [class.strct-dg__quickfilter--end]=\"quickFilterAlign() === 'end'\"\n [placeholder]=\"L().quickFilter\"\n [ariaLabel]=\"L().quickFilter\"\n [value]=\"quickFilter()\"\n (valueChange)=\"quickFilter.set($event)\"\n />\n @if (filterNote(); as note) {\n <span\n class=\"strct-dg__filternote\"\n [class.strct-dg__filternote--end]=\"quickFilterAlign() === 'end'\"\n >{{ note }}</span\n >\n }\n }\n <span class=\"strct-dg__actioncaption\">\n <ng-content select=\"[strctDatagridActionBarCaption]\" />\n </span>\n <ng-content select=\"[strctDatagridActionBar]\" />\n </div>\n}\n\n@if (hasEditableColumn()) {\n <!-- What the editable cells point at with aria-describedby: a keyboard\n user is told the cell can be edited, and how. -->\n <!-- In flow, not absolutely positioned: an out-of-flow box whose\n containing block is the page extends the document's scrollable area\n from wherever the grid happens to sit. 1x1 and clipped is enough. -->\n <span\n class=\"strct-sr-only\"\n style=\"\n display: inline-block;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n \"\n [id]=\"editHintId\"\n >{{ L().editCellInstructions }}</span\n >\n}\n\n@if (bulkHint(); as hint) {\n <!-- Selecting the page is not selecting the set. Say which one happened,\n and offer the other. -->\n <div class=\"strct-dg__bulk\" role=\"status\">\n <span>{{ hint.text }}</span>\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"hint.run()\">\n {{ hint.action }}\n </button>\n </div>\n}\n<div\n class=\"strct-dg__layout\"\n [class.strct-dg__layout--paned]=\"paneOpen()\"\n [class.strct-dg__layout--panetakeover]=\"paneOpen() && narrowPane()\"\n [style.--strct-dg-pane-list-w]=\"paneListWidth()\"\n>\n <div\n class=\"strct-dg__scroll\"\n tabindex=\"0\"\n role=\"region\"\n [attr.aria-label]=\"L().rows\"\n [style.max-height.px]=\"virtual() ? viewportHeight() : maxHeight()\"\n (scroll)=\"virtual() && onScroll($event)\"\n >\n <table\n class=\"strct-dg\"\n [attr.role]=\"childrenKey() ? 'treegrid' : gridMode() ? 'grid' : null\"\n [attr.aria-rowcount]=\"ariaRowCount()\"\n [attr.aria-labelledby]=\"caption() ? captionId : null\"\n >\n @if (caption()) {\n <caption class=\"strct-dg__caption\" [id]=\"captionId\">\n {{\n caption()\n }}\n </caption>\n }\n <thead>\n <tr>\n <!-- A utility column is still a column: axe reports every empty\n header, and a screen-reader user hears nothing where the\n detail, expand and select controls are. The name is hidden,\n not absent. -->\n @if (canDetail()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <span class=\"strct-sr-only\">{{ L().detailColumn }}</span>\n </th>\n }\n @if (canExpand()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <span class=\"strct-sr-only\">{{ L().expandColumn }}</span>\n </th>\n }\n @if (hasSelectionColumn()) {\n <th\n class=\"strct-dg__sel\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'multiple') {\n <strct-checkbox\n [ariaLabel]=\"L().selectAll\"\n [checked]=\"allPageSelected()\"\n [indeterminate]=\"somePageSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n } @else {\n <span class=\"strct-sr-only\">{{ L().selectColumn }}</span>\n }\n </th>\n }\n @for (col of visibleColumns(); track col.key) {\n <th\n scope=\"col\"\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [style.width]=\"colWidth(col.key) ?? col.width ?? null\"\n [class.strct-dg__th--sortable]=\"col.sortable\"\n [class.strct-dg__th--drop]=\"col.key === dropKey()\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-col]=\"col.key\"\n [attr.aria-sort]=\"col.sortable ? ariaSort(col.key) : null\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (dragstart)=\"onColDragStart(col.key, $event)\"\n (dragover)=\"onColDragOver(col.key, $event)\"\n (drop)=\"onColDrop(col.key)\"\n (dragend)=\"onColDragEnd()\"\n >\n <span class=\"strct-dg__hd\">\n <!-- A header that sorts is a button: a th with tabindex is\n read as \"column header, focusable\" and never says it\n can be activated. aria-sort stays on the th, which is\n what carries the column's sorted state. -->\n @if (col.sortable) {\n <!-- The button covers most of the header, so it carries\n the drag source too: a reorderable column has to be\n grabbable where the pointer actually lands. The\n event bubbles to the th's own handlers. -->\n <button\n type=\"button\"\n class=\"strct-dg__sortbtn\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (click)=\"sortBy(col.key)\"\n >\n {{ col.label }}\n <strct-icon class=\"strct-dg__sorticon\" [name]=\"sortIcon(col.key)\" [size]=\"13\" />\n </button>\n } @else {\n {{ col.label }}\n }\n @if (col.filterable || col.filterOptions) {\n <button\n #fbtn\n type=\"button\"\n class=\"strct-dg__filterbtn\"\n [class.strct-dg__filterbtn--active]=\"hasFilter(col.key)\"\n [attr.aria-expanded]=\"filterOpenKey() === col.key\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n (click)=\"toggleFilterPanel(col.key, $event)\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n <strct-icon name=\"filter\" [size]=\"12\" />\n </button>\n @if (filterOpenKey() === col.key) {\n <div\n class=\"strct-dg__filterpanel\"\n role=\"dialog\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [strctOverlay]=\"fbtn\"\n strctOverlayPlacement=\"bottom-start\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n @if (col.filterOptions; as opts) {\n @for (opt of opts; track $index) {\n <label class=\"strct-dg__filteropt\">\n <input\n type=\"checkbox\"\n [checked]=\"optionChecked(col.key, opt)\"\n (change)=\"toggleFilterOption(col.key, opt)\"\n />\n <span>{{ opt }}</span>\n </label>\n }\n } @else {\n <input\n class=\"strct-dg__filterinput\"\n type=\"text\"\n [attr.placeholder]=\"L().filterPlaceholder\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [value]=\"textFilter(col.key)\"\n (input)=\"setTextFilter(col.key, $any($event.target).value)\"\n />\n }\n <button\n type=\"button\"\n class=\"strct-dg__filterclear\"\n [disabled]=\"!hasFilter(col.key)\"\n (click)=\"clearFilter(col.key)\"\n >\n {{ L().clearFilter }}\n </button>\n </div>\n }\n }\n </span>\n @if (resizable() && col.resizable !== false) {\n <!-- A separator, not a bare div: a column could only be\n resized with a mouse, which is the one input a\n console cannot assume. -->\n <div\n class=\"strct-dg__resize\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"L().resizeColumn(col.label)\"\n [attr.aria-valuenow]=\"separatorValue(col.key)\"\n [attr.aria-valuemin]=\"40\"\n (mousedown)=\"onResizeStart($event, col.key)\"\n (keydown)=\"onResizeKey($event, col.key)\"\n (dblclick)=\"resetWidth(col.key)\"\n ></div>\n }\n </th>\n }\n @if (canActions()) {\n <th class=\"strct-dg__actioncol\" scope=\"col\" [attr.aria-label]=\"L().actions\"></th>\n }\n </tr>\n </thead>\n <!--\n focusin is the real keyboard path (focus does not bubble from cells);\n the plain focus binding exists to satisfy mouse-events-have-key-events.\n -->\n <tbody\n (mouseover)=\"revealIfClipped($event)\"\n (focusin)=\"revealIfClipped($event)\"\n (focus)=\"revealIfClipped($event)\"\n >\n @if (loading()) {\n @for (_ of [1, 2, 3, 4, 5]; track $index) {\n <tr class=\"strct-dg__skeleton-row\">\n @if (canDetail()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (canExpand()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (hasSelectionColumn()) {\n <td class=\"strct-dg__sel\"></td>\n }\n @for (col of visibleColumns(); track col.key) {\n <td [style.text-align]=\"col.align ?? 'start'\">\n <div class=\"strct-dg__skeleton-block\"></div>\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\"></td>\n }\n </tr>\n }\n } @else {\n @if (virtual() && topPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"topPad()\"></td>\n </tr>\n }\n @for (it of displayItems(); track itemKey(it)) {\n @if (it.group; as grp) {\n <!-- Group header: distinct value + count, collapsible. -->\n <tr class=\"strct-dg__grouprow\">\n @if (groupSelect() && mode() === 'multiple') {\n <td class=\"strct-dg__sel\">\n <strct-checkbox\n [ariaLabel]=\"L().selectAllInGroup(grp.label)\"\n [checked]=\"groupAllSelected(grp.key)\"\n [indeterminate]=\"groupSomeSelected(grp.key)\"\n (checkedChange)=\"toggleGroupSelection(grp.key)\"\n />\n </td>\n }\n <td\n [attr.colspan]=\"\n groupSelect() && mode() === 'multiple' ? colspan() - 1 : colspan()\n \"\n >\n <button\n type=\"button\"\n class=\"strct-dg__groupbtn\"\n [attr.aria-expanded]=\"!grp.collapsed\"\n (click)=\"toggleGroup(grp.key)\"\n >\n <span\n class=\"strct-dg__groupchev\"\n [class.strct-dg__groupchev--open]=\"!grp.collapsed\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </span>\n @if (groupHeaderDef(); as tpl) {\n <!-- The consumer draws the row; the button, its\n aria-expanded and the keyboard stay ours. -->\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: grp,\n group: grp,\n count: grp.count,\n }\"\n />\n } @else {\n <span\n class=\"strct-dg__grouplabel strct-dg__grouplabel--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.label }}</span\n >\n <!-- The three spans run together in the button's\n name (\"Needs your decision1Blocks the join…\");\n the separators are read, not seen. -->\n <span class=\"strct-dg__announcer\">, </span>\n <span\n class=\"strct-dg__groupcount strct-dg__groupcount--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.count }}</span\n >\n <!-- Seen: the chip's number. Heard: the number with\n the grid's noun, because \"Inconclusive, 3\" does\n not say three of what. -->\n <span class=\"strct-dg__announcer\"> {{ countNoun(grp.count) }}</span>\n @if (grp.note) {\n <span class=\"strct-dg__announcer\">. </span>\n <span class=\"strct-dg__groupnote\">{{ grp.note }}</span>\n }\n }\n </button>\n </td>\n </tr>\n } @else {\n @let row = it.row!;\n <tr\n [attr.aria-rowindex]=\"ariaRowIndex(row)\"\n [class.strct-dg__row--selected]=\"isSelected(row)\"\n [class.strct-dg__row--locked]=\"lockedReason(row) !== null\"\n [attr.aria-selected]=\"mode() === 'none' ? null : isSelected(row)\"\n [attr.aria-disabled]=\"lockedReason(row) !== null ? 'true' : null\"\n [attr.title]=\"lockedReason(row) || null\"\n (click)=\"onRowPick(row)\"\n [class.strct-dg__row--active]=\"paneOpen() && row === activeRow()\"\n [attr.aria-level]=\"childrenKey() ? treeDepth(row) + 1 : null\"\n [attr.aria-expanded]=\"\n childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null\n \"\n [attr.tabindex]=\"\n childrenKey()\n ? treeRowTabindex(row)\n : gridMode()\n ? gridRowTabindex(row)\n : canDetail()\n ? paneRowTabindex(row)\n : null\n \"\n (keydown)=\"\n childrenKey()\n ? onTreeRowKeydown(row, $event)\n : gridMode()\n ? onGridRowKeydown(row, $event)\n : onPaneRowKeydown(row, $event)\n \"\n (dblclick)=\"onRowActivate(row)\"\n >\n @if (canDetail()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__detailbtn\"\n [class.strct-dg__detailbtn--active]=\"row === activeRow()\"\n [attr.aria-expanded]=\"row === activeRow()\"\n [attr.aria-label]=\"L().openDetail\"\n (click)=\"openDetail(row)\"\n >\n <strct-icon name=\"chevronDoubleRight\" [size]=\"13\" [strokeWidth]=\"1.6\" />\n </button>\n </td>\n }\n @if (canExpand()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__expandbtn\"\n [class.strct-dg__expandbtn--open]=\"isExpanded(row)\"\n [attr.aria-expanded]=\"isExpanded(row)\"\n [attr.aria-label]=\"L().toggleDetail\"\n (click)=\"toggleExpand(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n </td>\n }\n @if (hasSelectionColumn()) {\n <td\n class=\"strct-dg__sel\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'single') {\n <!-- A native radio group: one name per grid, so the\n arrow keys move between rows and Space picks,\n without the grid inventing its own keyboarding. -->\n <input\n type=\"radio\"\n class=\"strct-dg__radio\"\n [name]=\"radioName\"\n [attr.aria-label]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [attr.tabindex]=\"ctlTabindex()\"\n (change)=\"selectSingle(row)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- click fires (and bubbles) before the input's change,\n so the modifier is recorded by the time toggleRow runs. -->\n <strct-checkbox\n [ariaLabel]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [tabIndex]=\"ctlTabindex()\"\n (click)=\"noteRangeIntent($event); $event.stopPropagation()\"\n (checkedChange)=\"toggleRow(row)\"\n />\n }\n </td>\n }\n @for (col of visibleColumns(); track col.key; let colIdx = $index) {\n <td\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [class.strct-dg__cell--mono]=\"col.mono\"\n [class.strct-dg__cell--muted]=\"col.muted\"\n [class.strct-dg__cell--numeric]=\"col.numeric\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [class.strct-dg__cell--editable]=\"col.editable\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-edit]=\"col.editable ? '' : null\"\n [attr.tabindex]=\"col.editable ? editCellTabindex(row, col) : null\"\n [attr.aria-describedby]=\"col.editable ? editHintId : null\"\n (dblclick)=\"col.editable && startEdit(row, col)\"\n (focus)=\"col.editable && onEditCellFocus(row, col)\"\n (keydown)=\"col.editable && onEditCellKeydown(row, col, $event)\"\n >\n @if (childrenKey() && colIdx === 0) {\n <span\n class=\"strct-dg__treepad\"\n [style.width.px]=\"treeDepth(row) * 18\"\n aria-hidden=\"true\"\n ></span>\n @if (treeHasChildren(row)) {\n <button\n type=\"button\"\n class=\"strct-dg__treebtn\"\n [class.strct-dg__treebtn--open]=\"treeExpanded(row)\"\n [attr.aria-expanded]=\"treeExpanded(row)\"\n [attr.aria-label]=\"L().toggleChildren\"\n (click)=\"toggleTreeRow(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n } @else {\n <span\n class=\"strct-dg__treebtn strct-dg__treebtn--leaf\"\n aria-hidden=\"true\"\n ></span>\n }\n }\n @if (isEditingCell(row, col)) {\n @switch (col.editor ?? 'text') {\n @case ('select') {\n <!-- Choosing is the commit: there is nothing to\n confirm with Enter once an option is picked. -->\n <strct-select\n class=\"strct-dg__editselect\"\n [options]=\"editorOptionsFor(row, col)\"\n [listLabel]=\"L().editCell + ': ' + col.label\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"commitValue(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @case ('number') {\n <strct-number\n class=\"strct-dg__editnumber\"\n [ariaLabel]=\"L().editCell + ': ' + col.label\"\n [min]=\"col.editorMin ?? null\"\n [max]=\"col.editorMax ?? null\"\n [step]=\"col.editorStep ?? 1\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"editDraft.set($event)\"\n (keydown.enter)=\"commitDraft(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (focusout)=\"commitOnLeave(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @default {\n <input\n class=\"strct-dg__editinput\"\n type=\"text\"\n [value]=\"row[col.key] ?? ''\"\n [attr.aria-label]=\"L().editCell + ': ' + col.label\"\n (keydown.enter)=\"commitEdit(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (blur)=\"commitEdit(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n }\n } @else if (cellTemplate(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: row,\n row,\n value: row[col.key],\n column: col,\n }\"\n />\n } @else if (isBlank(row, col)) {\n <span class=\"strct-dg__empty\" [attr.aria-label]=\"col.emptyLabel || null\">{{\n col.emptyText\n }}</span>\n } @else {\n {{ displayValue(row, col) }}\n @if (col.descriptionKey && row[col.descriptionKey]) {\n <span class=\"strct-dg__celldesc\">{{ row[col.descriptionKey] }}</span>\n }\n }\n <!-- The affordance a mouse never needed: a cell that\n can be edited says so on hover and on focus. -->\n @if (col.editable && editHint() && !isEditingCell(row, col)) {\n <strct-icon\n class=\"strct-dg__editpencil\"\n name=\"pencil\"\n [size]=\"12\"\n [strokeWidth]=\"1.6\"\n aria-hidden=\"true\"\n />\n }\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\">\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__kebab\"\n [attr.aria-label]=\"L().rowActions\"\n (click)=\"openRowMenu(row, $event)\"\n >\n <strct-icon name=\"dots\" [size]=\"16\" />\n </button>\n </td>\n }\n </tr>\n @if (canExpand() && isExpanded(row)) {\n <tr class=\"strct-dg__detailrow\">\n <td [attr.colspan]=\"colspan()\">\n <div class=\"strct-dg__detail\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, row }\"\n />\n </div>\n </td>\n </tr>\n }\n }\n } @empty {\n <!-- \"Nothing here\" and \"nothing matches what you typed\" are\n different states: the second one can be undone, and\n saying so is the whole point. -->\n <tr>\n <td class=\"strct-dg__emptyrow\" [attr.colspan]=\"colspan()\">\n @if (filteredAway()) {\n <strct-empty-state\n size=\"sm\"\n [titleLevel]=\"null\"\n [title]=\"L().noMatches\"\n [description]=\"noMatchesDescription()\"\n >\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"clearAllFilters()\">\n {{ L().clearFilters }}\n </button>\n </strct-empty-state>\n } @else {\n <!-- Not a heading: a grid is a region of a page, and an\n empty one used to put an <h3> in its outline. -->\n <strct-empty-state size=\"sm\" [titleLevel]=\"null\" [title]=\"emptyText()\" />\n }\n </td>\n </tr>\n }\n @if (virtual() && bottomPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"bottomPad()\"></td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n @if (paneOpen()) {\n <aside class=\"strct-dg__pane\">\n <div class=\"strct-dg__pane-head\">\n @if (narrowPane()) {\n <!-- The pane has taken the list's place, so there is a way back. -->\n <button\n type=\"button\"\n class=\"strct-dg__pane-back\"\n [attr.aria-label]=\"L().backToList\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"chevronLeft\" [size]=\"13\" />\n </button>\n }\n <span class=\"strct-dg__pane-title\">\n <ng-content select=\"[strctDetailPaneTitle]\" />\n @if (!paneTitleDef()) {\n {{ activeRow()![columns()[0].key] }}\n }\n </span>\n <span class=\"strct-dg__pane-meta\">\n <ng-content select=\"[strctDetailPaneMeta]\" />\n </span>\n <span class=\"strct-dg__pane-actions\">\n <ng-content select=\"[strctDetailPaneActions]\" />\n </span>\n <button\n type=\"button\"\n class=\"strct-dg__pane-close\"\n [attr.aria-label]=\"L().closeDetail\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n <div class=\"strct-dg__pane-body\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: activeRow(), row: activeRow() }\"\n />\n </div>\n </aside>\n }\n</div>\n\n@if (showFooter()) {\n <div class=\"strct-dg__foot\">\n <div class=\"strct-dg__foot-left\">\n @if (columnChooser() || sync()) {\n <div class=\"strct-dg__actions\">\n <strct-button-group>\n @if (columnChooser()) {\n <button\n strct-button\n size=\"sm\"\n [class.is-open]=\"chooserOpen()\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().chooseColumns\"\n (click)=\"toggleChooser($event)\"\n >\n <strct-icon name=\"settings\" [size]=\"16\" />\n </button>\n }\n @if (sync()) {\n <button\n strct-button\n size=\"sm\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().refresh\"\n (click)=\"syncChange.emit()\"\n >\n <strct-icon name=\"refresh\" [size]=\"16\" />\n </button>\n }\n </strct-button-group>\n @if (columnChooser() && chooserOpen()) {\n <div class=\"strct-dg__chooser-menu\">\n @for (col of columns(); track col.key) {\n <!-- The whole row toggles; the checkbox's own click must not\n reach the row or the column would flip twice. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"strct-dg__chooser-item\"\n (click)=\"toggleColumn(col.key, hiddenColumns().has(col.key))\"\n >\n <strct-checkbox\n [checked]=\"!hiddenColumns().has(col.key)\"\n (checkedChange)=\"toggleColumn(col.key, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n <span>{{ col.label }}</span>\n </div>\n }\n </div>\n }\n </div>\n }\n <span class=\"strct-dg__count\">\n @if (paging() === 'more') {\n {{ L().showing(rows().length, moreTotal() ?? undefined) }}\n } @else {\n {{ totalCount() }} {{ totalCount() === 1 ? L().row : L().rows }}\n }\n @if (selectedCount() && mode() !== 'single') {\n <span class=\"strct-dg__count-sep\">|</span>\n <span class=\"strct-dg__count-sel\">{{ selectedCount() }} {{ L().selected }}</span>\n }\n </span>\n </div>\n <div class=\"strct-dg__foot-right\">\n <!-- A feed that pages by cursor loads more at the end: page numbers\n are something a cursor API cannot answer. It takes the pager's\n place — the column chooser, the sync button and the count are\n the footer's, not the pager's. -->\n @if (paging() === 'more') {\n @if (hasMore()) {\n <button\n strct-button\n size=\"sm\"\n class=\"strct-dg__more\"\n [disabled]=\"loadingMore()\"\n (click)=\"loadMore.emit()\"\n >\n @if (loadingMore()) {\n <strct-spinner size=\"sm\" />\n }\n {{ L().loadMore }}\n </button>\n }\n } @else if (pageSize() > 0 && !groupBy()) {\n <strct-pagination\n [total]=\"totalCount()\"\n [pageSize]=\"pageSize()\"\n [page]=\"page()\"\n (pageChange)=\"goToPage($event)\"\n />\n }\n </div>\n </div>\n}\n",
26935
+ template: "<!-- One polite region for everything the grid does to itself: sorting,\n paging and filtering all change the rows without saying so. -->\n<span class=\"strct-dg__announcer\" role=\"status\" aria-live=\"polite\">{{ announcement() }}</span>\n@if (actionBarDef() || quickFilterable() || actionBarCaptionDef()) {\n <div class=\"strct-dg__toolbar\">\n @if (quickFilterable()) {\n <strct-searchbox\n class=\"strct-dg__quickfilter\"\n [class.strct-dg__quickfilter--end]=\"quickFilterAlign() === 'end'\"\n [placeholder]=\"L().quickFilter\"\n [ariaLabel]=\"L().quickFilter\"\n [value]=\"quickFilter()\"\n (valueChange)=\"quickFilter.set($event)\"\n />\n @if (filterNote(); as note) {\n <span\n class=\"strct-dg__filternote\"\n [class.strct-dg__filternote--end]=\"quickFilterAlign() === 'end'\"\n >{{ note }}</span\n >\n }\n }\n <span class=\"strct-dg__actioncaption\">\n <ng-content select=\"[strctDatagridActionBarCaption]\" />\n </span>\n <ng-content select=\"[strctDatagridActionBar]\" />\n </div>\n}\n\n@if (hasEditableColumn()) {\n <!-- What the editable cells point at with aria-describedby: a keyboard\n user is told the cell can be edited, and how. -->\n <!-- In flow, not absolutely positioned: an out-of-flow box whose\n containing block is the page extends the document's scrollable area\n from wherever the grid happens to sit. 1x1 and clipped is enough. -->\n <span\n class=\"strct-sr-only\"\n style=\"\n display: inline-block;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n \"\n [id]=\"editHintId\"\n >{{ L().editCellInstructions }}</span\n >\n}\n\n@if (bulkHint(); as hint) {\n <!-- Selecting the page is not selecting the set. Say which one happened,\n and offer the other. -->\n <div class=\"strct-dg__bulk\" role=\"status\">\n <span>{{ hint.text }}</span>\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"hint.run()\">\n {{ hint.action }}\n </button>\n </div>\n}\n<div\n class=\"strct-dg__layout\"\n [class.strct-dg__layout--paned]=\"paneOpen()\"\n [class.strct-dg__layout--panetakeover]=\"paneOpen() && narrowPane()\"\n [style.--strct-dg-pane-list-w]=\"paneListWidth()\"\n>\n <div\n class=\"strct-dg__scroll\"\n tabindex=\"0\"\n role=\"region\"\n [attr.aria-label]=\"regionName(L().rows)\"\n [style.max-height.px]=\"virtual() ? viewportHeight() : maxHeight()\"\n (scroll)=\"virtual() && onScroll($event)\"\n >\n <table\n class=\"strct-dg\"\n [attr.role]=\"childrenKey() ? 'treegrid' : gridMode() ? 'grid' : null\"\n [attr.aria-rowcount]=\"ariaRowCount()\"\n [attr.aria-labelledby]=\"caption() ? captionId : null\"\n >\n @if (caption()) {\n <caption class=\"strct-dg__caption\" [id]=\"captionId\">\n {{\n caption()\n }}\n </caption>\n }\n <thead>\n <tr>\n <!-- A utility column is still a column: axe reports every empty\n header, and a screen-reader user hears nothing where the\n detail, expand and select controls are. The name is hidden,\n not absent. -->\n @if (canDetail()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <span class=\"strct-sr-only\">{{ L().detailColumn }}</span>\n </th>\n }\n @if (canExpand()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <span class=\"strct-sr-only\">{{ L().expandColumn }}</span>\n </th>\n }\n @if (hasSelectionColumn()) {\n <th\n class=\"strct-dg__sel\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'multiple') {\n <strct-checkbox\n [ariaLabel]=\"L().selectAll\"\n [checked]=\"allPageSelected()\"\n [indeterminate]=\"somePageSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n } @else {\n <span class=\"strct-sr-only\">{{ L().selectColumn }}</span>\n }\n </th>\n }\n @for (col of visibleColumns(); track col.key) {\n <th\n scope=\"col\"\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [style.width]=\"colWidth(col.key) ?? col.width ?? null\"\n [class.strct-dg__th--sortable]=\"col.sortable\"\n [class.strct-dg__th--drop]=\"col.key === dropKey()\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-col]=\"col.key\"\n [attr.aria-sort]=\"col.sortable ? ariaSort(col.key) : null\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (dragstart)=\"onColDragStart(col.key, $event)\"\n (dragover)=\"onColDragOver(col.key, $event)\"\n (drop)=\"onColDrop(col.key)\"\n (dragend)=\"onColDragEnd()\"\n >\n <span class=\"strct-dg__hd\">\n <!-- A header that sorts is a button: a th with tabindex is\n read as \"column header, focusable\" and never says it\n can be activated. aria-sort stays on the th, which is\n what carries the column's sorted state. -->\n @if (col.sortable) {\n <!-- The button covers most of the header, so it carries\n the drag source too: a reorderable column has to be\n grabbable where the pointer actually lands. The\n event bubbles to the th's own handlers. -->\n <button\n type=\"button\"\n class=\"strct-dg__sortbtn\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (click)=\"sortBy(col.key)\"\n >\n {{ col.label }}\n <strct-icon class=\"strct-dg__sorticon\" [name]=\"sortIcon(col.key)\" [size]=\"13\" />\n </button>\n } @else {\n {{ col.label }}\n }\n @if (col.filterable || col.filterOptions) {\n <button\n #fbtn\n type=\"button\"\n class=\"strct-dg__filterbtn\"\n [class.strct-dg__filterbtn--active]=\"hasFilter(col.key)\"\n [attr.aria-expanded]=\"filterOpenKey() === col.key\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n (click)=\"toggleFilterPanel(col.key, $event)\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n <strct-icon name=\"filter\" [size]=\"12\" />\n </button>\n @if (filterOpenKey() === col.key) {\n <div\n class=\"strct-dg__filterpanel\"\n role=\"dialog\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [strctOverlay]=\"fbtn\"\n strctOverlayPlacement=\"bottom-start\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n @if (col.filterOptions; as opts) {\n @for (opt of opts; track $index) {\n <label class=\"strct-dg__filteropt\">\n <input\n type=\"checkbox\"\n [checked]=\"optionChecked(col.key, opt)\"\n (change)=\"toggleFilterOption(col.key, opt)\"\n />\n <span>{{ opt }}</span>\n </label>\n }\n } @else {\n <input\n class=\"strct-dg__filterinput\"\n type=\"text\"\n [attr.placeholder]=\"L().filterPlaceholder\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [value]=\"textFilter(col.key)\"\n (input)=\"setTextFilter(col.key, $any($event.target).value)\"\n />\n }\n <button\n type=\"button\"\n class=\"strct-dg__filterclear\"\n [disabled]=\"!hasFilter(col.key)\"\n (click)=\"clearFilter(col.key)\"\n >\n {{ L().clearFilter }}\n </button>\n </div>\n }\n }\n </span>\n @if (resizable() && col.resizable !== false) {\n <!-- A separator, not a bare div: a column could only be\n resized with a mouse, which is the one input a\n console cannot assume. -->\n <div\n class=\"strct-dg__resize\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"L().resizeColumn(col.label)\"\n [attr.aria-valuenow]=\"separatorValue(col.key)\"\n [attr.aria-valuemin]=\"40\"\n (mousedown)=\"onResizeStart($event, col.key)\"\n (keydown)=\"onResizeKey($event, col.key)\"\n (dblclick)=\"resetWidth(col.key)\"\n ></div>\n }\n </th>\n }\n @if (canActions()) {\n <th class=\"strct-dg__actioncol\" scope=\"col\" [attr.aria-label]=\"L().actions\"></th>\n }\n </tr>\n </thead>\n <!--\n focusin is the real keyboard path (focus does not bubble from cells);\n the plain focus binding exists to satisfy mouse-events-have-key-events.\n -->\n <tbody\n (mouseover)=\"revealIfClipped($event)\"\n (focusin)=\"revealIfClipped($event)\"\n (focus)=\"revealIfClipped($event)\"\n >\n @if (loading()) {\n @for (_ of [1, 2, 3, 4, 5]; track $index) {\n <tr class=\"strct-dg__skeleton-row\">\n @if (canDetail()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (canExpand()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (hasSelectionColumn()) {\n <td class=\"strct-dg__sel\"></td>\n }\n @for (col of visibleColumns(); track col.key) {\n <td [style.text-align]=\"col.align ?? 'start'\">\n <div class=\"strct-dg__skeleton-block\"></div>\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\"></td>\n }\n </tr>\n }\n } @else {\n @if (virtual() && topPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"topPad()\"></td>\n </tr>\n }\n @for (it of displayItems(); track itemKey(it)) {\n @if (it.group; as grp) {\n <!-- Group header: distinct value + count, collapsible. -->\n <tr class=\"strct-dg__grouprow\">\n @if (groupSelect() && mode() === 'multiple') {\n <td class=\"strct-dg__sel\">\n <strct-checkbox\n [ariaLabel]=\"L().selectAllInGroup(grp.label)\"\n [checked]=\"groupAllSelected(grp.key)\"\n [indeterminate]=\"groupSomeSelected(grp.key)\"\n (checkedChange)=\"toggleGroupSelection(grp.key)\"\n />\n </td>\n }\n <td\n [attr.colspan]=\"\n groupSelect() && mode() === 'multiple' ? colspan() - 1 : colspan()\n \"\n >\n <button\n type=\"button\"\n class=\"strct-dg__groupbtn\"\n [attr.aria-expanded]=\"!grp.collapsed\"\n (click)=\"toggleGroup(grp.key)\"\n >\n <span\n class=\"strct-dg__groupchev\"\n [class.strct-dg__groupchev--open]=\"!grp.collapsed\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </span>\n @if (groupHeaderDef(); as tpl) {\n <!-- The consumer draws the row; the button, its\n aria-expanded and the keyboard stay ours. -->\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: grp,\n group: grp,\n count: grp.count,\n }\"\n />\n } @else {\n <span\n class=\"strct-dg__grouplabel strct-dg__grouplabel--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.label }}</span\n >\n <!-- The three spans run together in the button's\n name (\"Needs your decision1Blocks the join…\");\n the separators are read, not seen. -->\n <span class=\"strct-dg__announcer\">, </span>\n <span\n class=\"strct-dg__groupcount strct-dg__groupcount--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.count }}</span\n >\n <!-- Seen: the chip's number. Heard: the number with\n the grid's noun, because \"Inconclusive, 3\" does\n not say three of what. -->\n <span class=\"strct-dg__announcer\"> {{ countNoun(grp.count) }}</span>\n @if (grp.note) {\n <span class=\"strct-dg__announcer\">. </span>\n <span class=\"strct-dg__groupnote\">{{ grp.note }}</span>\n }\n }\n </button>\n </td>\n </tr>\n } @else {\n @let row = it.row!;\n <tr\n [attr.aria-rowindex]=\"ariaRowIndex(row)\"\n [class.strct-dg__row--selected]=\"isSelected(row)\"\n [class.strct-dg__row--locked]=\"lockedReason(row) !== null\"\n [attr.aria-selected]=\"mode() === 'none' ? null : isSelected(row)\"\n [attr.aria-disabled]=\"lockedReason(row) !== null ? 'true' : null\"\n [attr.title]=\"lockedReason(row) || null\"\n (click)=\"onRowPick(row)\"\n [class.strct-dg__row--active]=\"paneOpen() && row === activeRow()\"\n [attr.aria-level]=\"childrenKey() ? treeDepth(row) + 1 : null\"\n [attr.aria-expanded]=\"\n childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null\n \"\n [attr.tabindex]=\"\n childrenKey()\n ? treeRowTabindex(row)\n : gridMode()\n ? gridRowTabindex(row)\n : canDetail()\n ? paneRowTabindex(row)\n : null\n \"\n (keydown)=\"\n childrenKey()\n ? onTreeRowKeydown(row, $event)\n : gridMode()\n ? onGridRowKeydown(row, $event)\n : onPaneRowKeydown(row, $event)\n \"\n (dblclick)=\"onRowActivate(row)\"\n >\n @if (canDetail()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__detailbtn\"\n [class.strct-dg__detailbtn--active]=\"row === activeRow()\"\n [attr.aria-expanded]=\"row === activeRow()\"\n [attr.aria-label]=\"L().openDetail\"\n (click)=\"openDetail(row)\"\n >\n <strct-icon name=\"chevronDoubleRight\" [size]=\"13\" [strokeWidth]=\"1.6\" />\n </button>\n </td>\n }\n @if (canExpand()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__expandbtn\"\n [class.strct-dg__expandbtn--open]=\"isExpanded(row)\"\n [attr.aria-expanded]=\"isExpanded(row)\"\n [attr.aria-label]=\"L().toggleDetail\"\n (click)=\"toggleExpand(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n </td>\n }\n @if (hasSelectionColumn()) {\n <td\n class=\"strct-dg__sel\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'single') {\n <!-- A native radio group: one name per grid, so the\n arrow keys move between rows and Space picks,\n without the grid inventing its own keyboarding. -->\n <input\n type=\"radio\"\n class=\"strct-dg__radio\"\n [name]=\"radioName\"\n [attr.aria-label]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [attr.tabindex]=\"ctlTabindex()\"\n (change)=\"selectSingle(row)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- click fires (and bubbles) before the input's change,\n so the modifier is recorded by the time toggleRow runs. -->\n <strct-checkbox\n [ariaLabel]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [tabIndex]=\"ctlTabindex()\"\n (click)=\"noteRangeIntent($event); $event.stopPropagation()\"\n (checkedChange)=\"toggleRow(row)\"\n />\n }\n </td>\n }\n @for (col of visibleColumns(); track col.key; let colIdx = $index) {\n <td\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [class.strct-dg__cell--mono]=\"col.mono\"\n [class.strct-dg__cell--muted]=\"col.muted\"\n [class.strct-dg__cell--numeric]=\"col.numeric\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [class.strct-dg__cell--editable]=\"col.editable\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-edit]=\"col.editable ? '' : null\"\n [attr.tabindex]=\"col.editable ? editCellTabindex(row, col) : null\"\n [attr.aria-describedby]=\"col.editable ? editHintId : null\"\n (dblclick)=\"col.editable && startEdit(row, col)\"\n (focus)=\"col.editable && onEditCellFocus(row, col)\"\n (keydown)=\"col.editable && onEditCellKeydown(row, col, $event)\"\n >\n @if (childrenKey() && colIdx === 0) {\n <span\n class=\"strct-dg__treepad\"\n [style.width.px]=\"treeDepth(row) * 18\"\n aria-hidden=\"true\"\n ></span>\n @if (treeHasChildren(row)) {\n <button\n type=\"button\"\n class=\"strct-dg__treebtn\"\n [class.strct-dg__treebtn--open]=\"treeExpanded(row)\"\n [attr.aria-expanded]=\"treeExpanded(row)\"\n [attr.aria-label]=\"L().toggleChildren\"\n (click)=\"toggleTreeRow(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n } @else {\n <span\n class=\"strct-dg__treebtn strct-dg__treebtn--leaf\"\n aria-hidden=\"true\"\n ></span>\n }\n }\n @if (isEditingCell(row, col)) {\n @switch (col.editor ?? 'text') {\n @case ('select') {\n <!-- Choosing is the commit: there is nothing to\n confirm with Enter once an option is picked. -->\n <strct-select\n class=\"strct-dg__editselect\"\n [options]=\"editorOptionsFor(row, col)\"\n [listLabel]=\"L().editCell + ': ' + col.label\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"commitValue(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @case ('number') {\n <strct-number\n class=\"strct-dg__editnumber\"\n [ariaLabel]=\"L().editCell + ': ' + col.label\"\n [min]=\"col.editorMin ?? null\"\n [max]=\"col.editorMax ?? null\"\n [step]=\"col.editorStep ?? 1\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"editDraft.set($event)\"\n (keydown.enter)=\"commitDraft(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (focusout)=\"commitOnLeave(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @default {\n <input\n class=\"strct-dg__editinput\"\n type=\"text\"\n [value]=\"row[col.key] ?? ''\"\n [attr.aria-label]=\"L().editCell + ': ' + col.label\"\n (keydown.enter)=\"commitEdit(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (blur)=\"commitEdit(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n }\n } @else if (cellTemplate(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: row,\n row,\n value: row[col.key],\n column: col,\n }\"\n />\n } @else if (isBlank(row, col)) {\n <span class=\"strct-dg__empty\" [attr.aria-label]=\"col.emptyLabel || null\">{{\n col.emptyText\n }}</span>\n } @else {\n {{ displayValue(row, col) }}\n @if (col.descriptionKey && row[col.descriptionKey]) {\n <span class=\"strct-dg__celldesc\">{{ row[col.descriptionKey] }}</span>\n }\n }\n <!-- The affordance a mouse never needed: a cell that\n can be edited says so on hover and on focus. -->\n @if (col.editable && editHint() && !isEditingCell(row, col)) {\n <strct-icon\n class=\"strct-dg__editpencil\"\n name=\"pencil\"\n [size]=\"12\"\n [strokeWidth]=\"1.6\"\n aria-hidden=\"true\"\n />\n }\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\">\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__kebab\"\n [attr.aria-label]=\"L().rowActions\"\n (click)=\"openRowMenu(row, $event)\"\n >\n <strct-icon name=\"dots\" [size]=\"16\" />\n </button>\n </td>\n }\n </tr>\n @if (canExpand() && isExpanded(row)) {\n <tr class=\"strct-dg__detailrow\">\n <td [attr.colspan]=\"colspan()\">\n <div class=\"strct-dg__detail\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, row }\"\n />\n </div>\n </td>\n </tr>\n }\n }\n } @empty {\n <!-- \"Nothing here\" and \"nothing matches what you typed\" are\n different states: the second one can be undone, and\n saying so is the whole point. -->\n <tr>\n <td class=\"strct-dg__emptyrow\" [attr.colspan]=\"colspan()\">\n @if (filteredAway()) {\n <strct-empty-state\n size=\"sm\"\n [titleLevel]=\"null\"\n [title]=\"L().noMatches\"\n [description]=\"noMatchesDescription()\"\n >\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"clearAllFilters()\">\n {{ L().clearFilters }}\n </button>\n </strct-empty-state>\n } @else {\n <!-- Not a heading: a grid is a region of a page, and an\n empty one used to put an <h3> in its outline. -->\n <strct-empty-state size=\"sm\" [titleLevel]=\"null\" [title]=\"emptyText()\" />\n }\n </td>\n </tr>\n }\n @if (virtual() && bottomPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"bottomPad()\"></td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n @if (paneOpen()) {\n <aside class=\"strct-dg__pane\">\n <div class=\"strct-dg__pane-head\">\n @if (narrowPane()) {\n <!-- The pane has taken the list's place, so there is a way back. -->\n <button\n type=\"button\"\n class=\"strct-dg__pane-back\"\n [attr.aria-label]=\"L().backToList\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"chevronLeft\" [size]=\"13\" />\n </button>\n }\n <span class=\"strct-dg__pane-title\">\n <ng-content select=\"[strctDetailPaneTitle]\" />\n @if (!paneTitleDef()) {\n {{ activeRow()![columns()[0].key] }}\n }\n </span>\n <span class=\"strct-dg__pane-meta\">\n <ng-content select=\"[strctDetailPaneMeta]\" />\n </span>\n <span class=\"strct-dg__pane-actions\">\n <ng-content select=\"[strctDetailPaneActions]\" />\n </span>\n <button\n type=\"button\"\n class=\"strct-dg__pane-close\"\n [attr.aria-label]=\"L().closeDetail\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n <div class=\"strct-dg__pane-body\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: activeRow(), row: activeRow() }\"\n />\n </div>\n </aside>\n }\n</div>\n\n@if (showFooter()) {\n <div class=\"strct-dg__foot\">\n <div class=\"strct-dg__foot-left\">\n @if (columnChooser() || sync()) {\n <div class=\"strct-dg__actions\">\n <strct-button-group>\n @if (columnChooser()) {\n <button\n strct-button\n size=\"sm\"\n [class.is-open]=\"chooserOpen()\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().chooseColumns\"\n (click)=\"toggleChooser($event)\"\n >\n <strct-icon name=\"settings\" [size]=\"16\" />\n </button>\n }\n @if (sync()) {\n <button\n strct-button\n size=\"sm\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().refresh\"\n (click)=\"syncChange.emit()\"\n >\n <strct-icon name=\"refresh\" [size]=\"16\" />\n </button>\n }\n </strct-button-group>\n @if (columnChooser() && chooserOpen()) {\n <div class=\"strct-dg__chooser-menu\">\n @for (col of columns(); track col.key) {\n <!-- The whole row toggles; the checkbox's own click must not\n reach the row or the column would flip twice. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"strct-dg__chooser-item\"\n (click)=\"toggleColumn(col.key, hiddenColumns().has(col.key))\"\n >\n <strct-checkbox\n [checked]=\"!hiddenColumns().has(col.key)\"\n (checkedChange)=\"toggleColumn(col.key, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n <span>{{ col.label }}</span>\n </div>\n }\n </div>\n }\n </div>\n }\n <span class=\"strct-dg__count\">\n @if (paging() === 'more') {\n {{ L().showing(rows().length, moreTotal() ?? undefined) }}\n } @else {\n {{ totalCount() }} {{ totalCount() === 1 ? L().row : L().rows }}\n }\n @if (selectedCount() && mode() !== 'single') {\n <span class=\"strct-dg__count-sep\">|</span>\n <span class=\"strct-dg__count-sel\">{{ selectedCount() }} {{ L().selected }}</span>\n }\n </span>\n </div>\n <div class=\"strct-dg__foot-right\">\n <!-- A feed that pages by cursor loads more at the end: page numbers\n are something a cursor API cannot answer. It takes the pager's\n place — the column chooser, the sync button and the count are\n the footer's, not the pager's. -->\n @if (paging() === 'more') {\n @if (hasMore()) {\n <button\n strct-button\n size=\"sm\"\n class=\"strct-dg__more\"\n [disabled]=\"loadingMore()\"\n (click)=\"loadMore.emit()\"\n >\n @if (loadingMore()) {\n <strct-spinner size=\"sm\" />\n }\n {{ L().loadMore }}\n </button>\n }\n } @else if (pageSize() > 0 && !groupBy()) {\n <strct-pagination\n [total]=\"totalCount()\"\n [pageSize]=\"pageSize()\"\n [page]=\"page()\"\n [regionLabel]=\"regionName(L().pagination)\"\n (pageChange)=\"goToPage($event)\"\n />\n }\n </div>\n </div>\n}\n",
26894
26936
  styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t2)}.strct-dg__bulk{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:4px 10px;padding:7px 12px;border-bottom:1px solid var(--b2);background:var(--acc-s);color:var(--t2);font-size:var(--text-md)}.strct-dg__announcer{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg-host--flush{border:0;border-radius:0;background:transparent;box-shadow:none}.strct-dg__more strct-spinner{margin-inline-end:4px}.strct-dg__caption{caption-side:top;text-align:start;padding:var(--space-2) var(--space-3);font-size:var(--text-sm);font-weight:600;color:var(--t2)}.strct-dg td.strct-dg__cell--mono{font-family:var(--mono);font-size:var(--text-sm)}.strct-dg td.strct-dg__cell--muted{color:var(--t2)}.strct-dg td.strct-dg__cell--numeric{font-variant-numeric:tabular-nums}.strct-dg__empty{color:var(--t2)}.strct-dg__celldesc{display:block;font-size:var(--text-sm);color:var(--t2);line-height:1.3}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll,.strct-dg-host--bounded .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th,.strct-dg-host--bounded .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.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:var(--text-sm);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__grouplabel--critical{color:var(--critical)}.strct-dg__grouplabel--warning{color:var(--warning)}.strct-dg__grouplabel--success{color:var(--success)}.strct-dg__grouplabel--accent{color:var(--acc)}.strct-dg__grouplabel--neutral,.strct-dg__grouplabel--none{color:var(--t1)}.strct-dg__groupnote{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);font-weight:400;color:var(--t2)}.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:4px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg__groupcount--critical{background:var(--critical-bg);color:var(--critical)}.strct-dg__groupcount--warning{background:var(--warning-bg);color:var(--warning)}.strct-dg__groupcount--success{background:var(--success-bg);color:var(--success)}.strct-dg__groupcount--neutral,.strct-dg__groupcount--none{background:var(--bg-3);color:var(--t2)}.strct-dg-host--singleline .strct-dg{border-collapse:separate;border-spacing:0}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg,.strct-dg-host--bounded .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:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.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:4px;border-start-end-radius:4px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:4px;border-end-end-radius:4px}.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__sortbtn{display:inline-flex;align-items:center;gap:inherit;padding:0;border:0;background:none;color:inherit;font:inherit;text-transform:inherit;letter-spacing:inherit;text-align:inherit;cursor:pointer}.strct-dg__sortbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:2px;border-radius:var(--radius-sm)}.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:calc(var(--z-base) + 1)}.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:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg tbody .strct-dg__row--selected td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody .strct-dg__row--selected td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg tbody tr.strct-dg__row--selected:hover td{background:linear-gradient(var(--acc-s),var(--acc-s)),linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody tr.strct-dg__row--selected:hover td{background:linear-gradient(var(--acc-s),var(--acc-s)),linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel .strct-cb,.strct-dg__sel .strct-dg__radio,.strct-dg__expandbtn{position:relative}.strct-dg__sel .strct-cb:after,.strct-dg__sel .strct-dg__radio:after,.strct-dg__expandbtn:after{content:\"\";position:absolute;inset:-11px -13px;min-width:24px;min-height:24px}.strct-dg-host--compact .strct-dg__sel .strct-cb:after,.strct-dg-host--compact .strct-dg__sel .strct-dg__radio:after,.strct-dg-host--compact .strct-dg__expandbtn:after{inset:-7px -11px}.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:2px;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{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:3px;border:0;border-radius:2px;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:2px;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:var(--strct-dg-pane-list-w, 260px);min-width:var(--strct-dg-pane-list-w, 260px);max-width:var(--strct-dg-pane-list-w, 260px)}.strct-dg__layout--paned>.strct-dg__scroll>.strct-dg{width:100%;min-width:0;max-width:none;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__layout--panetakeover>.strct-dg__scroll{display:none}.strct-dg__layout--panetakeover>.strct-dg__pane{flex:1;min-width:0}.strct-dg__pane-meta,.strct-dg__pane-actions{display:inline-flex;align-items:center;gap:var(--space-2)}.strct-dg__pane-meta:empty,.strct-dg__pane-actions:empty{display:none}.strct-dg__pane-actions{margin-inline-start:auto}.strct-dg__pane-back{display:inline-flex;align-items:center;border:0;background:none;color:var(--t2);cursor:pointer;padding:2px;border-radius:var(--radius-sm)}.strct-dg__row--clickable{cursor:pointer}.strct-dg tbody .strct-dg__row--active td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody .strct-dg__row--active td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.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;inset-inline-end: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:4px;border-start-start-radius:0;border-end-start-radius: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:2px;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;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t2);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:var(--text-sm);color:var(--t2);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:4px;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:3px;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:2px;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:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:3px;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:var(--text-sm);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 tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.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:2px;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;position:relative}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__cell--editable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-dg__editpencil{position:absolute;top:50%;inset-inline-end:6px;transform:translateY(-50%);color:var(--t3);opacity:0;pointer-events:none}td:hover>.strct-dg__editpencil,td:focus-within>.strct-dg__editpencil{opacity:1}.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__editselect,.strct-dg__editnumber{display:block;width:100%}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"],
26895
26937
  dependencies: [
26896
26938
  {
@@ -27117,7 +27159,7 @@ i0.ɵɵngDeclareClassMetadata({
27117
27159
  "[class.strct-dg-host--sticky]": "stickyActive()",
27118
27160
  "[class.strct-dg-host--flush]": "flush()"
27119
27161
  },
27120
- template: "<!-- One polite region for everything the grid does to itself: sorting,\n paging and filtering all change the rows without saying so. -->\n<span class=\"strct-dg__announcer\" role=\"status\" aria-live=\"polite\">{{ announcement() }}</span>\n@if (actionBarDef() || quickFilterable() || actionBarCaptionDef()) {\n <div class=\"strct-dg__toolbar\">\n @if (quickFilterable()) {\n <strct-searchbox\n class=\"strct-dg__quickfilter\"\n [class.strct-dg__quickfilter--end]=\"quickFilterAlign() === 'end'\"\n [placeholder]=\"L().quickFilter\"\n [ariaLabel]=\"L().quickFilter\"\n [value]=\"quickFilter()\"\n (valueChange)=\"quickFilter.set($event)\"\n />\n @if (filterNote(); as note) {\n <span\n class=\"strct-dg__filternote\"\n [class.strct-dg__filternote--end]=\"quickFilterAlign() === 'end'\"\n >{{ note }}</span\n >\n }\n }\n <span class=\"strct-dg__actioncaption\">\n <ng-content select=\"[strctDatagridActionBarCaption]\" />\n </span>\n <ng-content select=\"[strctDatagridActionBar]\" />\n </div>\n}\n\n@if (hasEditableColumn()) {\n <!-- What the editable cells point at with aria-describedby: a keyboard\n user is told the cell can be edited, and how. -->\n <!-- In flow, not absolutely positioned: an out-of-flow box whose\n containing block is the page extends the document's scrollable area\n from wherever the grid happens to sit. 1x1 and clipped is enough. -->\n <span\n class=\"strct-sr-only\"\n style=\"\n display: inline-block;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n \"\n [id]=\"editHintId\"\n >{{ L().editCellInstructions }}</span\n >\n}\n\n@if (bulkHint(); as hint) {\n <!-- Selecting the page is not selecting the set. Say which one happened,\n and offer the other. -->\n <div class=\"strct-dg__bulk\" role=\"status\">\n <span>{{ hint.text }}</span>\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"hint.run()\">\n {{ hint.action }}\n </button>\n </div>\n}\n<div\n class=\"strct-dg__layout\"\n [class.strct-dg__layout--paned]=\"paneOpen()\"\n [class.strct-dg__layout--panetakeover]=\"paneOpen() && narrowPane()\"\n [style.--strct-dg-pane-list-w]=\"paneListWidth()\"\n>\n <div\n class=\"strct-dg__scroll\"\n tabindex=\"0\"\n role=\"region\"\n [attr.aria-label]=\"L().rows\"\n [style.max-height.px]=\"virtual() ? viewportHeight() : maxHeight()\"\n (scroll)=\"virtual() && onScroll($event)\"\n >\n <table\n class=\"strct-dg\"\n [attr.role]=\"childrenKey() ? 'treegrid' : gridMode() ? 'grid' : null\"\n [attr.aria-rowcount]=\"ariaRowCount()\"\n [attr.aria-labelledby]=\"caption() ? captionId : null\"\n >\n @if (caption()) {\n <caption class=\"strct-dg__caption\" [id]=\"captionId\">\n {{\n caption()\n }}\n </caption>\n }\n <thead>\n <tr>\n <!-- A utility column is still a column: axe reports every empty\n header, and a screen-reader user hears nothing where the\n detail, expand and select controls are. The name is hidden,\n not absent. -->\n @if (canDetail()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <span class=\"strct-sr-only\">{{ L().detailColumn }}</span>\n </th>\n }\n @if (canExpand()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <span class=\"strct-sr-only\">{{ L().expandColumn }}</span>\n </th>\n }\n @if (hasSelectionColumn()) {\n <th\n class=\"strct-dg__sel\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'multiple') {\n <strct-checkbox\n [ariaLabel]=\"L().selectAll\"\n [checked]=\"allPageSelected()\"\n [indeterminate]=\"somePageSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n } @else {\n <span class=\"strct-sr-only\">{{ L().selectColumn }}</span>\n }\n </th>\n }\n @for (col of visibleColumns(); track col.key) {\n <th\n scope=\"col\"\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [style.width]=\"colWidth(col.key) ?? col.width ?? null\"\n [class.strct-dg__th--sortable]=\"col.sortable\"\n [class.strct-dg__th--drop]=\"col.key === dropKey()\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-col]=\"col.key\"\n [attr.aria-sort]=\"col.sortable ? ariaSort(col.key) : null\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (dragstart)=\"onColDragStart(col.key, $event)\"\n (dragover)=\"onColDragOver(col.key, $event)\"\n (drop)=\"onColDrop(col.key)\"\n (dragend)=\"onColDragEnd()\"\n >\n <span class=\"strct-dg__hd\">\n <!-- A header that sorts is a button: a th with tabindex is\n read as \"column header, focusable\" and never says it\n can be activated. aria-sort stays on the th, which is\n what carries the column's sorted state. -->\n @if (col.sortable) {\n <!-- The button covers most of the header, so it carries\n the drag source too: a reorderable column has to be\n grabbable where the pointer actually lands. The\n event bubbles to the th's own handlers. -->\n <button\n type=\"button\"\n class=\"strct-dg__sortbtn\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (click)=\"sortBy(col.key)\"\n >\n {{ col.label }}\n <strct-icon class=\"strct-dg__sorticon\" [name]=\"sortIcon(col.key)\" [size]=\"13\" />\n </button>\n } @else {\n {{ col.label }}\n }\n @if (col.filterable || col.filterOptions) {\n <button\n #fbtn\n type=\"button\"\n class=\"strct-dg__filterbtn\"\n [class.strct-dg__filterbtn--active]=\"hasFilter(col.key)\"\n [attr.aria-expanded]=\"filterOpenKey() === col.key\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n (click)=\"toggleFilterPanel(col.key, $event)\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n <strct-icon name=\"filter\" [size]=\"12\" />\n </button>\n @if (filterOpenKey() === col.key) {\n <div\n class=\"strct-dg__filterpanel\"\n role=\"dialog\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [strctOverlay]=\"fbtn\"\n strctOverlayPlacement=\"bottom-start\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n @if (col.filterOptions; as opts) {\n @for (opt of opts; track $index) {\n <label class=\"strct-dg__filteropt\">\n <input\n type=\"checkbox\"\n [checked]=\"optionChecked(col.key, opt)\"\n (change)=\"toggleFilterOption(col.key, opt)\"\n />\n <span>{{ opt }}</span>\n </label>\n }\n } @else {\n <input\n class=\"strct-dg__filterinput\"\n type=\"text\"\n [attr.placeholder]=\"L().filterPlaceholder\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [value]=\"textFilter(col.key)\"\n (input)=\"setTextFilter(col.key, $any($event.target).value)\"\n />\n }\n <button\n type=\"button\"\n class=\"strct-dg__filterclear\"\n [disabled]=\"!hasFilter(col.key)\"\n (click)=\"clearFilter(col.key)\"\n >\n {{ L().clearFilter }}\n </button>\n </div>\n }\n }\n </span>\n @if (resizable() && col.resizable !== false) {\n <!-- A separator, not a bare div: a column could only be\n resized with a mouse, which is the one input a\n console cannot assume. -->\n <div\n class=\"strct-dg__resize\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"L().resizeColumn(col.label)\"\n [attr.aria-valuenow]=\"separatorValue(col.key)\"\n [attr.aria-valuemin]=\"40\"\n (mousedown)=\"onResizeStart($event, col.key)\"\n (keydown)=\"onResizeKey($event, col.key)\"\n (dblclick)=\"resetWidth(col.key)\"\n ></div>\n }\n </th>\n }\n @if (canActions()) {\n <th class=\"strct-dg__actioncol\" scope=\"col\" [attr.aria-label]=\"L().actions\"></th>\n }\n </tr>\n </thead>\n <!--\n focusin is the real keyboard path (focus does not bubble from cells);\n the plain focus binding exists to satisfy mouse-events-have-key-events.\n -->\n <tbody\n (mouseover)=\"revealIfClipped($event)\"\n (focusin)=\"revealIfClipped($event)\"\n (focus)=\"revealIfClipped($event)\"\n >\n @if (loading()) {\n @for (_ of [1, 2, 3, 4, 5]; track $index) {\n <tr class=\"strct-dg__skeleton-row\">\n @if (canDetail()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (canExpand()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (hasSelectionColumn()) {\n <td class=\"strct-dg__sel\"></td>\n }\n @for (col of visibleColumns(); track col.key) {\n <td [style.text-align]=\"col.align ?? 'start'\">\n <div class=\"strct-dg__skeleton-block\"></div>\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\"></td>\n }\n </tr>\n }\n } @else {\n @if (virtual() && topPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"topPad()\"></td>\n </tr>\n }\n @for (it of displayItems(); track itemKey(it)) {\n @if (it.group; as grp) {\n <!-- Group header: distinct value + count, collapsible. -->\n <tr class=\"strct-dg__grouprow\">\n @if (groupSelect() && mode() === 'multiple') {\n <td class=\"strct-dg__sel\">\n <strct-checkbox\n [ariaLabel]=\"L().selectAllInGroup(grp.label)\"\n [checked]=\"groupAllSelected(grp.key)\"\n [indeterminate]=\"groupSomeSelected(grp.key)\"\n (checkedChange)=\"toggleGroupSelection(grp.key)\"\n />\n </td>\n }\n <td\n [attr.colspan]=\"\n groupSelect() && mode() === 'multiple' ? colspan() - 1 : colspan()\n \"\n >\n <button\n type=\"button\"\n class=\"strct-dg__groupbtn\"\n [attr.aria-expanded]=\"!grp.collapsed\"\n (click)=\"toggleGroup(grp.key)\"\n >\n <span\n class=\"strct-dg__groupchev\"\n [class.strct-dg__groupchev--open]=\"!grp.collapsed\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </span>\n @if (groupHeaderDef(); as tpl) {\n <!-- The consumer draws the row; the button, its\n aria-expanded and the keyboard stay ours. -->\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: grp,\n group: grp,\n count: grp.count,\n }\"\n />\n } @else {\n <span\n class=\"strct-dg__grouplabel strct-dg__grouplabel--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.label }}</span\n >\n <!-- The three spans run together in the button's\n name (\"Needs your decision1Blocks the join…\");\n the separators are read, not seen. -->\n <span class=\"strct-dg__announcer\">, </span>\n <span\n class=\"strct-dg__groupcount strct-dg__groupcount--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.count }}</span\n >\n <!-- Seen: the chip's number. Heard: the number with\n the grid's noun, because \"Inconclusive, 3\" does\n not say three of what. -->\n <span class=\"strct-dg__announcer\"> {{ countNoun(grp.count) }}</span>\n @if (grp.note) {\n <span class=\"strct-dg__announcer\">. </span>\n <span class=\"strct-dg__groupnote\">{{ grp.note }}</span>\n }\n }\n </button>\n </td>\n </tr>\n } @else {\n @let row = it.row!;\n <tr\n [attr.aria-rowindex]=\"ariaRowIndex(row)\"\n [class.strct-dg__row--selected]=\"isSelected(row)\"\n [class.strct-dg__row--locked]=\"lockedReason(row) !== null\"\n [attr.aria-selected]=\"mode() === 'none' ? null : isSelected(row)\"\n [attr.aria-disabled]=\"lockedReason(row) !== null ? 'true' : null\"\n [attr.title]=\"lockedReason(row) || null\"\n (click)=\"onRowPick(row)\"\n [class.strct-dg__row--active]=\"paneOpen() && row === activeRow()\"\n [attr.aria-level]=\"childrenKey() ? treeDepth(row) + 1 : null\"\n [attr.aria-expanded]=\"\n childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null\n \"\n [attr.tabindex]=\"\n childrenKey()\n ? treeRowTabindex(row)\n : gridMode()\n ? gridRowTabindex(row)\n : canDetail()\n ? paneRowTabindex(row)\n : null\n \"\n (keydown)=\"\n childrenKey()\n ? onTreeRowKeydown(row, $event)\n : gridMode()\n ? onGridRowKeydown(row, $event)\n : onPaneRowKeydown(row, $event)\n \"\n (dblclick)=\"onRowActivate(row)\"\n >\n @if (canDetail()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__detailbtn\"\n [class.strct-dg__detailbtn--active]=\"row === activeRow()\"\n [attr.aria-expanded]=\"row === activeRow()\"\n [attr.aria-label]=\"L().openDetail\"\n (click)=\"openDetail(row)\"\n >\n <strct-icon name=\"chevronDoubleRight\" [size]=\"13\" [strokeWidth]=\"1.6\" />\n </button>\n </td>\n }\n @if (canExpand()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__expandbtn\"\n [class.strct-dg__expandbtn--open]=\"isExpanded(row)\"\n [attr.aria-expanded]=\"isExpanded(row)\"\n [attr.aria-label]=\"L().toggleDetail\"\n (click)=\"toggleExpand(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n </td>\n }\n @if (hasSelectionColumn()) {\n <td\n class=\"strct-dg__sel\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'single') {\n <!-- A native radio group: one name per grid, so the\n arrow keys move between rows and Space picks,\n without the grid inventing its own keyboarding. -->\n <input\n type=\"radio\"\n class=\"strct-dg__radio\"\n [name]=\"radioName\"\n [attr.aria-label]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [attr.tabindex]=\"ctlTabindex()\"\n (change)=\"selectSingle(row)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- click fires (and bubbles) before the input's change,\n so the modifier is recorded by the time toggleRow runs. -->\n <strct-checkbox\n [ariaLabel]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [tabIndex]=\"ctlTabindex()\"\n (click)=\"noteRangeIntent($event); $event.stopPropagation()\"\n (checkedChange)=\"toggleRow(row)\"\n />\n }\n </td>\n }\n @for (col of visibleColumns(); track col.key; let colIdx = $index) {\n <td\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [class.strct-dg__cell--mono]=\"col.mono\"\n [class.strct-dg__cell--muted]=\"col.muted\"\n [class.strct-dg__cell--numeric]=\"col.numeric\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [class.strct-dg__cell--editable]=\"col.editable\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-edit]=\"col.editable ? '' : null\"\n [attr.tabindex]=\"col.editable ? editCellTabindex(row, col) : null\"\n [attr.aria-describedby]=\"col.editable ? editHintId : null\"\n (dblclick)=\"col.editable && startEdit(row, col)\"\n (focus)=\"col.editable && onEditCellFocus(row, col)\"\n (keydown)=\"col.editable && onEditCellKeydown(row, col, $event)\"\n >\n @if (childrenKey() && colIdx === 0) {\n <span\n class=\"strct-dg__treepad\"\n [style.width.px]=\"treeDepth(row) * 18\"\n aria-hidden=\"true\"\n ></span>\n @if (treeHasChildren(row)) {\n <button\n type=\"button\"\n class=\"strct-dg__treebtn\"\n [class.strct-dg__treebtn--open]=\"treeExpanded(row)\"\n [attr.aria-expanded]=\"treeExpanded(row)\"\n [attr.aria-label]=\"L().toggleChildren\"\n (click)=\"toggleTreeRow(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n } @else {\n <span\n class=\"strct-dg__treebtn strct-dg__treebtn--leaf\"\n aria-hidden=\"true\"\n ></span>\n }\n }\n @if (isEditingCell(row, col)) {\n @switch (col.editor ?? 'text') {\n @case ('select') {\n <!-- Choosing is the commit: there is nothing to\n confirm with Enter once an option is picked. -->\n <strct-select\n class=\"strct-dg__editselect\"\n [options]=\"editorOptionsFor(row, col)\"\n [listLabel]=\"L().editCell + ': ' + col.label\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"commitValue(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @case ('number') {\n <strct-number\n class=\"strct-dg__editnumber\"\n [ariaLabel]=\"L().editCell + ': ' + col.label\"\n [min]=\"col.editorMin ?? null\"\n [max]=\"col.editorMax ?? null\"\n [step]=\"col.editorStep ?? 1\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"editDraft.set($event)\"\n (keydown.enter)=\"commitDraft(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (focusout)=\"commitOnLeave(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @default {\n <input\n class=\"strct-dg__editinput\"\n type=\"text\"\n [value]=\"row[col.key] ?? ''\"\n [attr.aria-label]=\"L().editCell + ': ' + col.label\"\n (keydown.enter)=\"commitEdit(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (blur)=\"commitEdit(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n }\n } @else if (cellTemplate(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: row,\n row,\n value: row[col.key],\n column: col,\n }\"\n />\n } @else if (isBlank(row, col)) {\n <span class=\"strct-dg__empty\" [attr.aria-label]=\"col.emptyLabel || null\">{{\n col.emptyText\n }}</span>\n } @else {\n {{ displayValue(row, col) }}\n @if (col.descriptionKey && row[col.descriptionKey]) {\n <span class=\"strct-dg__celldesc\">{{ row[col.descriptionKey] }}</span>\n }\n }\n <!-- The affordance a mouse never needed: a cell that\n can be edited says so on hover and on focus. -->\n @if (col.editable && editHint() && !isEditingCell(row, col)) {\n <strct-icon\n class=\"strct-dg__editpencil\"\n name=\"pencil\"\n [size]=\"12\"\n [strokeWidth]=\"1.6\"\n aria-hidden=\"true\"\n />\n }\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\">\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__kebab\"\n [attr.aria-label]=\"L().rowActions\"\n (click)=\"openRowMenu(row, $event)\"\n >\n <strct-icon name=\"dots\" [size]=\"16\" />\n </button>\n </td>\n }\n </tr>\n @if (canExpand() && isExpanded(row)) {\n <tr class=\"strct-dg__detailrow\">\n <td [attr.colspan]=\"colspan()\">\n <div class=\"strct-dg__detail\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, row }\"\n />\n </div>\n </td>\n </tr>\n }\n }\n } @empty {\n <!-- \"Nothing here\" and \"nothing matches what you typed\" are\n different states: the second one can be undone, and\n saying so is the whole point. -->\n <tr>\n <td class=\"strct-dg__emptyrow\" [attr.colspan]=\"colspan()\">\n @if (filteredAway()) {\n <strct-empty-state\n size=\"sm\"\n [titleLevel]=\"null\"\n [title]=\"L().noMatches\"\n [description]=\"noMatchesDescription()\"\n >\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"clearAllFilters()\">\n {{ L().clearFilters }}\n </button>\n </strct-empty-state>\n } @else {\n <!-- Not a heading: a grid is a region of a page, and an\n empty one used to put an <h3> in its outline. -->\n <strct-empty-state size=\"sm\" [titleLevel]=\"null\" [title]=\"emptyText()\" />\n }\n </td>\n </tr>\n }\n @if (virtual() && bottomPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"bottomPad()\"></td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n @if (paneOpen()) {\n <aside class=\"strct-dg__pane\">\n <div class=\"strct-dg__pane-head\">\n @if (narrowPane()) {\n <!-- The pane has taken the list's place, so there is a way back. -->\n <button\n type=\"button\"\n class=\"strct-dg__pane-back\"\n [attr.aria-label]=\"L().backToList\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"chevronLeft\" [size]=\"13\" />\n </button>\n }\n <span class=\"strct-dg__pane-title\">\n <ng-content select=\"[strctDetailPaneTitle]\" />\n @if (!paneTitleDef()) {\n {{ activeRow()![columns()[0].key] }}\n }\n </span>\n <span class=\"strct-dg__pane-meta\">\n <ng-content select=\"[strctDetailPaneMeta]\" />\n </span>\n <span class=\"strct-dg__pane-actions\">\n <ng-content select=\"[strctDetailPaneActions]\" />\n </span>\n <button\n type=\"button\"\n class=\"strct-dg__pane-close\"\n [attr.aria-label]=\"L().closeDetail\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n <div class=\"strct-dg__pane-body\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: activeRow(), row: activeRow() }\"\n />\n </div>\n </aside>\n }\n</div>\n\n@if (showFooter()) {\n <div class=\"strct-dg__foot\">\n <div class=\"strct-dg__foot-left\">\n @if (columnChooser() || sync()) {\n <div class=\"strct-dg__actions\">\n <strct-button-group>\n @if (columnChooser()) {\n <button\n strct-button\n size=\"sm\"\n [class.is-open]=\"chooserOpen()\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().chooseColumns\"\n (click)=\"toggleChooser($event)\"\n >\n <strct-icon name=\"settings\" [size]=\"16\" />\n </button>\n }\n @if (sync()) {\n <button\n strct-button\n size=\"sm\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().refresh\"\n (click)=\"syncChange.emit()\"\n >\n <strct-icon name=\"refresh\" [size]=\"16\" />\n </button>\n }\n </strct-button-group>\n @if (columnChooser() && chooserOpen()) {\n <div class=\"strct-dg__chooser-menu\">\n @for (col of columns(); track col.key) {\n <!-- The whole row toggles; the checkbox's own click must not\n reach the row or the column would flip twice. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"strct-dg__chooser-item\"\n (click)=\"toggleColumn(col.key, hiddenColumns().has(col.key))\"\n >\n <strct-checkbox\n [checked]=\"!hiddenColumns().has(col.key)\"\n (checkedChange)=\"toggleColumn(col.key, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n <span>{{ col.label }}</span>\n </div>\n }\n </div>\n }\n </div>\n }\n <span class=\"strct-dg__count\">\n @if (paging() === 'more') {\n {{ L().showing(rows().length, moreTotal() ?? undefined) }}\n } @else {\n {{ totalCount() }} {{ totalCount() === 1 ? L().row : L().rows }}\n }\n @if (selectedCount() && mode() !== 'single') {\n <span class=\"strct-dg__count-sep\">|</span>\n <span class=\"strct-dg__count-sel\">{{ selectedCount() }} {{ L().selected }}</span>\n }\n </span>\n </div>\n <div class=\"strct-dg__foot-right\">\n <!-- A feed that pages by cursor loads more at the end: page numbers\n are something a cursor API cannot answer. It takes the pager's\n place — the column chooser, the sync button and the count are\n the footer's, not the pager's. -->\n @if (paging() === 'more') {\n @if (hasMore()) {\n <button\n strct-button\n size=\"sm\"\n class=\"strct-dg__more\"\n [disabled]=\"loadingMore()\"\n (click)=\"loadMore.emit()\"\n >\n @if (loadingMore()) {\n <strct-spinner size=\"sm\" />\n }\n {{ L().loadMore }}\n </button>\n }\n } @else if (pageSize() > 0 && !groupBy()) {\n <strct-pagination\n [total]=\"totalCount()\"\n [pageSize]=\"pageSize()\"\n [page]=\"page()\"\n (pageChange)=\"goToPage($event)\"\n />\n }\n </div>\n </div>\n}\n",
27162
+ template: "<!-- One polite region for everything the grid does to itself: sorting,\n paging and filtering all change the rows without saying so. -->\n<span class=\"strct-dg__announcer\" role=\"status\" aria-live=\"polite\">{{ announcement() }}</span>\n@if (actionBarDef() || quickFilterable() || actionBarCaptionDef()) {\n <div class=\"strct-dg__toolbar\">\n @if (quickFilterable()) {\n <strct-searchbox\n class=\"strct-dg__quickfilter\"\n [class.strct-dg__quickfilter--end]=\"quickFilterAlign() === 'end'\"\n [placeholder]=\"L().quickFilter\"\n [ariaLabel]=\"L().quickFilter\"\n [value]=\"quickFilter()\"\n (valueChange)=\"quickFilter.set($event)\"\n />\n @if (filterNote(); as note) {\n <span\n class=\"strct-dg__filternote\"\n [class.strct-dg__filternote--end]=\"quickFilterAlign() === 'end'\"\n >{{ note }}</span\n >\n }\n }\n <span class=\"strct-dg__actioncaption\">\n <ng-content select=\"[strctDatagridActionBarCaption]\" />\n </span>\n <ng-content select=\"[strctDatagridActionBar]\" />\n </div>\n}\n\n@if (hasEditableColumn()) {\n <!-- What the editable cells point at with aria-describedby: a keyboard\n user is told the cell can be edited, and how. -->\n <!-- In flow, not absolutely positioned: an out-of-flow box whose\n containing block is the page extends the document's scrollable area\n from wherever the grid happens to sit. 1x1 and clipped is enough. -->\n <span\n class=\"strct-sr-only\"\n style=\"\n display: inline-block;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n \"\n [id]=\"editHintId\"\n >{{ L().editCellInstructions }}</span\n >\n}\n\n@if (bulkHint(); as hint) {\n <!-- Selecting the page is not selecting the set. Say which one happened,\n and offer the other. -->\n <div class=\"strct-dg__bulk\" role=\"status\">\n <span>{{ hint.text }}</span>\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"hint.run()\">\n {{ hint.action }}\n </button>\n </div>\n}\n<div\n class=\"strct-dg__layout\"\n [class.strct-dg__layout--paned]=\"paneOpen()\"\n [class.strct-dg__layout--panetakeover]=\"paneOpen() && narrowPane()\"\n [style.--strct-dg-pane-list-w]=\"paneListWidth()\"\n>\n <div\n class=\"strct-dg__scroll\"\n tabindex=\"0\"\n role=\"region\"\n [attr.aria-label]=\"regionName(L().rows)\"\n [style.max-height.px]=\"virtual() ? viewportHeight() : maxHeight()\"\n (scroll)=\"virtual() && onScroll($event)\"\n >\n <table\n class=\"strct-dg\"\n [attr.role]=\"childrenKey() ? 'treegrid' : gridMode() ? 'grid' : null\"\n [attr.aria-rowcount]=\"ariaRowCount()\"\n [attr.aria-labelledby]=\"caption() ? captionId : null\"\n >\n @if (caption()) {\n <caption class=\"strct-dg__caption\" [id]=\"captionId\">\n {{\n caption()\n }}\n </caption>\n }\n <thead>\n <tr>\n <!-- A utility column is still a column: axe reports every empty\n header, and a screen-reader user hears nothing where the\n detail, expand and select controls are. The name is hidden,\n not absent. -->\n @if (canDetail()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <span class=\"strct-sr-only\">{{ L().detailColumn }}</span>\n </th>\n }\n @if (canExpand()) {\n <th\n class=\"strct-dg__expandcol\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <span class=\"strct-sr-only\">{{ L().expandColumn }}</span>\n </th>\n }\n @if (hasSelectionColumn()) {\n <th\n class=\"strct-dg__sel\"\n scope=\"col\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'multiple') {\n <strct-checkbox\n [ariaLabel]=\"L().selectAll\"\n [checked]=\"allPageSelected()\"\n [indeterminate]=\"somePageSelected()\"\n (checkedChange)=\"toggleAll()\"\n />\n } @else {\n <span class=\"strct-sr-only\">{{ L().selectColumn }}</span>\n }\n </th>\n }\n @for (col of visibleColumns(); track col.key) {\n <th\n scope=\"col\"\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [style.width]=\"colWidth(col.key) ?? col.width ?? null\"\n [class.strct-dg__th--sortable]=\"col.sortable\"\n [class.strct-dg__th--drop]=\"col.key === dropKey()\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-col]=\"col.key\"\n [attr.aria-sort]=\"col.sortable ? ariaSort(col.key) : null\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (dragstart)=\"onColDragStart(col.key, $event)\"\n (dragover)=\"onColDragOver(col.key, $event)\"\n (drop)=\"onColDrop(col.key)\"\n (dragend)=\"onColDragEnd()\"\n >\n <span class=\"strct-dg__hd\">\n <!-- A header that sorts is a button: a th with tabindex is\n read as \"column header, focusable\" and never says it\n can be activated. aria-sort stays on the th, which is\n what carries the column's sorted state. -->\n @if (col.sortable) {\n <!-- The button covers most of the header, so it carries\n the drag source too: a reorderable column has to be\n grabbable where the pointer actually lands. The\n event bubbles to the th's own handlers. -->\n <button\n type=\"button\"\n class=\"strct-dg__sortbtn\"\n [attr.draggable]=\"reorderable() ? true : null\"\n (click)=\"sortBy(col.key)\"\n >\n {{ col.label }}\n <strct-icon class=\"strct-dg__sorticon\" [name]=\"sortIcon(col.key)\" [size]=\"13\" />\n </button>\n } @else {\n {{ col.label }}\n }\n @if (col.filterable || col.filterOptions) {\n <button\n #fbtn\n type=\"button\"\n class=\"strct-dg__filterbtn\"\n [class.strct-dg__filterbtn--active]=\"hasFilter(col.key)\"\n [attr.aria-expanded]=\"filterOpenKey() === col.key\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n (click)=\"toggleFilterPanel(col.key, $event)\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n <strct-icon name=\"filter\" [size]=\"12\" />\n </button>\n @if (filterOpenKey() === col.key) {\n <div\n class=\"strct-dg__filterpanel\"\n role=\"dialog\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [strctOverlay]=\"fbtn\"\n strctOverlayPlacement=\"bottom-start\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n >\n @if (col.filterOptions; as opts) {\n @for (opt of opts; track $index) {\n <label class=\"strct-dg__filteropt\">\n <input\n type=\"checkbox\"\n [checked]=\"optionChecked(col.key, opt)\"\n (change)=\"toggleFilterOption(col.key, opt)\"\n />\n <span>{{ opt }}</span>\n </label>\n }\n } @else {\n <input\n class=\"strct-dg__filterinput\"\n type=\"text\"\n [attr.placeholder]=\"L().filterPlaceholder\"\n [attr.aria-label]=\"L().filterColumn + ': ' + col.label\"\n [value]=\"textFilter(col.key)\"\n (input)=\"setTextFilter(col.key, $any($event.target).value)\"\n />\n }\n <button\n type=\"button\"\n class=\"strct-dg__filterclear\"\n [disabled]=\"!hasFilter(col.key)\"\n (click)=\"clearFilter(col.key)\"\n >\n {{ L().clearFilter }}\n </button>\n </div>\n }\n }\n </span>\n @if (resizable() && col.resizable !== false) {\n <!-- A separator, not a bare div: a column could only be\n resized with a mouse, which is the one input a\n console cannot assume. -->\n <div\n class=\"strct-dg__resize\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabindex=\"0\"\n [attr.aria-label]=\"L().resizeColumn(col.label)\"\n [attr.aria-valuenow]=\"separatorValue(col.key)\"\n [attr.aria-valuemin]=\"40\"\n (mousedown)=\"onResizeStart($event, col.key)\"\n (keydown)=\"onResizeKey($event, col.key)\"\n (dblclick)=\"resetWidth(col.key)\"\n ></div>\n }\n </th>\n }\n @if (canActions()) {\n <th class=\"strct-dg__actioncol\" scope=\"col\" [attr.aria-label]=\"L().actions\"></th>\n }\n </tr>\n </thead>\n <!--\n focusin is the real keyboard path (focus does not bubble from cells);\n the plain focus binding exists to satisfy mouse-events-have-key-events.\n -->\n <tbody\n (mouseover)=\"revealIfClipped($event)\"\n (focusin)=\"revealIfClipped($event)\"\n (focus)=\"revealIfClipped($event)\"\n >\n @if (loading()) {\n @for (_ of [1, 2, 3, 4, 5]; track $index) {\n <tr class=\"strct-dg__skeleton-row\">\n @if (canDetail()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (canExpand()) {\n <td class=\"strct-dg__expandcell\"></td>\n }\n @if (hasSelectionColumn()) {\n <td class=\"strct-dg__sel\"></td>\n }\n @for (col of visibleColumns(); track col.key) {\n <td [style.text-align]=\"col.align ?? 'start'\">\n <div class=\"strct-dg__skeleton-block\"></div>\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\"></td>\n }\n </tr>\n }\n } @else {\n @if (virtual() && topPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"topPad()\"></td>\n </tr>\n }\n @for (it of displayItems(); track itemKey(it)) {\n @if (it.group; as grp) {\n <!-- Group header: distinct value + count, collapsible. -->\n <tr class=\"strct-dg__grouprow\">\n @if (groupSelect() && mode() === 'multiple') {\n <td class=\"strct-dg__sel\">\n <strct-checkbox\n [ariaLabel]=\"L().selectAllInGroup(grp.label)\"\n [checked]=\"groupAllSelected(grp.key)\"\n [indeterminate]=\"groupSomeSelected(grp.key)\"\n (checkedChange)=\"toggleGroupSelection(grp.key)\"\n />\n </td>\n }\n <td\n [attr.colspan]=\"\n groupSelect() && mode() === 'multiple' ? colspan() - 1 : colspan()\n \"\n >\n <button\n type=\"button\"\n class=\"strct-dg__groupbtn\"\n [attr.aria-expanded]=\"!grp.collapsed\"\n (click)=\"toggleGroup(grp.key)\"\n >\n <span\n class=\"strct-dg__groupchev\"\n [class.strct-dg__groupchev--open]=\"!grp.collapsed\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </span>\n @if (groupHeaderDef(); as tpl) {\n <!-- The consumer draws the row; the button, its\n aria-expanded and the keyboard stay ours. -->\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: grp,\n group: grp,\n count: grp.count,\n }\"\n />\n } @else {\n <span\n class=\"strct-dg__grouplabel strct-dg__grouplabel--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.label }}</span\n >\n <!-- The three spans run together in the button's\n name (\"Needs your decision1Blocks the join…\");\n the separators are read, not seen. -->\n <span class=\"strct-dg__announcer\">, </span>\n <span\n class=\"strct-dg__groupcount strct-dg__groupcount--{{\n grp.status ?? 'none'\n }}\"\n >{{ grp.count }}</span\n >\n <!-- Seen: the chip's number. Heard: the number with\n the grid's noun, because \"Inconclusive, 3\" does\n not say three of what. -->\n <span class=\"strct-dg__announcer\"> {{ countNoun(grp.count) }}</span>\n @if (grp.note) {\n <span class=\"strct-dg__announcer\">. </span>\n <span class=\"strct-dg__groupnote\">{{ grp.note }}</span>\n }\n }\n </button>\n </td>\n </tr>\n } @else {\n @let row = it.row!;\n <tr\n [attr.aria-rowindex]=\"ariaRowIndex(row)\"\n [class.strct-dg__row--selected]=\"isSelected(row)\"\n [class.strct-dg__row--locked]=\"lockedReason(row) !== null\"\n [attr.aria-selected]=\"mode() === 'none' ? null : isSelected(row)\"\n [attr.aria-disabled]=\"lockedReason(row) !== null ? 'true' : null\"\n [attr.title]=\"lockedReason(row) || null\"\n (click)=\"onRowPick(row)\"\n [class.strct-dg__row--active]=\"paneOpen() && row === activeRow()\"\n [attr.aria-level]=\"childrenKey() ? treeDepth(row) + 1 : null\"\n [attr.aria-expanded]=\"\n childrenKey() && treeHasChildren(row) ? treeExpanded(row) : null\n \"\n [attr.tabindex]=\"\n childrenKey()\n ? treeRowTabindex(row)\n : gridMode()\n ? gridRowTabindex(row)\n : canDetail()\n ? paneRowTabindex(row)\n : null\n \"\n (keydown)=\"\n childrenKey()\n ? onTreeRowKeydown(row, $event)\n : gridMode()\n ? onGridRowKeydown(row, $event)\n : onPaneRowKeydown(row, $event)\n \"\n (dblclick)=\"onRowActivate(row)\"\n >\n @if (canDetail()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('detail')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__detailbtn\"\n [class.strct-dg__detailbtn--active]=\"row === activeRow()\"\n [attr.aria-expanded]=\"row === activeRow()\"\n [attr.aria-label]=\"L().openDetail\"\n (click)=\"openDetail(row)\"\n >\n <strct-icon name=\"chevronDoubleRight\" [size]=\"13\" [strokeWidth]=\"1.6\" />\n </button>\n </td>\n }\n @if (canExpand()) {\n <td\n class=\"strct-dg__expandcell\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('expand')\"\n >\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__expandbtn\"\n [class.strct-dg__expandbtn--open]=\"isExpanded(row)\"\n [attr.aria-expanded]=\"isExpanded(row)\"\n [attr.aria-label]=\"L().toggleDetail\"\n (click)=\"toggleExpand(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n </td>\n }\n @if (hasSelectionColumn()) {\n <td\n class=\"strct-dg__sel\"\n [class.strct-dg__cell--sticky]=\"stickyActive()\"\n [style.insetInlineStart.px]=\"utilLeft('sel')\"\n >\n @if (mode() === 'single') {\n <!-- A native radio group: one name per grid, so the\n arrow keys move between rows and Space picks,\n without the grid inventing its own keyboarding. -->\n <input\n type=\"radio\"\n class=\"strct-dg__radio\"\n [name]=\"radioName\"\n [attr.aria-label]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [attr.tabindex]=\"ctlTabindex()\"\n (change)=\"selectSingle(row)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- click fires (and bubbles) before the input's change,\n so the modifier is recorded by the time toggleRow runs. -->\n <strct-checkbox\n [ariaLabel]=\"selectRowLabel(row)\"\n [attr.aria-description]=\"lockedReason(row) || null\"\n [checked]=\"isSelected(row)\"\n [disabled]=\"lockedReason(row) !== null\"\n [tabIndex]=\"ctlTabindex()\"\n (click)=\"noteRangeIntent($event); $event.stopPropagation()\"\n (checkedChange)=\"toggleRow(row)\"\n />\n }\n </td>\n }\n @for (col of visibleColumns(); track col.key; let colIdx = $index) {\n <td\n [style.text-align]=\"col.align ?? (col.numeric ? 'end' : 'start')\"\n [class.strct-dg__cell--mono]=\"col.mono\"\n [class.strct-dg__cell--muted]=\"col.muted\"\n [class.strct-dg__cell--numeric]=\"col.numeric\"\n [class.strct-dg__cell--sticky]=\"isSticky(col)\"\n [class.strct-dg__cell--sticky-last]=\"col.key === lastStickyKey()\"\n [class.strct-dg__cell--editable]=\"col.editable\"\n [style.insetInlineStart.px]=\"stickyLeft(col.key)\"\n [attr.data-edit]=\"col.editable ? '' : null\"\n [attr.tabindex]=\"col.editable ? editCellTabindex(row, col) : null\"\n [attr.aria-describedby]=\"col.editable ? editHintId : null\"\n (dblclick)=\"col.editable && startEdit(row, col)\"\n (focus)=\"col.editable && onEditCellFocus(row, col)\"\n (keydown)=\"col.editable && onEditCellKeydown(row, col, $event)\"\n >\n @if (childrenKey() && colIdx === 0) {\n <span\n class=\"strct-dg__treepad\"\n [style.width.px]=\"treeDepth(row) * 18\"\n aria-hidden=\"true\"\n ></span>\n @if (treeHasChildren(row)) {\n <button\n type=\"button\"\n class=\"strct-dg__treebtn\"\n [class.strct-dg__treebtn--open]=\"treeExpanded(row)\"\n [attr.aria-expanded]=\"treeExpanded(row)\"\n [attr.aria-label]=\"L().toggleChildren\"\n (click)=\"toggleTreeRow(row)\"\n >\n <strct-icon name=\"chevronRight\" [size]=\"12\" [strokeWidth]=\"1.7\" />\n </button>\n } @else {\n <span\n class=\"strct-dg__treebtn strct-dg__treebtn--leaf\"\n aria-hidden=\"true\"\n ></span>\n }\n }\n @if (isEditingCell(row, col)) {\n @switch (col.editor ?? 'text') {\n @case ('select') {\n <!-- Choosing is the commit: there is nothing to\n confirm with Enter once an option is picked. -->\n <strct-select\n class=\"strct-dg__editselect\"\n [options]=\"editorOptionsFor(row, col)\"\n [listLabel]=\"L().editCell + ': ' + col.label\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"commitValue(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @case ('number') {\n <strct-number\n class=\"strct-dg__editnumber\"\n [ariaLabel]=\"L().editCell + ': ' + col.label\"\n [min]=\"col.editorMin ?? null\"\n [max]=\"col.editorMax ?? null\"\n [step]=\"col.editorStep ?? 1\"\n [ngModel]=\"row[col.key]\"\n (ngModelChange)=\"editDraft.set($event)\"\n (keydown.enter)=\"commitDraft(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (focusout)=\"commitOnLeave(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n @default {\n <input\n class=\"strct-dg__editinput\"\n type=\"text\"\n [value]=\"row[col.key] ?? ''\"\n [attr.aria-label]=\"L().editCell + ': ' + col.label\"\n (keydown.enter)=\"commitEdit(row, col, $event)\"\n (keydown.escape)=\"cancelEdit($event)\"\n (blur)=\"commitEdit(row, col, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n }\n } @else if (cellTemplate(col.key); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: row,\n row,\n value: row[col.key],\n column: col,\n }\"\n />\n } @else if (isBlank(row, col)) {\n <span class=\"strct-dg__empty\" [attr.aria-label]=\"col.emptyLabel || null\">{{\n col.emptyText\n }}</span>\n } @else {\n {{ displayValue(row, col) }}\n @if (col.descriptionKey && row[col.descriptionKey]) {\n <span class=\"strct-dg__celldesc\">{{ row[col.descriptionKey] }}</span>\n }\n }\n <!-- The affordance a mouse never needed: a cell that\n can be edited says so on hover and on focus. -->\n @if (col.editable && editHint() && !isEditingCell(row, col)) {\n <strct-icon\n class=\"strct-dg__editpencil\"\n name=\"pencil\"\n [size]=\"12\"\n [strokeWidth]=\"1.6\"\n aria-hidden=\"true\"\n />\n }\n </td>\n }\n @if (canActions()) {\n <td class=\"strct-dg__actioncell\">\n <button\n type=\"button\"\n [attr.tabindex]=\"ctlTabindex()\"\n class=\"strct-dg__kebab\"\n [attr.aria-label]=\"L().rowActions\"\n (click)=\"openRowMenu(row, $event)\"\n >\n <strct-icon name=\"dots\" [size]=\"16\" />\n </button>\n </td>\n }\n </tr>\n @if (canExpand() && isExpanded(row)) {\n <tr class=\"strct-dg__detailrow\">\n <td [attr.colspan]=\"colspan()\">\n <div class=\"strct-dg__detail\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, row }\"\n />\n </div>\n </td>\n </tr>\n }\n }\n } @empty {\n <!-- \"Nothing here\" and \"nothing matches what you typed\" are\n different states: the second one can be undone, and\n saying so is the whole point. -->\n <tr>\n <td class=\"strct-dg__emptyrow\" [attr.colspan]=\"colspan()\">\n @if (filteredAway()) {\n <strct-empty-state\n size=\"sm\"\n [titleLevel]=\"null\"\n [title]=\"L().noMatches\"\n [description]=\"noMatchesDescription()\"\n >\n <button strct-button variant=\"flat\" size=\"sm\" (click)=\"clearAllFilters()\">\n {{ L().clearFilters }}\n </button>\n </strct-empty-state>\n } @else {\n <!-- Not a heading: a grid is a region of a page, and an\n empty one used to put an <h3> in its outline. -->\n <strct-empty-state size=\"sm\" [titleLevel]=\"null\" [title]=\"emptyText()\" />\n }\n </td>\n </tr>\n }\n @if (virtual() && bottomPad() > 0) {\n <tr class=\"strct-dg__vspacer\" aria-hidden=\"true\">\n <td [attr.colspan]=\"colspan()\" [style.height.px]=\"bottomPad()\"></td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n @if (paneOpen()) {\n <aside class=\"strct-dg__pane\">\n <div class=\"strct-dg__pane-head\">\n @if (narrowPane()) {\n <!-- The pane has taken the list's place, so there is a way back. -->\n <button\n type=\"button\"\n class=\"strct-dg__pane-back\"\n [attr.aria-label]=\"L().backToList\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"chevronLeft\" [size]=\"13\" />\n </button>\n }\n <span class=\"strct-dg__pane-title\">\n <ng-content select=\"[strctDetailPaneTitle]\" />\n @if (!paneTitleDef()) {\n {{ activeRow()![columns()[0].key] }}\n }\n </span>\n <span class=\"strct-dg__pane-meta\">\n <ng-content select=\"[strctDetailPaneMeta]\" />\n </span>\n <span class=\"strct-dg__pane-actions\">\n <ng-content select=\"[strctDetailPaneActions]\" />\n </span>\n <button\n type=\"button\"\n class=\"strct-dg__pane-close\"\n [attr.aria-label]=\"L().closeDetail\"\n (click)=\"closePane()\"\n >\n <strct-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n <div class=\"strct-dg__pane-body\">\n <ng-container\n [ngTemplateOutlet]=\"detailDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: activeRow(), row: activeRow() }\"\n />\n </div>\n </aside>\n }\n</div>\n\n@if (showFooter()) {\n <div class=\"strct-dg__foot\">\n <div class=\"strct-dg__foot-left\">\n @if (columnChooser() || sync()) {\n <div class=\"strct-dg__actions\">\n <strct-button-group>\n @if (columnChooser()) {\n <button\n strct-button\n size=\"sm\"\n [class.is-open]=\"chooserOpen()\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().chooseColumns\"\n (click)=\"toggleChooser($event)\"\n >\n <strct-icon name=\"settings\" [size]=\"16\" />\n </button>\n }\n @if (sync()) {\n <button\n strct-button\n size=\"sm\"\n [disabled]=\"footerActionsDisabled()\"\n [attr.aria-label]=\"L().refresh\"\n (click)=\"syncChange.emit()\"\n >\n <strct-icon name=\"refresh\" [size]=\"16\" />\n </button>\n }\n </strct-button-group>\n @if (columnChooser() && chooserOpen()) {\n <div class=\"strct-dg__chooser-menu\">\n @for (col of columns(); track col.key) {\n <!-- The whole row toggles; the checkbox's own click must not\n reach the row or the column would flip twice. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"strct-dg__chooser-item\"\n (click)=\"toggleColumn(col.key, hiddenColumns().has(col.key))\"\n >\n <strct-checkbox\n [checked]=\"!hiddenColumns().has(col.key)\"\n (checkedChange)=\"toggleColumn(col.key, $event)\"\n (click)=\"$event.stopPropagation()\"\n />\n <span>{{ col.label }}</span>\n </div>\n }\n </div>\n }\n </div>\n }\n <span class=\"strct-dg__count\">\n @if (paging() === 'more') {\n {{ L().showing(rows().length, moreTotal() ?? undefined) }}\n } @else {\n {{ totalCount() }} {{ totalCount() === 1 ? L().row : L().rows }}\n }\n @if (selectedCount() && mode() !== 'single') {\n <span class=\"strct-dg__count-sep\">|</span>\n <span class=\"strct-dg__count-sel\">{{ selectedCount() }} {{ L().selected }}</span>\n }\n </span>\n </div>\n <div class=\"strct-dg__foot-right\">\n <!-- A feed that pages by cursor loads more at the end: page numbers\n are something a cursor API cannot answer. It takes the pager's\n place — the column chooser, the sync button and the count are\n the footer's, not the pager's. -->\n @if (paging() === 'more') {\n @if (hasMore()) {\n <button\n strct-button\n size=\"sm\"\n class=\"strct-dg__more\"\n [disabled]=\"loadingMore()\"\n (click)=\"loadMore.emit()\"\n >\n @if (loadingMore()) {\n <strct-spinner size=\"sm\" />\n }\n {{ L().loadMore }}\n </button>\n }\n } @else if (pageSize() > 0 && !groupBy()) {\n <strct-pagination\n [total]=\"totalCount()\"\n [pageSize]=\"pageSize()\"\n [page]=\"page()\"\n [regionLabel]=\"regionName(L().pagination)\"\n (pageChange)=\"goToPage($event)\"\n />\n }\n </div>\n </div>\n}\n",
27121
27163
  styles: [".strct-dg__radio{width:15px;height:15px;accent-color:var(--acc);cursor:pointer}.strct-dg__radio:disabled{cursor:not-allowed}.strct-dg__row--locked .strct-dg__sel{opacity:.55}.strct-dg__actioncaption:empty{display:none}.strct-dg__actioncaption{display:inline-flex;align-items:center;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);color:var(--t2)}.strct-dg__bulk{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:4px 10px;padding:7px 12px;border-bottom:1px solid var(--b2);background:var(--acc-s);color:var(--t2);font-size:var(--text-md)}.strct-dg__announcer{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.strct-dg-host{display:block;border:1px solid var(--b2);border-radius:var(--radius-lg);background:var(--bg-2);box-shadow:var(--shadow-rest)}.strct-dg-host--flush{border:0;border-radius:0;background:transparent;box-shadow:none}.strct-dg__more strct-spinner{margin-inline-end:4px}.strct-dg__caption{caption-side:top;text-align:start;padding:var(--space-2) var(--space-3);font-size:var(--text-sm);font-weight:600;color:var(--t2)}.strct-dg td.strct-dg__cell--mono{font-family:var(--mono);font-size:var(--text-sm)}.strct-dg td.strct-dg__cell--muted{color:var(--t2)}.strct-dg td.strct-dg__cell--numeric{font-variant-numeric:tabular-nums}.strct-dg__empty{color:var(--t2)}.strct-dg__celldesc{display:block;font-size:var(--text-sm);color:var(--t2);line-height:1.3}.strct-dg__scroll{flex:1 1 auto;min-width:0;overflow-x:auto;-webkit-overflow-scrolling:touch}.strct-dg-host--virtual .strct-dg__scroll,.strct-dg-host--bounded .strct-dg__scroll{overflow-y:auto}.strct-dg-host--virtual .strct-dg thead th,.strct-dg-host--bounded .strct-dg thead th{position:sticky;top:0;z-index:var(--z-raised)}.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:var(--text-sm);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__grouplabel--critical{color:var(--critical)}.strct-dg__grouplabel--warning{color:var(--warning)}.strct-dg__grouplabel--success{color:var(--success)}.strct-dg__grouplabel--accent{color:var(--acc)}.strct-dg__grouplabel--neutral,.strct-dg__grouplabel--none{color:var(--t1)}.strct-dg__groupnote{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);font-weight:400;color:var(--t2)}.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:4px;background:var(--acc-m);color:var(--acc);font-variant-numeric:tabular-nums}.strct-dg__groupcount--critical{background:var(--critical-bg);color:var(--critical)}.strct-dg__groupcount--warning{background:var(--warning-bg);color:var(--warning)}.strct-dg__groupcount--success{background:var(--success-bg);color:var(--success)}.strct-dg__groupcount--neutral,.strct-dg__groupcount--none{background:var(--bg-3);color:var(--t2)}.strct-dg-host--singleline .strct-dg{border-collapse:separate;border-spacing:0}.strct-dg-host--sticky .strct-dg,.strct-dg-host--virtual .strct-dg,.strct-dg-host--bounded .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:calc(var(--z-base) + 2)}.strct-dg thead th.strct-dg__cell--sticky{z-index:calc(var(--z-raised) + 1)}.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:4px;border-start-end-radius:4px}.strct-dg-host:not(:has(.strct-dg__foot)) .strct-dg{border-end-start-radius:4px;border-end-end-radius:4px}.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__sortbtn{display:inline-flex;align-items:center;gap:inherit;padding:0;border:0;background:none;color:inherit;font:inherit;text-transform:inherit;letter-spacing:inherit;text-align:inherit;cursor:pointer}.strct-dg__sortbtn:focus-visible{outline:2px solid var(--acc50);outline-offset:2px;border-radius:var(--radius-sm)}.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:calc(var(--z-base) + 1)}.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:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-1)}[data-theme=dark] .strct-dg tbody tr:not(.strct-dg__detailrow):hover td{background:linear-gradient(var(--acc-s),var(--acc-s)) var(--bg-3)}.strct-dg tbody .strct-dg__row--selected td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody .strct-dg__row--selected td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg tbody tr.strct-dg__row--selected:hover td{background:linear-gradient(var(--acc-s),var(--acc-s)),linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody tr.strct-dg__row--selected:hover td{background:linear-gradient(var(--acc-s),var(--acc-s)),linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.strct-dg__sel{width:1%;white-space:nowrap}.strct-dg__sel .strct-cb,.strct-dg__sel .strct-dg__radio,.strct-dg__expandbtn{position:relative}.strct-dg__sel .strct-cb:after,.strct-dg__sel .strct-dg__radio:after,.strct-dg__expandbtn:after{content:\"\";position:absolute;inset:-11px -13px;min-width:24px;min-height:24px}.strct-dg-host--compact .strct-dg__sel .strct-cb:after,.strct-dg-host--compact .strct-dg__sel .strct-dg__radio:after,.strct-dg-host--compact .strct-dg__expandbtn:after{inset:-7px -11px}.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:2px;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{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:3px;border:0;border-radius:2px;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:2px;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:var(--strct-dg-pane-list-w, 260px);min-width:var(--strct-dg-pane-list-w, 260px);max-width:var(--strct-dg-pane-list-w, 260px)}.strct-dg__layout--paned>.strct-dg__scroll>.strct-dg{width:100%;min-width:0;max-width:none;flex-shrink:0;border-start-end-radius:0;border-end-end-radius:0}.strct-dg__layout--panetakeover>.strct-dg__scroll{display:none}.strct-dg__layout--panetakeover>.strct-dg__pane{flex:1;min-width:0}.strct-dg__pane-meta,.strct-dg__pane-actions{display:inline-flex;align-items:center;gap:var(--space-2)}.strct-dg__pane-meta:empty,.strct-dg__pane-actions:empty{display:none}.strct-dg__pane-actions{margin-inline-start:auto}.strct-dg__pane-back{display:inline-flex;align-items:center;border:0;background:none;color:var(--t2);cursor:pointer;padding:2px;border-radius:var(--radius-sm)}.strct-dg__row--clickable{cursor:pointer}.strct-dg tbody .strct-dg__row--active td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-1)}[data-theme=dark] .strct-dg tbody .strct-dg__row--active td{background:linear-gradient(var(--acc-m),var(--acc-m)) var(--bg-3)}.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;inset-inline-end: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:4px;border-start-start-radius:0;border-end-start-radius: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:2px;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;border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--bg-3) 25%,var(--skeleton-hi, var(--acc18)) 50%,var(--bg-3) 75%);background-size:200% 100%;animation:strct-dg-skeleton-sweep 1.1s linear infinite}@keyframes strct-dg-skeleton-sweep{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.strct-dg__skeleton-block{animation:none;background:var(--skeleton-hi, var(--acc18))}}.strct-dg__skeleton-row td{border-bottom:1px solid var(--b1)}.strct-dg__empty{text-align:center;color:var(--t2);padding:22px}.strct-dg__quickfilter{min-width:180px;max-width:260px}.strct-dg__quickfilter--end{order:2;margin-inline-start:auto}.strct-dg__filternote--end{order:3}.strct-dg__filternote{font-size:var(--text-sm);color:var(--t2);font-variant-numeric:tabular-nums;white-space:nowrap}.strct-dg__toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--b2)}.strct-dg__foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 14px;border-top:1px solid var(--b2);flex-wrap:wrap}.strct-dg__foot-left,.strct-dg__foot-right{display:flex;align-items:center;gap:12px}.strct-dg__actions{position:relative}.strct-dg__chooser-menu{position:absolute;bottom:calc(100% + 6px);inset-inline-start:0;z-index:var(--z-sticky);min-width:180px;background:var(--bg-1);border:1px solid var(--b2);border-radius:4px;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:3px;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:2px;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:calc(var(--z-dropdown) + 10);display:flex;flex-direction:column;gap:6px;min-width:180px;padding:10px;background:var(--bg-1);border:1px solid var(--b2);border-radius:3px;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:var(--text-sm);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 tbody tr[tabindex]:focus-visible{outline:2px solid var(--acc50);outline-offset:-2px}.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:2px;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;position:relative}.strct-dg__cell--editable:hover{box-shadow:inset 0 0 0 1px var(--b2)}.strct-dg__cell--editable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-dg__editpencil{position:absolute;top:50%;inset-inline-end:6px;transform:translateY(-50%);color:var(--t3);opacity:0;pointer-events:none}td:hover>.strct-dg__editpencil,td:focus-within>.strct-dg__editpencil{opacity:1}.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__editselect,.strct-dg__editnumber{display:block;width:100%}.strct-dg__editinput:focus-visible{outline:2px solid var(--acc50);outline-offset:1px}\n"]
27122
27164
  }]
27123
27165
  }],
@@ -27682,7 +27724,7 @@ var StrctStackItem = class StrctStackItem {
27682
27724
  version: "22.2.3",
27683
27725
  type: StrctStackItem,
27684
27726
  isStandalone: true,
27685
- selector: "strct-stack-item",
27727
+ selector: "strct-stack-item, div[strctStackItem]",
27686
27728
  inputs: { label: {
27687
27729
  classPropertyName: "label",
27688
27730
  publicName: "label",
@@ -27710,7 +27752,7 @@ i0.ɵɵngDeclareClassMetadata({
27710
27752
  decorators: [{
27711
27753
  type: Component,
27712
27754
  args: [{
27713
- selector: "strct-stack-item",
27755
+ selector: "strct-stack-item, div[strctStackItem]",
27714
27756
  changeDetection: ChangeDetectionStrategy.OnPush,
27715
27757
  encapsulation: ViewEncapsulation.None,
27716
27758
  template: `
@@ -31270,51 +31312,80 @@ var StrctFlow = class StrctFlow {
31270
31312
  @if (!col.nodes.length && col.emptyText) {
31271
31313
  <p class="strct-flow__colempty">{{ col.emptyText }}</p>
31272
31314
  }
31273
- <ul class="strct-flow__boxes">
31274
- @for (node of col.nodes; track node.id) {
31275
- <li
31276
- class="strct-flow__box strct-flow__box--{{ node.status ?? 'neutral' }}"
31277
- [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31278
- [class.strct-flow__box--navigable]="navigable()"
31279
- [attr.data-flow-node]="node.id"
31280
- [attr.role]="navigable() ? 'button' : null"
31281
- [attr.tabindex]="navigable() ? (isCursor(node) ? 0 : -1) : null"
31282
- (click)="activate(node)"
31283
- (focus)="cursor.set(node.id)"
31284
- (keydown)="onNodeKeydown($event, node)"
31285
- >
31286
- @if (nodeTpl(); as tpl) {
31315
+ <!-- A read-only diagram is a list of items. A navigable one is a
31316
+ group of buttons: an <li> may not take role="button" and a <ul>
31317
+ may only hold list items (axe \`aria-allowed-role\` / \`list\`),
31318
+ so its boxes are plain <div>s inside a plain <div>. -->
31319
+ @if (navigable()) {
31320
+ <div class="strct-flow__boxes">
31321
+ @for (node of col.nodes; track node.id) {
31322
+ <div
31323
+ class="strct-flow__box strct-flow__box--navigable strct-flow__box--{{
31324
+ node.status ?? 'neutral'
31325
+ }}"
31326
+ [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31327
+ [attr.data-flow-node]="node.id"
31328
+ role="button"
31329
+ [attr.tabindex]="isCursor(node) ? 0 : -1"
31330
+ (click)="activate(node)"
31331
+ (focus)="cursor.set(node.id)"
31332
+ (keydown)="onNodeKeydown($event, node)"
31333
+ >
31287
31334
  <ng-container
31288
- [ngTemplateOutlet]="tpl"
31289
- [ngTemplateOutletContext]="{ $implicit: node, node }"
31335
+ [ngTemplateOutlet]="boxContent"
31336
+ [ngTemplateOutletContext]="{ $implicit: node }"
31290
31337
  />
31291
- } @else {
31292
- @if (node.icon) {
31293
- <strct-icon
31294
- class="strct-flow__icon"
31295
- [name]="node.icon"
31296
- [size]="15"
31297
- [strokeWidth]="1.4"
31298
- />
31299
- }
31300
- <span class="strct-flow__label">{{ node.label }}</span>
31301
- @if (node.role) {
31302
- <span class="strct-flow__role">{{ node.role }}</span>
31303
- }
31304
- @if (node.sublabel) {
31305
- <span class="strct-flow__sub">{{ node.sublabel }}</span>
31306
- }
31307
- }
31308
- <span class="strct-flow__sr">{{ nodeSummary(node) }}</span>
31309
- </li>
31310
- }
31311
- </ul>
31338
+ </div>
31339
+ }
31340
+ </div>
31341
+ } @else {
31342
+ <ul class="strct-flow__boxes">
31343
+ @for (node of col.nodes; track node.id) {
31344
+ <li
31345
+ class="strct-flow__box strct-flow__box--{{ node.status ?? 'neutral' }}"
31346
+ [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31347
+ [attr.data-flow-node]="node.id"
31348
+ >
31349
+ <ng-container
31350
+ [ngTemplateOutlet]="boxContent"
31351
+ [ngTemplateOutletContext]="{ $implicit: node }"
31352
+ />
31353
+ </li>
31354
+ }
31355
+ </ul>
31356
+ }
31312
31357
  </div>
31313
31358
  }
31314
31359
  </div>
31315
31360
  @if (label()) {
31316
31361
  <div class="strct-flow__caption">{{ label() }}</div>
31317
31362
  }
31363
+ <!-- One box's content, shared by the list and the button forms. -->
31364
+ <ng-template #boxContent let-node>
31365
+ @if (nodeTpl(); as tpl) {
31366
+ <ng-container
31367
+ [ngTemplateOutlet]="tpl"
31368
+ [ngTemplateOutletContext]="{ $implicit: node, node }"
31369
+ />
31370
+ } @else {
31371
+ @if (node.icon) {
31372
+ <strct-icon
31373
+ class="strct-flow__icon"
31374
+ [name]="node.icon"
31375
+ [size]="15"
31376
+ [strokeWidth]="1.4"
31377
+ />
31378
+ }
31379
+ <span class="strct-flow__label">{{ node.label }}</span>
31380
+ @if (node.role) {
31381
+ <span class="strct-flow__role">{{ node.role }}</span>
31382
+ }
31383
+ @if (node.sublabel) {
31384
+ <span class="strct-flow__sub">{{ node.sublabel }}</span>
31385
+ }
31386
+ }
31387
+ <span class="strct-flow__sr">{{ nodeSummary(node) }}</span>
31388
+ </ng-template>
31318
31389
  } @else {
31319
31390
  <div class="strct-flow__row">
31320
31391
  @for (node of nodes(); track node.id; let last = $last) {
@@ -31433,51 +31504,80 @@ i0.ɵɵngDeclareClassMetadata({
31433
31504
  @if (!col.nodes.length && col.emptyText) {
31434
31505
  <p class="strct-flow__colempty">{{ col.emptyText }}</p>
31435
31506
  }
31436
- <ul class="strct-flow__boxes">
31437
- @for (node of col.nodes; track node.id) {
31438
- <li
31439
- class="strct-flow__box strct-flow__box--{{ node.status ?? 'neutral' }}"
31440
- [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31441
- [class.strct-flow__box--navigable]="navigable()"
31442
- [attr.data-flow-node]="node.id"
31443
- [attr.role]="navigable() ? 'button' : null"
31444
- [attr.tabindex]="navigable() ? (isCursor(node) ? 0 : -1) : null"
31445
- (click)="activate(node)"
31446
- (focus)="cursor.set(node.id)"
31447
- (keydown)="onNodeKeydown($event, node)"
31448
- >
31449
- @if (nodeTpl(); as tpl) {
31507
+ <!-- A read-only diagram is a list of items. A navigable one is a
31508
+ group of buttons: an <li> may not take role="button" and a <ul>
31509
+ may only hold list items (axe \`aria-allowed-role\` / \`list\`),
31510
+ so its boxes are plain <div>s inside a plain <div>. -->
31511
+ @if (navigable()) {
31512
+ <div class="strct-flow__boxes">
31513
+ @for (node of col.nodes; track node.id) {
31514
+ <div
31515
+ class="strct-flow__box strct-flow__box--navigable strct-flow__box--{{
31516
+ node.status ?? 'neutral'
31517
+ }}"
31518
+ [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31519
+ [attr.data-flow-node]="node.id"
31520
+ role="button"
31521
+ [attr.tabindex]="isCursor(node) ? 0 : -1"
31522
+ (click)="activate(node)"
31523
+ (focus)="cursor.set(node.id)"
31524
+ (keydown)="onNodeKeydown($event, node)"
31525
+ >
31450
31526
  <ng-container
31451
- [ngTemplateOutlet]="tpl"
31452
- [ngTemplateOutletContext]="{ $implicit: node, node }"
31527
+ [ngTemplateOutlet]="boxContent"
31528
+ [ngTemplateOutletContext]="{ $implicit: node }"
31453
31529
  />
31454
- } @else {
31455
- @if (node.icon) {
31456
- <strct-icon
31457
- class="strct-flow__icon"
31458
- [name]="node.icon"
31459
- [size]="15"
31460
- [strokeWidth]="1.4"
31461
- />
31462
- }
31463
- <span class="strct-flow__label">{{ node.label }}</span>
31464
- @if (node.role) {
31465
- <span class="strct-flow__role">{{ node.role }}</span>
31466
- }
31467
- @if (node.sublabel) {
31468
- <span class="strct-flow__sub">{{ node.sublabel }}</span>
31469
- }
31470
- }
31471
- <span class="strct-flow__sr">{{ nodeSummary(node) }}</span>
31472
- </li>
31473
- }
31474
- </ul>
31530
+ </div>
31531
+ }
31532
+ </div>
31533
+ } @else {
31534
+ <ul class="strct-flow__boxes">
31535
+ @for (node of col.nodes; track node.id) {
31536
+ <li
31537
+ class="strct-flow__box strct-flow__box--{{ node.status ?? 'neutral' }}"
31538
+ [class.strct-flow__box--surface]="node.emphasis === 'surface'"
31539
+ [attr.data-flow-node]="node.id"
31540
+ >
31541
+ <ng-container
31542
+ [ngTemplateOutlet]="boxContent"
31543
+ [ngTemplateOutletContext]="{ $implicit: node }"
31544
+ />
31545
+ </li>
31546
+ }
31547
+ </ul>
31548
+ }
31475
31549
  </div>
31476
31550
  }
31477
31551
  </div>
31478
31552
  @if (label()) {
31479
31553
  <div class="strct-flow__caption">{{ label() }}</div>
31480
31554
  }
31555
+ <!-- One box's content, shared by the list and the button forms. -->
31556
+ <ng-template #boxContent let-node>
31557
+ @if (nodeTpl(); as tpl) {
31558
+ <ng-container
31559
+ [ngTemplateOutlet]="tpl"
31560
+ [ngTemplateOutletContext]="{ $implicit: node, node }"
31561
+ />
31562
+ } @else {
31563
+ @if (node.icon) {
31564
+ <strct-icon
31565
+ class="strct-flow__icon"
31566
+ [name]="node.icon"
31567
+ [size]="15"
31568
+ [strokeWidth]="1.4"
31569
+ />
31570
+ }
31571
+ <span class="strct-flow__label">{{ node.label }}</span>
31572
+ @if (node.role) {
31573
+ <span class="strct-flow__role">{{ node.role }}</span>
31574
+ }
31575
+ @if (node.sublabel) {
31576
+ <span class="strct-flow__sub">{{ node.sublabel }}</span>
31577
+ }
31578
+ }
31579
+ <span class="strct-flow__sr">{{ nodeSummary(node) }}</span>
31580
+ </ng-template>
31481
31581
  } @else {
31482
31582
  <div class="strct-flow__row">
31483
31583
  @for (node of nodes(); track node.id; let last = $last) {
@@ -35196,6 +35296,7 @@ var StrctSplitter = class StrctSplitter {
35196
35296
  transform: booleanAttribute
35197
35297
  });
35198
35298
  gutterLabel = input("Resize panes", ...ngDevMode ? [{ debugName: "gutterLabel" }] : /* istanbul ignore next */ []);
35299
+ collapsedLabel = input("Collapsed", ...ngDevMode ? [{ debugName: "collapsedLabel" }] : /* istanbul ignore next */ []);
35199
35300
  step = input(3, ...ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []);
35200
35301
  lowerBound = computed(() => this.minSize() ?? (this.unit() === "px" ? 0 : this.min()), ...ngDevMode ? [{ debugName: "lowerBound" }] : /* istanbul ignore next */ []);
35201
35302
  upperBound = computed(() => this.maxSize() ?? (this.unit() === "px" ? Number.POSITIVE_INFINITY : this.max()), ...ngDevMode ? [{ debugName: "upperBound" }] : /* istanbul ignore next */ []);
@@ -35344,6 +35445,13 @@ var StrctSplitter = class StrctSplitter {
35344
35445
  isRequired: false,
35345
35446
  transformFunction: null
35346
35447
  },
35448
+ collapsedLabel: {
35449
+ classPropertyName: "collapsedLabel",
35450
+ publicName: "collapsedLabel",
35451
+ isSignal: true,
35452
+ isRequired: false,
35453
+ transformFunction: null
35454
+ },
35347
35455
  step: {
35348
35456
  classPropertyName: "step",
35349
35457
  publicName: "step",
@@ -35381,7 +35489,8 @@ var StrctSplitter = class StrctSplitter {
35381
35489
  [attr.aria-valuenow]="collapsed() ? 0 : clamped()"
35382
35490
  [attr.aria-valuemin]="lowerBound()"
35383
35491
  [attr.aria-valuemax]="upperBound()"
35384
- [attr.aria-expanded]="collapsible() ? !collapsed() : null"
35492
+ [attr.aria-valuetext]="collapsible() && collapsed() ? collapsedLabel() : null"
35493
+ [attr.data-collapsed]="collapsible() ? collapsed() : null"
35385
35494
  (pointerdown)="onDragStart($event)"
35386
35495
  (keydown)="onKeydown($event)"
35387
35496
  >
@@ -35425,7 +35534,8 @@ i0.ɵɵngDeclareClassMetadata({
35425
35534
  [attr.aria-valuenow]="collapsed() ? 0 : clamped()"
35426
35535
  [attr.aria-valuemin]="lowerBound()"
35427
35536
  [attr.aria-valuemax]="upperBound()"
35428
- [attr.aria-expanded]="collapsible() ? !collapsed() : null"
35537
+ [attr.aria-valuetext]="collapsible() && collapsed() ? collapsedLabel() : null"
35538
+ [attr.data-collapsed]="collapsible() ? collapsed() : null"
35429
35539
  (pointerdown)="onDragStart($event)"
35430
35540
  (keydown)="onKeydown($event)"
35431
35541
  >
@@ -35531,6 +35641,14 @@ i0.ɵɵngDeclareClassMetadata({
35531
35641
  required: false
35532
35642
  }]
35533
35643
  }],
35644
+ collapsedLabel: [{
35645
+ type: i0.Input,
35646
+ args: [{
35647
+ isSignal: true,
35648
+ alias: "collapsedLabel",
35649
+ required: false
35650
+ }]
35651
+ }],
35534
35652
  step: [{
35535
35653
  type: i0.Input,
35536
35654
  args: [{
@@ -35555,6 +35673,7 @@ var StrctResizeHandle = class StrctResizeHandle {
35555
35673
  transform: booleanAttribute
35556
35674
  });
35557
35675
  collapsed = model(false, ...ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []);
35676
+ collapsedLabel = input("Collapsed", ...ngDevMode ? [{ debugName: "collapsedLabel" }] : /* istanbul ignore next */ []);
35558
35677
  side = input("before", ...ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []);
35559
35678
  el = inject(ElementRef);
35560
35679
  dragging = signal(false, ...ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []);
@@ -35680,6 +35799,13 @@ var StrctResizeHandle = class StrctResizeHandle {
35680
35799
  isRequired: false,
35681
35800
  transformFunction: null
35682
35801
  },
35802
+ collapsedLabel: {
35803
+ classPropertyName: "collapsedLabel",
35804
+ publicName: "collapsedLabel",
35805
+ isSignal: true,
35806
+ isRequired: false,
35807
+ transformFunction: null
35808
+ },
35683
35809
  side: {
35684
35810
  classPropertyName: "side",
35685
35811
  publicName: "side",
@@ -35705,10 +35831,11 @@ var StrctResizeHandle = class StrctResizeHandle {
35705
35831
  "class.strct-resize--y": "axis() === 'y'",
35706
35832
  "class.strct-resize--dragging": "dragging()",
35707
35833
  "attr.aria-orientation": "axis() === 'y' ? 'horizontal' : 'vertical'",
35708
- "attr.aria-valuenow": "size()",
35834
+ "attr.aria-valuenow": "collapsible() && collapsed() ? 0 : size()",
35709
35835
  "attr.aria-valuemin": "min()",
35710
35836
  "attr.aria-valuemax": "max()",
35711
- "attr.aria-expanded": "collapsible() ? !collapsed() : null"
35837
+ "attr.aria-valuetext": "collapsible() && collapsed() ? collapsedLabel() : null",
35838
+ "attr.data-collapsed": "collapsible() ? collapsed() : null"
35712
35839
  },
35713
35840
  classAttribute: "strct-resize"
35714
35841
  },
@@ -35731,10 +35858,11 @@ i0.ɵɵngDeclareClassMetadata({
35731
35858
  "[class.strct-resize--y]": "axis() === 'y'",
35732
35859
  "[class.strct-resize--dragging]": "dragging()",
35733
35860
  "[attr.aria-orientation]": "axis() === 'y' ? 'horizontal' : 'vertical'",
35734
- "[attr.aria-valuenow]": "size()",
35861
+ "[attr.aria-valuenow]": "collapsible() && collapsed() ? 0 : size()",
35735
35862
  "[attr.aria-valuemin]": "min()",
35736
35863
  "[attr.aria-valuemax]": "max()",
35737
- "[attr.aria-expanded]": "collapsible() ? !collapsed() : null",
35864
+ "[attr.aria-valuetext]": "collapsible() && collapsed() ? collapsedLabel() : null",
35865
+ "[attr.data-collapsed]": "collapsible() ? collapsed() : null",
35738
35866
  "(pointerdown)": "onDown($event)",
35739
35867
  "(keydown)": "onKeydown($event)"
35740
35868
  }
@@ -35804,6 +35932,14 @@ i0.ɵɵngDeclareClassMetadata({
35804
35932
  type: i0.Output,
35805
35933
  args: ["collapsedChange"]
35806
35934
  }],
35935
+ collapsedLabel: [{
35936
+ type: i0.Input,
35937
+ args: [{
35938
+ isSignal: true,
35939
+ alias: "collapsedLabel",
35940
+ required: false
35941
+ }]
35942
+ }],
35807
35943
  side: [{
35808
35944
  type: i0.Input,
35809
35945
  args: [{
@@ -38023,6 +38159,7 @@ var StrctReorder = class StrctReorder {
38023
38159
  if (text && !this.hint) {
38024
38160
  this.hint = this.renderer.createElement("span");
38025
38161
  this.renderer.setAttribute(this.hint, "id", this.instructionsId);
38162
+ this.renderer.setAttribute(this.hint, "hidden", "");
38026
38163
  this.hint.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;";
38027
38164
  this.renderer.appendChild(this.doc.body, this.hint);
38028
38165
  }
@@ -38237,10 +38374,34 @@ i0.ɵɵngDeclareClassMetadata({
38237
38374
  }]
38238
38375
  }]
38239
38376
  });
38377
+ const SET_MEMBER_ROLES = /* @__PURE__ */ new Set([
38378
+ "listitem",
38379
+ "option",
38380
+ "row",
38381
+ "treeitem",
38382
+ "menuitem",
38383
+ "menuitemcheckbox",
38384
+ "menuitemradio",
38385
+ "radio",
38386
+ "tab",
38387
+ "article",
38388
+ "comment"
38389
+ ]);
38390
+ const SET_MEMBER_TAGS = /* @__PURE__ */ new Set([
38391
+ "LI",
38392
+ "TR",
38393
+ "OPTION",
38394
+ "ARTICLE"
38395
+ ]);
38396
+ function takesSetPosition(el) {
38397
+ const role = el.getAttribute("role")?.trim().split(/\s+/)[0];
38398
+ return role ? SET_MEMBER_ROLES.has(role) : SET_MEMBER_TAGS.has(el.tagName);
38399
+ }
38240
38400
  var StrctReorderItem = class StrctReorderItem {
38241
38401
  list = inject(StrctReorder);
38242
38402
  el = inject(ElementRef);
38243
38403
  hadOwnTabindex = this.el.nativeElement.hasAttribute("tabindex");
38404
+ inSet = takesSetPosition(this.el.nativeElement);
38244
38405
  hostTabindex() {
38245
38406
  if (this.hadOwnTabindex) return null;
38246
38407
  return this.list.reorderDisabled() ? null : 0;
@@ -38346,8 +38507,8 @@ var StrctReorderItem = class StrctReorderItem {
38346
38507
  "class.strct-reorder--over": "isOver()",
38347
38508
  "attr.aria-roledescription": "list.reorderDisabled() ? null : 'sortable'",
38348
38509
  "attr.aria-keyshortcuts": "list.reorderDisabled() ? null : shortcuts()",
38349
- "attr.aria-posinset": "list.reorderDisabled() ? null : index() + 1",
38350
- "attr.aria-setsize": "list.reorderDisabled() ? null : list.items().length",
38510
+ "attr.aria-posinset": "list.reorderDisabled() || !inSet ? null : index() + 1",
38511
+ "attr.aria-setsize": "list.reorderDisabled() || !inSet ? null : list.items().length",
38351
38512
  "attr.aria-describedby": "list.reorderDisabled() || !list.instructions() ? null : list.instructionsId"
38352
38513
  }
38353
38514
  },
@@ -38370,8 +38531,8 @@ i0.ɵɵngDeclareClassMetadata({
38370
38531
  "[class.strct-reorder--over]": "isOver()",
38371
38532
  "[attr.aria-roledescription]": "list.reorderDisabled() ? null : 'sortable'",
38372
38533
  "[attr.aria-keyshortcuts]": "list.reorderDisabled() ? null : shortcuts()",
38373
- "[attr.aria-posinset]": "list.reorderDisabled() ? null : index() + 1",
38374
- "[attr.aria-setsize]": "list.reorderDisabled() ? null : list.items().length",
38534
+ "[attr.aria-posinset]": "list.reorderDisabled() || !inSet ? null : index() + 1",
38535
+ "[attr.aria-setsize]": "list.reorderDisabled() || !inSet ? null : list.items().length",
38375
38536
  "[attr.aria-describedby]": "list.reorderDisabled() || !list.instructions() ? null : list.instructionsId"
38376
38537
  }
38377
38538
  }]