@mmlogic/components 0.5.4 → 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 (67) hide show
  1. package/dist/cjs/{cell-renderer-Coy-aD3k.js → cell-renderer-Mx3pwONr.js} +237 -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 +296 -56
  5. package/dist/cjs/mrd-merge-view.cjs.entry.js +3 -3
  6. package/dist/collection/components/fields/mrd-currency-field/mrd-currency-field.js +56 -5
  7. package/dist/collection/components/fields/mrd-date-field/mrd-date-field.css +55 -4
  8. package/dist/collection/components/fields/mrd-date-field/mrd-date-field.js +118 -8
  9. package/dist/collection/components/fields/mrd-datetime-field/mrd-datetime-field.css +55 -4
  10. package/dist/collection/components/fields/mrd-datetime-field/mrd-datetime-field.js +117 -15
  11. package/dist/collection/components/fields/mrd-email-field/mrd-email-field.js +1 -1
  12. package/dist/collection/components/fields/mrd-file-field/mrd-file-field.js +1 -1
  13. package/dist/collection/components/fields/mrd-image-field/mrd-image-field.js +1 -1
  14. package/dist/collection/components/fields/mrd-longtext-field/mrd-longtext-field.js +1 -1
  15. package/dist/collection/components/fields/mrd-number-field/mrd-number-field.js +33 -8
  16. package/dist/collection/components/fields/mrd-secret-field/mrd-secret-field.js +2 -2
  17. package/dist/collection/components/fields/mrd-text-field/mrd-text-field.js +1 -1
  18. package/dist/collection/components/fields/mrd-textarea-field/mrd-textarea-field.js +1 -1
  19. package/dist/collection/components/fields/mrd-time-field/mrd-time-field.js +1 -1
  20. package/dist/collection/components/form/mrd-field/mrd-field.js +11 -11
  21. package/dist/collection/components/invoice/mrd-invoice-view/mrd-invoice-view.js +1 -1
  22. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.js +1 -1
  23. package/dist/collection/components/merge/mrd-merge-view/mrd-merge-view.js +2 -2
  24. package/dist/collection/dev/app.js +58 -13
  25. package/dist/collection/utils/format.js +160 -0
  26. package/dist/collection/utils/i18n.js +72 -0
  27. package/dist/components/cell-renderer.js +1 -1
  28. package/dist/components/format.js +1 -1
  29. package/dist/components/i18n.js +1 -1
  30. package/dist/components/mrd-currency-field2.js +1 -1
  31. package/dist/components/mrd-date-field2.js +1 -1
  32. package/dist/components/mrd-datetime-field2.js +1 -1
  33. package/dist/components/mrd-document-list2.js +1 -1
  34. package/dist/components/mrd-document-view2.js +1 -1
  35. package/dist/components/mrd-email-field2.js +1 -1
  36. package/dist/components/mrd-email-view2.js +1 -1
  37. package/dist/components/mrd-field2.js +1 -1
  38. package/dist/components/mrd-file-field2.js +1 -1
  39. package/dist/components/mrd-image-field2.js +1 -1
  40. package/dist/components/mrd-invoice-view2.js +1 -1
  41. package/dist/components/mrd-layout-section.js +1 -1
  42. package/dist/components/mrd-longtext-field2.js +1 -1
  43. package/dist/components/mrd-merge-view.js +1 -1
  44. package/dist/components/mrd-number-field2.js +1 -1
  45. package/dist/components/mrd-secret-field2.js +1 -1
  46. package/dist/components/mrd-table2.js +1 -1
  47. package/dist/components/mrd-text-field2.js +1 -1
  48. package/dist/components/mrd-textarea-field2.js +1 -1
  49. package/dist/components/mrd-time-field2.js +1 -1
  50. package/dist/esm/{cell-renderer-C2FLUrMN.js → cell-renderer-UQOYlDv7.js} +233 -1
  51. package/dist/esm/loader.js +1 -1
  52. package/dist/esm/mosterdcomponents.js +1 -1
  53. package/dist/esm/mrd-boolean-field_25.entry.js +296 -56
  54. package/dist/esm/mrd-merge-view.entry.js +3 -3
  55. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  56. package/dist/mosterdcomponents/p-8cc374e0.entry.js +3 -0
  57. package/dist/mosterdcomponents/p-CF--ScWr.js +1 -0
  58. package/dist/mosterdcomponents/{p-4ea5b97c.entry.js → p-d31b71d9.entry.js} +1 -1
  59. package/dist/types/components/fields/mrd-currency-field/mrd-currency-field.d.ts +13 -0
  60. package/dist/types/components/fields/mrd-date-field/mrd-date-field.d.ts +22 -1
  61. package/dist/types/components/fields/mrd-datetime-field/mrd-datetime-field.d.ts +21 -2
  62. package/dist/types/components/fields/mrd-number-field/mrd-number-field.d.ts +6 -0
  63. package/dist/types/components.d.ts +26 -0
  64. package/dist/types/utils/format.d.ts +34 -0
  65. package/package.json +1 -1
  66. package/dist/mosterdcomponents/p-8736313b.entry.js +0 -3
  67. package/dist/mosterdcomponents/p-BgsPVesg.js +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  var index = require('./index-DVbdkYO3.js');
4
4
  var clientLayout = require('./client-layout-CqVuJumR.js');
5
- var cellRenderer = require('./cell-renderer-Coy-aD3k.js');
5
+ var cellRenderer = require('./cell-renderer-Mx3pwONr.js');
6
6
 
