@memberjunction/ng-base-forms 6.1.0-edge.6 → 6.1.0

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.
@@ -1068,6 +1068,45 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1068
1068
  get FieldInfo() {
1069
1069
  return this.Record?.EntityInfo?.Fields?.find(f => f.Name === this.FieldName);
1070
1070
  }
1071
+ /** Memoized answer for {@link IsFieldReadableByUser}; invalidated in ngOnChanges. */
1072
+ _isReadableByUser;
1073
+ /** Memoized answer for {@link IsFieldWritableByUser}; invalidated in ngOnChanges. */
1074
+ _isWritableByUser;
1075
+ /** The `Record.IsSaved` state {@link _isWritableByUser} was computed against. */
1076
+ _isWritableForSavedState;
1077
+ /**
1078
+ * Whether field-level security permits the current user to READ this field.
1079
+ *
1080
+ * The template gates the whole field on this BEFORE anything reads a value, because
1081
+ * `BaseEntity.Get()` throws for a denied field — so rendering one at all would take out the
1082
+ * entire form rather than hiding a column. Everything downstream (`Value`, `ShouldHideField`,
1083
+ * FK resolution) is safe only because this check runs first.
1084
+ *
1085
+ * Memoized: template getters are evaluated on every change-detection cycle, and
1086
+ * `GetDeniedReadFields` walks every field on the entity and aggregates permissions across the
1087
+ * user's roles. `ngOnChanges` clears the memo when `Record` or `FieldName` changes.
1088
+ *
1089
+ * Fails OPEN — on a missing entity, missing user, or an entity with field security switched
1090
+ * off — matching `BaseEntity`'s own gate. A form that hid fields because no user had resolved
1091
+ * yet would be worse than one that shows them, since the server is the real boundary.
1092
+ */
1093
+ get IsFieldReadableByUser() {
1094
+ if (this._isReadableByUser === undefined) {
1095
+ this._isReadableByUser = this.computeIsFieldReadableByUser();
1096
+ }
1097
+ return this._isReadableByUser;
1098
+ }
1099
+ computeIsFieldReadableByUser() {
1100
+ const entityInfo = this.Record?.EntityInfo;
1101
+ if (!entityInfo?.EnableFieldLevelSecurity) {
1102
+ return true; // one boolean for the overwhelming majority of entities
1103
+ }
1104
+ const user = this.ProviderToUse?.CurrentUser;
1105
+ if (!user || !this.FieldName) {
1106
+ return true;
1107
+ }
1108
+ return !entityInfo.GetDeniedReadFields(user).has(this.FieldName.trim().toLowerCase());
1109
+ }
1071
1110
  /** Display name from metadata or override */
1072
1111
  get DisplayName() {
1073
1112
  if (this.DisplayNameOverride)
@@ -1102,7 +1141,7 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1102
1141
  return;
1103
1142
  const oldValue = this.Record.Get(this.FieldName);
1104
1143
  this.Record.Set(this.FieldName, newValue);
1105
- this._touched = true;
1144
+ this.markTouched();
1106
1145
  this.runFieldValidation();
1107
1146
  this.ValueChange.emit({ FieldName: this.FieldName, OldValue: oldValue, NewValue: newValue });
1108
1147
  }
@@ -1110,7 +1149,54 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1110
1149
  get IsFieldReadOnly() {
1111
1150
  if (!this.FieldInfo)
1112
1151
  return false;
1113
- return this.FieldInfo.ReadOnly;
1152
+ return this.FieldInfo.ReadOnly || !this.IsFieldWritableByUser;
1153
+ }
1154
+ /**
1155
+ * Whether field-level security permits the current user to WRITE this field, against the verb
1156
+ * that actually applies to the record in hand: Create on a new record, Update on a saved one.
1157
+ *
1158
+ * Read access is handled separately and earlier ({@link IsFieldReadableByUser}) — a field the
1159
+ * user cannot see is not rendered at all. This is the narrower case of a field they CAN see
1160
+ * but may not change, which should render as a read-only value rather than an editable control.
1161
+ *
1162
+ * Both verbs matter and they fail differently, which is why this picks by record state rather
1163
+ * than checking update alone:
1164
+ *
1165
+ * - **Update denied** — the server rejects the save. Without this, the user types, saves, and
1166
+ * gets an error naming a field they had every reason to think was editable.
1167
+ * - **Create denied** — the server does NOT reject. It silently drops the value and takes the
1168
+ * column default, matching the read path where a denied field is simply absent. So an
1169
+ * editable control on a new record quietly discards what was typed, with no error anywhere.
1170
+ * That is the worse of the two, and the only signal the user gets is this.
1171
+ *
1172
+ * Memoized alongside the read check and invalidated by the same `ngOnChanges`, for the same
1173
+ * reason: template getters run every change-detection cycle and the underlying walk covers
1174
+ * every field on the entity.
1175
+ */
1176
+ get IsFieldWritableByUser() {
1177
+ // Keyed on IsSaved as well as memoized: the answer switches from the Create rules to the
1178
+ // Update rules the moment a new record is saved, and that happens WITHOUT ngOnChanges firing
1179
+ // — the Record input is the same object, only its state changed. A plain memo would keep
1180
+ // answering with the create-time verb for the rest of the form's life.
1181
+ const isSaved = this.Record?.IsSaved === true;
1182
+ if (this._isWritableByUser === undefined || this._isWritableForSavedState !== isSaved) {
1183
+ this._isWritableByUser = this.computeIsFieldWritableByUser();
1184
+ this._isWritableForSavedState = isSaved;
1185
+ }
1186
+ return this._isWritableByUser;
1187
+ }
1188
+ computeIsFieldWritableByUser() {
1189
+ const entityInfo = this.Record?.EntityInfo;
1190
+ if (!entityInfo?.EnableFieldLevelSecurity) {
1191
+ return true; // one boolean for the overwhelming majority of entities
1192
+ }
1193
+ const user = this.ProviderToUse?.CurrentUser;
1194
+ if (!user || !this.FieldName) {
1195
+ return true; // fail open, matching the read gate
1196
+ }
1197
+ return this.Record?.IsSaved
1198
+ ? entityInfo.IsFieldUpdatableByUser(this.FieldName, user)
1199
+ : entityInfo.IsFieldCreatableByUser(this.FieldName, user);
1114
1200
  }
1115
1201
  /** Whether this field has been modified (dirty). Only shown for saved records (not new). */
