@mmlogic/components 0.1.15 → 0.1.17

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 (37) hide show
  1. package/dist/cjs/{format-C-M0H0qJ.js → format-IFzg0q-6.js} +12 -0
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  4. package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +91 -26
  5. package/dist/cjs/mrd-table.cjs.entry.js +134 -15
  6. package/dist/collection/components/mrd-datetime-field/mrd-datetime-field.js +41 -7
  7. package/dist/collection/components/mrd-field/mrd-field.js +2 -2
  8. package/dist/collection/components/mrd-form/mrd-form.js +53 -20
  9. package/dist/collection/components/mrd-relation-field/mrd-relation-field.js +3 -3
  10. package/dist/collection/components/mrd-table/mrd-table.js +134 -14
  11. package/dist/collection/components/mrd-table/mrd-table.scss +88 -27
  12. package/dist/collection/dev/app.js +8 -6
  13. package/dist/collection/utils/i18n.js +12 -0
  14. package/dist/components/i18n.js +1 -1
  15. package/dist/components/mrd-datetime-field2.js +1 -1
  16. package/dist/components/mrd-form.js +1 -1
  17. package/dist/components/mrd-relation-field2.js +1 -1
  18. package/dist/components/mrd-table.js +1 -1
  19. package/dist/esm/{format-DM1S-0hy.js → format-Cc9kQ1j-.js} +12 -0
  20. package/dist/esm/loader.js +1 -1
  21. package/dist/esm/mosterdcomponents.js +1 -1
  22. package/dist/esm/mrd-boolean-field_16.entry.js +91 -26
  23. package/dist/esm/mrd-table.entry.js +134 -15
  24. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  25. package/dist/mosterdcomponents/p-829b9b6f.entry.js +1 -0
  26. package/dist/mosterdcomponents/p-Cc9kQ1j-.js +1 -0
  27. package/dist/mosterdcomponents/p-a3d8feb8.entry.js +1 -0
  28. package/dist/types/components/mrd-datetime-field/mrd-datetime-field.d.ts +5 -0
  29. package/dist/types/components/mrd-field/mrd-field.d.ts +1 -0
  30. package/dist/types/components/mrd-form/mrd-form.d.ts +9 -4
  31. package/dist/types/components/mrd-relation-field/mrd-relation-field.d.ts +1 -0
  32. package/dist/types/components/mrd-table/mrd-table.d.ts +11 -0
  33. package/dist/types/components.d.ts +6 -6
  34. package/package.json +1 -1
  35. package/dist/mosterdcomponents/p-013df6e4.entry.js +0 -1
  36. package/dist/mosterdcomponents/p-DM1S-0hy.js +0 -1
  37. package/dist/mosterdcomponents/p-bc7148e2.entry.js +0 -1
