@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.
- package/dist/lib/base-form-component.d.ts +23 -0
- package/dist/lib/base-form-component.d.ts.map +1 -1
- package/dist/lib/base-form-component.js +56 -13
- package/dist/lib/base-form-component.js.map +1 -1
- package/dist/lib/field/form-field.component.d.ts +69 -0
- package/dist/lib/field/form-field.component.d.ts.map +1 -1
- package/dist/lib/field/form-field.component.js +167 -18
- package/dist/lib/field/form-field.component.js.map +1 -1
- package/dist/lib/isa-related-panel/isa-related-card.component.d.ts.map +1 -1
- package/dist/lib/isa-related-panel/isa-related-card.component.js +7 -1
- package/dist/lib/isa-related-panel/isa-related-card.component.js.map +1 -1
- package/dist/lib/panel/collapsible-panel.component.d.ts +10 -0
- package/dist/lib/panel/collapsible-panel.component.d.ts.map +1 -1
- package/dist/lib/panel/collapsible-panel.component.js +25 -0
- package/dist/lib/panel/collapsible-panel.component.js.map +1 -1
- package/dist/lib/toolbar/form-toolbar.component.d.ts +9 -1
- package/dist/lib/toolbar/form-toolbar.component.d.ts.map +1 -1
- package/dist/lib/toolbar/form-toolbar.component.js +10 -2
- package/dist/lib/toolbar/form-toolbar.component.js.map +1 -1
- package/dist/lib/types/form-types.d.ts +8 -0
- package/dist/lib/types/form-types.d.ts.map +1 -1
- package/dist/lib/types/form-types.js.map +1 -1
- package/package.json +18 -18
|
@@ -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.
|
|
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
|
-
//
|
|
1146
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
3021
|
+
this.markTouched();
|
|
2873
3022
|
}
|
|
2874
3023
|
catch {
|
|
2875
3024
|
this.imageUploadError = 'Could not read that image.';
|
|
2876
|
-
this.
|
|
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: [{
|