1116
1202
  get IsDirty() {
@@ -1136,17 +1222,45 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1136
1222
  // ---- INLINE VALIDATION ----
1137
1223
  /** Whether the user has interacted with (changed) this field */
1138
1224
  _touched = false;
1225
+ /**
1226
+ * `FormContext.validationRevision` as it stood when the user last edited this field. Compared with
1227
+ * the current revision in {@link FieldErrors}: equal means the edit came AFTER the last failed
1228
+ * save, so the user is addressing it and live local validation is the truth; lower means the
1229
+ * failure is newer than the edit, so the form-level errors must win.
1230
+ */
1231
+ _touchedAtRevision = 0;
1139
1232
  /** Set when an image upload is rejected (type/size) — shown with field validation. */
1140
1233
  imageUploadError = null;
1141
1234
  /** Locally computed validation errors for this field */
1142
1235
  _fieldErrors = [];
1236
+ /**
1237
+ * The ONE way a user interaction marks this field touched: every site must also stamp the
1238
+ * revision, or a field touched after a failed save would keep showing the stale form-level error.
1239
+ */
1240
+ markTouched() {
1241
+ this._touched = true;
1242
+ this._touchedAtRevision = this.FormContext?.validationRevision ?? 0;
1243
+ }
1244
+ /**
1245
+ * True when the user has edited this field since the form last published validation errors.
1246
+ *
1247
+ * Plain `_touched` was the wrong test: it stayed true from the moment the user typed, so after a
1248
+ * failed save the field returned its own — necessarily local, synchronous — validation, and an
1249
+ * error the SERVER had just reported for that very field (a `ValidateAsync()` refusal, which the
1250
+ * client cannot compute) never showed on the one field the user had just been in.
1251
+ */
1252
+ get editedSinceLastValidationFailure() {
1253
+ return this._touched && this._touchedAtRevision === (this.FormContext?.validationRevision ?? 0);
1254
+ }
1143
1255
  /** Validation errors for this field from the best available source */
1144
1256
  get FieldErrors() {
1145
- // If touched, use fresh local validation (covers real-time feedback)
1146
- if (this._touched) {
1257
+ // Edited after the last failure: fresh local validation (real-time feedback, and how a
1258
+ // server-reported error clears — a later valid edit yields no local errors).
1259
+ if (this.editedSinceLastValidationFailure) {
1147
1260
  return this._fieldErrors;
1148
1261
  }
1149
- // Otherwise use form-level errors from save failure (covers untouched fields)
1262
+ // Otherwise the form-level errors from the failed save whether this field was never touched,
1263
+ // or was touched before the save that failed.
1150
1264
  const contextErrors = this.FormContext?.validationErrors;
1151
1265
  if (contextErrors && contextErrors.length > 0) {
1152
1266
  return contextErrors.filter(e => e.Source === this.FieldName);
@@ -1209,6 +1323,14 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1209
1323
  }
1210
1324
  /** Whether this field should be hidden (empty in read-only mode) */
1211
1325
  get ShouldHideField() {
1326
+ // A read-denied field is always hidden. This must run before Value is touched: the
1327
+ // template's own @if guards rendering behind IsFieldReadableByUser, but programmatic
1328
+ // callers (MjCollapsiblePanelComponent.hasRenderableContent sweeps ShouldHideField to
1329
+ // decide section visibility) reach this getter directly, and BaseEntity.Get() THROWS for
1330
+ // a denied field. Returning true here is also what lets a section whose fields are all
1331
+ // denied collapse away instead of rendering an empty card.
1332
+ if (!this.IsFieldReadableByUser)
1333
+ return true;
1212
1334
  if (this.EditMode)
1213
1335
  return false;
1214
1336
  if (!this.HideWhenEmptyInReadOnlyMode)
@@ -1248,9 +1370,15 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1248
1370
  const fkValue = this.Value;
1249
1371
  if (fkValue == null || fkValue === '')
1250
1372
  return null;
1251
- // Fast path: name field is joined into our record's view
1373
+ // Fast path: name field is joined into our record's view.
1374
+ //
1375
+ // The joined display column is a SEPARATE EntityField from the FK it accompanies, and can be
1376
+ // denied independently — CompanyID readable while Company is not. `IsFieldReadableByUser`
1377
+ // gates this component on its OWN field name, so it does not cover this read; without the
1378
+ // check here, Get() throws and takes out the form. Falling through to the async lookup is
1379
+ // right: that path reads the related record, which has its own permissions.
1252
1380
  const nameFieldMap = this.FieldInfo?.RelatedEntityNameFieldMap;
1253
- if (nameFieldMap) {
1381
+ if (nameFieldMap && this.Record.EntityInfo?.IsFieldReadableByUser(nameFieldMap, this.ProviderToUse?.CurrentUser)) {
1254
1382
  const val = this.Record.Get(nameFieldMap);
1255
1383
  if (val != null && val !== '')
1256
1384
  return String(val);
@@ -1613,17 +1741,26 @@ export class MjFormFieldComponent extends BaseAngularComponent {
1613
1741
  const relatedEntity = this.getRelatedEntityInfo();
1614
1742
  if (!relatedEntity)
1615
1743
  return null;
1616
- const nameField = relatedEntity.NameField;
1617
1744
  const pkFields = relatedEntity.PrimaryKeys;
1745
+ // Field-level security applies to the RELATED entity here, not this record's. A name field
1746
+ // the user cannot read would throw from every downstream Get() — so drop to the primary key,
1747
+ // which is unrestrictable by construction. This plan is the single chokepoint for every
1748
+ // related-record read the FK control makes (dropdown rows, search, create-and-select), so
1749
+ // filtering here is what keeps those safe rather than four separate guards.
1750
+ const user = this.ProviderToUse?.CurrentUser;
1751
+ const nameField = relatedEntity.NameField && relatedEntity.IsFieldReadableByUser(relatedEntity.NameField.Name, user)
1752
+ ? relatedEntity.NameField
1753
+ : null;
1618
1754
  if (!nameField && (!pkFields || pkFields.length !== 1))
1619
1755
  return null;
1620
1756
  const nameFieldName = nameField ? nameField.Name : pkFields[0].Name;
1621
1757
  const pkFieldName = this.FieldInfo?.RelatedEntityFieldName || (pkFields?.[0]?.Name ?? 'ID');
1622
1758
  // A field flagged ExtendedType='Icon' holds a per-row icon class — render it
1623
1759
  // as the row's leading glyph, not as a text column.
1624
- const iconField = relatedEntity.Fields.find(f => f.ExtendedType === 'Icon');
1760
+ const iconField = relatedEntity.Fields.find(f => f.ExtendedType === 'Icon' && relatedEntity.IsFieldReadableByUser(f.Name, user));
1625
1761
  const iconFieldName = iconField?.Name ?? null;
1626
- const columnFields = this.computeOrderedColumns(relatedEntity, nameFieldName, pkFieldName, iconFieldName);
1762
+ const columnFields = this.computeOrderedColumns(relatedEntity, nameFieldName, pkFieldName, iconFieldName)
1763
+ .filter(c => relatedEntity.IsFieldReadableByUser(c, user));
1627
1764
  const extraFieldNames = columnFields.filter(c => c !== nameFieldName);
1628
1765
  this._fkColumnPlan = {
1629
1766
  NameFieldName: nameFieldName,
@@ -2183,7 +2320,12 @@ export class MjFormFieldComponent extends BaseAngularComponent {
2183
2320
  }
2184
2321
  this._fkQuery = query;
2185
2322
  const accessor = (r) => ({ get: (field) => r.Get(field) });
2186
- const searchField = this.FKSearchField || plan.NameFieldName;
2323
+ // plan.NameFieldName is already vetted for readability by buildColumnPlan; FKSearchField is
2324
+ // caller-supplied and is not, so it falls back rather than throwing on the first row.
2325
+ const relatedInfo = records[0]?.EntityInfo;
2326
+ const searchField = this.FKSearchField && relatedInfo?.IsFieldReadableByUser(this.FKSearchField, this.ProviderToUse?.CurrentUser) !== false
2327
+ ? this.FKSearchField
2328
+ : plan.NameFieldName;
2187
2329
  const matches = FilterCachedFKRows(records, query, MjFormFieldComponent.FK_FOCUS_SHOW_LIMIT, r => r.Get(searchField));
2188
2330
  this.FKLoading = false;
2189
2331
  this.applySuggestions(this.buildSuggestions(matches.map(accessor), plan, query), plan);
@@ -2696,6 +2838,13 @@ export class MjFormFieldComponent extends BaseAngularComponent {
2696
2838
  return String(val);
2697
2839
  }
2698
2840
  ngOnChanges(changes) {
2841
+ // Field security depends on both the record's entity and which field this is, so the
2842
+ // memoized answer has to be dropped whenever either changes.
2843
+ if (changes['Record'] || changes['FieldName']) {
2844
+ this._isReadableByUser = undefined;
2845
+ this._isWritableByUser = undefined;
2846
+ this._isWritableForSavedState = undefined;
2847
+ }
2699
2848
  if (changes['FormContext'] || changes['EditMode'] || changes['Record']) {
2700
2849
  // Reset FK state when record changes
2701
2850
  if (changes['Record']) {
@@ -2837,14 +2986,14 @@ export class MjFormFieldComponent extends BaseAngularComponent {
2837
2986
  const allowed = new Set(['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/svg+xml']);
2838
2987
  if (file.type && !allowed.has(file.type)) {
2839
2988
  this.imageUploadError = 'That file is not a supported image type (PNG, JPEG, GIF, WebP, or SVG).';
2840
- this._touched = true;
2989
+ this.markTouched();
2841
2990
  this.cdr.markForCheck();
2842
2991
  return;
2843
2992
  }
2844
2993
  const absoluteReadCap = 8 * 1024 * 1024;
2845
2994
  if (file.size > absoluteReadCap) {
2846
2995
  this.imageUploadError = `File is too large to read (max ${FormatByteSize(absoluteReadCap)}).`;
2847
- this._touched = true;
2996
+ this.markTouched();
2848
2997
  this.cdr.markForCheck();
2849
2998
  return;
2850
2999
  }
@@ -2853,7 +3002,7 @@ export class MjFormFieldComponent extends BaseAngularComponent {
2853
3002
  const dataUri = await this.readFileAsDataUri(file);
2854
3003
  if (!IsInlineImageDataUri(dataUri)) {
2855
3004
  this.imageUploadError = 'That file is not a supported image type (PNG, JPEG, GIF, WebP, or SVG).';
2856
- this._touched = true;
3005
+ this.markTouched();
2857
3006
  this.cdr.markForCheck();
2858
3007
  return;
2859
3008
  }
@@ -2869,11 +3018,11 @@ export class MjFormFieldComponent extends BaseAngularComponent {
2869
3018
  return;
2870
3019
  }
2871
3020
  this.imageUploadError = `Image is too large for this field (max ${this.ImageMaxSizeLabel}). Try a smaller file or paste an image URL.`;
2872
- this._touched = true;
3021
+ this.markTouched();
2873
3022
  }
2874
3023
  catch {
2875
3024
  this.imageUploadError = 'Could not read that image.';
2876
- this._touched = true;
3025
+ this.markTouched();
2877
3026
  }
2878
3027
  this.cdr.markForCheck();
2879
3028
  }
@@ -3007,12 +3156,12 @@ export class MjFormFieldComponent extends BaseAngularComponent {
3007
3156
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MjFormFieldComponent, selectors: [["mj-form-field"]], inputs: { Record: "Record", EditMode: "EditMode", FieldName: "FieldName", Type: "Type", LinkType: "LinkType", ShowLabel: "ShowLabel", FormContext: "FormContext", _deprecatedRecord: [0, "record", "_deprecatedRecord"], _deprecatedFormContext: [0, "formContext", "_deprecatedFormContext"], HideWhenEmptyInReadOnlyMode: "HideWhenEmptyInReadOnlyMode", DisplayNameOverride: "DisplayNameOverride", PossibleValuesOverride: "PossibleValuesOverride", FKDropdownMaxWidth: "FKDropdownMaxWidth", FKHighlightMatches: "FKHighlightMatches", AllowFKCreate: "AllowFKCreate", FKCreatePresentation: "FKCreatePresentation" }, outputs: { ValueChange: "ValueChange", Navigate: "Navigate" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 1, vars: 1, consts: [["scopeRow", ""], ["imageFileInput", ""], [1, "mj-forms-field", 3, "mj-forms-field--readonly", "mj-forms-field--dirty"], [1, "mj-forms-field", "mj-forms-field--editing", 3, "mj-forms-field--dirty", "mj-forms-field--required-empty", "mj-forms-field--has-error", "mj-forms-field--has-warning"], [1, "mj-forms-field"], [1, "mj-forms-field-label", 3, "innerHTML"], [1, "mj-forms-field-control"], [1, "mj-forms-field-value"], [1, "mj-forms-image"], [1, "mj-forms-color"], [1, "mj-forms-json"], ["title", "View related record", 1, "mj-forms-field-link", "mj-forms-field-link--fk"], ["title", "View related record", 1, "mj-forms-field-link", "mj-forms-field-link--fk", 3, "click"], ["alt", "", 1, "mj-forms-image-preview", 3, "src"], [1, "mj-forms-image-meta"], ["title", "Open image", 1, "mj-forms-field-link"], ["title", "Open image", 1, "mj-forms-field-link", 3, "click"], [1, "fa-solid", "fa-arrow-up-right-from-square", "mj-forms-field-link-icon"], [1, "mj-forms-color-swatch"], ["title", "Send email", 1, "mj-forms-field-link", 3, "click"], [1, "fa-solid", "fa-envelope", "mj-forms-field-link-icon"], ["title", "Open link", 1, "mj-forms-field-link", 3, "click"], ["title", "View related record", 1, "mj-forms-field-link"], ["title", "View related record", 1, "mj-forms-field-link", 3, "click"], [1, "mj-forms-field-value", "mj-forms-field-value--rich"], [1, "mj-forms-field-value", "mj-forms-field-value--rich", 3, "innerHTML"], [1, "mj-forms-field-value", "mj-forms-field-value--code"], [1, "mj-forms-field-value", "mj-forms-field-value--long"], [1, "mj-forms-field-checkbox"], ["type", "checkbox", "disabled", "", 3, "checked"], [3, "data"], [3, "value", "language", "readonly"], [1, "mj-forms-field", "mj-forms-field--editing"], [1, "mj-fk-search", 3, "mj-fk-search--matched", "mj-fk-search--scoped"], [1, "mj-forms-image", "mj-forms-image--edit"], [1, "mj-forms-color", "mj-forms-color--edit"], [1, "mj-forms-field-input", "mj-forms-field-input--textarea", "mj-forms-json", 3, "value"], [1, "mj-forms-field-input--code", 3, "value", "language"], [1, "mj-forms-field-validation", "mj-forms-field-validation--error"], [1, "mj-forms-field-validation", "mj-forms-field-validation--warning"], [1, "mj-fk-search"], ["title", "Linked record", 1, "fa-solid", "fa-link", "mj-fk-link-icon"], ["type", "text", "placeholder", "Search...", 1, "mj-forms-field-input", 3, "input", "focus", "blur", "keydown", "value"], [1, "mj-fk-scope-pill", 3, "mj-fk-scope-pill--open", "title"], ["title", "Clear", 1, "mj-fk-clear"], [1, "fa-solid", "fa-magnifying-glass", "mj-fk-search-icon"], [1, "mj-fk-dropdown", "mj-fk-dropdown--multi", 3, "ngStyle", "mj-fk-dropdown--above", "mj-fk-dropdown--scope-open"], [1, "mj-fk-scope-pill", 3, "mousedown", "title"], [1, "fa-solid", "fa-magnifying-glass", "mj-fk-scope-pill-icon"], [1, "mj-fk-scope-pill-label"], [1, "fa-solid", "mj-fk-scope-pill-caret"], ["title", "Clear", 1, "mj-fk-clear", 3, "mousedown"], [1, "fa-solid", "fa-xmark"], [1, "mj-fk-dropdown", "mj-fk-dropdown--multi", 3, "ngStyle"], [1, "mj-fk-scope-menu", 3, "mj-fk-scope-menu--bottom"], [1, "mj-fk-current"], [1, "mj-fk-status"], [1, "mj-fk-status", "mj-fk-status--empty"], [1, "mj-fk-options"], ["type", "button", 1, "mj-fk-create-footer", 3, "title"], [1, "mj-fk-scope-menu"], [1, "mj-fk-scope-grp"], [4, "ngTemplateOutlet", "ngTemplateOutletContext"], [1, "mj-fk-scope-divider"], [1, "mj-fk-scope-mi", 3, "mousedown"], [1, "mj-fk-col-reorder"], [1, "mj-fk-scope-mi-label"], [1, "mj-fk-col-eye-slot"], ["type", "button", 1, "mj-fk-col-toggle", 3, "mj-fk-col-toggle--on", "title"], ["type", "button", "title", "Move column up", 1, "mj-fk-col-move", "mj-fk-col-move--up", 3, "mousedown", "disabled"], [1, "fa-solid", "fa-chevron-up"], ["type", "button", "title", "Move column down", 1, "mj-fk-col-move", "mj-fk-col-move--down", 3, "mousedown", "disabled"], [1, "fa-solid", "fa-chevron-down"], ["type", "button", 1, "mj-fk-col-toggle", 3, "mousedown", "title"], [1, "fa-solid"], [1, "mj-fk-current-head"], [1, "fa-solid", "fa-circle-check"], ["title", "Keep this selection", 1, "mj-fk-current-row", 3, "mousedown"], [1, "mj-fk-current-icon", 3, "class"], [1, "mj-fk-current-name", 3, "innerHTML"], [1, "mj-fk-current-detail"], ["type", "button", "title", "Clear selection", 1, "mj-fk-current-clear", 3, "mousedown"], [1, "mj-fk-current-icon"], [1, "fa-solid", "fa-circle-notch", "fa-spin"], [1, "fa-solid", "fa-magnifying-glass"], [1, "mj-fk-grid"], [1, "mj-fk-grid-row", "mj-fk-grid-row--header"], [1, "mj-fk-hcell", "mj-fk-hcell--icon"], [1, "mj-fk-hcell", 3, "title"], [1, "mj-fk-grid-row", 3, "mj-fk-grid-row--active", "mj-fk-grid-row--selected"], [1, "mj-fk-hcell", 3, "mousedown", "title"], [1, "mj-fk-hcell-label"], ["title", "Drag to resize", 1, "mj-fk-resizer", 3, "mousedown"], [1, "mj-fk-grid-row", 3, "mousedown", "mouseenter"], [1, "mj-fk-cell", "mj-fk-cell--icon"], [3, "class"], [1, "mj-fk-cell", "mj-fk-cell--name", 3, "mj-fk-cell--fill"], [1, "mj-fk-cell", 3, "mj-fk-cell--fill", "title"], [1, "mj-fk-cell", "mj-fk-cell--name"], ["title", "Currently selected", 1, "fa-solid", "fa-check", "mj-fk-selected-check"], [3, "innerHTML"], [1, "mj-fk-cell", 3, "title"], ["type", "button", 1, "mj-fk-create-footer", 3, "mousedown", "title"], [1, "fa-solid", "fa-plus", "mj-fk-create-icon"], [1, "mj-fk-create-label"], ["aria-hidden", "true", 1, "mj-forms-image-preview", "mj-forms-image-preview--empty"], [1, "mj-forms-image-hint"], [1, "mj-forms-image-actions"], ["type", "file", "accept", "image/png,image/jpeg,image/gif,image/webp,image/svg+xml", 1, "mj-forms-image-file", 3, "change"], ["type", "button", 1, "mj-forms-image-btn", 3, "click"], [1, "fa-solid", "fa-upload"], ["type", "button", 1, "mj-forms-image-btn", "mj-forms-image-btn--ghost"], ["type", "text", "placeholder", "https://\u2026 or paste a data URI", 1, "mj-forms-field-input", 3, "input", "value"], [1, "fa-regular", "fa-image"], ["type", "button", 1, "mj-forms-image-btn", "mj-forms-image-btn--ghost", 3, "click"], ["type", "color", 1, "mj-forms-color-picker", 3, "input", "value"], ["type", "text", "placeholder", "#RRGGBB", 1, "mj-forms-field-input", "mj-forms-color-hex", 3, "input", "value"], [1, "mj-forms-field-input", "mj-forms-field-input--textarea", "mj-forms-json", 3, "input", "blur", "value"], [1, "mj-forms-field-input--code", 3, "change", "value", "language"], ["type", "text", 1, "mj-forms-field-input", 3, "value"], [1, "mj-forms-field-input", "mj-forms-field-input--textarea", 3, "value"], ["type", "number", 1, "mj-forms-field-input", "mj-forms-field-input--number", 3, "value"], ["type", "date", 1, "mj-forms-field-input", "mj-forms-field-input--date", 3, "value"], ["type", "text", 1, "mj-forms-field-input", 3, "input", "value"], [1, "mj-forms-field-input", "mj-forms-field-input--textarea", 3, "input", "value"], ["type", "number", 1, "mj-forms-field-input", "mj-forms-field-input--number", 3, "change", "value"], ["type", "date", 1, "mj-forms-field-input", "mj-forms-field-input--date", 3, "change", "value"], ["type", "checkbox", 3, "change", "checked"], ["tabindex", "0", 1, "mj-custom-select-trigger", 3, "click", "blur"], [1, "mj-custom-select-value"], [1, "fa-solid", "fa-chevron-down", "mj-custom-select-chevron"], [1, "mj-fk-dropdown", 3, "ngStyle", "mj-fk-dropdown--above"], [1, "mj-fk-dropdown", 3, "ngStyle"], [1, "mj-fk-option", 3, "mj-fk-option--selected"], [1, "mj-fk-option", 3, "mousedown"], ["type", "text", "placeholder", "Type to search...", 1, "mj-forms-field-input", 3, "input", "focus", "blur", "value"], [1, "mj-fk-option"], ["type", "text", 1, "mj-forms-field-input", 3, "change", "value"], [1, "mj-forms-field-validation-item"], [1, "fa-solid", "fa-circle-exclamation"], [1, "fa-solid", "fa-triangle-exclamation"]], template: function MjFormFieldComponent_Template(rf, ctx) { if (rf & 1) {
3008
3157
  i0.ɵɵconditionalCreate(0, MjFormFieldComponent_Conditional_0_Template, 2, 2);
3009
3158
  } if (rf & 2) {
3010
- i0.ɵɵconditional(ctx.Record && (ctx.Record.IsSaved || !ctx.IsFieldReadOnly) ? 0 : -1);
3159
+ i0.ɵɵconditional(ctx.IsFieldReadableByUser && ctx.Record && (ctx.Record.IsSaved || !ctx.IsFieldReadOnly) ? 0 : -1);
3011
3160
  } }, dependencies: [i1.NgTemplateOutlet, i1.NgStyle, i2.MarkdownComponent, i3.CodeEditorComponent, i4.MJSafeRichHtmlPipe], styles: ["\n\n\n[_nghost-%COMP%] {\n --mj-forms-field-label-width: 200px;\n --mj-forms-field-gap: 12px;\n\n display: contents;\n}\n\n.mj-forms-field[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: var(--mj-forms-field-label-width) 1fr;\n gap: var(--mj-forms-field-gap);\n align-items: baseline;\n padding: 8px 0;\n transition: all var(--mj-transition-base) ease;\n}\n\n\n.mj-forms-field--editing[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 6px;\n margin: 0 -8px;\n padding: 8px 8px;\n}\n\n\n\n\n.mj-forms-field-label[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n padding-top: 2px;\n word-break: break-word;\n}\n\n\n\n\n.mj-forms-field-control[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-field-control[_ngcontent-%COMP%] > *[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n\n\n\n.mj-forms-field--dirty[_ngcontent-%COMP%] .mj-forms-field-label[_ngcontent-%COMP%]::after {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--mj-status-warning);\n margin-left: 6px;\n vertical-align: middle;\n}\n\n\n\n\n.mj-forms-field--required-empty[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%]:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty[_ngcontent-%COMP%] .mj-custom-select-trigger[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty[_ngcontent-%COMP%] .mj-fk-search[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n}\n\n\n\n\n.mj-forms-field-value[_ngcontent-%COMP%] {\n font-size: 14px;\n color: var(--mj-text-primary);\n min-height: 24px;\n display: flex;\n align-items: center;\n overflow-wrap: break-word;\n word-break: break-word;\n}\n\n.mj-forms-field-value--long[_ngcontent-%COMP%] {\n white-space: pre-line;\n max-height: 300px;\n overflow: auto;\n}\n\n\n.mj-forms-field-value--rich[_ngcontent-%COMP%] {\n display: block;\n align-items: initial;\n max-height: 480px;\n overflow: auto;\n width: 100%;\n}\n\n\n.mj-forms-field-value--code[_ngcontent-%COMP%] {\n display: block;\n align-items: initial;\n width: 100%;\n}\n\n.mj-forms-field-value--code[_ngcontent-%COMP%] mj-code-editor[_ngcontent-%COMP%], \n.mj-forms-field-input--code[_ngcontent-%COMP%] {\n display: block;\n width: 100%;\n min-height: 160px;\n max-height: 480px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius, 4px);\n overflow: auto;\n}\n\n\n.mj-forms-field--readonly[_ngcontent-%COMP%] {\n opacity: 0.7;\n}\n\n.mj-forms-field--readonly[_ngcontent-%COMP%] .mj-forms-field-value[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n\n\n\n\n.mj-forms-field-link[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-brand-primary);\n cursor: pointer;\n font-weight: 400;\n font-size: 14px;\n transition: color var(--mj-transition-base);\n text-decoration: none;\n}\n\n.mj-forms-field-link[_ngcontent-%COMP%]:hover {\n color: var(--mj-brand-primary-hover);\n}\n\n.mj-forms-field-link-icon[_ngcontent-%COMP%] {\n font-size: 11px;\n opacity: 0;\n transition: opacity var(--mj-transition-base);\n}\n\n.mj-forms-field-link[_ngcontent-%COMP%]:hover .mj-forms-field-link-icon[_ngcontent-%COMP%] {\n opacity: 1;\n}\n\n\n.mj-forms-field-link--fk[_ngcontent-%COMP%] {\n font-size: 14px;\n font-weight: 400;\n}\n\n\n\n\n.mj-forms-field-input[_ngcontent-%COMP%] {\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-forms-field-input[_ngcontent-%COMP%]:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-forms-field-input[_ngcontent-%COMP%]::placeholder {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n\n.mj-forms-field-input--textarea[_ngcontent-%COMP%] {\n resize: vertical;\n min-height: 60px;\n border: 1.5px solid var(--mj-border-default);\n border-radius: 6px;\n padding: 8px 10px;\n background: var(--mj-bg-surface);\n font-family: inherit;\n}\n\n.mj-forms-field-input--textarea[_ngcontent-%COMP%]:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n background: var(--mj-bg-surface);\n padding: 8px 10px;\n border-radius: 6px;\n}\n\n\n.mj-forms-field-input--number[_ngcontent-%COMP%] {\n max-width: 200px;\n}\n\n\n.mj-forms-field-input--date[_ngcontent-%COMP%] {\n max-width: 220px;\n}\n\n\n\n\n.mj-forms-field-checkbox[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-field-checkbox[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%] {\n appearance: none;\n width: 18px;\n height: 18px;\n border: 2px solid var(--mj-border-default);\n border-radius: 4px;\n cursor: pointer;\n position: relative;\n transition: all var(--mj-transition-base) ease;\n flex-shrink: 0;\n}\n\n.mj-forms-field-checkbox[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%]:checked {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-forms-field-checkbox[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%]:checked::after {\n content: '';\n position: absolute;\n left: 5px;\n top: 1px;\n width: 5px;\n height: 9px;\n border: solid var(--mj-text-inverse);\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n}\n\n.mj-forms-field-checkbox[_ngcontent-%COMP%] input[type=\"checkbox\"][_ngcontent-%COMP%]:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n\n\n\n.mj-custom-select-trigger[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n cursor: pointer;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-custom-select-trigger[_ngcontent-%COMP%]:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-custom-select-value[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mj-custom-select-value--placeholder[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n.mj-custom-select-chevron[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted);\n transition: transform var(--mj-transition-base) ease;\n flex-shrink: 0;\n margin-left: 8px;\n}\n\n.mj-custom-select-chevron--open[_ngcontent-%COMP%] {\n transform: rotate(180deg);\n}\n\n\n\n\n.mj-fk-search[_ngcontent-%COMP%] {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n}\n\n.mj-fk-search[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n padding-right: 28px;\n}\n\n.mj-fk-search-icon[_ngcontent-%COMP%] {\n position: absolute;\n right: 4px;\n color: var(--mj-text-muted);\n font-size: 12px;\n pointer-events: none;\n}\n\n.mj-fk-clear[_ngcontent-%COMP%] {\n position: absolute;\n right: 2px;\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 11px;\n padding: 4px 6px;\n line-height: 1;\n border-radius: 3px;\n transition: all var(--mj-transition-base);\n}\n\n.mj-fk-clear[_ngcontent-%COMP%]:hover {\n color: var(--mj-status-error);\n background: color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n\n\n.mj-fk-search--matched[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom: 2px solid var(--mj-brand-primary);\n}\n\n\n\n.mj-fk-link-icon[_ngcontent-%COMP%] {\n position: absolute;\n right: 26px;\n color: var(--mj-brand-primary);\n font-size: 10px;\n pointer-events: none;\n z-index: 1;\n}\n\n\n.mj-forms-field-input--linked[_ngcontent-%COMP%] {\n padding-right: 46px;\n}\n\n\n\n\n\n.mj-fk-search--scoped[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n padding-right: 150px;\n}\n\n.mj-fk-scope-pill[_ngcontent-%COMP%] {\n position: absolute;\n right: 30px;\n \n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 120px;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n color: var(--mj-text-secondary);\n font-size: 10.5px;\n font-weight: 600;\n font-family: inherit;\n \n padding: 1px 7px;\n border-radius: 999px;\n cursor: pointer;\n white-space: nowrap;\n line-height: 1.35;\n transition: background var(--mj-transition-base), border-color var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-pill[_ngcontent-%COMP%]:hover, \n.mj-fk-scope-pill--open[_ngcontent-%COMP%] {\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-primary);\n}\n\n.mj-fk-scope-pill-icon[_ngcontent-%COMP%] {\n font-size: 9px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-pill-label[_ngcontent-%COMP%] {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-scope-pill-caret[_ngcontent-%COMP%] {\n font-size: 8px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n\n\n\n.mj-fk-scope-menu[_ngcontent-%COMP%] {\n position: absolute;\n top: 4px;\n right: 8px;\n z-index: 5;\n min-width: 180px;\n max-width: calc(100vw - 24px);\n max-height: 260px;\n overflow-y: auto;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n border-radius: var(--mj-radius-sm);\n box-shadow: var(--mj-shadow-lg);\n padding: 5px;\n animation: _ngcontent-%COMP%_mj-dropdown-in 120ms ease;\n}\n\n\n\n.mj-fk-scope-menu--bottom[_ngcontent-%COMP%] {\n top: auto;\n bottom: 4px;\n}\n\n.mj-fk-scope-grp[_ngcontent-%COMP%] {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-text-muted);\n padding: 7px 10px 4px;\n font-weight: 700;\n}\n\n.mj-fk-scope-mi[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 13px;\n border-radius: 5px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n}\n\n.mj-fk-scope-mi[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-scope-mi--on[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n\n\n.mj-fk-col-reorder[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 22px;\n flex-shrink: 0;\n}\n\n.mj-fk-col-move[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 10px;\n padding: 0;\n background: transparent;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 8px;\n opacity: 0; \n transition: opacity var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-mi[_ngcontent-%COMP%]:hover .mj-fk-col-move[_ngcontent-%COMP%]:not(:disabled) {\n opacity: 0.8;\n}\n\n.mj-fk-col-move[_ngcontent-%COMP%]:hover:not(:disabled) {\n opacity: 1;\n color: var(--mj-brand-primary);\n}\n\n\n.mj-fk-col-move[_ngcontent-%COMP%]:disabled {\n opacity: 0 !important;\n cursor: default;\n}\n\n.mj-fk-scope-mi-icon[_ngcontent-%COMP%] {\n width: 14px;\n font-size: 11px;\n opacity: 0.75;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-mi-label[_ngcontent-%COMP%] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n\n.mj-fk-col-eye-slot[_ngcontent-%COMP%] {\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n\n.mj-fk-col-toggle[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-col-toggle[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-secondary);\n}\n\n.mj-fk-col-toggle--on[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-scope-divider[_ngcontent-%COMP%] {\n height: 1px;\n background: var(--mj-border-subtle);\n margin: 4px 6px;\n}\n\n\n\n@media (max-width: 560px) {\n .mj-fk-search--scoped[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n padding-right: 116px;\n }\n .mj-fk-scope-pill[_ngcontent-%COMP%] {\n max-width: 88px;\n }\n .mj-fk-scope-menu[_ngcontent-%COMP%] {\n min-width: 160px;\n }\n .mj-fk-scope-mi[_ngcontent-%COMP%] {\n padding: 10px 10px;\n }\n}\n\n\n\n\n.mj-fk-dropdown[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n max-height: 240px;\n overflow-y: auto;\n padding: 4px;\n animation: _ngcontent-%COMP%_mj-dropdown-in 150ms ease;\n}\n\n.mj-fk-dropdown--above[_ngcontent-%COMP%] {\n animation: _ngcontent-%COMP%_mj-dropdown-in-above 150ms ease;\n}\n\n.mj-fk-option[_ngcontent-%COMP%] {\n padding: 10px 14px;\n font-size: 14px;\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: background var(--mj-transition-base);\n border-radius: 6px;\n}\n\n.mj-fk-option[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n.mj-fk-option--selected[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-option--selected[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n\n\n\n\n.mj-fk-dropdown--multi[_ngcontent-%COMP%] {\n padding: 4px;\n \n\n overflow: hidden;\n max-height: none;\n}\n\n\n\n\n.mj-fk-dropdown--multi[_ngcontent-%COMP%] .mj-fk-options[_ngcontent-%COMP%] {\n overflow: auto;\n max-height: 260px;\n}\n\n\n\n.mj-fk-dropdown--scope-open[_ngcontent-%COMP%] {\n overflow: visible;\n}\n\n\n\n\n\n\n.mj-fk-grid[_ngcontent-%COMP%] {\n display: grid;\n width: 100%;\n min-width: max-content;\n align-items: stretch;\n}\n\n\n\n\n.mj-fk-grid-row[_ngcontent-%COMP%] {\n display: contents;\n}\n\n\n.mj-fk-hcell[_ngcontent-%COMP%] {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 7px 12px;\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-elevated);\n border-bottom: 1px solid var(--mj-border-default);\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n}\n\n.mj-fk-hcell[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-hcell--icon[_ngcontent-%COMP%] {\n cursor: default;\n padding: 7px 8px;\n}\n\n.mj-fk-hcell--icon[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-elevated);\n}\n\n.mj-fk-hcell-label[_ngcontent-%COMP%] {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-sort-icon[_ngcontent-%COMP%] {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n\n.mj-fk-sort-icon--idle[_ngcontent-%COMP%] {\n opacity: 0.35;\n}\n\n\n.mj-fk-cell[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 320px;\n cursor: pointer;\n}\n\n\n\n.mj-fk-cell--fill[_ngcontent-%COMP%] {\n max-width: none;\n}\n\n\n\n.mj-fk-cell[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.mj-fk-cell--icon[_ngcontent-%COMP%] {\n justify-content: center;\n padding: 8px;\n max-width: none;\n color: var(--mj-text-muted);\n font-size: 13px;\n}\n\n.mj-fk-cell--name[_ngcontent-%COMP%] {\n font-weight: 500;\n font-size: 14px;\n color: var(--mj-text-primary);\n gap: 6px;\n}\n\n.mj-fk-selected-check[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 11px;\n flex-shrink: 0;\n}\n\n\n\n.mj-fk-grid-row--active[_ngcontent-%COMP%] > .mj-fk-cell[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);\n}\n\n\n\n.mj-fk-grid-row--selected[_ngcontent-%COMP%] > .mj-fk-cell[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 6%, transparent);\n}\n\n.mj-fk-grid-row--selected[_ngcontent-%COMP%] > .mj-fk-cell--name[_ngcontent-%COMP%] {\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n\n\n\n\n.mj-fk-cell--name[_ngcontent-%COMP%] .mj-forms-search-highlight, \n.mj-fk-cell[_ngcontent-%COMP%] .mj-forms-search-highlight {\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n\n.mj-fk-resizer[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n right: 0;\n width: 7px;\n height: 100%;\n cursor: col-resize;\n user-select: none;\n}\n\n.mj-fk-resizer[_ngcontent-%COMP%]::after {\n content: '';\n position: absolute;\n top: 20%;\n right: 2px;\n width: 2px;\n height: 60%;\n background: transparent;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-hcell[_ngcontent-%COMP%]:hover .mj-fk-resizer[_ngcontent-%COMP%]::after, \n.mj-fk-resizer[_ngcontent-%COMP%]:hover::after {\n background: var(--mj-border-strong);\n}\n\n\n.mj-fk-status[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 14px;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-status[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 12px;\n}\n\n.mj-fk-status--empty[_ngcontent-%COMP%] {\n font-style: italic;\n}\n\n\n\n.mj-fk-create-footer[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n background: var(--mj-bg-surface-card);\n border: none;\n border-top: 1px solid var(--mj-border-default);\n color: var(--mj-brand-primary);\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-create-footer[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-create-icon[_ngcontent-%COMP%] {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.mj-fk-create-label[_ngcontent-%COMP%] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n\n\n\n.mj-fk-current[_ngcontent-%COMP%] {\n border-bottom: 1px solid var(--mj-border-default);\n background: color-mix(in srgb, var(--mj-brand-primary) 7%, var(--mj-bg-surface-card));\n animation: _ngcontent-%COMP%_mj-fk-current-in 220ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n@keyframes _ngcontent-%COMP%_mj-fk-current-in {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.mj-fk-current-head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px 3px;\n font-size: 10px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-current-head[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 10px;\n}\n\n.mj-fk-current-row[_ngcontent-%COMP%] {\n position: relative;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 6px 12px 10px;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n\n.mj-fk-current-row[_ngcontent-%COMP%]::before {\n content: '';\n position: absolute;\n left: 0;\n top: 4px;\n bottom: 8px;\n width: 3px;\n border-radius: 0 2px 2px 0;\n background: var(--mj-brand-primary);\n}\n\n.mj-fk-current-row[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.mj-fk-current-icon[_ngcontent-%COMP%] {\n flex-shrink: 0;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-current-name[_ngcontent-%COMP%] {\n font-weight: 600;\n font-size: 14px;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-detail[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-clear[_ngcontent-%COMP%] {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-current-clear[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-status-error);\n}\n\n@keyframes _ngcontent-%COMP%_mj-dropdown-in {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes _ngcontent-%COMP%_mj-dropdown-in-above {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n\n\n\n.mj-forms-field-label[_ngcontent-%COMP%] .mj-forms-search-highlight {\n \n\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n\n\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%]:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-forms-field-input--textarea[_ngcontent-%COMP%] {\n border-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-forms-field-input--textarea[_ngcontent-%COMP%]:focus {\n border-color: var(--mj-status-error);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-custom-select-trigger[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error[_ngcontent-%COMP%] .mj-fk-search[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n\n\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%]:focus {\n background: color-mix(in srgb, var(--mj-status-warning) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-forms-field-input--textarea[_ngcontent-%COMP%] {\n border-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-forms-field-input--textarea[_ngcontent-%COMP%]:focus {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-warning) 8%, transparent);\n}\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-custom-select-trigger[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning[_ngcontent-%COMP%] .mj-fk-search[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n\n\n\n.mj-forms-field-validation[_ngcontent-%COMP%] {\n grid-column: 2;\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: -2px;\n overflow: hidden;\n animation: _ngcontent-%COMP%_mj-validation-slide-in 200ms ease-out;\n}\n\n.mj-forms-field-validation-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n font-size: 12px;\n line-height: 1.4;\n padding: 3px 0 1px;\n}\n\n.mj-forms-field-validation-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 11px;\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n\n.mj-forms-field-validation--error[_ngcontent-%COMP%] .mj-forms-field-validation-item[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.mj-forms-field-validation--error[_ngcontent-%COMP%] .mj-forms-field-validation-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n\n.mj-forms-field-validation--warning[_ngcontent-%COMP%] .mj-forms-field-validation-item[_ngcontent-%COMP%] {\n color: var(--mj-status-warning-text);\n}\n\n.mj-forms-field-validation--warning[_ngcontent-%COMP%] .mj-forms-field-validation-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-warning);\n}\n\n\n\n\n.mj-forms-image[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-image-preview[_ngcontent-%COMP%] {\n width: 72px;\n height: 72px;\n object-fit: cover;\n object-position: center;\n border-radius: 8px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n display: block;\n}\n\n.mj-forms-image-preview--empty[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: 22px;\n}\n\n.mj-forms-image-meta[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n}\n\n.mj-forms-image-hint[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n\n.mj-forms-image-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-image-file[_ngcontent-%COMP%] {\n display: none;\n}\n\n.mj-forms-image-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-family: inherit;\n font-weight: 600;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n border-radius: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n\n.mj-forms-image-btn[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n}\n\n.mj-forms-image-btn--ghost[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n background: transparent;\n border-color: var(--mj-border-default);\n font-weight: 500;\n}\n\n.mj-forms-image--edit[_ngcontent-%COMP%] .mj-forms-field-input[_ngcontent-%COMP%] {\n width: 100%;\n}\n\n.mj-forms-color[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.mj-forms-color-swatch[_ngcontent-%COMP%] {\n width: 20px;\n height: 20px;\n border-radius: 4px;\n border: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.mj-forms-color-picker[_ngcontent-%COMP%] {\n width: 32px;\n height: 32px;\n padding: 0;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n flex: 0 0 auto;\n}\n\n.mj-forms-color-hex[_ngcontent-%COMP%] {\n max-width: 160px;\n}\n\n.mj-forms-json[_ngcontent-%COMP%] {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 12.5px;\n line-height: 1.45;\n white-space: pre-wrap;\n word-break: break-word;\n margin: 0;\n max-height: 360px;\n overflow: auto;\n width: 100%;\n}\n\n@keyframes _ngcontent-%COMP%_mj-validation-slide-in {\n from {\n opacity: 0;\n max-height: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n max-height: 100px;\n transform: translateY(0);\n }\n}\n\n\n\n\n@media (max-width: 640px) {\n .mj-forms-field[_ngcontent-%COMP%] {\n grid-template-columns: 1fr;\n gap: 4px;\n }\n\n .mj-forms-field-label[_ngcontent-%COMP%] {\n font-size: 12px;\n }\n\n .mj-forms-field-validation[_ngcontent-%COMP%] {\n grid-column: 1;\n }\n}"], changeDetection: 0 });
3012
3161
  }
3013
3162
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MjFormFieldComponent, [{
3014
3163
  type: Component,
3015
- args: [{ standalone: false, selector: 'mj-form-field', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (Record && (Record.IsSaved || !IsFieldReadOnly)) {\n <!-- READ-ONLY MODE -->\n @if ((!EditMode || IsFieldReadOnly) && !ShouldHideField) {\n <div class=\"mj-forms-field\" [class.mj-forms-field--readonly]=\"EditMode && IsFieldReadOnly\" [class.mj-forms-field--dirty]=\"IsDirty\">\n @if (ShowLabel) {\n <label class=\"mj-forms-field-label\" [innerHTML]=\"HighlightedDisplayName\"></label>\n }\n <div class=\"mj-forms-field-control\">\n <!-- FK fields with a mapped name field: show the name as a hyperlink -->\n @if (IsFKWithNameField) {\n <span class=\"mj-forms-field-value\">\n @if (RecordLinksEnabled) {\n <a class=\"mj-forms-field-link mj-forms-field-link--fk\" (click)=\"OnRecordLinkClick($event)\" title=\"View related record\">\n {{ FKDisplayName }}\n </a>\n } @else {\n {{ FKDisplayName }}\n }\n </span>\n }\n @else if (ExtendedDisplay === 'image') {\n <div class=\"mj-forms-image\">\n @if (ImagePreviewSrc) {\n <img class=\"mj-forms-image-preview\" [src]=\"ImagePreviewSrc\" alt=\"\" />\n }\n @if (IsInlineImageValue) {\n <span class=\"mj-forms-image-meta\">Inline image ({{ ImageValueSizeLabel }})</span>\n } @else if (ImagePreviewSrc) {\n <a class=\"mj-forms-field-link\" (click)=\"OnUrlLinkClick($event)\" title=\"Open image\">\n {{ ImageUrlDisplay }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n } @else {\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n }\n </div>\n }\n @else if (ExtendedDisplay === 'color') {\n <div class=\"mj-forms-color\">\n <span class=\"mj-forms-color-swatch\" [style.background-color]=\"ColorSwatchValue\"></span>\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n </div>\n }\n @else if (ExtendedDisplay === 'json') {\n <pre class=\"mj-forms-json\">{{ JsonDisplayValue }}</pre>\n }\n @else {\n @switch (LinkType) {\n @case ('Email') {\n <span class=\"mj-forms-field-value\">\n <a class=\"mj-forms-field-link\" (click)=\"OnEmailLinkClick($event)\" title=\"Send email\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-envelope mj-forms-field-link-icon\"></i>\n </a>\n </span>\n }\n @case ('URL') {\n <span class=\"mj-forms-field-value\">\n <a class=\"mj-forms-field-link\" (click)=\"OnUrlLinkClick($event)\" title=\"Open link\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n </span>\n }\n @case ('Record') {\n <span class=\"mj-forms-field-value\">\n @if (RecordLinksEnabled) {\n <a class=\"mj-forms-field-link\" (click)=\"OnRecordLinkClick($event)\" title=\"View related record\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n } @else {\n {{ FormatValue() }}\n }\n </span>\n }\n @default {\n @if (Type === 'checkbox') {\n <span class=\"mj-forms-field-value\">\n <div class=\"mj-forms-field-checkbox\">\n <input type=\"checkbox\" [checked]=\"!!Value\" disabled>\n </div>\n </span>\n }\n @else if (RichTextMode === 'markdown') {\n <div class=\"mj-forms-field-value mj-forms-field-value--rich\">\n <mj-markdown [data]=\"FormatValue()\"></mj-markdown>\n </div>\n }\n @else if (RichTextMode === 'html') {\n <!-- XSS: the mjSafeRichHtml pipe sanitizes with DOMPurify (HTML + SVG profiles)\n and then bypasses Angular's redundant re-sanitize. DOMPurify strips <script>,\n on* handlers, javascript:/data: URLs and <foreignObject> while KEEPING safe\n inline SVG + styles \u2014 richer than Angular's built-in sanitizer, still safe for\n untrusted content. -->\n <div class=\"mj-forms-field-value mj-forms-field-value--rich\" [innerHTML]=\"FormatValue() | mjSafeRichHtml\"></div>\n }\n @else if (RichTextMode === 'code') {\n <div class=\"mj-forms-field-value mj-forms-field-value--code\">\n <mj-code-editor [value]=\"FormatValue()\" [language]=\"CodeEditorLanguage\" [readonly]=\"true\"></mj-code-editor>\n </div>\n }\n @else if (IsLongText) {\n <span class=\"mj-forms-field-value mj-forms-field-value--long\">{{ FormatValue() }}</span>\n }\n @else {\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n }\n }\n }\n }\n </div>\n </div>\n }\n\n <!-- EDIT MODE -->\n @if (EditMode && !IsFieldReadOnly) {\n <div class=\"mj-forms-field mj-forms-field--editing\" [class.mj-forms-field--dirty]=\"IsDirty\" [class.mj-forms-field--required-empty]=\"IsRequiredEmpty\" [class.mj-forms-field--has-error]=\"ShowErrors\" [class.mj-forms-field--has-warning]=\"ShowWarnings || StoredDateIsUnreadable\">\n @if (ShowLabel) {\n <label class=\"mj-forms-field-label\" [innerHTML]=\"HighlightedDisplayName\"></label>\n }\n <div class=\"mj-forms-field-control\">\n <!-- Foreign Key fields get custom search autocomplete -->\n @if (HasRelatedEntity) {\n <div class=\"mj-fk-search\"\n [class.mj-fk-search--matched]=\"FKHasLinkedValue\"\n [class.mj-fk-search--scoped]=\"FKFocused && FKSearchableFields.length > 1\">\n @if (FKHasLinkedValue && !(FKFocused && FKSearchableFields.length > 1)) {\n <i class=\"fa-solid fa-link mj-fk-link-icon\" title=\"Linked record\"></i>\n }\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [class.mj-forms-field-input--linked]=\"FKHasLinkedValue\"\n [value]=\"FKSearchText\"\n (input)=\"OnFKInput($event)\"\n (focus)=\"OnFKFocus($event)\"\n (blur)=\"OnFKBlur()\"\n (keydown)=\"OnFKKeydown($event)\"\n placeholder=\"Search...\">\n\n <!-- Scope pill: only while focused, only when there's more than one searchable field.\n The menu itself lives INSIDE the results dropdown (below) so it overlays the\n list and rides the same above/below flip. -->\n @if (FKFocused && FKSearchableFields.length > 1) {\n <button class=\"mj-fk-scope-pill\"\n [class.mj-fk-scope-pill--open]=\"FKShowScopeMenu\"\n (mousedown)=\"OnFKScopeToggle($event)\"\n [title]=\"'Searching ' + FKSearchFieldLabel + ' \u2014 click to change'\">\n <i class=\"fa-solid fa-magnifying-glass mj-fk-scope-pill-icon\"></i>\n <span class=\"mj-fk-scope-pill-label\">{{ FKSearchFieldLabel }}</span>\n <i class=\"fa-solid mj-fk-scope-pill-caret\" [class.fa-chevron-up]=\"FKShowScopeMenu\" [class.fa-chevron-down]=\"!FKShowScopeMenu\"></i>\n </button>\n }\n\n @if (FKSearchText) {\n <button class=\"mj-fk-clear\" (mousedown)=\"OnFKClearClick($event)\" title=\"Clear\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n } @else {\n <i class=\"fa-solid fa-magnifying-glass mj-fk-search-icon\"></i>\n }\n @if (ShowFKDropdown) {\n <div class=\"mj-fk-dropdown mj-fk-dropdown--multi\"\n [ngStyle]=\"DropdownPositionStyle\"\n [class.mj-fk-dropdown--above]=\"OpenAbove\"\n [class.mj-fk-dropdown--scope-open]=\"FKShowScopeMenu\">\n\n <!-- Scope (search-field) picker + column toggles \u2014 overlays the list, anchored to the pill edge.\n Click a row to SEARCH that field; click the eye to SHOW/HIDE it as a column. -->\n @if (FKShowScopeMenu) {\n <div class=\"mj-fk-scope-menu\" [class.mj-fk-scope-menu--bottom]=\"OpenAbove\">\n <div class=\"mj-fk-scope-grp\">Columns shown</div>\n @for (f of FKSearchableFields; track f.FieldName) {\n @if (f.Group === 'shown') {\n <ng-container *ngTemplateOutlet=\"scopeRow; context: { $implicit: f }\"></ng-container>\n }\n }\n @if (FKHasOtherSearchableFields) {\n <div class=\"mj-fk-scope-divider\"></div>\n <div class=\"mj-fk-scope-grp\">Other fields</div>\n @for (f of FKSearchableFields; track f.FieldName) {\n @if (f.Group === 'other') {\n <ng-container *ngTemplateOutlet=\"scopeRow; context: { $implicit: f }\"></ng-container>\n }\n }\n }\n </div>\n }\n\n <!-- Shared scope-menu row, fixed-slot layout for clean alignment:\n [reorder \u25B2\u25BC \u2014 hover-reveal] [icon] [label] [eye].\n Click the row = search this field; the active search row is shown bold+accent. -->\n <ng-template #scopeRow let-f>\n <div class=\"mj-fk-scope-mi\"\n [class.mj-fk-scope-mi--on]=\"IsFKScopeActive(f)\"\n (mousedown)=\"OnFKScopeSelect($event, f)\">\n <span class=\"mj-fk-col-reorder\">\n @if (f.Group === 'shown') {\n <button type=\"button\" class=\"mj-fk-col-move mj-fk-col-move--up\" [disabled]=\"!CanMoveFKColumnUp(f.FieldName)\"\n (mousedown)=\"MoveFKColumn($event, f.FieldName, -1)\" title=\"Move column up\">\n <i class=\"fa-solid fa-chevron-up\"></i>\n </button>\n <button type=\"button\" class=\"mj-fk-col-move mj-fk-col-move--down\" [disabled]=\"!CanMoveFKColumnDown(f.FieldName)\"\n (mousedown)=\"MoveFKColumn($event, f.FieldName, 1)\" title=\"Move column down\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n </button>\n }\n </span>\n <i [class]=\"f.Icon + ' mj-fk-scope-mi-icon'\"></i>\n <span class=\"mj-fk-scope-mi-label\">{{ f.Label }}</span>\n <span class=\"mj-fk-col-eye-slot\">\n @if (CanToggleFKColumn(f)) {\n <button type=\"button\" class=\"mj-fk-col-toggle\"\n [class.mj-fk-col-toggle--on]=\"IsFKColumnVisible(f)\"\n (mousedown)=\"OnFKColumnToggle($event, f)\"\n [title]=\"IsFKColumnVisible(f) ? 'Hide this column' : 'Show as a column'\">\n <i class=\"fa-solid\" [class.fa-eye]=\"IsFKColumnVisible(f)\" [class.fa-eye-slash]=\"!IsFKColumnVisible(f)\"></i>\n </button>\n }\n </span>\n </div>\n </ng-template>\n\n <!-- Pinned \"currently selected\" \u2014 sticky at the top while browsing, so you can\n keep it or pick another without clearing first. -->\n @if (FKSelectedSuggestion && !FKLoading) {\n <div class=\"mj-fk-current\">\n <div class=\"mj-fk-current-head\">\n <i class=\"fa-solid fa-circle-check\"></i>\n <span>Currently selected</span>\n </div>\n <div class=\"mj-fk-current-row\" (mousedown)=\"OnFKSelectItem(FKSelectedSuggestion, $event)\" title=\"Keep this selection\">\n @if (FKSelectedSuggestion.Icon) { <i class=\"mj-fk-current-icon\" [class]=\"FKSelectedSuggestion.Icon\"></i> }\n <span class=\"mj-fk-current-name\" [innerHTML]=\"FKSelectedSuggestion.HighlightedName\"></span>\n @if (FKSelectedSuggestion.ExtraColumns.length && FKSelectedSuggestion.ExtraColumns[0].Value) {\n <span class=\"mj-fk-current-detail\">{{ FKSelectedSuggestion.ExtraColumns[0].Value }}</span>\n }\n <button type=\"button\" class=\"mj-fk-current-clear\" (mousedown)=\"OnFKClearClick($event)\" title=\"Clear selection\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n }\n\n @if (FKLoading) {\n <div class=\"mj-fk-status\">\n <i class=\"fa-solid fa-circle-notch fa-spin\"></i>\n <span>Searching\u2026</span>\n </div>\n }\n @if (!FKLoading && FKNoMatches) {\n <div class=\"mj-fk-status mj-fk-status--empty\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n <span>No matches</span>\n </div>\n }\n @if (!FKLoading && FKSuggestions.length > 0) {\n <div class=\"mj-fk-options\">\n <div class=\"mj-fk-grid\" [style.grid-template-columns]=\"FKGridTemplateColumns\">\n <!-- Header row: click to sort, drag the right edge to resize.\n (Column reorder lives in the field-selector menu, not here.) -->\n <div class=\"mj-fk-grid-row mj-fk-grid-row--header\">\n @if (FKHasIconColumn) {\n <div class=\"mj-fk-hcell mj-fk-hcell--icon\"></div>\n }\n @for (col of FKColumnFields; track col; let i = $index) {\n <div class=\"mj-fk-hcell\"\n (mousedown)=\"OnFKHeaderSort($event, col)\"\n [title]=\"'Click to sort by ' + FKColumnHeaders[i]\">\n <span class=\"mj-fk-hcell-label\">{{ FKColumnHeaders[i] }}</span>\n <i [class]=\"FKSortIcon(col)\"></i>\n <span class=\"mj-fk-resizer\" (mousedown)=\"OnFKColResizeStart($event, col)\" title=\"Drag to resize\"></span>\n </div>\n }\n </div>\n <!-- Data rows -->\n @for (suggestion of FKSuggestions; track suggestion.PrimaryKeyValue; let i = $index) {\n <div class=\"mj-fk-grid-row\"\n [class.mj-fk-grid-row--active]=\"i === FKActiveIndex\"\n [class.mj-fk-grid-row--selected]=\"IsFKRowSelected(suggestion)\"\n (mousedown)=\"OnFKSelectItem(suggestion, $event)\"\n (mouseenter)=\"FKActiveIndex = i\">\n @if (FKHasIconColumn) {\n <div class=\"mj-fk-cell mj-fk-cell--icon\">\n @if (suggestion.Icon) { <i [class]=\"suggestion.Icon\"></i> }\n </div>\n }\n @for (col of FKColumnFields; track col; let last = $last) {\n @if (col === FKNameField) {\n <div class=\"mj-fk-cell mj-fk-cell--name\" [class.mj-fk-cell--fill]=\"FKColumnFillsTrack(col, last)\">\n @if (IsFKRowSelected(suggestion)) {\n <i class=\"fa-solid fa-check mj-fk-selected-check\" title=\"Currently selected\"></i>\n }\n <span [innerHTML]=\"suggestion.HighlightedName\"></span>\n </div>\n } @else {\n <div class=\"mj-fk-cell\" [class.mj-fk-cell--fill]=\"FKColumnFillsTrack(col, last)\" [title]=\"FKCellFor(suggestion, col)?.Value || ''\">\n <span [innerHTML]=\"FKCellFor(suggestion, col)?.HighlightedValue || ''\"></span>\n </div>\n }\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Sticky \"create new\" footer \u2014 always visible at the bottom (no scrolling),\n prefilled with the typed text. Gated on create permission. -->\n @if (CanCreateFK) {\n <button type=\"button\" class=\"mj-fk-create-footer\" (mousedown)=\"OnFKCreateNew($event)\" [title]=\"FKCreateLabel\">\n <i class=\"fa-solid fa-plus mj-fk-create-icon\"></i>\n <span class=\"mj-fk-create-label\">{{ FKCreateLabel }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n @else if (ExtendedDisplay === 'image') {\n <div class=\"mj-forms-image mj-forms-image--edit\">\n @if (ImagePreviewSrc) {\n <img class=\"mj-forms-image-preview\" [src]=\"ImagePreviewSrc\" alt=\"\" />\n } @else {\n <div class=\"mj-forms-image-preview mj-forms-image-preview--empty\" aria-hidden=\"true\">\n <i class=\"fa-regular fa-image\"></i>\n </div>\n }\n <div class=\"mj-forms-image-meta\">\n @if (IsInlineImageValue) {\n <span>Inline image ({{ ImageValueSizeLabel }})</span>\n }\n <span class=\"mj-forms-image-hint\">Max {{ ImageMaxSizeLabel }}</span>\n </div>\n <div class=\"mj-forms-image-actions\">\n <input #imageFileInput type=\"file\" accept=\"image/png,image/jpeg,image/gif,image/webp,image/svg+xml\"\n class=\"mj-forms-image-file\" (change)=\"OnImageFileSelected($event)\">\n <button type=\"button\" class=\"mj-forms-image-btn\" (click)=\"imageFileInput.click()\">\n <i class=\"fa-solid fa-upload\"></i>\n {{ ImagePreviewSrc ? 'Replace' : 'Upload' }}\n </button>\n @if (ImagePreviewSrc) {\n <button type=\"button\" class=\"mj-forms-image-btn mj-forms-image-btn--ghost\" (click)=\"OnImageClear()\">Clear</button>\n }\n </div>\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"ImageUrlInputValue\"\n (input)=\"OnImageUrlInput($event)\"\n placeholder=\"https://\u2026 or paste a data URI\">\n </div>\n }\n @else if (ExtendedDisplay === 'color') {\n <div class=\"mj-forms-color mj-forms-color--edit\">\n <input type=\"color\"\n class=\"mj-forms-color-picker\"\n [value]=\"ColorPickerValue\"\n (input)=\"OnColorPickerInput($event)\"\n [attr.aria-label]=\"DisplayName + ' color'\">\n <input type=\"text\"\n class=\"mj-forms-field-input mj-forms-color-hex\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"\n placeholder=\"#RRGGBB\">\n </div>\n }\n @else if (ExtendedDisplay === 'json') {\n <textarea\n class=\"mj-forms-field-input mj-forms-field-input--textarea mj-forms-json\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"\n (blur)=\"OnJsonBlur()\"></textarea>\n }\n @else if (EditRichTextMode !== 'plain') {\n <!-- Markdown / HTML / Code long-text: edit raw with a syntax-highlighted code editor -->\n <mj-code-editor\n class=\"mj-forms-field-input--code\"\n [value]=\"FormatValue()\"\n [language]=\"CodeEditorLanguage\"\n (change)=\"OnCodeEditorChange($event)\"></mj-code-editor>\n }\n @else {\n @switch (Type) {\n @case ('textbox') {\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\">\n }\n @case ('textarea') {\n <textarea\n class=\"mj-forms-field-input mj-forms-field-input--textarea\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"></textarea>\n }\n @case ('number') {\n <input type=\"number\"\n class=\"mj-forms-field-input mj-forms-field-input--number\"\n [value]=\"Value\"\n (change)=\"OnNumberChange($event)\">\n }\n @case ('datepicker') {\n <input type=\"date\"\n class=\"mj-forms-field-input mj-forms-field-input--date\"\n [value]=\"DateInputValue\"\n (change)=\"OnDateChange($event)\">\n }\n @case ('checkbox') {\n <div class=\"mj-forms-field-checkbox\">\n <input type=\"checkbox\"\n [checked]=\"!!Value\"\n (change)=\"OnCheckboxChange($event)\">\n </div>\n }\n @case ('select') {\n <div class=\"mj-custom-select-trigger\"\n tabindex=\"0\"\n (click)=\"OnSelectTriggerClick($event)\"\n (blur)=\"OnSelectTriggerBlur()\">\n <span class=\"mj-custom-select-value\" [class.mj-custom-select-value--placeholder]=\"!FormatValue()\">\n {{ FormatValue() || '-- Select --' }}\n </span>\n <i class=\"fa-solid fa-chevron-down mj-custom-select-chevron\" [class.mj-custom-select-chevron--open]=\"ShowSelectDropdown\"></i>\n </div>\n @if (ShowSelectDropdown && PossibleValues.length > 0) {\n <div class=\"mj-fk-dropdown\" [ngStyle]=\"DropdownPositionStyle\" [class.mj-fk-dropdown--above]=\"OpenAbove\">\n @for (option of PossibleValues; track option) {\n <div class=\"mj-fk-option\" [class.mj-fk-option--selected]=\"FormatValue() === option\" (mousedown)=\"OnSelectOptionClick(option, $event)\">\n {{ option }}\n </div>\n }\n </div>\n }\n }\n @case ('autocomplete') {\n <div class=\"mj-fk-search\">\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (input)=\"OnValueListInput($event)\"\n (focus)=\"OnValueListFocus($event)\"\n (blur)=\"OnValueListBlur()\"\n placeholder=\"Type to search...\">\n <i class=\"fa-solid fa-magnifying-glass mj-fk-search-icon\"></i>\n @if (ShowValueListDropdown && FilteredPossibleValues.length > 0) {\n <div class=\"mj-fk-dropdown\" [ngStyle]=\"DropdownPositionStyle\" [class.mj-fk-dropdown--above]=\"OpenAbove\">\n @for (option of FilteredPossibleValues; track option) {\n <div class=\"mj-fk-option\" (mousedown)=\"OnValueListSelect(option, $event)\">\n {{ option }}\n </div>\n }\n </div>\n }\n </div>\n }\n @default {\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (change)=\"OnInputChange($event)\">\n }\n }\n }\n </div>\n <!-- Inline validation messages -->\n @if (DisplayErrors.length > 0) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--error\">\n @for (error of DisplayErrors; track error.Message) {\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-circle-exclamation\"></i>\n <span>{{ error.Message }}</span>\n </div>\n }\n </div>\n }\n @if (DisplayWarnings.length > 0) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--warning\">\n @for (warning of DisplayWarnings; track warning.Message) {\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ warning.Message }}</span>\n </div>\n }\n </div>\n }\n <!--\n An unreadable stored value renders as an EMPTY date box, identical to a field that\n was never set - and the next save writes that emptiness over whatever is stored. The\n element cannot display the bad value itself, so the warning sits with the other validation messages, beneath the control.\n -->\n @if (StoredDateIsUnreadable) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--warning\">\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>This field holds a value that is not a readable date, so the box above is\n empty. Saving now will replace it.</span>\n </div>\n </div>\n }\n </div>\n }\n}\n", styles: ["/* ============================================\n FORM FIELD - Shared styles for read and edit\n ============================================ */\n:host {\n --mj-forms-field-label-width: 200px;\n --mj-forms-field-gap: 12px;\n\n display: contents;\n}\n\n.mj-forms-field {\n display: grid;\n grid-template-columns: var(--mj-forms-field-label-width) 1fr;\n gap: var(--mj-forms-field-gap);\n align-items: baseline;\n padding: 8px 0;\n transition: all var(--mj-transition-base) ease;\n}\n\n/* Subtle row hover in edit mode */\n.mj-forms-field--editing:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 6px;\n margin: 0 -8px;\n padding: 8px 8px;\n}\n\n/* ============================================\n LABEL\n ============================================ */\n.mj-forms-field-label {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n padding-top: 2px;\n word-break: break-word;\n}\n\n/* ============================================\n FIELD CONTROL WRAPPER (value + dirty dot)\n ============================================ */\n.mj-forms-field-control {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-field-control > * {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n DIRTY INDICATOR (solid amber dot after label)\n ============================================ */\n.mj-forms-field--dirty .mj-forms-field-label::after {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--mj-status-warning);\n margin-left: 6px;\n vertical-align: middle;\n}\n\n/* ============================================\n REQUIRED FIELD - red underline when empty\n ============================================ */\n.mj-forms-field--required-empty .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n}\n\n/* ============================================\n READ-ONLY VALUE\n ============================================ */\n.mj-forms-field-value {\n font-size: 14px;\n color: var(--mj-text-primary);\n min-height: 24px;\n display: flex;\n align-items: center;\n overflow-wrap: break-word;\n word-break: break-word;\n}\n\n.mj-forms-field-value--long {\n white-space: pre-line;\n max-height: 300px;\n overflow: auto;\n}\n\n/* Rich (markdown / html) read-only rendering \u2014 let content flow as a block, not a flex row */\n.mj-forms-field-value--rich {\n display: block;\n align-items: initial;\n max-height: 480px;\n overflow: auto;\n width: 100%;\n}\n\n/* Code read-only / edit rendering \u2014 the embedded code editor needs an explicit height */\n.mj-forms-field-value--code {\n display: block;\n align-items: initial;\n width: 100%;\n}\n\n.mj-forms-field-value--code mj-code-editor,\n.mj-forms-field-input--code {\n display: block;\n width: 100%;\n min-height: 160px;\n max-height: 480px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius, 4px);\n overflow: auto;\n}\n\n/* Read-only fields in edit mode */\n.mj-forms-field--readonly {\n opacity: 0.7;\n}\n\n.mj-forms-field--readonly .mj-forms-field-value {\n color: var(--mj-text-secondary);\n}\n\n/* ============================================\n LINKS (FK, email, URL)\n ============================================ */\n.mj-forms-field-link {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-brand-primary);\n cursor: pointer;\n font-weight: 400;\n font-size: 14px;\n transition: color var(--mj-transition-base);\n text-decoration: none;\n}\n\n.mj-forms-field-link:hover {\n color: var(--mj-brand-primary-hover);\n}\n\n.mj-forms-field-link-icon {\n font-size: 11px;\n opacity: 0;\n transition: opacity var(--mj-transition-base);\n}\n\n.mj-forms-field-link:hover .mj-forms-field-link-icon {\n opacity: 1;\n}\n\n/* FK fields showing entity name instead of raw ID */\n.mj-forms-field-link--fk {\n font-size: 14px;\n font-weight: 400;\n}\n\n/* ============================================\n EDIT MODE INPUTS - Clean underline style\n ============================================ */\n.mj-forms-field-input {\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-forms-field-input:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-forms-field-input::placeholder {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n/* Textarea variant */\n.mj-forms-field-input--textarea {\n resize: vertical;\n min-height: 60px;\n border: 1.5px solid var(--mj-border-default);\n border-radius: 6px;\n padding: 8px 10px;\n background: var(--mj-bg-surface);\n font-family: inherit;\n}\n\n.mj-forms-field-input--textarea:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n background: var(--mj-bg-surface);\n padding: 8px 10px;\n border-radius: 6px;\n}\n\n/* Number input - narrower */\n.mj-forms-field-input--number {\n max-width: 200px;\n}\n\n/* Date input - narrower */\n.mj-forms-field-input--date {\n max-width: 220px;\n}\n\n/* ============================================\n CHECKBOX (custom styled)\n ============================================ */\n.mj-forms-field-checkbox {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"] {\n appearance: none;\n width: 18px;\n height: 18px;\n border: 2px solid var(--mj-border-default);\n border-radius: 4px;\n cursor: pointer;\n position: relative;\n transition: all var(--mj-transition-base) ease;\n flex-shrink: 0;\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:checked {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:checked::after {\n content: '';\n position: absolute;\n left: 5px;\n top: 1px;\n width: 5px;\n height: 9px;\n border: solid var(--mj-text-inverse);\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n/* ============================================\n CUSTOM SELECT DROPDOWN (replaces native <select>)\n ============================================ */\n.mj-custom-select-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n cursor: pointer;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-custom-select-trigger:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-custom-select-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mj-custom-select-value--placeholder {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n.mj-custom-select-chevron {\n font-size: 11px;\n color: var(--mj-text-muted);\n transition: transform var(--mj-transition-base) ease;\n flex-shrink: 0;\n margin-left: 8px;\n}\n\n.mj-custom-select-chevron--open {\n transform: rotate(180deg);\n}\n\n/* ============================================\n FK SEARCH (custom autocomplete)\n ============================================ */\n.mj-fk-search {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n}\n\n.mj-fk-search .mj-forms-field-input {\n padding-right: 28px;\n}\n\n.mj-fk-search-icon {\n position: absolute;\n right: 4px;\n color: var(--mj-text-muted);\n font-size: 12px;\n pointer-events: none;\n}\n\n.mj-fk-clear {\n position: absolute;\n right: 2px;\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 11px;\n padding: 4px 6px;\n line-height: 1;\n border-radius: 3px;\n transition: all var(--mj-transition-base);\n}\n\n.mj-fk-clear:hover {\n color: var(--mj-status-error);\n background: color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n/* Linked state: solid accent underline + a leading chain icon, so it's obvious the\n field holds a resolved record reference rather than free-typed text. */\n.mj-fk-search--matched .mj-forms-field-input {\n border-bottom: 2px solid var(--mj-brand-primary);\n}\n\n/* Linked indicator sits on the RIGHT (just left of the clear button) so it never\n interrupts left-to-right reading of the value or overlaps the text. */\n.mj-fk-link-icon {\n position: absolute;\n right: 26px;\n color: var(--mj-brand-primary);\n font-size: 10px;\n pointer-events: none;\n z-index: 1;\n}\n\n/* Make room on the right for the link icon + the clear button. */\n.mj-forms-field-input--linked {\n padding-right: 46px;\n}\n\n/* ============================================\n SCOPE PILL + FIELD-PICKER MENU (search-field scope)\n ============================================ */\n/* When the pill is showing, reserve room on the right of the input for it. */\n.mj-fk-search--scoped .mj-forms-field-input {\n padding-right: 150px;\n}\n\n.mj-fk-scope-pill {\n position: absolute;\n right: 30px;\n /* Vertically centered in the input so it clears the focus underline below. */\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 120px;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n color: var(--mj-text-secondary);\n font-size: 10.5px;\n font-weight: 600;\n font-family: inherit;\n /* Kept short so it clears the input's focus underline without making the row taller. */\n padding: 1px 7px;\n border-radius: 999px;\n cursor: pointer;\n white-space: nowrap;\n line-height: 1.35;\n transition: background var(--mj-transition-base), border-color var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-pill:hover,\n.mj-fk-scope-pill--open {\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-primary);\n}\n\n.mj-fk-scope-pill-icon {\n font-size: 9px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-pill-label {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-scope-pill-caret {\n font-size: 8px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n/* The menu is rendered INSIDE the results dropdown panel so it overlays the list\n (correct z-order, no extra real estate) and rides the dropdown's above/below\n flip. Anchored to the dropdown's top-right \u2014 right under the pill. */\n.mj-fk-scope-menu {\n position: absolute;\n top: 4px;\n right: 8px;\n z-index: 5;\n min-width: 180px;\n max-width: calc(100vw - 24px);\n max-height: 260px;\n overflow-y: auto;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n border-radius: var(--mj-radius-sm);\n box-shadow: var(--mj-shadow-lg);\n padding: 5px;\n animation: mj-dropdown-in 120ms ease;\n}\n\n/* When the dropdown opens ABOVE the input, anchor the menu to its bottom edge so it\n stays next to the pill (which is at the input/dropdown boundary). */\n.mj-fk-scope-menu--bottom {\n top: auto;\n bottom: 4px;\n}\n\n.mj-fk-scope-grp {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-text-muted);\n padding: 7px 10px 4px;\n font-weight: 700;\n}\n\n.mj-fk-scope-mi {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 13px;\n border-radius: 5px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n}\n\n.mj-fk-scope-mi:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-scope-mi--on {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n/* Fixed-width LEFT slot: a compact stacked up/down reorder control that reveals on row\n hover. Reserved (same width) on every row so the field icons + labels stay aligned. */\n.mj-fk-col-reorder {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 22px;\n flex-shrink: 0;\n}\n\n.mj-fk-col-move {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 10px;\n padding: 0;\n background: transparent;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 8px;\n opacity: 0; /* hover-reveal for a clean resting state */\n transition: opacity var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-mi:hover .mj-fk-col-move:not(:disabled) {\n opacity: 0.8;\n}\n\n.mj-fk-col-move:hover:not(:disabled) {\n opacity: 1;\n color: var(--mj-brand-primary);\n}\n\n/* End-of-range arrows stay hidden (slot still reserved) rather than greyed. */\n.mj-fk-col-move:disabled {\n opacity: 0 !important;\n cursor: default;\n}\n\n.mj-fk-scope-mi-icon {\n width: 14px;\n font-size: 11px;\n opacity: 0.75;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-mi-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Fixed-width RIGHT slot so the eye (or its empty space, for the name row) aligns. */\n.mj-fk-col-eye-slot {\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Eye toggle: show/hide a field as a grid column (independent of search scope). */\n.mj-fk-col-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-col-toggle:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-secondary);\n}\n\n.mj-fk-col-toggle--on {\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-scope-divider {\n height: 1px;\n background: var(--mj-border-subtle);\n margin: 4px 6px;\n}\n\n/* Narrow / mobile screens: shrink the pill + its reserved space, give menu rows a\n larger touch target, and keep the menu within the viewport. */\n@media (max-width: 560px) {\n .mj-fk-search--scoped .mj-forms-field-input {\n padding-right: 116px;\n }\n .mj-fk-scope-pill {\n max-width: 88px;\n }\n .mj-fk-scope-menu {\n min-width: 160px;\n }\n .mj-fk-scope-mi {\n padding: 10px 10px;\n }\n}\n\n/* ============================================\n FLOATING DROPDOWN PANEL (position: fixed via ngStyle)\n ============================================ */\n.mj-fk-dropdown {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n max-height: 240px;\n overflow-y: auto;\n padding: 4px;\n animation: mj-dropdown-in 150ms ease;\n}\n\n.mj-fk-dropdown--above {\n animation: mj-dropdown-in-above 150ms ease;\n}\n\n.mj-fk-option {\n padding: 10px 14px;\n font-size: 14px;\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: background var(--mj-transition-base);\n border-radius: 6px;\n}\n\n.mj-fk-option:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n.mj-fk-option--selected {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-option--selected:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n/* ============================================\n MULTI-COLUMN FK DROPDOWN \u2014 aligned mini-grid (icon + name + extra cols)\n with sortable headers and horizontal scroll.\n ============================================ */\n.mj-fk-dropdown--multi {\n padding: 4px;\n /* The inner .mj-fk-options is the single scroll container (both axes) so the\n sticky header anchors correctly; the dropdown just clips to its rounded border. */\n overflow: hidden;\n max-height: none;\n}\n\n/* Single scroll region (both axes) for the grid: vertical for long lists, horizontal\n when many DefaultInView columns make rows wider than the dropdown. Being the sole\n scroller lets the sticky column headers anchor to its top edge. */\n.mj-fk-dropdown--multi .mj-fk-options {\n overflow: auto;\n max-height: 260px;\n}\n\n/* While the scope menu is open it must escape the panel's clip (the inner\n .mj-fk-options keeps its own scroll, so the grid still behaves). */\n.mj-fk-dropdown--scope-open {\n overflow: visible;\n}\n\n/* One CSS grid spans the header + all data rows so every column lines up.\n grid-template-columns is supplied inline from FKGridTemplateColumns.\n `width: 100%` (with `min-width: max-content`) lets the last column's `1fr` track\n absorb leftover width, while still overflowing \u2192 horizontal scroll when the columns\n are collectively wider than the dropdown. */\n.mj-fk-grid {\n display: grid;\n width: 100%;\n min-width: max-content;\n align-items: stretch;\n}\n\n/* Rows are layout-transparent so their cells become direct grid items (this is\n what makes columns align across rows). The row element still exists for click\n handling and for the active-state selector below. */\n.mj-fk-grid-row {\n display: contents;\n}\n\n/* Header cells: sticky to the top of the vertical scroll, clickable to sort. */\n.mj-fk-hcell {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 7px 12px;\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-elevated);\n border-bottom: 1px solid var(--mj-border-default);\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n}\n\n.mj-fk-hcell:hover {\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-hcell--icon {\n cursor: default;\n padding: 7px 8px;\n}\n\n.mj-fk-hcell--icon:hover {\n background: var(--mj-bg-surface-elevated);\n}\n\n.mj-fk-hcell-label {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-sort-icon {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n/* Idle (unsorted) columns show a faded sort glyph to signal sortability. */\n.mj-fk-sort-icon--idle {\n opacity: 0.35;\n}\n\n/* Data cells */\n.mj-fk-cell {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 320px;\n cursor: pointer;\n}\n\n/* The fill column (last, or a user-resized one) drops the default content cap so its\n cells stretch to the full track width that the header already occupies. */\n.mj-fk-cell--fill {\n max-width: none;\n}\n\n/* Wrapping span keeps the highlighted text+<mark> as one inline run (so flex\n doesn't strip the space next to the mark) while still truncating with ellipsis. */\n.mj-fk-cell > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.mj-fk-cell--icon {\n justify-content: center;\n padding: 8px;\n max-width: none;\n color: var(--mj-text-muted);\n font-size: 13px;\n}\n\n.mj-fk-cell--name {\n font-weight: 500;\n font-size: 14px;\n color: var(--mj-text-primary);\n gap: 6px;\n}\n\n.mj-fk-selected-check {\n color: var(--mj-brand-primary);\n font-size: 11px;\n flex-shrink: 0;\n}\n\n/* Active (keyboard / hover) row \u2014 painted on the cells, since the row box is\n removed by display:contents. The child combinator still matches. */\n.mj-fk-grid-row--active > .mj-fk-cell {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);\n}\n\n/* Currently-linked row \u2014 a subtle persistent tint so the user sees which record\n is selected when they reopen the list (stacks under the brighter active tint). */\n.mj-fk-grid-row--selected > .mj-fk-cell {\n background: color-mix(in srgb, var(--mj-brand-primary) 6%, transparent);\n}\n\n.mj-fk-grid-row--selected > .mj-fk-cell--name {\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n/* Matched-substring highlight inside the name column */\n/* ::ng-deep (not :global, which Angular's emulated encapsulation ignores) so the\n style reaches the <mark> injected via [innerHTML]. Color comes from the shared\n theme token so light/dark/white-label all control it centrally. */\n.mj-fk-cell--name ::ng-deep .mj-forms-search-highlight,\n.mj-fk-cell ::ng-deep .mj-forms-search-highlight {\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n/* Column resize handle \u2014 thin grab strip on the right edge of each header cell. */\n.mj-fk-resizer {\n position: absolute;\n top: 0;\n right: 0;\n width: 7px;\n height: 100%;\n cursor: col-resize;\n user-select: none;\n}\n\n.mj-fk-resizer::after {\n content: '';\n position: absolute;\n top: 20%;\n right: 2px;\n width: 2px;\n height: 60%;\n background: transparent;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-hcell:hover .mj-fk-resizer::after,\n.mj-fk-resizer:hover::after {\n background: var(--mj-border-strong);\n}\n\n/* Loading / empty status rows */\n.mj-fk-status {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 14px;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-status i {\n font-size: 12px;\n}\n\n.mj-fk-status--empty {\n font-style: italic;\n}\n\n/* Sticky \"create new\" footer \u2014 sits below the (scrolling) options as a direct child of\n the dropdown panel, so it's always visible regardless of list length. */\n.mj-fk-create-footer {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n background: var(--mj-bg-surface-card);\n border: none;\n border-top: 1px solid var(--mj-border-default);\n color: var(--mj-brand-primary);\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-create-footer:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-create-icon {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.mj-fk-create-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ============================================\n PINNED \"CURRENTLY SELECTED\" SECTION\n ============================================ */\n.mj-fk-current {\n border-bottom: 1px solid var(--mj-border-default);\n background: color-mix(in srgb, var(--mj-brand-primary) 7%, var(--mj-bg-surface-card));\n animation: mj-fk-current-in 220ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n@keyframes mj-fk-current-in {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.mj-fk-current-head {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px 3px;\n font-size: 10px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-current-head i {\n font-size: 10px;\n}\n\n.mj-fk-current-row {\n position: relative;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 6px 12px 10px;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n/* Accent bar on the left for a bit of sizzle. */\n.mj-fk-current-row::before {\n content: '';\n position: absolute;\n left: 0;\n top: 4px;\n bottom: 8px;\n width: 3px;\n border-radius: 0 2px 2px 0;\n background: var(--mj-brand-primary);\n}\n\n.mj-fk-current-row:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.mj-fk-current-icon {\n flex-shrink: 0;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-current-name {\n font-weight: 600;\n font-size: 14px;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-detail {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-clear {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-current-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-status-error);\n}\n\n@keyframes mj-dropdown-in {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes mj-dropdown-in-above {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ============================================\n SEARCH HIGHLIGHTING\n ============================================ */\n.mj-forms-field-label ::ng-deep .mj-forms-search-highlight {\n /* Shared search-highlight theme token. ::ng-deep (not :global) so it reaches\n innerHTML content. */\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n/* ============================================\n VALIDATION ERROR STATE\n ============================================ */\n.mj-forms-field--has-error .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error .mj-forms-field-input--textarea {\n border-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error .mj-forms-field-input--textarea:focus {\n border-color: var(--mj-status-error);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n.mj-forms-field--has-error .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n/* ============================================\n VALIDATION WARNING STATE\n ============================================ */\n.mj-forms-field--has-warning .mj-forms-field-input {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-warning) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input--textarea {\n border-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input--textarea:focus {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-warning) 8%, transparent);\n}\n\n.mj-forms-field--has-warning .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n/* ============================================\n VALIDATION MESSAGE AREA\n ============================================ */\n.mj-forms-field-validation {\n grid-column: 2;\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: -2px;\n overflow: hidden;\n animation: mj-validation-slide-in 200ms ease-out;\n}\n\n.mj-forms-field-validation-item {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n font-size: 12px;\n line-height: 1.4;\n padding: 3px 0 1px;\n}\n\n.mj-forms-field-validation-item i {\n font-size: 11px;\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n/* Error message colors */\n.mj-forms-field-validation--error .mj-forms-field-validation-item {\n color: var(--mj-status-error);\n}\n\n.mj-forms-field-validation--error .mj-forms-field-validation-item i {\n color: var(--mj-status-error);\n}\n\n/* Warning message colors */\n.mj-forms-field-validation--warning .mj-forms-field-validation-item {\n color: var(--mj-status-warning-text);\n}\n\n.mj-forms-field-validation--warning .mj-forms-field-validation-item i {\n color: var(--mj-status-warning);\n}\n\n/* ============================================\n IMAGE / COLOR / JSON EXTENDED TYPES\n ============================================ */\n.mj-forms-image {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-image-preview {\n width: 72px;\n height: 72px;\n object-fit: cover;\n object-position: center;\n border-radius: 8px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n display: block;\n}\n\n.mj-forms-image-preview--empty {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: 22px;\n}\n\n.mj-forms-image-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n}\n\n.mj-forms-image-hint {\n color: var(--mj-text-muted);\n}\n\n.mj-forms-image-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-image-file {\n display: none;\n}\n\n.mj-forms-image-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-family: inherit;\n font-weight: 600;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n border-radius: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n\n.mj-forms-image-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n}\n\n.mj-forms-image-btn--ghost {\n color: var(--mj-text-secondary);\n background: transparent;\n border-color: var(--mj-border-default);\n font-weight: 500;\n}\n\n.mj-forms-image--edit .mj-forms-field-input {\n width: 100%;\n}\n\n.mj-forms-color {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.mj-forms-color-swatch {\n width: 20px;\n height: 20px;\n border-radius: 4px;\n border: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.mj-forms-color-picker {\n width: 32px;\n height: 32px;\n padding: 0;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n flex: 0 0 auto;\n}\n\n.mj-forms-color-hex {\n max-width: 160px;\n}\n\n.mj-forms-json {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 12.5px;\n line-height: 1.45;\n white-space: pre-wrap;\n word-break: break-word;\n margin: 0;\n max-height: 360px;\n overflow: auto;\n width: 100%;\n}\n\n@keyframes mj-validation-slide-in {\n from {\n opacity: 0;\n max-height: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n max-height: 100px;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n RESPONSIVE\n ============================================ */\n@media (max-width: 640px) {\n .mj-forms-field {\n grid-template-columns: 1fr;\n gap: 4px;\n }\n\n .mj-forms-field-label {\n font-size: 12px;\n }\n\n .mj-forms-field-validation {\n grid-column: 1;\n }\n}\n"] }]
3164
+ args: [{ standalone: false, selector: 'mj-form-field', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n IsFieldReadableByUser is FIRST deliberately. BaseEntity.Get() throws for a field the user\n cannot read, so every expression below \u2014 Value, ShouldHideField, the FK resolution \u2014 is only\n safe once this has short-circuited. A denied field renders nothing at all.\n-->\n@if (IsFieldReadableByUser && Record && (Record.IsSaved || !IsFieldReadOnly)) {\n <!-- READ-ONLY MODE -->\n @if ((!EditMode || IsFieldReadOnly) && !ShouldHideField) {\n <div class=\"mj-forms-field\" [class.mj-forms-field--readonly]=\"EditMode && IsFieldReadOnly\" [class.mj-forms-field--dirty]=\"IsDirty\">\n @if (ShowLabel) {\n <label class=\"mj-forms-field-label\" [innerHTML]=\"HighlightedDisplayName\"></label>\n }\n <div class=\"mj-forms-field-control\">\n <!-- FK fields with a mapped name field: show the name as a hyperlink -->\n @if (IsFKWithNameField) {\n <span class=\"mj-forms-field-value\">\n @if (RecordLinksEnabled) {\n <a class=\"mj-forms-field-link mj-forms-field-link--fk\" (click)=\"OnRecordLinkClick($event)\" title=\"View related record\">\n {{ FKDisplayName }}\n </a>\n } @else {\n {{ FKDisplayName }}\n }\n </span>\n }\n @else if (ExtendedDisplay === 'image') {\n <div class=\"mj-forms-image\">\n @if (ImagePreviewSrc) {\n <img class=\"mj-forms-image-preview\" [src]=\"ImagePreviewSrc\" alt=\"\" />\n }\n @if (IsInlineImageValue) {\n <span class=\"mj-forms-image-meta\">Inline image ({{ ImageValueSizeLabel }})</span>\n } @else if (ImagePreviewSrc) {\n <a class=\"mj-forms-field-link\" (click)=\"OnUrlLinkClick($event)\" title=\"Open image\">\n {{ ImageUrlDisplay }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n } @else {\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n }\n </div>\n }\n @else if (ExtendedDisplay === 'color') {\n <div class=\"mj-forms-color\">\n <span class=\"mj-forms-color-swatch\" [style.background-color]=\"ColorSwatchValue\"></span>\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n </div>\n }\n @else if (ExtendedDisplay === 'json') {\n <pre class=\"mj-forms-json\">{{ JsonDisplayValue }}</pre>\n }\n @else {\n @switch (LinkType) {\n @case ('Email') {\n <span class=\"mj-forms-field-value\">\n <a class=\"mj-forms-field-link\" (click)=\"OnEmailLinkClick($event)\" title=\"Send email\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-envelope mj-forms-field-link-icon\"></i>\n </a>\n </span>\n }\n @case ('URL') {\n <span class=\"mj-forms-field-value\">\n <a class=\"mj-forms-field-link\" (click)=\"OnUrlLinkClick($event)\" title=\"Open link\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n </span>\n }\n @case ('Record') {\n <span class=\"mj-forms-field-value\">\n @if (RecordLinksEnabled) {\n <a class=\"mj-forms-field-link\" (click)=\"OnRecordLinkClick($event)\" title=\"View related record\">\n {{ FormatValue() }}\n <i class=\"fa-solid fa-arrow-up-right-from-square mj-forms-field-link-icon\"></i>\n </a>\n } @else {\n {{ FormatValue() }}\n }\n </span>\n }\n @default {\n @if (Type === 'checkbox') {\n <span class=\"mj-forms-field-value\">\n <div class=\"mj-forms-field-checkbox\">\n <input type=\"checkbox\" [checked]=\"!!Value\" disabled>\n </div>\n </span>\n }\n @else if (RichTextMode === 'markdown') {\n <div class=\"mj-forms-field-value mj-forms-field-value--rich\">\n <mj-markdown [data]=\"FormatValue()\"></mj-markdown>\n </div>\n }\n @else if (RichTextMode === 'html') {\n <!-- XSS: the mjSafeRichHtml pipe sanitizes with DOMPurify (HTML + SVG profiles)\n and then bypasses Angular's redundant re-sanitize. DOMPurify strips <script>,\n on* handlers, javascript:/data: URLs and <foreignObject> while KEEPING safe\n inline SVG + styles \u2014 richer than Angular's built-in sanitizer, still safe for\n untrusted content. -->\n <div class=\"mj-forms-field-value mj-forms-field-value--rich\" [innerHTML]=\"FormatValue() | mjSafeRichHtml\"></div>\n }\n @else if (RichTextMode === 'code') {\n <div class=\"mj-forms-field-value mj-forms-field-value--code\">\n <mj-code-editor [value]=\"FormatValue()\" [language]=\"CodeEditorLanguage\" [readonly]=\"true\"></mj-code-editor>\n </div>\n }\n @else if (IsLongText) {\n <span class=\"mj-forms-field-value mj-forms-field-value--long\">{{ FormatValue() }}</span>\n }\n @else {\n <span class=\"mj-forms-field-value\">{{ FormatValue() }}</span>\n }\n }\n }\n }\n </div>\n </div>\n }\n\n <!-- EDIT MODE -->\n @if (EditMode && !IsFieldReadOnly) {\n <div class=\"mj-forms-field mj-forms-field--editing\" [class.mj-forms-field--dirty]=\"IsDirty\" [class.mj-forms-field--required-empty]=\"IsRequiredEmpty\" [class.mj-forms-field--has-error]=\"ShowErrors\" [class.mj-forms-field--has-warning]=\"ShowWarnings || StoredDateIsUnreadable\">\n @if (ShowLabel) {\n <label class=\"mj-forms-field-label\" [innerHTML]=\"HighlightedDisplayName\"></label>\n }\n <div class=\"mj-forms-field-control\">\n <!-- Foreign Key fields get custom search autocomplete -->\n @if (HasRelatedEntity) {\n <div class=\"mj-fk-search\"\n [class.mj-fk-search--matched]=\"FKHasLinkedValue\"\n [class.mj-fk-search--scoped]=\"FKFocused && FKSearchableFields.length > 1\">\n @if (FKHasLinkedValue && !(FKFocused && FKSearchableFields.length > 1)) {\n <i class=\"fa-solid fa-link mj-fk-link-icon\" title=\"Linked record\"></i>\n }\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [class.mj-forms-field-input--linked]=\"FKHasLinkedValue\"\n [value]=\"FKSearchText\"\n (input)=\"OnFKInput($event)\"\n (focus)=\"OnFKFocus($event)\"\n (blur)=\"OnFKBlur()\"\n (keydown)=\"OnFKKeydown($event)\"\n placeholder=\"Search...\">\n\n <!-- Scope pill: only while focused, only when there's more than one searchable field.\n The menu itself lives INSIDE the results dropdown (below) so it overlays the\n list and rides the same above/below flip. -->\n @if (FKFocused && FKSearchableFields.length > 1) {\n <button class=\"mj-fk-scope-pill\"\n [class.mj-fk-scope-pill--open]=\"FKShowScopeMenu\"\n (mousedown)=\"OnFKScopeToggle($event)\"\n [title]=\"'Searching ' + FKSearchFieldLabel + ' \u2014 click to change'\">\n <i class=\"fa-solid fa-magnifying-glass mj-fk-scope-pill-icon\"></i>\n <span class=\"mj-fk-scope-pill-label\">{{ FKSearchFieldLabel }}</span>\n <i class=\"fa-solid mj-fk-scope-pill-caret\" [class.fa-chevron-up]=\"FKShowScopeMenu\" [class.fa-chevron-down]=\"!FKShowScopeMenu\"></i>\n </button>\n }\n\n @if (FKSearchText) {\n <button class=\"mj-fk-clear\" (mousedown)=\"OnFKClearClick($event)\" title=\"Clear\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n } @else {\n <i class=\"fa-solid fa-magnifying-glass mj-fk-search-icon\"></i>\n }\n @if (ShowFKDropdown) {\n <div class=\"mj-fk-dropdown mj-fk-dropdown--multi\"\n [ngStyle]=\"DropdownPositionStyle\"\n [class.mj-fk-dropdown--above]=\"OpenAbove\"\n [class.mj-fk-dropdown--scope-open]=\"FKShowScopeMenu\">\n\n <!-- Scope (search-field) picker + column toggles \u2014 overlays the list, anchored to the pill edge.\n Click a row to SEARCH that field; click the eye to SHOW/HIDE it as a column. -->\n @if (FKShowScopeMenu) {\n <div class=\"mj-fk-scope-menu\" [class.mj-fk-scope-menu--bottom]=\"OpenAbove\">\n <div class=\"mj-fk-scope-grp\">Columns shown</div>\n @for (f of FKSearchableFields; track f.FieldName) {\n @if (f.Group === 'shown') {\n <ng-container *ngTemplateOutlet=\"scopeRow; context: { $implicit: f }\"></ng-container>\n }\n }\n @if (FKHasOtherSearchableFields) {\n <div class=\"mj-fk-scope-divider\"></div>\n <div class=\"mj-fk-scope-grp\">Other fields</div>\n @for (f of FKSearchableFields; track f.FieldName) {\n @if (f.Group === 'other') {\n <ng-container *ngTemplateOutlet=\"scopeRow; context: { $implicit: f }\"></ng-container>\n }\n }\n }\n </div>\n }\n\n <!-- Shared scope-menu row, fixed-slot layout for clean alignment:\n [reorder \u25B2\u25BC \u2014 hover-reveal] [icon] [label] [eye].\n Click the row = search this field; the active search row is shown bold+accent. -->\n <ng-template #scopeRow let-f>\n <div class=\"mj-fk-scope-mi\"\n [class.mj-fk-scope-mi--on]=\"IsFKScopeActive(f)\"\n (mousedown)=\"OnFKScopeSelect($event, f)\">\n <span class=\"mj-fk-col-reorder\">\n @if (f.Group === 'shown') {\n <button type=\"button\" class=\"mj-fk-col-move mj-fk-col-move--up\" [disabled]=\"!CanMoveFKColumnUp(f.FieldName)\"\n (mousedown)=\"MoveFKColumn($event, f.FieldName, -1)\" title=\"Move column up\">\n <i class=\"fa-solid fa-chevron-up\"></i>\n </button>\n <button type=\"button\" class=\"mj-fk-col-move mj-fk-col-move--down\" [disabled]=\"!CanMoveFKColumnDown(f.FieldName)\"\n (mousedown)=\"MoveFKColumn($event, f.FieldName, 1)\" title=\"Move column down\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n </button>\n }\n </span>\n <i [class]=\"f.Icon + ' mj-fk-scope-mi-icon'\"></i>\n <span class=\"mj-fk-scope-mi-label\">{{ f.Label }}</span>\n <span class=\"mj-fk-col-eye-slot\">\n @if (CanToggleFKColumn(f)) {\n <button type=\"button\" class=\"mj-fk-col-toggle\"\n [class.mj-fk-col-toggle--on]=\"IsFKColumnVisible(f)\"\n (mousedown)=\"OnFKColumnToggle($event, f)\"\n [title]=\"IsFKColumnVisible(f) ? 'Hide this column' : 'Show as a column'\">\n <i class=\"fa-solid\" [class.fa-eye]=\"IsFKColumnVisible(f)\" [class.fa-eye-slash]=\"!IsFKColumnVisible(f)\"></i>\n </button>\n }\n </span>\n </div>\n </ng-template>\n\n <!-- Pinned \"currently selected\" \u2014 sticky at the top while browsing, so you can\n keep it or pick another without clearing first. -->\n @if (FKSelectedSuggestion && !FKLoading) {\n <div class=\"mj-fk-current\">\n <div class=\"mj-fk-current-head\">\n <i class=\"fa-solid fa-circle-check\"></i>\n <span>Currently selected</span>\n </div>\n <div class=\"mj-fk-current-row\" (mousedown)=\"OnFKSelectItem(FKSelectedSuggestion, $event)\" title=\"Keep this selection\">\n @if (FKSelectedSuggestion.Icon) { <i class=\"mj-fk-current-icon\" [class]=\"FKSelectedSuggestion.Icon\"></i> }\n <span class=\"mj-fk-current-name\" [innerHTML]=\"FKSelectedSuggestion.HighlightedName\"></span>\n @if (FKSelectedSuggestion.ExtraColumns.length && FKSelectedSuggestion.ExtraColumns[0].Value) {\n <span class=\"mj-fk-current-detail\">{{ FKSelectedSuggestion.ExtraColumns[0].Value }}</span>\n }\n <button type=\"button\" class=\"mj-fk-current-clear\" (mousedown)=\"OnFKClearClick($event)\" title=\"Clear selection\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n }\n\n @if (FKLoading) {\n <div class=\"mj-fk-status\">\n <i class=\"fa-solid fa-circle-notch fa-spin\"></i>\n <span>Searching\u2026</span>\n </div>\n }\n @if (!FKLoading && FKNoMatches) {\n <div class=\"mj-fk-status mj-fk-status--empty\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n <span>No matches</span>\n </div>\n }\n @if (!FKLoading && FKSuggestions.length > 0) {\n <div class=\"mj-fk-options\">\n <div class=\"mj-fk-grid\" [style.grid-template-columns]=\"FKGridTemplateColumns\">\n <!-- Header row: click to sort, drag the right edge to resize.\n (Column reorder lives in the field-selector menu, not here.) -->\n <div class=\"mj-fk-grid-row mj-fk-grid-row--header\">\n @if (FKHasIconColumn) {\n <div class=\"mj-fk-hcell mj-fk-hcell--icon\"></div>\n }\n @for (col of FKColumnFields; track col; let i = $index) {\n <div class=\"mj-fk-hcell\"\n (mousedown)=\"OnFKHeaderSort($event, col)\"\n [title]=\"'Click to sort by ' + FKColumnHeaders[i]\">\n <span class=\"mj-fk-hcell-label\">{{ FKColumnHeaders[i] }}</span>\n <i [class]=\"FKSortIcon(col)\"></i>\n <span class=\"mj-fk-resizer\" (mousedown)=\"OnFKColResizeStart($event, col)\" title=\"Drag to resize\"></span>\n </div>\n }\n </div>\n <!-- Data rows -->\n @for (suggestion of FKSuggestions; track suggestion.PrimaryKeyValue; let i = $index) {\n <div class=\"mj-fk-grid-row\"\n [class.mj-fk-grid-row--active]=\"i === FKActiveIndex\"\n [class.mj-fk-grid-row--selected]=\"IsFKRowSelected(suggestion)\"\n (mousedown)=\"OnFKSelectItem(suggestion, $event)\"\n (mouseenter)=\"FKActiveIndex = i\">\n @if (FKHasIconColumn) {\n <div class=\"mj-fk-cell mj-fk-cell--icon\">\n @if (suggestion.Icon) { <i [class]=\"suggestion.Icon\"></i> }\n </div>\n }\n @for (col of FKColumnFields; track col; let last = $last) {\n @if (col === FKNameField) {\n <div class=\"mj-fk-cell mj-fk-cell--name\" [class.mj-fk-cell--fill]=\"FKColumnFillsTrack(col, last)\">\n @if (IsFKRowSelected(suggestion)) {\n <i class=\"fa-solid fa-check mj-fk-selected-check\" title=\"Currently selected\"></i>\n }\n <span [innerHTML]=\"suggestion.HighlightedName\"></span>\n </div>\n } @else {\n <div class=\"mj-fk-cell\" [class.mj-fk-cell--fill]=\"FKColumnFillsTrack(col, last)\" [title]=\"FKCellFor(suggestion, col)?.Value || ''\">\n <span [innerHTML]=\"FKCellFor(suggestion, col)?.HighlightedValue || ''\"></span>\n </div>\n }\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Sticky \"create new\" footer \u2014 always visible at the bottom (no scrolling),\n prefilled with the typed text. Gated on create permission. -->\n @if (CanCreateFK) {\n <button type=\"button\" class=\"mj-fk-create-footer\" (mousedown)=\"OnFKCreateNew($event)\" [title]=\"FKCreateLabel\">\n <i class=\"fa-solid fa-plus mj-fk-create-icon\"></i>\n <span class=\"mj-fk-create-label\">{{ FKCreateLabel }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n @else if (ExtendedDisplay === 'image') {\n <div class=\"mj-forms-image mj-forms-image--edit\">\n @if (ImagePreviewSrc) {\n <img class=\"mj-forms-image-preview\" [src]=\"ImagePreviewSrc\" alt=\"\" />\n } @else {\n <div class=\"mj-forms-image-preview mj-forms-image-preview--empty\" aria-hidden=\"true\">\n <i class=\"fa-regular fa-image\"></i>\n </div>\n }\n <div class=\"mj-forms-image-meta\">\n @if (IsInlineImageValue) {\n <span>Inline image ({{ ImageValueSizeLabel }})</span>\n }\n <span class=\"mj-forms-image-hint\">Max {{ ImageMaxSizeLabel }}</span>\n </div>\n <div class=\"mj-forms-image-actions\">\n <input #imageFileInput type=\"file\" accept=\"image/png,image/jpeg,image/gif,image/webp,image/svg+xml\"\n class=\"mj-forms-image-file\" (change)=\"OnImageFileSelected($event)\">\n <button type=\"button\" class=\"mj-forms-image-btn\" (click)=\"imageFileInput.click()\">\n <i class=\"fa-solid fa-upload\"></i>\n {{ ImagePreviewSrc ? 'Replace' : 'Upload' }}\n </button>\n @if (ImagePreviewSrc) {\n <button type=\"button\" class=\"mj-forms-image-btn mj-forms-image-btn--ghost\" (click)=\"OnImageClear()\">Clear</button>\n }\n </div>\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"ImageUrlInputValue\"\n (input)=\"OnImageUrlInput($event)\"\n placeholder=\"https://\u2026 or paste a data URI\">\n </div>\n }\n @else if (ExtendedDisplay === 'color') {\n <div class=\"mj-forms-color mj-forms-color--edit\">\n <input type=\"color\"\n class=\"mj-forms-color-picker\"\n [value]=\"ColorPickerValue\"\n (input)=\"OnColorPickerInput($event)\"\n [attr.aria-label]=\"DisplayName + ' color'\">\n <input type=\"text\"\n class=\"mj-forms-field-input mj-forms-color-hex\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"\n placeholder=\"#RRGGBB\">\n </div>\n }\n @else if (ExtendedDisplay === 'json') {\n <textarea\n class=\"mj-forms-field-input mj-forms-field-input--textarea mj-forms-json\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"\n (blur)=\"OnJsonBlur()\"></textarea>\n }\n @else if (EditRichTextMode !== 'plain') {\n <!-- Markdown / HTML / Code long-text: edit raw with a syntax-highlighted code editor -->\n <mj-code-editor\n class=\"mj-forms-field-input--code\"\n [value]=\"FormatValue()\"\n [language]=\"CodeEditorLanguage\"\n (change)=\"OnCodeEditorChange($event)\"></mj-code-editor>\n }\n @else {\n @switch (Type) {\n @case ('textbox') {\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\">\n }\n @case ('textarea') {\n <textarea\n class=\"mj-forms-field-input mj-forms-field-input--textarea\"\n [value]=\"FormatValue()\"\n (input)=\"OnInputChange($event)\"></textarea>\n }\n @case ('number') {\n <input type=\"number\"\n class=\"mj-forms-field-input mj-forms-field-input--number\"\n [value]=\"Value\"\n (change)=\"OnNumberChange($event)\">\n }\n @case ('datepicker') {\n <input type=\"date\"\n class=\"mj-forms-field-input mj-forms-field-input--date\"\n [value]=\"DateInputValue\"\n (change)=\"OnDateChange($event)\">\n }\n @case ('checkbox') {\n <div class=\"mj-forms-field-checkbox\">\n <input type=\"checkbox\"\n [checked]=\"!!Value\"\n (change)=\"OnCheckboxChange($event)\">\n </div>\n }\n @case ('select') {\n <div class=\"mj-custom-select-trigger\"\n tabindex=\"0\"\n (click)=\"OnSelectTriggerClick($event)\"\n (blur)=\"OnSelectTriggerBlur()\">\n <span class=\"mj-custom-select-value\" [class.mj-custom-select-value--placeholder]=\"!FormatValue()\">\n {{ FormatValue() || '-- Select --' }}\n </span>\n <i class=\"fa-solid fa-chevron-down mj-custom-select-chevron\" [class.mj-custom-select-chevron--open]=\"ShowSelectDropdown\"></i>\n </div>\n @if (ShowSelectDropdown && PossibleValues.length > 0) {\n <div class=\"mj-fk-dropdown\" [ngStyle]=\"DropdownPositionStyle\" [class.mj-fk-dropdown--above]=\"OpenAbove\">\n @for (option of PossibleValues; track option) {\n <div class=\"mj-fk-option\" [class.mj-fk-option--selected]=\"FormatValue() === option\" (mousedown)=\"OnSelectOptionClick(option, $event)\">\n {{ option }}\n </div>\n }\n </div>\n }\n }\n @case ('autocomplete') {\n <div class=\"mj-fk-search\">\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (input)=\"OnValueListInput($event)\"\n (focus)=\"OnValueListFocus($event)\"\n (blur)=\"OnValueListBlur()\"\n placeholder=\"Type to search...\">\n <i class=\"fa-solid fa-magnifying-glass mj-fk-search-icon\"></i>\n @if (ShowValueListDropdown && FilteredPossibleValues.length > 0) {\n <div class=\"mj-fk-dropdown\" [ngStyle]=\"DropdownPositionStyle\" [class.mj-fk-dropdown--above]=\"OpenAbove\">\n @for (option of FilteredPossibleValues; track option) {\n <div class=\"mj-fk-option\" (mousedown)=\"OnValueListSelect(option, $event)\">\n {{ option }}\n </div>\n }\n </div>\n }\n </div>\n }\n @default {\n <input type=\"text\"\n class=\"mj-forms-field-input\"\n [value]=\"FormatValue()\"\n (change)=\"OnInputChange($event)\">\n }\n }\n }\n </div>\n <!-- Inline validation messages -->\n @if (DisplayErrors.length > 0) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--error\">\n @for (error of DisplayErrors; track error.Message) {\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-circle-exclamation\"></i>\n <span>{{ error.Message }}</span>\n </div>\n }\n </div>\n }\n @if (DisplayWarnings.length > 0) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--warning\">\n @for (warning of DisplayWarnings; track warning.Message) {\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>{{ warning.Message }}</span>\n </div>\n }\n </div>\n }\n <!--\n An unreadable stored value renders as an EMPTY date box, identical to a field that\n was never set - and the next save writes that emptiness over whatever is stored. The\n element cannot display the bad value itself, so the warning sits with the other validation messages, beneath the control.\n -->\n @if (StoredDateIsUnreadable) {\n <div class=\"mj-forms-field-validation mj-forms-field-validation--warning\">\n <div class=\"mj-forms-field-validation-item\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>This field holds a value that is not a readable date, so the box above is\n empty. Saving now will replace it.</span>\n </div>\n </div>\n }\n </div>\n }\n}\n", styles: ["/* ============================================\n FORM FIELD - Shared styles for read and edit\n ============================================ */\n:host {\n --mj-forms-field-label-width: 200px;\n --mj-forms-field-gap: 12px;\n\n display: contents;\n}\n\n.mj-forms-field {\n display: grid;\n grid-template-columns: var(--mj-forms-field-label-width) 1fr;\n gap: var(--mj-forms-field-gap);\n align-items: baseline;\n padding: 8px 0;\n transition: all var(--mj-transition-base) ease;\n}\n\n/* Subtle row hover in edit mode */\n.mj-forms-field--editing:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 6px;\n margin: 0 -8px;\n padding: 8px 8px;\n}\n\n/* ============================================\n LABEL\n ============================================ */\n.mj-forms-field-label {\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-secondary);\n padding-top: 2px;\n word-break: break-word;\n}\n\n/* ============================================\n FIELD CONTROL WRAPPER (value + dirty dot)\n ============================================ */\n.mj-forms-field-control {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-field-control > * {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n DIRTY INDICATOR (solid amber dot after label)\n ============================================ */\n.mj-forms-field--dirty .mj-forms-field-label::after {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--mj-status-warning);\n margin-left: 6px;\n vertical-align: middle;\n}\n\n/* ============================================\n REQUIRED FIELD - red underline when empty\n ============================================ */\n.mj-forms-field--required-empty .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--required-empty .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n}\n\n/* ============================================\n READ-ONLY VALUE\n ============================================ */\n.mj-forms-field-value {\n font-size: 14px;\n color: var(--mj-text-primary);\n min-height: 24px;\n display: flex;\n align-items: center;\n overflow-wrap: break-word;\n word-break: break-word;\n}\n\n.mj-forms-field-value--long {\n white-space: pre-line;\n max-height: 300px;\n overflow: auto;\n}\n\n/* Rich (markdown / html) read-only rendering \u2014 let content flow as a block, not a flex row */\n.mj-forms-field-value--rich {\n display: block;\n align-items: initial;\n max-height: 480px;\n overflow: auto;\n width: 100%;\n}\n\n/* Code read-only / edit rendering \u2014 the embedded code editor needs an explicit height */\n.mj-forms-field-value--code {\n display: block;\n align-items: initial;\n width: 100%;\n}\n\n.mj-forms-field-value--code mj-code-editor,\n.mj-forms-field-input--code {\n display: block;\n width: 100%;\n min-height: 160px;\n max-height: 480px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius, 4px);\n overflow: auto;\n}\n\n/* Read-only fields in edit mode */\n.mj-forms-field--readonly {\n opacity: 0.7;\n}\n\n.mj-forms-field--readonly .mj-forms-field-value {\n color: var(--mj-text-secondary);\n}\n\n/* ============================================\n LINKS (FK, email, URL)\n ============================================ */\n.mj-forms-field-link {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-brand-primary);\n cursor: pointer;\n font-weight: 400;\n font-size: 14px;\n transition: color var(--mj-transition-base);\n text-decoration: none;\n}\n\n.mj-forms-field-link:hover {\n color: var(--mj-brand-primary-hover);\n}\n\n.mj-forms-field-link-icon {\n font-size: 11px;\n opacity: 0;\n transition: opacity var(--mj-transition-base);\n}\n\n.mj-forms-field-link:hover .mj-forms-field-link-icon {\n opacity: 1;\n}\n\n/* FK fields showing entity name instead of raw ID */\n.mj-forms-field-link--fk {\n font-size: 14px;\n font-weight: 400;\n}\n\n/* ============================================\n EDIT MODE INPUTS - Clean underline style\n ============================================ */\n.mj-forms-field-input {\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-forms-field-input:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-forms-field-input::placeholder {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n/* Textarea variant */\n.mj-forms-field-input--textarea {\n resize: vertical;\n min-height: 60px;\n border: 1.5px solid var(--mj-border-default);\n border-radius: 6px;\n padding: 8px 10px;\n background: var(--mj-bg-surface);\n font-family: inherit;\n}\n\n.mj-forms-field-input--textarea:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n background: var(--mj-bg-surface);\n padding: 8px 10px;\n border-radius: 6px;\n}\n\n/* Number input - narrower */\n.mj-forms-field-input--number {\n max-width: 200px;\n}\n\n/* Date input - narrower */\n.mj-forms-field-input--date {\n max-width: 220px;\n}\n\n/* ============================================\n CHECKBOX (custom styled)\n ============================================ */\n.mj-forms-field-checkbox {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"] {\n appearance: none;\n width: 18px;\n height: 18px;\n border: 2px solid var(--mj-border-default);\n border-radius: 4px;\n cursor: pointer;\n position: relative;\n transition: all var(--mj-transition-base) ease;\n flex-shrink: 0;\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:checked {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:checked::after {\n content: '';\n position: absolute;\n left: 5px;\n top: 1px;\n width: 5px;\n height: 9px;\n border: solid var(--mj-text-inverse);\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n}\n\n.mj-forms-field-checkbox input[type=\"checkbox\"]:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n/* ============================================\n CUSTOM SELECT DROPDOWN (replaces native <select>)\n ============================================ */\n.mj-custom-select-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 14px;\n font-family: inherit;\n color: var(--mj-text-primary);\n background: transparent;\n border: none;\n border-bottom: 1.5px solid var(--mj-border-default);\n padding: 4px 0;\n width: 100%;\n cursor: pointer;\n outline: none;\n transition: border-color var(--mj-transition-base) ease,\n background var(--mj-transition-base) ease;\n}\n\n.mj-custom-select-trigger:focus {\n border-bottom-color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-radius: 4px 4px 0 0;\n padding: 4px 6px;\n}\n\n.mj-custom-select-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mj-custom-select-value--placeholder {\n color: var(--mj-text-muted);\n font-style: italic;\n}\n\n.mj-custom-select-chevron {\n font-size: 11px;\n color: var(--mj-text-muted);\n transition: transform var(--mj-transition-base) ease;\n flex-shrink: 0;\n margin-left: 8px;\n}\n\n.mj-custom-select-chevron--open {\n transform: rotate(180deg);\n}\n\n/* ============================================\n FK SEARCH (custom autocomplete)\n ============================================ */\n.mj-fk-search {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n}\n\n.mj-fk-search .mj-forms-field-input {\n padding-right: 28px;\n}\n\n.mj-fk-search-icon {\n position: absolute;\n right: 4px;\n color: var(--mj-text-muted);\n font-size: 12px;\n pointer-events: none;\n}\n\n.mj-fk-clear {\n position: absolute;\n right: 2px;\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 11px;\n padding: 4px 6px;\n line-height: 1;\n border-radius: 3px;\n transition: all var(--mj-transition-base);\n}\n\n.mj-fk-clear:hover {\n color: var(--mj-status-error);\n background: color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n/* Linked state: solid accent underline + a leading chain icon, so it's obvious the\n field holds a resolved record reference rather than free-typed text. */\n.mj-fk-search--matched .mj-forms-field-input {\n border-bottom: 2px solid var(--mj-brand-primary);\n}\n\n/* Linked indicator sits on the RIGHT (just left of the clear button) so it never\n interrupts left-to-right reading of the value or overlaps the text. */\n.mj-fk-link-icon {\n position: absolute;\n right: 26px;\n color: var(--mj-brand-primary);\n font-size: 10px;\n pointer-events: none;\n z-index: 1;\n}\n\n/* Make room on the right for the link icon + the clear button. */\n.mj-forms-field-input--linked {\n padding-right: 46px;\n}\n\n/* ============================================\n SCOPE PILL + FIELD-PICKER MENU (search-field scope)\n ============================================ */\n/* When the pill is showing, reserve room on the right of the input for it. */\n.mj-fk-search--scoped .mj-forms-field-input {\n padding-right: 150px;\n}\n\n.mj-fk-scope-pill {\n position: absolute;\n right: 30px;\n /* Vertically centered in the input so it clears the focus underline below. */\n top: 50%;\n transform: translateY(-50%);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 120px;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n color: var(--mj-text-secondary);\n font-size: 10.5px;\n font-weight: 600;\n font-family: inherit;\n /* Kept short so it clears the input's focus underline without making the row taller. */\n padding: 1px 7px;\n border-radius: 999px;\n cursor: pointer;\n white-space: nowrap;\n line-height: 1.35;\n transition: background var(--mj-transition-base), border-color var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-pill:hover,\n.mj-fk-scope-pill--open {\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-primary);\n}\n\n.mj-fk-scope-pill-icon {\n font-size: 9px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-pill-label {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-scope-pill-caret {\n font-size: 8px;\n opacity: 0.7;\n flex-shrink: 0;\n}\n\n/* The menu is rendered INSIDE the results dropdown panel so it overlays the list\n (correct z-order, no extra real estate) and rides the dropdown's above/below\n flip. Anchored to the dropdown's top-right \u2014 right under the pill. */\n.mj-fk-scope-menu {\n position: absolute;\n top: 4px;\n right: 8px;\n z-index: 5;\n min-width: 180px;\n max-width: calc(100vw - 24px);\n max-height: 260px;\n overflow-y: auto;\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n border-radius: var(--mj-radius-sm);\n box-shadow: var(--mj-shadow-lg);\n padding: 5px;\n animation: mj-dropdown-in 120ms ease;\n}\n\n/* When the dropdown opens ABOVE the input, anchor the menu to its bottom edge so it\n stays next to the pill (which is at the input/dropdown boundary). */\n.mj-fk-scope-menu--bottom {\n top: auto;\n bottom: 4px;\n}\n\n.mj-fk-scope-grp {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-text-muted);\n padding: 7px 10px 4px;\n font-weight: 700;\n}\n\n.mj-fk-scope-mi {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 13px;\n border-radius: 5px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n}\n\n.mj-fk-scope-mi:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-scope-mi--on {\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n/* Fixed-width LEFT slot: a compact stacked up/down reorder control that reveals on row\n hover. Reserved (same width) on every row so the field icons + labels stay aligned. */\n.mj-fk-col-reorder {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 22px;\n flex-shrink: 0;\n}\n\n.mj-fk-col-move {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 10px;\n padding: 0;\n background: transparent;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 8px;\n opacity: 0; /* hover-reveal for a clean resting state */\n transition: opacity var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-scope-mi:hover .mj-fk-col-move:not(:disabled) {\n opacity: 0.8;\n}\n\n.mj-fk-col-move:hover:not(:disabled) {\n opacity: 1;\n color: var(--mj-brand-primary);\n}\n\n/* End-of-range arrows stay hidden (slot still reserved) rather than greyed. */\n.mj-fk-col-move:disabled {\n opacity: 0 !important;\n cursor: default;\n}\n\n.mj-fk-scope-mi-icon {\n width: 14px;\n font-size: 11px;\n opacity: 0.75;\n flex-shrink: 0;\n}\n\n.mj-fk-scope-mi-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Fixed-width RIGHT slot so the eye (or its empty space, for the name row) aligns. */\n.mj-fk-col-eye-slot {\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Eye toggle: show/hide a field as a grid column (independent of search scope). */\n.mj-fk-col-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-col-toggle:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-secondary);\n}\n\n.mj-fk-col-toggle--on {\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-scope-divider {\n height: 1px;\n background: var(--mj-border-subtle);\n margin: 4px 6px;\n}\n\n/* Narrow / mobile screens: shrink the pill + its reserved space, give menu rows a\n larger touch target, and keep the menu within the viewport. */\n@media (max-width: 560px) {\n .mj-fk-search--scoped .mj-forms-field-input {\n padding-right: 116px;\n }\n .mj-fk-scope-pill {\n max-width: 88px;\n }\n .mj-fk-scope-menu {\n min-width: 160px;\n }\n .mj-fk-scope-mi {\n padding: 10px 10px;\n }\n}\n\n/* ============================================\n FLOATING DROPDOWN PANEL (position: fixed via ngStyle)\n ============================================ */\n.mj-fk-dropdown {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md);\n box-shadow: var(--mj-shadow-lg);\n max-height: 240px;\n overflow-y: auto;\n padding: 4px;\n animation: mj-dropdown-in 150ms ease;\n}\n\n.mj-fk-dropdown--above {\n animation: mj-dropdown-in-above 150ms ease;\n}\n\n.mj-fk-option {\n padding: 10px 14px;\n font-size: 14px;\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: background var(--mj-transition-base);\n border-radius: 6px;\n}\n\n.mj-fk-option:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n.mj-fk-option--selected {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-option--selected:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n/* ============================================\n MULTI-COLUMN FK DROPDOWN \u2014 aligned mini-grid (icon + name + extra cols)\n with sortable headers and horizontal scroll.\n ============================================ */\n.mj-fk-dropdown--multi {\n padding: 4px;\n /* The inner .mj-fk-options is the single scroll container (both axes) so the\n sticky header anchors correctly; the dropdown just clips to its rounded border. */\n overflow: hidden;\n max-height: none;\n}\n\n/* Single scroll region (both axes) for the grid: vertical for long lists, horizontal\n when many DefaultInView columns make rows wider than the dropdown. Being the sole\n scroller lets the sticky column headers anchor to its top edge. */\n.mj-fk-dropdown--multi .mj-fk-options {\n overflow: auto;\n max-height: 260px;\n}\n\n/* While the scope menu is open it must escape the panel's clip (the inner\n .mj-fk-options keeps its own scroll, so the grid still behaves). */\n.mj-fk-dropdown--scope-open {\n overflow: visible;\n}\n\n/* One CSS grid spans the header + all data rows so every column lines up.\n grid-template-columns is supplied inline from FKGridTemplateColumns.\n `width: 100%` (with `min-width: max-content`) lets the last column's `1fr` track\n absorb leftover width, while still overflowing \u2192 horizontal scroll when the columns\n are collectively wider than the dropdown. */\n.mj-fk-grid {\n display: grid;\n width: 100%;\n min-width: max-content;\n align-items: stretch;\n}\n\n/* Rows are layout-transparent so their cells become direct grid items (this is\n what makes columns align across rows). The row element still exists for click\n handling and for the active-state selector below. */\n.mj-fk-grid-row {\n display: contents;\n}\n\n/* Header cells: sticky to the top of the vertical scroll, clickable to sort. */\n.mj-fk-hcell {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 7px 12px;\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-elevated);\n border-bottom: 1px solid var(--mj-border-default);\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n}\n\n.mj-fk-hcell:hover {\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-hcell--icon {\n cursor: default;\n padding: 7px 8px;\n}\n\n.mj-fk-hcell--icon:hover {\n background: var(--mj-bg-surface-elevated);\n}\n\n.mj-fk-hcell-label {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-sort-icon {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n/* Idle (unsorted) columns show a faded sort glyph to signal sortability. */\n.mj-fk-sort-icon--idle {\n opacity: 0.35;\n}\n\n/* Data cells */\n.mj-fk-cell {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 320px;\n cursor: pointer;\n}\n\n/* The fill column (last, or a user-resized one) drops the default content cap so its\n cells stretch to the full track width that the header already occupies. */\n.mj-fk-cell--fill {\n max-width: none;\n}\n\n/* Wrapping span keeps the highlighted text+<mark> as one inline run (so flex\n doesn't strip the space next to the mark) while still truncating with ellipsis. */\n.mj-fk-cell > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.mj-fk-cell--icon {\n justify-content: center;\n padding: 8px;\n max-width: none;\n color: var(--mj-text-muted);\n font-size: 13px;\n}\n\n.mj-fk-cell--name {\n font-weight: 500;\n font-size: 14px;\n color: var(--mj-text-primary);\n gap: 6px;\n}\n\n.mj-fk-selected-check {\n color: var(--mj-brand-primary);\n font-size: 11px;\n flex-shrink: 0;\n}\n\n/* Active (keyboard / hover) row \u2014 painted on the cells, since the row box is\n removed by display:contents. The child combinator still matches. */\n.mj-fk-grid-row--active > .mj-fk-cell {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent);\n}\n\n/* Currently-linked row \u2014 a subtle persistent tint so the user sees which record\n is selected when they reopen the list (stacks under the brighter active tint). */\n.mj-fk-grid-row--selected > .mj-fk-cell {\n background: color-mix(in srgb, var(--mj-brand-primary) 6%, transparent);\n}\n\n.mj-fk-grid-row--selected > .mj-fk-cell--name {\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n/* Matched-substring highlight inside the name column */\n/* ::ng-deep (not :global, which Angular's emulated encapsulation ignores) so the\n style reaches the <mark> injected via [innerHTML]. Color comes from the shared\n theme token so light/dark/white-label all control it centrally. */\n.mj-fk-cell--name ::ng-deep .mj-forms-search-highlight,\n.mj-fk-cell ::ng-deep .mj-forms-search-highlight {\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n/* Column resize handle \u2014 thin grab strip on the right edge of each header cell. */\n.mj-fk-resizer {\n position: absolute;\n top: 0;\n right: 0;\n width: 7px;\n height: 100%;\n cursor: col-resize;\n user-select: none;\n}\n\n.mj-fk-resizer::after {\n content: '';\n position: absolute;\n top: 20%;\n right: 2px;\n width: 2px;\n height: 60%;\n background: transparent;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-hcell:hover .mj-fk-resizer::after,\n.mj-fk-resizer:hover::after {\n background: var(--mj-border-strong);\n}\n\n/* Loading / empty status rows */\n.mj-fk-status {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 14px;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-status i {\n font-size: 12px;\n}\n\n.mj-fk-status--empty {\n font-style: italic;\n}\n\n/* Sticky \"create new\" footer \u2014 sits below the (scrolling) options as a direct child of\n the dropdown panel, so it's always visible regardless of list length. */\n.mj-fk-create-footer {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 10px 14px;\n background: var(--mj-bg-surface-card);\n border: none;\n border-top: 1px solid var(--mj-border-default);\n color: var(--mj-brand-primary);\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n.mj-fk-create-footer:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.mj-fk-create-icon {\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.mj-fk-create-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ============================================\n PINNED \"CURRENTLY SELECTED\" SECTION\n ============================================ */\n.mj-fk-current {\n border-bottom: 1px solid var(--mj-border-default);\n background: color-mix(in srgb, var(--mj-brand-primary) 7%, var(--mj-bg-surface-card));\n animation: mj-fk-current-in 220ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n@keyframes mj-fk-current-in {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.mj-fk-current-head {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px 3px;\n font-size: 10px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-brand-primary);\n}\n\n.mj-fk-current-head i {\n font-size: 10px;\n}\n\n.mj-fk-current-row {\n position: relative;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 6px 12px 10px;\n cursor: pointer;\n transition: background var(--mj-transition-base);\n}\n\n/* Accent bar on the left for a bit of sizzle. */\n.mj-fk-current-row::before {\n content: '';\n position: absolute;\n left: 0;\n top: 4px;\n bottom: 8px;\n width: 3px;\n border-radius: 0 2px 2px 0;\n background: var(--mj-brand-primary);\n}\n\n.mj-fk-current-row:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.mj-fk-current-icon {\n flex-shrink: 0;\n font-size: 13px;\n color: var(--mj-text-muted);\n}\n\n.mj-fk-current-name {\n font-weight: 600;\n font-size: 14px;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-detail {\n font-size: 13px;\n color: var(--mj-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-fk-current-clear {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n flex-shrink: 0;\n background: transparent;\n border: none;\n border-radius: 4px;\n color: var(--mj-text-muted);\n cursor: pointer;\n font-size: 12px;\n transition: background var(--mj-transition-base), color var(--mj-transition-base);\n}\n\n.mj-fk-current-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-status-error);\n}\n\n@keyframes mj-dropdown-in {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes mj-dropdown-in-above {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ============================================\n SEARCH HIGHLIGHTING\n ============================================ */\n.mj-forms-field-label ::ng-deep .mj-forms-search-highlight {\n /* Shared search-highlight theme token. ::ng-deep (not :global) so it reaches\n innerHTML content. */\n background-color: var(--mj-search-highlight-bg);\n color: var(--mj-search-highlight-text);\n border-radius: 2px;\n padding: 0 1px;\n font-weight: 600;\n}\n\n/* ============================================\n VALIDATION ERROR STATE\n ============================================ */\n.mj-forms-field--has-error .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-error) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error .mj-forms-field-input--textarea {\n border-color: var(--mj-status-error);\n}\n\n.mj-forms-field--has-error .mj-forms-field-input--textarea:focus {\n border-color: var(--mj-status-error);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-error) 8%, transparent);\n}\n\n.mj-forms-field--has-error .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-error .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-error);\n border-bottom-width: 2px;\n}\n\n/* ============================================\n VALIDATION WARNING STATE\n ============================================ */\n.mj-forms-field--has-warning .mj-forms-field-input {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input:focus {\n background: color-mix(in srgb, var(--mj-status-warning) 5%, var(--mj-bg-surface));\n border-bottom-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input--textarea {\n border-color: var(--mj-status-warning);\n}\n\n.mj-forms-field--has-warning .mj-forms-field-input--textarea:focus {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-warning) 8%, transparent);\n}\n\n.mj-forms-field--has-warning .mj-custom-select-trigger {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n.mj-forms-field--has-warning .mj-fk-search .mj-forms-field-input {\n border-bottom-color: var(--mj-status-warning);\n border-bottom-width: 2px;\n}\n\n/* ============================================\n VALIDATION MESSAGE AREA\n ============================================ */\n.mj-forms-field-validation {\n grid-column: 2;\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin-top: -2px;\n overflow: hidden;\n animation: mj-validation-slide-in 200ms ease-out;\n}\n\n.mj-forms-field-validation-item {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n font-size: 12px;\n line-height: 1.4;\n padding: 3px 0 1px;\n}\n\n.mj-forms-field-validation-item i {\n font-size: 11px;\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n/* Error message colors */\n.mj-forms-field-validation--error .mj-forms-field-validation-item {\n color: var(--mj-status-error);\n}\n\n.mj-forms-field-validation--error .mj-forms-field-validation-item i {\n color: var(--mj-status-error);\n}\n\n/* Warning message colors */\n.mj-forms-field-validation--warning .mj-forms-field-validation-item {\n color: var(--mj-status-warning-text);\n}\n\n.mj-forms-field-validation--warning .mj-forms-field-validation-item i {\n color: var(--mj-status-warning);\n}\n\n/* ============================================\n IMAGE / COLOR / JSON EXTENDED TYPES\n ============================================ */\n.mj-forms-image {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n min-width: 0;\n}\n\n.mj-forms-image-preview {\n width: 72px;\n height: 72px;\n object-fit: cover;\n object-position: center;\n border-radius: 8px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n display: block;\n}\n\n.mj-forms-image-preview--empty {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted);\n font-size: 22px;\n}\n\n.mj-forms-image-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n}\n\n.mj-forms-image-hint {\n color: var(--mj-text-muted);\n}\n\n.mj-forms-image-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-forms-image-file {\n display: none;\n}\n\n.mj-forms-image-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-family: inherit;\n font-weight: 600;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n border-radius: 6px;\n padding: 4px 10px;\n cursor: pointer;\n}\n\n.mj-forms-image-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n}\n\n.mj-forms-image-btn--ghost {\n color: var(--mj-text-secondary);\n background: transparent;\n border-color: var(--mj-border-default);\n font-weight: 500;\n}\n\n.mj-forms-image--edit .mj-forms-field-input {\n width: 100%;\n}\n\n.mj-forms-color {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.mj-forms-color-swatch {\n width: 20px;\n height: 20px;\n border-radius: 4px;\n border: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.mj-forms-color-picker {\n width: 32px;\n height: 32px;\n padding: 0;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n flex: 0 0 auto;\n}\n\n.mj-forms-color-hex {\n max-width: 160px;\n}\n\n.mj-forms-json {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 12.5px;\n line-height: 1.45;\n white-space: pre-wrap;\n word-break: break-word;\n margin: 0;\n max-height: 360px;\n overflow: auto;\n width: 100%;\n}\n\n@keyframes mj-validation-slide-in {\n from {\n opacity: 0;\n max-height: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n max-height: 100px;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n RESPONSIVE\n ============================================ */\n@media (max-width: 640px) {\n .mj-forms-field {\n grid-template-columns: 1fr;\n gap: 4px;\n }\n\n .mj-forms-field-label {\n font-size: 12px;\n }\n\n .mj-forms-field-validation {\n grid-column: 1;\n }\n}\n"] }]
3016
3165
  }], null, { Record: [{
3017
3166
  type: Input
3018
3167
  }], EditMode: [{