@memberjunction/ng-artifacts 5.22.0 → 5.23.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.
@@ -142,7 +142,7 @@ function ArtifactViewerPanelComponent_Conditional_1_Template(rf, ctx) { if (rf &
142
142
  } if (rf & 2) {
143
143
  const ctx_r1 = i0.ɵɵnextContext();
144
144
  i0.ɵɵadvance(3);
145
- i0.ɵɵproperty("ngClass", ctx_r1.getArtifactIcon());
145
+ i0.ɵɵproperty("ngClass", ctx_r1.artifactIcon);
146
146
  i0.ɵɵadvance();
147
147
  i0.ɵɵtextInterpolate1(" ", ctx_r1.displayName, " ");
148
148
  i0.ɵɵadvance();
@@ -734,20 +734,23 @@ export class ArtifactViewerPanelComponent {
734
734
  if (changes['versionNumber'] && !changes['versionNumber'].firstChange) {
735
735
  const newVersionNumber = changes['versionNumber'].currentValue;
736
736
  if (newVersionNumber != null) {
737
- // Check if we already have this version loaded (avoid reload if possible)
737
+ // Check if we have metadata for this version (allVersions has lightweight metadata)
738
738
  const targetVersion = this.allVersions.find(v => v.VersionNumber === newVersionNumber);
739
739
  if (targetVersion) {
740
- // Just switch to the version we already have
741
- this.artifactVersion = targetVersion;
742
740
  this.selectedVersionNumber = targetVersion.VersionNumber || 1;
743
- this.jsonContent = this.FormatJSON(targetVersion.Content || '{}');
744
- // Load version attributes
745
- await this.loadVersionAttributes();
746
- // Reload collection associations for this version
747
- await this.loadCollectionAssociations();
748
- // Reload links data
749
- await this.loadLinksData();
750
- this.cdr.detectChanges(); // zone.js 0.15: async chain doesn't trigger CD
741
+ // Load full content for the selected version
742
+ const fullVersion = await this.loadVersionContent(targetVersion.ID);
743
+ if (fullVersion) {
744
+ this.artifactVersion = fullVersion;
745
+ this.jsonContent = this.FormatJSON(fullVersion.Content || '{}');
746
+ }
747
+ // Load attributes and collection data in parallel
748
+ await Promise.all([
749
+ this.loadVersionAttributes(),
750
+ this.loadCollectionAssociations(),
751
+ this.loadLinksData()
752
+ ]);
753
+ this.cdr.detectChanges();
751
754
  }
752
755
  else {
753
756
  // Need to reload to get this version (shouldn't normally happen)
@@ -767,56 +770,81 @@ export class ArtifactViewerPanelComponent {
767
770
  // Clear links data from previous artifact to prevent stale Links tab
768
771
  this.clearLinksData();
769
772
  const md = new Metadata();
770
- // Load artifact
771
- this.artifact = await md.GetEntityObject('MJ: Artifacts', this.currentUser);
772
- const loaded = await this.artifact.Load(this.artifactId);
773
+ // Load artifact — assign to local first to avoid mid-cycle icon flicker
774
+ const artifactEntity = await md.GetEntityObject('MJ: Artifacts', this.currentUser);
775
+ const loaded = await artifactEntity.Load(this.artifactId);
773
776
  if (!loaded) {
774
777
  this.error = 'Failed to load artifact';
775
778
  return;
776
779
  }
777
- // Load artifact type to check for DriverClass
778
- await this.loadArtifactType();
779
- // Load ALL versions
780
+ this.artifact = artifactEntity;
781
+ // PERF: Batch load version metadata, collection associations, and conversation links
782
+ // in a single RunViews call. Content is excluded here — loaded on-demand for the selected version.
780
783
  const rv = new RunView();
781
- const result = await rv.RunView({
782
- EntityName: 'MJ: Artifact Versions',
783
- ExtraFilter: `ArtifactID='${this.artifactId}'`,
784
- OrderBy: 'VersionNumber DESC',
785
- ResultType: 'entity_object'
786
- }, this.currentUser);
787
- if (result.Success && result.Results && result.Results.length > 0) {
788
- this.allVersions = result.Results;
789
- // If target version specified, try to load it
790
- if (targetVersionNumber) {
791
- const targetVersion = this.allVersions.find(v => v.VersionNumber === targetVersionNumber);
792
- if (targetVersion) {
793
- this.artifactVersion = targetVersion;
794
- this.selectedVersionNumber = targetVersion.VersionNumber || 1;
795
- this.jsonContent = this.FormatJSON(targetVersion.Content || '{}');
796
- }
797
- else {
798
- // Target version not found, default to latest
799
- this.artifactVersion = result.Results[0];
800
- this.selectedVersionNumber = this.artifactVersion.VersionNumber || 1;
801
- this.jsonContent = this.FormatJSON(this.artifactVersion.Content || '{}');
802
- }
803
- }
804
- else {
805
- // No target version, default to latest version (first in DESC order)
806
- this.artifactVersion = result.Results[0];
807
- this.selectedVersionNumber = this.artifactVersion.VersionNumber || 1;
808
- this.jsonContent = this.FormatJSON(this.artifactVersion.Content || '{}');
784
+ const batchResults = await rv.RunViews([
785
+ {
786
+ // [0] Version metadata (lightweight — no Content field)
787
+ EntityName: 'MJ: Artifact Versions',
788
+ ExtraFilter: `ArtifactID='${this.artifactId}'`,
789
+ OrderBy: 'VersionNumber DESC',
790
+ Fields: ['ID', 'ArtifactID', 'VersionNumber', 'Name', 'Description', '__mj_CreatedAt', '__mj_UpdatedAt'],
791
+ ResultType: 'simple'
792
+ },
793
+ {
794
+ // [1] Collection associations for all versions of this artifact
795
+ EntityName: 'MJ: Collection Artifacts',
796
+ ExtraFilter: `ArtifactVersionID IN (
797
+ SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
798
+ )`,
799
+ Fields: ['ID', 'CollectionID', 'ArtifactVersionID', 'Sequence'],
800
+ ResultType: 'simple'
801
+ },
802
+ {
803
+ // [2] Conversation detail artifact links (for Links tab)
804
+ EntityName: 'MJ: Conversation Detail Artifacts',
805
+ ExtraFilter: `ArtifactVersionID IN (
806
+ SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
807
+ )`,
808
+ Fields: ['ID', 'ConversationDetailID', 'ArtifactVersionID'],
809
+ MaxRows: 1,
810
+ ResultType: 'simple'
809
811
  }
810
- // Load version attributes
811
- await this.loadVersionAttributes();
812
- // Load collection associations
813
- await this.loadCollectionAssociations();
814
- // Load links data
815
- await this.loadLinksData();
812
+ ], this.currentUser);
813
+ const [versionsResult, collectionsResult, convDetailResult] = batchResults;
814
+ if (!versionsResult.Success || !versionsResult.Results || versionsResult.Results.length === 0) {
815
+ this.error = 'No artifact version found';
816
+ return;
817
+ }
818
+ // Store version metadata as simple objects (used for dropdown display)
819
+ this.allVersions = versionsResult.Results;
820
+ // Determine which version to display
821
+ let selectedVersion;
822
+ if (targetVersionNumber) {
823
+ selectedVersion = versionsResult.Results.find((v) => v.VersionNumber === targetVersionNumber);
824
+ }
825
+ // Fall back to latest version (first in DESC order)
826
+ const versionToLoad = selectedVersion || versionsResult.Results[0];
827
+ this.selectedVersionNumber = versionToLoad.VersionNumber || 1;
828
+ // PERF: Start artifact type resolution and selected version content load in parallel
829
+ const [, selectedVersionEntity] = await Promise.all([
830
+ this.loadArtifactType(),
831
+ this.loadVersionContent(versionToLoad.ID)
832
+ ]);
833
+ if (selectedVersionEntity) {
834
+ this.artifactVersion = selectedVersionEntity;
835
+ this.jsonContent = this.FormatJSON(selectedVersionEntity.Content || '{}');
816
836
  }
817
837
  else {
818
- this.error = 'No artifact version found';
838
+ this.error = 'Failed to load artifact version content';
839
+ return;
819
840
  }
841
+ // PERF: Process collection and links data in parallel (uses already-fetched batch data)
842
+ await Promise.all([
843
+ this.processCollectionAssociations(collectionsResult),
844
+ this.processLinksData(collectionsResult, convDetailResult)
845
+ ]);
846
+ // Load version attributes (depends on selected version being set)
847
+ await this.loadVersionAttributes();
820
848
  }
821
849
  catch (err) {
822
850
  console.error('Error loading artifact:', err);
@@ -824,9 +852,26 @@ export class ArtifactViewerPanelComponent {
824
852
  }
825
853
  finally {
826
854
  this.isLoading = false;
855
+ this.updateArtifactIcon();
827
856
  this.cdr.detectChanges();
828
857
  }
829
858
  }
859
+ /**
860
+ * Load full content for a single version by ID.
861
+ * This avoids downloading Content for ALL versions when only one is displayed.
862
+ */
863
+ async loadVersionContent(versionId) {
864
+ try {
865
+ const md = new Metadata();
866
+ const versionEntity = await md.GetEntityObject('MJ: Artifact Versions', this.currentUser);
867
+ const loaded = await versionEntity.Load(versionId);
868
+ return loaded ? versionEntity : null;
869
+ }
870
+ catch (err) {
871
+ console.error('Error loading version content:', err);
872
+ return null;
873
+ }
874
+ }
830
875
  /**
831
876
  * Clear all links-related data to prevent stale data when switching artifacts
832
877
  */
@@ -861,7 +906,7 @@ export class ArtifactViewerPanelComponent {
861
906
  const result = await rv.RunView({
862
907
  EntityName: 'MJ: Artifact Version Attributes',
863
908
  ExtraFilter: `ArtifactVersionID='${this.artifactVersion.ID}'`,
864
- ResultType: 'entity_object'
909
+ ResultType: 'simple'
865
910
  }, this.currentUser);
866
911
  if (result.Success && result.Results) {
867
912
  this.versionAttributes = result.Results;
@@ -1013,53 +1058,73 @@ export class ArtifactViewerPanelComponent {
1013
1058
  cleaned = cleaned.replace(/\\\\n/g, '');
1014
1059
  return cleaned;
1015
1060
  }
1016
- async loadCollectionAssociations() {
1061
+ /**
1062
+ * Process pre-fetched collection association data.
1063
+ * Accepts the batch result from loadArtifact() to avoid duplicate queries.
1064
+ */
1065
+ async processCollectionAssociations(collectionsResult) {
1017
1066
  if (!this.artifactId)
1018
1067
  return;
1019
1068
  try {
1020
- const rv = new RunView();
1021
- // Load ALL collection associations for ALL versions of this artifact
1022
- const result = await rv.RunView({
1023
- EntityName: 'MJ: Collection Artifacts',
1024
- ExtraFilter: `ArtifactVersionID IN (
1025
- SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
1026
- )`,
1027
- ResultType: 'entity_object'
1028
- }, this.currentUser);
1029
- if (result.Success && result.Results) {
1030
- this.artifactCollections = result.Results;
1031
- // Filter to get only collections containing the CURRENT version
1032
- const currentVersionId = this.artifactVersion?.ID;
1033
- if (currentVersionId) {
1034
- // Type-safe comparison: ensure both IDs are strings and match exactly
1035
- const currentIdStr = String(currentVersionId).toLowerCase();
1036
- this.currentVersionCollections = result.Results.filter(ca => {
1037
- const versionIdStr = String(ca.ArtifactVersionID || '').toLowerCase();
1038
- return versionIdStr && currentIdStr && versionIdStr === currentIdStr;
1039
- });
1040
- }
1041
- else {
1042
- this.currentVersionCollections = [];
1043
- }
1044
- // Load the primary collection details if exists
1045
- if (this.artifactCollections.length > 0) {
1046
- const collectionId = this.artifactCollections[0].CollectionID;
1069
+ // If no pre-fetched data, fetch it (used by selectVersion/saveToCollections reload)
1070
+ let collectionRows;
1071
+ if (collectionsResult?.Success && collectionsResult.Results) {
1072
+ collectionRows = collectionsResult.Results;
1073
+ }
1074
+ else {
1075
+ const rv = new RunView();
1076
+ const result = await rv.RunView({
1077
+ EntityName: 'MJ: Collection Artifacts',
1078
+ ExtraFilter: `ArtifactVersionID IN (
1079
+ SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
1080
+ )`,
1081
+ Fields: ['ID', 'CollectionID', 'ArtifactVersionID', 'Sequence'],
1082
+ ResultType: 'simple'
1083
+ }, this.currentUser);
1084
+ collectionRows = (result.Success ? result.Results : []);
1085
+ }
1086
+ // Store as simple objects — these are read-only display data
1087
+ this.artifactCollections = collectionRows;
1088
+ // Filter to get only collections containing the CURRENT version
1089
+ const currentVersionId = this.artifactVersion?.ID;
1090
+ if (currentVersionId) {
1091
+ const currentIdStr = String(currentVersionId).toLowerCase();
1092
+ this.currentVersionCollections = collectionRows.filter(ca => {
1093
+ const versionIdStr = String(ca['ArtifactVersionID'] || ca.ArtifactVersionID || '').toLowerCase();
1094
+ return versionIdStr && currentIdStr && versionIdStr === currentIdStr;
1095
+ });
1096
+ }
1097
+ else {
1098
+ this.currentVersionCollections = [];
1099
+ }
1100
+ // Load the primary collection details if exists
1101
+ if (this.artifactCollections.length > 0) {
1102
+ const collectionId = collectionRows[0].CollectionID ||
1103
+ this.artifactCollections[0].CollectionID;
1104
+ if (collectionId) {
1047
1105
  const md = new Metadata();
1048
1106
  this.primaryCollection = await md.GetEntityObject('MJ: Collections', this.currentUser);
1049
1107
  await this.primaryCollection.Load(collectionId);
1050
1108
  }
1051
- else {
1052
- this.primaryCollection = null;
1053
- }
1109
+ }
1110
+ else {
1111
+ this.primaryCollection = null;
1054
1112
  }
1055
1113
  }
1056
1114
  catch (err) {
1057
- console.error('Error loading collection associations:', err);
1115
+ console.error('Error processing collection associations:', err);
1058
1116
  }
1059
1117
  finally {
1060
- this.cdr.detectChanges(); // zone.js 0.15: async RunView doesn't trigger CD
1118
+ this.cdr.detectChanges();
1061
1119
  }
1062
1120
  }
1121
+ /**
1122
+ * @deprecated Use processCollectionAssociations() instead. Kept as a shim for callers
1123
+ * that don't have pre-fetched data (e.g., selectVersion, saveToCollections).
1124
+ */
1125
+ async loadCollectionAssociations() {
1126
+ return this.processCollectionAssociations();
1127
+ }
1063
1128
  get isInCollection() {
1064
1129
  return this.currentVersionCollections.length > 0;
1065
1130
  }
@@ -1139,18 +1204,21 @@ export class ArtifactViewerPanelComponent {
1139
1204
  }
1140
1205
  }
1141
1206
  async selectVersion(version) {
1142
- this.artifactVersion = version;
1143
1207
  this.selectedVersionNumber = version.VersionNumber || 1;
1144
- this.jsonContent = this.FormatJSON(version.Content || '{}');
1145
1208
  this.showVersionDropdown = false;
1146
- // Load attributes for the selected version
1147
- await this.loadVersionAttributes();
1148
- // CRITICAL FIX: Reload collection associations for this version
1149
- // This ensures bookmark button and Links tab reflect the correct state
1150
- await this.loadCollectionAssociations();
1151
- // Also reload links data to update conversation/collection links
1152
- await this.loadLinksData();
1153
- this.cdr.detectChanges(); // zone.js 0.15: async chain doesn't trigger CD
1209
+ // Load full content for the selected version (allVersions only has metadata)
1210
+ const fullVersion = await this.loadVersionContent(version.ID);
1211
+ if (fullVersion) {
1212
+ this.artifactVersion = fullVersion;
1213
+ this.jsonContent = this.FormatJSON(fullVersion.Content || '{}');
1214
+ }
1215
+ // Load attributes and collection data in parallel
1216
+ await Promise.all([
1217
+ this.loadVersionAttributes(),
1218
+ this.loadCollectionAssociations(),
1219
+ this.loadLinksData()
1220
+ ]);
1221
+ this.cdr.detectChanges();
1154
1222
  }
1155
1223
  async onSaveToLibrary() {
1156
1224
  // Always show the collection picker modal
@@ -1230,9 +1298,10 @@ export class ArtifactViewerPanelComponent {
1230
1298
  }
1231
1299
  }
1232
1300
  /**
1233
- * Load links data: origin conversation and all collections containing this artifact
1301
+ * Process links data using pre-fetched batch results from loadArtifact().
1302
+ * Reuses collection data from the same batch to avoid duplicate queries.
1234
1303
  */
1235
- async loadLinksData() {
1304
+ async processLinksData(collectionsResult, convDetailResult) {
1236
1305
  if (!this.artifactId)
1237
1306
  return;
1238
1307
  // Clear old links data first to prevent stale data from previous artifact
@@ -1240,68 +1309,81 @@ export class ArtifactViewerPanelComponent {
1240
1309
  try {
1241
1310
  const md = new Metadata();
1242
1311
  const rv = new RunView();
1243
- // Load all collections containing any version of this artifact
1244
- const collArtifactsResult = await rv.RunView({
1245
- EntityName: 'MJ: Collection Artifacts',
1246
- ExtraFilter: `ArtifactVersionID IN (
1247
- SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
1248
- )`,
1249
- ResultType: 'entity_object'
1250
- }, this.currentUser);
1251
- if (collArtifactsResult.Success && collArtifactsResult.Results) {
1252
- // Get unique collection IDs
1253
- const collectionIds = [...new Set(collArtifactsResult.Results.map(ca => ca.CollectionID))];
1254
- if (collectionIds.length > 0) {
1255
- const collectionsFilter = collectionIds.map(id => `ID='${id}'`).join(' OR ');
1256
- const collectionsResult = await rv.RunView({
1257
- EntityName: 'MJ: Collections',
1258
- ExtraFilter: collectionsFilter,
1259
- ResultType: 'entity_object'
1312
+ // Use pre-fetched collection data or fetch if not provided
1313
+ let collectionRows = [];
1314
+ if (collectionsResult?.Success && collectionsResult.Results) {
1315
+ collectionRows = collectionsResult.Results;
1316
+ }
1317
+ else {
1318
+ const result = await rv.RunView({
1319
+ EntityName: 'MJ: Collection Artifacts',
1320
+ ExtraFilter: `ArtifactVersionID IN (
1321
+ SELECT ID FROM [__mj].[vwArtifactVersions] WHERE ArtifactID='${this.artifactId}'
1322
+ )`,
1323
+ Fields: ['ID', 'CollectionID', 'ArtifactVersionID'],
1324
+ ResultType: 'simple'
1325
+ }, this.currentUser);
1326
+ collectionRows = (result.Success ? result.Results : []);
1327
+ }
1328
+ // Get unique collection IDs and load collection details
1329
+ const collectionIds = [...new Set(collectionRows.map(ca => (ca['CollectionID'] || ca.CollectionID)))].filter(Boolean);
1330
+ if (collectionIds.length > 0) {
1331
+ const collectionsFilter = collectionIds.map(id => `ID='${id}'`).join(' OR ');
1332
+ const collectionsEntityResult = await rv.RunView({
1333
+ EntityName: 'MJ: Collections',
1334
+ ExtraFilter: collectionsFilter,
1335
+ Fields: ['ID', 'Name', 'UserID', 'Description'],
1336
+ ResultType: 'simple'
1337
+ }, this.currentUser);
1338
+ if (collectionsEntityResult.Success && collectionsEntityResult.Results) {
1339
+ this.allCollections = collectionsEntityResult.Results;
1340
+ }
1341
+ }
1342
+ // Use pre-fetched conversation detail artifact data or fetch if not provided
1343
+ let convDetailRows = [];
1344
+ if (convDetailResult?.Success && convDetailResult.Results) {
1345
+ convDetailRows = convDetailResult.Results;
1346
+ }
1347
+ else {
1348
+ const versionIds = this.allVersions.map(v => v.ID);
1349
+ if (versionIds.length > 0) {
1350
+ const versionFilter = versionIds.map(id => `ArtifactVersionID='${id}'`).join(' OR ');
1351
+ const result = await rv.RunView({
1352
+ EntityName: 'MJ: Conversation Detail Artifacts',
1353
+ ExtraFilter: versionFilter,
1354
+ Fields: ['ID', 'ConversationDetailID', 'ArtifactVersionID'],
1355
+ MaxRows: 1,
1356
+ ResultType: 'simple'
1260
1357
  }, this.currentUser);
1261
- if (collectionsResult.Success && collectionsResult.Results) {
1262
- this.allCollections = collectionsResult.Results;
1263
- }
1358
+ convDetailRows = (result.Success ? result.Results : []);
1264
1359
  }
1265
1360
  }
1266
- // Load origin conversation (if artifact came from conversation)
1267
- // Artifacts are linked to conversations via ConversationDetailArtifact -> ConversationDetail -> Conversation
1268
- // Get all version IDs for this artifact
1269
- const versionIds = this.allVersions.map(v => v.ID);
1270
- if (versionIds.length > 0) {
1271
- const versionFilter = versionIds.map(id => `ArtifactVersionID='${id}'`).join(' OR ');
1272
- const convDetailArtifactsResult = await rv.RunView({
1273
- EntityName: 'MJ: Conversation Detail Artifacts',
1274
- ExtraFilter: versionFilter,
1275
- MaxRows: 1,
1276
- ResultType: 'entity_object'
1277
- }, this.currentUser);
1278
- if (convDetailArtifactsResult.Success && convDetailArtifactsResult.Results && convDetailArtifactsResult.Results.length > 0) {
1279
- const conversationDetailId = convDetailArtifactsResult.Results[0].ConversationDetailID;
1280
- const artifactVersionId = convDetailArtifactsResult.Results[0].ArtifactVersionID;
1281
- // Store which version came from the origin conversation
1282
- this.originConversationVersionId = artifactVersionId;
1283
- // Load the conversation detail to get the conversation ID
1284
- const conversationDetail = await md.GetEntityObject('MJ: Conversation Details', this.currentUser);
1285
- const detailLoaded = await conversationDetail.Load(conversationDetailId);
1286
- if (detailLoaded && conversationDetail.ConversationID) {
1287
- const conversation = await md.GetEntityObject('MJ: Conversations', this.currentUser);
1288
- const loaded = await conversation.Load(conversationDetail.ConversationID);
1289
- if (loaded) {
1290
- this.originConversation = conversation;
1291
- // Check if user has access (is owner or participant)
1292
- const userIsOwner = UUIDsEqual(conversation.UserID, this.currentUser.ID);
1293
- // Check if user is a participant
1294
- const participantResult = await rv.RunView({
1295
- EntityName: 'MJ: Conversation Details',
1296
- ExtraFilter: `ConversationID='${conversation.ID}' AND UserID='${this.currentUser.ID}'`,
1297
- MaxRows: 1,
1298
- ResultType: 'simple'
1299
- }, this.currentUser);
1300
- const userIsParticipant = participantResult.Success &&
1301
- participantResult.Results &&
1302
- participantResult.Results.length > 0;
1303
- this.hasAccessToOriginConversation = userIsOwner || userIsParticipant;
1304
- }
1361
+ // Load origin conversation if we have a link
1362
+ if (convDetailRows.length > 0) {
1363
+ const conversationDetailId = (convDetailRows[0]['ConversationDetailID'] || convDetailRows[0].ConversationDetailID);
1364
+ const artifactVersionId = (convDetailRows[0]['ArtifactVersionID'] || convDetailRows[0].ArtifactVersionID);
1365
+ this.originConversationVersionId = artifactVersionId;
1366
+ // Load conversation detail to get conversation ID
1367
+ const conversationDetail = await md.GetEntityObject('MJ: Conversation Details', this.currentUser);
1368
+ const detailLoaded = await conversationDetail.Load(conversationDetailId);
1369
+ if (detailLoaded && conversationDetail.ConversationID) {
1370
+ const conversation = await md.GetEntityObject('MJ: Conversations', this.currentUser);
1371
+ const loaded = await conversation.Load(conversationDetail.ConversationID);
1372
+ if (loaded) {
1373
+ this.originConversation = conversation;
1374
+ // Check if user has access (is owner or participant)
1375
+ const userIsOwner = UUIDsEqual(conversation.UserID, this.currentUser.ID);
1376
+ const participantResult = await rv.RunView({
1377
+ EntityName: 'MJ: Conversation Details',
1378
+ ExtraFilter: `ConversationID='${conversation.ID}' AND UserID='${this.currentUser.ID}'`,
1379
+ MaxRows: 1,
1380
+ Fields: ['ID'],
1381
+ ResultType: 'simple'
1382
+ }, this.currentUser);
1383
+ const userIsParticipant = participantResult.Success &&
1384
+ participantResult.Results &&
1385
+ participantResult.Results.length > 0;
1386
+ this.hasAccessToOriginConversation = userIsOwner || userIsParticipant;
1305
1387
  }
1306
1388
  }
1307
1389
  }
@@ -1310,9 +1392,15 @@ export class ArtifactViewerPanelComponent {
1310
1392
  console.error('Error loading links data:', error);
1311
1393
  }
1312
1394
  finally {
1313
- this.cdr.detectChanges(); // zone.js 0.15: async chain doesn't trigger CD
1395
+ this.cdr.detectChanges();
1314
1396
  }
1315
1397
  }
1398
+ /**
1399
+ * @deprecated Use processLinksData() instead. Kept as shim for callers without pre-fetched data.
1400
+ */
1401
+ async loadLinksData() {
1402
+ return this.processLinksData();
1403
+ }
1316
1404
  get linksToShow() {
1317
1405
  const links = [];
1318
1406
  // Get current version ID being viewed
@@ -1513,10 +1601,13 @@ export class ArtifactViewerPanelComponent {
1513
1601
  * Get the icon for this artifact using the centralized icon service.
1514
1602
  * Fallback priority: Plugin icon > Metadata icon > Hardcoded mapping > Generic icon
1515
1603
  */
1516
- getArtifactIcon() {
1517
- if (!this.artifact)
1518
- return 'fa-file';
1519
- return this.artifactIconService.getArtifactIcon(this.artifact);
1604
+ /** Cached icon class — set once after artifact loads to avoid mid-cycle flicker */
1605
+ artifactIcon = 'fa-file';
1606
+ /** Update the cached icon from the loaded artifact */
1607
+ updateArtifactIcon() {
1608
+ this.artifactIcon = this.artifact
1609
+ ? this.artifactIconService.getArtifactIcon(this.artifact)
1610
+ : 'fa-file';
1520
1611
  }
1521
1612
  static ɵfac = function ArtifactViewerPanelComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ArtifactViewerPanelComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i1.MJNotificationService), i0.ɵɵdirectiveInject(i2.DomSanitizer), i0.ɵɵdirectiveInject(i3.ArtifactIconService)); };
1522
1613
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ArtifactViewerPanelComponent, selectors: [["mj-artifact-viewer-panel"]], viewQuery: function ArtifactViewerPanelComponent_Query(rf, ctx) { if (rf & 1) {
@@ -1546,7 +1637,7 @@ export class ArtifactViewerPanelComponent {
1546
1637
  }
1547
1638
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ArtifactViewerPanelComponent, [{
1548
1639
  type: Component,
1549
- args: [{ standalone: false, selector: 'mj-artifact-viewer-panel', template: "<div class=\"artifact-viewer-panel\" [class.no-header]=\"!showHeader\">\n @if (showHeader) {\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <h3>\n <i class=\"fas\" [ngClass]=\"getArtifactIcon()\"></i>\n {{ displayName }}\n </h3>\n @if (displayDescription) {\n <p class=\"header-description\">{{ displayDescription }}</p>\n }\n </div>\n <div class=\"panel-header-right\">\n <div class=\"version-selector\"\n [class.clickable]=\"allVersions.length > 1 && viewContext !== 'collection'\"\n (click)=\"viewContext !== 'collection' && toggleVersionDropdown()\">\n <i class=\"fas fa-history\"></i>\n <span class=\"version-label\">v{{ selectedVersionNumber }}</span>\n @if (allVersions.length > 1 && viewContext !== 'collection') {\n <i class=\"fas fa-chevron-down dropdown-icon\" [class.open]=\"showVersionDropdown\"></i>\n }\n @if (showVersionDropdown) {\n <div class=\"version-dropdown\" (click)=\"$event.stopPropagation()\">\n @for (version of allVersions; track version.ID) {\n <div class=\"version-option\"\n [class.selected]=\"version.VersionNumber === selectedVersionNumber\"\n (click)=\"selectVersion(version); $event.stopPropagation()\">\n <span class=\"version-number\">v{{ version.VersionNumber }}</span>\n <span class=\"version-date\">{{ version.__mj_CreatedAt | date:'short' }}</span>\n @if (version.VersionNumber === selectedVersionNumber) {\n <i class=\"fas fa-check\"></i>\n }\n </div>\n }\n </div>\n }\n </div>\n @if (pluginViewer?.SupportsFeedback) {\n <button class=\"feedback-btn\"\n (click)=\"pluginViewer?.AskUserForFeedback()\"\n title=\"Inspect & rate components\">\n <i class=\"fas fa-star-half-stroke\"></i>\n </button>\n }\n @if (showSaveToCollection) {\n <button class=\"save-to-collection-btn\"\n [class.in-collection]=\"isInCollection\"\n (click)=\"onSaveToLibrary()\"\n [title]=\"isInCollection ? 'Current version saved to ' + currentVersionCollections.length + ' collection(s)' : 'Save to Collection'\">\n <i [class]=\"isInCollection ? 'fas fa-bookmark' : 'far fa-bookmark'\"></i>\n </button>\n }\n @if (canShare) {\n <button class=\"share-btn\"\n (click)=\"onShare()\"\n title=\"Share\">\n <i class=\"fas fa-share-nodes\"></i>\n </button>\n }\n @if (showMaximizeButton) {\n <button class=\"maximize-btn\"\n (click)=\"onMaximizeToggle()\"\n [title]=\"isMaximized ? 'Restore Width' : 'Maximize Width'\">\n <i class=\"fas\" [ngClass]=\"isMaximized ? 'fa-compress-arrows-alt' : 'fa-arrows-left-right'\"></i>\n </button>\n }\n @if (showCloseButton) {\n <button class=\"close-btn\" (click)=\"onClose()\" title=\"Close\">\n <i class=\"fas fa-times\"></i>\n </button>\n }\n </div>\n </div>\n }\n\n <div class=\"panel-content\">\n @if (isLoading) {\n <div class=\"loading-state\">\n <i class=\"fas fa-spinner fa-spin\"></i>\n <span>Loading artifact...</span>\n </div>\n }\n\n @if (error) {\n <div class=\"error-state\">\n <i class=\"fas fa-exclamation-triangle\"></i>\n <span>{{ error }}</span>\n </div>\n }\n\n @if (!isLoading && !error && artifact) {\n <div class=\"artifact-content-wrapper\" [class.no-tabs]=\"!showTabs\">\n <!-- Tab Navigation (Dynamic) - Hidden when showTabs is false -->\n @if (showTabs) {\n <div class=\"tab-navigation\">\n @for (tab of allTabs; track tab) {\n <button class=\"tab-btn\"\n [class.active]=\"activeTab === tab.toLowerCase()\"\n (click)=\"SetActiveTab(tab)\">\n @if (GetTabIcon(tab)) {\n <i [class]=\"GetTabIcon(tab)!\"></i>\n }\n {{ tab }}\n </button>\n }\n </div>\n }\n\n <!-- Tab Content (Dynamic) -->\n <div class=\"tab-content\">\n <!-- Plugin Viewer - Created once, kept alive, shown/hidden with CSS -->\n @if (hasPlugin && artifactVersion && artifactTypeName) {\n <div class=\"plugin-container\" [class.plugin-hidden]=\"activeTab !== 'display'\">\n <mj-artifact-type-plugin-viewer\n [artifactVersion]=\"artifactVersion\"\n [artifactTypeName]=\"artifactTypeName\"\n [contentType]=\"contentType\"\n [readonly]=\"true\"\n (openEntityRecord)=\"onOpenEntityRecord($event)\"\n (navigationRequest)=\"onNavigationRequest($event)\"\n (pluginLoaded)=\"onPluginLoaded()\"\n (tabsChanged)=\"onTabsChanged()\">\n </mj-artifact-type-plugin-viewer>\n </div>\n }\n\n <!-- Display Tab - Fallback content when no plugin -->\n @if (activeTab === 'display' && (!hasPlugin || !artifactVersion)) {\n <div class=\"display-content\">\n @if (displayMarkdown || displayHtml) {\n <!-- Fallback to extracted markdown/HTML attributes -->\n <div class=\"display-toolbar\">\n <button class=\"btn-icon\" title=\"Print\" (click)=\"onPrintDisplayContent()\">\n <i class=\"fas fa-print\"></i> Print\n </button>\n <button class=\"btn-icon\" title=\"Copy Content\" (click)=\"onCopyDisplayContent()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n @if (displayMarkdown) {\n <div class=\"markdown-content\">\n <mj-markdown [data]=\"displayMarkdown\"\n [enableCollapsibleHeadings]=\"true\"\n [enableLineNumbers]=\"true\"\n [enableSmartypants]=\"true\"\n [enableHtml]=\"true\"></mj-markdown>\n </div>\n } @else if (displayHtml) {\n <div class=\"html-content\" [innerHTML]=\"displayHtml\"></div>\n }\n }\n </div>\n }\n\n <!-- Details Tab - Artifact metadata (always in DOM, hidden with CSS) -->\n <div class=\"details-content\" [class.tab-hidden]=\"activeTab !== 'details'\">\n <div class=\"artifact-meta\">\n <div class=\"meta-item\">\n <label>Type</label>\n <span>{{ artifact.Type }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Version</label>\n <span>{{ artifactVersion?.VersionNumber || 1 }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Created</label>\n <span>{{ artifact.__mj_CreatedAt | date:'short' }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Updated</label>\n <span>{{ artifactVersion?.__mj_UpdatedAt | date:'short' }}</span>\n </div>\n @if (artifact.Description) {\n <div class=\"meta-item full-width\">\n <label>Artifact Description</label>\n <span>{{ artifact.Description }}</span>\n </div>\n }\n @if (artifactVersion?.Description && artifact.Description && artifactVersion?.Description !== artifact.Description) {\n <div class=\"meta-item full-width\">\n <label>Version Description</label>\n <span>{{ artifactVersion?.Description }}</span>\n </div>\n }\n </div>\n\n <!-- Version Attributes -->\n @if (filteredAttributes.length > 0) {\n <div class=\"attributes-section\">\n <h4>Version Attributes</h4>\n <div class=\"attributes-list\">\n @for (attr of filteredAttributes; track attr.ID) {\n <div class=\"attribute-item\">\n <label>{{ attr.Name }}</label>\n @if (attr.Value) {\n <span>{{ attr.Value }}</span>\n } @else {\n <span class=\"attribute-empty-pill\">Empty</span>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Links Tab - Conversations and Collections (always in DOM, hidden with CSS) -->\n <div class=\"links-container\" [class.tab-hidden]=\"activeTab !== 'links'\">\n @if (linksToShow.length > 0) {\n <div class=\"links-section\">\n @for (link of linksToShow; track link.id) {\n <div class=\"link-item\"\n [class.disabled]=\"!link.hasAccess\"\n [class.clickable]=\"link.hasAccess\"\n (click)=\"link.hasAccess ? onNavigateToLink(link) : null\"\n [title]=\"link.hasAccess ? 'Click to open' : 'No access'\">\n <div class=\"link-icon\">\n @if (link.type === 'conversation') {\n <i class=\"fas fa-comments\"></i>\n } @else {\n <i class=\"fas fa-folder\"></i>\n }\n </div>\n <div class=\"link-content\">\n <div class=\"link-name\">{{ link.name }}</div>\n <div class=\"link-type\">{{ link.type === 'conversation' ? 'Conversation' : 'Collection' }}</div>\n </div>\n <div class=\"link-actions\">\n <i class=\"fas fa-arrow-right\"></i>\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <i class=\"fas fa-link\"></i>\n <p>No links available</p>\n </div>\n }\n </div>\n\n <!-- Dynamic Plugin Tabs and Base Tabs (JSON, etc.) -->\n @if (activeTab !== 'display' && activeTab !== 'details' && activeTab !== 'links') {\n <!-- Check if this is a component tab -->\n @if (GetComponentTabType(activeTab); as componentType) {\n <div class=\"component-tab-content\">\n <ng-container *ngComponentOutlet=\"componentType; inputs: GetComponentInputs(activeTab)\"></ng-container>\n </div>\n } @else {\n @if (GetTabContent(activeTab); as tabData) {\n <div class=\"dynamic-tab-content\">\n @switch (tabData.type) {\n @case ('markdown') {\n <div class=\"markdown-viewer\">\n <mj-markdown [data]=\"tabData.content\"\n [enableCollapsibleHeadings]=\"true\"\n [enableLineNumbers]=\"true\"\n [enableSmartypants]=\"true\"\n [enableHtml]=\"true\"></mj-markdown>\n </div>\n }\n @case ('json') {\n <div class=\"json-viewer\">\n <div class=\"json-toolbar\">\n <button class=\"btn-icon\" title=\"Copy JSON\" (click)=\"onCopyToClipboard()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n <div class=\"json-editor-container\">\n <mj-code-editor\n [value]=\"tabData.content\"\n [language]=\"'json'\"\n [readonly]=\"true\"\n [lineWrapping]=\"true\"\n style=\"width: 100%; height: 100%;\">\n </mj-code-editor>\n </div>\n </div>\n }\n @case ('code') {\n <div class=\"code-viewer\">\n <div class=\"code-toolbar\">\n <button class=\"btn-icon\" title=\"Copy Code\" (click)=\"onCopyToClipboard()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n <div class=\"code-editor-container\">\n <mj-code-editor\n [value]=\"tabData.content\"\n [language]=\"tabData.language || 'typescript'\"\n [readonly]=\"true\"\n [lineWrapping]=\"true\"\n style=\"width: 100%; height: 100%;\">\n </mj-code-editor>\n </div>\n </div>\n }\n @case ('html') {\n <div class=\"html-viewer\" [innerHTML]=\"tabData.content\"></div>\n }\n @case ('plaintext') {\n <pre class=\"plaintext-viewer\">{{ tabData.content }}</pre>\n }\n }\n </div>\n }\n }\n }\n </div>\n </div>\n }\n </div>\n</div>\n\n", styles: [".artifact-viewer-panel {\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface);\n border-left: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.panel-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n padding: 12px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n width: 100%;\n box-sizing: border-box;\n}\n\n.panel-header-left {\n flex: 1;\n min-width: 0;\n margin-right: 16px;\n}\n\n.panel-header h3 {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n word-wrap: break-word;\n overflow-wrap: break-word;\n}\n\n.panel-header h3 i {\n color: var(--mj-brand-primary);\n flex-shrink: 0;\n margin-top: 2px;\n}\n\n.header-description {\n margin: 4px 0 0 0;\n font-size: 12px;\n color: var(--mj-text-muted);\n font-weight: normal;\n word-wrap: break-word;\n overflow-wrap: break-word;\n}\n\n.panel-header-right {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n}\n\n.version-selector {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 12px;\n font-size: 12px;\n color: var(--mj-text-muted);\n font-weight: 500;\n transition: all 0.2s;\n}\n\n.version-selector.clickable {\n cursor: pointer;\n}\n\n.version-selector.clickable:hover {\n background: var(--mj-border-default);\n}\n\n.version-selector i {\n font-size: 11px;\n}\n\n.version-selector .dropdown-icon {\n margin-left: 2px;\n transition: transform 0.2s;\n}\n\n.version-selector .dropdown-icon.open {\n transform: rotate(180deg);\n}\n\n.version-label {\n font-family: monospace;\n}\n\n.version-dropdown {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n min-width: 200px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-md);\n z-index: 1000;\n overflow: hidden;\n}\n\n.version-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n cursor: pointer;\n transition: background 0.15s;\n font-size: 13px;\n}\n\n.version-option:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.version-option.selected {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.version-option .version-number {\n font-family: monospace;\n font-weight: 600;\n min-width: 35px;\n}\n\n.version-option .version-date {\n flex: 1;\n color: var(--mj-text-muted);\n font-size: 12px;\n}\n\n.version-option i.fa-check {\n color: var(--mj-status-success);\n margin-left: auto;\n}\n\n.feedback-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.feedback-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-brand-primary);\n}\n\n.feedback-btn i {\n font-size: 14px;\n}\n\n.save-to-collection-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.save-to-collection-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n}\n\n.save-to-collection-btn i {\n font-size: 14px;\n}\n\n.save-to-collection-btn.in-collection {\n color: var(--mj-brand-primary);\n}\n\n.save-to-collection-btn.in-collection:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.share-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.share-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-brand-primary);\n}\n\n.share-btn i {\n font-size: 14px;\n}\n\n.maximize-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.maximize-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n.maximize-btn i {\n font-size: 14px;\n}\n\n.close-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.close-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n.close-btn i {\n font-size: 14px;\n}\n\n.panel-content {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n}\n\n.artifact-content-wrapper {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n min-height: 0;\n width: 100%;\n box-sizing: border-box;\n}\n\n.loading-state,\n.error-state {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 40px 20px;\n color: var(--mj-text-muted);\n}\n\n.error-state {\n color: var(--mj-status-error);\n}\n\n.loading-state i {\n font-size: 24px;\n}\n\n.artifact-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 16px;\n margin-bottom: 16px;\n padding: 16px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n flex-shrink: 0;\n}\n\n.meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.meta-item label {\n font-size: 12px;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.meta-item span {\n font-size: 14px;\n color: var(--mj-text-primary);\n}\n\n.artifact-description {\n padding: 12px;\n margin-bottom: 16px;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border-left: 3px solid var(--mj-brand-primary);\n border-radius: 4px;\n color: var(--mj-brand-primary);\n font-size: 14px;\n flex-shrink: 0;\n}\n\n.json-viewer {\n flex: 1;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n min-height: 0;\n}\n\n.json-editor-container {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n}\n\n.json-toolbar {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.btn-icon {\n display: flex;\n align-items: center;\n gap: 6px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-radius: 4px;\n padding: 6px 12px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 14px;\n transition: all 0.2s;\n}\n\n.btn-icon:hover {\n background: var(--mj-bg-surface-sunken);\n border-color: var(--mj-text-disabled);\n}\n\n.btn-icon.btn-primary {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.btn-icon.btn-primary:hover {\n background: var(--mj-brand-primary-hover);\n border-color: var(--mj-brand-primary-hover);\n}\n\n.btn-icon i {\n font-size: 14px;\n}\n\n/* Library Dialog Styles */\n.modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n}\n\n.modal-content {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n max-width: 500px;\n width: 90%;\n max-height: 90vh;\n display: flex;\n flex-direction: column;\n}\n\n.modal-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.modal-header h3 {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.modal-header h3 i {\n color: var(--mj-status-warning);\n}\n\n.modal-body {\n padding: 20px;\n overflow-y: auto;\n}\n\n.modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.form-section {\n margin-bottom: 20px;\n}\n\n.form-section label {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.form-select,\n.form-input {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid var(--mj-border-strong);\n border-radius: 6px;\n font-size: 14px;\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface);\n transition: border-color 0.2s;\n}\n\n.form-select:focus,\n.form-input:focus {\n outline: none;\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}\n\n.divider {\n display: flex;\n align-items: center;\n margin: 24px 0;\n color: var(--mj-text-disabled);\n font-size: 12px;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.divider::before,\n.divider::after {\n content: '';\n flex: 1;\n height: 1px;\n background: var(--mj-border-default);\n}\n\n.divider span {\n padding: 0 12px;\n}\n\n.create-collection-row {\n display: flex;\n gap: 8px;\n}\n\n.create-collection-row .form-input {\n flex: 1;\n}\n\n.btn {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n white-space: nowrap;\n}\n\n.btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.btn-primary {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.btn-primary:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.btn-secondary {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n color: var(--mj-text-secondary);\n}\n\n.btn-secondary:hover:not(:disabled) {\n background: var(--mj-bg-surface-sunken);\n}\n\n/* Tab Navigation */\n.tab-navigation {\n display: flex;\n gap: 0;\n border-bottom: 2px solid var(--mj-border-default);\n padding: 0 16px;\n background: var(--mj-bg-surface);\n}\n\n.tab-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 12px 20px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n margin-bottom: -2px;\n color: var(--mj-text-muted);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 150ms ease;\n}\n\n.tab-btn:hover {\n color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.tab-btn.active {\n color: var(--mj-brand-primary);\n border-bottom-color: var(--mj-brand-primary);\n}\n\n.tab-btn i {\n font-size: 14px;\n}\n\n/* Tab Content */\n.tab-content {\n flex: 1;\n overflow: auto;\n width: 100%;\n box-sizing: border-box;\n margin-bottom: 5px;\n min-width: 0;\n min-height: 0;\n}\n\n/* Plugin Container */\n.plugin-container {\n width: 100%;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.plugin-container.plugin-hidden,\n.tab-hidden {\n display: none;\n}\n\n/* Ensure artifact viewer component takes full height */\n.plugin-container mj-artifact-type-plugin-viewer {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n width: 100%;\n}\n\n.display-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n box-sizing: border-box;\n overflow: hidden;\n min-height: 0;\n min-width: 0;\n padding: 7px;\n}\n\n.display-toolbar {\n position: absolute;\n top: 12px;\n right: 12px;\n display: flex;\n gap: 8px;\n z-index: 10;\n}\n\n.markdown-content,\n.html-content {\n flex: 1;\n font-size: 14px;\n line-height: 1.6;\n width: 100%;\n padding: 20px 10px 5px 20px; /* top right bottom left */\n box-sizing: border-box;\n overflow: auto;\n min-height: 0;\n}\n\n.details-content {\n padding: 20px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.details-content .artifact-meta {\n margin-bottom: 0;\n padding: 20px;\n}\n\n.details-content .meta-item.full-width {\n grid-column: 1 / -1;\n}\n\n.attributes-section {\n margin-top: 24px;\n padding: 24px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n}\n\n.attributes-section h4 {\n margin: 0 0 16px 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.attributes-list {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n gap: 16px;\n}\n\n.attribute-item {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px;\n background: var(--mj-bg-surface);\n border-radius: 6px;\n border: 1px solid var(--mj-border-default);\n}\n\n.attribute-item label {\n font-size: 11px;\n font-weight: 600;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.attribute-item span {\n font-size: 13px;\n color: var(--mj-text-primary);\n word-break: break-word;\n}\n\n.attribute-empty-pill {\n display: inline-block;\n padding: 2px 8px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-disabled);\n font-size: 11px;\n font-weight: 500;\n border-radius: 4px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* Links Tab */\n.links-container {\n padding: 20px;\n}\n\n.links-section {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.link-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n transition: all 0.15s;\n}\n\n.link-item.clickable {\n cursor: pointer;\n}\n\n.link-item.clickable:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n background: var(--mj-bg-surface-sunken);\n}\n\n.link-item.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.link-item:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.link-icon {\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface);\n border-radius: 8px;\n flex-shrink: 0;\n}\n\n.link-icon i {\n font-size: 18px;\n color: var(--mj-brand-primary);\n}\n\n.link-content {\n flex: 1;\n min-width: 0;\n}\n\n.link-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.link-type {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin-top: 2px;\n}\n\n.link-actions {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n}\n\n.link-actions i {\n font-size: 14px;\n color: var(--mj-text-muted);\n transition: all 0.15s;\n}\n\n.link-item.clickable:hover .link-actions i {\n color: var(--mj-brand-primary);\n transform: translateX(2px);\n}\n\n.links-container .empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n color: var(--mj-text-disabled);\n text-align: center;\n}\n\n.links-container .empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.3;\n}\n\n.links-container .empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Dynamic Tab Content Styles */\n.dynamic-tab-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 16px;\n}\n\n/* Custom component tab content */\n.component-tab-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: auto;\n padding: 16px;\n}\n\n.markdown-viewer {\n flex: 1;\n overflow: auto;\n padding: 20px;\n background: var(--mj-bg-surface);\n line-height: 1.6;\n}\n\n.markdown-viewer h1, .markdown-viewer h2, .markdown-viewer h3 {\n margin-top: 24px;\n margin-bottom: 12px;\n font-weight: 600;\n}\n\n.markdown-viewer h1 { font-size: 24px; }\n.markdown-viewer h2 { font-size: 20px; }\n.markdown-viewer h3 { font-size: 18px; }\n\n.markdown-viewer p {\n margin-bottom: 12px;\n}\n\n.markdown-viewer ul, .markdown-viewer ol {\n margin-bottom: 12px;\n padding-left: 24px;\n}\n\n.markdown-viewer code {\n background: var(--mj-bg-surface-sunken);\n padding: 2px 6px;\n border-radius: 3px;\n font-family: 'Courier New', monospace;\n font-size: 13px;\n}\n\n.markdown-viewer pre {\n background: var(--mj-bg-surface-sunken);\n padding: 12px;\n border-radius: 6px;\n overflow-x: auto;\n margin-bottom: 12px;\n}\n\n.markdown-viewer pre code {\n background: none;\n padding: 0;\n}\n\n.code-viewer {\n flex: 1;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n min-height: 0;\n}\n\n.code-toolbar {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.code-editor-container {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n}\n\n.plaintext-viewer {\n flex: 1;\n overflow: auto;\n padding: 12px;\n background: var(--mj-bg-surface-sunken);\n font-family: 'Courier New', monospace;\n font-size: 13px;\n white-space: pre-wrap;\n}\n\n.html-viewer {\n flex: 1;\n overflow: auto;\n padding: 20px;\n background: var(--mj-bg-surface);\n}\n\n/* Mobile adjustments: 481px - 768px */\n@media (max-width: 768px) {\n /* Add top border with slight visual separation from top nav */\n .artifact-viewer-panel {\n border-top: 3px solid var(--mj-brand-primary);\n position: relative;\n }\n\n /* Create a small dark strip above the blue border to separate from top nav */\n .artifact-viewer-panel::before {\n content: '';\n position: absolute;\n top: -8px;\n left: 0;\n right: 0;\n height: 8px;\n background: var(--mj-brand-secondary);\n }\n\n .tab-navigation {\n padding: 0 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n }\n\n .tab-navigation::-webkit-scrollbar {\n display: none;\n }\n\n .tab-btn {\n padding: 10px 12px;\n font-size: 12px;\n white-space: nowrap;\n flex-shrink: 0;\n }\n\n .tab-btn i {\n font-size: 13px;\n }\n\n .panel-header {\n padding: 12px;\n }\n\n .dynamic-tab-content {\n padding: 12px;\n }\n\n .markdown-viewer {\n padding: 12px;\n }\n}\n\n/* Small Phone adjustments: <= 480px */\n@media (max-width: 480px) {\n .tab-navigation {\n padding: 0 4px;\n }\n\n .tab-btn {\n padding: 8px 10px;\n font-size: 11px;\n gap: 4px;\n }\n\n .tab-btn i {\n font-size: 12px;\n }\n\n .panel-header {\n padding: 8px;\n }\n\n .dynamic-tab-content {\n padding: 8px;\n }\n\n .markdown-viewer {\n padding: 8px;\n }\n}\n"] }]
1640
+ args: [{ standalone: false, selector: 'mj-artifact-viewer-panel', template: "<div class=\"artifact-viewer-panel\" [class.no-header]=\"!showHeader\">\n @if (showHeader) {\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <h3>\n <i class=\"fas\" [ngClass]=\"artifactIcon\"></i>\n {{ displayName }}\n </h3>\n @if (displayDescription) {\n <p class=\"header-description\">{{ displayDescription }}</p>\n }\n </div>\n <div class=\"panel-header-right\">\n <div class=\"version-selector\"\n [class.clickable]=\"allVersions.length > 1 && viewContext !== 'collection'\"\n (click)=\"viewContext !== 'collection' && toggleVersionDropdown()\">\n <i class=\"fas fa-history\"></i>\n <span class=\"version-label\">v{{ selectedVersionNumber }}</span>\n @if (allVersions.length > 1 && viewContext !== 'collection') {\n <i class=\"fas fa-chevron-down dropdown-icon\" [class.open]=\"showVersionDropdown\"></i>\n }\n @if (showVersionDropdown) {\n <div class=\"version-dropdown\" (click)=\"$event.stopPropagation()\">\n @for (version of allVersions; track version.ID) {\n <div class=\"version-option\"\n [class.selected]=\"version.VersionNumber === selectedVersionNumber\"\n (click)=\"selectVersion(version); $event.stopPropagation()\">\n <span class=\"version-number\">v{{ version.VersionNumber }}</span>\n <span class=\"version-date\">{{ version.__mj_CreatedAt | date:'short' }}</span>\n @if (version.VersionNumber === selectedVersionNumber) {\n <i class=\"fas fa-check\"></i>\n }\n </div>\n }\n </div>\n }\n </div>\n @if (pluginViewer?.SupportsFeedback) {\n <button class=\"feedback-btn\"\n (click)=\"pluginViewer?.AskUserForFeedback()\"\n title=\"Inspect & rate components\">\n <i class=\"fas fa-star-half-stroke\"></i>\n </button>\n }\n @if (showSaveToCollection) {\n <button class=\"save-to-collection-btn\"\n [class.in-collection]=\"isInCollection\"\n (click)=\"onSaveToLibrary()\"\n [title]=\"isInCollection ? 'Current version saved to ' + currentVersionCollections.length + ' collection(s)' : 'Save to Collection'\">\n <i [class]=\"isInCollection ? 'fas fa-bookmark' : 'far fa-bookmark'\"></i>\n </button>\n }\n @if (canShare) {\n <button class=\"share-btn\"\n (click)=\"onShare()\"\n title=\"Share\">\n <i class=\"fas fa-share-nodes\"></i>\n </button>\n }\n @if (showMaximizeButton) {\n <button class=\"maximize-btn\"\n (click)=\"onMaximizeToggle()\"\n [title]=\"isMaximized ? 'Restore Width' : 'Maximize Width'\">\n <i class=\"fas\" [ngClass]=\"isMaximized ? 'fa-compress-arrows-alt' : 'fa-arrows-left-right'\"></i>\n </button>\n }\n @if (showCloseButton) {\n <button class=\"close-btn\" (click)=\"onClose()\" title=\"Close\">\n <i class=\"fas fa-times\"></i>\n </button>\n }\n </div>\n </div>\n }\n\n <div class=\"panel-content\">\n @if (isLoading) {\n <div class=\"loading-state\">\n <i class=\"fas fa-spinner fa-spin\"></i>\n <span>Loading artifact...</span>\n </div>\n }\n\n @if (error) {\n <div class=\"error-state\">\n <i class=\"fas fa-exclamation-triangle\"></i>\n <span>{{ error }}</span>\n </div>\n }\n\n @if (!isLoading && !error && artifact) {\n <div class=\"artifact-content-wrapper\" [class.no-tabs]=\"!showTabs\">\n <!-- Tab Navigation (Dynamic) - Hidden when showTabs is false -->\n @if (showTabs) {\n <div class=\"tab-navigation\">\n @for (tab of allTabs; track tab) {\n <button class=\"tab-btn\"\n [class.active]=\"activeTab === tab.toLowerCase()\"\n (click)=\"SetActiveTab(tab)\">\n @if (GetTabIcon(tab)) {\n <i [class]=\"GetTabIcon(tab)!\"></i>\n }\n {{ tab }}\n </button>\n }\n </div>\n }\n\n <!-- Tab Content (Dynamic) -->\n <div class=\"tab-content\">\n <!-- Plugin Viewer - Created once, kept alive, shown/hidden with CSS -->\n @if (hasPlugin && artifactVersion && artifactTypeName) {\n <div class=\"plugin-container\" [class.plugin-hidden]=\"activeTab !== 'display'\">\n <mj-artifact-type-plugin-viewer\n [artifactVersion]=\"artifactVersion\"\n [artifactTypeName]=\"artifactTypeName\"\n [contentType]=\"contentType\"\n [readonly]=\"true\"\n (openEntityRecord)=\"onOpenEntityRecord($event)\"\n (navigationRequest)=\"onNavigationRequest($event)\"\n (pluginLoaded)=\"onPluginLoaded()\"\n (tabsChanged)=\"onTabsChanged()\">\n </mj-artifact-type-plugin-viewer>\n </div>\n }\n\n <!-- Display Tab - Fallback content when no plugin -->\n @if (activeTab === 'display' && (!hasPlugin || !artifactVersion)) {\n <div class=\"display-content\">\n @if (displayMarkdown || displayHtml) {\n <!-- Fallback to extracted markdown/HTML attributes -->\n <div class=\"display-toolbar\">\n <button class=\"btn-icon\" title=\"Print\" (click)=\"onPrintDisplayContent()\">\n <i class=\"fas fa-print\"></i> Print\n </button>\n <button class=\"btn-icon\" title=\"Copy Content\" (click)=\"onCopyDisplayContent()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n @if (displayMarkdown) {\n <div class=\"markdown-content\">\n <mj-markdown [data]=\"displayMarkdown\"\n [enableCollapsibleHeadings]=\"true\"\n [enableLineNumbers]=\"true\"\n [enableSmartypants]=\"true\"\n [enableHtml]=\"true\"></mj-markdown>\n </div>\n } @else if (displayHtml) {\n <div class=\"html-content\" [innerHTML]=\"displayHtml\"></div>\n }\n }\n </div>\n }\n\n <!-- Details Tab - Artifact metadata (always in DOM, hidden with CSS) -->\n <div class=\"details-content\" [class.tab-hidden]=\"activeTab !== 'details'\">\n <div class=\"artifact-meta\">\n <div class=\"meta-item\">\n <label>Type</label>\n <span>{{ artifact.Type }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Version</label>\n <span>{{ artifactVersion?.VersionNumber || 1 }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Created</label>\n <span>{{ artifact.__mj_CreatedAt | date:'short' }}</span>\n </div>\n <div class=\"meta-item\">\n <label>Updated</label>\n <span>{{ artifactVersion?.__mj_UpdatedAt | date:'short' }}</span>\n </div>\n @if (artifact.Description) {\n <div class=\"meta-item full-width\">\n <label>Artifact Description</label>\n <span>{{ artifact.Description }}</span>\n </div>\n }\n @if (artifactVersion?.Description && artifact.Description && artifactVersion?.Description !== artifact.Description) {\n <div class=\"meta-item full-width\">\n <label>Version Description</label>\n <span>{{ artifactVersion?.Description }}</span>\n </div>\n }\n </div>\n\n <!-- Version Attributes -->\n @if (filteredAttributes.length > 0) {\n <div class=\"attributes-section\">\n <h4>Version Attributes</h4>\n <div class=\"attributes-list\">\n @for (attr of filteredAttributes; track attr.ID) {\n <div class=\"attribute-item\">\n <label>{{ attr.Name }}</label>\n @if (attr.Value) {\n <span>{{ attr.Value }}</span>\n } @else {\n <span class=\"attribute-empty-pill\">Empty</span>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Links Tab - Conversations and Collections (always in DOM, hidden with CSS) -->\n <div class=\"links-container\" [class.tab-hidden]=\"activeTab !== 'links'\">\n @if (linksToShow.length > 0) {\n <div class=\"links-section\">\n @for (link of linksToShow; track link.id) {\n <div class=\"link-item\"\n [class.disabled]=\"!link.hasAccess\"\n [class.clickable]=\"link.hasAccess\"\n (click)=\"link.hasAccess ? onNavigateToLink(link) : null\"\n [title]=\"link.hasAccess ? 'Click to open' : 'No access'\">\n <div class=\"link-icon\">\n @if (link.type === 'conversation') {\n <i class=\"fas fa-comments\"></i>\n } @else {\n <i class=\"fas fa-folder\"></i>\n }\n </div>\n <div class=\"link-content\">\n <div class=\"link-name\">{{ link.name }}</div>\n <div class=\"link-type\">{{ link.type === 'conversation' ? 'Conversation' : 'Collection' }}</div>\n </div>\n <div class=\"link-actions\">\n <i class=\"fas fa-arrow-right\"></i>\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <i class=\"fas fa-link\"></i>\n <p>No links available</p>\n </div>\n }\n </div>\n\n <!-- Dynamic Plugin Tabs and Base Tabs (JSON, etc.) -->\n @if (activeTab !== 'display' && activeTab !== 'details' && activeTab !== 'links') {\n <!-- Check if this is a component tab -->\n @if (GetComponentTabType(activeTab); as componentType) {\n <div class=\"component-tab-content\">\n <ng-container *ngComponentOutlet=\"componentType; inputs: GetComponentInputs(activeTab)\"></ng-container>\n </div>\n } @else {\n @if (GetTabContent(activeTab); as tabData) {\n <div class=\"dynamic-tab-content\">\n @switch (tabData.type) {\n @case ('markdown') {\n <div class=\"markdown-viewer\">\n <mj-markdown [data]=\"tabData.content\"\n [enableCollapsibleHeadings]=\"true\"\n [enableLineNumbers]=\"true\"\n [enableSmartypants]=\"true\"\n [enableHtml]=\"true\"></mj-markdown>\n </div>\n }\n @case ('json') {\n <div class=\"json-viewer\">\n <div class=\"json-toolbar\">\n <button class=\"btn-icon\" title=\"Copy JSON\" (click)=\"onCopyToClipboard()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n <div class=\"json-editor-container\">\n <mj-code-editor\n [value]=\"tabData.content\"\n [language]=\"'json'\"\n [readonly]=\"true\"\n [lineWrapping]=\"true\"\n style=\"width: 100%; height: 100%;\">\n </mj-code-editor>\n </div>\n </div>\n }\n @case ('code') {\n <div class=\"code-viewer\">\n <div class=\"code-toolbar\">\n <button class=\"btn-icon\" title=\"Copy Code\" (click)=\"onCopyToClipboard()\">\n <i class=\"fas fa-copy\"></i> Copy\n </button>\n </div>\n <div class=\"code-editor-container\">\n <mj-code-editor\n [value]=\"tabData.content\"\n [language]=\"tabData.language || 'typescript'\"\n [readonly]=\"true\"\n [lineWrapping]=\"true\"\n style=\"width: 100%; height: 100%;\">\n </mj-code-editor>\n </div>\n </div>\n }\n @case ('html') {\n <div class=\"html-viewer\" [innerHTML]=\"tabData.content\"></div>\n }\n @case ('plaintext') {\n <pre class=\"plaintext-viewer\">{{ tabData.content }}</pre>\n }\n }\n </div>\n }\n }\n }\n </div>\n </div>\n }\n </div>\n</div>\n\n", styles: [".artifact-viewer-panel {\n width: 100%;\n height: 100%;\n background: var(--mj-bg-surface);\n border-left: 1px solid var(--mj-border-default);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.panel-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n padding: 12px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n width: 100%;\n box-sizing: border-box;\n}\n\n.panel-header-left {\n flex: 1;\n min-width: 0;\n margin-right: 16px;\n}\n\n.panel-header h3 {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n word-wrap: break-word;\n overflow-wrap: break-word;\n}\n\n.panel-header h3 i {\n color: var(--mj-brand-primary);\n flex-shrink: 0;\n margin-top: 2px;\n}\n\n.header-description {\n margin: 4px 0 0 0;\n font-size: 12px;\n color: var(--mj-text-muted);\n font-weight: normal;\n word-wrap: break-word;\n overflow-wrap: break-word;\n}\n\n.panel-header-right {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n}\n\n.version-selector {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 12px;\n font-size: 12px;\n color: var(--mj-text-muted);\n font-weight: 500;\n transition: all 0.2s;\n}\n\n.version-selector.clickable {\n cursor: pointer;\n}\n\n.version-selector.clickable:hover {\n background: var(--mj-border-default);\n}\n\n.version-selector i {\n font-size: 11px;\n}\n\n.version-selector .dropdown-icon {\n margin-left: 2px;\n transition: transform 0.2s;\n}\n\n.version-selector .dropdown-icon.open {\n transform: rotate(180deg);\n}\n\n.version-label {\n font-family: monospace;\n}\n\n.version-dropdown {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n min-width: 200px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-md);\n z-index: 1000;\n overflow: hidden;\n}\n\n.version-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n cursor: pointer;\n transition: background 0.15s;\n font-size: 13px;\n}\n\n.version-option:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.version-option.selected {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.version-option .version-number {\n font-family: monospace;\n font-weight: 600;\n min-width: 35px;\n}\n\n.version-option .version-date {\n flex: 1;\n color: var(--mj-text-muted);\n font-size: 12px;\n}\n\n.version-option i.fa-check {\n color: var(--mj-status-success);\n margin-left: auto;\n}\n\n.feedback-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.feedback-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-brand-primary);\n}\n\n.feedback-btn i {\n font-size: 14px;\n}\n\n.save-to-collection-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.save-to-collection-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n}\n\n.save-to-collection-btn i {\n font-size: 14px;\n}\n\n.save-to-collection-btn.in-collection {\n color: var(--mj-brand-primary);\n}\n\n.save-to-collection-btn.in-collection:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.share-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-disabled);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.share-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-brand-primary);\n}\n\n.share-btn i {\n font-size: 14px;\n}\n\n.maximize-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.maximize-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n.maximize-btn i {\n font-size: 14px;\n}\n\n.close-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--mj-text-muted);\n padding: 6px;\n border-radius: 4px;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n}\n\n.close-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n.close-btn i {\n font-size: 14px;\n}\n\n.panel-content {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n display: flex;\n flex-direction: column;\n width: 100%;\n box-sizing: border-box;\n}\n\n.artifact-content-wrapper {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n min-height: 0;\n width: 100%;\n box-sizing: border-box;\n}\n\n.loading-state,\n.error-state {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 40px 20px;\n color: var(--mj-text-muted);\n}\n\n.error-state {\n color: var(--mj-status-error);\n}\n\n.loading-state i {\n font-size: 24px;\n}\n\n.artifact-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 16px;\n margin-bottom: 16px;\n padding: 16px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n flex-shrink: 0;\n}\n\n.meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.meta-item label {\n font-size: 12px;\n font-weight: 500;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.meta-item span {\n font-size: 14px;\n color: var(--mj-text-primary);\n}\n\n.artifact-description {\n padding: 12px;\n margin-bottom: 16px;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border-left: 3px solid var(--mj-brand-primary);\n border-radius: 4px;\n color: var(--mj-brand-primary);\n font-size: 14px;\n flex-shrink: 0;\n}\n\n.json-viewer {\n flex: 1;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n min-height: 0;\n}\n\n.json-editor-container {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n}\n\n.json-toolbar {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.btn-icon {\n display: flex;\n align-items: center;\n gap: 6px;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n border-radius: 4px;\n padding: 6px 12px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 14px;\n transition: all 0.2s;\n}\n\n.btn-icon:hover {\n background: var(--mj-bg-surface-sunken);\n border-color: var(--mj-text-disabled);\n}\n\n.btn-icon.btn-primary {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.btn-icon.btn-primary:hover {\n background: var(--mj-brand-primary-hover);\n border-color: var(--mj-brand-primary-hover);\n}\n\n.btn-icon i {\n font-size: 14px;\n}\n\n/* Library Dialog Styles */\n.modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: var(--mj-bg-overlay);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n}\n\n.modal-content {\n background: var(--mj-bg-surface);\n border-radius: 12px;\n box-shadow: var(--mj-shadow-lg);\n max-width: 500px;\n width: 90%;\n max-height: 90vh;\n display: flex;\n flex-direction: column;\n}\n\n.modal-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.modal-header h3 {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.modal-header h3 i {\n color: var(--mj-status-warning);\n}\n\n.modal-body {\n padding: 20px;\n overflow-y: auto;\n}\n\n.modal-footer {\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n padding: 16px 20px;\n border-top: 1px solid var(--mj-border-default);\n}\n\n.form-section {\n margin-bottom: 20px;\n}\n\n.form-section label {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-secondary);\n}\n\n.form-select,\n.form-input {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid var(--mj-border-strong);\n border-radius: 6px;\n font-size: 14px;\n color: var(--mj-text-primary);\n background: var(--mj-bg-surface);\n transition: border-color 0.2s;\n}\n\n.form-select:focus,\n.form-input:focus {\n outline: none;\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}\n\n.divider {\n display: flex;\n align-items: center;\n margin: 24px 0;\n color: var(--mj-text-disabled);\n font-size: 12px;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.divider::before,\n.divider::after {\n content: '';\n flex: 1;\n height: 1px;\n background: var(--mj-border-default);\n}\n\n.divider span {\n padding: 0 12px;\n}\n\n.create-collection-row {\n display: flex;\n gap: 8px;\n}\n\n.create-collection-row .form-input {\n flex: 1;\n}\n\n.btn {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n white-space: nowrap;\n}\n\n.btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.btn-primary {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.btn-primary:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n}\n\n.btn-secondary {\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-strong);\n color: var(--mj-text-secondary);\n}\n\n.btn-secondary:hover:not(:disabled) {\n background: var(--mj-bg-surface-sunken);\n}\n\n/* Tab Navigation */\n.tab-navigation {\n display: flex;\n gap: 0;\n border-bottom: 2px solid var(--mj-border-default);\n padding: 0 16px;\n background: var(--mj-bg-surface);\n}\n\n.tab-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 12px 20px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n margin-bottom: -2px;\n color: var(--mj-text-muted);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 150ms ease;\n}\n\n.tab-btn:hover {\n color: var(--mj-brand-primary);\n background: var(--mj-bg-surface-sunken);\n}\n\n.tab-btn.active {\n color: var(--mj-brand-primary);\n border-bottom-color: var(--mj-brand-primary);\n}\n\n.tab-btn i {\n font-size: 14px;\n}\n\n/* Tab Content */\n.tab-content {\n flex: 1;\n overflow: auto;\n width: 100%;\n box-sizing: border-box;\n margin-bottom: 5px;\n min-width: 0;\n min-height: 0;\n}\n\n/* Plugin Container */\n.plugin-container {\n width: 100%;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.plugin-container.plugin-hidden,\n.tab-hidden {\n display: none;\n}\n\n/* Ensure artifact viewer component takes full height */\n.plugin-container mj-artifact-type-plugin-viewer {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n width: 100%;\n}\n\n.display-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n box-sizing: border-box;\n overflow: hidden;\n min-height: 0;\n min-width: 0;\n padding: 7px;\n}\n\n.display-toolbar {\n position: absolute;\n top: 12px;\n right: 12px;\n display: flex;\n gap: 8px;\n z-index: 10;\n}\n\n.markdown-content,\n.html-content {\n flex: 1;\n font-size: 14px;\n line-height: 1.6;\n width: 100%;\n padding: 20px 10px 5px 20px; /* top right bottom left */\n box-sizing: border-box;\n overflow: auto;\n min-height: 0;\n}\n\n.details-content {\n padding: 20px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.details-content .artifact-meta {\n margin-bottom: 0;\n padding: 20px;\n}\n\n.details-content .meta-item.full-width {\n grid-column: 1 / -1;\n}\n\n.attributes-section {\n margin-top: 24px;\n padding: 24px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 8px;\n}\n\n.attributes-section h4 {\n margin: 0 0 16px 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.attributes-list {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n gap: 16px;\n}\n\n.attribute-item {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px;\n background: var(--mj-bg-surface);\n border-radius: 6px;\n border: 1px solid var(--mj-border-default);\n}\n\n.attribute-item label {\n font-size: 11px;\n font-weight: 600;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.attribute-item span {\n font-size: 13px;\n color: var(--mj-text-primary);\n word-break: break-word;\n}\n\n.attribute-empty-pill {\n display: inline-block;\n padding: 2px 8px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-disabled);\n font-size: 11px;\n font-weight: 500;\n border-radius: 4px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* Links Tab */\n.links-container {\n padding: 20px;\n}\n\n.links-section {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.link-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n transition: all 0.15s;\n}\n\n.link-item.clickable {\n cursor: pointer;\n}\n\n.link-item.clickable:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n background: var(--mj-bg-surface-sunken);\n}\n\n.link-item.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.link-item:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-sm);\n}\n\n.link-icon {\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface);\n border-radius: 8px;\n flex-shrink: 0;\n}\n\n.link-icon i {\n font-size: 18px;\n color: var(--mj-brand-primary);\n}\n\n.link-content {\n flex: 1;\n min-width: 0;\n}\n\n.link-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--mj-text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.link-type {\n font-size: 12px;\n color: var(--mj-text-muted);\n margin-top: 2px;\n}\n\n.link-actions {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n}\n\n.link-actions i {\n font-size: 14px;\n color: var(--mj-text-muted);\n transition: all 0.15s;\n}\n\n.link-item.clickable:hover .link-actions i {\n color: var(--mj-brand-primary);\n transform: translateX(2px);\n}\n\n.links-container .empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n color: var(--mj-text-disabled);\n text-align: center;\n}\n\n.links-container .empty-state i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.3;\n}\n\n.links-container .empty-state p {\n margin: 0;\n font-size: 14px;\n}\n\n/* Dynamic Tab Content Styles */\n.dynamic-tab-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 16px;\n}\n\n/* Custom component tab content */\n.component-tab-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: auto;\n padding: 16px;\n}\n\n.markdown-viewer {\n flex: 1;\n overflow: auto;\n padding: 20px;\n background: var(--mj-bg-surface);\n line-height: 1.6;\n}\n\n.markdown-viewer h1, .markdown-viewer h2, .markdown-viewer h3 {\n margin-top: 24px;\n margin-bottom: 12px;\n font-weight: 600;\n}\n\n.markdown-viewer h1 { font-size: 24px; }\n.markdown-viewer h2 { font-size: 20px; }\n.markdown-viewer h3 { font-size: 18px; }\n\n.markdown-viewer p {\n margin-bottom: 12px;\n}\n\n.markdown-viewer ul, .markdown-viewer ol {\n margin-bottom: 12px;\n padding-left: 24px;\n}\n\n.markdown-viewer code {\n background: var(--mj-bg-surface-sunken);\n padding: 2px 6px;\n border-radius: 3px;\n font-family: 'Courier New', monospace;\n font-size: 13px;\n}\n\n.markdown-viewer pre {\n background: var(--mj-bg-surface-sunken);\n padding: 12px;\n border-radius: 6px;\n overflow-x: auto;\n margin-bottom: 12px;\n}\n\n.markdown-viewer pre code {\n background: none;\n padding: 0;\n}\n\n.code-viewer {\n flex: 1;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n overflow: hidden;\n min-height: 0;\n}\n\n.code-toolbar {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.code-editor-container {\n flex: 1;\n overflow: hidden;\n min-height: 0;\n}\n\n.plaintext-viewer {\n flex: 1;\n overflow: auto;\n padding: 12px;\n background: var(--mj-bg-surface-sunken);\n font-family: 'Courier New', monospace;\n font-size: 13px;\n white-space: pre-wrap;\n}\n\n.html-viewer {\n flex: 1;\n overflow: auto;\n padding: 20px;\n background: var(--mj-bg-surface);\n}\n\n/* Mobile adjustments: 481px - 768px */\n@media (max-width: 768px) {\n /* Add top border with slight visual separation from top nav */\n .artifact-viewer-panel {\n border-top: 3px solid var(--mj-brand-primary);\n position: relative;\n }\n\n /* Create a small dark strip above the blue border to separate from top nav */\n .artifact-viewer-panel::before {\n content: '';\n position: absolute;\n top: -8px;\n left: 0;\n right: 0;\n height: 8px;\n background: var(--mj-brand-secondary);\n }\n\n .tab-navigation {\n padding: 0 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n }\n\n .tab-navigation::-webkit-scrollbar {\n display: none;\n }\n\n .tab-btn {\n padding: 10px 12px;\n font-size: 12px;\n white-space: nowrap;\n flex-shrink: 0;\n }\n\n .tab-btn i {\n font-size: 13px;\n }\n\n .panel-header {\n padding: 12px;\n }\n\n .dynamic-tab-content {\n padding: 12px;\n }\n\n .markdown-viewer {\n padding: 12px;\n }\n}\n\n/* Small Phone adjustments: <= 480px */\n@media (max-width: 480px) {\n .tab-navigation {\n padding: 0 4px;\n }\n\n .tab-btn {\n padding: 8px 10px;\n font-size: 11px;\n gap: 4px;\n }\n\n .tab-btn i {\n font-size: 12px;\n }\n\n .panel-header {\n padding: 8px;\n }\n\n .dynamic-tab-content {\n padding: 8px;\n }\n\n .markdown-viewer {\n padding: 8px;\n }\n}\n"] }]
1550
1641
  }], () => [{ type: i0.ChangeDetectorRef }, { type: i1.MJNotificationService }, { type: i2.DomSanitizer }, { type: i3.ArtifactIconService }], { artifactId: [{
1551
1642
  type: Input
1552
1643
  }], currentUser: [{