7
7
  /**
8
8
  * Whether a layout item can appear in the merge diff at all. m-n relations
@@ -257,14 +257,14 @@ const MrdMergeView = class {
257
257
  var _a, _b;
258
258
  let stepContent;
259
259
  if (this.step === 'search') {
260
- stepContent = (index.h(MergeSearchStep, { key: '3adb36fb46aa7260641a46a2cf27cc0ef320876f', locale: this.locale, searchQuery: this.searchQuery, searchResults: this.searchResults, isLoading: this.isLoading, onSearchInput: this.handleSearchInput, onSelect: this.handleSelect }));
260
+ stepContent = (index.h(MergeSearchStep, { key: 'b965c21176faf7844e58cb156958fa1305d07cdc', locale: this.locale, searchQuery: this.searchQuery, searchResults: this.searchResults, isLoading: this.isLoading, onSearchInput: this.handleSearchInput, onSelect: this.handleSelect }));
261
261
  }
262
262
  else {
263
263
  const diff = diffFields(this.fields, this.currentRecord, this.candidate);
264
264
  const bLabel = candidateLabel(this.candidate);
265
265
  stepContent = this.step === 'compare' ? (index.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 })) : (index.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 }));
266
266
  }
267
- return (index.h(index.Host, { key: 'e33a6d66a220b28cc9533da78dc274e2d7ec5c4a' }, index.h("div", { key: 'b5fb1390d86192f9f6f15e3ccb89e2d21d9c7c8b', class: "mrd-merge-view" }, stepContent)));
267
+ return (index.h(index.Host, { key: 'e9cffda6c5cff51f802b044f3d8e417df83b7846' }, index.h("div", { key: '26690ef2db7489cbf7994191ad19491a0fdd7002', class: "mrd-merge-view" }, stepContent)));
268
268
  }
269
269
  get el() { return index.getElement(this); }
270
270
  };
@@ -1,5 +1,5 @@
1
1
  import { Host, h } from "@stencil/core";
2
- import { parseLocalizedNumber } from "../../../utils/format";
2
+ import { formatNumber, parseLocalizedNumber } from "../../../utils/format";
3
3
  import { FieldLabel, FieldError, controlClass, requiredError } from "../field-helpers";
4
4
  const COMMON_CURRENCIES = [
5
5
  'EUR', 'USD', 'GBP', 'CHF', 'JPY', 'CNY', 'AUD', 'CAD',
@@ -17,13 +17,26 @@ export class MrdCurrencyField {
17
17
  this.amountDisplay = '';
18
18
  this.currency = 'EUR';
19
19
  this.error = '';
20
+ /** While focused the user owns the text; prop updates must not overwrite it. */
21
+ this.focused = false;
20
22
  this.handleAmountInput = (e) => {
21
23
  this.amountDisplay = e.target.value;
22
24
  };
25
+ this.handleAmountFocus = () => {
26
+ this.focused = true;
27
+ // Drop the grouping separators while editing, but keep the locale's decimal
28
+ // separator: String(1234.56) would be re-read as 123456 where '.' groups.
29
+ const parsed = parseLocalizedNumber(this.amountDisplay, this.locale);
30
+ if (parsed !== null) {
31
+ this.amountDisplay = formatNumber(parsed, this.locale, { useGrouping: false, maximumFractionDigits: 20 });
32
+ }
33
+ };
23
34
  this.handleAmountBlur = (e) => {
35
+ this.focused = false;
24
36
  const raw = e.target.value;
25
37
  const parsed = parseLocalizedNumber(raw, this.locale);
26
38
  this.error = requiredError(raw, this.required, this.locale);
39
+ this.amountDisplay = parsed !== null ? this.formatAmount(parsed) : '';
27
40
  const val = { amount: parsed, currency: this.currency };
28
41
  this.mrdChange.emit({ name: this.name, value: val });
29
42
  this.mrdBlur.emit({ name: this.name, value: val });
@@ -35,14 +48,43 @@ export class MrdCurrencyField {
35
48
  };
36
49
  }
