@salesforcedevs/docs-components 1.17.0-hack-alpha1 → 1.17.0-hack-alpha2

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": "@salesforcedevs/docs-components",
3
- "version": "1.17.0-hack-alpha1",
3
+ "version": "1.17.0-hack-alpha2",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -1,24 +1,28 @@
1
1
  /* Comment Icon Container */
2
2
  .comment-icon-container {
3
- position: absolute;
4
- top: 20px;
5
- right: 20px;
3
+ position: relative;
4
+ display: inline-block;
5
+ vertical-align: top;
6
+ margin-right: var(--dx-g-spacing-xs, 4px);
7
+ margin-top: var(--dx-g-spacing-sm, 8px);
8
+ margin-bottom: calc(-1 * var(--dx-g-spacing-lg, 24px));
6
9
  z-index: 1000;
7
10
  }
8
11
 
9
12
  .comment-icon-button {
10
- background: #1976d2;
11
- border-radius: 50%;
12
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
13
- width: 56px;
14
- height: 56px;
13
+ background: var(--dx-g-blue-vibrant-20);
15
14
  border: none;
15
+ border-radius: 50%;
16
16
  cursor: pointer;
17
17
  transition: all 0.3s ease;
18
18
  display: flex;
19
19
  align-items: center;
20
20
  justify-content: center;
21
21
  color: white;
22
+ padding: 6px;
23
+ width: 28px;
24
+ height: 28px;
25
+ box-sizing: border-box;
22
26
  }
23
27
 
24
28
  .comment-icon-button lightning-icon,
@@ -26,10 +30,14 @@
26
30
  --sds-c-icon-color-foreground: white;
27
31
  }
28
32
 
33
+ .comment-icon-button:hover lightning-icon,
34
+ .comment-icon-button:hover c-primitive-icon {
35
+ --sds-c-icon-color-foreground: white;
36
+ }
37
+
29
38
  .comment-icon-button:hover {
30
- background: #1565c0;
31
- transform: scale(1.05);
32
- box-shadow: 0 6px 16px rgb(0 0 0 / 20%);
39
+ background: var(--dx-g-blue-vibrant-50);
40
+ color: white;
33
41
  }
34
42
 
35
43
  .comment-icon-button:active {
@@ -56,6 +64,7 @@
56
64
  box-sizing: border-box;
57
65
  border: 2px solid white;
58
66
  box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
67
+ z-index: 1001;
59
68
  }
60
69
 
61
70
  /* Popup Overlay */
@@ -228,13 +237,15 @@
228
237
  /* Responsive Design */
