@mmlogic/components 0.5.3 → 0.5.5

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 (85) hide show
  1. package/dist/cjs/{cell-renderer-7KX9Qbml.js → cell-renderer-Mx3pwONr.js} +261 -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_25.cjs.entry.js +391 -77
  5. package/dist/cjs/mrd-merge-view.cjs.entry.js +3 -3
  6. package/dist/collection/components/documents/mrd-document-container/mrd-document-container.css +3 -0
  7. package/dist/collection/components/documents/mrd-document-container/mrd-document-container.js +23 -3
  8. package/dist/collection/components/documents/mrd-document-list/mrd-document-list.js +76 -2
  9. package/dist/collection/components/fields/mrd-currency-field/mrd-currency-field.js +56 -5
  10. package/dist/collection/components/fields/mrd-date-field/mrd-date-field.css +55 -4
  11. package/dist/collection/components/fields/mrd-date-field/mrd-date-field.js +118 -8
  12. package/dist/collection/components/fields/mrd-datetime-field/mrd-datetime-field.css +55 -4
  13. package/dist/collection/components/fields/mrd-datetime-field/mrd-datetime-field.js +117 -15
  14. package/dist/collection/components/fields/mrd-email-field/mrd-email-field.js +1 -1
  15. package/dist/collection/components/fields/mrd-file-field/mrd-file-field.js +1 -1
  16. package/dist/collection/components/fields/mrd-image-field/mrd-image-field.js +1 -1
  17. package/dist/collection/components/fields/mrd-longtext-field/mrd-longtext-field.js +1 -1
  18. package/dist/collection/components/fields/mrd-number-field/mrd-number-field.js +33 -8
  19. package/dist/collection/components/fields/mrd-relation-field/mrd-relation-field.js +13 -3
  20. package/dist/collection/components/fields/mrd-secret-field/mrd-secret-field.js +2 -2
  21. package/dist/collection/components/fields/mrd-text-field/mrd-text-field.js +1 -1
  22. package/dist/collection/components/fields/mrd-textarea-field/mrd-textarea-field.js +1 -1
  23. package/dist/collection/components/fields/mrd-time-field/mrd-time-field.js +1 -1
  24. package/dist/collection/components/form/mrd-field/mrd-field.js +11 -11
  25. package/dist/collection/components/form/mrd-form/mrd-form.js +8 -4
  26. package/dist/collection/components/invoice/mrd-invoice-view/mrd-invoice-view.js +1 -1
  27. package/dist/collection/components/layout/mrd-layout-section/field-value.js +4 -2
  28. package/dist/collection/components/layout/mrd-layout-section/layout-helpers.js +6 -0
  29. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.css +4 -0
  30. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.js +8 -8
  31. package/dist/collection/components/merge/mrd-merge-view/mrd-merge-view.js +2 -2
  32. package/dist/collection/dev/app.js +58 -13
  33. package/dist/collection/dev/example-data.js +6 -1
  34. package/dist/collection/utils/format.js +160 -0
  35. package/dist/collection/utils/i18n.js +96 -0
  36. package/dist/components/cell-renderer.js +1 -1
  37. package/dist/components/format.js +1 -1
  38. package/dist/components/i18n.js +1 -1
  39. package/dist/components/mrd-currency-field2.js +1 -1
  40. package/dist/components/mrd-date-field2.js +1 -1
  41. package/dist/components/mrd-datetime-field2.js +1 -1
  42. package/dist/components/mrd-document-container2.js +1 -1
  43. package/dist/components/mrd-document-list2.js +1 -1
  44. package/dist/components/mrd-document-view2.js +1 -1
  45. package/dist/components/mrd-email-field2.js +1 -1
  46. package/dist/components/mrd-email-view2.js +1 -1
  47. package/dist/components/mrd-field2.js +1 -1
  48. package/dist/components/mrd-file-field2.js +1 -1
  49. package/dist/components/mrd-form.js +1 -1
  50. package/dist/components/mrd-image-field2.js +1 -1
  51. package/dist/components/mrd-invoice-view2.js +1 -1
  52. package/dist/components/mrd-layout-section.js +1 -1
  53. package/dist/components/mrd-longtext-field2.js +1 -1
  54. package/dist/components/mrd-merge-view.js +1 -1
  55. package/dist/components/mrd-number-field2.js +1 -1
  56. package/dist/components/mrd-relation-field2.js +1 -1
  57. package/dist/components/mrd-secret-field2.js +1 -1
  58. package/dist/components/mrd-table2.js +1 -1
  59. package/dist/components/mrd-text-field2.js +1 -1
  60. package/dist/components/mrd-textarea-field2.js +1 -1
  61. package/dist/components/mrd-time-field2.js +1 -1
  62. package/dist/esm/{cell-renderer-DIsNX9D7.js → cell-renderer-UQOYlDv7.js} +257 -1
  63. package/dist/esm/loader.js +1 -1
  64. package/dist/esm/mosterdcomponents.js +1 -1
  65. package/dist/esm/mrd-boolean-field_25.entry.js +391 -77
  66. package/dist/esm/mrd-merge-view.entry.js +3 -3
  67. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  68. package/dist/mosterdcomponents/p-8cc374e0.entry.js +3 -0
  69. package/dist/mosterdcomponents/p-CF--ScWr.js +1 -0
  70. package/dist/mosterdcomponents/{p-4e061416.entry.js → p-d31b71d9.entry.js} +1 -1
  71. package/dist/types/components/documents/mrd-document-container/mrd-document-container.d.ts +7 -0
  72. package/dist/types/components/documents/mrd-document-list/mrd-document-list.d.ts +17 -0
  73. package/dist/types/components/fields/mrd-currency-field/mrd-currency-field.d.ts +13 -0
  74. package/dist/types/components/fields/mrd-date-field/mrd-date-field.d.ts +22 -1
  75. package/dist/types/components/fields/mrd-datetime-field/mrd-datetime-field.d.ts +21 -2
  76. package/dist/types/components/fields/mrd-number-field/mrd-number-field.d.ts +6 -0
  77. package/dist/types/components/fields/mrd-relation-field/mrd-relation-field.d.ts +7 -0
  78. package/dist/types/components/form/mrd-form/mrd-form.d.ts +3 -2
  79. package/dist/types/components/layout/mrd-layout-section/layout-helpers.d.ts +3 -0
  80. package/dist/types/components.d.ts +35 -0
  81. package/dist/types/types/client-layout.d.ts +7 -0
  82. package/dist/types/utils/format.d.ts +34 -0
  83. package/package.json +1 -1
  84. package/dist/mosterdcomponents/p-910ebfee.entry.js +0 -3
  85. package/dist/mosterdcomponents/p-Ce2QDq0t.js +0 -1