37
50
  componentWillLoad() {
38
- if (this.value) {
39
- this.currency = this.value.currency || 'EUR';
40
- this.amountDisplay = this.value.amount !== null ? String(this.value.amount) : '';
51
+ this.syncFromValue();
52
+ }
53
+ componentDidRender() {
54
+ // A rendered `selected` attribute is ignored once the select has an established
55
+ // value, so the currency is applied imperatively — same reason as the DROPDOWN
56
+ // preselect in mrd-relation-field. Without this an edit-mode record that loads
57
+ // after mount keeps showing EUR and would be saved back with the wrong currency.
58
+ if (this.selectEl && this.selectEl.value !== this.currency) {
59
+ this.selectEl.value = this.currency;
41
60
  }
42
61
  }
62
+ /** The record's own currency may sit outside the common list; never drop it. */
63
+ currencyOptions() {
64
+ return COMMON_CURRENCIES.includes(this.currency) ? COMMON_CURRENCIES : [this.currency, ...COMMON_CURRENCIES];
65
+ }
66
+ /** Edit mode loads the record after mount, so the incoming value must re-sync. */
67
+ valueChanged() {
68
+ if (!this.focused)
69
+ this.syncFromValue();
70
+ }
71
+ localeChanged() {
72
+ if (!this.focused)
73
+ this.syncFromValue();
74
+ }
75
+ syncFromValue() {
76
+ const value = this.value;
77
+ this.currency = (value === null || value === void 0 ? void 0 : value.currency) || 'EUR';
78
+ this.amountDisplay =
79
+ (value === null || value === void 0 ? void 0 : value.amount) !== null && (value === null || value === void 0 ? void 0 : value.amount) !== undefined ? this.formatAmount(value.amount) : '';
80
+ }
81
+ /** Display form: locale separators, always two decimals. The ISO code sits in the select. */
82
+ formatAmount(amount) {
83
+ return formatNumber(amount, this.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
84
+ }
43
85
  render() {
44
86
  const hasError = !!this.error;
45
- return (h(Host, { key: 'e1a76ab778820c0d781d5ec1549448716d368c1f' }, h("div", { key: '8298720abac7f689fe3e697c3c4410702e6a87f2', class: "mrd-currency-field" }, h(FieldLabel, { key: 'b23782aef9a866f1fb72e152cb91aeae00a975fa', base: "mrd-currency-field", label: this.label, required: this.required }), h("div", { key: '51e5177f9be3c5a6bdf3a5c1187f9261fcc349cf', class: "mrd-currency-field__row" }, h("input", { key: 'c6bda6cc92f3a2a34861d44b6163d417e200a6e4', class: controlClass('mrd-currency-field', hasError, 'amount'), type: "text", inputMode: "decimal", name: `${this.name}_amount`, value: this.amountDisplay, placeholder: "0.00", required: this.required, disabled: this.disabled, onInput: this.handleAmountInput, onBlur: this.handleAmountBlur }), h("select", { key: '29231cfa142d26aa81594609f1bf7bd5168df073', class: "mrd-currency-field__currency", disabled: this.disabled, onChange: this.handleCurrencyChange }, COMMON_CURRENCIES.map(c => (h("option", { key: c, value: c, selected: c === this.currency }, c))))), h(FieldError, { key: 'bea9615466427631f08a6ac8a779ae77695d36e3', base: "mrd-currency-field", error: this.error }))));
87
+ return (h(Host, { key: '43ef1fe660c4eaef025203544916b077975b6aa3' }, h("div", { key: '00b8e79182c8e2a353d57ad5c341024caf00d4ea', class: "mrd-currency-field" }, h(FieldLabel, { key: '96835c311e0d490864c484b818f1dcc623f488b2', base: "mrd-currency-field", label: this.label, required: this.required }), h("div", { key: '80317cfe562af00cc4cbf9aabf4bac2126cf6fe5', class: "mrd-currency-field__row" }, h("input", { key: '1983ef1e4d19e0fc529cf5e8be7d287fea36620a', class: controlClass('mrd-currency-field', hasError, 'amount'), type: "text", inputMode: "decimal", name: `${this.name}_amount`, value: this.amountDisplay, placeholder: this.formatAmount(0), required: this.required, disabled: this.disabled, onInput: this.handleAmountInput, onFocus: this.handleAmountFocus, onBlur: this.handleAmountBlur }), h("select", { key: 'b5fe3f03590bebaa564d76bc5fcb47c327eacece', class: "mrd-currency-field__currency", ref: el => (this.selectEl = el), disabled: this.disabled, onChange: this.handleCurrencyChange }, this.currencyOptions().map(c => (h("option", { key: c, value: c, selected: c === this.currency }, c))))), h(FieldError, { key: '1a128b26293170d4137f787622d4e89a23e62c0b', base: "mrd-currency-field", error: this.error }))));
46
88
  }
47
89
  static get is() { return "mrd-currency-field"; }
48
90
  static get encapsulation() { return "scoped"; }
@@ -239,4 +281,13 @@ export class MrdCurrencyField {
239
281
  }
240
282
  }];
241
283
  }
284
+ static get watchers() {
285
+ return [{
286
+ "propName": "value",
287
+ "methodName": "valueChanged"
288
+ }, {
289
+ "propName": "locale",
290
+ "methodName": "localeChanged"
291
+ }];
292
+ }
242
293
  }
@@ -19,6 +19,10 @@
19
19
  content: " *";
20
20
  color: var(--mrd-color-danger);
21
21
  }
