gh-here 3.0.3 → 3.1.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.
Files changed (41) hide show
  1. package/.env +0 -0
  2. package/.playwright-mcp/fixed-alignment.png +0 -0
  3. package/.playwright-mcp/fixed-layout.png +0 -0
  4. package/.playwright-mcp/gh-here-home-header-table.png +0 -0
  5. package/.playwright-mcp/gh-here-home.png +0 -0
  6. package/.playwright-mcp/line-selection-multiline.png +0 -0
  7. package/.playwright-mcp/line-selection-test-after.png +0 -0
  8. package/.playwright-mcp/line-selection-test-before.png +0 -0
  9. package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
  10. package/lib/constants.js +25 -15
  11. package/lib/content-search.js +212 -0
  12. package/lib/error-handler.js +39 -28
  13. package/lib/file-utils.js +438 -287
  14. package/lib/git.js +10 -54
  15. package/lib/gitignore.js +70 -41
  16. package/lib/renderers.js +15 -19
  17. package/lib/server.js +70 -193
  18. package/lib/symbol-parser.js +600 -0
  19. package/package.json +1 -1
  20. package/public/app.js +134 -68
  21. package/public/js/constants.js +50 -34
  22. package/public/js/content-search-handler.js +551 -0
  23. package/public/js/file-viewer.js +437 -0
  24. package/public/js/focus-mode.js +280 -0
  25. package/public/js/inline-search.js +659 -0
  26. package/public/js/modal-manager.js +14 -28
  27. package/public/js/symbol-outline.js +454 -0
  28. package/public/js/utils.js +152 -94
  29. package/public/styles.css +2049 -296
  30. package/.claude/settings.local.json +0 -30
  31. package/SAMPLE.md +0 -287
  32. package/lib/validation.js +0 -77
  33. package/public/app.js.backup +0 -1902
  34. package/public/js/draft-manager.js +0 -36
  35. package/public/js/editor-manager.js +0 -159
  36. package/test.js +0 -138
  37. package/tests/draftManager.test.js +0 -241
  38. package/tests/fileTypeDetection.test.js +0 -111
  39. package/tests/httpService.test.js +0 -268
  40. package/tests/languageDetection.test.js +0 -145
  41. package/tests/pathUtils.test.js +0 -136
