hale-commenting-system 3.5.2 → 3.7.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.
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { findElementBySelector } from '../utils/selectorUtils';
2
3
  import { useLocation } from 'react-router-dom';
3
4
  import {
4
5
  ActionList,
@@ -121,7 +122,11 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
121
122
  }
122
123
  };
123
124
 
124
- const handleRemovePin = () => {
125
+ const handleRemovePin = (e?: React.MouseEvent) => {
126
+ if (e) {
127
+ e.stopPropagation();
128
+ e.preventDefault();
129
+ }
125
130
  if (!selectedThread) return;
126
131
  removePin(selectedThread.id);
127
132
  };
@@ -211,13 +216,67 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
211
216
  </EmptyState>
212
217
  ) : (
213
218
  <>
214
- {/* Thread summary header (scaffold) */}
219
+ {/* Thread summary header with component information */}
215
220
  <Card style={{ marginBottom: '1rem' }}>
216
221
  <CardBody>
217
- <div style={{ display: 'grid', gap: '0.5rem' }}>
218
- <div style={{ fontSize: '0.875rem' }}>
219
- <strong>Location:</strong> ({selectedThread.xPercent.toFixed(1)}%, {selectedThread.yPercent.toFixed(1)}%)
220
- </div>
222
+ <div style={{ display: 'grid', gap: '0.75rem' }}>
223
+ {/* Component Information (Component-Based) */}
224
+ {selectedThread.componentMetadata ? (
225
+ <div style={{ display: 'grid', gap: '0.5rem', padding: '0.75rem', backgroundColor: 'var(--pf-t--global--background--color--secondary--default)', borderRadius: 'var(--pf-t--global--border--radius--small)' }}>
226
+ <div style={{ fontSize: '0.875rem', fontWeight: 'bold' }}>
227
+ React Component
228
+ </div>
229
+ <div style={{ fontSize: '0.875rem' }}>
230
+ <strong>Name:</strong> {selectedThread.componentMetadata.componentName || selectedThread.componentMetadata.displayName || 'Unknown'}
231
+ {selectedThread.componentMetadata.componentType && selectedThread.componentMetadata.componentType !== 'native' && (
232
+ <span style={{ color: 'var(--pf-t--global--text--color--subtle)', marginLeft: '0.5rem' }}>
233
+ ({selectedThread.componentMetadata.componentType})
234
+ </span>
235
+ )}
236
+ </div>
237
+ {selectedThread.componentMetadata.componentPath && selectedThread.componentMetadata.componentPath.length > 0 && (
238
+ <div style={{ fontSize: '0.875rem' }}>
239
+ <strong>Path:</strong>{' '}
240
+ <span style={{ fontFamily: 'monospace', color: 'var(--pf-t--global--text--color--subtle)' }}>
241
+ {selectedThread.componentMetadata.componentPath.join(' > ')}
242
+ </span>
243
+ </div>
244
+ )}
245
+ {selectedThread.componentMetadata.props && Object.keys(selectedThread.componentMetadata.props).length > 0 && (
246
+ <details style={{ fontSize: '0.875rem' }}>
247
+ <summary style={{ cursor: 'pointer', marginBottom: '0.25rem' }}>
248
+ <strong>Props</strong> ({Object.keys(selectedThread.componentMetadata.props).length})
249
+ </summary>
250
+ <pre
251
+ style={{
252
+ marginTop: '0.5rem',
253
+ padding: '0.5rem',
254
+ backgroundColor: 'var(--pf-t--global--background--color--default)',
255
+ borderRadius: 'var(--pf-t--global--border--radius--small)',
256
+ fontSize: '0.75rem',
257
+ overflow: 'auto',
258
+ maxHeight: '200px',
259
+ }}
260
+ >
261
+ {JSON.stringify(selectedThread.componentMetadata.props, null, 2)}
262
+ </pre>
263
+ </details>
264
+ )}
265
+ {selectedThread.cssSelector && !findElementBySelector(selectedThread.cssSelector) && (
266
+ <div style={{ fontSize: '0.875rem', color: 'var(--pf-t--global--color--status--danger--default)' }}>
267
+ ⚠️ Component element not found in DOM
268
+ </div>
269
+ )}
270
+ </div>
271
+ ) : (
272
+ <div style={{ fontSize: '0.875rem' }}>
273
+ <strong>Element:</strong> {selectedThread.elementDescription || 'unknown'}
274
+ {selectedThread.cssSelector && !findElementBySelector(selectedThread.cssSelector) && (
275
+ <span style={{ color: 'var(--pf-t--global--color--status--danger--default)' }}> [deleted]</span>
276
+ )}
277
+ </div>
278
+ )}
279
+
221
280
  <div style={{ fontSize: '0.875rem' }}>
222
281
  <strong>Comments:</strong> {selectedThread.comments.length}
223
282
  </div>
@@ -245,10 +304,6 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
245
304
  </span>
246
305
  )}
