@customviews-js/customviews 1.2.0 → 1.4.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/dist/custom-views.core.cjs.js +1409 -55
  2. package/dist/custom-views.core.cjs.js.map +1 -1
  3. package/dist/custom-views.core.esm.js +1409 -55
  4. package/dist/custom-views.core.esm.js.map +1 -1
  5. package/dist/custom-views.esm.js +1409 -55
  6. package/dist/custom-views.esm.js.map +1 -1
  7. package/dist/custom-views.js +1409 -55
  8. package/dist/custom-views.js.map +1 -1
  9. package/dist/custom-views.min.js +2 -2
  10. package/dist/custom-views.min.js.map +1 -1
  11. package/dist/types/core/anchor-engine.d.ts +55 -0
  12. package/dist/types/core/anchor-engine.d.ts.map +1 -0
  13. package/dist/types/core/config.d.ts +3 -0
  14. package/dist/types/core/config.d.ts.map +1 -0
  15. package/dist/types/core/core.d.ts +9 -1
  16. package/dist/types/core/core.d.ts.map +1 -1
  17. package/dist/types/core/custom-elements.d.ts +2 -2
  18. package/dist/types/core/custom-elements.d.ts.map +1 -1
  19. package/dist/types/core/focus-manager.d.ts +38 -0
  20. package/dist/types/core/focus-manager.d.ts.map +1 -0
  21. package/dist/types/core/share-manager.d.ts +70 -0
  22. package/dist/types/core/share-manager.d.ts.map +1 -0
  23. package/dist/types/core/toast-manager.d.ts +12 -0
  24. package/dist/types/core/toast-manager.d.ts.map +1 -0
  25. package/dist/types/core/url-state-manager.d.ts +6 -2
  26. package/dist/types/core/url-state-manager.d.ts.map +1 -1
  27. package/dist/types/core/widget.d.ts +1 -0
  28. package/dist/types/core/widget.d.ts.map +1 -1
  29. package/dist/types/styles/focus-mode-styles.d.ts +8 -0
  30. package/dist/types/styles/focus-mode-styles.d.ts.map +1 -0
  31. package/dist/types/styles/share-mode-styles.d.ts +10 -0
  32. package/dist/types/styles/share-mode-styles.d.ts.map +1 -0
  33. package/dist/types/styles/toast-styles.d.ts +4 -0
  34. package/dist/types/styles/toast-styles.d.ts.map +1 -0
  35. package/dist/types/styles/widget-styles.d.ts +1 -1
  36. package/dist/types/styles/widget-styles.d.ts.map +1 -1
  37. package/dist/types/types/types.d.ts +7 -0
  38. package/dist/types/types/types.d.ts.map +1 -1
  39. package/dist/types/utils/icons.d.ts +6 -0
  40. package/dist/types/utils/icons.d.ts.map +1 -1
  41. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.2.0
2
+ * @customviews-js/customviews v1.4.0
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
@@ -154,7 +154,9 @@ class URLStateManager {
154
154
  return url.toString();
155
155
  }
156
156
  /**
157
- * Encode state into URL-safe string (Toggles and Tabs only currently)
157
+ * Encode state into URL-safe string
158
+ *
159
+ * (Covers Toggles, Tabs and Focus currently)
158
160
  */
