@nanahoshi/mona-ui 0.22.1 → 0.23.1

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.
Files changed (26) hide show
  1. package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +1 -1
  2. package/fesm2022/nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs +169 -0
  3. package/fesm2022/nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs +176 -0
  4. package/fesm2022/{nanahoshi-mona-ui-grid-grid-toggle.component-DZDeYdqc.mjs → nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs} +201 -192
  5. package/fesm2022/nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs +173 -0
  6. package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-C_mj53LF.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs} +554 -667
  7. package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
  8. package/fesm2022/nanahoshi-mona-ui-internal-filter-input.mjs +47 -7
  9. package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +386 -180
  10. package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +266 -99
  11. package/fesm2022/nanahoshi-mona-ui-internal.mjs +35 -32
  12. package/fesm2022/nanahoshi-mona-ui-locales.mjs +13 -0
  13. package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +1 -1
  14. package/fesm2022/nanahoshi-mona-ui-tree-view.mjs +1 -1
  15. package/package.json +1 -1
  16. package/types/nanahoshi-mona-ui-grid.d.ts +126 -26
  17. package/types/nanahoshi-mona-ui-i18n.d.ts +1 -0
  18. package/types/nanahoshi-mona-ui-internal-filter-input.d.ts +35 -1
  19. package/types/nanahoshi-mona-ui-internal-list.d.ts +82 -16
  20. package/types/nanahoshi-mona-ui-internal-tree.d.ts +50 -3
  21. package/types/nanahoshi-mona-ui-internal.d.ts +18 -4
  22. package/types/nanahoshi-mona-ui-locales.d.ts +13 -0
  23. package/fesm2022/nanahoshi-mona-ui-grid-grid-footer-cell.component-blRlw0Ie.mjs +0 -108
  24. package/fesm2022/nanahoshi-mona-ui-grid-grid-footer-table-cell.directive-Cviex3cR.mjs +0 -44
  25. package/fesm2022/nanahoshi-mona-ui-grid-grid-list.component-H38ZTj-_.mjs +0 -164
  26. package/fesm2022/nanahoshi-mona-ui-grid-grid-virtual-list.component-b1W7r45p.mjs +0 -239
@@ -1,9 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ElementRef, Renderer2, input, effect, Directive } from '@angular/core';
3
- import { forEach, compact } from '@mirei/ts-collections';
3
+ import { forEach } from '@mirei/ts-collections';
4
4
  import { Observable, timer, take, debounceTime, map, merge, scan, filter } from 'rxjs';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { isIterable } from 'rxjs/internal/util/isIterable';
7
6
 
8
7
  class AttributeBinderDirective {
9
8
  #element;
@@ -111,23 +110,16 @@ const hsva2rgba = (hue, saturation, value, alpha) => {
111
110
  };
112
111
  };
113
112
 
113
+ const toHexByte = (value) => {
114
+ const byte = Math.min(255, Math.max(0, Math.round(value)));
115
+ return byte.toString(16).padStart(2, "0");
116
+ };
114
117
  const rgba2hex = (r, g, b, a) => {
115
118
  if (r == null || g == null || b == null || a == null) {
116
119
  return "";
117
120
  }
118
- let rHex = r.toString(16);
119
- let gHex = g.toString(16);
120
- let bHex = b.toString(16);
121
- let aHex = a.toString(16);
122
- if (rHex.length == 1)
123
- rHex = "0" + rHex;
124
- if (gHex.length == 1)
125
- gHex = "0" + gHex;
126
- if (bHex.length == 1)
127
- bHex = "0" + bHex;
128
- if (aHex.length == 1)
129
- aHex = "0" + aHex;
130
- return `#${rHex}${gHex}${bHex}${aHex.toUpperCase() !== "FF" ? aHex : ""}`;
121
+ const aHex = toHexByte(a);
122
+ return `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}${aHex !== "ff" ? aHex : ""}`;
131
123
  };
132
124
 