22
+ .mrd-date-field__control {
23
+ position: relative;
24
+ display: block;
25
+ }
22
26
  .mrd-date-field__input {
23
27
  display: block;
24
28
  width: 100%;
@@ -53,12 +57,59 @@
53
57
  .mrd-date-field__input--error:focus {
54
58
  box-shadow: var(--mrd-shadow-focus-error);
55
59
  }
60
+ .mrd-date-field__input {
61
+ padding-inline-end: calc(var(--mrd-input-padding-x) + 1.75rem);
62
+ }
63
+ .mrd-date-field__picker {
64
+ position: absolute;
65
+ inset-inline-end: var(--mrd-space-1);
66
+ top: 50%;
67
+ transform: translateY(-50%);
68
+ width: 1.75rem;
69
+ height: 1.75rem;
70
+ }
71
+ .mrd-date-field__picker-button {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ width: 100%;
76
+ height: 100%;
77
+ padding: 0;
78
+ border: 0;
79
+ border-radius: var(--mrd-border-radius-sm);
80
+ background: transparent;
81
+ color: var(--mrd-color-neutral-500);
82
+ cursor: pointer;
83
+ transition: color var(--mrd-transition), background-color var(--mrd-transition);
84
+ }
85
+ .mrd-date-field__picker-button svg {
86
+ width: 1.125rem;
87
+ height: 1.125rem;
88
+ }
89
+ .mrd-date-field__picker-button:hover:not(:disabled) {
90
+ color: var(--mrd-color-primary);
91
+ background-color: var(--mrd-color-neutral-100);
92
+ }
93
+ .mrd-date-field__picker-button:focus-visible {
94
+ outline: 2px solid var(--mrd-border-color-focus);
95
+ outline-offset: 1px;
96
+ }
97
+ .mrd-date-field__picker-button:disabled {
98
+ cursor: not-allowed;
99
+ opacity: 0.5;
100
+ }
101
+ .mrd-date-field__picker-native {
102
+ position: absolute;
103
+ inset: 0;
104
+ width: 100%;
105
+ height: 100%;
106
+ padding: 0;
107
+ border: 0;
108
+ opacity: 0;
109
+ pointer-events: none;
110
+ }
56
111
  .mrd-date-field__error {
57
112
  font-family: var(--mrd-font-family);
58
113
  font-size: var(--mrd-error-font-size);
59
114
  color: var(--mrd-error-color);
60
- }
61
-
62
- .mrd-date-field__input {
63
- cursor: pointer;
64
115
  }
@@ -1,26 +1,106 @@
1
1
  import { Host, h } from "@stencil/core";
2
+ import { t } from "../../../utils/i18n";
3
+ import { formatIsoDate, localeDatePattern, parseLocalizedDate } from "../../../utils/format";
2
4
  import { FieldLabel, FieldError, controlClass, requiredError } from "../field-helpers";
3
5
  export class MrdDateField {
4
6
  constructor() {
5
7
  this.name = '';
6
8
  this.label = '';
9
+ /** ISO date, 'YYYY-MM-DD'. */
7
10
  this.value = '';
11
+ /** Overrides the locale-derived pattern placeholder (e.g. 'dd-mm-jjjj'). */
12
+ this.placeholder = '';
8
13
  this.required = false;
9
14
  this.disabled = false;
10
15
  this.locale = navigator.language;
16
+ /** What the user sees: the value formatted for `locale`, or their raw typing. */
17
+ this.displayValue = '';
11
18
  this.error = '';
12
- this.handleChange = (e) => {
13
- const val = e.target.value;
14
- this.error = requiredError(val, this.required, this.locale);
15
- this.mrdChange.emit({ name: this.name, value: val });
19
+ /** While focused the user owns the text; prop updates must not overwrite it. */
20
+ this.focused = false;
21
+ this.handleFocus = () => {
22
+ this.focused = true;
23
+ };
24
+ this.handleInput = (e) => {
25
+ this.displayValue = e.target.value;
16
26
  };
17
27
  this.handleBlur = (e) => {
18
- const val = e.target.value;
19
- this.mrdBlur.emit({ name: this.name, value: val });
28
+ this.focused = false;
29
+ const value = this.commit(e.target.value);
30
+ this.mrdChange.emit({ name: this.name, value });
31
+ this.mrdBlur.emit({ name: this.name, value });
32
+ };
33
+ this.handleKeyDown = (e) => {
34
+ // Submitting with Enter never fires blur, so commit the typed text here too.
35
+ if (e.key !== 'Enter')
36
+ return;
37
+ const value = this.commit(e.target.value);
38
+ this.mrdChange.emit({ name: this.name, value });
39
+ };
40
+ this.openPicker = () => {
41
+ const native = this.nativeInput;
42
+ if (!native || this.disabled)
43
+ return;
44
+ // The blur commit that precedes this click re-renders asynchronously, so sync by hand first.
45
+ native.value = this.currentIso();
46
+ try {
47
+ native.showPicker();
48
+ }
49
+ catch (_a) {
50
+ // Browsers without showPicker(): focusing at least surfaces the platform control.
51
+ native.focus();
52
+ }
20
53
  };
54
+ this.handlePickerChange = (e) => {
55
+ const iso = e.target.value; // native inputs always yield 'YYYY-MM-DD'
56
+ this.displayValue = formatIsoDate(iso, this.locale);
57
+ this.error = requiredError(iso, this.required, this.locale);
58
+ this.mrdChange.emit({ name: this.name, value: iso });
59
+ this.mrdBlur.emit({ name: this.name, value: iso });
60
+ };
61
+ }
62
+ componentWillLoad() {
63
+ this.displayValue = formatIsoDate(this.value, this.locale);
64
+ }
65
+ valueChanged(newValue) {
66
+ if (!this.focused)
67
+ this.displayValue = formatIsoDate(newValue, this.locale);
68
+ }
69
+ localeChanged() {
70
+ if (!this.focused)
71
+ this.displayValue = formatIsoDate(this.value, this.locale);
72
+ }
73
+ patternPlaceholder() {
74
+ if (this.placeholder)
75
+ return this.placeholder;
76
+ return localeDatePattern(this.locale, {
77
+ day: t('date_token_day', this.locale),
78
+ month: t('date_token_month', this.locale),
79
+ year: t('date_token_year', this.locale),
80
+ });
81
+ }
82
+ /** Parse the typed text, reformat it, and emit the ISO value. */
83
+ commit(raw) {
84
+ const trimmed = raw.trim();
85
+ const iso = trimmed ? parseLocalizedDate(trimmed, this.locale) : '';
86
+ if (trimmed && iso === null) {
87
+ // Keep the raw text so the user can correct it instead of retyping.
88
+ this.error = t('invalid_date', this.locale);
89
+ this.displayValue = trimmed;
90
+ }
91
+ else {
92
+ this.error = requiredError(iso, this.required, this.locale);
93
+ this.displayValue = iso ? formatIsoDate(iso, this.locale) : '';
94
+ }
95
+ return iso !== null && iso !== void 0 ? iso : '';
96
+ }
97
+ /** The ISO value currently shown, so the picker opens on the right date. */
98
+ currentIso() {
99
+ var _a;
100
+ return (_a = (this.displayValue ? parseLocalizedDate(this.displayValue, this.locale) : '')) !== null && _a !== void 0 ? _a : '';
21
101
  }
22
102
  render() {
23
- return (h(Host, { key: 'd83beef11c2b9402e251a50885b92e7a057f02ee' }, h("div", { key: '6a26e4969e9bde8c5c87771d7b8c7b4d6c1dd942', class: "mrd-date-field" }, h(FieldLabel, { key: 'a7bc1b1c56e933a51f6b1b1feb794a4678d9d334', base: "mrd-date-field", label: this.label, required: this.required }), h("input", { key: '82e3855654f3b3c554f80d647e783234cca8dae1', class: controlClass('mrd-date-field', !!this.error), type: "date", name: this.name, value: this.value, required: this.required, disabled: this.disabled, onChange: this.handleChange, onBlur: this.handleBlur }), h(FieldError, { key: '294b3cdc1004725267aa8265366f5009ab324203', base: "mrd-date-field", error: this.error }))));
103
+ return (h(Host, { key: '06ba48bd538ee4c8de4e071285535451f783c259' }, h("div", { key: '73ea6ea23c85fe3325bb151afb755cc11a13cf58', class: "mrd-date-field" }, h(FieldLabel, { key: '527848cf72323aef176fc7d5bcd47dcf2f7e46f6', base: "mrd-date-field", label: this.label, required: this.required }), h("div", { key: '2ff7586f4e6298d1f9b9b131106044533de82c6d', class: "mrd-date-field__control" }, h("input", { key: '68fdf2f1f7961ca049ab6493494c8c0707e6a4af', class: controlClass('mrd-date-field', !!this.error), type: "text", name: this.name, value: this.displayValue, placeholder: this.patternPlaceholder(), autocomplete: "off", required: this.required, disabled: this.disabled, onFocus: this.handleFocus, onInput: this.handleInput, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown }), h("span", { key: '9557b7a3f6780b8a05030ab8e7c941c44df9f4a5', class: "mrd-date-field__picker" }, h("button", { key: '586973e0440ed0b74e1fa0affae9e462257024c3', type: "button", class: "mrd-date-field__picker-button", title: t('open_calendar', this.locale), "aria-label": t('open_calendar', this.locale), disabled: this.disabled, onClick: this.openPicker }, h("svg", { key: '144319d3a50d0d38c75c59dd3321d4b9097322a6', viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "aria-hidden": "true" }, h("rect", { key: 'aec01ca5c620bf3116705b7a28052eb3bc210cfd', x: "3", y: "4.5", width: "14", height: "12", rx: "2" }), h("path", { key: 'a0e13dd3a2005bfbee40f5a071b6760270d0d58d', d: "M3 8.5h14M7 2.5v3M13 2.5v3", "stroke-linecap": "round" }))), h("input", { key: 'be02df44f31a59a26ce166ab9a9eab9db2635d89', class: "mrd-date-field__picker-native", type: "date", ref: el => (this.nativeInput = el), value: this.currentIso(), disabled: this.disabled, tabIndex: -1, "aria-hidden": "true", onChange: this.handlePickerChange }))), h(FieldError, { key: '2f8b2072bee32170a35f98ec5c962018774c7e8c', base: "mrd-date-field", error: this.error }))));
24
104
  }
25
105
  static get is() { return "mrd-date-field"; }
26
106
  static get encapsulation() { return "scoped"; }
@@ -88,7 +168,7 @@ export class MrdDateField {
88
168
  "optional": false,
89
169
  "docs": {
90
170
  "tags": [],
91
- "text": ""
171
+ "text": "ISO date, 'YYYY-MM-DD'."
92
172
  },
93
173
  "getter": false,
94
174
  "setter": false,
@@ -96,6 +176,26 @@ export class MrdDateField {
96
176
  "attribute": "value",
97
177
  "defaultValue": "''"
98
178
  },
179
+ "placeholder": {
180
+ "type": "string",
181
+ "mutable": false,
182
+ "complexType": {
183
+ "original": "string",
184
+ "resolved": "string",
185
+ "references": {}
186
+ },
187
+ "required": false,
188
+ "optional": false,
189
+ "docs": {
190
+ "tags": [],
191
+ "text": "Overrides the locale-derived pattern placeholder (e.g. 'dd-mm-jjjj')."
192
+ },
193
+ "getter": false,
194
+ "setter": false,
195
+ "reflect": false,
196
+ "attribute": "placeholder",
197
+ "defaultValue": "''"
198
+ },
99
199
  "required": {
100
200
  "type": "boolean",
101
201
  "mutable": false,
@@ -160,6 +260,7 @@ export class MrdDateField {
160
260
  }
161
261
  static get states() {
162
262
  return {
263
+ "displayValue": {},
163
264
  "error": {}
164
265
  };
165
266
  }
@@ -196,4 +297,13 @@ export class MrdDateField {
196
297
  }
197
298
  }];
198
299
  }
300
+ static get watchers() {
301
+ return [{
302
+ "propName": "value",
303
+ "methodName": "valueChanged"
304
+ }, {
305
+ "propName": "locale",
306
+ "methodName": "localeChanged"
307
+ }];
308
+ }
199
309
  }
@@ -19,6 +19,10 @@
19
19
  content: " *";
20
20
  color: var(--mrd-color-danger);
21
21
  }