159
161
  static encodeState(state) {
160
162
  try {
@@ -168,6 +170,10 @@ class URLStateManager {
168
170
  if (state.tabs && Object.keys(state.tabs).length > 0) {
169
171
  compact.g = Object.entries(state.tabs);
170
172
  }
173
+ // Add focus if present
174
+ if (state.focus && state.focus.length > 0) {
175
+ compact.f = state.focus;
176
+ }
171
177
  // Convert to JSON and encode
172
178
  const json = JSON.stringify(compact);
173
179
  let encoded;
@@ -189,7 +195,9 @@ class URLStateManager {
189
195
  }
190
196
  }
191
197
  /**
192
- * Decode custom state from URL parameter (Toggles and Tabs only currently)
198
+ * Decode custom state from URL parameter
199
+ *
200
+ * (Covers Toggles, Tabs and Focus currently)
193
201
  */
194
202
  static decodeState(encoded) {
195
203
  try {
@@ -228,6 +236,10 @@ class URLStateManager {
228
236
  }
229
237
  }
230
238
  }
239
+ // Reconstruct Focus
240
+ if (Array.isArray(compact.f)) {
241
+ state.focus = compact.f;
242
+ }
231
243
  return state;
232
244
  }
233
245
  catch (error) {
@@ -412,6 +424,20 @@ function getPinIcon(isPinned = false) {
412
424
  </svg>
413
425
  `.trim();
414
426
  }
427
+ function getShareIcon() {
428
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
429
+ <path fill="currentColor" d="M18 8h-2a1 1 0 0 0 0 2h2v8H6v-8h2a1 1 0 0 0 0-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2z"/>
430
+ <path fill="currentColor" d="M11 6.41V12a1 1 0 0 0 2 0V6.41l1.29 1.3a1 1 0 0 0 1.42 0a1 1 0 0 0 0-1.42l-3-3a1 1 0 0 0-1.42 0l-3 3a1 1 0 1 0 1.42 1.42L11 6.41z"/>
431
+ </svg>`;
432
+ }
433
+ /**
434
+ * GitHub icon for footer link
435
+ */
436
+ function getGitHubIcon() {
437
+ return `<svg viewBox="0 0 98 96" width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
438
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/>
439
+ </svg>`;
440
+ }
415
441
 
416
442
  // Constants for selectors
417
443
  const TABGROUP_SELECTOR$1 = 'cv-tabgroup';
@@ -1376,6 +1402,1077 @@ function injectCoreStyles() {
1376
1402
  document.head.appendChild(style);
1377
1403
  }
1378
1404
 
1405
+ const TOAST_STYLE_ID = 'cv-toast-styles';
1406
+ const TOAST_CLASS = 'cv-toast-notification';
1407
+ const TOAST_STYLES = `
1408
+ .cv-toast-notification {
1409
+ position: fixed;
1410
+ top: 20px;
1411
+ left: 50%;
1412
+ transform: translateX(-50%);
1413
+ background-color: #323232;
1414
+ color: white;
1415
+ padding: 12px 24px;
1416
+ border-radius: 4px;
1417
+ z-index: 100000;
1418
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1419
+ opacity: 0;
1420
+ transition: opacity 0.3s ease;
1421
+ pointer-events: none; /* Let clicks pass through if needed, though usually it blocks */
1422
+ font-family: system-ui, -apple-system, sans-serif;
1423
+ font-size: 14px;
1424
+ }
1425
+ `;
1426
+
1427
+ /**
1428
+ * Manages toast notifications for the application.
1429
+ */
1430
+ class ToastManager {
1431
+ static isStyleInjected = false;
1432
+ static toastEl = null;
1433
+ static navTimeout = null;
1434
+ static fadeTimeout = null;
1435
+ static show(message, duration = 2500) {
1436
+ this.injectStyles();
1437
+ // specific reuse logic
1438
+ if (!this.toastEl) {
1439
+ this.toastEl = document.createElement('div');
1440
+ this.toastEl.className = TOAST_CLASS;
1441
+ document.body.appendChild(this.toastEl);
1442
+ }
1443
+ // Reset state
1444
+ this.toastEl.textContent = message;
1445
+ this.toastEl.style.opacity = '0';
1446
+ this.toastEl.style.display = 'block';
1447
+ // Clear any pending dismissal
1448
+ if (this.navTimeout)
1449
+ clearTimeout(this.navTimeout);
1450
+ if (this.fadeTimeout)
1451
+ clearTimeout(this.fadeTimeout);
1452
+ // Trigger reflow & fade in
1453
+ requestAnimationFrame(() => {
1454
+ if (this.toastEl)
1455
+ this.toastEl.style.opacity = '1';
1456
+ });
1457
+ // Schedule fade out
1458
+ this.navTimeout = setTimeout(() => {
1459
+ if (this.toastEl)
1460
+ this.toastEl.style.opacity = '0';
1461
+ this.fadeTimeout = setTimeout(() => {
1462
+ if (this.toastEl)
1463
+ this.toastEl.style.display = 'none';
1464
+ }, 300);
1465
+ }, duration);
1466
+ }
1467
+ static injectStyles() {
1468
+ if (this.isStyleInjected)
1469
+ return;
1470
+ if (document.getElementById(TOAST_STYLE_ID)) {
1471
+ this.isStyleInjected = true;
1472
+ return;
1473
+ }
1474
+ const style = document.createElement('style');
1475
+ style.id = TOAST_STYLE_ID;
1476
+ style.innerHTML = TOAST_STYLES;
1477
+ document.head.appendChild(style);
1478
+ this.isStyleInjected = true;
1479
+ }
1480
+ }
1481
+
1482
+ /**
1483
+ * Engine for generating and resolving robust anchors.
1484
+ *
1485
+ * It implements a simple anchor generation and resolution algorithm that uses a combination of
1486
+ * structural, contextual, and content-based hints to generate a unique anchor for a given DOM element.
1487
+ *
1488
+ * The anchor is generated by first creating an AnchorDescriptor for the element, which contains
1489
+ * information about the element's tag, index, parent ID, and text content. This descriptor is then
1490
+ * serialized into a URL-safe string using a minification algorithm.
1491
+ *
1492
+ * The anchor is then resolved by searching for the element in the DOM using the serialized string.
1493
+ *
1494
+ */
1495
+ class AnchorEngine {
1496
+ /**
1497
+ * Generates a simple hash code for a string.
1498
+ *
1499
+ * It takes each character's Unicode code point and uses it to update the hash value.
1500
+ */
1501
+ static hashCode(str) {
1502
+ let hash = 0;
1503
+ if (str.length === 0)
1504
+ return hash;
1505
+ for (let i = 0; i < str.length; i++) {
1506
+ const char = str.charCodeAt(i);
1507
+ hash = ((hash << 5) - hash) + char;
1508
+ hash = hash & hash; // Convert to 32bit integer
1509
+ }
1510
+ return hash;
1511
+ }
1512
+ /**
1513
+ * Normalizes text content by removing excessive whitespace.
1514
+ *
1515
+ * It trims leading and trailing whitespace and replaces multiple spaces with a single space.
1516
+ */
1517
+ static normalizeText(text) {
1518
+ return text.trim().replace(/\s+/g, ' ');
1519
+ }
1520
+ /**
1521
+ * Creates an AnchorDescriptor for a given DOM element.
1522
+ */
1523
+ static createDescriptor(el) {
1524
+ const tag = el.tagName;
1525
+ const textContent = el.textContent || "";
1526
+ const normalizedText = this.normalizeText(textContent);
1527
+ // Find nearest parent with an ID
1528
+ let parentId;
1529
+ let parent = el.parentElement;
1530
+ while (parent) {
1531
+ if (parent.id) {
1532
+ parentId = parent.id;
1533
+ break;
1534
+ }
1535
+ parent = parent.parentElement;
1536
+ }
1537
+ // Calculate index relative to the container (either the found parent or document.body)
1538
+ const container = parent || document.body;
1539
+ const siblings = Array.from(container.querySelectorAll(tag));
1540
+ // Index is the position of the element in the list of siblings, where siblings are those of the same tag.
1541
+ const index = siblings.indexOf(el);
1542
+ const descriptor = {
1543
+ tag,
1544
+ index: index !== -1 ? index : 0,
1545
+ textSnippet: normalizedText.substring(0, 32),
1546
+ textHash: this.hashCode(normalizedText)
1547
+ };
1548
+ if (parentId) {
1549
+ descriptor.parentId = parentId;
1550
+ }
1551
+ return descriptor;
1552
+ }
1553
+ /**
1554
+ * Serializes a list of AnchorDescriptors into a URL-safe string.
1555
+ */
1556
+ static serialize(descriptors) {
1557
+ // Minify keys for compactness
1558
+ const minified = descriptors.map(d => ({
1559
+ t: d.tag,
1560
+ i: d.index,
1561
+ p: d.parentId,
1562
+ s: d.textSnippet,
1563
+ h: d.textHash
1564
+ }));
1565
+ const json = JSON.stringify(minified);
1566
+ // Base64 encode
1567
+ return btoa(encodeURIComponent(json));
1568
+ }
1569
+ /**
1570
+ * Deserializes a URL-safe string back into a list of AnchorDescriptors.
1571
+ */
1572
+ static deserialize(encoded) {
1573
+ try {
1574
+ const json = decodeURIComponent(atob(encoded));
1575
+ const minified = JSON.parse(json);
1576
+ return minified.map((m) => ({
1577
+ tag: m.t,
1578
+ index: m.i,
1579
+ parentId: m.p,
1580
+ textSnippet: m.s,
1581
+ textHash: m.h
1582
+ }));
1583
+ }
1584
+ catch (e) {
1585
+ console.error("Failed to deserialize anchor:", e);
1586
+ return [];
1587
+ }
1588
+ }
1589
+ /**
1590
+ * Finds the best DOM element match for a descriptor.
1591
+ */
1592
+ static resolve(root, descriptor) {
1593
+ // 1. Scope
1594
+ let scope = root;
1595
+ if (descriptor.parentId) {
1596
+ const foundParent = root.querySelector(`#${descriptor.parentId}`);
1597
+ if (foundParent instanceof HTMLElement) {
1598
+ scope = foundParent;
1599
+ }
1600
+ else {
1601
+ // Fallback: if parent ID not found, search global root - document.body
1602
+ const globalParent = document.getElementById(descriptor.parentId);
1603
+ if (globalParent) {
1604
+ scope = globalParent;
1605
+ }
1606
+ }
1607
+ }
1608
+ // 2. Candidate Search
1609
+ const candidates = Array.from(scope.querySelectorAll(descriptor.tag));
1610
+ // 3. Scoring
1611
+ let bestMatch = null;
1612
+ let highestScore = 0;
1613
+ candidates.forEach((candidate) => {
1614
+ let score = 0;
1615
+ const text = this.normalizeText(candidate.textContent || "");
1616
+ // Exact Text Match (Hash check is faster proxy for full string compare, but let's check hash first)
1617
+ if (this.hashCode(text) === descriptor.textHash) {
1618
+ score += 50;
1619
+ }
1620
+ else if (text.startsWith(descriptor.textSnippet)) {
1621
+ // Fuzzy Text Match (Snippet) - +30 score
1622
+ score += 30;
1623
+ }
1624
+ // Structural Match (Index)
1625
+ // We need to re-calculate index of this candidate to compare with descriptor.index
1626
+ // The descriptor.index is relative to the *found* parentId container.
1627
+ // So we must compare index within the scope we are searching.
1628
+ const siblings = Array.from(scope.querySelectorAll(descriptor.tag));
1629
+ const index = siblings.indexOf(candidate);
1630
+ if (index === descriptor.index) {
1631
+ score += 10;
1632
+ }
1633
+ if (score > highestScore) {
1634
+ highestScore = score;
1635
+ bestMatch = candidate;
1636
+ }
1637
+ });
1638
+ // 4. Winner
1639
+ if (highestScore > 30) {
1640
+ return bestMatch;
1641
+ }
1642
+ return null;
1643
+ }
1644
+ }
1645
+
1646
+ const SHARE_MODE_STYLE_ID = 'cv-share-mode-styles';
1647
+ const FLOATING_ACTION_BAR_ID = 'cv-floating-action-bar';
1648
+ const HOVER_HELPER_ID = 'cv-hover-helper';
1649
+ const HIGHLIGHT_TARGET_CLASS = 'cv-highlight-target';
1650
+ const SELECTED_CLASS = 'cv-share-selected';
1651
+ /**
1652
+ * CSS styles to be injected during Share Mode.
1653
+ */
1654
+ const SHARE_MODE_STYLES = `
1655
+ body.cv-share-mode {
1656
+ cursor: default;
1657
+ }
1658
+
1659
+ /* Highlight outlines */
1660
+ .${HIGHLIGHT_TARGET_CLASS} {
1661
+ outline: 2px dashed #0078D4 !important;
1662
+ outline-offset: 2px;
1663
+ cursor: crosshair;
1664
+ }
1665
+
1666
+ .${SELECTED_CLASS} {
1667
+ outline: 3px solid #005a9e !important;
1668
+ outline-offset: 2px;
1669
+ background-color: rgba(0, 120, 212, 0.05);
1670
+ }
1671
+
1672
+ /* Floating Action Bar */
1673
+ #${FLOATING_ACTION_BAR_ID} {
1674
+ position: fixed;
1675
+ bottom: 20px;
1676
+ left: 50%;
1677
+ transform: translateX(-50%);
1678
+ background-color: #2c2c2c;
1679
+ color: #f1f1f1;
1680
+ border-radius: 8px;
1681
+ padding: 12px 20px;
1682
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1683
+ display: flex;
1684
+ align-items: center;
1685
+ gap: 16px;
1686
+ z-index: 99999;
1687
+ font-family: system-ui, -apple-system, sans-serif;
1688
+ font-size: 14px;
1689
+ border: 1px solid #4a4a4a;
1690
+ }
1691
+
1692
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button {
1693
+ background-color: #0078D4;
1694
+ color: white;
1695
+ border: none;
1696
+ padding: 8px 14px;
1697
+ border-radius: 5px;
1698
+ cursor: pointer;
1699
+ font-weight: 500;
1700
+ transition: background-color 0.2s;
1701
+ }
1702
+
1703
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button:hover {
1704
+ background-color: #005a9e;
1705
+ }
1706
+
1707
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.clear {
1708
+ background-color: #5a5a5a;
1709
+ }
1710
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.clear:hover {
1711
+ background-color: #4a4a4a;
1712
+ }
1713
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.clear:hover {
1714
+ background-color: #4a4a4a;
1715
+ }
1716
+
1717
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.preview {
1718
+ background-color: #106ebe;
1719
+ }
1720
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.preview:hover {
1721
+ background-color: #005a9e;
1722
+ }
1723
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.exit {
1724
+ background-color: #d13438;
1725
+ }
1726
+ #${FLOATING_ACTION_BAR_ID} .cv-action-button.exit:hover {
1727
+ background-color: #a42628;
1728
+ }
1729
+
1730
+ /* Hover Helper (Smart Label & Level Up) */
1731
+ #${HOVER_HELPER_ID} {
1732
+ position: fixed;
1733
+ z-index: 99999;
1734
+ background-color: #333;
1735
+ color: white;
1736
+ padding: 4px 8px;
1737
+ border-radius: 4px;
1738
+ font-size: 12px;
1739
+ font-family: monospace;
1740
+ display: none;
1741
+ pointer-events: auto; /* Allow clicking buttons inside */
1742
+ align-items: center;
1743
+ gap: 8px;
1744
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
1745
+ }
1746
+
1747
+ #${HOVER_HELPER_ID} button {
1748
+ background: #555;
1749
+ border: none;
1750
+ color: white;
1751
+ border-radius: 3px;
1752
+ cursor: pointer;
1753
+ padding: 2px 6px;
1754
+ font-size: 14px;
1755
+ line-height: 1;
1756
+ }
1757
+ #${HOVER_HELPER_ID} button:hover {
1758
+ background: #777;
1759
+ }
1760
+
1761
+ `;
1762
+
1763
+ const CV_CUSTOM_ELEMENTS = 'cv-tabgroup, cv-toggle';
1764
+ const SHAREABLE_SELECTOR = 'div, p, blockquote, pre, li, h1, h2, h3, h4, h5, h6, [data-share], ' + CV_CUSTOM_ELEMENTS;
1765
+ /**
1766
+ * Manages the "Share Mode" for creating custom focus links.
1767
+ * Implementing Robust Granular Sharing with "Innermost Wins" and "Level Up" UI.
1768
+ */
1769
+ class ShareManager {
1770
+ isActive = false;
1771
+ selectedElements = new Set();
1772
+ floatingBarEl = null;
1773
+ helperEl = null;
1774
+ currentHoverTarget = null;
1775
+ excludedTags;
1776
+ excludedIds;
1777
+ boundHandleHover;
1778
+ boundHandleClick;
1779
+ boundHandleKeydown;
1780
+ constructor(options) {
1781
+ this.excludedTags = new Set(options.excludedTags.map(t => t.toUpperCase()));
1782
+ this.excludedIds = new Set(options.excludedIds);
1783
+ this.boundHandleHover = this.handleHover.bind(this);
1784
+ this.boundHandleClick = this.handleClick.bind(this);
1785
+ this.boundHandleKeydown = this.handleKeydown.bind(this);
1786
+ }
1787
+ listeners = [];
1788
+ addStateChangeListener(listener) {
1789
+ this.listeners.push(listener);
1790
+ }
1791
+ removeStateChangeListener(listener) {
1792
+ this.listeners = this.listeners.filter(l => l !== listener);
1793
+ }
1794
+ notifyListeners() {
1795
+ this.listeners.forEach(listener => listener(this.isActive));
1796
+ }
1797
+ toggleShareMode() {
1798
+ this.isActive = !this.isActive;
1799
+ if (this.isActive) {
1800
+ this.activate();
1801
+ }
1802
+ else {
1803
+ this.cleanup();
1804
+ }
1805
+ this.notifyListeners();
1806
+ }
1807
+ /**
1808
+ * Activates the share mode.
1809
+ * Injects styles, creates floating bar, and helper element.
1810
+ * Adds event listeners for hover and click.
1811
+ */
1812
+ activate() {
1813
+ this.injectStyles();
1814
+ this.createFloatingBar();
1815
+ this.helperEl = this.createHelperPopover();
1816
+ // Event Listeners
1817
+ document.addEventListener('mouseover', this.boundHandleHover, true);
1818
+ document.addEventListener('click', this.boundHandleClick, true);
1819
+ document.addEventListener('keydown', this.boundHandleKeydown, true);
1820
+ }
1821
+ injectStyles() {
1822
+ const styleElement = document.createElement('style');
1823
+ styleElement.id = SHARE_MODE_STYLE_ID;
1824
+ styleElement.innerHTML = SHARE_MODE_STYLES;
1825
+ document.head.appendChild(styleElement);
1826
+ }
1827
+ /**
1828
+ * Creates the hover helper element that shows up when hovering over a shareable element.
1829
+ */
1830
+ createHelperPopover() {
1831
+ const div = document.createElement('div');
1832
+ div.id = HOVER_HELPER_ID;
1833
+ div.innerHTML = `
1834
+ <span id="cv-helper-tag">TAG</span>
1835
+ <button id="cv-helper-select-btn" title="Select This Element">✓</button>
1836
+ <button id="cv-helper-up-btn" title="Select Parent">↰</button>
1837
+ `;
1838
+ document.body.appendChild(div);
1839
+ // Select parent button
1840
+ div.querySelector('#cv-helper-up-btn')?.addEventListener('click', (e) => {
1841
+ e.preventDefault();
1842
+ e.stopPropagation();
1843
+ this.handleSelectParent();
1844
+ });
1845
+ // Select element button
1846
+ div.querySelector('#cv-helper-select-btn')?.addEventListener('click', (e) => {
1847
+ e.preventDefault();
1848
+ e.stopPropagation();
1849
+ if (this.currentHoverTarget) {
1850
+ this.toggleSelection(this.currentHoverTarget);
1851
+ }
1852
+ });
1853
+ return div;
1854
+ }
1855
+ /**
1856
+ * Handles mouse hover events.
1857
+ *
1858
+ * This function is called when the user hovers over an element.
1859
+ * It checks if the element is shareable and highlights it.
1860
+ * If a parent element is already selected, it highlights the parent instead,
1861
+ * allowing the helper to remain visible for the selected parent.
1862
+ *
1863
+ * @param e The mouse event triggered by the hover.
1864
+ */
1865
+ handleHover(e) {
1866
+ if (!this.isActive)
1867
+ return;
1868
+ // Check if we are hovering over the helper itself
1869
+ if (this.helperEl && this.helperEl.contains(e.target)) {
1870
+ return;
1871
+ }
1872
+ const target = e.target;
1873
+ // Exclude by Tag or ID
1874
+ const upperTag = target.tagName.toUpperCase();
1875
+ if (this.excludedTags.has(upperTag) || (target.id && this.excludedIds.has(target.id))) {
1876
+ return;
1877
+ }
1878
+ // Check closest excluded (for nested elements in excluded regions)
1879
+ let ancestor = target.parentElement;
1880
+ while (ancestor) {
1881
+ if (this.excludedTags.has(ancestor.tagName.toUpperCase()) || (ancestor.id && this.excludedIds.has(ancestor.id))) {
1882
+ return;
1883
+ }
1884
+ ancestor = ancestor.parentElement;
1885
+ }
1886
+ // Find closest shareable parent element
1887
+ const shareablePart = target.closest(SHAREABLE_SELECTOR);
1888
+ if (!shareablePart) {
1889
+ this.clearHover();
1890
+ return;
1891
+ }
1892
+ // Cast to HTMLElement
1893
+ const finalTarget = shareablePart;
1894
+ // Check if any ancestor is already selected. If so, do NOT highlight this child.
1895
+ // Instead, highlight (or keep highlighted) the SELECTED PARENT so the user can see the helper for it.
1896
+ let parent = finalTarget.parentElement;
1897
+ let selectedAncestor = null;
1898
+ // Loop outwards until we find a selected parent or reach the top
1899
+ while (parent) {
1900
+ if (this.selectedElements.has(parent)) {
1901
+ selectedAncestor = parent;
1902
+ break;
1903
+ }
1904
+ parent = parent.parentElement;
1905
+ }
1906
+ if (selectedAncestor) {
1907
+ // If we are hovering deep inside a selected block, show the helper for that block
1908
+ this.setNewHoverTarget(selectedAncestor);
1909
+ return;
1910
+ }
1911
+ // stop bubbling to parent
1912
+ // when element found for highlight
1913
+ e.stopPropagation();
1914
+ // If we are already on this target, do nothing (and keep it selected/highlighted)
1915
+ if (this.currentHoverTarget === finalTarget)
1916
+ return;
1917
+ // Highlight
1918
+ this.setNewHoverTarget(finalTarget);
1919
+ }
1920
+ setNewHoverTarget(target) {
1921
+ if (this.currentHoverTarget) {
1922
+ this.currentHoverTarget.classList.remove(HIGHLIGHT_TARGET_CLASS);
1923
+ }
1924
+ this.currentHoverTarget = target;
1925
+ this.currentHoverTarget.classList.add(HIGHLIGHT_TARGET_CLASS);
1926
+ this.positionHelper(target);
1927
+ }
1928
+ positionHelper(target) {
1929
+ if (!this.helperEl)
1930
+ return;
1931
+ const rect = target.getBoundingClientRect();
1932
+ const tagLabel = this.helperEl.querySelector('#cv-helper-tag');
1933
+ const upBtn = this.helperEl.querySelector('#cv-helper-up-btn');
1934
+ if (tagLabel)
1935
+ tagLabel.textContent = target.tagName;
1936
+ // Position at top-right of the element
1937
+ // Prevent going off-screen
1938
+ let top = rect.top - 20;
1939
+ if (top < 0)
1940
+ top = rect.top + 10; // Flip down if too close to top
1941
+ let left = rect.right - 80;
1942
+ if (left < 0)
1943
+ left = 10;
1944
+ this.helperEl.style.display = 'flex';
1945
+ this.helperEl.style.top = `${top}px`;
1946
+ this.helperEl.style.left = `${left}px`;
1947
+ // Update Select Button State (Tick or Cross)
1948
+ const selectBtn = this.helperEl.querySelector('#cv-helper-select-btn');
1949
+ if (selectBtn) {
1950
+ if (this.selectedElements.has(target)) {
1951
+ selectBtn.textContent = '✕';
1952
+ selectBtn.title = 'Deselect This Element';
1953
+ selectBtn.style.backgroundColor = '#d13438'; // Reddish
1954
+ }
1955
+ else {
1956
+ selectBtn.textContent = '✓';
1957
+ selectBtn.title = 'Select This Element';
1958
+ selectBtn.style.backgroundColor = ''; // Reset
1959
+ }
1960
+ }
1961
+ // Ancestry Check
1962
+ const parent = target.parentElement;
1963
+ const parentIsShareable = parent && parent.matches(SHAREABLE_SELECTOR);
1964
+ if (parentIsShareable) {
1965
+ upBtn.style.display = 'inline-block';
1966
+ }
1967
+ else {
1968
+ upBtn.style.display = 'none';
1969
+ }
1970
+ }
1971
+ handleSelectParent() {
1972
+ if (this.currentHoverTarget && this.currentHoverTarget.parentElement) {
1973
+ const parent = this.currentHoverTarget.parentElement;
1974
+ if (parent.matches(SHAREABLE_SELECTOR)) {
1975
+ this.setNewHoverTarget(parent);
1976
+ }
1977
+ }
1978
+ }
1979
+ handleClick(e) {
1980
+ if (!this.isActive)
1981
+ return;
1982
+ // If clicking helper
1983
+ if (this.helperEl && this.helperEl.contains(e.target))
1984
+ return;
1985
+ // If clicking floating bar
1986
+ if (this.floatingBarEl && this.floatingBarEl.contains(e.target))
1987
+ return;
1988
+ e.preventDefault();
1989
+ e.stopPropagation();
1990
+ if (this.currentHoverTarget) {
1991
+ this.toggleSelection(this.currentHoverTarget);
1992
+ }
1993
+ }
1994
+ handleKeydown(e) {
1995
+ if (!this.isActive)
1996
+ return;
1997
+ if (e.key === 'Escape') {
1998
+ e.preventDefault();
1999
+ e.stopPropagation();
2000
+ this.toggleShareMode();
2001
+ }
2002
+ }
2003
+ /**
2004
+ * Toggles the selection state of a given HTML element.
2005
+ * Implements selection logic:
2006
+ * - If an ancestor of the element is already selected, the click is ignored.
2007
+ * - If the element being selected is a parent of already selected elements, those children are deselected.
2008
+ * @param el The HTMLElement to toggle selection for.
2009
+ */
2010
+ toggleSelection(el) {
2011
+ if (this.selectedElements.has(el)) {
2012
+ this.selectedElements.delete(el);
2013
+ el.classList.remove(SELECTED_CLASS);
2014
+ }
2015
+ else {
2016
+ // Selection Logic
2017
+ // Scenario A: Selecting a Parent -> Remove children selected while selecting parent
2018
+ // Scenario B: Selecting a Child -> Ignore if ancestor selected (or handle)
2019
+ // B. Check if any ancestor is already selected, return if any (Scenario B)
2020
+ let parent = el.parentElement;
2021
+ while (parent) {
2022
+ if (this.selectedElements.has(parent)) {
2023
+ // Ancestor is selected. Ignore click.
2024
+ return;
2025
+ }
2026
+ parent = parent.parentElement;
2027
+ }
2028
+ // A. Check if any children are selected (Scenario A)
2029
+ // We must iterate over currently selected elements
2030
+ const toRemove = [];
2031
+ this.selectedElements.forEach(selected => {
2032
+ if (el.contains(selected) && el !== selected) {
2033
+ toRemove.push(selected);
2034
+ }
2035
+ });
2036
+ toRemove.forEach(child => {
2037
+ this.selectedElements.delete(child);
2038
+ child.classList.remove(SELECTED_CLASS);
2039
+ });
2040
+ // Add new selection
2041
+ this.selectedElements.add(el);
2042
+ el.classList.add(SELECTED_CLASS);
2043
+ }
2044
+ this.updateFloatingBarCount();
2045
+ }
2046
+ createFloatingBar() {
2047
+ const bar = document.createElement('div');
2048
+ bar.id = FLOATING_ACTION_BAR_ID;
2049
+ bar.innerHTML = `
2050
+ <span id="cv-selected-count">0 items selected</span>
2051
+ <button class="cv-action-button clear">Clear All</button>
2052
+ <button class="cv-action-button preview">Preview</button>
2053
+ <button class="cv-action-button generate">Generate Link</button>
2054
+ <button class="cv-action-button exit">Exit</button>
2055
+ `;
2056
+ document.body.appendChild(bar);
2057
+ this.floatingBarEl = bar;
2058
+ bar.querySelector('.clear')?.addEventListener('click', () => this.clearAll());
2059
+ bar.querySelector('.preview')?.addEventListener('click', () => this.previewLink());
2060
+ bar.querySelector('.generate')?.addEventListener('click', () => this.generateLink());
2061
+ bar.querySelector('.exit')?.addEventListener('click', () => this.toggleShareMode());
2062
+ }
2063
+ updateFloatingBarCount() {
2064
+ if (this.floatingBarEl) {
2065
+ const countElement = this.floatingBarEl.querySelector('#cv-selected-count');
2066
+ if (countElement) {
2067
+ const count = this.selectedElements.size;
2068
+ countElement.textContent = `${count} item${count === 1 ? '' : 's'} selected`;
2069
+ }
2070
+ }
2071
+ }
2072
+ clearAll() {
2073
+ this.selectedElements.forEach(el => el.classList.remove('cv-share-selected'));
2074
+ this.selectedElements.clear();
2075
+ this.updateFloatingBarCount();
2076
+ }
2077
+ getShareUrl() {
2078
+ if (this.selectedElements.size === 0) {
2079
+ return null;
2080
+ }
2081
+ const descriptors = Array.from(this.selectedElements).map(el => AnchorEngine.createDescriptor(el));
2082
+ const serialized = AnchorEngine.serialize(descriptors);
2083
+ const url = new URL(window.location.href);
2084
+ url.searchParams.set('cv-focus', serialized);
2085
+ return url;
2086
+ }
2087
+ async generateLink() {
2088
+ const url = this.getShareUrl();
2089
+ if (!url) {
2090
+ ToastManager.show('Please select at least one item.');
2091
+ return;
2092
+ }
2093
+ try {
2094
+ await navigator.clipboard.writeText(url.toString());
2095
+ ToastManager.show('Link copied to clipboard!');
2096
+ }
2097
+ catch (e) {
2098
+ console.error('Clipboard failed', e);
2099
+ ToastManager.show('Failed to copy link.');
2100
+ }
2101
+ }
2102
+ previewLink() {
2103
+ const url = this.getShareUrl();
2104
+ if (!url) {
2105
+ ToastManager.show('Please select at least one item.');
2106
+ return;
2107
+ }
2108
+ window.open(url.toString(), '_blank');
2109
+ }
2110
+ clearHover() {
2111
+ if (this.currentHoverTarget) {
2112
+ this.currentHoverTarget.classList.remove(HIGHLIGHT_TARGET_CLASS);
2113
+ this.currentHoverTarget = null;
2114
+ }
2115
+ if (this.helperEl) {
2116
+ this.helperEl.style.display = 'none';
2117
+ }
2118
+ }
2119
+ cleanup() {
2120
+ document.body.classList.remove('cv-share-mode');
2121
+ this.clearAll();
2122
+ const style = document.getElementById(SHARE_MODE_STYLE_ID);
2123
+ if (style)
2124
+ document.head.removeChild(style);
2125
+ if (this.floatingBarEl) {
2126
+ document.body.removeChild(this.floatingBarEl);
2127
+ this.floatingBarEl = null;
2128
+ }
2129
+ if (this.helperEl) {
2130
+ document.body.removeChild(this.helperEl);
2131
+ this.helperEl = null;
2132
+ }
2133
+ if (this.currentHoverTarget) {
2134
+ this.currentHoverTarget.classList.remove(HIGHLIGHT_TARGET_CLASS);
2135
+ this.currentHoverTarget = null;
2136
+ }
2137
+ document.removeEventListener('mouseover', this.boundHandleHover, true);
2138
+ document.removeEventListener('click', this.boundHandleClick, true);
2139
+ document.removeEventListener('keydown', this.boundHandleKeydown, true);
2140
+ this.isActive = false;
2141
+ }
2142
+ }
2143
+
2144
+ const FOCUS_MODE_STYLE_ID = 'cv-focus-mode-styles';
2145
+ const BODY_FOCUS_CLASS = 'cv-focus-mode';
2146
+ const HIDDEN_CLASS = 'cv-focus-hidden';
2147
+ const FOCUSED_CLASS = 'cv-focused-element';
2148
+ const DIVIDER_CLASS = 'cv-context-divider';
2149
+ const EXIT_BANNER_ID = 'cv-exit-focus-banner';
2150
+ const styles = `
2151
+ body.${BODY_FOCUS_CLASS} {
2152
+ /* e.g. potentially hide scrollbars or adjust layout */
2153
+ }
2154
+
2155
+ .${HIDDEN_CLASS} {
2156
+ display: none !important;
2157
+ }
2158
+
2159
+ .${FOCUSED_CLASS} {
2160
+ /* No visual style for focused elements, just logic class for now. Can add borders for debugging*/
2161
+ }
2162
+
2163
+ .${DIVIDER_CLASS} {
2164
+ padding: 12px;
2165
+ margin: 16px 0;
2166
+ background-color: #f8f8f8;
2167
+ border-top: 1px dashed #ccc;
2168
+ border-bottom: 1px dashed #ccc;
2169
+ color: #555;
2170
+ text-align: center;
2171
+ cursor: pointer;
2172
+ font-family: system-ui, sans-serif;
2173
+ font-size: 13px;
2174
+ transition: background-color 0.2s;
2175
+ }
2176
+ .${DIVIDER_CLASS}:hover {
2177
+ background-color: #e8e8e8;
2178
+ color: #333;
2179
+ }
2180
+
2181
+ #${EXIT_BANNER_ID} {
2182
+ position: sticky;
2183
+ top: 0;
2184
+ left: 0;
2185
+ right: 0;
2186
+ background-color: #0078D4;
2187
+ color: white;
2188
+ padding: 10px 20px;
2189
+ display: flex;
2190
+ align-items: center;
2191
+ justify-content: center;
2192
+ gap: 16px;
2193
+ z-index: 100000;
2194
+ font-family: system-ui, sans-serif;
2195
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
2196
+ }
2197
+
2198
+ #${EXIT_BANNER_ID} button {
2199
+ background: white;
2200
+ color: #0078D4;
2201
+ border: none;
2202
+ padding: 4px 12px;
2203
+ border-radius: 4px;
2204
+ cursor: pointer;
2205
+ font-weight: 600;
2206
+ }
2207
+ #${EXIT_BANNER_ID} button:hover {
2208
+ background: #f0f0f0;
2209
+ }
2210
+ `;
2211
+ const FOCUS_MODE_STYLES = styles;
2212
+
2213
+ /**
2214
+ * Manages the "Focus Mode" (Presentation View).
2215
+ * Parses the URL for robust anchors, resolves them, hides irrelevant content, and inserts context dividers.
2216
+ */
2217
+ const FOCUS_PARAM = 'cv-focus';
2218
+ class FocusManager {
2219
+ rootEl;
2220
+ hiddenElements = new Set();
2221
+ dividers = [];
2222
+ exitBanner = null;
2223
+ excludedTags;
2224
+ excludedIds;
2225
+ constructor(rootEl, options) {
2226
+ this.rootEl = rootEl;
2227
+ this.excludedTags = new Set(options.excludedTags.map(t => t.toUpperCase()));
2228
+ this.excludedIds = new Set(options.excludedIds);
2229
+ }
2230
+ /**
2231
+ * Initializes the Focus Manager. Checks URL for focus parameter.
2232
+ */
2233
+ init() {
2234
+ this.handleUrlChange();
2235
+ }
2236
+ handleUrlChange() {
2237
+ const urlParams = new URLSearchParams(window.location.search);
2238
+ const encodedDescriptors = urlParams.get(FOCUS_PARAM);
2239
+ if (encodedDescriptors) {
2240
+ this.applyFocusMode(encodedDescriptors);
2241
+ }
2242
+ else {
2243
+ // encoding missing, ensure we exit focus mode if active
2244
+ if (document.body.classList.contains(BODY_FOCUS_CLASS)) {
2245
+ this.exitFocusMode();
2246
+ }
2247
+ }
2248
+ }
2249
+ /**
2250
+ * Applies Focus Mode based on encoded descriptors.
2251
+ */
2252
+ applyFocusMode(encodedDescriptors) {
2253
+ const descriptors = AnchorEngine.deserialize(encodedDescriptors);
2254
+ if (!descriptors || descriptors.length === 0)
2255
+ return;
2256
+ // Resolve anchors to DOM elements
2257
+ const targets = [];
2258
+ descriptors.forEach(desc => {
2259
+ const el = AnchorEngine.resolve(this.rootEl, desc);
2260
+ if (el) {
2261
+ targets.push(el);
2262
+ }
2263
+ });
2264
+ if (targets.length === 0) {
2265
+ ToastManager.show("Some shared sections could not be found.");
2266
+ return;
2267
+ }
2268
+ if (targets.length < descriptors.length) {
2269
+ ToastManager.show("Some shared sections could not be found.");
2270
+ }
2271
+ this.injectStyles();
2272
+ document.body.classList.add(BODY_FOCUS_CLASS);
2273
+ this.renderFocusedView(targets);
2274
+ this.showExitBanner();
2275
+ }
2276
+ injectStyles() {
2277
+ if (document.getElementById(FOCUS_MODE_STYLE_ID))
2278
+ return;
2279
+ const style = document.createElement('style');
2280
+ style.id = FOCUS_MODE_STYLE_ID;
2281
+ style.textContent = FOCUS_MODE_STYLES;
2282
+ document.head.appendChild(style);
2283
+ }
2284
+ /**
2285
+ * Hides irrelevant content and adds dividers.
2286
+ */
2287
+ renderFocusedView(targets) {
2288
+ // 1. Mark targets
2289
+ targets.forEach(t => t.classList.add(FOCUSED_CLASS));
2290
+ // 2. We need to hide siblings of targets (and their ancestors up to root generally,
2291
+ // but "siblings between focused zones" suggests we are mostly looking at a flat list or specific nesting).
2292
+ //
2293
+ // "All sibling elements between the focused zones are collapsed and hidden."
2294
+ // "If a user selects a parent element, all of its child elements must be visible."
2295
+ //
2296
+ // Algorithm:
2297
+ // Walk up from each target to finding the common container?
2298
+ // Or just assume targets are somewhat related.
2299
+ //
2300
+ // Let's implement a robust "Hide Siblings" approach.
2301
+ // For every target, we ensure it is visible.
2302
+ // We look at its siblings. If a sibling is NOT a target AND NOT an ancestor of a target, we hide it.
2303
+ // We need to identify all "Keep Visible" elements (targets + ancestors)
2304
+ const keepVisible = new Set();
2305
+ targets.forEach(t => {
2306
+ let curr = t;
2307
+ while (curr && curr !== document.body && curr !== document.documentElement) {
2308
+ keepVisible.add(curr);
2309
+ curr = curr.parentElement;
2310
+ }
2311
+ });
2312
+ // Now iterate through siblings of "Keep Visible" elements?
2313
+ // Actually, we can just walk the tree or iterate siblings of targets/ancestors?
2314
+ //
2315
+ // Improved Algorithm:
2316
+ // 1. Collect all direct siblings of every element in keepVisible set.
2317
+ // 2. If a sibling is NOT in keepVisible, hide it.
2318
+ // To avoid processing the entire DOM, we start from targets and walk up.
2319
+ keepVisible.forEach(el => {
2320
+ if (el === document.body)
2321
+ return; // Don't hide siblings of body (scripts etc) unless we are sure.
2322
+ // Actually usually we want to hide siblings of the content container.
2323
+ const parent = el.parentElement;
2324
+ if (!parent)
2325
+ return;
2326
+ // FIX: "Parent Dominance"
2327
+ // If the parent itself is a target (or we otherwise decided its whole content is meaningful),
2328
+ // then we should NOT hide anything inside it.
2329
+ // We check if 'parent' is one of the explicitly resolved targets.
2330
+ // We can check if it has the FOCUSED_CLASS class, since we added it in step 1.
2331
+ if (parent.classList.contains(FOCUSED_CLASS)) {
2332
+ return;
2333
+ }
2334
+ // Using children because we want element nodes
2335
+ Array.from(parent.children).forEach(child => {
2336
+ if (child instanceof HTMLElement && !keepVisible.has(child)) {
2337
+ this.hideElement(child);
2338
+ }
2339
+ });
2340
+ });
2341
+ // 3. Insert Dividers
2342
+ // We process each container that has hidden elements
2343
+ const processedContainers = new Set();
2344
+ keepVisible.forEach(el => {
2345
+ const parent = el.parentElement;
2346
+ if (parent && !processedContainers.has(parent)) {
2347
+ this.insertDividersForContainer(parent);
2348
+ processedContainers.add(parent);
2349
+ }
2350
+ });
2351
+ }
2352
+ hideElement(el) {
2353
+ if (this.hiddenElements.has(el))
2354
+ return; // Already hidden
2355
+ // Exclude by Tag
2356
+ if (this.excludedTags.has(el.tagName.toUpperCase()))
2357
+ return;
2358
+ // Exclude by ID (if strictly matching)
2359
+ if (el.id && this.excludedIds.has(el.id))
2360
+ return;
2361
+ // Also don't hide things that are aria-hidden
2362
+ if (el.getAttribute('aria-hidden') === 'true')
2363
+ return;
2364
+ // Exclude Toast Notification
2365
+ if (el.classList.contains(TOAST_CLASS))
2366
+ return;
2367
+ // We check if it is already hidden (e.g. by previous focus mode run? No, isActive check handles that)
2368
+ // Just mark it.
2369
+ el.classList.add(HIDDEN_CLASS);
2370
+ this.hiddenElements.add(el);
2371
+ }
2372
+ insertDividersForContainer(container) {
2373
+ const children = Array.from(container.children);
2374
+ let hiddenCount = 0;
2375
+ let hiddenGroupStart = null;
2376
+ children.forEach((child) => {
2377
+ if (child.classList.contains(HIDDEN_CLASS)) {
2378
+ if (hiddenCount === 0)
2379
+ hiddenGroupStart = child;
2380
+ hiddenCount++;
2381
+ }
2382
+ else {
2383
+ // Found a visible element. Was there a hidden group before this?
2384
+ if (hiddenCount > 0 && hiddenGroupStart) {
2385
+ this.createDivider(container, hiddenGroupStart, hiddenCount);
2386
+ hiddenCount = 0;
2387
+ hiddenGroupStart = null;
2388
+ }
2389
+ }
2390
+ });
2391
+ // Trailing hidden group
2392
+ if (hiddenCount > 0 && hiddenGroupStart) {
2393
+ this.createDivider(container, hiddenGroupStart, hiddenCount);
2394
+ }
2395
+ }
2396
+ createDivider(container, insertBeforeEl, count) {
2397
+ const divider = document.createElement('div');
2398
+ divider.className = DIVIDER_CLASS;
2399
+ divider.textContent = `... ${count} section${count > 1 ? 's' : ''} hidden (Click to expand) ...`;
2400
+ divider.onclick = () => this.expandContext(insertBeforeEl, count, divider);
2401
+ container.insertBefore(divider, insertBeforeEl);
2402
+ this.dividers.push(divider);
2403
+ }
2404
+ expandContext(firstHidden, count, divider) {
2405
+ // Divider is inserted BEFORE firstHidden.
2406
+ // So firstHidden is the first element to reveal.
2407
+ let curr = firstHidden;
2408
+ let expanded = 0;
2409
+ while (curr && expanded < count) {
2410
+ if (curr instanceof HTMLElement && curr.classList.contains(HIDDEN_CLASS)) {
2411
+ curr.classList.remove(HIDDEN_CLASS);
2412
+ this.hiddenElements.delete(curr);
2413
+ }
2414
+ curr = curr.nextElementSibling;
2415
+ // Note: If nested dividers or other elements exist, they shouldn't count?
2416
+ // "Children" iteration in insertDividers covered direct children.
2417
+ // sibling iteration also covers direct children.
2418
+ // We assume contiguous hidden siblings.
2419
+ expanded++;
2420
+ }
2421
+ divider.remove();
2422
+ const idx = this.dividers.indexOf(divider);
2423
+ if (idx > -1)
2424
+ this.dividers.splice(idx, 1);
2425
+ // If no more hidden elements, remove the banner
2426
+ if (this.hiddenElements.size === 0) {
2427
+ this.removeExitBanner();
2428
+ }
2429
+ }
2430
+ removeExitBanner() {
2431
+ if (this.exitBanner) {
2432
+ this.exitBanner.remove();
2433
+ this.exitBanner = null;
2434
+ }
2435
+ }
2436
+ /**
2437
+ * Override of renderFocusedView with robust logic
2438
+ */
2439
+ // (We use the class method `renderFocusedView` and internal helpers)
2440
+ showExitBanner() {
2441
+ if (document.getElementById(EXIT_BANNER_ID))
2442
+ return;
2443
+ const banner = document.createElement('div');
2444
+ banner.id = EXIT_BANNER_ID;
2445
+ banner.innerHTML = `
2446
+ <span>You are viewing a focused selection.</span>
2447
+ <button id="cv-exit-focus-btn">Show Full Page</button>
2448
+ `;
2449
+ document.body.prepend(banner); // Top of body
2450
+ banner.querySelector('button')?.addEventListener('click', () => this.exitFocusMode());
2451
+ this.exitBanner = banner;
2452
+ }
2453
+ exitFocusMode() {
2454
+ document.body.classList.remove(BODY_FOCUS_CLASS);
2455
+ // Show all hidden elements
2456
+ this.hiddenElements.forEach(el => el.classList.remove(HIDDEN_CLASS));
2457
+ this.hiddenElements.clear();
2458
+ // Remove dividers
2459
+ this.dividers.forEach(d => d.remove());
2460
+ this.dividers = [];
2461
+ // Remove styling from targets
2462
+ const targets = document.querySelectorAll(`.${FOCUSED_CLASS}`);
2463
+ targets.forEach(t => t.classList.remove(FOCUSED_CLASS));
2464
+ // Remove banner
2465
+ this.removeExitBanner();
2466
+ // Update URL
2467
+ const url = new URL(window.location.href);
2468
+ url.searchParams.delete(FOCUS_PARAM);
2469
+ window.history.pushState({}, '', url.toString());
2470
+ }
2471
+ }
2472
+
2473
+ const DEFAULT_EXCLUDED_TAGS = ['HEADER', 'NAV', 'FOOTER', 'SCRIPT', 'STYLE'];
2474
+ const DEFAULT_EXCLUDED_IDS = ['cv-floating-action-bar', 'cv-hover-helper', 'cv-toast-notification'];
2475
+
1379
2476
  const TOGGLE_SELECTOR = "[data-cv-toggle], [data-customviews-toggle], cv-toggle";
1380
2477
  const TABGROUP_SELECTOR = 'cv-tabgroup';
1381
2478
  class CustomViewsCore {
@@ -1384,6 +2481,8 @@ class CustomViewsCore {
1384
2481
  persistenceManager;
1385
2482
  visibilityManager;
1386
2483
  observer = null;
2484
+ shareManager;
2485
+ focusManager;
1387
2486
  componentRegistry = {
1388
2487
  toggles: new Set(),
1389
2488
  tabGroups: new Set(),
@@ -1400,6 +2499,21 @@ class CustomViewsCore {
1400
2499
  this.visibilityManager = new VisibilityManager();
1401
2500
  this.showUrlEnabled = opt.showUrl ?? false;
1402
2501
  this.lastAppliedState = this.cloneState(this.getComputedDefaultState());
2502
+ // Resolve Exclusions
2503
+ const excludedTags = [...DEFAULT_EXCLUDED_TAGS, ...(this.config.shareExclusions?.tags || [])];
2504
+ const excludedIds = [...DEFAULT_EXCLUDED_IDS, ...(this.config.shareExclusions?.ids || [])];
2505
+ const commonOptions = { excludedTags, excludedIds };
2506
+ this.shareManager = new ShareManager(commonOptions);
2507
+ this.focusManager = new FocusManager(this.rootEl, commonOptions);
2508
+ }
2509
+ getShareManager() {
2510
+ return this.shareManager;
2511
+ }
2512
+ /**
2513
+ * Toggles the share mode on or off.
2514
+ */
2515
+ toggleShareMode() {
2516
+ this.shareManager.toggleShareMode();
1403
2517
  }
1404
2518
  /**
1405
2519
  * Scan the given element for toggles and tab groups, register them
@@ -1432,7 +2546,7 @@ class CustomViewsCore {
1432
2546
  return newComponentsFound;
1433
2547
  }
1434
2548
  /**
1435
- * Unscan the given element for toggles and tab groups, de-register them
2549
+ * Unscan the given element for toggles and tab groups, de-register them from registry
1436
2550
  */
1437
2551
  unscan(element) {
1438
2552
  // Unscan for toggles
@@ -1536,8 +2650,10 @@ class CustomViewsCore {
1536
2650
  // For session history, clicks on back/forward button
1537
2651
  window.addEventListener("popstate", () => {
1538
2652
  this.loadAndCallApplyState();
2653
+ this.focusManager.handleUrlChange();
1539
2654
  });
1540
2655
  this.loadAndCallApplyState();
2656
+ this.focusManager.init();
1541
2657
  this.initObserver();
1542
2658
  }
1543
2659
  initializeNewComponents() {
@@ -1559,7 +2675,7 @@ class CustomViewsCore {
1559
2675
  const currentToggles = this.getCurrentActiveToggles();
1560
2676
  const newState = {
1561
2677
  toggles: currentToggles,
1562
- tabs: currentTabs
2678
+ tabs: currentTabs,
1563
2679
  };
1564
2680
  // 2. Apply state with scroll anchor information
1565
2681
  this.applyState(newState, {
@@ -1827,10 +2943,11 @@ function prependBaseUrl(path, baseUrl) {
1827
2943
  }
1828
2944
 
1829
2945
  /**
1830
- * Custom Elements for Tab Groups and Tabs
2946
+ * Defines the custom elements used by CustomViews.
1831
2947
  */
1832
2948
  /**
1833
- * <cv-tab> element - represents a single tab panel
2949
+ * `<cv-tab>`: A custom element representing a single tab panel within a tab group.
2950
+ * Its content is displayed when the corresponding tab is active.
1834
2951
  */
1835
2952
  class CVTab extends HTMLElement {
1836
2953
  connectedCallback() {
@@ -1838,7 +2955,8 @@ class CVTab extends HTMLElement {
1838
2955
  }
1839
2956
  }
1840
2957
  /**
1841
- * <cv-tabgroup> element - represents a group of tabs
2958
+ * `<cv-tabgroup>`: A custom element that encapsulates a set of tabs (`<cv-tab>`).
2959
+ * It manages the tab navigation and content visibility for the group.
1842
2960
  */
1843
2961
  class CVTabgroup extends HTMLElement {
1844
2962
  connectedCallback() {
@@ -1854,7 +2972,7 @@ class CVTabgroup extends HTMLElement {
1854
2972
  }
1855
2973
  }
1856
2974
  /**
1857
- * <cv-toggle> element - represents a toggleable content block
2975
+ * `<cv-toggle>`: A custom element for creating a toggleable content block.
1858
2976
  */
1859
2977
  class CVToggle extends HTMLElement {
1860
2978
  connectedCallback() {
@@ -1862,8 +2980,8 @@ class CVToggle extends HTMLElement {
1862
2980
  }
1863
2981
  }
1864
2982
  /**
1865
- * <cv-tab-header> element - represents tab header with rich HTML formatting
1866
- * Content is extracted and used in the navigation link
2983
+ * `<cv-tab-header>`: A semantic container for a tab's header content.
2984
+ * The content of this element is used to create the navigation link for the tab.
1867
2985
  */
1868
2986
  class CVTabHeader extends HTMLElement {
1869
2987
  connectedCallback() {
@@ -1871,8 +2989,7 @@ class CVTabHeader extends HTMLElement {
1871
2989
  }
1872
2990
  }
1873
2991
  /**
1874
- * <cv-tab-body> element - represents tab body content
1875
- * Semantic container for tab panel content
2992
+ * `<cv-tab-body>`: A semantic container for the main content of a tab panel.
1876
2993
  */
1877
2994
  class CVTabBody extends HTMLElement {
1878
2995
  connectedCallback() {
@@ -1880,7 +2997,7 @@ class CVTabBody extends HTMLElement {
1880
2997
  }
1881
2998
  }
1882
2999
  /**
1883
- * Register custom elements
3000
+ * Registers all CustomViews custom elements with the CustomElementRegistry.
1884
3001
  */
1885
3002
  function registerCustomElements() {
1886
3003
  // Only register if not already defined
@@ -2845,6 +3962,33 @@ const WIDGET_STYLES = `
2845
3962
  padding: 0.75rem;
2846
3963
  border-top: 1px solid rgba(0, 0, 0, 0.1);
2847
3964
  }
3965
+
3966
+ .cv-footer-link {
3967
+ display: flex;
3968
+ align-items: center;
3969
+ justify-content: center;
3970
+ gap: 0.5rem;
3971
+ font-size: 0.75rem;
3972
+ color: rgba(0, 0, 0, 0.5);
3973
+ text-decoration: none;
3974
+ transition: color 0.2s ease;
3975
+ }
3976
+
3977
+ .cv-footer-link:hover {
3978
+ color: #3e84f4;
3979
+ }
3980
+
3981
+ .cv-footer-link svg {
3982
+ opacity: 0.8;
3983
+ }
3984
+
3985
+ .cv-widget-theme-dark .cv-footer-link {
3986
+ color: rgba(255, 255, 255, 0.4);
3987
+ }
3988
+
3989
+ .cv-widget-theme-dark .cv-footer-link:hover {
3990
+ color: #60a5fa;
3991
+ }
2848
3992
 
2849
3993
  .cv-reset-btn,
2850
3994
  .cv-share-btn {
@@ -3036,6 +4180,154 @@ const WIDGET_STYLES = `
3036
4180
  display: none !important;
3037
4181
  }
3038
4182
  }
4183
+ /* Widget Modal Tabs */
4184
+ .cv-modal-tabs {
4185
+ display: flex;
4186
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
4187
+ margin-bottom: 0.5rem;
4188
+ }
4189
+
4190
+ .cv-tab-content > .cv-content-section + .cv-content-section {
4191
+ margin-top: 1.5rem;
4192
+ }
4193
+
4194
+ .cv-modal-tab {
4195
+ padding: 0.75rem 1.5rem;
4196
+ font-size: 0.875rem;
4197
+ font-weight: 500;
4198
+ color: rgba(0, 0, 0, 0.6);
4199
+ background: none;
4200
+ border: none;
4201
+ border-bottom: 2px solid transparent;
4202
+ cursor: pointer;
4203
+ transition: all 0.2s ease;
4204
+ }
4205
+
4206
+ .cv-modal-tab:hover {
4207
+ color: rgba(0, 0, 0, 0.9);
4208
+ }
4209
+
4210
+ .cv-modal-tab.active {
4211
+ color: #3e84f4;
4212
+ border-bottom-color: #3e84f4;
4213
+ }
4214
+
4215
+ .cv-tab-content {
4216
+ display: none;
4217
+ animation: fadeIn 0.3s ease;
4218
+ }
4219
+
4220
+ .cv-tab-content.active {
4221
+ display: block;
4222
+ }
4223
+
4224
+ /* Share Tab Content */
4225
+ .cv-share-content {
4226
+ display: flex;
4227
+ flex-direction: column;
4228
+ gap: 1rem;
4229
+ padding: 1rem 0;
4230
+ align-items: center;
4231
+ text-align: center;
4232
+ }
4233
+
4234
+ .cv-share-instruction {
4235
+ font-size: 0.9rem;
4236
+ color: rgba(0, 0, 0, 0.7);
4237
+ margin-bottom: 1rem;
4238
+ }
4239
+
4240
+ .cv-share-action-btn {
4241
+ display: flex;
4242
+ align-items: center;
4243
+ justify-content: center;
4244
+ gap: 0.5rem;
4245
+ width: 100%;
4246
+ padding: 12px 16px;
4247
+ background: white;
4248
+ color: #333;
4249
+ border: 1px solid rgba(0, 0, 0, 0.15);
4250
+ border-radius: 6px;
4251
+ cursor: pointer;
4252
+ font-size: 0.9rem;
4253
+ font-weight: 500;
4254
+ transition: all 0.2s ease;
4255
+ }
4256
+
4257
+ .cv-share-action-btn:hover {
4258
+ background: #f8f9fa;
4259
+ border-color: rgba(0, 0, 0, 0.25);
4260
+ transform: translateY(-1px);
4261
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
4262
+ }
4263
+
4264
+ .cv-share-action-btn.primary {
4265
+ background: #3e84f4;
4266
+ color: white;
4267
+ border-color: #3e84f4;
4268
+ }
4269
+
4270
+ .cv-share-action-btn.primary:hover {
4271
+ background: #2b74e6;
4272
+ border-color: #2b74e6;
4273
+ }
4274
+
4275
+ .cv-done-btn {
4276
+ padding: 0.375rem 1rem;
4277
+ background: #3e84f4;
4278
+ color: white;
4279
+ border: none;
4280
+ border-radius: 0.5rem;
4281
+ font-weight: 600;
4282
+ font-size: 0.875rem;
4283
+ cursor: pointer;
4284
+ transition: all 0.2s ease;
4285
+ }
4286
+
4287
+ .cv-done-btn:hover {
4288
+ background: #2b74e6;
4289
+ }
4290
+
4291
+ /* Dark Theme Adjustments */
4292
+ .cv-widget-theme-dark .cv-modal-tabs {
4293
+ border-color: rgba(255, 255, 255, 0.1);
4294
+ }
4295
+
4296
+ .cv-widget-theme-dark .cv-modal-tab {
4297
+ color: rgba(255, 255, 255, 0.6);
4298
+ }
4299
+
4300
+ .cv-widget-theme-dark .cv-modal-tab:hover {
4301
+ color: rgba(255, 255, 255, 0.9);
4302
+ }
4303
+
4304
+ .cv-widget-theme-dark .cv-modal-tab.active {
4305
+ color: #60a5fa;
4306
+ border-bottom-color: #60a5fa;
4307
+ }
4308
+
4309
+ .cv-widget-theme-dark .cv-share-instruction {
4310
+ color: rgba(255, 255, 255, 0.7);
4311
+ }
4312
+
4313
+ .cv-widget-theme-dark .cv-share-action-btn {
4314
+ background: #1a202c;
4315
+ color: white;
4316
+ border-color: rgba(255, 255, 255, 0.15);
4317
+ }
4318
+
4319
+ .cv-widget-theme-dark .cv-share-action-btn:hover {
4320
+ background: #2d3748;
4321
+ }
4322
+
4323
+ .cv-widget-theme-dark .cv-share-action-btn.primary {
4324
+ background: #3e84f4;
4325
+ border-color: #3e84f4;
4326
+ }
4327
+
4328
+ .cv-widget-theme-dark .cv-share-action-btn.primary:hover {
4329
+ background: #2b74e6;
4330
+ }
3039
4331
  `;
3040
4332
  /**
3041
4333
  * Inject widget styles into the document head
@@ -3058,6 +4350,7 @@ class CustomViewsWidget {
3058
4350
  _hasVisibleConfig = false;
3059
4351
  pageToggleIds = new Set();
3060
4352
  pageTabIds = new Set();
4353
+ currentTab = 'customize';
3061
4354
  // Modal state
3062
4355
  stateModal = null;
3063
4356
  constructor(options) {
@@ -3233,12 +4526,12 @@ class CustomViewsWidget {
3233
4526
  </div>
3234
4527
  <div class="cv-tabgroup-info">
3235
4528
  <div class="cv-tabgroup-title-container">
3236
- <p class="cv-tabgroup-title">Navigation Headers</p>
4529
+ <p class="cv-tabgroup-title">Show only the selected tab</p>
3237
4530
  </div>
3238
- <p class="cv-tabgroup-description">Show or hide navigation headers</p>
4531
+ <p class="cv-tabgroup-description">Hide the navigation headers</p>
3239
4532
  </div>
3240
4533
  <label class="cv-toggle-switch cv-nav-toggle">
3241
- <input class="cv-nav-pref-input" type="checkbox" ${initialNavsVisible ? 'checked' : ''} aria-label="Show or hide navigation headers" />
4534
+ <input class="cv-nav-pref-input" type="checkbox" ${initialNavsVisible ? '' : 'checked'} aria-label="Show only the selected tab" />
3242
4535
  <span class="cv-switch-bg"></span>
3243
4536
  <span class="cv-switch-knob"></span>
3244
4537
  </label>
@@ -3274,36 +4567,64 @@ class CustomViewsWidget {
3274
4567
  <main class="cv-modal-main">
3275
4568
  ${this.options.description ? `<p class="cv-modal-description">${this.options.description}</p>` : ''}
3276
4569
 
3277
- ${visibleToggles.length ? `
3278
- <div class="cv-content-section">
3279
- <div class="cv-section-heading">Toggles</div>
3280
- <div class="cv-toggles-container">
3281
- ${toggleControlsHtml}
4570
+ <div class="cv-modal-tabs">
4571
+ <button class="cv-modal-tab ${this.currentTab === 'customize' ? 'active' : ''}" data-tab="customize">Customize</button>
4572
+ <button class="cv-modal-tab ${this.currentTab === 'share' ? 'active' : ''}" data-tab="share">Share</button>
4573
+ </div>
4574
+
4575
+ <div class="cv-tab-content ${this.currentTab === 'customize' ? 'active' : ''}" data-content="customize">
4576
+ ${visibleToggles.length ? `
4577
+ <div class="cv-content-section">
4578
+ <div class="cv-section-heading">Toggles</div>
4579
+ <div class="cv-toggles-container">
4580
+ ${toggleControlsHtml}
4581
+ </div>
4582
+ </div>
4583
+ ` : ''}
4584
+
4585
+ ${this.options.showTabGroups && tabGroups && tabGroups.length > 0 ? `
4586
+ <div class="cv-content-section">
4587
+ <div class="cv-section-heading">Tab Groups</div>
4588
+ <div class="cv-tabgroups-container">
4589
+ ${tabGroupControlsHTML}
4590
+ </div>
3282
4591
  </div>
4592
+ ` : ''}
3283
4593
  </div>
3284
- ` : ''}
3285
-
3286
- ${this.options.showTabGroups && tabGroups && tabGroups.length > 0 ? `
3287
- <div class="cv-content-section">
3288
- <div class="cv-section-heading">Tab Groups</div>
3289
- <div class="cv-tabgroups-container">
3290
- ${tabGroupControlsHTML}
4594
+
4595
+ <div class="cv-tab-content ${this.currentTab === 'share' ? 'active' : ''}" data-content="share">
4596
+ <div class="cv-share-content">
4597
+ <div class="cv-share-instruction">
4598
+ Create a shareable link for your current customization, or select specific parts of the page to share.
4599
+ </div>
4600
+
4601
+ <button class="cv-share-action-btn primary cv-start-share-btn">
4602
+ <span class="cv-btn-icon">${getShareIcon()}</span>
4603
+ <span>Select elements to share</span>
4604
+ </button>
4605
+
4606
+ <button class="cv-share-action-btn cv-copy-url-btn">
4607
+ <span class="cv-btn-icon">${getCopyIcon()}</span>
4608
+ <span>Copy Shareable URL of Settings</span>
4609
+ </button>
3291
4610
  </div>
3292
4611
  </div>
3293
- ` : ''}
3294
4612
  </main>
3295
4613
 
3296
4614
  <footer class="cv-modal-footer">
3297
4615
  ${this.options.showReset ? `
3298
- <button class="cv-reset-btn">
4616
+ <button class="cv-reset-btn" title="Reset to Default">
3299
4617
  <span class="cv-reset-btn-icon">${getResetIcon()}</span>
3300
- <span>Reset to Default</span>
3301
- </button>
3302
- ` : ''}
3303
- <button class="cv-share-btn">
3304
- <span>Copy Shareable URL</span>
3305
- <span class="cv-share-btn-icon">${getCopyIcon()}</span>
4618
+ <span>Reset</span>
3306
4619
  </button>
4620
+ ` : '<div></div>'}
4621
+
4622
+ <a href="https://github.com/customviews-js/customviews" target="_blank" class="cv-footer-link">
4623
+ ${getGitHubIcon()}
4624
+ <span>View on GitHub</span>
4625
+ </a>
4626
+
4627
+ <button class="cv-done-btn">Done</button>
3307
4628
  </footer>
3308
4629
  </div>
3309
4630
  `;
@@ -3324,20 +4645,6 @@ class CustomViewsWidget {
3324
4645
  this.closeModal();
3325
4646
  return;
3326
4647
  }
3327
- // Copy URL button
3328
- if (target.closest('.cv-share-btn')) {
3329
- this.copyShareableURL();
3330
- const copyUrlBtn = target.closest('.cv-share-btn');
3331
- const iconContainer = copyUrlBtn?.querySelector('.cv-share-btn-icon');
3332
- if (iconContainer) {
3333
- const originalIcon = iconContainer.innerHTML;
3334
- iconContainer.innerHTML = getTickIcon();
3335
- setTimeout(() => {
3336
- iconContainer.innerHTML = originalIcon;
3337
- }, 3000);
3338
- }
3339
- return;
3340
- }
3341
4648
  // Reset to default button
3342
4649
  if (target.closest('.cv-reset-btn')) {
3343
4650
  const resetBtn = target.closest('.cv-reset-btn');
@@ -3354,6 +4661,11 @@ class CustomViewsWidget {
3354
4661
  }, 600);
3355
4662
  return;
3356
4663
  }
4664
+ // Done button
4665
+ if (target.closest('.cv-done-btn')) {
4666
+ this.closeModal();
4667
+ return;
4668
+ }
3357
4669
  // Overlay click to close
3358
4670
  if (e.target === this.stateModal) {
3359
4671
  this.closeModal();
@@ -3414,10 +4726,10 @@ class CustomViewsWidget {
3414
4726
  navIcon.innerHTML = isVisible ? getNavHeadingOnIcon() : getNavHeadingOffIcon();
3415
4727
  }
3416
4728
  };
3417
- navHeaderCard.addEventListener('mouseenter', () => updateIcon(tabNavToggle.checked, true));
3418
- navHeaderCard.addEventListener('mouseleave', () => updateIcon(tabNavToggle.checked, false));
4729
+ navHeaderCard.addEventListener('mouseenter', () => updateIcon(!tabNavToggle.checked, true));
4730
+ navHeaderCard.addEventListener('mouseleave', () => updateIcon(!tabNavToggle.checked, false));
3419
4731
  tabNavToggle.addEventListener('change', () => {
3420
- const visible = tabNavToggle.checked;
4732
+ const visible = !tabNavToggle.checked;
3421
4733
  updateIcon(visible, false);
3422
4734
  this.core.persistTabNavVisibility(visible);
3423
4735
  try {
@@ -3428,6 +4740,48 @@ class CustomViewsWidget {
3428
4740
  }
3429
4741
  });
3430
4742
  }
4743
+ // Tab switching
4744
+ const tabs = this.stateModal.querySelectorAll('.cv-modal-tab');
4745
+ tabs.forEach(tab => {
4746
+ tab.addEventListener('click', () => {
4747
+ const tabId = tab.dataset.tab;
4748
+ if (tabId === 'customize' || tabId === 'share') {
4749
+ this.currentTab = tabId;
4750
+ // Update UI without full re-render
4751
+ tabs.forEach(t => t.classList.remove('active'));
4752
+ tab.classList.add('active');
4753
+ const contents = this.stateModal?.querySelectorAll('.cv-tab-content');
4754
+ contents?.forEach(c => {
4755
+ c.classList.remove('active');
4756
+ if (c.dataset.content === tabId) {
4757
+ c.classList.add('active');
4758
+ }
4759
+ });
4760
+ }
4761
+ });
4762
+ });
4763
+ // Share buttons (inside content)
4764
+ const startShareBtn = this.stateModal.querySelector('.cv-start-share-btn');
4765
+ if (startShareBtn) {
4766
+ startShareBtn.addEventListener('click', () => {
4767
+ this.closeModal();
4768
+ this.core.toggleShareMode();
4769
+ });
4770
+ }
4771
+ const copyUrlBtn = this.stateModal.querySelector('.cv-copy-url-btn');
4772
+ if (copyUrlBtn) {
4773
+ copyUrlBtn.addEventListener('click', () => {
4774
+ this.copyShareableURL();
4775
+ const iconContainer = copyUrlBtn.querySelector('.cv-btn-icon');
4776
+ if (iconContainer) {
4777
+ const originalIcon = iconContainer.innerHTML;
4778
+ iconContainer.innerHTML = getTickIcon();
4779
+ setTimeout(() => {
4780
+ iconContainer.innerHTML = originalIcon;
4781
+ }, 2000);
4782
+ }
4783
+ });
4784
+ }
3431
4785
  }
3432
4786
  /**
3433
4787
  * Apply theme class to the modal overlay based on options
@@ -3523,7 +4877,7 @@ class CustomViewsWidget {
3523
4877
  const tabNavToggle = this.stateModal.querySelector('.cv-nav-pref-input');
3524
4878
  const navIcon = this.stateModal?.querySelector('#cv-nav-icon');
3525
4879
  if (tabNavToggle) {
3526
- tabNavToggle.checked = navPref;
4880
+ tabNavToggle.checked = !navPref;
3527
4881
  // Ensure UI matches actual visibility
3528
4882
  TabManager.setNavsVisibility(document.body, navPref);
3529
4883
  // Update the nav icon to reflect the current state