@@ -3,6 +3,7 @@ import { ClientLayoutItemFieldDataType } from "../../../types/client-layout";
3
3
  import { CellRenderer } from "../../../utils/cell-renderer";
4
4
  import { t } from "../../../utils/i18n";
5
5
  import { formatDate } from "../../../utils/format";
6
+ import { showsEmpty } from "./layout-helpers";
6
7
  /** FILE and IMAGE cells share the same download button. */
7
8
  function downloadLink(ctx, href, fileName, label) {
8
9
  return (h("button", { class: "mrd-layout-section__download-link", onClick: () => href && ctx.on.download({ href, fileName }) }, h("svg", { class: "mrd-layout-section__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" })), label));
@@ -106,10 +107,11 @@ export function renderField(ctx, item) {
106
107
  if (item.header) {
107
108
  return (h("h1", { class: "mrd-layout-section__field-header", key: item.name }, typeof renderedValue === 'string' ? renderedValue : rawValue != null ? String(rawValue) : item.label));
108
109
  }
109
- if (renderedValue == null)
110
+ const isEmpty = renderedValue == null;
111
+ if (isEmpty && !showsEmpty(item))
110
112
  return null;
111
113
  const isBlock = item.dataType === ClientLayoutItemFieldDataType.TEXTBLOCK
112
114
  || item.dataType === ClientLayoutItemFieldDataType.LONGTEXT
113
115
  || item.dataType === ClientLayoutItemFieldDataType.JSON;
114
- return (h("div", { class: `mrd-layout-section__field${isBlock ? ' mrd-layout-section__field--block' : ''}`, key: item.name }, h("span", { class: "mrd-layout-section__field-label" }, item.label), h("span", { class: "mrd-layout-section__field-value" }, renderedValue, renderHistoryBadge(ctx, item, raw))));
116
+ return (h("div", { class: `mrd-layout-section__field${isBlock ? ' mrd-layout-section__field--block' : ''}${isEmpty ? ' mrd-layout-section__field--empty' : ''}`, key: item.name }, h("span", { class: "mrd-layout-section__field-label" }, item.label), h("span", { class: "mrd-layout-section__field-value" }, isEmpty ? t('value_empty', ctx.locale) : renderedValue, renderHistoryBadge(ctx, item, raw))));
115
117
  }
