hale-commenting-system 2.2.97 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hale-commenting-system",
3
- "version": "2.2.97",
3
+ "version": "3.0.0",
4
4
  "description": "A commenting system for PatternFly React applications that allows designers and developers to add comments directly on design pages, sync with GitHub Issues, and link Jira tickets.",
5
5
  "repository": "https://github.com/patternfly/patternfly-react-seed.git",
6
6
  "homepage": "https://www.npmjs.com/package/hale-commenting-system",
@@ -71,19 +71,20 @@
71
71
  "webpack-dev-server": "^5.2.1",
72
72
  "webpack-merge": "^6.0.1"
73
73
  },
74
+ "peerDependencies": {
75
+ "@patternfly/react-core": "^6.0.0",
76
+ "@patternfly/react-icons": "^6.0.0",
77
+ "react": "^18.0.0",
78
+ "react-dom": "^18.0.0",
79
+ "react-router-dom": "^7.0.0"
80
+ },
74
81
  "dependencies": {
75
82
  "@babel/generator": "^7.23.0",
76
83
  "@babel/parser": "^7.23.0",
77
84
  "@babel/traverse": "^7.23.0",
78
85
  "@babel/types": "^7.23.0",
79
- "@patternfly/react-core": "^6.4.0",
80
- "@patternfly/react-icons": "^6.4.0",
81
- "@patternfly/react-styles": "^6.4.0",
82
86
  "inquirer": "^8.2.6",
83
- "node-fetch": "^2.7.0",
84
- "react": "^18",
85
- "react-dom": "^18",
86
- "sirv-cli": "^3.0.0"
87
+ "node-fetch": "^2.7.0"
87
88
  },
88
89
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
89
90
  }
@@ -512,41 +512,6 @@ JIRA_API_TOKEN=
512
512
  }
513
513
  }
514
514
 