247
306
  </div>
248
-
249
- <div>
250
- {/* AI summarize removed for now */}
251
- </div>
252
307
  </div>
253
308
  </CardBody>
254
309
  </Card>
@@ -431,7 +486,7 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
431
486
  <Button variant="primary" onClick={handleReopenThread}>
432
487
  Reopen Thread
433
488
  </Button>
434
- <Button variant="link" isDanger onClick={handleRemovePin}>
489
+ <Button variant="link" isDanger onClick={handleRemovePin} aria-label="Remove pin">
435
490
  Remove pin
436
491
  </Button>
437
492
  </div>
@@ -455,7 +510,7 @@ export const CommentPanel: React.FunctionComponent<CommentPanelProps> = ({ child
455
510
  <Button variant="secondary" onClick={handleCloseThread}>
456
511
  Close Thread
457
512
  </Button>
458
- <Button variant="link" isDanger onClick={handleRemovePin}>
513
+ <Button variant="link" isDanger onClick={handleRemovePin} aria-label="Remove pin">
459
514
  Remove pin
460
515
  </Button>
461
516
  </div>
@@ -1,8 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { Button } from '@patternfly/react-core';
3
3
  import { CommentIcon } from '@patternfly/react-icons';
4
+ import { findElementBySelector } from '../utils/selectorUtils';
4
5
 
5
6
  interface CommentPinProps {
7
+ cssSelector?: string;
6
8
  xPercent: number;
7
9
  yPercent: number;
8
10
  commentCount: number;
@@ -12,6 +14,7 @@ interface CommentPinProps {
12
14
  }
13
15
 
14
16
  export const CommentPin: React.FunctionComponent<CommentPinProps> = ({
17
+ cssSelector,
15
18
  xPercent,
16
19
  yPercent,
17
20
  commentCount,
@@ -19,15 +22,90 @@ export const CommentPin: React.FunctionComponent<CommentPinProps> = ({
19
22
  isSelected,
20
23
  onClick,
21
24
  }) => {
25
+ const [position, setPosition] = React.useState({ left: `${xPercent}%`, top: `${yPercent}%` });
26
+ const [elementExists, setElementExists] = React.useState(true);
27
+
28
+ const updatePosition = React.useCallback(() => {
29
+ if (!cssSelector) {
30
+ // No selector - use fallback coordinates
31
+ setPosition({ left: `${xPercent}%`, top: `${yPercent}%` });
32
+ setElementExists(true);
33
+ return;
34
+ }
35
+
36
+ const element = findElementBySelector(cssSelector);
37
+ if (element) {
38
+ // Element found - position pin at top-left of element
39
+ const rect = element.getBoundingClientRect();
40
+
41
+ // Find the overlay container - it should be a parent with data-comment-overlay
42
+ // or find the CommentPanel's relative container
43
+ let overlayContainer: Element | null = null;
44
+ let current: Element | null = element;
45
+
46
+ // Walk up the DOM to find the overlay or its parent container
47
+ while (current && current !== document.body) {
48
+ if (current.hasAttribute('data-comment-overlay')) {
49
+ overlayContainer = current;
50
+ break;
51
+ }
52
+ // Also check if parent has position relative (likely the CommentPanel wrapper)
53
+ const parent = current.parentElement;
54
+ if (parent) {
55
+ const style = window.getComputedStyle(parent);
56
+ if (style.position === 'relative') {
57
+ overlayContainer = parent;
58
+ break;
59
+ }
60
+ }
61
+ current = parent;
62
+ }
63
+
64
+ // If no overlay found, use the element's offsetParent or body
65
+ if (!overlayContainer) {
66
+ overlayContainer = (element as HTMLElement).offsetParent as Element || document.body;
67
+ }
68
+
69
+ const overlayRect = overlayContainer.getBoundingClientRect();
70
+
71
+ // Position at top-left of element, offset by 4px (just outside the element border)
72
+ // Use absolute pixel positioning relative to the overlay container
73
+ const leftPx = rect.left - overlayRect.left + 4;
74
+ const topPx = rect.top - overlayRect.top + 4;
75
+
76
+ setPosition({ left: `${leftPx}px`, top: `${topPx}px` });
77
+ setElementExists(true);
78
+ } else {
79
+ // Element not found - fall back to stored coordinates and fade
80
+ setPosition({ left: `${xPercent}%`, top: `${yPercent}%` });
81
+ setElementExists(false);
82
+ }
83
+ }, [cssSelector, xPercent, yPercent]);
84
+
85
+ React.useEffect(() => {
86
+ updatePosition();
87
+
88
+ // Update position on scroll and resize
89
+ window.addEventListener('scroll', updatePosition, true);
90
+ window.addEventListener('resize', updatePosition);
91
+
92
+ return () => {
93
+ window.removeEventListener('scroll', updatePosition, true);
94
+ window.removeEventListener('resize', updatePosition);
95
+ };
96
+ }, [updatePosition]);
97
+
98
+ const opacity = elementExists ? 1.0 : 0.4;
99
+
22
100
  return (
23
101
  <Button
24
102
  variant="plain"
25
103
  data-comment-pin
26
104
  style={{
27
105
  position: 'absolute',
28
- left: `${xPercent}%`,
29
- top: `${yPercent}%`,
30
- transform: 'translate(-50%, -50%)',
106
+ left: position.left,
107
+ top: position.top,
108
+ transform: 'translate(0, 0)',
31
109
  width: '32px',
32
110
  height: '32px',
33
111
  borderRadius: '50%',
@@ -44,9 +122,13 @@ export const CommentPin: React.FunctionComponent<CommentPinProps> = ({
44
122
  justifyContent: 'center',
45
123
  transition: 'all 0.2s ease',
46
124
  pointerEvents: 'auto',
125
+ opacity,
126
+ }}
127
+ onClick={(e) => {
128
+ e.stopPropagation();
129
+ onClick();
47
130
  }}
48
- onClick={onClick}
49
- aria-label={`${isClosed ? 'Closed ' : ''}comment thread with ${commentCount} comment${commentCount !== 1 ? 's' : ''}`}
131
+ aria-label={`${isClosed ? 'Closed ' : ''}comment thread with ${commentCount} comment${commentCount !== 1 ? 's' : ''}${!elementExists ? ' (element deleted)' : ''}`}
50
132
  >
51
133
  {commentCount === 0 ? (
52
134
  <CommentIcon style={{ fontSize: '16px' }} />
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { Button, Switch, Title } from '@patternfly/react-core';
4
- import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon } from '@patternfly/react-icons';
4
+ import { GripVerticalIcon, WindowMinimizeIcon, GithubIcon, ArrowsAltVIcon } from '@patternfly/react-icons';
5
5
  import { useComments } from '../contexts/CommentContext';
6
6
  import { useGitHubAuth } from '../contexts/GitHubAuthContext';
7
7
 
@@ -16,9 +16,13 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
16
16
  const [dragOffset, setDragOffset] = React.useState({ x: 0, y: 0 });
17
17
  const [isMinimized, setIsMinimized] = React.useState(false);
18
18
  const [viewportHeight, setViewportHeight] = React.useState(window.innerHeight);
19
+ const [widgetSize, setWidgetSize] = React.useState({ width: 500, height: window.innerHeight * 0.8 });
20
+ const [isResizing, setIsResizing] = React.useState(false);
21
+ const [resizeStart, setResizeStart] = React.useState({ x: 0, y: 0, width: 0, height: 0 });
19
22
  const widgetRef = React.useRef<HTMLDivElement>(null);
23
+ const resizeHandleRef = React.useRef<HTMLDivElement>(null);
20
24
 
21
- const { commentsEnabled, setCommentsEnabled } = useComments();
25
+ const { commentsEnabled, setCommentsEnabled, showPinsEnabled, setShowPinsEnabled } = useComments();
22
26
  const { isAuthenticated, user, login, logout } = useGitHubAuth();
23
27
 
24
28
  const handleMouseDown = (e: React.MouseEvent) => {
@@ -64,11 +68,52 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
64
68
  return () => window.removeEventListener('resize', handleResize);
65
69
  }, []);
66
70
 
71
+ // Handle resize
72
+ const handleResizeStart = (e: React.MouseEvent) => {
73
+ e.preventDefault();
74
+ e.stopPropagation();
75
+ if (!widgetRef.current) return;
76
+ const rect = widgetRef.current.getBoundingClientRect();
77
+ setIsResizing(true);
78
+ setResizeStart({
79
+ x: e.clientX,
80
+ y: e.clientY,
81
+ width: rect.width,
82
+ height: rect.height,
83
+ });
84
+ };
85
+
86
+ React.useEffect(() => {
87
+ if (!isResizing) return;
88
+
89
+ const handleMouseMove = (e: MouseEvent) => {
90
+ const deltaX = e.clientX - resizeStart.x;
91
+ const deltaY = e.clientY - resizeStart.y;
92
+
93
+ setWidgetSize({
94
+ width: Math.max(300, Math.min(800, resizeStart.width + deltaX)),
95
+ height: Math.max(200, Math.min(viewportHeight - 100, resizeStart.height + deltaY)),
96
+ });
97
+ };
98
+
99
+ const handleMouseUp = () => {
100
+ setIsResizing(false);
101
+ };
102
+
103
+ document.addEventListener('mousemove', handleMouseMove);
104
+ document.addEventListener('mouseup', handleMouseUp);
105
+
106
+ return () => {
107
+ document.removeEventListener('mousemove', handleMouseMove);
108
+ document.removeEventListener('mouseup', handleMouseUp);
109
+ };
110
+ }, [isResizing, resizeStart, viewportHeight]);
111
+
67
112
  // Constrain to viewport but allow moving into topbar area (just keep drag handle accessible)
68
113
  const constrainedPosition = React.useMemo(() => {
69
- const widgetWidth = 500;
70
- // Calculate actual widget height: 80vh when expanded, or estimate ~120px when minimized
71
- const widgetHeight = isMinimized ? 120 : viewportHeight * 0.8;
114
+ const widgetWidth = widgetSize.width;
115
+ // Calculate actual widget height
116
+ const widgetHeight = isMinimized ? 120 : widgetSize.height;
72
117
  const maxX = window.innerWidth - 50; // Allow 50px of widget to be visible for dragging
73
118
  const maxY = viewportHeight - 50;
74
119
  // Allow widget to move into topbar, but keep at least 60px of drag handle visible
@@ -77,7 +122,7 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
77
122
  x: Math.max(-widgetWidth + 50, Math.min(position.x, maxX)),
78
123
  y: Math.max(minY, Math.min(position.y, maxY)),
79
124
  };
80
- }, [position, isMinimized, viewportHeight]);
125
+ }, [position, isMinimized, viewportHeight, widgetSize]);
81
126
 
82
127
  const widgetContent = (
83
128
  <div
@@ -87,9 +132,9 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
87
132
  position: 'fixed',
88
133
  left: `${constrainedPosition.x}px`,
89
134
  top: `${constrainedPosition.y}px`,
90
- width: '500px',
91
- height: isMinimized ? 'fit-content' : '80vh',
92
- maxHeight: '80vh',
135
+ width: `${widgetSize.width}px`,
136
+ height: isMinimized ? 'fit-content' : `${widgetSize.height}px`,
137
+ maxHeight: `${viewportHeight - 100}px`,
93
138
  zIndex: 99999,
94
139
  boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
95
140
  borderRadius: 'var(--pf-t--global--border--radius--medium)',
@@ -155,6 +200,13 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
155
200
  onChange={(_event, checked) => setCommentsEnabled(checked)}
156
201
  aria-label="Enable or disable comments"
157
202
  />
203
+ <Switch
204
+ id="floating-show-pins-switch"
205
+ label="Show pins"
206
+ isChecked={showPinsEnabled}
207
+ onChange={(_event, checked) => setShowPinsEnabled(checked)}
208
+ aria-label="Show or hide comment pins"
209
+ />
158
210
  <div style={{ flex: 1 }} />
159
211
  {isAuthenticated ? (
160
212
  <>
@@ -188,6 +240,24 @@ export const FloatingWidget: React.FunctionComponent<FloatingWidgetProps> = ({ c
188
240
  {children}
189
241
  </div>
190
242
  )}
243
+ {/* Resize handle */}
244
+ {!isMinimized && (
245
+ <div
246
+ ref={resizeHandleRef}
247
+ onMouseDown={handleResizeStart}
248
+ style={{
249
+ position: 'absolute',
250
+ bottom: 0,
251
+ right: 0,
252
+ width: '20px',
253
+ height: '20px',
254
+ cursor: 'nwse-resize',
255
+ background: 'linear-gradient(135deg, transparent 0%, transparent 40%, var(--pf-t--global--border--color--default) 40%, var(--pf-t--global--border--color--default) 45%, transparent 45%, transparent 55%, var(--pf-t--global--border--color--default) 55%, var(--pf-t--global--border--color--default) 60%, transparent 60%)',
256
+ borderRadius: '0 0 var(--pf-t--global--border--radius--medium) 0',
257
+ }}
258
+ aria-label="Resize widget"
259
+ />
260
+ )}
191
261
  </div>
192
262
  );
193
263