@@ -6,6 +6,12 @@ export function viewKeyFor(item) {
6
6
  return (_b = (_a = item.relatedClass) !== null && _a !== void 0 ? _a : item.name) !== null && _b !== void 0 ? _b : '';
7
7
  return (_d = (_c = item.dataClass) !== null && _c !== void 0 ? _c : item.name) !== null && _d !== void 0 ? _d : '';
8
8
  }
9
+ /** Whether a FIELD/RELATION item stays visible on an OBJECT_DASHBOARD when it
10
+ * has no value. Reads the flat API flag, falling back to the nested wrappers. */
11
+ export function showsEmpty(item) {
12
+ var _a, _b, _c, _d, _e;
13
+ return (_e = (_c = (_a = item.showEmpty) !== null && _a !== void 0 ? _a : (_b = item.field) === null || _b === void 0 ? void 0 : _b.showEmpty) !== null && _c !== void 0 ? _c : (_d = item.relation) === null || _d === void 0 ? void 0 : _d.showEmpty) !== null && _e !== void 0 ? _e : false;
14
+ }
9
15
  /** Depth-first flatten of the SECTION/GROUP tree. */
10
16
  export function flattenItems(items) {
11
17
  const result = [];
@@ -33,6 +33,10 @@
33
33
  align-items: center;
34
34
  gap: var(--mrd-space-1);
35
35
  }