@@ -0,0 +1,659 @@
1
+ /**
2
+ * In-file search functionality
3
+ * Search within the current file with match highlighting
4
+ *
5
+ * @class InlineSearch
6
+ */
7
+
8
+ import { escapeHtml } from './utils.js';
9
+
10
+ // ============================================================================
11
+ // Constants (alpha-sorted)
12
+ // ============================================================================
13
+
14
+ const CODE_BLOCK_SELECTOR = '.file-content pre code.hljs.with-line-numbers';
15
+ const DEBOUNCE_DELAY = 200;
16
+ const SEARCH_OVERLAY_CLASS = 'inline-search-overlay';
17
+
18
+ // ============================================================================
19
+ // InlineSearch Class
20
+ // ============================================================================
21
+
22
+ export class InlineSearch {
23
+ constructor() {
24
+ this.isActive = false;
25
+ this.searchOverlay = null;
26
+ this.searchInput = null;
27
+ this.currentMatchIndex = -1;
28
+ this.matches = [];
29
+ this.caseSensitive = false;
30
+ this.regexMode = false;
31
+ this.originalContent = new Map();
32
+ this.eventHandlers = new Map();
33
+ this.debounceTimer = null;
34
+ }
35
+
36
+ // ========================================================================
37
+ // Public API
38
+ // ========================================================================
39
+
40
+ /**
41
+ * Initialize inline search
42
+ */
43
+ init() {
44
+ if (!this.isFileViewPage()) return;
45
+
46
+ // Skip if Monaco viewer is active (Monaco has built-in search)
47
+ if (document.querySelector('.monaco-file-viewer')) {
48
+ return;
49
+ }
50
+
51
+ this.storeOriginalContent();
52
+ this.setupKeyboardShortcuts();
53
+ }
54
+
55
+ /**
56
+ * Show search overlay
57
+ */
58
+ show() {
59
+ if (this.isActive) {
60
+ this.focusSearchInput();
61
+ return;
62
+ }
63
+
64
+ this.isActive = true;
65
+ this.createSearchOverlay();
66
+ document.body.appendChild(this.searchOverlay);
67
+ this.focusSearchInput();
68
+ }
69
+
70
+ /**
71
+ * Hide search overlay and restore original content
72
+ */
73
+ hide() {
74
+ if (!this.isActive) return;
75
+
76
+ this.clearHighlights();
77
+ this.cleanupDebounce();
78
+ this.removeSearchOverlay();
79
+ this.resetState();
80
+ }
81
+
82
+ /**
83
+ * Cleanup: Remove event listeners and restore content
84
+ */
85
+ destroy() {
86
+ this.hide();
87
+ this.removeEventListeners();
88
+ this.originalContent.clear();
89
+ this.matches = [];
90
+ }
91
+
92
+ // ========================================================================
93
+ // Search Operations
94
+ // ========================================================================
95
+
96
+ /**
97
+ * Perform search and highlight matches
98
+ */
99
+ performSearch() {
100
+ if (!this.searchInput) return;
101
+
102
+ const query = this.searchInput.value.trim();
103
+
104
+ if (!query) {
105
+ this.clearHighlights();
106
+ this.updateMatchCount(0);
107
+ return;
108
+ }
109
+
110
+ this.clearHighlights();
111
+ this.resetMatches();
112
+
113
+ const codeBlock = this.findCodeBlock();
114
+ if (!codeBlock) return;
115
+
116
+ const pattern = this.createSearchPattern(query);
117
+ if (!pattern) return;
118
+
119
+ this.findMatches(codeBlock, pattern);
120
+ this.displayMatches();
121
+ }
122
+
123
+ /**
124
+ * Create search pattern from query
125
+ */
126
+ createSearchPattern(query) {
127
+ try {
128
+ if (this.regexMode) {
129
+ return new RegExp(query, this.caseSensitive ? 'g' : 'gi');
130
+ } else {
131
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
132
+ return new RegExp(escaped, this.caseSensitive ? 'g' : 'gi');
133
+ }
134
+ } catch (error) {
135
+ this.updateMatchCount(0, 'Invalid regular expression');
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Find all matches in code block
142
+ */
143
+ findMatches(codeBlock, pattern) {
144
+ const lines = codeBlock.querySelectorAll('.line-container');
145
+
146
+ lines.forEach((line, lineIndex) => {
147
+ const lineContent = line.querySelector('.line-content');
148
+ if (!lineContent) return;
149
+
150
+ const text = lineContent.textContent;
151
+ const lineMatches = this.findLineMatches(text, pattern, lineIndex + 1, line, lineContent);
152
+ this.matches.push(...lineMatches);
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Find matches in a single line
158
+ */
159
+ findLineMatches(text, pattern, lineNum, lineElement, contentElement) {
160
+ const matches = [];
161
+
162
+ try {
163
+ const patternMatches = [...text.matchAll(pattern)];
164
+
165
+ patternMatches.forEach(match => {
166
+ if (match.index !== undefined && match[0]) {
167
+ matches.push({
168
+ line: lineNum,
169
+ lineElement,
170
+ contentElement,
171
+ index: match.index,
172
+ length: match[0].length
173
+ });
174
+ }
175
+ });
176
+ } catch (error) {
177
+ // Handle regex errors gracefully
178
+ }
179
+
180
+ return matches;
181
+ }
182
+
183
+ /**
184
+ * Display matches with highlighting
185
+ */
186
+ displayMatches() {
187
+ if (this.matches.length > 0) {
188
+ this.highlightMatches();
189
+ this.currentMatchIndex = 0;
190
+ this.scrollToMatch(0);
191
+ this.updateMatchCount(this.matches.length, null, 1);
192
+ } else {
193
+ this.updateMatchCount(0);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Highlight all matches in the code
199
+ */
200
+ highlightMatches() {
201
+ const matchesByLine = this.groupMatchesByLine();
202
+
203
+ matchesByLine.forEach((lineMatches, lineNum) => {
204
+ const match = this.matches.find(m => m.line === lineNum);
205
+ if (!match?.contentElement) return;
206
+
207
+ const highlighted = this.buildHighlightedHTML(match.contentElement.textContent, lineMatches);
208
+ match.contentElement.innerHTML = highlighted;
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Group matches by line number
214
+ */
215
+ groupMatchesByLine() {
216
+ const matchesByLine = new Map();
217
+
218
+ this.matches.forEach((match, index) => {
219
+ if (!matchesByLine.has(match.line)) {
220
+ matchesByLine.set(match.line, []);
221
+ }
222
+ matchesByLine.get(match.line).push({ ...match, globalIndex: index });
223
+ });
224
+
225
+ return matchesByLine;
226
+ }
227
+
228
+ /**
229
+ * Build highlighted HTML for a line
230
+ */
231
+ buildHighlightedHTML(text, lineMatches) {
232
+ let highlighted = '';
233
+ let lastIndex = 0;
234
+
235
+ const sortedMatches = lineMatches.sort((a, b) => a.index - b.index);
236
+
237
+ sortedMatches.forEach((m) => {
238
+ highlighted += escapeHtml(text.substring(lastIndex, m.index));
239
+
240
+ const isActive = m.globalIndex === this.currentMatchIndex;
241
+ const matchText = text.substring(m.index, m.index + m.length);
242
+ highlighted += `<mark class="inline-search-match ${isActive ? 'active' : ''}" data-match-index="${m.globalIndex}">${escapeHtml(matchText)}</mark>`;
243
+
244
+ lastIndex = m.index + m.length;
245
+ });
246
+
247
+ highlighted += escapeHtml(text.substring(lastIndex));
248
+ return highlighted;
249
+ }
250
+
251
+ /**
252
+ * Clear highlights and restore original content
253
+ */
254
+ clearHighlights() {
255
+ const codeBlock = this.findCodeBlock();
256
+ if (!codeBlock) return;
257
+
258
+ const lines = codeBlock.querySelectorAll('.line-container');
259
+ lines.forEach((line, index) => {
260
+ const lineContent = line.querySelector('.line-content');
261
+ if (!lineContent) return;
262
+
263
+ const original = this.originalContent.get(index + 1);
264
+ if (original !== undefined) {
265
+ lineContent.innerHTML = original;
266
+ } else {
267
+ lineContent.textContent = lineContent.textContent;
268
+ }
269
+ });
270
+ }
271
+
272
+ // ========================================================================
273
+ // Navigation
274
+ // ========================================================================
275
+
276
+ /**
277
+ * Navigate to next match
278
+ */
279
+ nextMatch() {
280
+ if (this.matches.length === 0) return;
281
+
282
+ this.currentMatchIndex = (this.currentMatchIndex + 1) % this.matches.length;
283
+ this.navigateToMatch(this.currentMatchIndex);
284
+ }
285
+
286
+ /**
287
+ * Navigate to previous match
288
+ */
289
+ previousMatch() {
290
+ if (this.matches.length === 0) return;
291
+
292
+ this.currentMatchIndex = this.currentMatchIndex <= 0
293
+ ? this.matches.length - 1
294
+ : this.currentMatchIndex - 1;
295
+ this.navigateToMatch(this.currentMatchIndex);
296
+ }
297
+
298
+ /**
299
+ * Navigate to a specific match
300
+ */
301
+ navigateToMatch(index) {
302
+ this.scrollToMatch(index);
303
+ this.updateActiveMatch();
304
+ this.updateMatchCount(this.matches.length, null, index + 1);
305
+ }
306
+
307
+ /**
308
+ * Scroll to a specific match
309
+ */
310
+ scrollToMatch(index) {
311
+ if (index < 0 || index >= this.matches.length) return;
312
+
313
+ const match = this.matches[index];
314
+ if (match?.lineElement) {
315
+ match.lineElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Update active match highlighting
321
+ */
322
+ updateActiveMatch() {
323
+ document.querySelectorAll('.inline-search-match').forEach(m => {
324
+ m.classList.remove('active');
325
+ });
326
+
327
+ const activeMatch = document.querySelector(
328
+ `.inline-search-match[data-match-index="${this.currentMatchIndex}"]`
329
+ );
330
+ if (activeMatch) {
331
+ activeMatch.classList.add('active');
332
+ this.highlightMatches(); // Re-highlight to update active state
333
+ }
334
+ }
335
+
336
+ // ========================================================================
337
+ // UI Management
338
+ // ========================================================================
339
+
340
+ /**
341
+ * Create search overlay UI
342
+ */
343
+ createSearchOverlay() {
344
+ this.searchOverlay = document.createElement('div');
345
+ this.searchOverlay.className = SEARCH_OVERLAY_CLASS;
346
+ this.searchOverlay.setAttribute('role', 'dialog');
347
+ this.searchOverlay.setAttribute('aria-label', 'Search in file');
348
+ this.searchOverlay.innerHTML = this.getSearchOverlayHTML();
349
+
350
+ this.attachSearchOverlayHandlers();
351
+ }
352
+
353
+ /**
354
+ * Get search overlay HTML template
355
+ */
356
+ getSearchOverlayHTML() {
357
+ return `
358
+ <div class="inline-search-container">
359
+ <div class="inline-search-input-wrapper">
360
+ <input
361
+ type="text"
362
+ class="inline-search-input"
363
+ placeholder="Search in file..."
364
+ autocomplete="off"
365
+ aria-label="Search query"
366
+ />
367
+ <div class="inline-search-options">
368
+ <button class="inline-search-option" data-option="case" title="Case sensitive" aria-pressed="false">
369
+ <span class="option-label">Aa</span>
370
+ </button>
371
+ <button class="inline-search-option" data-option="regex" title="Use regular expression" aria-pressed="false">
372
+ <span class="option-label">.*</span>
373
+ </button>
374
+ </div>
375
+ </div>
376
+ <div class="inline-search-info">
377
+ <span class="inline-search-count">No matches</span>
378
+ <button class="inline-search-close" aria-label="Close search">×</button>
379
+ </div>
380
+ </div>
381
+ `;
382
+ }
383
+
384
+ /**
385
+ * Attach event handlers to search overlay elements
386
+ */
387
+ attachSearchOverlayHandlers() {
388
+ this.searchInput = this.searchOverlay.querySelector('.inline-search-input');
389
+ const closeBtn = this.searchOverlay.querySelector('.inline-search-close');
390
+ const caseBtn = this.searchOverlay.querySelector('[data-option="case"]');
391
+ const regexBtn = this.searchOverlay.querySelector('[data-option="regex"]');
392
+
393
+ this.attachCloseHandler(closeBtn);
394
+ this.attachCaseHandler(caseBtn);
395
+ this.attachRegexHandler(regexBtn);
396
+ this.attachInputHandler(this.searchInput);
397
+ this.updateButtonStates(caseBtn, regexBtn);
398
+ }
399
+
400
+ /**
401
+ * Attach close button handler
402
+ */
403
+ attachCloseHandler(closeBtn) {
404
+ const handler = () => this.hide();
405
+ closeBtn.addEventListener('click', handler);
406
+ this.eventHandlers.set('close', handler);
407
+ }
408
+
409
+ /**
410
+ * Attach case sensitive toggle handler
411
+ */
412
+ attachCaseHandler(caseBtn) {
413
+ const handler = () => {
414
+ this.caseSensitive = !this.caseSensitive;
415
+ caseBtn.classList.toggle('active', this.caseSensitive);
416
+ caseBtn.setAttribute('aria-pressed', String(this.caseSensitive));
417
+ this.performSearch();
418
+ };
419
+ caseBtn.addEventListener('click', handler);
420
+ this.eventHandlers.set('case', handler);
421
+ }
422
+
423
+ /**
424
+ * Attach regex toggle handler
425
+ */
426
+ attachRegexHandler(regexBtn) {
427
+ const handler = () => {
428
+ this.regexMode = !this.regexMode;
429
+ regexBtn.classList.toggle('active', this.regexMode);
430
+ regexBtn.setAttribute('aria-pressed', String(this.regexMode));
431
+ this.performSearch();
432
+ };
433
+ regexBtn.addEventListener('click', handler);
434
+ this.eventHandlers.set('regex', handler);
435
+ }
436
+
437
+ /**
438
+ * Attach input handler with debounce
439
+ */
440
+ attachInputHandler(input) {
441
+ const handler = () => {
442
+ this.cleanupDebounce();
443
+ this.debounceTimer = setTimeout(() => {
444
+ this.performSearch();
445
+ }, DEBOUNCE_DELAY);
446
+ };
447
+ input.addEventListener('input', handler);
448
+ this.eventHandlers.set('input', handler);
449
+ }
450
+
451
+ /**
452
+ * Update button states to reflect current settings
453
+ */
454
+ updateButtonStates(caseBtn, regexBtn) {
455
+ if (this.caseSensitive) {
456
+ caseBtn.classList.add('active');
457
+ caseBtn.setAttribute('aria-pressed', 'true');
458
+ }
459
+ if (this.regexMode) {
460
+ regexBtn.classList.add('active');
461
+ regexBtn.setAttribute('aria-pressed', 'true');
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Update match count display
467
+ */
468
+ updateMatchCount(total, error = null, current = null) {
469
+ const countEl = this.searchOverlay?.querySelector('.inline-search-count');
470
+ if (!countEl) return;
471
+
472
+ if (error) {
473
+ countEl.textContent = error;
474
+ countEl.classList.add('error');
475
+ } else if (total === 0) {
476
+ countEl.textContent = 'No matches';
477
+ countEl.classList.remove('error');
478
+ } else if (current !== null) {
479
+ countEl.textContent = `${current} of ${total}`;
480
+ countEl.classList.remove('error');
481
+ } else {
482
+ countEl.textContent = `${total} match${total !== 1 ? 'es' : ''}`;
483
+ countEl.classList.remove('error');
484
+ }
485
+ }
486
+
487
+ // ========================================================================
488
+ // Event Handling
489
+ // ========================================================================
490
+
491
+ /**
492
+ * Setup keyboard shortcuts
493
+ */
494
+ setupKeyboardShortcuts() {
495
+ const keyHandler = (e) => this.handleKeydown(e);
496
+ document.addEventListener('keydown', keyHandler);
497
+ this.eventHandlers.set('keydown', keyHandler);
498
+ }
499
+
500
+ /**
501
+ * Handle keyboard events
502
+ */
503
+ handleKeydown(e) {
504
+ // Allow Cmd/Ctrl+F in inputs
505
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
506
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
507
+ e.preventDefault();
508
+ this.show();
509
+ }
510
+ return;
511
+ }
512
+
513
+ // Cmd/Ctrl+F: Show search
514
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
515
+ e.preventDefault();
516
+ this.show();
517
+ return;
518
+ }
519
+
520
+ // Only handle other shortcuts when search is active
521
+ if (!this.isActive) return;
522
+
523
+ // Escape: Close search
524
+ if (e.key === 'Escape') {
525
+ e.preventDefault();
526
+ this.hide();
527
+ return;
528
+ }
529
+
530
+ // Cmd/Ctrl+G: Next match
531
+ if ((e.ctrlKey || e.metaKey) && e.key === 'g' && !e.shiftKey) {
532
+ e.preventDefault();
533
+ this.nextMatch();
534
+ return;
535
+ }
536
+
537
+ // Cmd/Ctrl+Shift+G: Previous match
538
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'G') {
539
+ e.preventDefault();
540
+ this.previousMatch();
541
+ return;
542
+ }
543
+
544
+ // Enter: Next match
545
+ if (e.key === 'Enter' && !e.shiftKey) {
546
+ e.preventDefault();
547
+ this.nextMatch();
548
+ return;
549
+ }
550
+
551
+ // Shift+Enter: Previous match
552
+ if (e.key === 'Enter' && e.shiftKey) {
553
+ e.preventDefault();
554
+ this.previousMatch();
555
+ return;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Remove all event listeners
561
+ */
562
+ removeEventListeners() {
563
+ this.eventHandlers.forEach((handler, event) => {
564
+ if (event === 'keydown') {
565
+ document.removeEventListener(event, handler);
566
+ }
567
+ });
568
+ this.eventHandlers.clear();
569
+ }
570
+
571
+ // ========================================================================
572
+ // State Management
573
+ // ========================================================================
574
+
575
+ /**
576
+ * Store original line content for restoration
577
+ */
578
+ storeOriginalContent() {
579
+ const codeBlock = this.findCodeBlock();
580
+ if (!codeBlock) return;
581
+
582
+ const lines = codeBlock.querySelectorAll('.line-container');
583
+ lines.forEach((line, index) => {
584
+ const lineContent = line.querySelector('.line-content');
585
+ if (lineContent) {
586
+ this.originalContent.set(index + 1, lineContent.innerHTML);
587
+ }
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Reset search state
593
+ */
594
+ resetState() {
595
+ this.isActive = false;
596
+ this.matches = [];
597
+ this.currentMatchIndex = -1;
598
+ this.searchInput = null;
599
+ }
600
+
601
+ /**
602
+ * Reset matches array
603
+ */
604
+ resetMatches() {
605
+ this.matches = [];
606
+ this.currentMatchIndex = -1;
607
+ }
608
+
609
+ // ========================================================================
610
+ // Utilities
611
+ // ========================================================================
612
+
613
+ /**
614
+ * Check if current page is a file view page
615
+ */
616
+ isFileViewPage() {
617
+ const fileContent = document.querySelector('.file-content');
618
+ return fileContent?.querySelector(CODE_BLOCK_SELECTOR) !== null;
619
+ }
620
+
621
+ /**
622
+ * Find the code block element
623
+ */
624
+ findCodeBlock() {
625
+ return document.querySelector(CODE_BLOCK_SELECTOR);
626
+ }
627
+
628
+ /**
629
+ * Focus search input
630
+ */
631
+ focusSearchInput() {
632
+ if (!this.searchInput) return;
633
+
634
+ requestAnimationFrame(() => {
635
+ this.searchInput.focus();
636
+ this.searchInput.select();
637
+ });
638
+ }
639
+
640
+ /**
641
+ * Remove search overlay from DOM
642
+ */
643
+ removeSearchOverlay() {
644
+ if (this.searchOverlay) {
645
+ this.searchOverlay.remove();
646
+ this.searchOverlay = null;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Cleanup debounce timer
652
+ */
653
+ cleanupDebounce() {
654
+ if (this.debounceTimer) {
655
+ clearTimeout(this.debounceTimer);
656
+ this.debounceTimer = null;
657
+ }
658
+ }
659
+ }
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * Modal management utilities
3
+ * @module modal-manager
3
4
  */
4
5
 
6
+ /**
7
+ * Creates a modal overlay with content
8
+ * @param {string} content - HTML content for the modal
9
+ * @param {string} [className=''] - Additional CSS classes
10
+ * @returns {{element: HTMLElement, close: Function}} Modal control object
11
+ */
5
12
  export function createModal(content, className = '') {
6
13
  const modal = document.createElement('div');
7
14
  modal.className = `modal-overlay ${className}`;
@@ -15,34 +22,13 @@ export function createModal(content, className = '') {
15
22
  };
16
23
  }
17
24
 
18
- export function showDraftDialog(filePath) {
19
- return new Promise(resolve => {
20
- const modal = document.createElement('div');
21
- modal.className = 'modal-overlay';
22
- modal.innerHTML = `
23
- <div class="modal-content draft-modal">
24
- <h3>Unsaved Changes Found</h3>
25
- <p>You have unsaved changes for this file. What would you like to do?</p>
26
- <div class="draft-actions">
27
- <button class="btn btn-primary" data-action="load">Load Draft</button>
28
- <button class="btn btn-secondary" data-action="discard">Discard Draft</button>
29
- <button class="btn btn-secondary" data-action="cancel">Cancel</button>
30
- </div>
31
- </div>
32
- `;
33
-
34
- modal.addEventListener('click', e => {
35
- if (e.target.matches('[data-action]') || e.target === modal) {
36
- const action = e.target.dataset?.action || 'cancel';
37
- modal.remove();
38
- resolve(action);
39
- }
40
- });
41
-
42
- document.body.appendChild(modal);
43
- });
44
- }
45
-
25
+ /**
26
+ * Shows a confirmation dialog
27
+ * @param {string} message - Message to display
28
+ * @param {string} [confirmText='Confirm'] - Confirm button text
29
+ * @param {string} [cancelText='Cancel'] - Cancel button text
30
+ * @returns {Promise<boolean>} Resolves to true if confirmed
31
+ */
46
32
  export function showConfirmDialog(message, confirmText = 'Confirm', cancelText = 'Cancel') {
47
33
  return new Promise(resolve => {
48
34
  const modal = document.createElement('div');