@@ -10,24 +10,51 @@ export class MrdDatetimeField {
10
10
  this.disabled = false;
11
11
  this.locale = navigator.language;
12
12
  this.error = '';
13
+ this.localValue = '';
13
14
  this.handleChange = (e) => {
14
- const val = e.target.value;
15
- if (this.required && !validateRequired(val)) {
15
+ const localVal = e.target.value;
16
+ this.localValue = localVal;
17
+ if (this.required && !validateRequired(localVal)) {
16
18
  this.error = t('required', this.locale);
17
19
  }
18
20
  else {
19
21
  this.error = '';
20
22
  }
21
- this.mrdChange.emit({ name: this.name, value: val });
23
+ this.mrdChange.emit({ name: this.name, value: this.localToUtc(localVal) });
22
24
  };
23
25
  this.handleBlur = (e) => {
24
- const val = e.target.value;
25
- this.mrdBlur.emit({ name: this.name, value: val });
26
+ const localVal = e.target.value;
27
+ this.mrdBlur.emit({ name: this.name, value: this.localToUtc(localVal) });
26
28
  };
27
29
  }
30
+ componentWillLoad() {
31
+ this.localValue = this.utcToLocal(this.value);
32
+ }
33
+ valueChanged(newVal) {
34
+ this.localValue = this.utcToLocal(newVal);
35
+ }
36
+ // UTC ISO string → "YYYY-MM-DDTHH:mm" in local timezone (for datetime-local input)
37
+ utcToLocal(utcStr) {
38
+ if (!utcStr)
39
+ return '';
40
+ const d = new Date(utcStr);
41
+ if (isNaN(d.getTime()))
42
+ return '';
43
+ const pad = (n) => String(n).padStart(2, '0');
44
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
45
+ }
46
+ // "YYYY-MM-DDTHH:mm" local time → UTC ISO string (for API)
47
+ localToUtc(localStr) {
48
+ if (!localStr)
49
+ return '';
50
+ const d = new Date(localStr);
51
+ if (isNaN(d.getTime()))
52
+ return '';
53
+ return d.toISOString().replace(/\.\d{3}Z$/, 'Z');
54
+ }
28
55
  render() {
29
56
  const hasError = !!this.error;
30
- return (h(Host, { key: '79331514eedaf208598a440423da6c154d0d516c' }, h("div", { key: 'a02a65bdef03a4243b700a68e32722a30781fa45', class: "mrd-datetime-field" }, this.label && (h("label", { key: 'adcf653253f64e8686b33211da4a5e8597b0786a', class: `mrd-datetime-field__label${this.required ? ' mrd-datetime-field__label--required' : ''}` }, this.label)), h("input", { key: '49a9fba058d2857c476d8ff038766f8b19b21a41', class: `mrd-datetime-field__input${hasError ? ' mrd-datetime-field__input--error' : ''}`, type: "datetime-local", name: this.name, value: this.value, required: this.required, disabled: this.disabled, onChange: this.handleChange, onBlur: this.handleBlur }), hasError && h("span", { key: '6d0d822b7cce825121b4abc77425442eb2556d76', class: "mrd-datetime-field__error" }, this.error))));
57
+ return (h(Host, { key: '6052b08238484bee345ae4bb9b3025c7f2474c35' }, h("div", { key: 'f9073be4496e35a7d40405ae4e6a27a5c1607626', class: "mrd-datetime-field" }, this.label && (h("label", { key: '5933c3b2dfcae9bfad378441210ada6b625657a2', class: `mrd-datetime-field__label${this.required ? ' mrd-datetime-field__label--required' : ''}` }, this.label)), h("input", { key: '7a947b104c38960d2326a4877e4ca52db9fd9d84', class: `mrd-datetime-field__input${hasError ? ' mrd-datetime-field__input--error' : ''}`, type: "datetime-local", name: this.name, value: this.localValue, required: this.required, disabled: this.disabled, onChange: this.handleChange, onBlur: this.handleBlur }), hasError && h("span", { key: 'cea3dba7e14e18a3a08394ef3027b661f1808969', class: "mrd-datetime-field__error" }, this.error))));
31
58
  }
32
59
  static get is() { return "mrd-datetime-field"; }
33
60
  static get encapsulation() { return "scoped"; }
@@ -167,7 +194,8 @@ export class MrdDatetimeField {
167
194
  }
168
195
  static get states() {
169
196
  return {
170
- "error": {}
197
+ "error": {},
198
+ "localValue": {}
171
199
  };
172
200
  }
173
201
  static get events() {
@@ -203,4 +231,10 @@ export class MrdDatetimeField {
203
231
  }
204
232
  }];
205
233
  }
234
+ static get watchers() {
235
+ return [{
236
+ "propName": "value",
237
+ "methodName": "valueChanged"
238
+ }];
239
+ }
206
240
  }
@@ -186,8 +186,8 @@ export class MrdField {
186
186
  "text": ""
187
187
  },
188
188
  "complexType": {
189
- "original": "{ query: string; relatedClass: string }",
190
- "resolved": "{ query: string; relatedClass: string; }",
189
+ "original": "{ name: string; query: string; relatedClass: string }",
190
+ "resolved": "{ name: string; query: string; relatedClass: string; }",
191
191
  "references": {}
192
192
  }