36
+ .mrd-layout-section__field--empty .mrd-layout-section__field-value {
37
+ color: var(--mrd-color-neutral-400);
38
+ font-weight: var(--mrd-font-weight-normal);
39
+ }
36
40
  .mrd-layout-section__history-wrap {
37
41
  position: relative;
38
42
  display: inline-flex;
@@ -1,6 +1,7 @@
1
1
  import { Host, h } from "@stencil/core";
2
2
  import { ClientLayoutItemType, ClientLayoutItemFieldDataType, ClientLayoutNavigateBehaviour, resolveArchetype, ARCHETYPE_DOCUMENT, ARCHETYPE_INVOICE, ARCHETYPE_EMAIL, ARCHETYPE_INVOICE_LINE, } from "../../../types/client-layout";
3
- import { viewKeyFor, flattenItems, buildContainerHref } from "./layout-helpers";
3
+ import { t } from "../../../utils/i18n";
4
+ import { viewKeyFor, flattenItems, buildContainerHref, showsEmpty } from "./layout-helpers";
4
5
  import { renderField } from "./field-value";
5
6
  export class MrdLayoutSection {
6
7
  constructor() {
@@ -173,19 +174,18 @@ export class MrdLayoutSection {
173
174
  return null;
174
175
  const links = ((_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a._links) !== null && _b !== void 0 ? _b : {});
175
176
  const link = links[item.name];
176
- if (!link)
177
- return null;
178
177
  const makeBtn = (href, name) => (h("button", { key: href, class: "mrd-layout-section__relation-link", onClick: () => this.mrdNavigate.emit({ href, label: name }) }, name));
179
178
  let valueContent;
180
- if ((_c = link.values) === null || _c === void 0 ? void 0 : _c.length) {
179
+ if ((_c = link === null || link === void 0 ? void 0 : link.values) === null || _c === void 0 ? void 0 : _c.length) {
181
180
  valueContent = link.values.map(v => makeBtn(v.href, v.name));
182
181
  }
183
- else if (link.name) {
182
+ else if (link === null || link === void 0 ? void 0 : link.name) {
184
183
  valueContent = makeBtn(link.href, link.name);
185
184
  }
186
- if (!valueContent)
185
+ const isEmpty = !valueContent;
186
+ if (isEmpty && !showsEmpty(item))
187
187
  return null;
188
- return (h("div", { class: "mrd-layout-section__field", key: item.name }, h("span", { class: "mrd-layout-section__field-label" }, item.label), h("span", { class: "mrd-layout-section__field-value" }, valueContent)));
188
+ return (h("div", { class: `mrd-layout-section__field${isEmpty ? ' mrd-layout-section__field--empty' : ''}`, key: item.name }, h("span", { class: "mrd-layout-section__field-label" }, item.label), h("span", { class: "mrd-layout-section__field-value" }, isEmpty ? t('value_empty', this.locale) : valueContent)));
189
189
  }
190
190
  renderSearch(item) {
191
191
  var _a, _b, _c;
@@ -359,7 +359,7 @@ export class MrdLayoutSection {
359
359
  const docArchetype = resolveArchetype(this.archetypes, ARCHETYPE_DOCUMENT);
360
360
  const invoiceArchetype = !docArchetype ? resolveArchetype(this.archetypes, ARCHETYPE_INVOICE) : undefined;
361
361
  const emailArchetype = !docArchetype && !invoiceArchetype ? resolveArchetype(this.archetypes, ARCHETYPE_EMAIL) : undefined;
362
- return (h(Host, { key: '0078764a136e7ca9561632718c1b527ec3bc01c5' }, h("div", { key: '0c1428ce20bd7d2bbce467a36b6c25ffa0e6367e', class: "mrd-layout-section" }, docArchetype && this.renderDocumentObject(docArchetype), invoiceArchetype && this.renderInvoiceObject(invoiceArchetype), emailArchetype && this.renderEmailObject(emailArchetype), !docArchetype && !invoiceArchetype && !emailArchetype && this.items.map(item => this.renderItem(item))), this.renderImageModal()));
362
+ return (h(Host, { key: '8c4e0a0e71d777295010702584b4aa8f7817cafb' }, h("div", { key: 'fe021021eba48acad3d52ee4849ebe355e83c348', class: "mrd-layout-section" }, docArchetype && this.renderDocumentObject(docArchetype), invoiceArchetype && this.renderInvoiceObject(invoiceArchetype), emailArchetype && this.renderEmailObject(emailArchetype), !docArchetype && !invoiceArchetype && !emailArchetype && this.items.map(item => this.renderItem(item))), this.renderImageModal()));
363
363
  }
364
364
  static get is() { return "mrd-layout-section"; }
365
365
  static get encapsulation() { return "scoped"; }
@@ -121,14 +121,14 @@ export class MrdMergeView {
121
121
  var _a, _b;
122
122
  let stepContent;
123
123
  if (this.step === 'search') {
124
- stepContent = (h(MergeSearchStep, { key: '3adb36fb46aa7260641a46a2cf27cc0ef320876f', locale: this.locale, searchQuery: this.searchQuery, searchResults: this.searchResults, isLoading: this.isLoading, onSearchInput: this.handleSearchInput, onSelect: this.handleSelect }));
124
+ stepContent = (h(MergeSearchStep, { key: 'b965c21176faf7844e58cb156958fa1305d07cdc', locale: this.locale, searchQuery: this.searchQuery, searchResults: this.searchResults, isLoading: this.isLoading, onSearchInput: this.handleSearchInput, onSelect: this.handleSelect }));
125
125
  }
126
126
  else {
127
127
  const diff = diffFields(this.fields, this.currentRecord, this.candidate);
128
128
  const bLabel = candidateLabel(this.candidate);
129
129
  stepContent = this.step === 'compare' ? (h(MergeCompareStep, { locale: this.locale, currentLabel: this.currentLabel, candidateLabel: bLabel, conflicts: diff.conflicts, autoFilled: diff.autoFilled, recordA: this.currentRecord, recordB: (_a = this.candidate) !== null && _a !== void 0 ? _a : {}, resolutions: this.resolutions, openHistoryKey: this.openHistoryKey, onResolve: this.handleResolve, onToggleHistory: this.handleToggleHistory, onChangeCandidate: this.handleChangeCandidate, onContinue: this.handleContinue })) : (h(MergeConfirmStep, { locale: this.locale, candidateLabel: bLabel, conflicts: diff.conflicts, autoFilled: diff.autoFilled, resolutions: this.resolutions, recordB: (_b = this.candidate) !== null && _b !== void 0 ? _b : {}, openHistoryKey: this.openHistoryKey, onToggleHistory: this.handleToggleHistory, onBack: this.handleBack, onConfirm: this.handleConfirm }));
130
130
  }
131
- return (h(Host, { key: 'e33a6d66a220b28cc9533da78dc274e2d7ec5c4a' }, h("div", { key: 'b5fb1390d86192f9f6f15e3ccb89e2d21d9c7c8b', class: "mrd-merge-view" }, stepContent)));
131
+ return (h(Host, { key: 'e9cffda6c5cff51f802b044f3d8e417df83b7846' }, h("div", { key: '26690ef2db7489cbf7994191ad19491a0fdd7002', class: "mrd-merge-view" }, stepContent)));
132
132
  }
133
133
  static get is() { return "mrd-merge-view"; }
134
134
  static get encapsulation() { return "scoped"; }
@@ -134,7 +134,7 @@ async function initDetailViewTab() {
134
134
  table.item = window.EXAMPLE_COMPANY_VIEW_ITEM;
135
135
  table.totalElements = window.EXAMPLE_COMPANIES.length;
136
136
  table.pageSize = 20;
137
- table.locale = 'nl-NL';
137
+ table.locale = _locale;
138
138
 
139
139
  // Register before init(): init() emits mrdLoadPage for page 0 itself.
140
140
  table.addEventListener('mrdLoadPage', async (e) => {
@@ -174,7 +174,7 @@ function showCompanyDetail(row) {
174
174
  const section = document.createElement('mrd-layout-section');
175
175
  section.items = layout.items;
176
176
  section.data = company;
177
- section.locale = 'nl-NL';
177
+ section.locale = _locale;
178
178
 
179
179
  section.addEventListener('mrdNavigate', (e) => {
180
180
  logEvent('mrdNavigate', e.detail);
@@ -1136,16 +1136,57 @@ customElements.whenDefined('mrd-form').then(() => {
1136
1136
  form.addEventListener('mrdFetchAll', (e) => { console.log('[mrdFetchAll]', e.detail); logEvent('mrdFetchAll', e.detail); });
1137
1137
  });
1138
1138
 
1139
- document.getElementById('locale-select').addEventListener('change', (e) => {
1140
- _locale = e.target.value;
1141
- const demoForm = document.getElementById('demo-form');
1142
- if (demoForm) demoForm.locale = _locale;
1143
- const liveForm = document.getElementById('live-form');
1144
- if (liveForm) liveForm.locale = _locale;
1145
- // Update actieve section als aanwezig
1146
- const section = document.querySelector('#sections-container mrd-layout-section');
1147
- if (section) section.locale = _locale;
1148
- });
1139
+ /* =====================================================================
1140
+ LOCALE CONTROL
1141
+ ===================================================================== */
1142
+
1143
+ /** Every component that takes a `locale` prop — the control drives all of them. */
1144
+ const LOCALE_HOSTS = [
1145
+ 'mrd-form', 'mrd-layout-section', 'mrd-table', 'mrd-merge-view',
1146
+ 'mrd-document-container', 'mrd-document-list', 'mrd-document-view',
1147
+ 'mrd-invoice-view', 'mrd-email-view',
1148
+ ].join(', ');
1149
+
1150
+ /** Single source of truth: push the selected locale into everything on the page. */
1151
+ function applyLocale(locale) {
1152
+ _locale = locale;
1153
+ document.querySelectorAll(LOCALE_HOSTS).forEach((el) => { el.locale = locale; });
1154
+ renderLocaleHint();
1155
+ }
1156
+
1157
+ /**
1158
+ * Shows what the selected locale does next to what the browser would do on its
1159
+ * own. Those two differ exactly where the `locale` prop earns its keep — the
1160
+ * date fields format by prop, a native date input would follow the browser.
1161
+ */
1162
+ function renderLocaleHint() {
1163
+ const hint = document.getElementById('locale-hint');
1164
+ if (!hint) return;
1165
+ const sample = new Date(2026, 6, 6, 14, 30); // 6 July — day > 12 keeps d/m order readable
1166
+ const format = (loc) => new Intl.DateTimeFormat(loc, {
1167
+ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
1168
+ }).format(sample);
1169
+
1170
+ hint.innerHTML =
1171
+ `<span>prop <code>${escHtml(_locale)}</code> → <b>${escHtml(format(_locale))}</b></span>` +
1172
+ `<span>browser <code>${escHtml(navigator.language)}</code> → <b>${escHtml(format(navigator.language))}</b></span>`;
1173
+ }
1174
+
1175
+ function initLocaleControl() {
1176
+ const select = document.getElementById('locale-select');
1177
+ // The browser locale is worth testing too, even when it is not in the list.
1178
+ if (![...select.options].some((o) => o.value === _locale)) {
1179
+ const option = document.createElement('option');
1180
+ option.value = _locale;
1181
+ option.textContent = `🖥️ Browser (${_locale})`;
1182
+ select.insertBefore(option, select.firstChild);
1183
+ }
1184
+ select.value = _locale;
1185
+ select.addEventListener('change', (e) => applyLocale(e.target.value));
1186
+ applyLocale(_locale);
1187
+ }
1188
+
1189
+ initLocaleControl();
1149
1190
 
1150
1191
  document.getElementById('btn-inject-results').addEventListener('click', () => {
1151
1192
  const relationField = document.querySelector('mrd-relation-field');
@@ -1170,7 +1211,7 @@ document.addEventListener('DOMContentLoaded', async () => {
1170
1211
  const ftSection = document.getElementById('field-types-section');
1171
1212
  ftSection.items = window.EXAMPLE_FIELD_TYPES_ITEMS;
1172
1213
  ftSection.data = window.EXAMPLE_FIELD_TYPES_DATA;
1173
- ftSection.locale = 'nl-NL';
1214
+ ftSection.locale = _locale;
1174
1215
 
1175
1216
  // Simulate host resolving a signed URL for IMAGE fields (thumbnail on load).
1176
1217
  ftSection.addEventListener('mrdLoadImage', (e) => {
@@ -1197,6 +1238,10 @@ document.addEventListener('DOMContentLoaded', async () => {
1197
1238
  ftSection.setSearchResults(mock, dataClass);
1198
1239
  });
1199
1240
 
1241
+ // Re-apply once the whole page is parsed, so components mounted in markup
1242
+ // (and any upgraded after the control ran) all agree on the locale.
1243
+ applyLocale(_locale);
1244
+
1200
1245
  authRestoreSession();
1201
1246
  await authHandleCallback();
1202
1247
 
@@ -223,7 +223,9 @@ window.EXAMPLE_COMPANY_METADATA = {
223
223
  { type: 'FIELD', label: 'legal form extended', name: 'legalFormExtended', dataType: 'TEXT', required: false },
224
224
  { type: 'FIELD', label: 'rsin', name: 'rsin', dataType: 'TEXT', required: false },
225
225
  { type: 'FIELD', label: 'start date', name: 'startDate', dataType: 'DATE', required: false },
226
- { type: 'FIELD', label: 'end date', name: 'endDate', dataType: 'DATE', required: false },
226
+ // showEmpty demo (TASK-0231): endDate is null on EXAMPLE_COMPANY, so it
227
+ // stays visible with a placeholder instead of being hidden.
228
+ { type: 'FIELD', label: 'end date', name: 'endDate', dataType: 'DATE', required: false, showEmpty: true },
227
229
  { type: 'FIELD', label: 'primary activity description', name: 'primaryActivityDescription', dataType: 'TEXT', required: false },
228
230
  { type: 'FIELD', label: 'primary activity code', name: 'primaryActivityCode', dataType: 'TEXT', required: false },
229
231
  { type: 'FIELD', label: 'total employees', name: 'totalEmployees', dataType: 'INTEGER', required: false },
@@ -243,6 +245,9 @@ window.EXAMPLE_COMPANY_METADATA = {
243
245
  { type: 'FIELD', label: 'mailing postal code', name: 'mailingPostalCode', dataType: 'TEXT', required: false },
244
246
  { type: 'FIELD', label: 'mailing city', name: 'mailingCity', dataType: 'TEXT', required: false },
245
247
  { type: 'RELATION', label: 'country', name: 'country', relatedClass: 'commons.country', mostSignificantClass: 'countries', required: false },
248
+ // showEmpty demo (TASK-0231): no `parentCompany` link on EXAMPLE_COMPANY,
249
+ // so the relation still renders with a placeholder value.
250
+ { type: 'RELATION', label: 'parent company', name: 'parentCompany', relatedClass: 'commons.company', mostSignificantClass: 'companies', required: false, showEmpty: true },
246
251
  ],
247
252
  },
248
253
  {
@@ -53,6 +53,166 @@ export function formatTime(value, locale) {
53
53
  date.setHours(hours, minutes, 0, 0);
54
54
  return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(date);
55
55
  }
56
+ /**
57
+ * Locale-aware date input support.
58
+ *
59
+ * A native <input type="date"> always displays in the browser/OS locale, so the
60
+ * date fields render a plain text input instead and use these helpers to format
61
+ * and parse the value in the locale the host passed down — the same approach
62
+ * mrd-number-field takes with formatNumber / parseLocalizedNumber.
63
+ */
64
+ /** 31 December 2026, 13:45 — day > 12 so day/month order is unambiguous. */
65
+ const PATTERN_SAMPLE = new Date(2026, 11, 31, 13, 45, 0);
66
+ function dateTimeParts(locale, withTime) {
67
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
68
+ if (withTime) {
69
+ options.hour = '2-digit';
70
+ options.minute = '2-digit';
71
+ }
72
+ return new Intl.DateTimeFormat(locale, options).formatToParts(PATTERN_SAMPLE);
73
+ }
74
+ /** Order in which day/month/year appear for a locale, e.g. ['day','month','year'] for nl-NL. */
75
+ function datePartOrder(locale) {
76
+ const order = dateTimeParts(locale, false)
77
+ .filter(p => p.type === 'day' || p.type === 'month' || p.type === 'year')
78
+ .map(p => p.type);
79
+ return order.length === 3 ? order : ['day', 'month', 'year'];
80
+ }
81
+ /** The locale's am/pm markers, so 12-hour input can be parsed back. */
82
+ function dayPeriodMarkers(locale) {
83
+ const marker = (hour) => {
84
+ var _a, _b;
85
+ return (_b = (_a = new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true })
86
+ .formatToParts(new Date(2026, 0, 1, hour))
87
+ .find(p => p.type === 'dayPeriod')) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '';
88
+ };
89
+ return { am: marker(9), pm: marker(21) };
90
+ }
91
+ /** Arabic-Indic and extended Arabic-Indic digits → ASCII, so typed input parses everywhere. */
92
+ function toLatinDigits(str) {
93
+ return str.replace(/[٠-٩۰-۹]/g, d => {
94
+ const code = d.charCodeAt(0);
95
+ return String(code >= 0x06f0 ? code - 0x06f0 : code - 0x0660);
96
+ });
97
+ }
98
+ function pad(n, size = 2) {
99
+ return String(n).padStart(size, '0');
100
+ }
101
+ /** Two-digit years: < 70 → 20xx, otherwise 19xx. */
102
+ function expandYear(value, digits) {
103
+ if (digits > 2)
104
+ return value;
105
+ return value < 70 ? 2000 + value : 1900 + value;
106
+ }
107
+ function isValidYmd(year, month, day) {
108
+ if (year < 1 || month < 1 || month > 12 || day < 1)
109
+ return false;
110
+ return day <= new Date(year, month, 0).getDate();
111
+ }
112
+ /**
113
+ * Placeholder showing the locale's date order, e.g. 'dd-mm-jjjj' (nl-NL) or
114
+ * 'mm/dd/yyyy' (en-US). Pass `hour`/`minute` tokens to include the time part.
115
+ */
116
+ export function localeDatePattern(locale, tokens) {
117
+ const withTime = !!(tokens.hour && tokens.minute);
118
+ return dateTimeParts(locale, withTime)
119
+ .map(part => {
120
+ var _a, _b;
121
+ switch (part.type) {
122
+ case 'day': return tokens.day;
123
+ case 'month': return tokens.month;
124
+ case 'year': return tokens.year;
125
+ case 'hour': return (_a = tokens.hour) !== null && _a !== void 0 ? _a : '';
126
+ case 'minute': return (_b = tokens.minute) !== null && _b !== void 0 ? _b : '';
127
+ case 'dayPeriod': return 'AM';
128
+ case 'literal': return part.value;
129
+ default: return '';
130
+ }
131
+ })
132
+ .join('')
133
+ .trim();
134
+ }
135
+ /** Pulls day/month/year out of the number groups a user typed, per locale order. */
136
+ function ymdFromGroups(groups, locale) {
137
+ const order = datePartOrder(locale);
138
+ const values = {};
139
+ order.forEach((type, i) => (values[type] = groups[i]));
140
+ const year = expandYear(Number(values.year), values.year.length);
141
+ const month = Number(values.month);
142
+ const day = Number(values.day);
143
+ return isValidYmd(year, month, day) ? { year, month, day } : null;
144
+ }
145
+ /**
146
+ * Parses a date the user typed in their own locale format → 'YYYY-MM-DD'.
147
+ * Accepts any separator, one- or two-digit day/month, two-digit years, and
148
+ * plain ISO input (pasted values). Returns null when it is not a valid date.
149
+ */
150
+ export function parseLocalizedDate(input, locale) {
151
+ const raw = toLatinDigits(String(input !== null && input !== void 0 ? input : '')).trim();
152
+ if (!raw)
153
+ return null;
154
+ const groups = raw.match(/\d+/g);
155
+ if (!groups || groups.length < 3)
156
+ return null;
157
+ const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw);
158
+ const parsed = iso
159
+ ? (isValidYmd(+iso[1], +iso[2], +iso[3]) ? { year: +iso[1], month: +iso[2], day: +iso[3] } : null)
160
+ : ymdFromGroups(groups.slice(0, 3), locale);
161
+ if (!parsed)
162
+ return null;
163
+ return `${pad(parsed.year, 4)}-${pad(parsed.month)}-${pad(parsed.day)}`;
164
+ }
165
+ /**
166
+ * Parses a date+time the user typed in their own locale format →
167
+ * 'YYYY-MM-DDTHH:mm' in local time (the shape mrd-datetime-field converts to
168
+ * UTC before emitting). A missing time defaults to 00:00; am/pm is recognised
169
+ * both in English and in the locale's own markers.
170
+ */
171
+ export function parseLocalizedDateTime(input, locale) {
172
+ const raw = toLatinDigits(String(input !== null && input !== void 0 ? input : '')).trim();
173
+ if (!raw)
174
+ return null;
175
+ const groups = raw.match(/\d+/g);
176
+ if (!groups || groups.length < 3)
177
+ return null;
178
+ const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ]|$)/.exec(raw);
179
+ const date = iso
180
+ ? (isValidYmd(+iso[1], +iso[2], +iso[3]) ? { year: +iso[1], month: +iso[2], day: +iso[3] } : null)
181
+ : ymdFromGroups(groups.slice(0, 3), locale);
182
+ if (!date)
183
+ return null;
184
+ let hour = groups.length > 3 ? Number(groups[3]) : 0;
185
+ const minute = groups.length > 4 ? Number(groups[4]) : 0;
186
+ const lower = raw.toLowerCase();
187
+ const { am, pm } = dayPeriodMarkers(locale);
188
+ const isPm = /p\.?\s?m\.?/.test(lower) || (!!pm && lower.includes(pm.toLowerCase()));
189
+ const isAm = /a\.?\s?m\.?/.test(lower) || (!!am && lower.includes(am.toLowerCase()));
190
+ if (isPm && hour < 12)
191
+ hour += 12;
192
+ if (isAm && hour === 12)
193
+ hour = 0;
194
+ if (hour > 23 || minute > 59)
195
+ return null;
196
+ return `${pad(date.year, 4)}-${pad(date.month)}-${pad(date.day)}T${pad(hour)}:${pad(minute)}`;
197
+ }
198
+ /**
199
+ * 'YYYY-MM-DD' → localized display string. Parsed as a *local* date, unlike
200
+ * formatDate(), which would read a date-only string as UTC midnight and shift
201
+ * the day in negative-offset timezones.
202
+ */
203
+ export function formatIsoDate(iso, locale) {
204
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso !== null && iso !== void 0 ? iso : ''));
205
+ if (!m)
206
+ return '';
207
+ return formatDate(new Date(+m[1], +m[2] - 1, +m[3]), locale);
208
+ }
209
+ /** 'YYYY-MM-DDTHH:mm' in local time → localized display string. */
210
+ export function formatLocalDateTime(local, locale) {
211
+ const m = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/.exec(String(local !== null && local !== void 0 ? local : ''));
212
+ if (!m)
213
+ return '';
214
+ return formatDateTime(new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]), locale);
215
+ }
56
216
  export function parseLocalizedNumber(str, locale) {
57
217
  var _a, _b, _c, _d;
58
218
  if (!str || !str.trim())