22
+ .mrd-datetime-field__control {
23
+ position: relative;
24
+ display: block;
25
+ }
22
26
  .mrd-datetime-field__input {
23
27
  display: block;
24
28
  width: 100%;
@@ -53,12 +57,59 @@
53
57
  .mrd-datetime-field__input--error:focus {
54
58
  box-shadow: var(--mrd-shadow-focus-error);
55
59
  }
60
+ .mrd-datetime-field__input {
61
+ padding-inline-end: calc(var(--mrd-input-padding-x) + 1.75rem);
62
+ }
63
+ .mrd-datetime-field__picker {
64
+ position: absolute;
65
+ inset-inline-end: var(--mrd-space-1);
66
+ top: 50%;
67
+ transform: translateY(-50%);
68
+ width: 1.75rem;
69
+ height: 1.75rem;
70
+ }
71
+ .mrd-datetime-field__picker-button {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ width: 100%;
76
+ height: 100%;
77
+ padding: 0;
78
+ border: 0;
79
+ border-radius: var(--mrd-border-radius-sm);
80
+ background: transparent;
81
+ color: var(--mrd-color-neutral-500);
82
+ cursor: pointer;
83
+ transition: color var(--mrd-transition), background-color var(--mrd-transition);
84
+ }
85
+ .mrd-datetime-field__picker-button svg {
86
+ width: 1.125rem;
87
+ height: 1.125rem;
88
+ }
89
+ .mrd-datetime-field__picker-button:hover:not(:disabled) {
90
+ color: var(--mrd-color-primary);
91
+ background-color: var(--mrd-color-neutral-100);
92
+ }
93
+ .mrd-datetime-field__picker-button:focus-visible {
94
+ outline: 2px solid var(--mrd-border-color-focus);
95
+ outline-offset: 1px;
96
+ }
97
+ .mrd-datetime-field__picker-button:disabled {
98
+ cursor: not-allowed;
99
+ opacity: 0.5;
100
+ }
101
+ .mrd-datetime-field__picker-native {
102
+ position: absolute;
103
+ inset: 0;
104
+ width: 100%;
105
+ height: 100%;
106
+ padding: 0;
107
+ border: 0;
108
+ opacity: 0;
109
+ pointer-events: none;
110
+ }
56
111
  .mrd-datetime-field__error {
57
112
  font-family: var(--mrd-font-family);
58
113
  font-size: var(--mrd-error-font-size);
59
114
  color: var(--mrd-error-color);
60
- }
61
-
62
- .mrd-datetime-field__input {
63
- cursor: pointer;
64
115
  }
@@ -1,33 +1,77 @@
1
1
  import { Host, h } from "@stencil/core";
2
+ import { t } from "../../../utils/i18n";
3
+ import { formatLocalDateTime, localeDatePattern, parseLocalizedDateTime } from "../../../utils/format";
2
4
  import { FieldLabel, FieldError, controlClass, requiredError } from "../field-helpers";
3
5
  export class MrdDatetimeField {
4
6
  constructor() {
5
7
  this.name = '';
6
8
  this.label = '';
9
+ /** UTC ISO string, e.g. '2026-07-06T12:30:00Z'. */
7
10
  this.value = '';
11
+ /** Overrides the locale-derived pattern placeholder (e.g. 'dd-mm-jjjj uu:mm'). */
12
+ this.placeholder = '';
8
13
  this.required = false;
9
14
  this.disabled = false;
10
15
  this.locale = navigator.language;
16
+ /** What the user sees: the value formatted for `locale`, or their raw typing. */
17
+ this.displayValue = '';
11
18
  this.error = '';
12
- this.localValue = '';
13
- this.handleChange = (e) => {
14
- const localVal = e.target.value;
15
- this.localValue = localVal;
16
- this.error = requiredError(localVal, this.required, this.locale);
17
- this.mrdChange.emit({ name: this.name, value: this.localToUtc(localVal) });
19
+ /** While focused the user owns the text; prop updates must not overwrite it. */
20
+ this.focused = false;
21
+ this.handleFocus = () => {
22
+ this.focused = true;
23
+ };
24
+ this.handleInput = (e) => {
25
+ this.displayValue = e.target.value;
18
26
  };
19
27
  this.handleBlur = (e) => {
20
- const localVal = e.target.value;
21
- this.mrdBlur.emit({ name: this.name, value: this.localToUtc(localVal) });
28
+ this.focused = false;
29
+ const value = this.commit(e.target.value);
30
+ this.mrdChange.emit({ name: this.name, value });
31
+ this.mrdBlur.emit({ name: this.name, value });
32
+ };
33
+ this.handleKeyDown = (e) => {
34
+ // Submitting with Enter never fires blur, so commit the typed text here too.
35
+ if (e.key !== 'Enter')
36
+ return;
37
+ const value = this.commit(e.target.value);
38
+ this.mrdChange.emit({ name: this.name, value });
39
+ };
40
+ this.openPicker = () => {
41
+ const native = this.nativeInput;
42
+ if (!native || this.disabled)
43
+ return;
44
+ // The blur commit that precedes this click re-renders asynchronously, so sync by hand first.
45
+ native.value = this.currentLocal();
46
+ try {
47
+ native.showPicker();
48
+ }
49
+ catch (_a) {
50
+ // Browsers without showPicker(): focusing at least surfaces the platform control.
51
+ native.focus();
52
+ }
53
+ };
54
+ this.handlePickerChange = (e) => {
55
+ const local = e.target.value; // native inputs always yield 'YYYY-MM-DDTHH:mm'
56
+ this.displayValue = formatLocalDateTime(local, this.locale);
57
+ this.error = requiredError(local, this.required, this.locale);
58
+ const value = this.localToUtc(local);
59
+ this.mrdChange.emit({ name: this.name, value });
60
+ this.mrdBlur.emit({ name: this.name, value });
22
61
  };
23
62
  }
24
63
  componentWillLoad() {
25
- this.localValue = this.utcToLocal(this.value);
64
+ this.displayValue = this.toDisplay(this.value);
26
65
  }
27
66
  valueChanged(newVal) {
28
- this.localValue = this.utcToLocal(newVal);
67
+ if (!this.focused)
68
+ this.displayValue = this.toDisplay(newVal);
29
69
  }
30
- // UTC ISO string → "YYYY-MM-DDTHH:mm" in local timezone (for datetime-local input)
70
+ localeChanged() {
71
+ if (!this.focused)
72
+ this.displayValue = this.toDisplay(this.value);
73
+ }
74
+ // UTC ISO string → "YYYY-MM-DDTHH:mm" in local timezone
31
75
  utcToLocal(utcStr) {
32
76
  if (!utcStr)
33
77
  return '';
@@ -46,8 +90,43 @@ export class MrdDatetimeField {
46
90
  return '';
47
91
  return d.toISOString().replace(/\.\d{3}Z$/, 'Z');
48
92
  }
93
+ // UTC ISO string → locale-formatted text for the input
94
+ toDisplay(utcStr) {
95
+ return formatLocalDateTime(this.utcToLocal(utcStr), this.locale);
96
+ }
97
+ patternPlaceholder() {
98
+ if (this.placeholder)
99
+ return this.placeholder;
100
+ return localeDatePattern(this.locale, {
101
+ day: t('date_token_day', this.locale),
102
+ month: t('date_token_month', this.locale),
103
+ year: t('date_token_year', this.locale),
104
+ hour: t('time_token_hour', this.locale),
105
+ minute: t('time_token_minute', this.locale),
106
+ });
107
+ }
108
+ /** Parse the typed text, reformat it, and return the UTC value to emit. */
109
+ commit(raw) {
110
+ const trimmed = raw.trim();
111
+ const local = trimmed ? parseLocalizedDateTime(trimmed, this.locale) : '';
112
+ if (trimmed && local === null) {
113
+ // Keep the raw text so the user can correct it instead of retyping.
114
+ this.error = t('invalid_datetime', this.locale);
115
+ this.displayValue = trimmed;
116
+ return '';
117
+ }
118
+ const localValue = local !== null && local !== void 0 ? local : '';
119
+ this.error = requiredError(localValue, this.required, this.locale);
120
+ this.displayValue = localValue ? formatLocalDateTime(localValue, this.locale) : '';
121
+ return this.localToUtc(localValue);
122
+ }
123
+ /** The local 'YYYY-MM-DDTHH:mm' currently shown, so the picker opens on the right moment. */
124
+ currentLocal() {
125
+ var _a;
126
+ return (_a = (this.displayValue ? parseLocalizedDateTime(this.displayValue, this.locale) : '')) !== null && _a !== void 0 ? _a : '';
127
+ }
49
128
  render() {
50
- return (h(Host, { key: '6f8020590ac879992058aa9547b90c11250fa303' }, h("div", { key: '76abff48cae58d156540181a6577729a449bb888', class: "mrd-datetime-field" }, h(FieldLabel, { key: 'a8c3688980347a27d6467987f11a3c4135ac5234', base: "mrd-datetime-field", label: this.label, required: this.required }), h("input", { key: '1bb1aa7536a6a82313f51240030c828028cafd76', class: controlClass('mrd-datetime-field', !!this.error), type: "datetime-local", name: this.name, value: this.localValue, required: this.required, disabled: this.disabled, onChange: this.handleChange, onBlur: this.handleBlur }), h(FieldError, { key: 'c97301640980bf04311751124fc8e87023d59f87', base: "mrd-datetime-field", error: this.error }))));
129
+ return (h(Host, { key: 'e075e65c76a627a8f19db488c07704e2b1a7270b' }, h("div", { key: 'a6b5a5df3168272f1f775933dfc372e1b2ccbd95', class: "mrd-datetime-field" }, h(FieldLabel, { key: '39e232c066b8c354eeda7df9bcba4e3f95c18ee5', base: "mrd-datetime-field", label: this.label, required: this.required }), h("div", { key: '4ffac4089cbb9e5506aa0854c8b1f15862bc8456', class: "mrd-datetime-field__control" }, h("input", { key: 'd33400133173c7f3e8b045339f44fbeba7f0b35a', class: controlClass('mrd-datetime-field', !!this.error), type: "text", name: this.name, value: this.displayValue, placeholder: this.patternPlaceholder(), autocomplete: "off", required: this.required, disabled: this.disabled, onFocus: this.handleFocus, onInput: this.handleInput, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown }), h("span", { key: '54d8d3b1f508d6226d1341d87c1fec19d5914a10', class: "mrd-datetime-field__picker" }, h("button", { key: '27cd3977984eedd9e3471dd039fc73798f1c7abc', type: "button", class: "mrd-datetime-field__picker-button", title: t('open_calendar', this.locale), "aria-label": t('open_calendar', this.locale), disabled: this.disabled, onClick: this.openPicker }, h("svg", { key: '634ca817fa001263933795e35e1a215a06da7a77', viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "aria-hidden": "true" }, h("rect", { key: '66eadf6b2cfee53376f6ea0f50dbd2ec04fc5663', x: "3", y: "4.5", width: "14", height: "12", rx: "2" }), h("path", { key: '4bb61793e0501bd18bb16c4e203b20f509692970', d: "M3 8.5h14M7 2.5v3M13 2.5v3", "stroke-linecap": "round" }))), h("input", { key: '8877da44e4dc527e7c05244359a0caf9d2e81fdd', class: "mrd-datetime-field__picker-native", type: "datetime-local", ref: el => (this.nativeInput = el), value: this.currentLocal(), disabled: this.disabled, tabIndex: -1, "aria-hidden": "true", onChange: this.handlePickerChange }))), h(FieldError, { key: '4ee97180614972c4dd334d68873a9fb7f91e48f5', base: "mrd-datetime-field", error: this.error }))));
51
130
  }
52
131
  static get is() { return "mrd-datetime-field"; }
53
132
  static get encapsulation() { return "scoped"; }
@@ -115,7 +194,7 @@ export class MrdDatetimeField {
115
194
  "optional": false,
116
195
  "docs": {
117
196
  "tags": [],
118
- "text": ""
197
+ "text": "UTC ISO string, e.g. '2026-07-06T12:30:00Z'."
119
198
  },
120
199
  "getter": false,
121
200
  "setter": false,
@@ -123,6 +202,26 @@ export class MrdDatetimeField {
123
202
  "attribute": "value",
124
203
  "defaultValue": "''"
125
204
  },
205
+ "placeholder": {
206
+ "type": "string",
207
+ "mutable": false,
208
+ "complexType": {
209
+ "original": "string",
210
+ "resolved": "string",
211
+ "references": {}
212
+ },
213
+ "required": false,
214
+ "optional": false,
215
+ "docs": {
216
+ "tags": [],
217
+ "text": "Overrides the locale-derived pattern placeholder (e.g. 'dd-mm-jjjj uu:mm')."
218
+ },
219
+ "getter": false,
220
+ "setter": false,
221
+ "reflect": false,
222
+ "attribute": "placeholder",
223
+ "defaultValue": "''"
224
+ },
126
225
  "required": {
127
226
  "type": "boolean",
128
227
  "mutable": false,
@@ -187,8 +286,8 @@ export class MrdDatetimeField {
187
286
  }
188
287
  static get states() {
189
288
  return {
190
- "error": {},
191
- "localValue": {}
289
+ "displayValue": {},
290
+ "error": {}
192
291
  };
193
292
  }
194
293
  static get events() {
@@ -228,6 +327,9 @@ export class MrdDatetimeField {
228
327
  return [{
229
328
  "propName": "value",
230
329
  "methodName": "valueChanged"
330
+ }, {
331
+ "propName": "locale",
332
+ "methodName": "localeChanged"
231
333
  }];
232
334
  }
233
335
  }
@@ -24,7 +24,7 @@ export class MrdEmailField {
24
24
  };
25
25
  }
26
26
  render() {
27
- return (h(Host, { key: '3253124dadc7f0b3f120c11f8c35604a2f671b0a' }, h("div", { key: 'db820652452145c30f21b41a65942c0a4c6f45f1', class: "mrd-email-field" }, h(FieldLabel, { key: '8b1c5a84afce9fd18c3f644e5c92f07363e116d4', base: "mrd-email-field", label: this.label, required: this.required }), h("input", { key: 'dc4bada3208aa6cb7585b1872f53605177371493', class: controlClass('mrd-email-field', !!this.error), type: "email", name: this.name, value: this.value, placeholder: this.placeholder || 'name@example.com', required: this.required, disabled: this.disabled, onInput: this.handleInput, onBlur: this.handleBlur }), h(FieldError, { key: 'c44377d1b6a4a4c468b080b23eecd4ba9ca326e1', base: "mrd-email-field", error: this.error }))));
27
+ return (h(Host, { key: 'e185145e628e3a23dd338d93dd8e42bf11569728' }, h("div", { key: '7d77dd982543a8add5fa1f47aa7985cc15079080', class: "mrd-email-field" }, h(FieldLabel, { key: '14eb00a535a052ebe3cb2793cc7dfc05168c402b', base: "mrd-email-field", label: this.label, required: this.required }), h("input", { key: '11fb9856144f7da30a054b4433350d80d316de64', class: controlClass('mrd-email-field', !!this.error), type: "email", name: this.name, value: this.value, placeholder: this.placeholder || 'name@example.com', required: this.required, disabled: this.disabled, onInput: this.handleInput, onBlur: this.handleBlur }), h(FieldError, { key: 'be33c0a43cd99a7152c1d10b7067ea5e52834742', base: "mrd-email-field", error: this.error }))));
28
28
  }
29
29
  static get is() { return "mrd-email-field"; }
30
30
  static get encapsulation() { return "scoped"; }