515
- function createCommentsComponent() {
516
- const cwd = process.cwd();
517
- const commentsDir = path.join(cwd, 'src', 'app', 'Comments');
518
- const commentsFile = path.join(commentsDir, 'Comments.tsx');
519
-
520
- // Check if already exists
521
- if (fs.existsSync(commentsFile)) {
522
- return; // Already exists, skip
523
- }
524
-
525
- // Create directory if it doesn't exist
526
- if (!fs.existsSync(commentsDir)) {
527
- fs.mkdirSync(commentsDir, { recursive: true });
528
- }
529
-
530
- // Read the Comments component from the package and modify the import
531
- // The file is in the package at src/app/Comments/Comments.tsx
532
- const scriptDir = __dirname || path.dirname(require.resolve('./integrate.js'));
533
- const packageCommentsFile = path.join(scriptDir, '..', 'src', 'app', 'Comments', 'Comments.tsx');
534
-
535
- let commentsComponentContent;
536
- if (fs.existsSync(packageCommentsFile)) {
537
- // Read from package and replace import path
538
- commentsComponentContent = fs.readFileSync(packageCommentsFile, 'utf8')
539
- .replace(/from ['"]@app\/commenting-system['"]/g, "from 'hale-commenting-system'");
540
- } else {
541
- // Fallback: create a minimal version (shouldn't happen if package is properly built)
542
- console.log(' ⚠️ Comments component not found in package, skipping creation');
543
- return;
544
- }
545
-
546
- fs.writeFileSync(commentsFile, commentsComponentContent);
547
- console.log(' ✅ Created Comments component');
548
- }
549
-
550
515
  function integrateWebpackMiddleware() {
551
516
  const cwd = process.cwd();
552
517
  const webpackDevPath = path.join(cwd, 'webpack.dev.js');
@@ -1035,19 +1000,13 @@ function modifyIndexTsx(filePath) {
1035
1000
  }
1036
1001
  }
1037
1002
 
1038
- function modifyRoutesTsx(filePath) {
1003
+ function modifyAppLayoutTsx(filePath) {
1039
1004
  const content = fs.readFileSync(filePath, 'utf8');
1040
-
1041
- // Check if Comments route already exists with actual routes
1042
- // Use a more sophisticated check that looks for the route path
1043
- if (content.includes("label: 'Comments'") || content.includes('label: "Comments"')) {
1044
- // Check if it has routes with the /comments path
1045
- if (content.includes("path: '/comments'") || content.includes('path: "/comments"')) {
1046
- console.log(' ⚠️ Already integrated (Comments route found)');
1047
- return false;
1048
- }
1049
- // If Comments group exists but has no routes, continue to add them
1050
- console.log(' ℹ️ Comments group exists but has no routes, adding route...');
1005
+
1006
+ // Check if already integrated
1007
+ if (content.includes('CommentPanel') && content.includes('CommentOverlay')) {
1008
+ console.log(' ⚠️ Already integrated (CommentPanel and CommentOverlay found)');
1009
+ return false;
1051
1010
  }
1052
1011
 
1053
1012
  try {
@@ -1056,392 +1015,87 @@ function modifyRoutesTsx(filePath) {
1056
1015
  plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties']
1057
1016
  });
1058
1017
 
1059
- let routesArray = null;
1018
+ let hasCommentingImport = false;
1060
1019
 
1020
+ // Check if commenting system imports exist
1061
1021
  traverse(ast, {
1062
- VariableDeclarator(path) {
1063
- if (path.node.id.name === 'routes' && path.node.init.type === 'ArrayExpression') {
1064
- routesArray = path.node.init;
1022
+ ImportDeclaration(path) {
1023
+ const source = path.node.source.value;
1024
+ if (source.includes('hale-commenting-system')) {
1025
+ hasCommentingImport = true;
1065
1026
  }
1066
1027
  }
1067
1028
  });
1068
1029
 
1069
- if (routesArray) {
1070
- // Check if Comments component is imported
1071
- let hasCommentsImport = false;
1072
- let commentsImportName = 'Comments';
1073
-
1074
- traverse(ast, {
1075
- ImportDeclaration(path) {
1076
- const source = path.node.source.value;
1077
- if (source.includes('Comments') || source.includes('@app/Comments')) {
1078
- hasCommentsImport = true;
1079
- // Get the imported name
1080
- path.node.specifiers.forEach(spec => {
1081
- if (spec.type === 'ImportSpecifier' && spec.imported.name === 'Comments') {
1082
- commentsImportName = spec.local.name;
1083
- }
1084
- });
1085
- }
1086
- }
1087
- });
1088
-
1089
- // Add Comments import if missing
1090
- if (!hasCommentsImport) {
1091
- let lastImportIndex = -1;
1092
- for (let i = ast.program.body.length - 1; i >= 0; i--) {
1093
- if (ast.program.body[i].type === 'ImportDeclaration') {
1094
- lastImportIndex = i;
1095
- break;
1096
- }
1097
- }
1098
- const importIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
1099
-
1100
- const commentsImport = types.importDeclaration(
1101
- [types.importSpecifier(types.identifier('Comments'), types.identifier('Comments'))],
1102
- types.stringLiteral('@app/Comments/Comments')
1103
- );
1104
-
1105
- ast.program.body.splice(importIndex, 0, commentsImport);
1106
- }
1107
-
1108
- // Check if Comments route group already exists
1109
- let existingCommentsGroup = null;
1110
- let existingCommentsRoutes = null;
1111
-
1112
- for (const element of routesArray.elements) {
1113
- if (element.type === 'ObjectExpression') {
1114
- const labelProp = element.properties.find(
1115
- prop => prop.key && prop.key.name === 'label' &&
1116
- prop.value && prop.value.value === 'Comments'
1117
- );
1118
- if (labelProp) {
1119
- existingCommentsGroup = element;
1120
- const routesProp = element.properties.find(
1121
- prop => prop.key && prop.key.name === 'routes'
1122
- );
1123
- if (routesProp && routesProp.value.type === 'ArrayExpression') {
1124
- existingCommentsRoutes = routesProp.value;
1125
- }
1126
- break;
1127
- }
1030
+ // Add imports if missing
1031
+ if (!hasCommentingImport) {
1032
+ let lastImportIndex = -1;
1033
+ for (let i = ast.program.body.length - 1; i >= 0; i--) {
1034
+ if (ast.program.body[i].type === 'ImportDeclaration') {
1035
+ lastImportIndex = i;
1036
+ break;
1128
1037
  }
1129
1038
  }
1039
+ const importIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
1130
1040
 
1131
- // Create the Comments route item
1132
- const commentsRouteElement = types.jsxElement(
1133
- types.jsxOpeningElement(types.jsxIdentifier(commentsImportName), [], true),
1134
- null,
1135
- []
1041
+ const commentingImport = types.importDeclaration(
1042
+ [
1043
+ types.importSpecifier(types.identifier('CommentPanel'), types.identifier('CommentPanel')),
1044
+ types.importSpecifier(types.identifier('CommentOverlay'), types.identifier('CommentOverlay'))
1045
+ ],
1046
+ types.stringLiteral('hale-commenting-system')
1136
1047
  );
1137
1048
 
1138
- const commentsRouteItem = types.objectExpression([
1139
- types.objectProperty(types.identifier('element'), commentsRouteElement),
1140
- types.objectProperty(types.identifier('exact'), types.booleanLiteral(true)),
1141
- types.objectProperty(types.identifier('label'), types.stringLiteral('View all')),
1142
- types.objectProperty(types.identifier('path'), types.stringLiteral('/comments')),
1143
- types.objectProperty(types.identifier('title'), types.stringLiteral('Hale Commenting System | Comments'))
1144
- ]);
1145
-
1146
- if (existingCommentsGroup && existingCommentsRoutes) {
1147
- // Add route to existing Comments group
1148
- existingCommentsRoutes.elements.push(commentsRouteItem);
1149
- } else {
1150
- // Create new Comments route group
1151
- const commentsRoute = types.objectExpression([
1152
- types.objectProperty(types.identifier('label'), types.stringLiteral('Comments')),
1153
- types.objectProperty(types.identifier('routes'), types.arrayExpression([commentsRouteItem]))
1154
- ]);
1155
- routesArray.elements.push(commentsRoute);
1156
- }
1049
+ ast.program.body.splice(importIndex, 0, commentingImport);
1157
1050
  }
1158
1051
 
1159
- const output = generate(ast, {
1160
- retainLines: false,
1161
- compact: false
1162
- }, content);
1163
-
1164
- fs.writeFileSync(filePath, output.code);
1165
- return true;
1166
- } catch (error) {
1167
- console.error(` ❌ Error modifying ${filePath}:`, error.message);
1168
- return false;
1169
- }
1170
- }
1171
-
1172
- function modifyAppLayoutTsx(filePath) {
1173
- let content = fs.readFileSync(filePath, 'utf8');
1174
-
1175
- // Check if already integrated - look for the comprehensive integration
1176
- if (content.includes('useComments') && content.includes('useGitHubAuth') &&
1177
- content.includes('setCommentsEnabled') && content.includes('setFloatingWidgetMode')) {
1178
- console.log(' ⚠️ Already integrated (full commenting system controls found)');
1179
- return false;
1180
- }
1052
+ // Find Page component and wrap its children
1053
+ let pageComponentFound = false;
1054
+ traverse(ast, {
1055
+ JSXElement(path) {
1056
+ const openingElement = path.node.openingElement;
1057
+ if (openingElement.name && openingElement.name.name === 'Page') {
1058
+ pageComponentFound = true;
1059
+ const children = path.node.children;
1060
+
1061
+ // Check if already wrapped
1062
+ if (children.length > 0 &&
1063
+ children.some(child =>
1064
+ child.type === 'JSXElement' &&
1065
+ child.openingElement.name.name === 'CommentPanel')) {
1066
+ return;
1067
+ }
1181
1068
 
1182
- try {
1183
- // Step 1: Add imports using string manipulation (more reliable for complex imports)
1184
-
1185
- // Check and add PatternFly imports
1186
- const patternflyImportRegex = /from\s+['"]@patternfly\/react-core['"]/;
1187
- const patternflyMatch = content.match(patternflyImportRegex);
1188
-
1189
- if (patternflyMatch) {
1190
- // Find the import statement
1191
- const importMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-core['"]/);
1192
- if (importMatch) {
1193
- const imports = importMatch[1];
1194
- // Add Switch if not present
1195
- if (!imports.includes('Switch')) {
1196
- const newImports = imports.trim() + ',\n Switch';
1197
- content = content.replace(
1198
- /import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-core['"]/,
1199
- `import {${newImports}\n} from '@patternfly/react-core'`
1069
+ // Create CommentOverlay element
1070
+ const commentOverlay = types.jsxElement(
1071
+ types.jsxOpeningElement(types.jsxIdentifier('CommentOverlay'), [], true),
1072
+ null,
1073
+ []
1200
1074
  );
1201
- }
1202
- }
1203
- }
1204
1075
 
1205
- // Check and add PatternFly icons imports
1206
- const iconsImportRegex = /from\s+['"]@patternfly\/react-icons['"]/;
1207
- const iconsMatch = content.match(iconsImportRegex);
1208
-
1209
- if (iconsMatch) {
1210
- const importMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-icons['"]/);
1211
- if (importMatch) {
1212
- const imports = importMatch[1];
1213
- // Add icons if not present
1214
- let newImports = imports.trim();
1215
- if (!imports.includes('ExternalLinkAltIcon')) {
1216
- newImports += ', ExternalLinkAltIcon';
1217
- }
1218
- if (!imports.includes('GithubIcon')) {
1219
- newImports += ', GithubIcon';
1220
- }
1221
- if (newImports !== imports.trim()) {
1222
- content = content.replace(
1223
- /import\s+\{([^}]+)\}\s+from\s+['"]@patternfly\/react-icons['"]/,
1224
- `import { ${newImports} } from '@patternfly/react-icons'`
1076
+ // Create CommentPanel wrapping existing children
1077
+ const commentPanel = types.jsxElement(
1078
+ types.jsxOpeningElement(types.jsxIdentifier('CommentPanel'), []),
1079
+ types.jsxClosingElement(types.jsxIdentifier('CommentPanel')),
1080
+ [commentOverlay, ...children]
1225
1081
  );
1226
- }
1227
- }
1228
- }
1229
1082
 
1230
- // Add commenting system imports
1231
- if (!content.includes('hale-commenting-system')) {
1232
- // Find where to insert (after other imports)
1233
- const lastImportMatch = content.match(/import[^;]*;(?=\s*(?:interface|const|export|function))/g);
1234
- if (lastImportMatch) {
1235
- const lastImport = lastImportMatch[lastImportMatch.length - 1];
1236
- const insertPos = content.indexOf(lastImport) + lastImport.length;
1237
- const commentingImport = `\nimport { CommentOverlay, CommentPanel, useComments, useGitHubAuth } from "hale-commenting-system";`;
1238
- content = content.slice(0, insertPos) + commentingImport + content.slice(insertPos);
1239
- }
1240
- } else {
1241
- // Update existing import to include all needed items
1242
- const commentingImportMatch = content.match(/import\s+\{([^}]+)\}\s+from\s+["']hale-commenting-system["']/);
1243
- if (commentingImportMatch) {
1244
- const imports = commentingImportMatch[1];
1245
- let newImports = imports.split(',').map(i => i.trim());
1246
-
1247
- const needed = ['CommentOverlay', 'CommentPanel', 'useComments', 'useGitHubAuth'];
1248
- needed.forEach(item => {
1249
- if (!newImports.includes(item)) {
1250
- newImports.push(item);
1251
- }
1252
- });
1253
-
1254
- content = content.replace(
1255
- /import\s+\{[^}]+\}\s+from\s+["']hale-commenting-system["']/,
1256
- `import { ${newImports.join(', ')} } from "hale-commenting-system"`
1257
- );
1258
- }
1259
- }
1260
-
1261
- // Step 2: Add hooks to the component
1262
- // Find the AppLayout function/component
1263
- const componentMatch = content.match(/(const\s+AppLayout[^=]+=\s*\([^)]*\)\s*=>\s*\{)/);
1264
- if (componentMatch) {
1265
- const componentStart = content.indexOf(componentMatch[0]);
1266
- const afterComponentStart = componentStart + componentMatch[0].length;
1267
-
1268
- // Check if hooks are already added
1269
- if (!content.includes('const { commentsEnabled, setCommentsEnabled')) {
1270
- // Find where to insert hooks (after existing useState declarations)
1271
- const stateMatch = content.slice(afterComponentStart).match(/const\s+\[[^\]]+\]\s*=\s*React\.useState/);
1272
- let hookInsertPos;
1273
-
1274
- if (stateMatch) {
1275
- const statePos = content.indexOf(stateMatch[0], afterComponentStart);
1276
- const semicolonPos = content.indexOf(';', statePos);
1277
- hookInsertPos = semicolonPos + 1;
1278
- } else {
1279
- hookInsertPos = afterComponentStart;
1083
+ path.node.children = [commentPanel];
1280
1084
  }
1281
-
1282
- const hooks = `
1283
- const { commentsEnabled, setCommentsEnabled, drawerPinnedOpen, setDrawerPinnedOpen, floatingWidgetMode, setFloatingWidgetMode } = useComments();
1284
- const { isAuthenticated, user, login, logout } = useGitHubAuth();
1285
- `;
1286
- content = content.slice(0, hookInsertPos) + hooks + content.slice(hookInsertPos);
1287
1085
  }
1288
- }
1289
-
1290
- // Step 3: Add the special renderNavGroup logic
1291
- // Replace the simple arrow function with a block function that has special Comments handling
1292
- if (!content.includes("group.label === 'Comments'")) {
1293
- // Find the renderNavGroup function - handle both arrow expression () => (...) and block () => {...}
1294
- const arrowFuncMatch = content.match(/const\s+renderNavGroup\s*=\s*\(([^)]+)\)\s*=>\s*\(/);
1295
-
1296
- if (arrowFuncMatch) {
1297
- const params = arrowFuncMatch[1];
1298
-
1299
- // Find the entire function including the closing parenthesis and semicolon
1300
- const funcStart = content.indexOf(arrowFuncMatch[0]);
1301
- const afterArrow = funcStart + arrowFuncMatch[0].length;
1302
-
1303
- // Find matching closing paren and semicolon
1304
- let depth = 1;
1305
- let endPos = afterArrow;
1306
- for (let i = afterArrow; i < content.length; i++) {
1307
- if (content.charAt(i) === '(') depth++;
1308
- if (content.charAt(i) === ')') {
1309
- depth--;
1310
- if (depth === 0) {
1311
- // Found the closing paren, now find the semicolon
1312
- endPos = i + 1;
1313
- while (endPos < content.length && content.charAt(endPos).trim() === '') endPos++;
1314
- if (content.charAt(endPos) === ';') endPos++;
1315
- break;
1316
- }
1317
- }
1318
- }
1319
-
1320
- // Extract the original NavExpandable JSX (we'll use it as the default case)
1321
- const originalBody = content.slice(funcStart + arrowFuncMatch[0].length - 1, endPos - 1); // Remove opening ( and closing );
1322
-
1323
- // Create the new block function
1324
- const newFunction = `const renderNavGroup = (${params}) => {
1325
- // Special handling for Comments group
1326
- if (group.label === 'Comments') {
1327
- return (
1328
- <NavExpandable
1329
- key={\`\${group.label}-\${groupIndex}\`}
1330
- id={\`\${group.label}-\${groupIndex}\`}
1331
- title="Hale Commenting System"
1332
- isActive={group.routes.some((route) => route.path === location.pathname)}
1333
- >
1334
- <NavItem
1335
- onClick={(e) => {
1336
- e.stopPropagation();
1337
- setFloatingWidgetMode(!floatingWidgetMode);
1338
- if (!floatingWidgetMode) {
1339
- setDrawerPinnedOpen(false);
1340
- }
1341
- }}
1342
- style={{ cursor: 'pointer' }}
1343
- >
1344
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
1345
- <ExternalLinkAltIcon />
1346
- <span>{floatingWidgetMode ? 'Close widget' : 'Pop out'}</span>
1347
- </div>
1348
- </NavItem>
1349
- <NavItem>
1350
- <div
1351
- data-comment-controls
1352
- style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
1353
- >
1354
- <span>Enable Comments</span>
1355
- <Switch
1356
- id="comments-enabled-switch"
1357
- isChecked={commentsEnabled}
1358
- onChange={(_event, checked) => {
1359
- setCommentsEnabled(checked);
1360
- if (checked) {
1361
- setDrawerPinnedOpen(true);
1362
- }
1363
- }}
1364
- aria-label="Enable or disable comments"
1365
- />
1366
- </div>
1367
- </NavItem>
1368
- <NavItem>
1369
- <div
1370
- data-comment-controls
1371
- style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
1372
- >
1373
- <span>Page info drawer</span>
1374
- <Switch
1375
- id="page-info-drawer-switch"
1376
- isChecked={drawerPinnedOpen}
1377
- onChange={(_event, checked) => setDrawerPinnedOpen(checked)}
1378
- aria-label="Pin page info drawer open"
1379
- />
1380
- </div>
1381
- </NavItem>
1382
- <NavItem>
1383
- <div
1384
- data-comment-controls
1385
- style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '1rem' }}
1386
- >
1387
- {isAuthenticated ? (
1388
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1389
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
1390
- <GithubIcon />
1391
- {user?.login ? \`@\${user.login}\` : 'Signed in'}
1392
- </span>
1393
- <Button variant="link" isInline onClick={logout}>
1394
- Sign out
1395
- </Button>
1396
- </div>
1397
- ) : (
1398
- <Button variant="link" isInline icon={<GithubIcon />} onClick={login}>
1399
- Sign in with GitHub
1400
- </Button>
1401
- )}
1402
- </div>
1403
- </NavItem>
1404
- {group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
1405
- </NavExpandable>
1406
- );
1407
- }
1408
-
1409
- // Default handling for other groups
1410
- return ${originalBody};
1411
- };`;
1086
+ });
1412
1087
 
1413
- // Replace the old function with the new one
1414
- content = content.slice(0, funcStart) + newFunction + content.slice(endPos);
1415
- }
1088
+ if (!pageComponentFound) {
1089
+ console.error(' ❌ Could not find Page component in AppLayout.tsx');
1090
+ return false;
1416
1091
  }
1417
1092
 
1418
- // Step 4: Wrap Page children if not already done
1419
- if (!content.includes('<CommentPanel>')) {
1420
- // Find the return statement with Page
1421
- const pageMatch = content.match(/<Page[^>]*>/);
1422
- if (pageMatch) {
1423
- const pageStart = content.indexOf(pageMatch[0]);
1424
- const pageEnd = content.indexOf('</Page>', pageStart);
1425
-
1426
- if (pageEnd > pageStart) {
1427
- // Extract children between Page tags
1428
- const pageOpenTagEnd = content.indexOf('>', pageStart) + 1;
1429
- const children = content.slice(pageOpenTagEnd, pageEnd);
1430
-
1431
- // Wrap with CommentPanel and add CommentOverlay
1432
- const wrappedChildren = `
1433
- <CommentPanel>
1434
- <CommentOverlay />
1435
- ${children.trim()}
1436
- </CommentPanel>
1437
- `;
1438
-
1439
- content = content.slice(0, pageOpenTagEnd) + wrappedChildren + content.slice(pageEnd);
1440
- }
1441
- }
1442
- }
1093
+ const output = generate(ast, {
1094
+ retainLines: false,
1095
+ compact: false
1096
+ }, content);
1443
1097
 
1444
- fs.writeFileSync(filePath, content);
1098
+ fs.writeFileSync(filePath, output.code);
1445
1099
  return true;
1446
1100
  } catch (error) {
1447
1101
  console.error(` ❌ Error modifying ${filePath}:`, error.message);
@@ -1821,15 +1475,13 @@ async function main() {
1821
1475
 
1822
1476
  // Step 5: Integrate into project
1823
1477
  console.log('\n🔧 Step 5: Integrating into PatternFly Seed project...\n');
1824
-
1478
+
1825
1479
  console.log('This will modify the following files:');
1826
1480
  console.log(' • src/app/index.tsx');
1827
- console.log(' • src/app/routes.tsx');
1828
1481
  console.log(' • src/app/AppLayout/AppLayout.tsx');
1829
1482
  console.log(' • webpack.dev.js\n');
1830
1483
 
1831
1484
  const indexPath = findFile('index.tsx');
1832
- const routesPath = findFile('routes.tsx');
1833
1485
  const appLayoutPath = findFile('AppLayout/AppLayout.tsx') || findFile('AppLayout.tsx');
1834
1486
 
1835
1487
  if (!indexPath) {
@@ -1837,11 +1489,6 @@ async function main() {
1837
1489
  rl.close();
1838
1490
  process.exit(1);
1839
1491
  }
1840
- if (!routesPath) {
1841
- console.error('❌ Could not find src/app/routes.tsx');
1842
- rl.close();
1843
- process.exit(1);
1844
- }
1845
1492
  if (!appLayoutPath) {
1846
1493
  console.error('❌ Could not find src/app/AppLayout/AppLayout.tsx');
1847
1494
  rl.close();
@@ -1862,19 +1509,6 @@ async function main() {
1862
1509
  skippedCount++;
1863
1510
  }
1864
1511
 
1865
- // Create Comments component first (needed for routes)
1866
- console.log('\n📝 Creating Comments component...');
1867
- createCommentsComponent();
1868
-
1869
- // Modify routes.tsx
1870
- console.log(`\n📝 ${routesPath}`);
1871
- if (modifyRoutesTsx(routesPath)) {
1872
- console.log(' ✅ Added Comments route');
1873
- successCount++;
1874
- } else {
1875
- skippedCount++;
1876
- }
1877
-
1878
1512
  // Modify AppLayout.tsx
1879
1513
  console.log(`\n📝 ${appLayoutPath}`);
1880
1514
  if (modifyAppLayoutTsx(appLayoutPath)) {
@@ -16,7 +16,7 @@ export const CommentOverlay: React.FunctionComponent = () => {
16
16
  const handlePageClick = (e: MouseEvent) => {
17
17
  if (!commentsEnabled) return;
18
18
 
19
- // Check if clicking on a pin or any interactive element
19
+ // Check if clicking on a pin or any interactive element (including FloatingWidget)
20
20
  const target = e.target as HTMLElement;
21
21
  if (
22
22
  target.closest('button') ||
@@ -26,9 +26,10 @@ export const CommentOverlay: React.FunctionComponent = () => {
26
26
  target.closest('textarea') ||
27
27
  target.closest('[role="button"]') ||
28
28
  target.closest('[data-comment-controls]') ||
29
- target.closest('[data-comment-pin]')
29
+ target.closest('[data-comment-pin]') ||
30
+ target.closest('[data-floating-widget]')
30
31
  ) {
31
- return; // Don't create pin if clicking interactive elements
32
+ return; // Don't create pin if clicking interactive elements or floating widget
32
33
  }
33
34
 
34
35
  // Get the overlay container dimensions (accounts for drawer being open)
@@ -6,14 +6,6 @@ import {
6
6
  Button,
7
7
  Card,
8
8
  CardBody,
9
- Drawer,
10
- DrawerActions,
11
- DrawerCloseButton,
12
- DrawerContent,
13
- DrawerContentBody,
14
- DrawerHead,
15
- DrawerPanelBody,
16
- DrawerPanelContent,
17
9
  EmptyState,
18
10
  EmptyStateBody,
19
11
  Label,
@@ -40,10 +32,6 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
40
32
  getThreadsForRoute,
41
33
  selectedThreadId,
42
34
  setSelectedThreadId,
43
- drawerPinnedOpen,
44
- setDrawerPinnedOpen,
45
- floatingWidgetMode,
46
- setFloatingWidgetMode,
47
35
  addReply,
48
36
  updateComment,
49
37
  deleteComment,
@@ -59,28 +47,18 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
59
47
  const [replyTextByCommentId, setReplyTextByCommentId] = React.useState<Record<string, string>>({});
60
48
  const [editingCommentId, setEditingCommentId] = React.useState<string | null>(null);
61
49
  const [editText, setEditText] = React.useState('');
62
- const drawerRef = React.useRef<HTMLSpanElement>(null);
63
50
  const [activeTabKey, setActiveTabKey] = React.useState<string | number>('comments');
64
51
 
65
52
  const currentThreads = getThreadsForRoute(location.pathname, detectedVersion);
66
53
  const selectedThread = currentThreads.find((t) => t.id === selectedThreadId);
67
- const isExpanded = !!selectedThreadId || drawerPinnedOpen || floatingWidgetMode;
68
-
69
- const onExpand = () => {
70
- drawerRef.current && drawerRef.current.focus();
71
- };
72
54
 
73
55
  React.useEffect(() => {
74
56
  if (selectedThreadId) {
75
57
  setActiveTabKey('comments');
76
- }
77
- }, [selectedThreadId]);
78
-
79
- React.useEffect(() => {
80
- if (drawerPinnedOpen && !selectedThreadId) {
58
+ } else {
81
59
  setActiveTabKey('details');
82
60
  }
83
- }, [drawerPinnedOpen, selectedThreadId]);
61
+ }, [selectedThreadId]);
84
62
 
85
63
  const handleAddComment = () => {
86
64
  if (newCommentText.trim() && selectedThread) {
@@ -150,11 +128,6 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
150
128
 
151
129
  const handleClose = () => {
152
130
  setSelectedThreadId(null);
153
- if (floatingWidgetMode) {
154
- setFloatingWidgetMode(false);
155
- } else {
156
- setDrawerPinnedOpen(false);
157
- }
158
131
  setEditingCommentId(null);
159
132
  setEditText('');
160
133
  setNewCommentText('');
@@ -221,17 +194,17 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
221
194
  aria-label="Hale Commenting System drawer tabs"
222
195
  >
223
196
  <Tab eventKey="details" title={<TabTitleText>Details</TabTitleText>}>
224
- <div style={{ paddingTop: '1rem' }}>
197
+ <div style={{ padding: '1rem' }}>
225
198
  <DetailsTab />
226
199
  </div>
227
200
  </Tab>
228
201
  <Tab eventKey="jira" title={<TabTitleText>Jira</TabTitleText>}>
229
- <div style={{ paddingTop: '1rem' }}>
202
+ <div style={{ padding: '1rem' }}>
230
203
  <JiraTab />
231
204
  </div>
232
205
  </Tab>
233
206
  <Tab eventKey="comments" title={<TabTitleText>Comments</TabTitleText>}>
234
- <div style={{ paddingTop: '1rem' }}>
207
+ <div style={{ padding: '1rem' }}>
235
208
  {!selectedThread ? (
236
209
  <EmptyState icon={InfoCircleIcon} titleText="No pin selected" headingLevel="h3">
237
210
  <EmptyStateBody>Select or create a comment pin to start a thread.</EmptyStateBody>
@@ -497,38 +470,12 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
497
470
  </>
498
471
  );
499
472
 
500
- if (floatingWidgetMode && isExpanded) {
501
- return (
502
- <>
503
- <FloatingWidget onClose={handleClose} title="Hale Commenting System">
504
- <div style={{ padding: '1rem' }}>{panelContent}</div>
505
- </FloatingWidget>
506
- <div style={{ position: 'relative' }}>{children}</div>
507
- </>
508
- );
509
- }
510
-
511
- const drawerPanelContent = isExpanded ? (
512
- <DrawerPanelContent isResizable defaultSize={'500px'} minSize={'300px'}>
513
- <DrawerHead>
514
- <span tabIndex={isExpanded ? 0 : -1} ref={drawerRef}>
515
- <Title headingLevel="h2" size="lg">
516
- Hale Commenting System
517
- </Title>
518
- </span>
519
- <DrawerActions>
520
- <DrawerCloseButton onClick={handleClose} />
521
- </DrawerActions>
522
- </DrawerHead>
523
- <DrawerPanelBody>{panelContent}</DrawerPanelBody>
524
- </DrawerPanelContent>
525
- ) : null;
526
-
527
473
  return (
528
- <Drawer isExpanded={isExpanded} isInline onExpand={onExpand}>
529
- <DrawerContent panelContent={drawerPanelContent}>
530
- <DrawerContentBody style={{ position: 'relative' }}>{children}</DrawerContentBody>
531
- </DrawerContent>
532
- </Drawer>
474
+ <>
475
+ <FloatingWidget title="Hale Commenting System">
476
+ {panelContent}
477
+ </FloatingWidget>
478
+ <div style={{ position: 'relative' }}>{children}</div>
479
+ </>
533
480
  );
534
481
  };
@@ -1,20 +1,26 @@
1
1
  import * as React from 'react';
2
- import { Button, Card, CardBody, Title } from '@patternfly/react-core';
3
- import { GripVerticalIcon, TimesIcon, WindowMinimizeIcon } from '@patternfly/react-icons';
2
+ import { createPortal } from 'react-dom';
3
+ import { Button, Switch, Title } from '@patternfly/react-core';
4
+ import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon } from '@patternfly/react-icons';
5
+ import { useComments } from '../contexts/CommentContext';
6
+ import { useGitHubAuth } from '../contexts/GitHubAuthContext';
4
7
 
5
8
  interface FloatingWidgetProps {
6
9
  children: React.ReactNode;
7
- onClose: () => void;
8
10
  title?: string;
9
11
  }
10
12
 
11
- export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ children, onClose, title = 'Hale Commenting System' }) => {
12
- const [position, setPosition] = React.useState({ x: window.innerWidth - 520, y: 100 });
13
+ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ children, title = 'Hale Commenting System' }) => {
14
+ const [position, setPosition] = React.useState({ x: window.innerWidth - 520, y: 20 });
13
15
  const [isDragging, setIsDragging] = React.useState(false);
14
16
  const [dragOffset, setDragOffset] = React.useState({ x: 0, y: 0 });
15
17
  const [isMinimized, setIsMinimized] = React.useState(false);
18
+ const [viewportHeight, setViewportHeight] = React.useState(window.innerHeight);
16
19
  const widgetRef = React.useRef<HTMLDivElement>(null);
17
20
 
21
+ const { commentsEnabled, setCommentsEnabled } = useComments();
22
+ const { isAuthenticated, user, login, logout } = useGitHubAuth();
23
+
18
24
  const handleMouseDown = (e: React.MouseEvent) => {
19
25
  if (!widgetRef.current) return;
20
26
  const rect = widgetRef.current.getBoundingClientRect();
@@ -48,29 +54,43 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
48
54
  };
49
55
  }, [isDragging, dragOffset]);
50
56
 
51
- // Constrain to viewport but allow dragging header even when partially off-screen
57
+ // Update viewport height on resize to recalculate constraints
58
+ React.useEffect(() => {
59
+ const handleResize = () => {
60
+ setViewportHeight(window.innerHeight);
61
+ };
62
+
63
+ window.addEventListener('resize', handleResize);
64
+ return () => window.removeEventListener('resize', handleResize);
65
+ }, []);
66
+
67
+ // Constrain to viewport but allow moving into topbar area (just keep drag handle accessible)
52
68
  const constrainedPosition = React.useMemo(() => {
53
69
  const widgetWidth = 500;
54
- const widgetHeight = isMinimized ? 60 : 400;
70
+ // Calculate actual widget height: 80vh when expanded, or estimate ~120px when minimized
71
+ const widgetHeight = isMinimized ? 120 : viewportHeight * 0.8;
55
72
  const maxX = window.innerWidth - 50; // Allow 50px of widget to be visible for dragging
56
- const maxY = window.innerHeight - 50;
73
+ const maxY = viewportHeight - 50;
74
+ // Allow widget to move into topbar, but keep at least 60px of drag handle visible
75
+ const minY = -widgetHeight + 60;
57
76
  return {
58
77
  x: Math.max(-widgetWidth + 50, Math.min(position.x, maxX)),
59
- y: Math.max(-widgetHeight + 50, Math.min(position.y, maxY)),
78
+ y: Math.max(minY, Math.min(position.y, maxY)),
60
79
  };
61
- }, [position, isMinimized]);
80
+ }, [position, isMinimized, viewportHeight]);
62
81
 
63
- return (
82
+ const widgetContent = (
64
83
  <div
65
84
  ref={widgetRef}
85
+ data-floating-widget
66
86
  style={{
67
87
  position: 'fixed',
68
88
  left: `${constrainedPosition.x}px`,
69
89
  top: `${constrainedPosition.y}px`,
70
90
  width: '500px',
71
- height: isMinimized ? '60px' : '80vh',
91
+ height: isMinimized ? 'fit-content' : '80vh',
72
92
  maxHeight: '80vh',
73
- zIndex: 10000,
93
+ zIndex: 99999,
74
94
  boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
75
95
  borderRadius: 'var(--pf-t--global--border--radius--medium)',
76
96
  backgroundColor: '#ffffff',
@@ -80,43 +100,87 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
80
100
  }}
81
101
  >
82
102
  <div
83
- onMouseDown={handleMouseDown}
84
103
  style={{
85
- padding: '1rem',
86
104
  borderBottom: isMinimized ? 'none' : '1px solid var(--pf-t--global--border--color--default)',
87
- display: 'flex',
88
- alignItems: 'center',
89
- justifyContent: 'space-between',
90
- cursor: 'grab',
91
- userSelect: 'none',
92
105
  backgroundColor: '#ffffff',
93
106
  borderRadius: isMinimized ? 'var(--pf-t--global--border--radius--medium)' : 'var(--pf-t--global--border--radius--medium) var(--pf-t--global--border--radius--medium) 0 0',
94
107
  }}
95
108
  >
96
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1 }}>
97
- <GripVerticalIcon style={{ color: 'var(--pf-t--global--text--color--subtle)' }} />
98
- <Title headingLevel="h2" size="lg">
99
- {title}
100
- </Title>
109
+ {/* Title bar with drag handle */}
110
+ <div
111
+ onMouseDown={handleMouseDown}
112
+ style={{
113
+ padding: '1rem',
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ justifyContent: 'space-between',
117
+ cursor: 'grab',
118
+ userSelect: 'none',
119
+ }}
120
+ >
121
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1 }}>
122
+ <GripVerticalIcon style={{ color: 'var(--pf-t--global--text--color--subtle)' }} />
123
+ <Title headingLevel="h2" size="lg">
124
+ {title}
125
+ </Title>
126
+ </div>
127
+ <div style={{ display: 'flex', gap: '0.25rem' }}>
128
+ <Button
129
+ variant="plain"
130
+ icon={<WindowMinimizeIcon />}
131
+ onClick={(e) => {
132
+ e.stopPropagation();
133
+ setIsMinimized(!isMinimized);
134
+ }}
135
+ aria-label={isMinimized ? 'Maximize widget' : 'Minimize widget'}
136
+ />
137
+ </div>
101
138
  </div>
102
- <div style={{ display: 'flex', gap: '0.25rem' }}>
103
- <Button
104
- variant="plain"
105
- icon={<WindowMinimizeIcon />}
106
- onClick={(e) => {
107
- e.stopPropagation();
108
- setIsMinimized(!isMinimized);
139
+
140
+ {/* Controls row */}
141
+ {!isMinimized && (
142
+ <div
143
+ style={{
144
+ padding: '0 1rem 0.75rem 1rem',
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ gap: '1rem',
148
+ borderBottom: '1px solid var(--pf-t--global--border--color--default)',
109
149
  }}
110
- aria-label={isMinimized ? 'Maximize widget' : 'Minimize widget'}
111
- />
112
- <Button variant="plain" icon={<TimesIcon />} onClick={onClose} aria-label="Close widget" />
113
- </div>
150
+ >
151
+ <Switch
152
+ id="floating-comments-enabled-switch"
153
+ label="Enable Comments"
154
+ isChecked={commentsEnabled}
155
+ onChange={(_event, checked) => setCommentsEnabled(checked)}
156
+ aria-label="Enable or disable comments"
157
+ />
158
+ <div style={{ flex: 1 }} />
159
+ {isAuthenticated ? (
160
+ <>
161
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', fontSize: 'var(--pf-t--global--font--size--sm)' }}>
162
+ <GithubIcon />
163
+ {user?.login ? `@${user.login}` : 'Signed in'}
164
+ </span>
165
+ <Button variant="link" isInline onClick={logout} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
166
+ Sign out
167
+ </Button>
168
+ </>
169
+ ) : (
170
+ <Button variant="link" isInline icon={<GithubIcon />} onClick={login} style={{ fontSize: 'var(--pf-t--global--font--size--sm)' }}>
171
+ Sign in with GitHub
172
+ </Button>
173
+ )}
174
+ </div>
175
+ )}
114
176
  </div>
115
177
  {!isMinimized && (
116
178
  <div
117
179
  style={{
118
- overflow: 'auto',
119
- flex: 1,
180
+ overflowY: 'auto',
181
+ overflowX: 'hidden',
182
+ flex: '1 1 0',
183
+ minHeight: 0,
120
184
  backgroundColor: '#ffffff',
121
185
  borderRadius: '0 0 var(--pf-t--global--border--radius--medium) var(--pf-t--global--border--radius--medium)',
122
186
  }}
@@ -126,5 +190,7 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
126
190
  )}
127
191
  </div>
128
192
  );
129
- };
130
193
 
194
+ // Render widget in a portal to document.body so it floats above ALL page elements
195
+ return typeof document !== 'undefined' ? createPortal(widgetContent, document.body) : null;
196
+ };
@@ -204,7 +204,9 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
204
204
  React.useEffect(() => {
205
205
  if (typeof window === 'undefined') return;
206
206
  try {
207
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(threads));
207
+ // Filter out temporary threads before saving to localStorage
208
+ const persistedThreads = threads.filter(t => !t.isTemporary);
209
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(persistedThreads));
208
210
  } catch {
209
211
  // ignore quota/serialization errors
210
212
  }
@@ -255,14 +257,17 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
255
257
  version,
256
258
  comments: [],
257
259
  provider: 'github',
258
- syncStatus: isConfigured ? 'syncing' : 'local',
260
+ syncStatus: 'local',
259
261
  status: 'open',
262
+ isTemporary: true, // Mark as temporary until first comment is added
260
263
  };
261
264
  setThreads((prev) => [...prev, newThread]);
262
265
 
263
- console.log(`📌 Thread created locally with syncStatus: ${newThread.syncStatus}`);
266
+ console.log(`📌 Thread created as temporary (will persist when first comment is added)`);
264
267
 
265
- // Background sync to GitHub (optimistic UI)
268
+ // Don't sync to GitHub yet - wait for first comment
269
+ // This prevents creating empty GitHub issues
270
+ /* Commented out - only sync when comment is added
266
271
  if (isConfigured) {
267
272
  console.log(`🔵 Creating GitHub issue for thread ${threadId}...`);
268
273
 
@@ -319,6 +324,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
319
324
  console.log(`📌 Thread ${threadId} syncStatus updated to: error (exception caught)`);
320
325
  });
321
326
  }
327
+ */
322
328
 
323
329
  return threadId;
324
330
  };
@@ -521,6 +527,7 @@ export const CommentProvider: React.FunctionComponent<{ children: React.ReactNod
521
527
  return {
522
528
  ...thread,
523
529
  comments: [...thread.comments, newComment],
530
+ isTemporary: undefined, // Remove temporary flag - thread now persists
524
531
  };
525
532
  }),
526
533
  );
@@ -24,4 +24,5 @@ export interface Thread {
24
24
  syncStatus?: SyncStatus;
25
25
  syncError?: string;
26
26
  status?: ThreadStatus; // open or closed (mirrors GitHub issue state)
27
+ isTemporary?: boolean; // If true, thread is not persisted until first comment is added
27
28
  }
@@ -58,10 +58,6 @@ const routes: AppRouteConfig[] = [
58
58
  },
59
59
  ],
60
60
  },
61
- {
62
- label: 'Comments',
63
- routes: [],
64
- },
65
61
  ];
66
62
 
67
63
  const flattenedRoutes: IAppRoute[] = routes.reduce(
package/tsconfig.json CHANGED
@@ -30,5 +30,5 @@
30
30
  "**/*.jsx",
31
31
  "**/*.js"
32
32
  ],
33
- "exclude": ["node_modules"]
33
+ "exclude": ["node_modules", "scripts/AppLayout.template.tsx"]
34
34
  }