133
125
  const rgba2hsla = (red, green, blue, alpha) => {
@@ -179,10 +171,9 @@ const rgba2hsva = (r, g, b, a) => {
179
171
  const max = Math.max(r, g, b);
180
172
  const min = Math.min(r, g, b);
181
173
  let h = 0;
182
- let s;
183
174
  const v = max;
184
175
  const d = max - min;
185
- s = max === 0 ? 0 : d / max;
176
+ const s = max === 0 ? 0 : d / max;
186
177
  if (max !== min) {
187
178
  if (max === r) {
188
179
  h = (g - b) / d + (g < b ? 6 : 0);
@@ -231,7 +222,8 @@ const string2rgba = (rgbText) => {
231
222
 
232
223
  /**
233
224
  * Generates a unique control ID for an element.
234
- * The ID consists of a prefix, a random hexadecimal string, and a suffix.
225
+ * The ID consists of the `mona-` prefix followed by a random hexadecimal string.
226
+ * It contains only characters that are safe in CSS selectors, so it can be used with `querySelector("#…")` unescaped.
235
227
  * @param {number} [level=0] - The nesting level of the element. This increases the length of the generated ID.
236
228
  * @param {number} [length=7] - The base length of the random part of the ID.
237
229
  * @returns {string} A unique control ID string for use for elements.
@@ -245,14 +237,14 @@ const createElementControlId = (level = 0, length = 7) => {
245
237
  const randomId = Array.from(array, byte => byte.toString(16).padStart(2, "0"))
246
238
  .join("")
247
239
  .substring(0, idLength);
248
- return `${prefix}:${randomId}:`;
240
+ return `${prefix}${randomId}`;
249
241
  }
250
242
  let hex = "";
251
243
  const chars = "0123456789abcdef";
252
244
  for (let i = 0; i < idLength; i++) {
253
245
  hex += chars[Math.floor(Math.random() * 16)];
254
246
  }
255
- return `${prefix}:${hex}:`;
247
+ return `${prefix}${hex}`;
256
248
  };
257
249
 
258
250
  function focusElement(host, element) {
@@ -272,7 +264,7 @@ function focusElement(host, element) {
272
264
 
273
265
  const rxFromResize = (target, options) => {
274
266
  return new Observable(observer => {
275
- const elements = Array.isArray(target) ? target : [target];
267
+ const elements = target instanceof Element ? [target] : Array.from(target ?? []);
276
268
  for (const element of elements) {
277
269
  if (!(element instanceof Element)) {
278
270
  observer.error(new TypeError("target must be an Element or an iterable of Elements"));
@@ -348,18 +340,22 @@ const findScrollableElement = (container) => {
348
340
  return null;
349
341
  };
350
342
 
343
+ const toPixels = (value) => (value != null ? `${value}px` : "");
351
344
  const setWindowStyles = (element, settings) => {
352
345
  if (!element) {
353
346
  return;
354
347
  }
355
- const classes = isIterable(settings.windowClass) ? [...settings.windowClass] : [settings.windowClass];
356
- compact(classes).forEach(className => element.classList.add(className));
357
- const minWidth = settings.minWidth ? `${settings.minWidth}px` : "";
358
- const minHeight = settings.minHeight ? `${settings.minHeight}px` : "";
359
- const maxWidth = settings.maxWidth ? `${settings.maxWidth}px` : "";
360
- const maxHeight = settings.maxHeight ? `${settings.maxHeight}px` : "";
361
- const top = settings.top ? `${settings.top}px` : `calc(50% - ${element.getBoundingClientRect().height / 2}px)`;
362
- const left = settings.left ? `${settings.left}px` : `calc(50% - ${element.getBoundingClientRect().width / 2}px)`;
348
+ const classes = typeof settings.windowClass === "string" ? [settings.windowClass] : (settings.windowClass ?? []);
349
+ classes
350
+ .flatMap(className => (className ?? "").split(/\s+/))
351
+ .filter(className => className !== "")
352
+ .forEach(className => element.classList.add(className));
353
+ const minWidth = toPixels(settings.minWidth);
354
+ const minHeight = toPixels(settings.minHeight);
355
+ const maxWidth = toPixels(settings.maxWidth);
356
+ const maxHeight = toPixels(settings.maxHeight);
357
+ const top = settings.top != null ? `${settings.top}px` : `calc(50% - ${element.getBoundingClientRect().height / 2}px)`;
358
+ const left = settings.left != null ? `${settings.left}px` : `calc(50% - ${element.getBoundingClientRect().width / 2}px)`;
363
359
  const position = "absolute";
364
360
  Object.assign(element.style, { left, maxHeight, maxWidth, minHeight, minWidth, position, top });
365
361
  };
@@ -374,7 +370,7 @@ const setWindowStyles = (element, settings) => {
374
370
  * @returns A CSS-compliant string value.
375
371
  */
376
372
  const toCssValue = (value) => {
377
- if (value == null || value === "") {
373
+ if (value == null || (typeof value === "string" && value.trim() === "")) {
378
374
  return "";
379
375
  }
380
376
  if (typeof value === "number" || isFinite(Number(value))) {
@@ -402,8 +398,15 @@ const isNavigationKey = (key) => {
402
398
  return NavigationKeysList.includes(key);
403
399
  };
404
400
 
405
- const isTypeaheadKey = (key) => {
406
- return /^[a-zA-Z0-9\-_\s]$/.test(key);
401
+ /**
402
+ * Returns whether a `KeyboardEvent.key` value should feed typeahead.
403
+ * Any single letter or digit in any script counts, plus space, `-` and `_`.
404
+ */
405
+ const isTypeaheadKey = (key, modifiers) => {
406
+ if (modifiers?.ctrlKey || modifiers?.metaKey || modifiers?.altKey) {
407
+ return false;
408
+ }
409
+ return /^[\p{L}\p{M}\p{N}\-_\s]$/u.test(key);
407
410
  };
408
411
  const setupTypeahead = (typeaheadKey$) => {
409
412
  const resetTrigger$ = typeaheadKey$.pipe(debounceTime(200), map(() => ({ type: "reset", value: "" })));
@@ -260,6 +260,7 @@ const AR_SA_MESSAGES = {
260
260
  selectRow: (rowNumber) => `تحديد الصف ${formatInteger(rowNumber)}`
261
261
  },
262
262
  list: {
263
+ filterList: "تصفية القائمة",
263
264
  noData: "لا توجد بيانات"
264
265
  },
265
266
  listBox: {
@@ -639,6 +640,7 @@ const ES_ES_MESSAGES = {
639
640
  selectRow: (rowNumber) => `Seleccionar fila ${rowNumber}`
640
641
  },
641
642
  list: {
643
+ filterList: "Filtrar lista",
642
644
  noData: "No hay datos"
643
645
  },
644
646
  listBox: {
@@ -1018,6 +1020,7 @@ const DE_DE_MESSAGES = {
1018
1020
  selectRow: (rowNumber) => `Zeile ${rowNumber} auswählen`
1019
1021
  },
1020
1022
  list: {
1023
+ filterList: "Liste filtern",
1021
1024
  noData: "Keine Daten"
1022
1025
  },
1023
1026
  listBox: {
@@ -1397,6 +1400,7 @@ const FR_FR_MESSAGES = {
1397
1400
  selectRow: (rowNumber) => `Sélectionner la ligne ${rowNumber}`
1398
1401
  },
1399
1402
  list: {
1403
+ filterList: "Filtrer la liste",
1400
1404
  noData: "Aucune donnée"
1401
1405
  },
1402
1406
  listBox: {
@@ -1776,6 +1780,7 @@ const IT_IT_MESSAGES = {
1776
1780
  selectRow: (rowNumber) => `Seleziona riga ${rowNumber}`
1777
1781
  },
1778
1782
  list: {
1783
+ filterList: "Filtra elenco",
1779
1784
  noData: "Nessun dato disponibile"
1780
1785
  },
1781
1786
  listBox: {
@@ -2155,6 +2160,7 @@ const JA_JP_MESSAGES = {
2155
2160
  selectRow: (rowNumber) => `${rowNumber}行目を選択`
2156
2161
  },
2157
2162
  list: {
2163
+ filterList: "リストをフィルター",
2158
2164
  noData: "データがありません"
2159
2165
  },
2160
2166
  listBox: {
@@ -2534,6 +2540,7 @@ const KO_KR_MESSAGES = {
2534
2540
  selectRow: (rowNumber) => `${rowNumber}행 선택`
2535
2541
  },
2536
2542
  list: {
2543
+ filterList: "목록 필터",
2537
2544
  noData: "데이터 없음"
2538
2545
  },
2539
2546
  listBox: {
@@ -2913,6 +2920,7 @@ const PT_BR_MESSAGES = {
2913
2920
  selectRow: (rowNumber) => `Selecionar linha ${rowNumber}`
2914
2921
  },
2915
2922
  list: {
2923
+ filterList: "Filtrar lista",
2916
2924
  noData: "Nenhum dado disponível"
2917
2925
  },
2918
2926
  listBox: {
@@ -3292,6 +3300,7 @@ const ZH_CN_MESSAGES = {
3292
3300
  selectRow: (rowNumber) => `选择第 ${rowNumber} 行`
3293
3301
  },
3294
3302
  list: {
3303
+ filterList: "筛选列表",
3295
3304
  noData: "没有数据"
3296
3305
  },
3297
3306
  listBox: {
@@ -3671,6 +3680,7 @@ const ZH_TW_MESSAGES = {
3671
3680
  selectRow: (rowNumber) => `選取第 ${rowNumber} 個資料列`
3672
3681
  },
3673
3682
  list: {
3683
+ filterList: "篩選清單",
3674
3684
  noData: "沒有資料"
3675
3685
  },
3676
3686
  listBox: {
@@ -4050,6 +4060,7 @@ const TR_TR_MESSAGES = {
4050
4060
  selectRow: (rowNumber) => `${rowNumber}. satırı seç`
4051
4061
  },
4052
4062
  list: {
4063
+ filterList: "Listeyi filtrele",
4053
4064
  noData: "Veri yok"
4054
4065
  },
4055
4066
  listBox: {
@@ -4429,6 +4440,7 @@ const ID_ID_MESSAGES = {
4429
4440
  selectRow: (rowNumber) => `Pilih baris ${rowNumber}`
4430
4441
  },
4431
4442
  list: {
4443
+ filterList: "Saring daftar",
4432
4444
  noData: "Tidak ada data"
4433
4445
  },
4434
4446
  listBox: {
@@ -4837,6 +4849,7 @@ const RU_RU_MESSAGES = {
4837
4849
  selectRow: (rowNumber) => `Выбрать строку ${rowNumber}`
4838
4850
  },
4839
4851
  list: {
4852
+ filterList: "Фильтр списка",
4840
4853
  noData: "Нет данных"
4841
4854
  },
4842
4855
  listBox: {
@@ -545,7 +545,7 @@ class PopupMenuListComponent {
545
545
  this.#typeaheadKey$.next("");
546
546
  this.handleNavigationKey(e);
547
547
  }
548
- else if (isTypeaheadKey(e.key)) {
548
+ else if (isTypeaheadKey(e.key, e)) {
549
549
  e.preventDefault();
550
550
  this.handleTypeaheadKey(e);
551
551
  }
@@ -196,7 +196,7 @@ class TreeViewComponent {
196
196
  this.treeService.blur$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(e => this.blur.emit(e));
197
197
  }
198
198
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: TreeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
199
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: TreeViewComponent, isStandalone: true, selector: "mona-tree-view", inputs: { animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, hasChildren: { classPropertyName: "hasChildren", publicName: "hasChildren", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, parentIdField: { classPropertyName: "parentIdField", publicName: "parentIdField", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blur: "blur", focus: "focus", nodeClick: "nodeClick" }, providers: [TreeService], queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeViewNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (treeService.filterableOptions().enabled) {\n <div role=\"search\" [attr.aria-label]=\"filterAriaLabel()\">\n <mona-filter-input\n [filter]=\"treeService.filterText()\"\n [debounce]=\"treeService.filterableOptions().debounce\"\n [placeholder]=\"treeService.filterPlaceholder()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n<div class=\"flex-1 overflow-auto\">\n <mona-tree [ariaLabel]=\"ariaLabel()\">\n @if (nodeTemplate()) {\n <ng-template monaTreeNodeTemplate let-dataItem let-element=\"element\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate() ?? null\"\n [ngTemplateOutletContext]=\"{ $implicit: dataItem, element }\"></ng-container>\n </ng-template>\n }\n </mona-tree>\n</div>\n", dependencies: [{ kind: "component", type: FilterInputComponent, selector: "mona-filter-input", inputs: ["debounce", "filter", "placeholder", "size"], outputs: ["filterChange", "inputBlur", "inputFocus"] }, { kind: "component", type: TreeComponent, selector: "mona-tree", inputs: ["ariaLabel", "data"] }, { kind: "directive", type: TreeNodeTemplateDirective, selector: "ng-template[monaTreeNodeTemplate]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
199
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: TreeViewComponent, isStandalone: true, selector: "mona-tree-view", inputs: { animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, hasChildren: { classPropertyName: "hasChildren", publicName: "hasChildren", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, parentIdField: { classPropertyName: "parentIdField", publicName: "parentIdField", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blur: "blur", focus: "focus", nodeClick: "nodeClick" }, providers: [TreeService], queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeViewNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (treeService.filterableOptions().enabled) {\n <div role=\"search\" [attr.aria-label]=\"filterAriaLabel()\">\n <mona-filter-input\n [filter]=\"treeService.filterText()\"\n [debounce]=\"treeService.filterableOptions().debounce\"\n [placeholder]=\"treeService.filterPlaceholder()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n<div class=\"flex-1 overflow-auto\">\n <mona-tree [ariaLabel]=\"ariaLabel()\">\n @if (nodeTemplate()) {\n <ng-template monaTreeNodeTemplate let-dataItem let-element=\"element\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate() ?? null\"\n [ngTemplateOutletContext]=\"{ $implicit: dataItem, element }\"></ng-container>\n </ng-template>\n }\n </mona-tree>\n</div>\n", dependencies: [{ kind: "component", type: FilterInputComponent, selector: "mona-filter-input", inputs: ["activeDescendant", "ariaLabel", "controls", "debounce", "filter", "placeholder", "size"], outputs: ["filterChange", "inputBlur", "inputFocus"] }, { kind: "component", type: TreeComponent, selector: "mona-tree", inputs: ["ariaLabel", "data"] }, { kind: "directive", type: TreeNodeTemplateDirective, selector: "ng-template[monaTreeNodeTemplate]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
200
200
  }
201
201
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: TreeViewComponent, decorators: [{
202
202
  type: Component,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanahoshi/mona-ui",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "description": "A very light Angular UI framework",
5
5
  "author": "Nanahoshi",
6
6
  "repository": {
@@ -274,6 +274,11 @@ interface FilterableOptions {
274
274
  type: "menu" | "row" | "menu, row";
275
275
  }
276
276
 
277
+ /** The classes a {@link GridRowClass} function adds to a data row. Nullish values add nothing. */
278
+ type GridRowClassValue = string | readonly string[] | null | undefined;
279
+ /** Returns the classes to add to the data row that shows the given item. */
280
+ type GridRowClass<TData> = (data: TData) => GridRowClassValue;
281
+
277
282
  interface SortableOptions {
278
283
  allowUnsort?: boolean;
279
284
  enabled?: boolean;
@@ -898,16 +903,59 @@ interface RowEditContext {
898
903
  }
899
904
  type GridEditContext = CellEditContext | RowEditContext;
900
905
 
901
- interface GridLockedColumnState {
902
- readonly edge: boolean;
903
- readonly offset: number;
904
- readonly side: GridColumnLockedPosition;
906
+ /** A single row of the grid body — either a group header, a group footer, or a data row. */
907
+ type GridViewRow = GridViewGroupRow | GridViewGroupFooterRow | GridViewDataRow;
908
+ /** A group header row in the flattened grid view. */
909
+ interface GridViewGroupRow {
910
+ /** The number of data rows (leaf rows) contained in this group. */
911
+ readonly count: number;
912
+ /** The nesting depth (0 = top-level group). */
913
+ readonly depth: number;
914
+ /** The field this group is grouped by. */
915
+ readonly field: string;
916
+ /** A unique identifier for this group (used for collapse tracking). */
917
+ readonly groupKey: string;
918
+ /** The display value for this group header. */
919
+ readonly groupValue: unknown;
920
+ /** A key that identifies this row among all body rows, for view tracking. */
921
+ readonly key: string;
922
+ /** The display title for this group header. */
923
+ readonly title: string;
924
+ readonly type: "group";
925
+ }
926
+ /** A footer row for a grouped section in the flattened grid view. */
927
+ interface GridViewGroupFooterRow {
928
+ /** The nesting depth (0 = top-level group). */
929
+ readonly depth: number;
930
+ /** The aggregate metadata for this grouped footer. */
931
+ readonly groupAggregate: GridGroupAggregate;
932
+ /** A key that identifies this row among all body rows, for view tracking. */
933
+ readonly key: string;
934
+ readonly type: "groupFooter";
935
+ }
936
+ /** A data row in the flattened grid view. */
937
+ interface GridViewDataRow {
938
+ /** The depth of the innermost group this row belongs to, or 0 when the grid is not grouped. */
939
+ readonly depth: number;
940
+ /** The groupKey of the innermost group this row belongs to, or null when the grid is not grouped. */
941
+ readonly groupKey: string | null;
942
+ /** A key that identifies this row among all body rows, for view tracking. */
943
+ readonly key: string;
944
+ /** The actual Row model. */
945
+ readonly row: Row;
946
+ readonly type: "data";
905
947
  }
906
948
 
907
- interface PaginationState {
908
- readonly page: number;
909
- readonly skip: number;
910
- readonly take: number;
949
+ /**
950
+ * How a layout column stays pinned while the grid scrolls horizontally.
951
+ * Sides are logical, so `start` is the left edge in LTR and the right edge in RTL.
952
+ */
953
+ interface GridStickyState {
954
+ /** Whether this is the innermost pinned column on its side, which draws the separator border. */
955
+ readonly edge: boolean;
956
+ /** The distance from the pinned side, in pixels. */
957
+ readonly offset: number;
958
+ readonly side: "end" | "start";
911
959
  }
912
960
 
913
961
  type StructuralColumnKind = "detail" | "reorder" | "selection";
@@ -917,6 +965,56 @@ interface StructuralColumnDescriptor {
917
965
  readonly width: number;
918
966
  }
919
967
 
968
+ /**
969
+ * A single physical column of the grid tables, in inline-start to inline-end order.
970
+ * Every table section (header, body, add row, footer) renders the same sequence of layout columns.
971
+ */
972
+ type GridLayoutColumn = GridLayoutDataColumn | GridLayoutGroupColumn | GridLayoutStructuralColumn;
973
+ /** A user-defined data or command column. */
974
+ interface GridLayoutDataColumn {
975
+ readonly column: Column;
976
+ /** The index among the visible user columns. */
977
+ readonly columnIndex: number;
978
+ /** Whether ArrowLeft (inline-start) stops at this cell. */
979
+ readonly firstInRow: boolean;
980
+ readonly key: string;
981
+ readonly kind: "column";
982
+ /** Whether ArrowRight (inline-end) stops at this cell. */
983
+ readonly lastInRow: boolean;
984
+ /** The keyboard-navigation column index. */
985
+ readonly logicalIndex: number;
986
+ readonly sticky: GridStickyState | null;
987
+ readonly width: number | null;
988
+ }
989
+ /** An indent column rendered for each active group level. It does not participate in keyboard navigation. */
990
+ interface GridLayoutGroupColumn {
991
+ readonly depth: number;
992
+ readonly key: string;
993
+ readonly kind: "group";
994
+ readonly logicalIndex: null;
995
+ readonly sticky: GridStickyState | null;
996
+ readonly width: number;
997
+ }
998
+ /** A built-in column such as the row reorder handle, the detail toggle or the selection checkbox. */
999
+ interface GridLayoutStructuralColumn {
1000
+ /** Whether ArrowLeft (inline-start) stops at this cell. */
1001
+ readonly firstInRow: boolean;
1002
+ readonly key: string;
1003
+ readonly kind: StructuralColumnKind;
1004
+ /** Whether ArrowRight (inline-end) stops at this cell. */
1005
+ readonly lastInRow: boolean;
1006
+ /** The keyboard-navigation column index. */
1007
+ readonly logicalIndex: number;
1008
+ readonly sticky: GridStickyState | null;
1009
+ readonly width: number;
1010
+ }
1011
+
1012
+ interface PaginationState {
1013
+ readonly page: number;
1014
+ readonly skip: number;
1015
+ readonly take: number;
1016
+ }
1017
+
920
1018
  interface RowEditDataItem extends Record<string, unknown> {
921
1019
  readonly $rowId: string;
922
1020
  }
@@ -945,9 +1043,7 @@ declare class GridService {
945
1043
  readonly columns: _angular_core.WritableSignal<ImmutableList<Column>>;
946
1044
  readonly contextMenuItems: _angular_core.WritableSignal<ImmutableSet<PopupMenuItem>>;
947
1045
  readonly dataStateChange$: Subject<GridDataState>;
948
- readonly detailColumnOffset: _angular_core.Signal<number>;
949
1046
  readonly detailColumnWidth = 36;
950
- readonly detailStructuralWidth: _angular_core.Signal<36 | 0>;
951
1047
  readonly draggingRowUid: _angular_core.WritableSignal<string | null>;
952
1048
  readonly edit$: Subject<GridEditEvent>;
953
1049
  readonly editBaseDict: _angular_core.Signal<ImmutableDictionary<string, RowEditDataItem>>;
@@ -972,13 +1068,9 @@ declare class GridService {
972
1068
  readonly headerTableElement: _angular_core.WritableSignal<HTMLTableElement | null>;
973
1069
  readonly horizontalScrollLeft: _angular_core.Signal<number>;
974
1070
  readonly isInEditMode: _angular_core.Signal<boolean>;
975
- readonly lastLeftStructuralOffset: _angular_core.Signal<number | null>;
976
- readonly leadingLogicalColumnCount: _angular_core.Signal<number>;
977
- readonly leadingStructuralColumnCount: _angular_core.Signal<number>;
1071
+ readonly layout: _angular_core.Signal<readonly GridLayoutColumn[]>;
1072
+ readonly layoutColumnByKey: _angular_core.Signal<ReadonlyMap<string, GridLayoutColumn>>;
978
1073
  readonly leadingStructuralWidth: _angular_core.Signal<number>;
979
- readonly leftLockedStructuralWidth: _angular_core.Signal<number>;
980
- readonly lockedColumnStates: _angular_core.Signal<ReadonlyMap<string, GridLockedColumnState>>;
981
- readonly masterDetailEmptyCellWidth: _angular_core.Signal<number>;
982
1074
  readonly masterDetailRowWidth: _angular_core.Signal<number>;
983
1075
  readonly masterDetailTemplate: _angular_core.WritableSignal<TemplateRef<unknown> | null>;
984
1076
  readonly messages: _angular_core.Signal<_nanahoshi_mona_ui_i18n.MonaGridMessages>;
@@ -988,16 +1080,15 @@ declare class GridService {
988
1080
  readonly remove$: Subject<GridRemoveEvent>;
989
1081
  readonly reorderableOptions: _angular_core.Signal<ReorderableOptions>;
990
1082
  readonly resizableOptions: _angular_core.Signal<ResizableOptions>;
1083
+ readonly rowClass: _angular_core.WritableSignal<GridRowClass<unknown> | null>;
991
1084
  readonly rowDragInProgress: _angular_core.Signal<boolean>;
992
1085
  readonly rowEdit$: Subject<RowEditEvent>;
993
1086
  readonly rowKey: _angular_core.WritableSignal<GridKeySelector<unknown>>;
994
1087
  readonly rowReorder$: Subject<RowReorderEvent<Record<PropertyKey, unknown>>>;
995
- readonly rowReorderColumnOffset: _angular_core.Signal<number>;
996
1088
  readonly rowReorderColumnVisible: _angular_core.Signal<boolean>;
997
1089
  readonly rowReorderColumnWidth = 36;
998
1090
  readonly rowReorderDisabledReason: _angular_core.Signal<RowReorderDisabledReason>;
999
1091
  readonly rowReorderInteractionEnabled: _angular_core.Signal<boolean>;
1000
- readonly rowReorderStructuralWidth: _angular_core.Signal<36 | 0>;
1001
1092
  readonly rowReorderableOptions: _angular_core.Signal<RowReorderableOptions>;
1002
1093
  readonly rows: _angular_core.WritableSignal<ImmutableList<Row>>;
1003
1094
  readonly save$: Subject<GridSaveEvent>;
@@ -1010,18 +1101,14 @@ declare class GridService {
1010
1101
  readonly selectedKeysChange$: rxjs.Observable<ImmutableSet<unknown>>;
1011
1102
  readonly selectedKeysLoad$: BehaviorSubject<ImmutableSet<unknown>>;
1012
1103
  readonly selectedRows: _angular_core.Signal<ImmutableSet<Row>>;
1013
- readonly selectionColumnOffset: _angular_core.Signal<number>;
1014
1104
  readonly selectionColumnVisible: _angular_core.Signal<boolean>;
1015
1105
  readonly selectionColumnWidth = 40;
1016
- readonly selectionStructuralWidth: _angular_core.Signal<0 | 40>;
1017
1106
  readonly serverBindingEnabled: _angular_core.Signal<boolean>;
1018
1107
  readonly serverLoading: _angular_core.Signal<boolean>;
1019
1108
  readonly serverTotal: _angular_core.Signal<number>;
1020
1109
  readonly someBulkSelectionRowsSelected: _angular_core.Signal<boolean>;
1021
1110
  readonly sortableOptions: _angular_core.WritableSignal<SortableOptions>;
1022
- readonly structuralColIndexMap: _angular_core.Signal<Map<StructuralColumnKind, number>>;
1023
1111
  readonly structuralColumns: _angular_core.Signal<readonly StructuralColumnDescriptor[]>;
1024
- readonly structuralLastKind: _angular_core.Signal<StructuralColumnKind | null>;
1025
1112
  readonly tableWidth: _angular_core.Signal<number | null>;
1026
1113
  readonly viewPageRows: _angular_core.Signal<ImmutableList<Row>>;
1027
1114
  readonly viewRowCount: _angular_core.Signal<number>;
@@ -1041,6 +1128,11 @@ declare class GridService {
1041
1128
  clearCollapsedGroups(predicate?: (key: string) => boolean): void;
1042
1129
  clearColumnGrouping(column: Column): void;
1043
1130
  commitRowEdit(originalEvent?: Event): boolean;
1131
+ /**
1132
+ * Builds the body rows for the given data rows: plain data rows when ungrouped, otherwise the flattened
1133
+ * group headers, data rows and group footers. Reads grouping state, so call it inside a computed.
1134
+ */
1135
+ createBodyRows(rows: Iterable<Row>): GridViewRow[];
1044
1136
  deselectAllRows(): void;
1045
1137
  deselectRow(row: Row): void;
1046
1138
  findTextWidthOfColumn(column: Column, element: HTMLTableCellElement): number;
@@ -1107,7 +1199,6 @@ declare class GridService {
1107
1199
  startCellEdit(uid: string, row: Row, column: Column, originalEvent?: Event): boolean;
1108
1200
  startRowEdit(row: Row, originalEvent?: Event): boolean;
1109
1201
  stopCellEdit(originalEvent?: Event): boolean;
1110
- structuralColumnOffset(kind: StructuralColumnKind, groupPrefixWidth: number): number;
1111
1202
  toggleGroupCollapse(groupKey: string): void;
1112
1203
  updateColumn(columnId: string, patch: Partial<Omit<Column, "id">>): void;
1113
1204
  private applyColumnIndexes;
@@ -1196,7 +1287,6 @@ declare class GridComponent<T> implements GridVariantInput {
1196
1287
  protected readonly filterRowEnabled: _angular_core.Signal<boolean>;
1197
1288
  protected readonly footerClass: _angular_core.Signal<string>;
1198
1289
  protected readonly footerTableClass: _angular_core.Signal<string>;
1199
- protected readonly footerTableRowClass: _angular_core.Signal<string>;
1200
1290
  protected readonly footerVisible: _angular_core.Signal<boolean>;
1201
1291
  protected readonly gridDetailTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
1202
1292
  protected readonly gridHeaderElement: _angular_core.Signal<ElementRef<HTMLDivElement>>;
@@ -1267,6 +1357,16 @@ declare class GridComponent<T> implements GridVariantInput {
1267
1357
  * @default "medium"
1268
1358
  */
1269
1359
  readonly rounded: _angular_core.InputSignal<"small" | "none" | "medium" | "large" | null | undefined>;
1360
+ /**
1361
+ * @description Returns extra CSS classes for the data row that shows an item, merged onto the row's own
1362
+ * classes via `tailwind-merge`. It runs again whenever the item's row is rebound.
1363
+ * Group, group footer and detail rows are not affected.
1364
+ *
1365
+ * To dim a row, prefer a text style such as `text-muted-foreground` over `opacity`: locked cells stay
1366
+ * opaque so scrolled cells pass beneath them, and opacity on the row or its cells would let those show through.
1367
+ * @default null
1368
+ */
1369
+ readonly rowClass: _angular_core.InputSignal<GridRowClass<T> | null>;
1270
1370
  /**
1271
1371
  * @description Field name or selector used to derive a stable identity for each row.
1272
1372
  * The resolved value must be unique within the grid data.
@@ -1315,7 +1415,7 @@ declare class GridComponent<T> implements GridVariantInput {
1315
1415
  private setResizeObserver;
1316
1416
  private syncToolbarFocusTargets;
1317
1417
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<GridComponent<any>, never>;
1318
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<GridComponent<any>, "mona-grid", never, { "data": { "alias": "data"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageSizeValues": { "alias": "pageSizeValues"; "required": false; "isSignal": true; }; "resizeMethod": { "alias": "resizeMethod"; "required": false; "isSignal": true; }; "responsivePager": { "alias": "responsivePager"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "rowKey": { "alias": "rowKey"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, ["columnDefinitions", "gridDetailTemplate", "noDataTemplate", "toolbarTemplate"], never, true, never>;
1418
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<GridComponent<any>, "mona-grid", never, { "data": { "alias": "data"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageSizeValues": { "alias": "pageSizeValues"; "required": false; "isSignal": true; }; "resizeMethod": { "alias": "resizeMethod"; "required": false; "isSignal": true; }; "responsivePager": { "alias": "responsivePager"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "rowClass": { "alias": "rowClass"; "required": false; "isSignal": true; }; "rowKey": { "alias": "rowKey"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, ["columnDefinitions", "gridDetailTemplate", "noDataTemplate", "toolbarTemplate"], never, true, never>;
1319
1419
  }
1320
1420
 
1321
1421
  declare const GRID_DEFAULT_MESSAGES: {
@@ -1363,4 +1463,4 @@ declare const GRID_DEFAULT_MESSAGES: {
1363
1463
  };
1364
1464
 
1365
1465
  export { CellEditEvent, ColumnReorderEvent, ColumnSortEvent, GRID_DEFAULT_MESSAGES, GridAddCommandDirective, GridAddEvent, GridCancelCommandDirective, GridCancelEvent, GridCellTemplateDirective, GridColumnComponent, GridColumnTitleTemplateDirective, GridCommandColumnComponent, GridComponent, GridContextMenuDirective, GridDetailTemplateDirective, GridEditCommandDirective, GridEditEvent, GridEditTemplateDirective, GridEditableDirective, GridExportDirective, GridFilterableDirective, GridFooterTemplateDirective, GridGroupFooterTemplateDirective, GridGroupableDirective, GridHeaderTemplateDirective, GridNoDataTemplateDirective, GridRemoveCommandDirective, GridRemoveEvent, GridReorderableDirective, GridResizableDirective, GridRowReorderableDirective, GridSaveCommandDirective, GridSaveEvent, GridSelectableDirective, GridServerBindingDirective, GridSortableDirective, GridStatePersistenceDirective, GridToolbarTemplateDirective, GridVirtualScrollDirective, RowEditEvent, RowReorderEvent };
1366
- export type { AggregateFunction, Column, ColumnConfig, ColumnFormat, ColumnKind, ColumnResizeEvent, EditableOptions, FilterableOptions, GridColumnLockedPosition, GridColumnState, GridDataState, GridEditFormContext, GridEditOperation, GridEditSchemaFactory, GridEditSession, GridEditTemplateContext, GridSaveOperation, GridState, GridStateCompositeFilterDescriptor, GridStateFilterDescriptor, GridStateFilterValue, GridStateLoadResult, GridStateLoadStatus, GridStatePersistenceOptions, GridStateSortDescriptor, GridStateVersion, GroupDescriptor, GroupableOptions, ReorderableOptions, ResizableOptions, ResizeMethod, RowReorderableOptions, SelectableOptions, SortableOptions };
1466
+ export type { AggregateFunction, Column, ColumnConfig, ColumnFormat, ColumnKind, ColumnResizeEvent, EditableOptions, FilterableOptions, GridColumnLockedPosition, GridColumnState, GridDataState, GridEditFormContext, GridEditOperation, GridEditSchemaFactory, GridEditSession, GridEditTemplateContext, GridRowClass, GridRowClassValue, GridSaveOperation, GridState, GridStateCompositeFilterDescriptor, GridStateFilterDescriptor, GridStateFilterValue, GridStateLoadResult, GridStateLoadStatus, GridStatePersistenceOptions, GridStateSortDescriptor, GridStateVersion, GroupDescriptor, GroupableOptions, ReorderableOptions, ResizableOptions, ResizeMethod, RowReorderableOptions, SelectableOptions, SortableOptions };
@@ -271,6 +271,7 @@ interface MonaGridMessages {
271
271
  }
272
272
 
273
273
  interface MonaListMessages {
274
+ readonly filterList: string;
274
275
  readonly noData: string;
275
276
  }
276
277
 
@@ -7,6 +7,40 @@ declare class FilterInputComponent implements OnInit {
7
7
  #private;
8
8
  protected readonly filter$: Subject<string>;
9
9
  protected readonly filterText: _angular_core.WritableSignal<string>;
10
+ /**
11
+ * When the input controls a list, it acts as a combobox (WAI-ARIA APG): focus stays in the input
12
+ * and the highlighted option is exposed through `aria-activedescendant`.
13
+ */
14
+ protected readonly inputAttributes: _angular_core.Signal<{
15
+ "aria-label": string | null;
16
+ role?: undefined;
17
+ "aria-activedescendant"?: undefined;
18
+ "aria-autocomplete"?: undefined;
19
+ "aria-controls"?: undefined;
20
+ "aria-expanded"?: undefined;
21
+ } | {
22
+ role: string;
23
+ "aria-label": string | null;
24
+ "aria-activedescendant": string | null;
25
+ "aria-autocomplete": string;
26
+ "aria-controls": string;
27
+ "aria-expanded": boolean;
28
+ }>;
29
+ /**
30
+ * @description Id of the highlighted option in the controlled list, exposed as `aria-activedescendant`.
31
+ * @default ""
32
+ */
33
+ readonly activeDescendant: _angular_core.InputSignal<string | null>;
34
+ /**
35
+ * @description Accessible name of the input. The placeholder is not a reliable label, so set this whenever possible.
36
+ * @default ""
37
+ */
38
+ readonly ariaLabel: _angular_core.InputSignal<string>;
39
+ /**
40
+ * @description Id of the list this input filters. When set, the input is exposed as a combobox controlling it.
41
+ * @default ""
42
+ */
43
+ readonly controls: _angular_core.InputSignal<string>;
10
44
  readonly debounce: _angular_core.InputSignal<number>;
11
45
  readonly filter: _angular_core.InputSignal<string>;
12
46
  readonly filterChange: _angular_core.OutputEmitterRef<FilterChangeEvent>;
@@ -19,7 +53,7 @@ declare class FilterInputComponent implements OnInit {
19
53
  ngOnInit(): void;
20
54
  private setSubscriptions;
21
55
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<FilterInputComponent, never>;
22
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<FilterInputComponent, "mona-filter-input", never, { "debounce": { "alias": "debounce"; "required": false; "isSignal": true; }; "filter": { "alias": "filter"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; }, never, never, true, never>;
56
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FilterInputComponent, "mona-filter-input", never, { "activeDescendant": { "alias": "activeDescendant"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "controls": { "alias": "controls"; "required": false; "isSignal": true; }; "debounce": { "alias": "debounce"; "required": false; "isSignal": true; }; "filter": { "alias": "filter"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; }, never, never, true, never>;
23
57
  }
24
58
 
25
59
  export { FilterInputComponent };