229
238
  @media (max-width: 768px) {
230
239
  .comment-icon-container {
231
- top: 16px;
232
- right: 16px;
240
+ margin-right: var(--dx-g-spacing-xs, 4px);
241
+ margin-top: var(--dx-g-spacing-xs, 4px);
242
+ margin-bottom: calc(-1 * var(--dx-g-spacing-md, 16px));
233
243
  }
234
244
 
235
245
  .comment-icon-button {
236
- width: 48px;
237
- height: 48px;
246
+ width: 24px;
247
+ height: 24px;
248
+ padding: 4px;
238
249
  }
239
250
 
240
251
  .comment-count-badge {
@@ -15,6 +15,13 @@ export default class CommentPopup extends LightningElement {
15
15
  @api submitButtonLabel = "Post Comment";
16
16
  @api emailPlaceholder = "Enter your email";
17
17
  @api commentPlaceholder = "Enter your comment";
18
+ @api index?: string;
19
+ @api contentType?: string;
20
+ @api headingTitle?: string;
21
+ @api filePath?: string;
22
+ @api startLine?: string;
23
+ @api endLine?: string;
24
+ @api currentBranch?: string;
18
25
 
19
26
  private _open = false;
20
27
  @api get open() {
@@ -67,42 +67,53 @@ h1 {
67
67
  font-size: var(--dx-g-text-3xl);
68
68
  letter-spacing: -0.8px;
69
69
  line-height: var(--dx-g-spacing-3xl);
70
- margin: var(--dx-g-spacing-2xl) 0 var(--dx-g-spacing-md) 0;
70
+ margin: var(--heading-top-margin, var(--dx-g-spacing-2xl)) 0
71
+ var(--dx-g-spacing-md) 0;
71
72
  }
72
73
 
73
74
  h2 {
74
75
  font-size: var(--dx-g-text-2xl);
75
76
  letter-spacing: -0.4px;
76
77
  line-height: var(--dx-g-spacing-2xl);
77
- margin: var(--dx-g-spacing-2xl) 0 var(--dx-g-spacing-md) 0;
78
+ margin: var(--heading-top-margin, var(--dx-g-spacing-2xl)) 0
79
+ var(--dx-g-spacing-md) 0;
78
80
  }
79
81
 
80
82
  h3 {
81
83
  font-size: var(--dx-g-text-xl);
82
84
  letter-spacing: -0.4px;
83
85
  line-height: var(--dx-g-spacing-xl);
84
- margin: var(--dx-g-spacing-xl) 0 var(--dx-g-spacing-md) 0;
86
+ margin: var(--heading-top-margin, var(--dx-g-spacing-md)) 0
87
+ var(--dx-g-spacing-md) 0;
85
88
  }
86
89
 
87
90
  h4 {
88
91
  font-size: var(--dx-g-text-base);
89
92
  letter-spacing: -0.5px;
90
93
  line-height: var(--dx-g-spacing-lg);
91
- margin: var(--dx-g-spacing-lg) 0 var(--dx-g-spacing-sm) 0;
94
+ margin: var(--heading-top-margin, var(--dx-g-spacing-lg)) 0
95
+ var(--dx-g-spacing-sm) 0;
92
96
  }
93
97
 
94
98
  h5 {
95
99
  font-size: var(--dx-g-text-sm);
96
100
  letter-spacing: -0.3px;
97
101
  line-height: var(--dx-g-spacing-mlg);
98
- margin: var(--dx-g-spacing-md) 0 var(--dx-g-spacing-sm) 0;
102
+ margin: var(--heading-top-margin, var(--dx-g-spacing-md)) 0
103
+ var(--dx-g-spacing-sm) 0;
99
104
  }
100
105
 
101
106
  h6 {
102
107
  font-size: var(--dx-g-text-xs);
103
108
  letter-spacing: -0.3px;
104
109
  line-height: var(--dx-g-spacing-md);
105
- margin: var(--dx-g-spacing-sm) 0 var(--dx-g-spacing-sm) 0;
110
+ margin: var(--heading-top-margin, var(--dx-g-spacing-sm)) 0
111
+ var(--dx-g-spacing-sm) 0;
112
+ }
113
+
114
+ /* Override top margin for doc-heading preceded by doc-comment-popup */
115
+ doc-comment-popup + doc-heading {
116
+ --heading-top-margin: 10px;
106
117
  }
107
118
 
108
119
  img {
@@ -31,3 +31,429 @@ h4 {
31
31
  h4 doc-heading-content {
32
32
  --doc-c-heading-anchor-button-bottom: -6px;
33
33
  }
34
+
35
+ /* Heading alignment when comment popup is present */
36
+ h1:has(.comment-icon-container),
37
+ h2:has(.comment-icon-container),
38
+ h3:has(.comment-icon-container),
39
+ h4:has(.comment-icon-container) {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: var(--dx-g-spacing-xs, 4px);
43
+ }
44
+
45
+ /* Ensure heading content takes up remaining space */
46
+ doc-heading-content {
47
+ flex: 1;
48
+ display: inline-block;
49
+ }
50
+
51
+ /* When no comment popup is present, reset flex to normal display */
52
+ h1:not(:has(.comment-icon-container)),
53
+ h2:not(:has(.comment-icon-container)),
54
+ h3:not(:has(.comment-icon-container)),
55
+ h4:not(:has(.comment-icon-container)) {
56
+ display: block;
57
+ }
58
+
59
+ /* Fallback for browsers that don't support :has() -
60
+ rely on JavaScript or assume all headings might have popups */
61
+ @supports not (selector(:has(*))) {
62
+ h1,
63
+ h2,
64
+ h3,
65
+ h4 {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: var(--dx-g-spacing-xs, 4px);
69
+ }
70
+ }
71
+
72
+ /* Comment Icon Container */
73
+ .comment-icon-container {
74
+ position: relative;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ flex-shrink: 0;
79
+ z-index: 1000;
80
+ }
81
+
82
+ .comment-icon-button {
83
+ background: var(--dx-g-blue-vibrant-20);
84
+ border: none;
85
+ border-radius: 50%;
86
+ cursor: pointer;
87
+ transition: all 0.3s ease;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ color: white;
92
+ padding: 6px;
93
+ width: 28px;
94
+ height: 28px;
95
+ box-sizing: border-box;
96
+ }
97
+
98
+ .comment-icon-button dx-icon {
99
+ color: white;
100
+ }
101
+
102
+ .comment-icon-button:hover {
103
+ background: var(--dx-g-blue-vibrant-50);
104
+ color: white;
105
+ }
106
+
107
+ .comment-icon-button:active {
108
+ transform: scale(0.95);
109
+ }
110
+
111
+ /* Comment Count Badge */
112
+ .comment-count-badge {
113
+ position: absolute;
114
+ top: -8px;
115
+ right: -8px;
116
+ background: #f44336;
117
+ color: white;
118
+ border-radius: 50%;
119
+ width: 20px;
120
+ height: 20px;
121
+ font-size: 12px;
122
+ font-weight: bold;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ min-width: 20px;
127
+ padding: 0 2px;
128
+ box-sizing: border-box;
129
+ border: 2px solid white;
130
+ box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
131
+ z-index: 1001;
132
+ }
133
+
134
+ /* Popup Overlay */
135
+ .popup-overlay {
136
+ position: fixed;
137
+ top: 0;
138
+ left: 0;
139
+ width: 100%;
140
+ height: 100%;
141
+ background: rgb(0 0 0 / 50%);
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ z-index: 2000;
146
+ padding: 20px;
147
+ box-sizing: border-box;
148
+ }
149
+
150
+ /* Popup Container */
151
+ .popup-container {
152
+ background: white;
153
+ border-radius: 12px;
154
+ box-shadow: 0 20px 40px rgb(0 0 0 / 10%);
155
+ max-width: 500px;
156
+ width: 100%;
157
+ max-height: 80vh;
158
+ overflow-y: auto;
159
+ position: relative;
160
+ }
161
+
162
+ /* Popup Header */
163
+ .popup-header {
164
+ display: flex;
165
+ justify-content: space-between;
166
+ align-items: center;
167
+ padding: 24px 24px 0;
168
+ border-bottom: 1px solid #e0e0e0;
169
+ margin-bottom: 24px;
170
+ }
171
+
172
+ .popup-title {
173
+ margin: 0;
174
+ font-size: 20px;
175
+ font-weight: 600;
176
+ color: #212121;
177
+ }
178
+
179
+ .close-button {
180
+ background: none;
181
+ border: none;
182
+ cursor: pointer;
183
+ padding: 8px;
184
+ border-radius: 50%;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ transition: background-color 0.2s ease;
189
+ }
190
+
191
+ .close-button:hover {
192
+ background: #f5f5f5;
193
+ }
194
+
195
+ /* Popup Content */
196
+ .popup-content {
197
+ padding: 0 24px 24px;
198
+ }
199
+
200
+ /* Comment Form */
201
+ .comment-form {
202
+ margin-bottom: 24px;
203
+ }
204
+
205
+ .form-group {
206
+ margin-bottom: 20px;
207
+ }
208
+
209
+ .form-label {
210
+ display: block;
211
+ margin-bottom: 8px;
212
+ font-weight: 500;
213
+ color: #424242;
214
+ font-size: 14px;
215
+ }
216
+
217
+ .form-input,
218
+ .form-textarea {
219
+ width: 100%;
220
+ padding: 12px 16px;
221
+ border: 2px solid #e0e0e0;
222
+ border-radius: 8px;
223
+ font-size: 14px;
224
+ font-family: inherit;
225
+ transition: border-color 0.2s ease;
226
+ box-sizing: border-box;
227
+ }
228
+
229
+ .form-input:focus,
230
+ .form-textarea:focus {
231
+ outline: none;
232
+ border-color: #1976d2;
233
+ }
234
+
235
+ .form-textarea {
236
+ resize: vertical;
237
+ min-height: 100px;
238
+ }
239
+
240
+ .error-message {
241
+ color: #d32f2f;
242
+ font-size: 12px;
243
+ margin-top: 4px;
244
+ }
245
+
246
+ .form-actions {
247
+ display: flex;
248
+ justify-content: flex-end;
249
+ }
250
+
251
+ .submit-button {
252
+ background: #1976d2;
253
+ color: white;
254
+ border: none;
255
+ border-radius: 8px;
256
+ padding: 12px 24px;
257
+ font-size: 14px;
258
+ font-weight: 500;
259
+ cursor: pointer;
260
+ transition: background-color 0.2s ease;
261
+ font-family: inherit;
262
+ }
263
+
264
+ .submit-button:hover:not(:disabled) {
265
+ background: #1565c0;
266
+ }
267
+
268
+ .submit-button:disabled {
269
+ background: #ccc;
270
+ cursor: not-allowed;
271
+ }
272
+
273
+ /* Comments Section */
274
+ .comments-section {
275
+ border-top: 1px solid #e0e0e0;
276
+ padding-top: 24px;
277
+ }
278
+
279
+ .comments-title {
280
+ margin: 0 0 16px;
281
+ font-size: 16px;
282
+ font-weight: 600;
283
+ color: #212121;
284
+ }
285
+
286
+ .comments-list {
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 16px;
290
+ }
291
+
292
+ .comment-item {
293
+ background: #f8f9fa;
294
+ border-radius: 8px;
295
+ padding: 16px;
296
+ border-left: 4px solid #1976d2;
297
+ }
298
+
299
+ .comment-header {
300
+ display: flex;
301
+ justify-content: space-between;
302
+ align-items: center;
303
+ margin-bottom: 8px;
304
+ }
305
+
306
+ .comment-email {
307
+ font-weight: 500;
308
+ color: #1976d2;
309
+ font-size: 14px;
310
+ }
311
+
312
+ .comment-time {
313
+ font-size: 12px;
314
+ color: #757575;
315
+ }
316
+
317
+ .comment-text {
318
+ color: #424242;
319
+ line-height: 1.5;
320
+ font-size: 14px;
321
+ }
322
+
323
+ /* Responsive Design */
324
+ @media (max-width: 768px) {
325
+ h1:has(.comment-icon-container),
326
+ h2:has(.comment-icon-container),
327
+ h3:has(.comment-icon-container),
328
+ h4:has(.comment-icon-container) {
329
+ gap: var(--dx-g-spacing-2xs, 2px);
330
+ }
331
+
332
+ @supports not (selector(:has(*))) {
333
+ h1,
334
+ h2,
335
+ h3,
336
+ h4 {
337
+ gap: var(--dx-g-spacing-2xs, 2px);
338
+ }
339
+ }
340
+
341
+ .comment-icon-button {
342
+ width: 24px;
343
+ height: 24px;
344
+ padding: 4px;
345
+ }
346
+
347
+ .comment-count-badge {
348
+ width: 18px;
349
+ height: 18px;
350
+ font-size: 11px;
351
+ top: -6px;
352
+ right: -6px;
353
+ }
354
+
355
+ .popup-overlay {
356
+ padding: 16px;
357
+ }
358
+
359
+ .popup-container {
360
+ max-height: 90vh;
361
+ }
362
+
363
+ .popup-header {
364
+ padding: 16px 16px 0;
365
+ margin-bottom: 16px;
366
+ }
367
+
368
+ .popup-title {
369
+ font-size: 18px;
370
+ }
371
+
372
+ .popup-content {
373
+ padding: 0 16px 16px;
374
+ }
375
+
376
+ .comments-section {
377
+ padding-top: 16px;
378
+ }
379
+ }
380
+
381
+ /* Dark mode support */
382
+ @media (prefers-color-scheme: dark) {
383
+ .popup-container {
384
+ background: #303030;
385
+ color: #fff;
386
+ }
387
+
388
+ .popup-title {
389
+ color: #fff;
390
+ }
391
+
392
+ .popup-header {
393
+ border-bottom-color: #424242;
394
+ }
395
+
396
+ .form-label {
397
+ color: #e0e0e0;
398
+ }
399
+
400
+ .form-input,
401
+ .form-textarea {
402
+ background: #424242;
403
+ border-color: #616161;
404
+ color: #fff;
405
+ }
406
+
407
+ .form-input:focus,
408
+ .form-textarea:focus {
409
+ border-color: #42a5f5;
410
+ }
411
+
412
+ .form-input::placeholder,
413
+ .form-textarea::placeholder {
414
+ color: #bdbdbd;
415
+ }
416
+
417
+ .close-button:hover {
418
+ background: #424242;
419
+ }
420
+
421
+ .comments-section {
422
+ border-top-color: #424242;
423
+ }
424
+
425
+ .comments-title {
426
+ color: #fff;
427
+ }
428
+
429
+ .comment-item {
430
+ background: #424242;
431
+ border-left-color: #42a5f5;
432
+ }
433
+
434
+ .comment-email {
435
+ color: #42a5f5;
436
+ }
437
+
438
+ .comment-text {
439
+ color: #e0e0e0;
440
+ }
441
+
442
+ .comment-count-badge {
443
+ border-color: #303030;
444
+ }
445
+
446
+ .submit-button {
447
+ background: #1976d2;
448
+ color: white;
449
+ }
450
+
451
+ .submit-button:hover:not(:disabled) {
452
+ background: #1565c0;
453
+ }
454
+
455
+ .submit-button:disabled {
456
+ background: #555;
457
+ color: #999;
458
+ }
459
+ }
@@ -1,14 +1,176 @@
1
1
  <template>
2
2
  <h1 class={className} if:true={isAriaLevelOne}>
3
+ <!-- Comment Popup -->
4
+ <template if:true={hasCommentPopup}>
5
+ <div class="comment-icon-container">
6
+ <button
7
+ class="comment-icon-button"
8
+ onclick={handleIconClick}
9
+ aria-label="Open comment popup"
10
+ >
11
+ <dx-icon symbol={iconSymbol} size={iconSize}></dx-icon>
12
+ </button>
13
+ <div if:true={showCommentCount} class="comment-count-badge">
14
+ {commentCount}
15
+ </div>
16
+ </div>
17
+ </template>
3
18
  <doc-heading-content header={header} hash={hash}></doc-heading-content>
4
19
  </h1>
5
20
  <h2 class={className} if:true={isAriaLevelTwo}>
21
+ <!-- Comment Popup -->
22
+ <template if:true={hasCommentPopup}>
23
+ <div class="comment-icon-container">
24
+ <button
25
+ class="comment-icon-button"
26
+ onclick={handleIconClick}
27
+ aria-label="Open comment popup"
28
+ >
29
+ <dx-icon symbol={iconSymbol} size={iconSize}></dx-icon>
30
+ </button>
31
+ <div if:true={showCommentCount} class="comment-count-badge">
32
+ {commentCount}
33
+ </div>
34
+ </div>
35
+ </template>
6
36
  <doc-heading-content header={header} hash={hash}></doc-heading-content>
7
37
  </h2>
8
38
  <h3 class={className} if:true={isAriaLevelThree}>
39
+ <!-- Comment Popup -->
40
+ <template if:true={hasCommentPopup}>
41
+ <div class="comment-icon-container">
42
+ <button
43
+ class="comment-icon-button"
44
+ onclick={handleIconClick}
45
+ aria-label="Open comment popup"
46
+ >
47
+ <dx-icon symbol={iconSymbol} size={iconSize}></dx-icon>
48
+ </button>
49
+ <div if:true={showCommentCount} class="comment-count-badge">
50
+ {commentCount}
51
+ </div>
52
+ </div>
53
+ </template>
9
54
  <doc-heading-content header={header} hash={hash}></doc-heading-content>
10
55
  </h3>
11
56
  <h4 class={className} if:true={isAriaLevelFour}>
57
+ <!-- Comment Popup -->
58
+ <template if:true={hasCommentPopup}>
59
+ <div class="comment-icon-container">
60
+ <button
61
+ class="comment-icon-button"
62
+ onclick={handleIconClick}
63
+ aria-label="Open comment popup"
64
+ >
65
+ <dx-icon symbol={iconSymbol} size={iconSize}></dx-icon>
66
+ </button>
67
+ <div if:true={showCommentCount} class="comment-count-badge">
68
+ {commentCount}
69
+ </div>
70
+ </div>
71
+ </template>
12
72
  <doc-heading-content header={header} hash={hash}></doc-heading-content>
13
73
  </h4>
74
+
75
+ <!-- Popup Overlay -->
76
+ <div if:true={open} class="popup-overlay" onclick={handleOverlayClick}>
77
+ <div
78
+ class="popup-container"
79
+ role="dialog"
80
+ aria-modal="true"
81
+ aria-labelledby="popup-title"
82
+ >
83
+ <!-- Popup Header -->
84
+ <div class="popup-header">
85
+ <h3 id="popup-title" class="popup-title">{popupTitle}</h3>
86
+ <button
87
+ class="close-button"
88
+ onclick={handleClose}
89
+ aria-label="Close popup"
90
+ >
91
+ <dx-icon symbol="close" size="small"></dx-icon>
92
+ </button>
93
+ </div>
94
+
95
+ <!-- Comment Form -->
96
+ <div class="popup-content">
97
+ <form class="comment-form" onsubmit={handleSubmit}>
98
+ <div class="form-group">
99
+ <label for="email-input" class="form-label">
100
+ Email
101
+ </label>
102
+ <input
103
+ id="email-input"
104
+ type="email"
105
+ class="form-input"
106
+ placeholder={emailPlaceholder}
107
+ value={email}
108
+ oninput={handleEmailChange}
109
+ required
110
+ />
111
+ <div if:true={emailError} class="error-message">
112
+ {emailError}
113
+ </div>
114
+ </div>
115
+
116
+ <div class="form-group">
117
+ <label for="comment-input" class="form-label">
118
+ Comment
119
+ </label>
120
+ <textarea
121
+ id="comment-input"
122
+ class="form-textarea"
123
+ placeholder={commentPlaceholder}
124
+ value={comment}
125
+ oninput={handleCommentChange}
126
+ rows="4"
127
+ required
128
+ ></textarea>
129
+ <div if:true={commentError} class="error-message">
130
+ {commentError}
131
+ </div>
132
+ </div>
133
+
134
+ <div class="form-actions">
135
+ <button
136
+ type="submit"
137
+ class="submit-button"
138
+ disabled={submitButtonDisabled}
139
+ onclick={handleSubmit}
140
+ >
141
+ <span if:false={isSubmitting}>
142
+ {submitButtonLabel}
143
+ </span>
144
+ <span if:true={isSubmitting}>Posting...</span>
145
+ </button>
146
+ </div>
147
+ </form>
148
+
149
+ <!-- Comments List -->
150
+ <div if:true={showComments} class="comments-section">
151
+ <h4 class="comments-title">Comments</h4>
152
+ <div class="comments-list">
153
+ <template
154
+ for:each={formattedComments}
155
+ for:item="comment"
156
+ >
157
+ <div key={comment.email} class="comment-item">
158
+ <div class="comment-header">
159
+ <span class="comment-email">
160
+ {comment.maskedEmail}
161
+ </span>
162
+ <span class="comment-time">
163
+ {comment.formattedTimestamp}
164
+ </span>
165
+ </div>
166
+ <div class="comment-text">
167
+ {comment.comment}
168
+ </div>
169
+ </div>
170
+ </template>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
14
176
  </template>
@@ -1,4 +1,12 @@
1
- import { LightningElement, api } from "lwc";
1
+ import { LightningElement, api, track } from "lwc";
2
+ import { normalizeBoolean } from "dxUtils/normalizers";
3
+
4
+ interface Comment {
5
+ email: string;
6
+ comment: string;
7
+ url: string;
8
+ timestamp: Date;
9
+ }
2
10
 
3
11
  export const ariaDisplayLevels: { [key: string]: string } = {
4
12
  "1": "4",
@@ -16,6 +24,38 @@ export default class Heading extends LightningElement {
16
24
  @api header: string = "";
17
25
  @api hash: string | null = null;
18
26
 
27
+ // Comment popup attributes
28
+ @api index?: string;
29
+ @api contentType?: string;
30
+ @api headingTitle?: string;
31
+ @api filePath?: string;
32
+ @api startLine?: string;
33
+ @api endLine?: string;
34
+ @api currentBranch?: string;
35
+
36
+ // Comment popup properties
37
+ @api iconSymbol = "chat";
38
+ @api iconSize = "medium";
39
+ @api popupTitle = "Leave a Comment";
40
+ @api submitButtonLabel = "Post Comment";
41
+ @api emailPlaceholder = "Enter your email";
42
+ @api commentPlaceholder = "Enter your comment";
43
+
44
+ private _open = false;
45
+ @api get open() {
46
+ return this._open;
47
+ }
48
+ set open(value) {
49
+ this._open = normalizeBoolean(value);
50
+ }
51
+
52
+ @track private comments: Comment[] = [];
53
+ @track private email = "";
54
+ @track private comment = "";
55
+ @track private emailError = "";
56
+ @track private commentError = "";
57
+ @track private isSubmitting = false;
58
+
19
59
  @api
20
60
  private get ariaLevel(): string {
21
61
  // Really Dark Magic (TM)
@@ -64,4 +104,190 @@ export default class Heading extends LightningElement {
64
104
  private get className(): string {
65
105
  return `dx-text-display-${this.displayLevel}`;
66
106
  }
107
+
108
+ // Comment popup getters
109
+ get showComments() {
110
+ const unwrapped = JSON.parse(JSON.stringify(this.comments));
111
+ console.log("Comments (JSON unwrapped):", unwrapped);
112
+ return this.comments.length > 0;
113
+ }
114
+
115
+ get commentCount() {
116
+ return this.comments.length;
117
+ }
118
+
119
+ get showCommentCount() {
120
+ return this.commentCount > 0;
121
+ }
122
+
123
+ get sortedComments() {
124
+ return [...this.comments].sort(
125
+ (a, b) => b.timestamp.getTime() - a.timestamp.getTime()
126
+ );
127
+ }
128
+
129
+ get isFormValid() {
130
+ return this.email.trim() !== "" && this.comment.trim() !== "";
131
+ }
132
+
133
+ get submitButtonDisabled() {
134
+ return !this.isFormValid || this.isSubmitting;
135
+ }
136
+
137
+ get formattedComments() {
138
+ return this.sortedComments.map((comment) => ({
139
+ ...comment,
140
+ formattedTimestamp: this.formatTimestamp(comment.timestamp),
141
+ maskedEmail: this.maskEmail(comment.email)
142
+ }));
143
+ }
144
+
145
+ get hasCommentPopup() {
146
+ return this.index !== undefined;
147
+ }
148
+
149
+ // Comment popup handlers
150
+ handleIconClick() {
151
+ this._open = !this._open;
152
+ }
153
+
154
+ handleEmailChange(event: Event) {
155
+ this.email = (event.target as HTMLInputElement).value;
156
+ this.emailError = "";
157
+ }
158
+
159
+ handleCommentChange(event: Event) {
160
+ this.comment = (event.target as HTMLTextAreaElement).value;
161
+ this.commentError = "";
162
+ }
163
+
164
+ handleSubmit() {
165
+ if (!this.validateForm()) {
166
+ return;
167
+ }
168
+
169
+ this.isSubmitting = true;
170
+
171
+ // Simulate API call
172
+ setTimeout(() => {
173
+ this.addComment();
174
+ this.resetForm();
175
+ this.isSubmitting = false;
176
+ }, 500);
177
+ }
178
+
179
+ private validateForm(): boolean {
180
+ let isValid = true;
181
+
182
+ if (!this.email.trim()) {
183
+ this.emailError = "Email is required";
184
+ isValid = false;
185
+ } else if (!this.isValidEmail(this.email)) {
186
+ this.emailError = "Please enter a valid email address";
187
+ isValid = false;
188
+ }
189
+
190
+ if (!this.comment.trim()) {
191
+ this.commentError = "Comment is required";
192
+ isValid = false;
193
+ }
194
+
195
+ return isValid;
196
+ }
197
+
198
+ private isValidEmail(email: string): boolean {
199
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
200
+ return emailRegex.test(email);
201
+ }
202
+
203
+ private addComment() {
204
+ const newComment: Comment = {
205
+ email: this.email.trim(),
206
+ comment: this.comment.trim(),
207
+ url: window.location.href,
208
+ timestamp: new Date()
209
+ };
210
+
211
+ this.comments = [newComment, ...this.comments];
212
+
213
+ // Dispatch custom event
214
+ this.dispatchEvent(
215
+ new CustomEvent("commentadded", {
216
+ detail: newComment
217
+ })
218
+ );
219
+ }
220
+
221
+ private resetForm() {
222
+ this.email = "";
223
+ this.comment = "";
224
+ this.emailError = "";
225
+ this.commentError = "";
226
+
227
+ // Force DOM update by accessing the form elements
228
+ const emailInput = this.template.querySelector(
229
+ "#email-input"
230
+ ) as HTMLInputElement;
231
+ const commentInput = this.template.querySelector(
232
+ "#comment-input"
233
+ ) as HTMLTextAreaElement;
234
+
235
+ if (emailInput) {
236
+ emailInput.value = "";
237
+ }
238
+ if (commentInput) {
239
+ commentInput.value = "";
240
+ }
241
+ }
242
+
243
+ handleClose() {
244
+ this._open = false;
245
+ this.resetForm();
246
+ }
247
+
248
+ handleOverlayClick(event: Event) {
249
+ if (event.target === event.currentTarget) {
250
+ this.handleClose();
251
+ }
252
+ }
253
+
254
+ handleKeyDown(event: KeyboardEvent) {
255
+ if (event.key === "Escape") {
256
+ this.handleClose();
257
+ }
258
+ }
259
+
260
+ connectedCallback() {
261
+ document.addEventListener("keydown", this.handleKeyDown.bind(this));
262
+ }
263
+
264
+ disconnectedCallback() {
265
+ document.removeEventListener("keydown", this.handleKeyDown.bind(this));
266
+ }
267
+
268
+ private formatTimestamp(timestamp: Date): string {
269
+ const now = new Date();
270
+ const diff = now.getTime() - timestamp.getTime();
271
+ const minutes = Math.floor(diff / 60000);
272
+ const hours = Math.floor(diff / 3600000);
273
+ const days = Math.floor(diff / 86400000);
274
+
275
+ if (minutes < 1) {
276
+ return "Just now";
277
+ } else if (minutes < 60) {
278
+ return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
279
+ } else if (hours < 24) {
280
+ return `${hours} hour${hours > 1 ? "s" : ""} ago`;
281
+ }
282
+ return `${days} day${days > 1 ? "s" : ""} ago`;
283
+ }
284
+
285
+ private maskEmail(email: string): string {
286
+ const [localPart, domain] = email.split("@");
287
+ const maskedLocal =
288
+ localPart.length > 2
289
+ ? localPart.substring(0, 2) + "*".repeat(localPart.length - 2)
290
+ : localPart;
291
+ return `${maskedLocal}@${domain}`;
292
+ }
67
293
  }