@memberjunction/ng-record-changes 6.1.4 → 6.2.0-edge.1
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/ng-record-changes.component.d.ts +96 -8
- package/dist/lib/ng-record-changes.component.d.ts.map +1 -1
- package/dist/lib/ng-record-changes.component.js +218 -56
- package/dist/lib/ng-record-changes.component.js.map +1 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.d.ts +2 -0
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.d.ts.map +1 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js +5 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js.map +1 -1
- package/package.json +10 -10
|
@@ -621,7 +621,15 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
621
621
|
sanitizer;
|
|
622
622
|
IsLoading = false;
|
|
623
623
|
IsVisible = false;
|
|
624
|
-
|
|
624
|
+
DialogClosed = new EventEmitter();
|
|
625
|
+
/**
|
|
626
|
+
* @deprecated Use {@link DialogClosed}.
|
|
627
|
+
*
|
|
628
|
+
* The same emitter under the old binding name, so a template still binding
|
|
629
|
+
* (dialogClosed) keeps working. Must stay AFTER DialogClosed: class fields
|
|
630
|
+
* initialise in order, and the other way round this captures undefined.
|
|
631
|
+
*/
|
|
632
|
+
dialogClosed = this.DialogClosed;
|
|
625
633
|
record;
|
|
626
634
|
/** Whether to show a "Restore" button on each historical version row. Default false. */
|
|
627
635
|
AllowRestore = false;
|
|
@@ -631,8 +639,24 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
631
639
|
* saving with `record.SetRestoreContext()` set first.
|
|
632
640
|
*/
|
|
633
641
|
RestoreRequested = new EventEmitter();
|
|
634
|
-
|
|
635
|
-
|
|
642
|
+
ViewData = [];
|
|
643
|
+
/** @deprecated Use {@link ViewData}. */
|
|
644
|
+
get viewData() {
|
|
645
|
+
return this.ViewData;
|
|
646
|
+
}
|
|
647
|
+
/** @deprecated Use {@link ViewData}. */
|
|
648
|
+
set viewData(value) {
|
|
649
|
+
this.ViewData = value;
|
|
650
|
+
}
|
|
651
|
+
FilteredData = [];
|
|
652
|
+
/** @deprecated Use {@link FilteredData}. */
|
|
653
|
+
get filteredData() {
|
|
654
|
+
return this.FilteredData;
|
|
655
|
+
}
|
|
656
|
+
/** @deprecated Use {@link FilteredData}. */
|
|
657
|
+
set filteredData(value) {
|
|
658
|
+
this.FilteredData = value;
|
|
659
|
+
}
|
|
636
660
|
/**
|
|
637
661
|
* Change lookup keyed by NormalizeUUID(ID), rebuilt whenever {@link viewData}
|
|
638
662
|
* loads. Lets {@link getRestoredFromSourceChange} resolve a restore row's
|
|
@@ -641,8 +665,24 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
641
665
|
* template `@if` and once inside `getChangeSummary`), giving O(rows^2)/CD.
|
|
642
666
|
*/
|
|
643
667
|
viewDataById = new Map();
|
|
644
|
-
|
|
645
|
-
|
|
668
|
+
DateGroups = [];
|
|
669
|
+
/** @deprecated Use {@link DateGroups}. */
|
|
670
|
+
get dateGroups() {
|
|
671
|
+
return this.DateGroups;
|
|
672
|
+
}
|
|
673
|
+
/** @deprecated Use {@link DateGroups}. */
|
|
674
|
+
set dateGroups(value) {
|
|
675
|
+
this.DateGroups = value;
|
|
676
|
+
}
|
|
677
|
+
ExpandedItems = new Set();
|
|
678
|
+
/** @deprecated Use {@link ExpandedItems}. */
|
|
679
|
+
get expandedItems() {
|
|
680
|
+
return this.ExpandedItems;
|
|
681
|
+
}
|
|
682
|
+
/** @deprecated Use {@link ExpandedItems}. */
|
|
683
|
+
set expandedItems(value) {
|
|
684
|
+
this.ExpandedItems = value;
|
|
685
|
+
}
|
|
646
686
|
/** The change record currently selected for restore preview, or null. */
|
|
647
687
|
RestorePreviewChange = null;
|
|
648
688
|
/** Visibility of the embedded restore preview slide-in. */
|
|
@@ -654,10 +694,34 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
654
694
|
IsLoadingLabels = false;
|
|
655
695
|
ShowCreateWizard = false;
|
|
656
696
|
// Filter properties
|
|
657
|
-
|
|
697
|
+
SearchTerm = '';
|
|
698
|
+
/** @deprecated Use {@link SearchTerm}. */
|
|
699
|
+
get searchTerm() {
|
|
700
|
+
return this.SearchTerm;
|
|
701
|
+
}
|
|
702
|
+
/** @deprecated Use {@link SearchTerm}. */
|
|
703
|
+
set searchTerm(value) {
|
|
704
|
+
this.SearchTerm = value;
|
|
705
|
+
}
|
|
658
706
|
/** Single selected type filter (legacy, kept for backwards compat). */
|
|
659
|
-
|
|
660
|
-
|
|
707
|
+
SelectedType = '';
|
|
708
|
+
/** @deprecated Use {@link SelectedType}. */
|
|
709
|
+
get selectedType() {
|
|
710
|
+
return this.SelectedType;
|
|
711
|
+
}
|
|
712
|
+
/** @deprecated Use {@link SelectedType}. */
|
|
713
|
+
set selectedType(value) {
|
|
714
|
+
this.SelectedType = value;
|
|
715
|
+
}
|
|
716
|
+
SelectedSource = '';
|
|
717
|
+
/** @deprecated Use {@link SelectedSource}. */
|
|
718
|
+
get selectedSource() {
|
|
719
|
+
return this.SelectedSource;
|
|
720
|
+
}
|
|
721
|
+
/** @deprecated Use {@link SelectedSource}. */
|
|
722
|
+
set selectedSource(value) {
|
|
723
|
+
this.SelectedSource = value;
|
|
724
|
+
}
|
|
661
725
|
/** Map of Key → selected, used by the conditional chip system. */
|
|
662
726
|
ChipSelections = {};
|
|
663
727
|
/** Whether the overflow popover is open. */
|
|
@@ -732,7 +796,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
732
796
|
this.IsVisible = false;
|
|
733
797
|
this.cdr.markForCheck();
|
|
734
798
|
// Allow the slide-out animation to complete before emitting
|
|
735
|
-
setTimeout(() => this.
|
|
799
|
+
setTimeout(() => this.DialogClosed.emit(), 300);
|
|
736
800
|
}
|
|
737
801
|
async LoadRecordChanges(pkey, appName, entityName) {
|
|
738
802
|
if (pkey && entityName) {
|
|
@@ -751,7 +815,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
751
815
|
}
|
|
752
816
|
this.ngZone.run(() => {
|
|
753
817
|
if (changes) {
|
|
754
|
-
this.
|
|
818
|
+
this.ViewData = changes.sort((a, b) => new Date(b.ChangedAt).getTime() - new Date(a.ChangedAt).getTime());
|
|
755
819
|
this.rebuildViewDataIndex();
|
|
756
820
|
this.rebuildConditionalPills();
|
|
757
821
|
this.applyFilters();
|
|
@@ -766,9 +830,13 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
766
830
|
}
|
|
767
831
|
}
|
|
768
832
|
// ─── Filter & Search ────────────────────────────────────────────
|
|
769
|
-
|
|
833
|
+
OnSearchChange() {
|
|
770
834
|
this.applyFilters();
|
|
771
835
|
}
|
|
836
|
+
/** @deprecated Use {@link OnSearchChange}. */
|
|
837
|
+
onSearchChange() {
|
|
838
|
+
return this.OnSearchChange();
|
|
839
|
+
}
|
|
772
840
|
onFilterChange() {
|
|
773
841
|
this.applyFilters();
|
|
774
842
|
}
|
|
@@ -819,7 +887,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
819
887
|
this.cdr.markForCheck();
|
|
820
888
|
}
|
|
821
889
|
ClearFilters() {
|
|
822
|
-
this.
|
|
890
|
+
this.SearchTerm = '';
|
|
823
891
|
for (const k of Object.keys(this.ChipSelections))
|
|
824
892
|
this.ChipSelections[k] = false;
|
|
825
893
|
this.applyFilters();
|
|
@@ -831,7 +899,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
831
899
|
rebuildConditionalPills() {
|
|
832
900
|
const counts = {};
|
|
833
901
|
let restoreCount = 0;
|
|
834
|
-
for (const c of this.
|
|
902
|
+
for (const c of this.ViewData) {
|
|
835
903
|
const type = c.Type ?? 'Update';
|
|
836
904
|
counts[type] = (counts[type] ?? 0) + 1;
|
|
837
905
|
if (this.isRestoreChange(c))
|
|
@@ -869,9 +937,9 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
869
937
|
return c.Source === 'Restore' || c.RestoredFromID != null;
|
|
870
938
|
}
|
|
871
939
|
applyFilters() {
|
|
872
|
-
let filtered = [...this.
|
|
873
|
-
if (this.
|
|
874
|
-
const search = this.
|
|
940
|
+
let filtered = [...this.ViewData];
|
|
941
|
+
if (this.SearchTerm.trim()) {
|
|
942
|
+
const search = this.SearchTerm.toLowerCase();
|
|
875
943
|
filtered = filtered.filter(change => change.ChangesDescription?.toLowerCase().includes(search) ||
|
|
876
944
|
change.User?.toLowerCase().includes(search) ||
|
|
877
945
|
change.Comments?.toLowerCase().includes(search));
|
|
@@ -893,8 +961,8 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
893
961
|
return false;
|
|
894
962
|
});
|
|
895
963
|
}
|
|
896
|
-
this.
|
|
897
|
-
this.
|
|
964
|
+
this.FilteredData = filtered;
|
|
965
|
+
this.DateGroups = this.buildDateGroups(this.FilteredData);
|
|
898
966
|
this.cdr.markForCheck();
|
|
899
967
|
}
|
|
900
968
|
// ─── Version Labels ─────────────────────────────────────────────
|
|
@@ -967,7 +1035,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
967
1035
|
this.ShowCreateWizard = false;
|
|
968
1036
|
this.cdr.markForCheck();
|
|
969
1037
|
}
|
|
970
|
-
|
|
1038
|
+
GetLabelStatusClass(status) {
|
|
971
1039
|
switch (status) {
|
|
972
1040
|
case 'Active': return 'label-status-active';
|
|
973
1041
|
case 'Archived': return 'label-status-archived';
|
|
@@ -975,22 +1043,34 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
975
1043
|
default: return '';
|
|
976
1044
|
}
|
|
977
1045
|
}
|
|
1046
|
+
/** @deprecated Use {@link GetLabelStatusClass}. */
|
|
1047
|
+
getLabelStatusClass(status) {
|
|
1048
|
+
return this.GetLabelStatusClass(status);
|
|
1049
|
+
}
|
|
978
1050
|
// ─── Timeline Interaction ───────────────────────────────────────
|
|
979
|
-
|
|
980
|
-
if (this.
|
|
981
|
-
this.
|
|
1051
|
+
ToggleExpansion(changeId) {
|
|
1052
|
+
if (this.ExpandedItems.has(changeId)) {
|
|
1053
|
+
this.ExpandedItems.delete(changeId);
|
|
982
1054
|
}
|
|
983
1055
|
else {
|
|
984
|
-
this.
|
|
1056
|
+
this.ExpandedItems.add(changeId);
|
|
985
1057
|
}
|
|
986
1058
|
this.cdr.markForCheck();
|
|
987
1059
|
}
|
|
988
|
-
|
|
1060
|
+
/** @deprecated Use {@link ToggleExpansion}. */
|
|
1061
|
+
toggleExpansion(changeId) {
|
|
1062
|
+
return this.ToggleExpansion(changeId);
|
|
1063
|
+
}
|
|
1064
|
+
OnTimelineItemKeydown(event, changeId) {
|
|
989
1065
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
990
1066
|
event.preventDefault();
|
|
991
|
-
this.
|
|
1067
|
+
this.ToggleExpansion(changeId);
|
|
992
1068
|
}
|
|
993
1069
|
}
|
|
1070
|
+
/** @deprecated Use {@link OnTimelineItemKeydown}. */
|
|
1071
|
+
onTimelineItemKeydown(event, changeId) {
|
|
1072
|
+
return this.OnTimelineItemKeydown(event, changeId);
|
|
1073
|
+
}
|
|
994
1074
|
/**
|
|
995
1075
|
* Opens the embedded restore preview panel for a given change record.
|
|
996
1076
|
* The panel computes a full-record diff (current vs the change's
|
|
@@ -1044,19 +1124,23 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1044
1124
|
* currently loaded changes. Returns null when the source isn't loaded
|
|
1045
1125
|
* (e.g., it's been pruned from history) or when this row isn't a restore.
|
|
1046
1126
|
*/
|
|
1047
|
-
|
|
1127
|
+
GetRestoredFromSourceChange(change) {
|
|
1048
1128
|
const sourceId = change.RestoredFromID;
|
|
1049
1129
|
if (!sourceId)
|
|
1050
1130
|
return null;
|
|
1051
1131
|
return this.viewDataById.get(NormalizeUUID(sourceId)) ?? null;
|
|
1052
1132
|
}
|
|
1133
|
+
/** @deprecated Use {@link GetRestoredFromSourceChange}. */
|
|
1134
|
+
getRestoredFromSourceChange(change) {
|
|
1135
|
+
return this.GetRestoredFromSourceChange(change);
|
|
1136
|
+
}
|
|
1053
1137
|
/**
|
|
1054
1138
|
* Rebuilds {@link viewDataById} from the current {@link viewData}. Called once
|
|
1055
1139
|
* per data load so source-change resolution is an O(1) Map read.
|
|
1056
1140
|
*/
|
|
1057
1141
|
rebuildViewDataIndex() {
|
|
1058
1142
|
this.viewDataById.clear();
|
|
1059
|
-
for (const c of this.
|
|
1143
|
+
for (const c of this.ViewData) {
|
|
1060
1144
|
this.viewDataById.set(NormalizeUUID(c.ID), c);
|
|
1061
1145
|
}
|
|
1062
1146
|
}
|
|
@@ -1064,17 +1148,25 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1064
1148
|
* True when the row was produced by a restore operation (either the
|
|
1065
1149
|
* `Source` is `'Restore'` OR `RestoredFromID` is populated).
|
|
1066
1150
|
*/
|
|
1067
|
-
|
|
1151
|
+
IsRestoreRow(change) {
|
|
1068
1152
|
return this.isRestoreChange(change);
|
|
1069
1153
|
}
|
|
1154
|
+
/** @deprecated Use {@link IsRestoreRow}. */
|
|
1155
|
+
isRestoreRow(change) {
|
|
1156
|
+
return this.IsRestoreRow(change);
|
|
1157
|
+
}
|
|
1070
1158
|
/**
|
|
1071
1159
|
* True when this change is the most recent in the loaded history.
|
|
1072
1160
|
* `viewData` is sorted DESC by `ChangedAt`, so the most recent is index 0.
|
|
1073
1161
|
* Restoring to the most recent version is a no-op, so the timeline hides
|
|
1074
1162
|
* the Restore button on this row.
|
|
1075
1163
|
*/
|
|
1164
|
+
IsMostRecentChange(change) {
|
|
1165
|
+
return this.ViewData.length > 0 && this.ViewData[0] === change;
|
|
1166
|
+
}
|
|
1167
|
+
/** @deprecated Use {@link IsMostRecentChange}. */
|
|
1076
1168
|
isMostRecentChange(change) {
|
|
1077
|
-
return this.
|
|
1169
|
+
return this.IsMostRecentChange(change);
|
|
1078
1170
|
}
|
|
1079
1171
|
/**
|
|
1080
1172
|
* Click handler for the lineage chip — scrolls/highlights the source
|
|
@@ -1083,7 +1175,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1083
1175
|
JumpToSourceChange(sourceChange, event) {
|
|
1084
1176
|
event.stopPropagation();
|
|
1085
1177
|
this.HighlightedChangeID = sourceChange.ID;
|
|
1086
|
-
this.
|
|
1178
|
+
this.ExpandedItems.add(sourceChange.ID);
|
|
1087
1179
|
this.cdr.markForCheck();
|
|
1088
1180
|
// Auto-clear the highlight after 3s
|
|
1089
1181
|
setTimeout(() => {
|
|
@@ -1123,7 +1215,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1123
1215
|
return formatted;
|
|
1124
1216
|
}
|
|
1125
1217
|
// ─── Display Helpers ────────────────────────────────────────────
|
|
1126
|
-
|
|
1218
|
+
GetChangeTypeCardClass(type) {
|
|
1127
1219
|
switch (type) {
|
|
1128
1220
|
case 'Create': return 'type-create';
|
|
1129
1221
|
case 'Update': return 'type-update';
|
|
@@ -1132,7 +1224,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1132
1224
|
default: return 'type-update';
|
|
1133
1225
|
}
|
|
1134
1226
|
}
|
|
1135
|
-
|
|
1227
|
+
/** @deprecated Use {@link GetChangeTypeCardClass}. */
|
|
1228
|
+
getChangeTypeCardClass(type) {
|
|
1229
|
+
return this.GetChangeTypeCardClass(type);
|
|
1230
|
+
}
|
|
1231
|
+
GetChangeTypeBadgeText(type) {
|
|
1136
1232
|
switch (type) {
|
|
1137
1233
|
case 'Create': return 'Created';
|
|
1138
1234
|
case 'Delete': return 'Deleted';
|
|
@@ -1140,36 +1236,52 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1140
1236
|
default: return type;
|
|
1141
1237
|
}
|
|
1142
1238
|
}
|
|
1239
|
+
/** @deprecated Use {@link GetChangeTypeBadgeText}. */
|
|
1240
|
+
getChangeTypeBadgeText(type) {
|
|
1241
|
+
return this.GetChangeTypeBadgeText(type);
|
|
1242
|
+
}
|
|
1143
1243
|
/**
|
|
1144
1244
|
* Badge text for the row's primary type tag. For restore rows we override
|
|
1145
1245
|
* the underlying `Type='Update'` and show "Restore" instead — matches the
|
|
1146
1246
|
* mockup's intent of treating restore as a first-class operation in the
|
|
1147
1247
|
* timeline rather than a flavor of update.
|
|
1148
1248
|
*/
|
|
1149
|
-
|
|
1150
|
-
if (this.
|
|
1249
|
+
GetEffectiveBadgeText(change) {
|
|
1250
|
+
if (this.IsRestoreRow(change))
|
|
1151
1251
|
return 'Restore';
|
|
1152
|
-
return this.
|
|
1252
|
+
return this.GetChangeTypeBadgeText(change.Type);
|
|
1253
|
+
}
|
|
1254
|
+
/** @deprecated Use {@link GetEffectiveBadgeText}. */
|
|
1255
|
+
getEffectiveBadgeText(change) {
|
|
1256
|
+
return this.GetEffectiveBadgeText(change);
|
|
1153
1257
|
}
|
|
1154
1258
|
/**
|
|
1155
1259
|
* Optional restore reason — pulled from the dynamic `RestoreReason`
|
|
1156
1260
|
* column added by the lineage migration. Returns null when not present
|
|
1157
1261
|
* or not a restore row.
|
|
1158
1262
|
*/
|
|
1159
|
-
|
|
1160
|
-
if (!this.
|
|
1263
|
+
GetRestoreReason(change) {
|
|
1264
|
+
if (!this.IsRestoreRow(change))
|
|
1161
1265
|
return null;
|
|
1162
1266
|
const reason = change.RestoreReason;
|
|
1163
1267
|
return reason && reason.trim().length > 0 ? reason : null;
|
|
1164
1268
|
}
|
|
1165
|
-
|
|
1269
|
+
/** @deprecated Use {@link GetRestoreReason}. */
|
|
1270
|
+
getRestoreReason(change) {
|
|
1271
|
+
return this.GetRestoreReason(change);
|
|
1272
|
+
}
|
|
1273
|
+
GetSourceClass(source) {
|
|
1166
1274
|
if (source === 'Restore')
|
|
1167
1275
|
return 'source-restore';
|
|
1168
1276
|
if (source === 'Internal')
|
|
1169
1277
|
return 'source-internal';
|
|
1170
1278
|
return 'source-external';
|
|
1171
1279
|
}
|
|
1172
|
-
|
|
1280
|
+
/** @deprecated Use {@link GetSourceClass}. */
|
|
1281
|
+
getSourceClass(source) {
|
|
1282
|
+
return this.GetSourceClass(source);
|
|
1283
|
+
}
|
|
1284
|
+
GetStatusClass(status) {
|
|
1173
1285
|
switch (status) {
|
|
1174
1286
|
case 'Complete': return 'status-complete';
|
|
1175
1287
|
case 'Pending': return 'status-pending';
|
|
@@ -1177,10 +1289,18 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1177
1289
|
default: return 'status-unknown';
|
|
1178
1290
|
}
|
|
1179
1291
|
}
|
|
1292
|
+
/** @deprecated Use {@link GetStatusClass}. */
|
|
1293
|
+
getStatusClass(status) {
|
|
1294
|
+
return this.GetStatusClass(status);
|
|
1295
|
+
}
|
|
1296
|
+
GetTimelineItemLabel(change) {
|
|
1297
|
+
return `${change.Type} by ${change.User || 'Unknown User'} on ${this.FormatFullDateTime(change.ChangedAt)}`;
|
|
1298
|
+
}
|
|
1299
|
+
/** @deprecated Use {@link GetTimelineItemLabel}. */
|
|
1180
1300
|
getTimelineItemLabel(change) {
|
|
1181
|
-
return
|
|
1301
|
+
return this.GetTimelineItemLabel(change);
|
|
1182
1302
|
}
|
|
1183
|
-
|
|
1303
|
+
GetUserInitials(user) {
|
|
1184
1304
|
if (!user)
|
|
1185
1305
|
return '?';
|
|
1186
1306
|
if (user.includes('@')) {
|
|
@@ -1193,25 +1313,41 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1193
1313
|
}
|
|
1194
1314
|
return user.substring(0, 2).toUpperCase();
|
|
1195
1315
|
}
|
|
1196
|
-
|
|
1316
|
+
/** @deprecated Use {@link GetUserInitials}. */
|
|
1317
|
+
getUserInitials(user) {
|
|
1318
|
+
return this.GetUserInitials(user);
|
|
1319
|
+
}
|
|
1320
|
+
GetUserDisplayName(user) {
|
|
1197
1321
|
if (!user)
|
|
1198
1322
|
return 'Unknown';
|
|
1199
1323
|
if (user.includes('@'))
|
|
1200
1324
|
return user.split('@')[0];
|
|
1201
1325
|
return user;
|
|
1202
1326
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1327
|
+
/** @deprecated Use {@link GetUserDisplayName}. */
|
|
1328
|
+
getUserDisplayName(user) {
|
|
1329
|
+
return this.GetUserDisplayName(user);
|
|
1330
|
+
}
|
|
1331
|
+
GetUniqueContributorCount() {
|
|
1332
|
+
const users = new Set(this.ViewData.map(c => c.User).filter(Boolean));
|
|
1205
1333
|
return users.size;
|
|
1206
1334
|
}
|
|
1207
|
-
|
|
1335
|
+
/** @deprecated Use {@link GetUniqueContributorCount}. */
|
|
1336
|
+
getUniqueContributorCount() {
|
|
1337
|
+
return this.GetUniqueContributorCount();
|
|
1338
|
+
}
|
|
1339
|
+
FormatTime(date) {
|
|
1208
1340
|
return new Intl.DateTimeFormat('en-US', {
|
|
1209
1341
|
hour: 'numeric',
|
|
1210
1342
|
minute: '2-digit',
|
|
1211
1343
|
hour12: true,
|
|
1212
1344
|
}).format(new Date(date));
|
|
1213
1345
|
}
|
|
1214
|
-
|
|
1346
|
+
/** @deprecated Use {@link FormatTime}. */
|
|
1347
|
+
formatTime(date) {
|
|
1348
|
+
return this.FormatTime(date);
|
|
1349
|
+
}
|
|
1350
|
+
FormatRelativeTime(date) {
|
|
1215
1351
|
const now = new Date();
|
|
1216
1352
|
const diffMs = now.getTime() - new Date(date).getTime();
|
|
1217
1353
|
const diffMins = Math.floor(diffMs / 60000);
|
|
@@ -1231,7 +1367,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1231
1367
|
year: diffDays > 365 ? 'numeric' : undefined,
|
|
1232
1368
|
}).format(new Date(date));
|
|
1233
1369
|
}
|
|
1234
|
-
|
|
1370
|
+
/** @deprecated Use {@link FormatRelativeTime}. */
|
|
1371
|
+
formatRelativeTime(date) {
|
|
1372
|
+
return this.FormatRelativeTime(date);
|
|
1373
|
+
}
|
|
1374
|
+
FormatFullDateTime(date) {
|
|
1235
1375
|
return new Intl.DateTimeFormat('en-US', {
|
|
1236
1376
|
year: 'numeric',
|
|
1237
1377
|
month: 'long',
|
|
@@ -1242,16 +1382,20 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1242
1382
|
timeZoneName: 'short',
|
|
1243
1383
|
}).format(new Date(date));
|
|
1244
1384
|
}
|
|
1385
|
+
/** @deprecated Use {@link FormatFullDateTime}. */
|
|
1386
|
+
formatFullDateTime(date) {
|
|
1387
|
+
return this.FormatFullDateTime(date);
|
|
1388
|
+
}
|
|
1245
1389
|
// ─── Change Summary ─────────────────────────────────────────────
|
|
1246
|
-
|
|
1390
|
+
GetChangeSummary(change) {
|
|
1247
1391
|
// Restore rows get a distinctive summary so the timeline reads like a
|
|
1248
1392
|
// sentence — "Restored to 5:56 PM version" rather than yet another
|
|
1249
1393
|
// "Name and Description changed". The lineage chip carries the full
|
|
1250
1394
|
// source-version detail.
|
|
1251
|
-
if (this.
|
|
1252
|
-
const src = this.
|
|
1395
|
+
if (this.IsRestoreRow(change)) {
|
|
1396
|
+
const src = this.GetRestoredFromSourceChange(change);
|
|
1253
1397
|
if (src)
|
|
1254
|
-
return `Restored to ${this.
|
|
1398
|
+
return `Restored to ${this.FormatTime(src.ChangedAt)} version`;
|
|
1255
1399
|
return 'Restored from earlier version';
|
|
1256
1400
|
}
|
|
1257
1401
|
if (change.Type === 'Create')
|
|
@@ -1271,7 +1415,11 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1271
1415
|
return change.ChangesDescription || 'Changes made';
|
|
1272
1416
|
}
|
|
1273
1417
|
}
|
|
1274
|
-
|
|
1418
|
+
/** @deprecated Use {@link GetChangeSummary}. */
|
|
1419
|
+
getChangeSummary(change) {
|
|
1420
|
+
return this.GetChangeSummary(change);
|
|
1421
|
+
}
|
|
1422
|
+
GetCreatedFieldCount(change) {
|
|
1275
1423
|
try {
|
|
1276
1424
|
if (!change.FullRecordJSON)
|
|
1277
1425
|
return 0;
|
|
@@ -1282,6 +1430,10 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1282
1430
|
return 0;
|
|
1283
1431
|
}
|
|
1284
1432
|
}
|
|
1433
|
+
/** @deprecated Use {@link GetCreatedFieldCount}. */
|
|
1434
|
+
getCreatedFieldCount(change) {
|
|
1435
|
+
return this.GetCreatedFieldCount(change);
|
|
1436
|
+
}
|
|
1285
1437
|
extractFieldDisplayNames(changesJson) {
|
|
1286
1438
|
return Object.keys(changesJson).map(fieldKey => {
|
|
1287
1439
|
const changeInfo = changesJson[fieldKey];
|
|
@@ -1302,7 +1454,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1302
1454
|
return `${fieldNames.slice(0, 3).join(', ')}, and ${remaining} other field${remaining > 1 ? 's' : ''} changed`;
|
|
1303
1455
|
}
|
|
1304
1456
|
// ─── Field Changes (type-aware) ────────────────────────────────
|
|
1305
|
-
|
|
1457
|
+
GetFieldChanges(change) {
|
|
1306
1458
|
try {
|
|
1307
1459
|
const changesJson = JSON.parse(change.ChangesJSON || '{}');
|
|
1308
1460
|
return Object.keys(changesJson).map(fieldKey => this.buildFieldChangeInfo(changesJson[fieldKey]));
|
|
@@ -1311,6 +1463,10 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1311
1463
|
return [];
|
|
1312
1464
|
}
|
|
1313
1465
|
}
|
|
1466
|
+
/** @deprecated Use {@link GetFieldChanges}. */
|
|
1467
|
+
getFieldChanges(change) {
|
|
1468
|
+
return this.GetFieldChanges(change);
|
|
1469
|
+
}
|
|
1314
1470
|
buildFieldChangeInfo(changeInfo) {
|
|
1315
1471
|
const field = this.record.EntityInfo.Fields.find((f) => f.Name.trim().toLowerCase() === changeInfo.field?.trim().toLowerCase());
|
|
1316
1472
|
const fieldType = this.classifyFieldType(field);
|
|
@@ -1341,7 +1497,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1341
1497
|
return 'number';
|
|
1342
1498
|
return 'text';
|
|
1343
1499
|
}
|
|
1344
|
-
|
|
1500
|
+
GetCreatedFields(change) {
|
|
1345
1501
|
try {
|
|
1346
1502
|
if (!change.FullRecordJSON)
|
|
1347
1503
|
return [];
|
|
@@ -1359,6 +1515,10 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1359
1515
|
return [];
|
|
1360
1516
|
}
|
|
1361
1517
|
}
|
|
1518
|
+
/** @deprecated Use {@link GetCreatedFields}. */
|
|
1519
|
+
getCreatedFields(change) {
|
|
1520
|
+
return this.GetCreatedFields(change);
|
|
1521
|
+
}
|
|
1362
1522
|
// ─── Value Formatting ───────────────────────────────────────────
|
|
1363
1523
|
formatChangeValue(value, isDateField) {
|
|
1364
1524
|
if (value == null)
|
|
@@ -1420,7 +1580,7 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1420
1580
|
return div.innerHTML;
|
|
1421
1581
|
}
|
|
1422
1582
|
static ɵfac = function RecordChangesComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RecordChangesComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone), i0.ɵɵdirectiveInject(i1.MJNotificationService), i0.ɵɵdirectiveInject(i2.DomSanitizer)); };
|
|
1423
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecordChangesComponent, selectors: [["mj-record-changes"]], inputs: { record: "record", AllowRestore: "AllowRestore" }, outputs: { dialogClosed: "dialogClosed", RestoreRequested: "RestoreRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 5, vars: 13, consts: [[3, "Closed", "Mode", "Title", "Visible", "Resizable", "MinWidthPx", "MaxWidthRatio"], ["text", "Loading history...", "size", "medium"], [1, "rc-container"], [3, "RestoreConfirmed", "RestoreCancelled", "Visible", "Mode", "RecordChange", "LiveRecord"], [1, "rc-wizard-overlay"], [1, "rc-labels-section"], [1, "rc-labels-header"], [1, "rc-labels-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-tags"], [1, "rc-labels-count"], ["title", "Create a version label for this record", 1, "rc-create-label-btn", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "rc-labels-loading"], ["Size", "compact", "Icon", "", "Title", "No version labels for this record yet.", 1, "rc-labels-empty"], [1, "rc-labels-list"], ["Icon", "fa-solid fa-clock-rotate-left", "Title", "No Change History", "Message", "This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.", "Size", "large"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "rc-label-chip", 3, "title"], [1, "fa-solid", "fa-tag", "rc-label-chip-icon"], [1, "rc-label-chip-name"], [1, "rc-label-chip-meta"], [1, "rc-label-chip-status"], [1, "rc-label-chip-items"], [1, "rc-empty-state-hint"], [1, "fa-solid", "fa-shield-halved"], [1, "rc-page-header"], [1, "rc-page-title"], [1, "rc-page-subtitle"], [1, "rc-header-meta"], [1, "rc-entity-badge"], ["aria-hidden", "true", 1, "fa-solid", "fa-database"], [1, "rc-change-count"], [1, "rc-filter-bar"], [1, "rc-search-wrap"], ["aria-hidden", "true", 1, "fa-solid", "fa-search"], ["type", "text", "placeholder", "Search changes...", "aria-label", "Search record changes", 1, "rc-search-box", 3, "ngModelChange", "input", "ngModel"], ["type", "button", 1, "rc-filter-pill", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-layer-group"], [1, "rc-filter-results"], [1, "rc-timeline"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left"], [1, "rc-filter-overflow-wrap"], ["type", "button", 1, "rc-filter-pill", 3, "active", "rc-filter-pill-restore"], ["aria-hidden", "true"], [1, "rc-filter-pill-count"], ["type", "button", 1, "rc-filter-pill", "rc-filter-pill-overflow", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-sliders"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-filter-overflow-caret"], [1, "rc-filter-overflow-backdrop", 3, "click"], ["role", "menu", 1, "rc-filter-overflow-popover"], [1, "rc-filter-overflow-header"], [1, "rc-filter-overflow-row", 3, "rc-filter-overflow-row-restore"], [1, "rc-filter-overflow-actions"], ["type", "button", 1, "rc-filter-overflow-clear", 3, "click"], ["type", "button", 1, "rc-filter-overflow-done", 3, "click"], [1, "rc-filter-overflow-row"], ["type", "checkbox", 3, "change", "checked"], [1, "rc-filter-overflow-label"], [1, "rc-filter-overflow-count"], [1, "rc-clear-filters-link", 3, "click"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left", 3, "Action"], [1, "rc-date-group"], [1, "rc-date-label"], [1, "rc-card", 3, "class", "expanded", "rc-card-restore", "rc-card-highlight"], [1, "rc-card", 3, "keydown"], [1, "rc-card-header", 3, "click"], [1, "rc-card-header-left"], [1, "rc-card-type-badge"], [1, "rc-card-summary"], [1, "rc-card-meta"], [1, "rc-card-user"], [1, "rc-avatar"], [1, "rc-card-time", 3, "title"], [1, "rc-card-source"], [1, "rc-card-status"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-card-chevron"], [1, "rc-card-body"], [1, "rc-restored-chip", 3, "title"], ["title", "Source version was not loaded into this view", 1, "rc-restored-chip", "rc-restored-chip-orphan"], [1, "rc-restored-chip", 3, "click", "title"], ["aria-hidden", "true", 1, "fa-solid", "fa-link"], ["aria-hidden", "true", 1, "fa-solid", "fa-clock-rotate-left"], [1, "rc-restore-reason"], [1, "rc-deletion-note"], [1, "rc-comments"], [1, "rc-errors"], [1, "rc-restore-action"], [1, "rc-restore-reason-label"], [1, "rc-restore-reason-text"], [1, "rc-field-row"], [1, "rc-field-label"], [1, "rc-field-values"], [1, "rc-val-new"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], [1, "rc-bool-change"], [1, "rc-atomic-change"], [1, "rc-text-diff", 3, "innerHTML"], [1, "rc-bool-dot"], [1, "rc-bool-label", "old"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-right", "rc-val-arrow"], [1, "rc-bool-label", "new"], [1, "rc-val-old"], ["aria-hidden", "true", 1, "fa-solid", "fa-comment"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle"], [1, "rc-error-log"], ["title", "Restore the entire record to its state at this version", 1, "rc-restore-btn", 3, "click"], [1, "rc-wizard-overlay", 3, "click"], [1, "rc-wizard-container", 3, "click"], [3, "Created", "Cancel", "PreselectedEntity", "PreselectedRecordIds"]], template: function RecordChangesComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1583
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RecordChangesComponent, selectors: [["mj-record-changes"]], inputs: { record: "record", AllowRestore: "AllowRestore" }, outputs: { DialogClosed: "DialogClosed", dialogClosed: "dialogClosed", RestoreRequested: "RestoreRequested" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 5, vars: 13, consts: [[3, "Closed", "Mode", "Title", "Visible", "Resizable", "MinWidthPx", "MaxWidthRatio"], ["text", "Loading history...", "size", "medium"], [1, "rc-container"], [3, "RestoreConfirmed", "RestoreCancelled", "Visible", "Mode", "RecordChange", "LiveRecord"], [1, "rc-wizard-overlay"], [1, "rc-labels-section"], [1, "rc-labels-header"], [1, "rc-labels-title"], ["aria-hidden", "true", 1, "fa-solid", "fa-tags"], [1, "rc-labels-count"], ["title", "Create a version label for this record", 1, "rc-create-label-btn", 3, "click"], [1, "fa-solid", "fa-plus"], [1, "rc-labels-loading"], ["Size", "compact", "Icon", "", "Title", "No version labels for this record yet.", 1, "rc-labels-empty"], [1, "rc-labels-list"], ["Icon", "fa-solid fa-clock-rotate-left", "Title", "No Change History", "Message", "This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.", "Size", "large"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "rc-label-chip", 3, "title"], [1, "fa-solid", "fa-tag", "rc-label-chip-icon"], [1, "rc-label-chip-name"], [1, "rc-label-chip-meta"], [1, "rc-label-chip-status"], [1, "rc-label-chip-items"], [1, "rc-empty-state-hint"], [1, "fa-solid", "fa-shield-halved"], [1, "rc-page-header"], [1, "rc-page-title"], [1, "rc-page-subtitle"], [1, "rc-header-meta"], [1, "rc-entity-badge"], ["aria-hidden", "true", 1, "fa-solid", "fa-database"], [1, "rc-change-count"], [1, "rc-filter-bar"], [1, "rc-search-wrap"], ["aria-hidden", "true", 1, "fa-solid", "fa-search"], ["type", "text", "placeholder", "Search changes...", "aria-label", "Search record changes", 1, "rc-search-box", 3, "ngModelChange", "input", "ngModel"], ["type", "button", 1, "rc-filter-pill", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-layer-group"], [1, "rc-filter-results"], [1, "rc-timeline"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left"], [1, "rc-filter-overflow-wrap"], ["type", "button", 1, "rc-filter-pill", 3, "active", "rc-filter-pill-restore"], ["aria-hidden", "true"], [1, "rc-filter-pill-count"], ["type", "button", 1, "rc-filter-pill", "rc-filter-pill-overflow", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-sliders"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-filter-overflow-caret"], [1, "rc-filter-overflow-backdrop", 3, "click"], ["role", "menu", 1, "rc-filter-overflow-popover"], [1, "rc-filter-overflow-header"], [1, "rc-filter-overflow-row", 3, "rc-filter-overflow-row-restore"], [1, "rc-filter-overflow-actions"], ["type", "button", 1, "rc-filter-overflow-clear", 3, "click"], ["type", "button", 1, "rc-filter-overflow-done", 3, "click"], [1, "rc-filter-overflow-row"], ["type", "checkbox", 3, "change", "checked"], [1, "rc-filter-overflow-label"], [1, "rc-filter-overflow-count"], [1, "rc-clear-filters-link", 3, "click"], ["Variant", "no-results", "Icon", "fa-solid fa-filter", "Title", "No changes match your current filters", "ActionText", "Clear filters", "ActionIcon", "fa-solid fa-rotate-left", 3, "Action"], [1, "rc-date-group"], [1, "rc-date-label"], [1, "rc-card", 3, "class", "expanded", "rc-card-restore", "rc-card-highlight"], [1, "rc-card", 3, "keydown"], [1, "rc-card-header", 3, "click"], [1, "rc-card-header-left"], [1, "rc-card-type-badge"], [1, "rc-card-summary"], [1, "rc-card-meta"], [1, "rc-card-user"], [1, "rc-avatar"], [1, "rc-card-time", 3, "title"], [1, "rc-card-source"], [1, "rc-card-status"], ["aria-hidden", "true", 1, "fa-solid", "fa-chevron-down", "rc-card-chevron"], [1, "rc-card-body"], [1, "rc-restored-chip", 3, "title"], ["title", "Source version was not loaded into this view", 1, "rc-restored-chip", "rc-restored-chip-orphan"], [1, "rc-restored-chip", 3, "click", "title"], ["aria-hidden", "true", 1, "fa-solid", "fa-link"], ["aria-hidden", "true", 1, "fa-solid", "fa-clock-rotate-left"], [1, "rc-restore-reason"], [1, "rc-deletion-note"], [1, "rc-comments"], [1, "rc-errors"], [1, "rc-restore-action"], [1, "rc-restore-reason-label"], [1, "rc-restore-reason-text"], [1, "rc-field-row"], [1, "rc-field-label"], [1, "rc-field-values"], [1, "rc-val-new"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"], [1, "rc-bool-change"], [1, "rc-atomic-change"], [1, "rc-text-diff", 3, "innerHTML"], [1, "rc-bool-dot"], [1, "rc-bool-label", "old"], ["aria-hidden", "true", 1, "fa-solid", "fa-arrow-right", "rc-val-arrow"], [1, "rc-bool-label", "new"], [1, "rc-val-old"], ["aria-hidden", "true", 1, "fa-solid", "fa-comment"], ["aria-hidden", "true", 1, "fa-solid", "fa-exclamation-triangle"], [1, "rc-error-log"], ["title", "Restore the entire record to its state at this version", 1, "rc-restore-btn", 3, "click"], [1, "rc-wizard-overlay", 3, "click"], [1, "rc-wizard-container", 3, "click"], [3, "Created", "Cancel", "PreselectedEntity", "PreselectedRecordIds"]], template: function RecordChangesComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1424
1584
|
i0.ɵɵelementStart(0, "mj-slide-panel", 0);
|
|
1425
1585
|
i0.ɵɵlistener("Closed", function RecordChangesComponent_Template_mj_slide_panel_Closed_0_listener() { return ctx.OnClose(); });
|
|
1426
1586
|
i0.ɵɵconditionalCreate(1, RecordChangesComponent_Conditional_1_Template, 1, 0, "mj-loading", 1);
|
|
@@ -1445,7 +1605,9 @@ export class RecordChangesComponent extends BaseAngularComponent {
|
|
|
1445
1605
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RecordChangesComponent, [{
|
|
1446
1606
|
type: Component,
|
|
1447
1607
|
args: [{ standalone: false, selector: 'mj-record-changes', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<mj-slide-panel\n [Mode]=\"'slide'\"\n [Title]=\"'Record Changes History'\"\n [Visible]=\"IsVisible\"\n [Resizable]=\"true\"\n [MinWidthPx]=\"400\"\n [MaxWidthRatio]=\"0.6\"\n (Closed)=\"OnClose()\">\n\n @if (IsLoading) {\n <mj-loading text=\"Loading history...\" size=\"medium\"></mj-loading>\n }\n\n @if (!IsLoading) {\n <div class=\"rc-container\">\n <!-- Version Labels Section -->\n <div class=\"rc-labels-section\">\n <div class=\"rc-labels-header\">\n <div class=\"rc-labels-title\">\n <i class=\"fa-solid fa-tags\" aria-hidden=\"true\"></i>\n <span>Version Labels</span>\n @if (RecordLabels.length > 0) {\n <span class=\"rc-labels-count\">{{ RecordLabels.length }}</span>\n }\n </div>\n <button class=\"rc-create-label-btn\" (click)=\"OpenCreateWizard()\" title=\"Create a version label for this record\">\n <i class=\"fa-solid fa-plus\"></i> Create Label\n </button>\n </div>\n @if (IsLoadingLabels) {\n <div class=\"rc-labels-loading\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i> Loading labels...\n </div>\n } @else if (RecordLabels.length === 0) {\n <mj-empty-state class=\"rc-labels-empty\" Size=\"compact\"\n Icon=\"\" Title=\"No version labels for this record yet.\" />\n } @else {\n <div class=\"rc-labels-list\">\n @for (label of RecordLabels; track label.ID) {\n <div class=\"rc-label-chip\" [title]=\"label.Description || label.Name\">\n <i class=\"fa-solid fa-tag rc-label-chip-icon\"></i>\n <span class=\"rc-label-chip-name\">{{ label.Name }}</span>\n <span class=\"rc-label-chip-meta\">\n <span class=\"rc-label-chip-status\" [class]=\"getLabelStatusClass(label.Status)\">{{ label.Status }}</span>\n <span class=\"rc-label-chip-items\">{{ label.ItemCount }} item{{ label.ItemCount !== 1 ? 's' : '' }}</span>\n </span>\n </div>\n }\n </div>\n }\n </div>\n\n @if (viewData.length === 0) {\n <!-- Empty state -->\n <mj-empty-state\n Icon=\"fa-solid fa-clock-rotate-left\"\n Title=\"No Change History\"\n Message=\"This record doesn't have any tracked changes yet. Changes will appear here automatically as edits are made.\"\n Size=\"large\">\n <div class=\"rc-empty-state-hint\">\n <i class=\"fa-solid fa-shield-halved\"></i>\n <span>Record change tracking is managed at the entity level</span>\n </div>\n </mj-empty-state>\n } @else {\n <!-- Page Header -->\n <div class=\"rc-page-header\">\n <div>\n <h2 class=\"rc-page-title\">Change History</h2>\n <div class=\"rc-page-subtitle\">{{ record.EntityInfo.Name }}</div>\n </div>\n <div class=\"rc-header-meta\">\n <div class=\"rc-entity-badge\">\n <i class=\"fa-solid fa-database\" aria-hidden=\"true\"></i>\n {{ record.EntityInfo.Name }}\n </div>\n <div class=\"rc-change-count\">{{ viewData.length }} change{{ viewData.length !== 1 ? 's' : '' }} · {{ getUniqueContributorCount() }} contributor{{ getUniqueContributorCount() !== 1 ? 's' : '' }}</div>\n </div>\n </div>\n\n <!-- Filter Bar \u2014 chips render only for change types/sources actually present in loaded data.\n When more than 2 conditional chips would render, they collapse into a \"More filters\" popover. -->\n <div class=\"rc-filter-bar\">\n <div class=\"rc-search-wrap\">\n <i class=\"fa-solid fa-search\" aria-hidden=\"true\"></i>\n <input\n class=\"rc-search-box\"\n type=\"text\"\n placeholder=\"Search changes...\"\n [(ngModel)]=\"searchTerm\"\n (input)=\"onSearchChange()\"\n aria-label=\"Search record changes\"\n />\n </div>\n\n <button class=\"rc-filter-pill\" [class.active]=\"IsAllSelected\" (click)=\"SelectAllPill()\" type=\"button\">\n <i class=\"fa-solid fa-layer-group\" aria-hidden=\"true\"></i> All\n </button>\n\n @if (HasConditionalPills) {\n @if (!UseOverflowPopover) {\n <!-- 1-2 conditional pills: render inline -->\n @for (pill of ConditionalPills; track pill.Key) {\n <button class=\"rc-filter-pill\"\n [class.active]=\"ChipSelections[pill.Key]\"\n [class.rc-filter-pill-restore]=\"pill.Variant === 'restore'\"\n (click)=\"TogglePill(pill.Key)\"\n type=\"button\">\n <i class=\"fa-solid {{ pill.Icon }}\" aria-hidden=\"true\"></i>\n {{ pill.Label }}\n <span class=\"rc-filter-pill-count\">{{ pill.Count }}</span>\n </button>\n }\n } @else {\n <!-- 3+ conditional pills: overflow popover -->\n <div class=\"rc-filter-overflow-wrap\">\n <button class=\"rc-filter-pill rc-filter-pill-overflow\"\n [class.active]=\"SelectedConditionalCount > 0\"\n (click)=\"ToggleFilterOverflow()\"\n type=\"button\">\n <i class=\"fa-solid fa-sliders\" aria-hidden=\"true\"></i>\n More filters\n @if (SelectedConditionalCount > 0) {\n <span class=\"rc-filter-pill-count\">{{ SelectedConditionalCount }}</span>\n }\n <i class=\"fa-solid fa-chevron-down rc-filter-overflow-caret\" aria-hidden=\"true\"></i>\n </button>\n\n @if (ShowFilterOverflow) {\n <!-- Backdrop: clicks close the popover -->\n <div class=\"rc-filter-overflow-backdrop\" (click)=\"CloseFilterOverflow()\"></div>\n <div class=\"rc-filter-overflow-popover\" role=\"menu\">\n <div class=\"rc-filter-overflow-header\">Filter by</div>\n @for (pill of ConditionalPills; track pill.Key) {\n <label class=\"rc-filter-overflow-row\" [class.rc-filter-overflow-row-restore]=\"pill.Variant === 'restore'\">\n <input type=\"checkbox\"\n [checked]=\"ChipSelections[pill.Key]\"\n (change)=\"TogglePill(pill.Key)\" />\n <i class=\"fa-solid {{ pill.Icon }}\" aria-hidden=\"true\"></i>\n <span class=\"rc-filter-overflow-label\">{{ pill.Label }}</span>\n <span class=\"rc-filter-overflow-count\">{{ pill.Count }}</span>\n </label>\n }\n <div class=\"rc-filter-overflow-actions\">\n <button class=\"rc-filter-overflow-clear\" (click)=\"SelectAllPill(); CloseFilterOverflow()\" type=\"button\">\n Clear all\n </button>\n <button class=\"rc-filter-overflow-done\" (click)=\"CloseFilterOverflow()\" type=\"button\">\n Done\n </button>\n </div>\n </div>\n }\n </div>\n }\n }\n </div>\n\n @if (filteredData.length !== viewData.length) {\n <div class=\"rc-filter-results\">\n Showing {{ filteredData.length }} of {{ viewData.length }} changes\n <button class=\"rc-clear-filters-link\" (click)=\"ClearFilters()\">Clear filters</button>\n </div>\n }\n\n <!-- Timeline -->\n <div class=\"rc-timeline\" [attr.aria-label]=\"'Timeline of changes for ' + record.EntityInfo.Name + ' record'\">\n @if (filteredData.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-filter\"\n Title=\"No changes match your current filters\"\n ActionText=\"Clear filters\"\n ActionIcon=\"fa-solid fa-rotate-left\"\n (Action)=\"ClearFilters()\" />\n } @else {\n @for (group of dateGroups; track group.label) {\n <div class=\"rc-date-group\">\n <div class=\"rc-date-label\">{{ group.label }}</div>\n\n @for (change of group.changes; track change.ID) {\n <div\n class=\"rc-card\"\n [attr.data-change-id]=\"change.ID\"\n [class]=\"getChangeTypeCardClass(change.Type)\"\n [class.expanded]=\"expandedItems.has(change.ID)\"\n [class.rc-card-restore]=\"isRestoreRow(change)\"\n [class.rc-card-highlight]=\"HighlightedChangeID === change.ID\"\n [attr.tabindex]=\"0\"\n [attr.aria-expanded]=\"expandedItems.has(change.ID)\"\n [attr.aria-label]=\"getTimelineItemLabel(change)\"\n (keydown)=\"onTimelineItemKeydown($event, change.ID)\"\n >\n <!-- Card Header -->\n <div class=\"rc-card-header\" (click)=\"toggleExpansion(change.ID)\">\n <div class=\"rc-card-header-left\">\n <span class=\"rc-card-type-badge\"\n [class.rc-card-type-badge-restore]=\"isRestoreRow(change)\">{{ getEffectiveBadgeText(change) }}</span>\n <span class=\"rc-card-summary\">{{ getChangeSummary(change) }}</span>\n\n <!-- Lineage chip \u2014 visible when this row was produced by a restore -->\n @if (isRestoreRow(change)) {\n @if (getRestoredFromSourceChange(change); as srcChange) {\n <a class=\"rc-restored-chip\"\n (click)=\"JumpToSourceChange(srcChange, $event)\"\n [title]=\"'Jump to source version (' + formatFullDateTime(srcChange.ChangedAt) + ')'\">\n <i class=\"fa-solid fa-link\" aria-hidden=\"true\"></i>\n Restored from {{ formatTime(srcChange.ChangedAt) }} {{ srcChange.ChangedAt ? 'on ' + (srcChange.ChangedAt | date:'mediumDate') : '' }}\n @if (srcChange.User) {\n by {{ getUserDisplayName(srcChange.User) }}\n }\n </a>\n } @else {\n <span class=\"rc-restored-chip rc-restored-chip-orphan\"\n title=\"Source version was not loaded into this view\">\n <i class=\"fa-solid fa-clock-rotate-left\" aria-hidden=\"true\"></i>\n Restored from earlier version\n </span>\n }\n }\n </div>\n <div class=\"rc-card-meta\">\n <div class=\"rc-card-user\">\n <div class=\"rc-avatar\">{{ getUserInitials(change.User) }}</div>\n {{ getUserDisplayName(change.User) }}\n </div>\n <span class=\"rc-card-time\" [title]=\"formatFullDateTime(change.ChangedAt)\">{{ formatTime(change.ChangedAt) }}</span>\n <span class=\"rc-card-source\" [class]=\"getSourceClass(change.Source)\">{{ change.Source }}</span>\n <span class=\"rc-card-status\" [class]=\"getStatusClass(change.Status)\">{{ change.Status }}</span>\n <i class=\"fa-solid fa-chevron-down rc-card-chevron\" aria-hidden=\"true\"></i>\n </div>\n </div>\n\n <!-- Card Body (expanded) -->\n @if (expandedItems.has(change.ID)) {\n <div class=\"rc-card-body\">\n <!-- Restore reason (when present) \u2014 Notion/Linear-style\n blockquote: a thin violet left bar + italic text,\n with a tiny \"Reason\" prefix. No box, no header,\n no large vertical footprint. Reads as a\n justification note rather than a callout. -->\n @if (getRestoreReason(change); as reason) {\n <blockquote class=\"rc-restore-reason\">\n <span class=\"rc-restore-reason-label\">Reason</span>\n <span class=\"rc-restore-reason-text\">{{ reason }}</span>\n </blockquote>\n }\n\n @if (change.Type === 'Create') {\n @if (change.FullRecordJSON) {\n @for (field of getCreatedFields(change); track field.name) {\n <div class=\"rc-field-row\">\n <div class=\"rc-field-label\">{{ field.displayName }}</div>\n <div class=\"rc-field-values\">\n <span class=\"rc-val-new\">{{ field.value }}</span>\n </div>\n </div>\n }\n }\n } @else if (change.Type === 'Delete') {\n <div class=\"rc-deletion-note\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i>\n This record was permanently removed from the system.\n </div>\n } @else {\n <!-- Update / Snapshot / Restore: type-aware field changes -->\n @for (fc of getFieldChanges(change); track fc.field) {\n <div class=\"rc-field-row\">\n <div class=\"rc-field-label\">{{ fc.displayName }}</div>\n <div class=\"rc-field-values\">\n @if (fc.fieldType === 'boolean') {\n <div class=\"rc-bool-change\">\n <span class=\"rc-bool-dot\" [class.on]=\"fc.oldValue === 'true' || fc.oldValue === '1'\" [class.off]=\"fc.oldValue !== 'true' && fc.oldValue !== '1'\"></span>\n <span class=\"rc-bool-label old\">{{ fc.oldValue === 'true' || fc.oldValue === '1' ? 'Yes' : 'No' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-bool-dot\" [class.on]=\"fc.newValue === 'true' || fc.newValue === '1'\" [class.off]=\"fc.newValue !== 'true' && fc.newValue !== '1'\"></span>\n <span class=\"rc-bool-label new\" [class.active]=\"fc.newValue === 'true' || fc.newValue === '1'\" [class.inactive]=\"fc.newValue !== 'true' && fc.newValue !== '1'\">{{ fc.newValue === 'true' || fc.newValue === '1' ? 'Yes' : 'No' }}</span>\n </div>\n } @else if (fc.fieldType === 'date' || fc.fieldType === 'number') {\n <div class=\"rc-atomic-change\">\n <span class=\"rc-val-old\">{{ fc.oldValue || '(empty)' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-val-new\">{{ fc.newValue || '(empty)' }}</span>\n </div>\n } @else if (fc.diffHtml) {\n <div class=\"rc-text-diff\" [innerHTML]=\"fc.diffHtml\"></div>\n } @else {\n <div class=\"rc-atomic-change\">\n <span class=\"rc-val-old\">{{ fc.oldValue || '(empty)' }}</span>\n <i class=\"fa-solid fa-arrow-right rc-val-arrow\" aria-hidden=\"true\"></i>\n <span class=\"rc-val-new\">{{ fc.newValue || '(empty)' }}</span>\n </div>\n }\n </div>\n </div>\n }\n }\n\n @if (change.Comments) {\n <div class=\"rc-comments\">\n <i class=\"fa-solid fa-comment\" aria-hidden=\"true\"></i>\n {{ change.Comments }}\n </div>\n }\n\n @if (change.ErrorLog) {\n <div class=\"rc-errors\">\n <i class=\"fa-solid fa-exclamation-triangle\" aria-hidden=\"true\"></i>\n <pre class=\"rc-error-log\">{{ change.ErrorLog }}</pre>\n </div>\n }\n\n <!-- Restore action: shown for Update, Create, and Snapshot rows.\n Hidden on Delete rows (handled via the Recycle Bin path)\n and on the most recent change (restoring to current state is a no-op). -->\n @if (AllowRestore && change.Type !== 'Delete' && !isMostRecentChange(change)) {\n <div class=\"rc-restore-action\">\n <button class=\"rc-restore-btn\"\n (click)=\"OnRestoreVersion(change, $event)\"\n title=\"Restore the entire record to its state at this version\">\n <i class=\"fa-solid fa-clock-rotate-left\" aria-hidden=\"true\"></i>\n Restore record to this version\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Restore preview slide-in (reusable component) -->\n <mj-restore-preview-panel\n [Visible]=\"RestorePreviewVisible\"\n [Mode]=\"'live'\"\n [RecordChange]=\"RestorePreviewChange\"\n [LiveRecord]=\"record\"\n (RestoreConfirmed)=\"OnRestorePanelConfirmed($event)\"\n (RestoreCancelled)=\"OnRestorePanelCancelled()\">\n </mj-restore-preview-panel>\n\n @if (ShowCreateWizard) {\n <div class=\"rc-wizard-overlay\" (click)=\"OnLabelCreateCancelled()\">\n <div class=\"rc-wizard-container\" (click)=\"$event.stopPropagation()\">\n <mj-label-create\n [PreselectedEntity]=\"record.EntityInfo\"\n [PreselectedRecordIds]=\"[record.PrimaryKey.KeyValuePairs[0].Value]\"\n (Created)=\"OnLabelCreated($event)\"\n (Cancel)=\"OnLabelCreateCancelled()\">\n </mj-label-create>\n </div>\n </div>\n }\n</mj-slide-panel>\n", styles: ["/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n Record Changes \u2014 Option A \"Clean Timeline\"\n Linear / Notion-inspired minimal timeline design\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n/* \u2500\u2500\u2500 Container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n padding: 20px 24px;\n background: var(--mj-bg-surface-card);\n}\n\n/* \u2500\u2500\u2500 Version Labels Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-labels-section {\n background: var(--mj-bg-surface-card);\n border-radius: 8px;\n padding: 12px 16px;\n margin-bottom: 20px;\n border: 1px solid var(--mj-border-default);\n}\n\n.rc-labels-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.rc-labels-title {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n font-size: 13px;\n color: var(--mj-text-secondary);\n}\n\n.rc-labels-title i { color: var(--mj-brand-primary); }\n\n.rc-labels-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n border-radius: 10px;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n font-size: 10px;\n font-weight: 700;\n}\n\n.rc-create-label-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 5px 12px;\n border: 1px solid var(--mj-brand-primary);\n border-radius: 6px;\n background: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.rc-create-label-btn:hover {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.rc-labels-loading {\n font-size: 12px;\n color: var(--mj-text-muted);\n padding: 4px 0;\n}\n\n.rc-labels-loading i { margin-right: 4px; }\n\n.rc-labels-empty {\n font-size: 12px;\n color: var(--mj-text-disabled);\n font-style: italic;\n padding: 4px 0;\n}\n\n.rc-labels-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.rc-label-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 16px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n transition: all 0.15s ease;\n cursor: default;\n}\n\n.rc-label-chip:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.rc-label-chip-icon { color: var(--mj-brand-primary); font-size: 10px; }\n\n.rc-label-chip-name {\n font-weight: 600;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-label-chip-meta {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n\n.rc-label-chip-status {\n padding: 1px 5px;\n border-radius: 4px;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 9px;\n letter-spacing: 0.03em;\n}\n\n.label-status-active { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.label-status-archived { background: var(--mj-bg-surface-sunken); color: var(--mj-text-secondary); }\n.label-status-restored { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n\n.rc-label-chip-items { white-space: nowrap; }\n\n/* \u2500\u2500\u2500 Empty State (hint projected into <mj-empty-state> default slot) \u2500\u2500\u2500 */\n.rc-empty-state-hint {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-top: var(--mj-space-4);\n padding: 8px 16px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 20px;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n\n.rc-empty-state-hint i { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 Page Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-page-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 24px;\n padding-bottom: 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-page-title {\n font-size: 20px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n}\n\n.rc-page-subtitle {\n font-size: 13px;\n color: var(--mj-text-muted);\n margin-top: 4px;\n}\n\n.rc-header-meta { text-align: right; }\n\n.rc-entity-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n border-radius: 6px;\n font-size: 12px;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, var(--mj-bg-surface));\n}\n\n.rc-change-count {\n font-size: 12px;\n color: var(--mj-text-disabled);\n margin-top: 6px;\n}\n\n/* \u2500\u2500\u2500 Filter Bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-filter-bar {\n display: flex;\n gap: 8px;\n margin-bottom: 20px;\n flex-wrap: wrap;\n align-items: center;\n}\n\n.rc-search-wrap {\n position: relative;\n flex: 1;\n min-width: 180px;\n}\n\n.rc-search-wrap i {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--mj-text-disabled);\n font-size: 13px;\n pointer-events: none;\n}\n\n.rc-search-box {\n width: 100%;\n padding: 8px 12px 8px 36px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n font-size: 13px;\n background: var(--mj-bg-surface);\n outline: none;\n color: var(--mj-text-primary);\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n\n.rc-search-box:focus {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-search-box::placeholder { color: var(--mj-text-disabled); }\n\n.rc-filter-pill {\n padding: 7px 14px;\n border-radius: 8px;\n font-size: 12px;\n font-weight: 500;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n cursor: pointer;\n color: var(--mj-text-secondary);\n transition: all 0.15s;\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n.rc-filter-pill:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.rc-filter-pill.active {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary);\n}\n\n.rc-filter-results {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin-bottom: 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-clear-filters-link {\n background: none;\n border: none;\n color: var(--mj-brand-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n padding: 0;\n text-decoration: underline;\n}\n\n.rc-clear-filters-link:hover { color: var(--mj-brand-primary-hover); }\n\n/* \u2500\u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-timeline {\n position: relative;\n flex: 1;\n overflow-y: auto;\n}\n\n/* Vertical line running through the timeline */\n.rc-timeline::before {\n content: '';\n position: absolute;\n left: 15px;\n top: 24px;\n bottom: 0;\n width: 2px;\n background: var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Date Group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-date-group { margin-bottom: 8px; }\n\n.rc-date-label {\n position: relative;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.8px;\n color: var(--mj-text-disabled);\n padding: 16px 0 12px 44px;\n}\n\n/* Date dot on the timeline line */\n.rc-date-label::before {\n content: '';\n position: absolute;\n left: 10px;\n top: 50%;\n transform: translateY(-50%);\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--mj-border-default);\n border: 2px solid var(--mj-bg-surface-card);\n}\n\n/* \u2500\u2500\u2500 Change Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-card {\n position: relative;\n margin-left: 44px;\n margin-bottom: 12px;\n background: var(--mj-bg-surface);\n border-radius: 12px;\n border: 1px solid var(--mj-border-default);\n transition: all 0.2s;\n overflow: hidden;\n}\n\n.rc-card:hover {\n border-color: var(--mj-border-strong);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.rc-card:focus {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n\n/* Colored dot on the timeline line for each card */\n.rc-card::before {\n content: '';\n position: absolute;\n left: -23px;\n top: 20px;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n z-index: 1;\n}\n\n.rc-card.type-update::before {\n background: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-card.type-create::before {\n background: var(--mj-status-success);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-success) 15%, transparent);\n}\n\n.rc-card.type-delete::before {\n background: var(--mj-status-error);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-status-error) 15%, transparent);\n}\n\n/* \u2500\u2500\u2500 Card Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-card-header {\n padding: 14px 18px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n cursor: pointer;\n user-select: none;\n gap: 12px;\n}\n\n.rc-card-header-left {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n flex: 1;\n /* Allow the lineage chip to wrap to a second line on narrow panel widths\n instead of squeezing the title text into a \"Name and Description c...\"\n truncation. The badge stays on the first line; chip wraps gracefully. */\n flex-wrap: wrap;\n row-gap: 6px;\n}\n\n.rc-card-type-badge {\n padding: 3px 10px;\n border-radius: 5px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.3px;\n text-transform: uppercase;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.type-update .rc-card-type-badge { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n.type-create .rc-card-type-badge { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.type-delete .rc-card-type-badge { background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface)); color: var(--mj-status-error); }\n\n.rc-card-summary {\n font-size: 13px;\n color: var(--mj-text-secondary);\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-card-meta {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 12px;\n color: var(--mj-text-disabled);\n flex-shrink: 0;\n}\n\n.rc-card-user {\n display: flex;\n align-items: center;\n gap: 5px;\n white-space: nowrap;\n}\n\n.rc-avatar {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 8px;\n font-weight: 700;\n color: var(--mj-text-inverse);\n flex-shrink: 0;\n}\n\n.rc-card-time { white-space: nowrap; }\n\n.rc-card-source {\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.source-internal { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n.source-external { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--mj-status-warning); }\n\n.rc-card-status {\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n font-weight: 600;\n text-transform: uppercase;\n white-space: nowrap;\n}\n\n.status-complete { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--mj-status-success); }\n.status-pending { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--mj-status-warning); }\n.status-error { background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface)); color: var(--mj-status-error); }\n.status-unknown { background: var(--mj-bg-surface-sunken); color: var(--mj-text-muted); }\n\n.rc-card-chevron {\n color: var(--mj-text-disabled);\n transition: transform 0.2s;\n font-size: 12px;\n flex-shrink: 0;\n}\n\n.rc-card.expanded .rc-card-chevron { transform: rotate(180deg); }\n\n/* \u2500\u2500\u2500 Card Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-card-body {\n border-top: 1px solid var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Field Row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-field-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-field-row:last-child { border-bottom: none; }\n\n.rc-field-label {\n width: 140px;\n min-width: 140px;\n padding: 12px 16px;\n font-size: 12px;\n font-weight: 600;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-card);\n display: flex;\n align-items: center;\n border-right: 1px solid var(--mj-border-default);\n}\n\n.rc-field-values {\n flex: 1;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n/* \u2500\u2500\u2500 Atomic Change (numbers, dates) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-atomic-change {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.rc-val-old {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-disabled);\n text-decoration: line-through;\n font-weight: 400;\n}\n\n.rc-val-arrow {\n color: var(--mj-border-strong);\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.rc-val-new {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-primary);\n font-weight: 500;\n}\n\n/* \u2500\u2500\u2500 Boolean Change \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-bool-change {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-bool-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.rc-bool-dot.on { background: var(--mj-status-success); }\n.rc-bool-dot.off { background: var(--mj-border-strong); }\n\n.rc-bool-label {\n font-size: 13px;\n font-weight: 500;\n}\n\n.rc-bool-label.old {\n color: var(--mj-text-disabled);\n text-decoration: line-through;\n}\n\n.rc-bool-label.new.active { color: var(--mj-status-success); }\n.rc-bool-label.new.inactive { color: var(--mj-text-disabled); }\n\n/* \u2500\u2500\u2500 Text Diff (strings only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-text-diff {\n font-size: 13px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n line-height: 1.6;\n word-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.rc-diff-added {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n padding: 1px 3px;\n border-radius: 3px;\n}\n\n.rc-diff-removed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n padding: 1px 3px;\n border-radius: 3px;\n text-decoration: line-through;\n}\n\n.rc-diff-unchanged {\n color: var(--mj-text-secondary);\n}\n\n/* \u2500\u2500\u2500 Deletion Note \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-deletion-note {\n padding: 16px 18px;\n color: var(--mj-text-muted);\n font-size: 13px;\n font-style: italic;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.rc-deletion-note i {\n color: var(--mj-status-error);\n font-size: 14px;\n}\n\n/* \u2500\u2500\u2500 Comments & Errors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-comments {\n padding: 12px 18px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n font-size: 13px;\n color: var(--mj-brand-primary);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-comments i {\n color: var(--mj-brand-primary);\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n.rc-errors {\n padding: 12px 18px;\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n font-size: 13px;\n color: var(--mj-status-error);\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-errors i {\n color: var(--mj-status-error);\n margin-top: 2px;\n flex-shrink: 0;\n}\n\n.rc-error-log {\n background: var(--mj-bg-surface);\n padding: 8px;\n border-radius: 4px;\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n font-size: 12px;\n color: var(--mj-status-error);\n margin: 0;\n white-space: pre-wrap;\n overflow-x: auto;\n flex: 1;\n}\n\n/* \u2500\u2500\u2500 Create Wizard Overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-wizard-overlay {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: rc-fadeIn 0.15s ease;\n}\n\n.rc-wizard-container {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n width: 90%;\n max-width: 600px;\n max-height: 80vh;\n overflow-y: auto;\n padding: 24px;\n animation: rc-slideUp 0.2s ease;\n}\n\n@keyframes rc-fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes rc-slideUp {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* \u2500\u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n@media (max-width: 768px) {\n .rc-card-header {\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n }\n\n .rc-card-meta {\n flex-wrap: wrap;\n gap: 8px;\n }\n\n .rc-field-row {\n flex-direction: column;\n }\n\n .rc-field-label {\n width: 100%;\n min-width: unset;\n border-right: none;\n border-bottom: 1px solid var(--mj-border-default);\n padding: 8px 16px;\n }\n\n .rc-field-values {\n padding: 8px 16px;\n }\n\n .rc-atomic-change {\n flex-wrap: wrap;\n }\n\n .rc-filter-bar {\n flex-direction: column;\n align-items: stretch;\n }\n\n .rc-search-wrap { min-width: unset; }\n\n .rc-filter-pill { justify-content: center; }\n\n .rc-page-header {\n flex-direction: column;\n gap: 12px;\n }\n\n .rc-header-meta { text-align: left; }\n}\n\n@media (max-width: 480px) {\n .rc-container { padding: 12px 16px; }\n\n .rc-card { margin-left: 36px; }\n\n .rc-date-label { padding-left: 36px; }\n\n .rc-card::before { left: -19px; }\n\n .rc-timeline::before { left: 12px; }\n\n .rc-date-label::before { left: 7px; }\n}\n\n/* \u2500\u2500\u2500 Restore Action \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-restore-action {\n display: flex;\n justify-content: flex-end;\n padding-top: 10px;\n margin-top: 10px;\n /* Breathing room from the card's bottom and right edges so the button\n doesn't sit flush against the border. */\n padding-right: 10px;\n padding-bottom: 10px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.rc-restore-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n font-size: 12px;\n font-weight: 500;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n border-radius: 6px;\n cursor: pointer;\n transition: all 150ms ease;\n font-family: inherit;\n line-height: 1;\n}\n\n.rc-restore-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 6px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.rc-restore-btn:active {\n transform: translateY(1px);\n box-shadow: none;\n}\n\n.rc-restore-btn i {\n font-size: 13px;\n}\n\n/* \u2500\u2500\u2500 Restore Preview Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.rc-restore-preview-backdrop {\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n animation: rc-fadeIn 0.15s ease;\n}\n\n.rc-restore-preview-panel {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n width: 90%;\n max-width: 700px;\n max-height: 80vh;\n overflow-y: auto;\n padding: 0;\n animation: rc-slideUp 0.2s ease;\n}\n\n.rc-restore-preview-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 20px;\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-title {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.rc-restore-preview-title i {\n color: var(--mj-brand-primary);\n}\n\n.rc-restore-preview-close {\n background: none;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 16px;\n transition: color 0.15s, background 0.15s;\n}\n\n.rc-restore-preview-close:hover {\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface-hover);\n}\n\n.rc-restore-preview-meta {\n display: flex;\n gap: 20px;\n padding: 12px 20px;\n font-size: 13px;\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-empty {\n padding: 24px 20px;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: 13px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n}\n\n.rc-restore-preview-empty i {\n color: var(--mj-text-disabled);\n}\n\n/* Restore Preview Table */\n.rc-restore-preview-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n\n.rc-restore-preview-table th {\n text-align: left;\n padding: 10px 16px;\n font-weight: 600;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-preview-table td {\n padding: 10px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n vertical-align: top;\n}\n\n.rc-restore-preview-table tr:last-child td {\n border-bottom: none;\n}\n\n.rc-restore-preview-table tr.rc-diff-changed {\n background: color-mix(in srgb, var(--mj-status-warning) 5%, var(--mj-bg-surface));\n}\n\n.rc-restore-preview-table tr.rc-diff-unchanged {\n opacity: 0.6;\n}\n\n.rc-restore-field-name {\n font-weight: 600;\n color: var(--mj-text-primary);\n white-space: nowrap;\n}\n\n.rc-restore-current {\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-secondary);\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-restore-version {\n font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;\n color: var(--mj-text-primary);\n font-weight: 500;\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rc-restore-indicator {\n text-align: center;\n width: 30px;\n}\n\n.rc-diff-changed .rc-restore-indicator i {\n color: var(--mj-status-warning);\n}\n\n.rc-diff-unchanged .rc-restore-indicator i {\n color: var(--mj-status-success);\n font-size: 12px;\n}\n\n/* Restore Preview Actions */\n.rc-restore-preview-actions {\n display: flex;\n gap: 10px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.rc-restore-confirm-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 600;\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s, box-shadow 0.15s;\n font-family: inherit;\n}\n\n.rc-restore-confirm-btn:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n}\n\n.rc-restore-confirm-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.rc-restore-cancel-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 18px;\n font-size: 13px;\n font-weight: 500;\n color: var(--mj-text-secondary);\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.15s;\n font-family: inherit;\n}\n\n.rc-restore-cancel-btn:hover:not(:disabled) {\n border-color: var(--mj-border-strong);\n color: var(--mj-text-primary);\n}\n\n.rc-restore-cancel-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Restore lineage + conditional filter chips + overflow popover\n * (added with the Restore Prior Version feature)\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* \u2500\u2500\u2500 Filter pill enhancements \u2500\u2500\u2500 */\n\n.rc-filter-pill-count {\n margin-left: 4px;\n background: color-mix(in srgb, currentColor 12%, transparent);\n padding: 1px 6px;\n border-radius: 10px;\n font-size: 10px;\n font-weight: 700;\n}\n\n.rc-filter-pill.active .rc-filter-pill-count {\n background: color-mix(in srgb, var(--mj-text-inverse, #fff) 22%, transparent);\n}\n\n/* Restore-variant pill: violet treatment to set it apart from type pills */\n.rc-filter-pill-restore {\n background: color-mix(in srgb, var(--mj-status-info) 8%, var(--mj-bg-surface));\n color: color-mix(in srgb, var(--mj-status-info) 80%, var(--mj-text-secondary));\n border-color: color-mix(in srgb, var(--mj-status-info) 30%, var(--mj-border-default));\n}\n.rc-filter-pill-restore:hover {\n border-color: var(--mj-status-info);\n color: var(--mj-status-info);\n}\n.rc-filter-pill-restore.active {\n background: var(--mj-status-info);\n color: var(--mj-text-inverse, #fff);\n border-color: var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Overflow filter popover \u2500\u2500\u2500 */\n\n.rc-filter-overflow-wrap {\n position: relative;\n display: inline-block;\n}\n\n.rc-filter-pill-overflow {\n /* Overflow trigger inherits the standard filter pill look */\n}\n\n.rc-filter-overflow-caret {\n font-size: 9px;\n margin-left: 2px;\n}\n\n.rc-filter-overflow-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10;\n}\n\n.rc-filter-overflow-popover {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 11;\n min-width: 240px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 10px;\n box-shadow: 0 14px 40px rgba(0, 0, 0, 0.18);\n padding: 8px;\n}\n\n.rc-filter-overflow-header {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--mj-text-muted);\n font-weight: 600;\n padding: 6px 8px 8px;\n}\n\n.rc-filter-overflow-row {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 8px;\n border-radius: 6px;\n font-size: 13px;\n color: var(--mj-text-primary);\n cursor: pointer;\n}\n\n.rc-filter-overflow-row:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.rc-filter-overflow-row input[type=\"checkbox\"] {\n margin: 0;\n accent-color: var(--mj-brand-primary);\n}\n\n.rc-filter-overflow-row i {\n color: var(--mj-text-muted);\n font-size: 12px;\n width: 14px;\n text-align: center;\n}\n\n.rc-filter-overflow-row-restore i {\n color: var(--mj-status-info);\n}\n\n.rc-filter-overflow-label {\n flex: 1;\n}\n\n.rc-filter-overflow-count {\n color: var(--mj-text-muted);\n font-size: 11px;\n font-weight: 500;\n}\n\n.rc-filter-overflow-actions {\n display: flex;\n justify-content: space-between;\n border-top: 1px solid var(--mj-border-subtle);\n padding-top: 8px;\n margin-top: 6px;\n}\n\n.rc-filter-overflow-clear,\n.rc-filter-overflow-done {\n background: none;\n border: none;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n padding: 4px 10px;\n border-radius: 5px;\n}\n\n.rc-filter-overflow-clear {\n color: var(--mj-text-muted);\n}\n.rc-filter-overflow-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.rc-filter-overflow-done {\n color: var(--mj-brand-primary);\n}\n.rc-filter-overflow-done:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n}\n\n/* \u2500\u2500\u2500 Snapshot type badge + card accent \u2500\u2500\u2500 */\n\n.type-snapshot .rc-card-type-badge {\n background: color-mix(in srgb, var(--mj-text-muted) 15%, var(--mj-bg-surface));\n color: var(--mj-text-secondary);\n}\n\n/* \u2500\u2500\u2500 Restore Source pill (in card meta) \u2500\u2500\u2500 */\n\n.source-restore {\n background: color-mix(in srgb, var(--mj-status-info) 12%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Restore card variant: violet left accent \u2500\u2500\u2500 */\n\n.rc-card.rc-card-restore {\n border-left: 3px solid var(--mj-status-info);\n}\n\n/* \u2500\u2500\u2500 Lineage chip \u2500\u2500\u2500 */\n\n/* Lineage chip \u2014 outline-only \"interactive link\" treatment so it reads as\n distinct from the filled RESTORE badge sitting next to it. The badge says\n \"this row IS a restore\" (filled, static); the chip says \"click here to\n see the source version\" (outline, hoverable). */\n.rc-restored-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n margin-left: 8px;\n padding: 2px 9px;\n border-radius: 11px;\n font-size: 11px;\n font-weight: 500;\n background: transparent;\n color: color-mix(in srgb, var(--mj-status-info) 90%, var(--mj-text-primary));\n border: 1px solid color-mix(in srgb, var(--mj-status-info) 40%, var(--mj-border-default));\n cursor: pointer;\n text-decoration: none;\n white-space: nowrap;\n transition: background-color 0.12s, border-color 0.12s, color 0.12s;\n}\n\n.rc-restored-chip:hover {\n background: color-mix(in srgb, var(--mj-status-info) 12%, transparent);\n border-color: var(--mj-status-info);\n color: var(--mj-status-info);\n}\n\n.rc-restored-chip i {\n font-size: 10px;\n}\n\n.rc-restored-chip-orphan {\n cursor: default;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-muted);\n border-color: var(--mj-border-default);\n}\n\n.rc-restored-chip-orphan:hover {\n background: var(--mj-bg-surface-card);\n border-color: var(--mj-border-default);\n}\n\n/* \u2500\u2500\u2500 Card highlight (when jumped-to via lineage chip) \u2500\u2500\u2500 */\n\n.rc-card.rc-card-highlight {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n animation: rc-card-pulse 1.4s ease-in-out;\n}\n\n@keyframes rc-card-pulse {\n 0%, 100% { background: var(--mj-bg-surface); }\n 50% { background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface)); }\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Restore-row badge override + Restore Reason block\n * (mockup-aligned treatment for restore rows in the timeline)\n * \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* The base type-update badge styling would otherwise paint a restore row's\n badge blue with \"Update\" text. Override to violet + \"Restore\" text so it\n matches the source pill, lineage chip, and card border. */\n.rc-card-type-badge.rc-card-type-badge-restore {\n background: color-mix(in srgb, var(--mj-status-info) 14%, var(--mj-bg-surface));\n color: var(--mj-status-info);\n}\n\n/* Restore reason \u2014 minimal blockquote treatment.\n A thin violet left bar + italic text reads like a margin note rather than\n a heavy callout. Visually connected to the restore family (left bar uses\n the same violet as the badge / source pill / card border) without\n competing with the field-diff rows that follow. */\n.rc-restore-reason {\n /* Indented in from the card body edge so it visually nests one level\n deeper than the field-diff rows that follow, and given generous\n vertical breathing room so it doesn't crowd the lineage chip above\n or the first field row below. */\n margin: 14px 8px 18px 18px;\n padding: 0 0 0 12px;\n border-left: 2px solid color-mix(in srgb, var(--mj-status-info) 55%, var(--mj-border-default));\n display: flex;\n align-items: baseline;\n gap: 8px;\n font-size: 13px;\n line-height: 1.5;\n}\n\n.rc-restore-reason-label {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n color: color-mix(in srgb, var(--mj-status-info) 75%, var(--mj-text-muted));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.rc-restore-reason-text {\n color: var(--mj-text-secondary);\n font-style: italic;\n}\n"] }]
|
|
1448
|
-
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i1.MJNotificationService }, { type: i2.DomSanitizer }], {
|
|
1608
|
+
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i1.MJNotificationService }, { type: i2.DomSanitizer }], { DialogClosed: [{
|
|
1609
|
+
type: Output
|
|
1610
|
+
}], dialogClosed: [{
|
|
1449
1611
|
type: Output
|
|
1450
1612
|
}], record: [{
|
|
1451
1613
|
type: Input
|