193
193
  }, {
@@ -30,6 +30,7 @@ export class MrdForm {
30
30
  this.formValues = {};
31
31
  this.errors = {};
32
32
  this.submitted = false;
33
+ this.initialValues = {};
33
34
  this.handleFieldChange = (e) => {
34
35
  const { name, value } = e.detail;
35
36
  const prevHref = this.getHref(this.formValues[name]);
@@ -80,8 +81,9 @@ export class MrdForm {
80
81
  };
81
82
  }
82
83
  componentWillLoad() {
83
- var _a;
84
- this.formValues = Object.assign({}, ((_a = this.values) !== null && _a !== void 0 ? _a : {}));
84
+ var _a, _b;
85
+ this.initialValues = Object.assign({}, ((_a = this.values) !== null && _a !== void 0 ? _a : {}));
86
+ this.formValues = Object.assign({}, ((_b = this.values) !== null && _b !== void 0 ? _b : {}));
85
87
  }
86
88
  componentDidLoad() {
87
89
  // Apply reference pre-fill and emit mrdFetchAll for dependent DROPDOWN fields.
@@ -94,6 +96,7 @@ export class MrdForm {
94
96
  /** Sync formValues when the values prop is set from outside after mount
95
97
  * (e.g. when pre-filling an existing record in edit mode). */
96
98
  valuesChanged(newValues) {
99
+ this.initialValues = Object.assign({}, (newValues !== null && newValues !== void 0 ? newValues : {}));
97
100
  this.formValues = Object.assign({}, (newValues !== null && newValues !== void 0 ? newValues : {}));
98
101
  this.applyReferenceValue();
99
102
  this.errors = {};
@@ -221,10 +224,42 @@ export class MrdForm {
221
224
  this.errors = newErrors;
222
225
  return Object.keys(newErrors).length === 0;
223
226
  }
224
- /** Build a clean submit payload containing only layout fields in API format.
225
- * - Only fields defined in the layout are included (system fields are excluded).
226
- * - Relation values stored as RelationSearchResult { id, label } are unwrapped
227
- * to just the href string (the `id`), matching what the API expects.
227
+ normalizeFieldValue(value) {
228
+ return (value === '' || value == null) ? null : value;
229
+ }
230
+ normalizeRelationValue(value) {
231
+ if (value == null || value === '')
232
+ return null;
233
+ if (typeof value === 'string')
234
+ return value || null;
235
+ if (Array.isArray(value)) {
236
+ return value.map(v => typeof v === 'object' && v !== null && 'id' in v ? v.id : String(v));
237
+ }
238
+ if (typeof value === 'object' && 'id' in value) {
239
+ return value.id || null;
240
+ }
241
+ return null;
242
+ }
243
+ deepEqual(a, b) {
244
+ if (a === b)
245
+ return true;
246
+ if (a == null && b == null)
247
+ return true;
248
+ if (a == null || b == null)
249
+ return false;
250
+ if (Array.isArray(a) && Array.isArray(b)) {
251
+ if (a.length !== b.length)
252
+ return false;
253
+ const sa = [...a].sort();
254
+ const sb = [...b].sort();
255
+ return JSON.stringify(sa) === JSON.stringify(sb);
256
+ }
257
+ return JSON.stringify(a) === JSON.stringify(b);
258
+ }
259
+ /** Build a submit payload containing only fields that changed relative to
260
+ * initialValues. For a new record (POST) initialValues is empty, so every
261
+ * non-null field is included. For edit (PATCH) only modified fields are sent,
262
+ * including fields explicitly cleared to null.
228
263
  */
229
264
  buildSubmitPayload() {
230
265
  var _a, _b;
@@ -234,23 +269,21 @@ export class MrdForm {
234
269
  if (item.type === ClientLayoutItemType.FIELD && item.field) {
235
270
  const name = item.field.name;
236
271
  const value = this.formValues[name];
237
- // Skip file/image fields that are still uploading (value is a File object)
238
272
  if (value instanceof File)
239
273
  continue;
240
- // Omit empty strings for optional fields
241
- if (value !== '')
242
- payload[name] = value !== null && value !== void 0 ? value : null;
274
+ const current = this.normalizeFieldValue(value);
275
+ const initial = this.normalizeFieldValue(this.initialValues[name]);
276
+ if (this.deepEqual(current, initial))
277
+ continue;
278
+ payload[name] = current;
243
279
  }
244
280
  else if (item.type === ClientLayoutItemType.RELATION && item.relation) {
245
281
  const name = item.relation.name;
246
- const value = this.formValues[name];
247
- if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'id' in value) {
248
- // RelationSearchResult { id: href, label } → plain href
249
- payload[name] = value.id;
250
- }
251
- else if (value !== '' && value !== undefined) {
252
- payload[name] = value !== null && value !== void 0 ? value : null;
253
- }
282
+ const current = this.normalizeRelationValue(this.formValues[name]);
283
+ const initial = this.normalizeRelationValue(this.initialValues[name]);
284
+ if (this.deepEqual(current, initial))
285
+ continue;
286
+ payload[name] = current;
254
287
  }
255
288
  }
256
289
  return payload;
@@ -473,8 +506,8 @@ export class MrdForm {
473
506
  "text": ""
474
507
  },
475
508
  "complexType": {
476
- "original": "{ query: string; relatedClass: string }",
477
- "resolved": "{ query: string; relatedClass: string; }",
509
+ "original": "{ name: string; query: string; relatedClass: string }",
510
+ "resolved": "{ name: string; query: string; relatedClass: string; }",
478
511
  "references": {}
479
512
  }
480
513
  }, {
@@ -70,7 +70,7 @@ export class MrdRelationField {
70
70
  this.isLoading = true;
71
71
  this.showResults = true;
72
72
  this.searchDebounce = setTimeout(() => {
73
- this.mrdSearch.emit({ query, relatedClass: this.mostSignificantClass });
73
+ this.mrdSearch.emit({ name: this.name, query, relatedClass: this.mostSignificantClass });
74
74
  }, 300);
75
75
  }
76
76
  else {
@@ -543,8 +543,8 @@ export class MrdRelationField {
543
543
  "text": ""
544
544
  },
545
545
  "complexType": {
546
- "original": "{ query: string; relatedClass: string }",
547
- "resolved": "{ query: string; relatedClass: string; }",
546
+ "original": "{ name: string; query: string; relatedClass: string }",
547
+ "resolved": "{ name: string; query: string; relatedClass: string; }",
548
548
  "references": {}
549
549
  }
550
550
  }, {
@@ -17,6 +17,7 @@ export class MrdTable {
17
17
  this.pendingPages = new Set();
18
18
  this.debounceTimer = null;
19
19
  this.outsideClickHandler = null;
20
+ this.keydownHandler = null;
20
21
  // ── Props ──────────────────────────────────────────────────────────────────
21
22
  this.columns = [];
22
23
  /** Direct rows (non-paginated mode, used when totalElements === 0). */
@@ -55,6 +56,8 @@ export class MrdTable {
55
56
  this.popupPos = { top: 0, left: 0 };
56
57
  /** Current scroll offset of the scroll container — drives pagination footer. */
57
58
  this.scrollTop = 0;
59
+ /** Full text shown in the TEXTBLOCK expand modal (null = closed). */
60
+ this.textblockModal = null;
58
61
  this.handleScroll = (e) => {
59
62
  const scroller = e.currentTarget;
60
63
  const scrollTop = scroller.scrollTop;
@@ -131,6 +134,10 @@ export class MrdTable {
131
134
  document.removeEventListener('click', this.outsideClickHandler);
132
135
  this.outsideClickHandler = null;
133
136
  }
137
+ if (this.keydownHandler) {
138
+ document.removeEventListener('keydown', this.keydownHandler);
139
+ this.keydownHandler = null;
140
+ }
134
141
  }
135
142
  componentDidRender() {
136
143
  if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
@@ -275,7 +282,26 @@ export class MrdTable {
275
282
  const defaultOperator = (TEXT_TYPES.has(dataType) || dataType === 'RELATION')
276
283
  ? 'startsWith'
277
284
  : undefined;
278
- this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
285
+ // For DATETIME, stored values are UTC ISO strings; convert back to local
286
+ // "YYYY-MM-DD" dates so the date inputs show what the user originally entered.
287
+ // If from and to cover the same local day it was an exact-date filter — restore
288
+ // to exact mode so the user sees the single-date input again.
289
+ if (dataType === 'DATETIME' && existing) {
290
+ const display = Object.assign({}, existing);
291
+ if (typeof display.from === 'string' && display.from)
292
+ display.from = this.utcISOToLocalDate(display.from);
293
+ if (typeof display.to === 'string' && display.to)
294
+ display.to = this.utcISOToLocalDateExclusiveEnd(display.to);
295
+ if (display.from && display.to && display.from === display.to) {
296
+ this.pendingFilter = Object.assign(Object.assign({}, display), { value: display.from, from: undefined, to: undefined });
297
+ }
298
+ else {
299
+ this.pendingFilter = display;
300
+ }
301
+ }
302
+ else {
303
+ this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
304
+ }
279
305
  this.openFilterCol = name;
280
306
  // Close on outside click — re-register to replace any stale handler
281
307
  if (this.outsideClickHandler)
@@ -295,6 +321,23 @@ export class MrdTable {
295
321
  this.outsideClickHandler = null;
296
322
  }
297
323
  }
324
+ openTextblockModal(text) {
325
+ this.textblockModal = text;
326
+ if (this.keydownHandler)
327
+ document.removeEventListener('keydown', this.keydownHandler);
328
+ this.keydownHandler = (ev) => {
329
+ if (ev.key === 'Escape')
330
+ this.closeTextblockModal();
331
+ };
332
+ document.addEventListener('keydown', this.keydownHandler);
333
+ }
334
+ closeTextblockModal() {
335
+ this.textblockModal = null;
336
+ if (this.keydownHandler) {
337
+ document.removeEventListener('keydown', this.keydownHandler);
338
+ this.keydownHandler = null;
339
+ }
340
+ }
298
341
  setPending(key, val) {
299
342
  this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { [key]: val });
300
343
  }
@@ -318,18 +361,70 @@ export class MrdTable {
318
361
  return true;
319
362
  return false;
320
363
  }
364
+ // Convert a local "YYYY-MM-DD" date string to the UTC ISO string at the
365
+ // start of that local day (midnight). new Date(y, m, d) uses local time.
366
+ dateLocalToUTCStart(dateStr) {
367
+ if (!dateStr)
368
+ return dateStr;
369
+ const [year, month, day] = dateStr.split('-').map(Number);
370
+ return new Date(year, month - 1, day).toISOString().replace(/\.\d{3}Z$/, 'Z');
371
+ }
372
+ // Start of the day AFTER the given local date (exclusive range end).
373
+ dateLocalToUTCEndExclusive(dateStr) {
374
+ if (!dateStr)
375
+ return dateStr;
376
+ const [year, month, day] = dateStr.split('-').map(Number);
377
+ return new Date(year, month - 1, day + 1).toISOString().replace(/\.\d{3}Z$/, 'Z');
378
+ }
379
+ // Convert a stored UTC ISO string back to the local "YYYY-MM-DD" date.
380
+ utcISOToLocalDate(utcStr) {
381
+ if (!utcStr)
382
+ return utcStr;
383
+ const d = new Date(utcStr);
384
+ if (isNaN(d.getTime()))
385
+ return utcStr;
386
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
387
+ }
388
+ // The stored "to" value is the exclusive end (midnight of the next day).
389
+ // Subtract one day to recover the local date the user entered.
390
+ utcISOToLocalDateExclusiveEnd(utcStr) {
391
+ if (!utcStr)
392
+ return utcStr;
393
+ const d = new Date(utcStr);
394
+ if (isNaN(d.getTime()))
395
+ return utcStr;
396
+ d.setDate(d.getDate() - 1);
397
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
398
+ }
321
399
  applyFilter() {
322
400
  const f = this.pendingFilter;
323
401
  if (!(f === null || f === void 0 ? void 0 : f.field)) {
324
402
  this.closeFilterPopup();
325
403
  return;
326
404
  }
405
+ // For DATETIME fields the user enters local dates; convert to UTC ISO strings.
406
+ // Exact date → range covering the full local day (from = midnight, to = next midnight).
407
+ // "to" is always the exclusive end (midnight of the next local day).
408
+ let normalized = Object.assign({}, f);
409
+ if (f.dataType === 'DATETIME') {
410
+ if (typeof normalized.value === 'string' && normalized.value) {
411
+ normalized.from = this.dateLocalToUTCStart(normalized.value);
412
+ normalized.to = this.dateLocalToUTCEndExclusive(normalized.value);
413
+ normalized.value = undefined;
414
+ }
415
+ else {
416
+ if (typeof normalized.from === 'string' && normalized.from)
417
+ normalized.from = this.dateLocalToUTCStart(normalized.from);
418
+ if (typeof normalized.to === 'string' && normalized.to)
419
+ normalized.to = this.dateLocalToUTCEndExclusive(normalized.to);
420
+ }
421
+ }
327
422
  const next = new Map(this.activeFilters);
328
- if (this.filterHasValue(f)) {
329
- next.set(f.field, f);
423
+ if (this.filterHasValue(normalized)) {
424
+ next.set(normalized.field, normalized);
330
425
  }
331
426
  else {
332
- next.delete(f.field);
427
+ next.delete(normalized.field);
333
428
  }
334
429
  this.activeFilters = next;
335
430
  this.closeFilterPopup();
@@ -374,7 +469,7 @@ export class MrdTable {
374
469
  }
375
470
  // ── Render: filter popup ───────────────────────────────────────────────────
376
471
  renderFilterEditor(col) {
377
- var _a, _b, _c, _d, _e, _f, _g;
472
+ var _a, _b, _c, _d, _e, _f, _g, _h;
378
473
  const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
379
474
  const dataType = this.colDataType(col);
380
475
  if (NO_FILTER_TYPES.has(dataType)) {
@@ -406,12 +501,14 @@ export class MrdTable {
406
501
  const rangeMode = pf.from !== undefined || pf.to !== undefined;
407
502
  return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "number", class: "mrd-table__filter-input", value: pf.value != null ? String(pf.value) : '', onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("span", { class: "mrd-table__filter-range-sep" }, "\u2013"), h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
408
503
  }
504
+ if (dataType === 'DATETIME') {
505
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
506
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "date", class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range mrd-table__filter-range--stacked" }, h("label", { class: "mrd-table__filter-range-label" }, t('filter_from', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("label", { class: "mrd-table__filter-range-label" }, t('filter_to', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
507
+ }
409
508
  if (DATE_TYPES.has(dataType)) {
410
- const inputType = dataType === 'DATE' ? 'date'
411
- : dataType === 'DATETIME' ? 'datetime-local'
412
- : 'time';
509
+ const inputType = dataType === 'DATE' ? 'date' : 'time';
413
510
  const rangeMode = pf.from !== undefined || pf.to !== undefined;
414
- return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
511
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_h = pf.value) !== null && _h !== void 0 ? _h : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
415
512
  }
416
513
  return null;
417
514
  }
@@ -464,6 +561,15 @@ export class MrdTable {
464
561
  this.mrdDownload.emit({ href, fileName });
465
562
  } }, h("svg", { class: "mrd-table__file-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zm-3 8l-3-3 1.41-1.41L10 14.17l4.59-4.58L16 11l-6 6z" })), t('download', this.locale))) : ''));
466
563
  }
564
+ const TEXTBLOCK_MAX = 200;
565
+ if (dataType === 'TEXTBLOCK') {
566
+ const full = CellRenderer.render(col, row, this.locale);
567
+ if (full.length <= TEXTBLOCK_MAX) {
568
+ return h("td", { class: "mrd-table__cell" }, full);
569
+ }
570
+ const preview = full.slice(0, TEXTBLOCK_MAX) + '…';
571
+ return (h("td", { class: "mrd-table__cell" }, preview, h("button", { class: "mrd-table__textblock-btn", onClick: (e) => { e.stopPropagation(); this.openTextblockModal(full); }, "aria-label": t('textblock_show_more', this.locale) }, "\u22EF")));
572
+ }
467
573
  const value = CellRenderer.render(col, row, this.locale);
468
574
  return (h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}` }, value));
469
575
  }
@@ -478,8 +584,13 @@ export class MrdTable {
478
584
  var _a, _b, _c, _d;
479
585
  const name = this.colName(col);
480
586
  const isFiltered = this.activeFilters.has(name);
481
- return (h("th", { class: `mrd-table__header${isFiltered ? ' mrd-table__header--filtered' : ''}` }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => this.handleFilterOpen(col, e) }, "\u25BE"))));
482
- }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row))))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup()));
587
+ const cls = [
588
+ 'mrd-table__header',
589
+ isFiltered ? 'mrd-table__header--filtered' : '',
590
+ this.filterMode ? 'mrd-table__header--sortable' : '',
591
+ ].filter(Boolean).join(' ');
592
+ return (h("th", { class: cls, onClick: this.filterMode ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), isFiltered && this.renderFilterIcon()));
593
+ }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row))))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup(), this.renderTextblockModal()));
483
594
  }
484
595
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
485
596
  // Derive the authoritative row count from loaded pages:
@@ -521,8 +632,16 @@ export class MrdTable {
521
632
  isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
522
633
  isFiltered ? 'mrd-table__header--filtered' : '',
523
634
  ].filter(Boolean).join(' ');
524
- return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: () => this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, isActive ? (this.sortDir === 'asc' ? '▲' : '▼') : '⇅'), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => { e.stopPropagation(); this.handleFilterOpen(col, e); } }, "\u25BE"))));
525
- }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal), this.renderFilterPopup()));
635
+ return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: (e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isFiltered && this.renderFilterIcon()));
636
+ }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal), this.renderFilterPopup(), this.renderTextblockModal()));
637
+ }
638
+ renderFilterIcon() {
639
+ return (h("span", { class: "mrd-table__filter-icon", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "currentColor" }, h("path", { d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" }))));
640
+ }
641
+ renderTextblockModal() {
642
+ if (this.textblockModal === null)
643
+ return null;
644
+ return (h("div", { class: "mrd-table__modal-backdrop", onClick: () => this.closeTextblockModal(), role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: () => this.closeTextblockModal(), "aria-label": t('close', this.locale) }, "\u2715"), h("p", { class: "mrd-table__modal-text" }, this.textblockModal))));
526
645
  }
527
646
  static get is() { return "mrd-table"; }
528
647
  static get encapsulation() { return "scoped"; }
@@ -747,7 +866,8 @@ export class MrdTable {
747
866
  "openFilterCol": {},
748
867
  "pendingFilter": {},
749
868
  "popupPos": {},
750
- "scrollTop": {}
869
+ "scrollTop": {},
870
+ "textblockModal": {}
751
871
  };
752
872
  }
753
873
  static get events() {
@@ -73,7 +73,7 @@
73
73
  }
74
74
 
75
75
  .mrd-table__sort-icon {
76
- font-size: 0.65rem;
76
+ font-size: 0.85rem;
77
77
  opacity: 0.4;
78
78
  vertical-align: middle;
79
79
  }
@@ -84,6 +84,14 @@
84
84
  color: var(--mrd-color-primary);
85
85
  }
86
86
 
87
+ .mrd-table__filter-icon {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ vertical-align: middle;
91
+ margin-left: var(--mrd-space-1);
92
+ color: var(--mrd-color-primary);
93
+ }
94
+
87
95
  /* ── Data rows ─────────────────────────────────────────────────────────── */
88
96
  /* Background is set via inline style based on absolute row index,
89
97
  so zebra striping stays correct even with virtual spacer rows. */
@@ -273,37 +281,12 @@
273
281
  pointer-events: none;
274
282
  }
275
283
 
276
- /* ── Header: filtered indicator + filter button ─────────────────────────── */
284
+ /* ── Header: filtered indicator ─────────────────────────────────────────── */
277
285
  .mrd-table__header--filtered {
278
286
  color: var(--mrd-color-primary);
279
287
  border-bottom-color: var(--mrd-color-primary);
280
288
  }
281
289
 
282
- .mrd-table__header-filter-btn {
283
- display: inline-flex;
284
- align-items: center;
285
- justify-content: center;
286
- margin-left: var(--mrd-space-1);
287
- padding: 0 3px;
288
- background: transparent;
289
- border: none;
290
- border-radius: 3px;
291
- cursor: pointer;
292
- color: var(--mrd-color-neutral-500);
293
- font-size: 0.8rem;
294
- line-height: 1;
295
- vertical-align: middle;
296
- }
297
-
298
- .mrd-table__header-filter-btn:hover {
299
- background: var(--mrd-color-neutral-200);
300
- color: var(--mrd-color-neutral-800);
301
- }
302
-
303
- .mrd-table__header-filter-btn--active {
304
- color: var(--mrd-color-primary);
305
- }
306
-
307
290
  /* ── Filter popup ───────────────────────────────────────────────────────── */
308
291
  .mrd-table__filter-popup {
309
292
  position: fixed;
@@ -433,6 +416,18 @@
433
416
  flex-shrink: 0;
434
417
  }
435
418
 
419
+ .mrd-table__filter-range--stacked {
420
+ flex-direction: column;
421
+ align-items: stretch;
422
+ gap: var(--mrd-space-2);
423
+ }
424
+
425
+ .mrd-table__filter-range-label {
426
+ font-size: var(--mrd-font-size-xs);
427
+ color: var(--mrd-color-neutral-500);
428
+ margin-bottom: 2px;
429
+ }
430
+
436
431
  /* Radio group */
437
432
  .mrd-table__filter-radio-group {
438
433
  display: flex;
@@ -576,3 +571,69 @@
576
571
  height: 1rem;
577
572
  }
578
573
 
574
+ /* ── TEXTBLOCK expand button ────────────────────────────────────────────── */
575
+ .mrd-table__textblock-btn {
576
+ display: inline;
577
+ background: none;
578
+ border: none;
579
+ padding: 0 0 0 var(--mrd-space-1);
580
+ cursor: pointer;
581
+ color: var(--mrd-color-primary);
582
+ font-size: var(--mrd-font-size-sm);
583
+ font-family: inherit;
584
+ line-height: inherit;
585
+ vertical-align: middle;
586
+ }
587
+
588
+ .mrd-table__textblock-btn:hover {
589
+ color: var(--mrd-color-primary-dark);
590
+ }
591
+
592
+ /* ── TEXTBLOCK modal ────────────────────────────────────────────────────── */
593
+ .mrd-table__modal-backdrop {
594
+ position: fixed;
595
+ inset: 0;
596
+ background: rgba(0, 0, 0, 0.4);
597
+ z-index: var(--mrd-z-modal, 300);
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ }
602
+
603
+ .mrd-table__modal {
604
+ background: #fff;
605
+ border-radius: var(--mrd-radius-md, 0.5rem);
606
+ padding: var(--mrd-space-6);
607
+ max-width: min(600px, 90vw);
608
+ max-height: 70vh;
609
+ overflow-y: auto;
610
+ position: relative;
611
+ box-shadow: var(--mrd-shadow-lg);
612
+ }
613
+
614
+ .mrd-table__modal-close {
615
+ position: absolute;
616
+ top: var(--mrd-space-3);
617
+ right: var(--mrd-space-3);
618
+ background: none;
619
+ border: none;
620
+ cursor: pointer;
621
+ font-size: 1.25rem;
622
+ line-height: 1;
623
+ color: var(--mrd-color-text-muted, #6b7280);
624
+ padding: 0;
625
+ }
626
+
627
+ .mrd-table__modal-close:hover {
628
+ color: var(--mrd-color-text, #111827);
629
+ }
630
+
631
+ .mrd-table__modal-text {
632
+ margin: 0;
633
+ padding-right: var(--mrd-space-6);
634
+ white-space: pre-wrap;
635
+ word-break: break-word;
636
+ font-size: var(--mrd-font-size-sm);
637
+ line-height: 1.6;
638
+ }
639
+