@customviews-js/customviews 1.4.0 → 1.4.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.4.0
2
+ * @customviews-js/customviews v1.4.1-beta.1
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
@@ -169,8 +169,12 @@
169
169
  // Create a compact representation
170
170
  const compact = {};
171
171
  // Add toggles if present and non-empty
172
- if (state.toggles && state.toggles.length > 0) {
173
- compact.t = state.toggles;
172
+ if (state.shownToggles && state.shownToggles.length > 0) {
173
+ compact.t = state.shownToggles;
174
+ }
175
+ // Add peek toggles if present and non-empty
176
+ if (state.peekToggles && state.peekToggles.length > 0) {
177
+ compact.p = state.peekToggles;
174
178
  }
175
179
  // Add tab groups if present
176
180
  if (state.tabs && Object.keys(state.tabs).length > 0) {
@@ -231,7 +235,8 @@
231
235
  // Reconstruct State from compact format
232
236
  // Reconstruct Toggles
233
237
  const state = {
234
- toggles: Array.isArray(compact.t) ? compact.t : []
238
+ shownToggles: Array.isArray(compact.t) ? compact.t : [],
239
+ peekToggles: Array.isArray(compact.p) ? compact.p : []
235
240
  };
236
241
  // Reconstruct Tabs
237
242
  if (Array.isArray(compact.g)) {
@@ -444,6 +449,12 @@
444
449
  <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"/>
445
450
  </svg>`;
446
451
  }
452
+ function getChevronDownIcon() {
453
+ return `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
454
+ }
455
+ function getChevronUpIcon() {
456
+ return `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>`;
457
+ }
447
458
 
448
459
  // Constants for selectors
449
460
  const TABGROUP_SELECTOR$1 = 'cv-tabgroup';
@@ -699,7 +710,7 @@
699
710
  });
700
711
  }
701
712
  // Add tooltip for UX feedback (use native title attribute)
702
- navLink.setAttribute('title', 'Double click to change switch tabs across all groups');
713
+ navLink.setAttribute('title', "Double-click a tab to 'pin' it in all similar tab groups.");
703
714
  listItem.appendChild(navLink);
704
715
  navContainer.appendChild(listItem);
705
716
  });
@@ -1014,20 +1025,33 @@
1014
1025
  * ToggleManager handles discovery, visibility, and asset rendering for toggle elements
1015
1026
  */
1016
1027
  class ToggleManager {
1028
+ /**
1029
+ * Track locally expanded elements (that were in peek mode but user expanded them)
1030
+ */
1031
+ static expandedPeekElements = new WeakSet();
1017
1032
  /**
1018
1033
  * Apply toggle visibility to a given list of toggle elements
1019
1034
  */
1020
- static applyToggles(elements, activeToggles) {
1021
- elements.forEach(el => {
1035
+ static applyTogglesVisibility(allToggleElements, activeToggles, peekToggles = []) {
1036
+ allToggleElements.forEach(el => {
1022
1037
  const categories = this.getToggleCategories(el);
1023
1038
  const shouldShow = categories.some(cat => activeToggles.includes(cat));
1024
- this.applyToggleVisibility(el, shouldShow);
1039
+ const shouldPeek = !shouldShow && categories.some(cat => peekToggles.includes(cat));
1040
+ if (!shouldPeek) {
1041
+ this.expandedPeekElements.delete(el);
1042
+ }
1043
+ // If locally expanded, treat as shown (override peek)
1044
+ // Note: If neither show nor peek is active (i.e. hidden), local expansion is ignored/cleared effectively
1045
+ this.applyToggleVisibility(el, shouldShow || (shouldPeek && this.expandedPeekElements.has(el)), shouldPeek && !this.expandedPeekElements.has(el));
1025
1046
  });
1026
1047
  }
1027
1048
  /**
1028
1049
  * Render assets into a given list of toggle elements that are currently visible
1050
+ * Toggles that have a toggleId and are currently visible will have their assets rendered (if any)
1029
1051
  */
1030
- static renderAssets(elements, activeToggles, assetsManager) {
1052
+ static renderToggleAssets(elements, activeToggles, assetsManager) {
1053
+ // TO DO: (gerteck) Enable for peek toggles as well
1054
+ // Also, rework the rendering logic again to make it more user friendly.
1031
1055
  elements.forEach(el => {
1032
1056
  const categories = this.getToggleCategories(el);
1033
1057
  const toggleId = this.getToggleId(el);
@@ -1040,6 +1064,7 @@
1040
1064
  }
1041
1065
  /**
1042
1066
  * Get toggle categories from an element (supports both data attributes and cv-toggle elements)
1067
+ * Note: a toggle can have multiple categories.
1043
1068
  */
1044
1069
  static getToggleCategories(el) {
1045
1070
  if (el.tagName.toLowerCase() === 'cv-toggle') {
@@ -1060,14 +1085,101 @@
1060
1085
  /**
1061
1086
  * Apply simple class-based visibility to a toggle element
1062
1087
  */
1063
- static applyToggleVisibility(el, visible) {
1088
+ static applyToggleVisibility(toggleElement, visible, peek = false) {
1089
+ const isLocallyExpanded = this.expandedPeekElements.has(toggleElement);
1064
1090
  if (visible) {
1065
- el.classList.remove('cv-hidden');
1066
- el.classList.add('cv-visible');
1091
+ toggleElement.classList.remove('cv-hidden', 'cv-peek');
1092
+ toggleElement.classList.add('cv-visible');
1093
+ // Show collapse button ONLY if locally expanded (meaning we are actually in peek mode but expanded).
1094
+ // If globally visible (because of 'Show' state), isLocallyExpanded should have been cleared by applyTogglesVisibility,
1095
+ // so this will be false, and button will be removed.
1096
+ this.manageExpandButton(toggleElement, false, isLocallyExpanded);
1097
+ }
1098
+ else if (peek) {
1099
+ toggleElement.classList.remove('cv-hidden', 'cv-visible');
1100
+ toggleElement.classList.add('cv-peek');
1101
+ // Show/create expand button if peeked
1102
+ this.manageExpandButton(toggleElement, true, false);
1067
1103
  }
1068
1104
  else {
1069
- el.classList.add('cv-hidden');
1070
- el.classList.remove('cv-visible');
1105
+ toggleElement.classList.add('cv-hidden');
1106
+ toggleElement.classList.remove('cv-visible', 'cv-peek');
1107
+ // Ensure button is gone/hidden
1108
+ this.manageExpandButton(toggleElement, false, false);
1109
+ }
1110
+ }
1111
+ /**
1112
+ * Manage the presence of the inline Expand/Collapse button using a wrapper approach
1113
+ */
1114
+ static manageExpandButton(toggleElement, showExpand, showCollapse = false) {
1115
+ // 1. Ensure wrapper exists
1116
+ let wrapper = toggleElement.parentElement;
1117
+ if (!wrapper || !wrapper.classList.contains('cv-wrapper')) {
1118
+ wrapper = document.createElement('div');
1119
+ wrapper.className = 'cv-wrapper';
1120
+ toggleElement.parentNode?.insertBefore(wrapper, toggleElement);
1121
+ wrapper.appendChild(toggleElement);
1122
+ }
1123
+ const btn = wrapper.querySelector('.cv-expand-btn');
1124
+ // 2. Handle "No Button" case (neither expand nor collapse)
1125
+ if (!showExpand && !showCollapse) {
1126
+ if (btn)
1127
+ btn.style.display = 'none';
1128
+ // If content is visible globally (not hidden), ensure wrapper has 'cv-expanded'
1129
+ // to hide the peek fade effect (since fade is for peek state only).
1130
+ if (!toggleElement.classList.contains('cv-hidden')) {
1131
+ wrapper.classList.add('cv-expanded');
1132
+ }
1133
+ else {
1134
+ wrapper.classList.remove('cv-expanded');
1135
+ }
1136
+ return;
1137
+ }
1138
+ // 3. Handle Button Needed (Expand or Collapse)
1139
+ const action = showExpand ? 'expand' : 'collapse';
1140
+ // Update Wrapper Class Logic
1141
+ // If showExpand (Peek state) -> remove cv-expanded (show fade)
1142
+ // If showCollapse (Expanded peek) -> add cv-expanded (hide fade)
1143
+ if (showExpand) {
1144
+ wrapper.classList.remove('cv-expanded');
1145
+ }
1146
+ else {
1147
+ if (!wrapper.classList.contains('cv-expanded'))
1148
+ wrapper.classList.add('cv-expanded');
1149
+ }
1150
+ // Check if existing button matches desired state
1151
+ const currentAction = btn?.getAttribute('data-action');
1152
+ if (btn && currentAction === action) {
1153
+ btn.style.display = 'flex';
1154
+ return;
1155
+ }
1156
+ // 4. Create New Button (if missing or state changed)
1157
+ const iconSvg = showExpand ? getChevronDownIcon() : getChevronUpIcon();
1158
+ const newBtn = document.createElement('button');
1159
+ newBtn.className = 'cv-expand-btn';
1160
+ newBtn.innerHTML = iconSvg;
1161
+ newBtn.setAttribute('aria-label', showExpand ? 'Expand content' : 'Collapse content');
1162
+ newBtn.setAttribute('data-action', action); // Track state
1163
+ newBtn.style.display = 'flex';
1164
+ newBtn.addEventListener('click', (e) => {
1165
+ e.stopPropagation();
1166
+ // Logic: Toggle expansion state
1167
+ if (showExpand) {
1168
+ wrapper.classList.add('cv-expanded');
1169
+ this.expandedPeekElements.add(toggleElement);
1170
+ this.applyToggleVisibility(toggleElement, true, false);
1171
+ }
1172
+ else {
1173
+ wrapper.classList.remove('cv-expanded');
1174
+ this.expandedPeekElements.delete(toggleElement);
1175
+ this.applyToggleVisibility(toggleElement, false, true);
1176
+ }
1177
+ });
1178
+ if (btn) {
1179
+ btn.replaceWith(newBtn);
1180
+ }
1181
+ else {
1182
+ wrapper.appendChild(newBtn);
1071
1183
  }
1072
1184
  }
1073
1185
  /**
@@ -1075,15 +1187,15 @@
1075
1187
  * This includes applying visibility and rendering assets.
1076
1188
  */
1077
1189
  static initializeToggles(root, activeToggles, assetsManager) {
1078
- const elements = [];
1190
+ const allToggleElements = [];
1079
1191
  if (root.matches('[data-cv-toggle], [data-customviews-toggle], cv-toggle')) {
1080
- elements.push(root);
1192
+ allToggleElements.push(root);
1081
1193
  }
1082
- root.querySelectorAll('[data-cv-toggle], [data-customviews-toggle], cv-toggle').forEach(el => elements.push(el));
1083
- if (elements.length === 0)
1194
+ root.querySelectorAll('[data-cv-toggle], [data-customviews-toggle], cv-toggle').forEach(el => allToggleElements.push(el));
1195
+ if (allToggleElements.length === 0)
1084
1196
  return;
1085
- this.applyToggles(elements, activeToggles);
1086
- this.renderAssets(elements, activeToggles, assetsManager);
1197
+ this.applyTogglesVisibility(allToggleElements, activeToggles);
1198
+ this.renderToggleAssets(allToggleElements, activeToggles, assetsManager);
1087
1199
  }
1088
1200
  }
1089
1201
 
@@ -1191,17 +1303,16 @@
1191
1303
  const TOGGLE_STYLES = `
1192
1304
  /* Core toggle visibility transitions */
1193
1305
  [data-cv-toggle], [data-customviews-toggle], cv-toggle {
1194
- transition: opacity 150ms ease,
1195
- transform 150ms ease,
1196
- max-height 200ms ease,
1197
- margin 150ms ease;
1198
- will-change: opacity, transform, max-height, margin;
1306
+ display: block;
1307
+ overflow: hidden;
1308
+ /* Removed transitions for instant toggling */
1199
1309
  }
1200
1310
 
1311
+ /* Open State */
1201
1312
  .cv-visible {
1202
1313
  opacity: 1 !important;
1203
1314
  transform: translateY(0) !important;
1204
- max-height: var(--cv-max-height, 9999px) !important;
1315
+ max-height: none !important;
1205
1316
  }
1206
1317
 
1207
1318
  .cv-hidden {
@@ -1217,6 +1328,61 @@
1217
1328
  margin-bottom: 0 !important;
1218
1329
  overflow: hidden !important;
1219
1330
  }
1331
+
1332
+ /* Close/Peek State */
1333
+ .cv-peek {
1334
+ display: block !important;
1335
+ max-height: 70px !important;
1336
+ overflow: hidden !important;
1337
+ opacity: 1 !important;
1338
+ transform: translateY(0) !important;
1339
+ mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
1340
+ -webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
1341
+ }
1342
+
1343
+ .cv-wrapper {
1344
+ position: relative;
1345
+ width: 100%;
1346
+ display: block;
1347
+ margin-bottom: 24px; /* Space for the button */
1348
+ }
1349
+
1350
+ .cv-expand-btn {
1351
+ position: absolute;
1352
+ bottom: -28px; /* Mostly outside, slight overlap */
1353
+ left: 50%;
1354
+ transform: translateX(-50%);
1355
+ display: flex;
1356
+ background: transparent;
1357
+ border: none;
1358
+ border-radius: 50%;
1359
+ padding: 4px;
1360
+ width: 32px;
1361
+ height: 32px;
1362
+ cursor: pointer;
1363
+ z-index: 100;
1364
+ align-items: center;
1365
+ justify-content: center;
1366
+ color: #888;
1367
+ transition: all 0.2s ease;
1368
+ box-shadow: none;
1369
+ }
1370
+
1371
+ .cv-expand-btn:hover {
1372
+ background: rgba(0, 0, 0, 0.05);
1373
+ color: #000;
1374
+ transform: translateX(-50%) scale(1.1);
1375
+ }
1376
+
1377
+ .cv-expand-btn svg {
1378
+ display: block;
1379
+ opacity: 0.6;
1380
+ }
1381
+
1382
+ .cv-expand-btn:hover svg {
1383
+ opacity: 1;
1384
+ }
1385
+
1220
1386
  `;
1221
1387
 
1222
1388
  /**
@@ -2572,6 +2738,12 @@ ${TAB_STYLES}
2572
2738
  this.componentRegistry.tabGroups.delete(tabGroup);
2573
2739
  });
2574
2740
  }
2741
+ /**
2742
+ * Check if there are any active components in the registry
2743
+ */
2744
+ hasActiveComponents() {
2745
+ return this.componentRegistry.toggles.size > 0 || this.componentRegistry.tabGroups.size > 0;
2746
+ }
2575
2747
  getConfig() {
2576
2748
  return this.config;
2577
2749
  }
@@ -2606,7 +2778,7 @@ ${TAB_STYLES}
2606
2778
  });
2607
2779
  }
2608
2780
  const computedState = {
2609
- toggles: this.config.toggles?.map(t => t.id) || [],
2781
+ shownToggles: this.config.toggles?.map(t => t.id) || [],
2610
2782
  tabs
2611
2783
  };
2612
2784
  return computedState;
@@ -2678,9 +2850,9 @@ ${TAB_STYLES}
2678
2850
  const initialTop = anchorElement.getBoundingClientRect().top;
2679
2851
  const currentTabs = this.getCurrentActiveTabs();
2680
2852
  currentTabs[groupId] = tabId;
2681
- const currentToggles = this.getCurrentActiveToggles();
2853
+ const currentState = this.getCurrentState();
2682
2854
  const newState = {
2683
- toggles: currentToggles,
2855
+ ...currentState,
2684
2856
  tabs: currentTabs,
2685
2857
  };
2686
2858
  // 2. Apply state with scroll anchor information
@@ -2736,7 +2908,8 @@ ${TAB_STYLES}
2736
2908
  // 1. URL State
2737
2909
  const urlState = URLStateManager.parseURL();
2738
2910
  if (urlState) {
2739
- this.applyState(urlState);
2911
+ // Apply URL state temporarily (do not persist until interaction)
2912
+ this.applyState(urlState, { persist: false });
2740
2913
  return;
2741
2914
  }
2742
2915
  // 2. Persisted State
@@ -2750,9 +2923,10 @@ ${TAB_STYLES}
2750
2923
  }
2751
2924
  /**
2752
2925
  * Apply a custom state, saves to localStorage and updates the URL
2753
- * Add 'source' in options to indicate the origin of the state change
2926
+ * 'source' in options indicates the origin of the state change
2754
2927
  * (e.g., 'widget' to trigger scroll behavior)
2755
- * Add scrollAnchor in options to maintain scroll position of a specific element
2928
+ * 'scrollAnchor' in options indicates the element to maintain scroll position of
2929
+ * 'persist' (default true) to control whether to save to localStorage
2756
2930
  */
2757
2931
  applyState(state, options) {
2758
2932
  // console.log(`[Core] applyState called with source: ${options?.source}`, state);
@@ -2762,7 +2936,10 @@ ${TAB_STYLES}
2762
2936
  }
2763
2937
  const snapshot = this.cloneState(state);
2764
2938
  this.renderState(snapshot);
2765
- this.persistenceManager.persistState(snapshot);
2939
+ // Only persist if explicitly requested (default true)
2940
+ if (options?.persist !== false) {
2941
+ this.persistenceManager.persistState(snapshot);
2942
+ }
2766
2943
  if (this.showUrlEnabled) {
2767
2944
  URLStateManager.updateURL(snapshot);
2768
2945
  }
@@ -2789,14 +2966,13 @@ ${TAB_STYLES}
2789
2966
  renderState(state) {
2790
2967
  this.observer?.disconnect();
2791
2968
  this.lastAppliedState = this.cloneState(state);
2792
- const toggles = state?.toggles || [];
2969
+ const toggles = state?.shownToggles || [];
2793
2970
  const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
2794
- const toggleElements = Array.from(this.componentRegistry.toggles);
2971
+ const allToggleElements = Array.from(this.componentRegistry.toggles);
2795
2972
  const tabGroupElements = Array.from(this.componentRegistry.tabGroups);
2796
- // Apply toggle visibility
2797
- ToggleManager.applyToggles(toggleElements, finalToggles);
2973
+ ToggleManager.applyTogglesVisibility(allToggleElements, finalToggles, state.peekToggles);
2798
2974
  // Render assets into toggles
2799
- ToggleManager.renderAssets(toggleElements, finalToggles, this.assetsManager);
2975
+ ToggleManager.renderToggleAssets(allToggleElements, finalToggles, this.assetsManager);
2800
2976
  // Apply tab selections
2801
2977
  TabManager.applyTabSelections(tabGroupElements, state.tabs || {}, this.config.tabGroups);
2802
2978
  // Update nav active states (without rebuilding)
@@ -2827,16 +3003,16 @@ ${TAB_STYLES}
2827
3003
  URLStateManager.clearURL();
2828
3004
  }
2829
3005
  /**
2830
- * Get the currently active toggles regardless of whether they come from custom state or default configuration
3006
+ * Get the full current state including active toggles, peek toggles, and tabs
2831
3007
  */
2832
- getCurrentActiveToggles() {
3008
+ getCurrentState() {
2833
3009
  if (this.lastAppliedState) {
2834
- return this.lastAppliedState.toggles || [];
3010
+ return this.cloneState(this.lastAppliedState);
2835
3011
  }
2836
3012
  if (this.config) {
2837
- return this.getComputedDefaultState().toggles || [];
3013
+ return this.cloneState(this.getComputedDefaultState());
2838
3014
  }
2839
- return [];
3015
+ return {};
2840
3016
  }
2841
3017
  /**
2842
3018
  * Clear all persistence and reset to default
@@ -3532,10 +3708,6 @@ ${TAB_STYLES}
3532
3708
  color: rgba(255, 255, 255, 0.6);
3533
3709
  }
3534
3710
 
3535
- .cv-widget-theme-dark .cv-toggle-slider {
3536
- background: rgba(255, 255, 255, 0.2);
3537
- }
3538
-
3539
3711
  .cv-widget-theme-dark .cv-tab-group-description {
3540
3712
  color: rgba(255, 255, 255, 0.8);
3541
3713
  }
@@ -3656,40 +3828,32 @@ ${TAB_STYLES}
3656
3828
  }
3657
3829
 
3658
3830
  .cv-toggle-input {
3831
+ /* Only hide if it is part of a custom slider toggle */
3832
+ }
3833
+ .cv-toggle-label .cv-toggle-input {
3659
3834
  opacity: 0;
3660
3835
  width: 0;
3661
3836
  height: 0;
3662
3837
  }
3663
3838
 
3664
- .cv-toggle-slider {
3665
- position: absolute;
3666
- top: 0;
3667
- left: 0;
3668
- right: 0;
3669
- bottom: 0;
3670
- background: rgba(0, 0, 0, 0.2);
3671
- border-radius: 9999px;
3672
- transition: background-color 0.2s ease;
3673
- }
3674
-
3675
- .cv-toggle-slider:before {
3676
- position: absolute;
3677
- content: "";
3678
- height: 1rem;
3679
- width: 1rem;
3680
- left: 0.25rem;
3681
- bottom: 0.25rem;
3682
- background: white;
3683
- border-radius: 50%;
3684
- transition: transform 0.2s ease;
3839
+ .cv-toggle-radios {
3840
+ display: flex;
3841
+ gap: 8px;
3685
3842
  }
3686
3843
 
3687
- .cv-toggle-input:checked + .cv-toggle-slider {
3688
- background: #3e84f4;
3844
+ .cv-radio-label {
3845
+ display: flex;
3846
+ align-items: center;
3847
+ gap: 4px;
3848
+ font-size: 0.85rem;
3849
+ cursor: pointer;
3689
3850
  }
3690
3851
 
3691
- .cv-toggle-input:checked + .cv-toggle-slider:before {
3692
- transform: translateX(1.25rem);
3852
+ .cv-radio-label input {
3853
+ margin: 0;
3854
+ opacity: 1;
3855
+ width: auto;
3856
+ height: auto;
3693
3857
  }
3694
3858
 
3695
3859
  /* Dark theme toggle switch styles */
@@ -4038,97 +4202,7 @@ ${TAB_STYLES}
4038
4202
  }
4039
4203
 
4040
4204
  /* Dark theme custom state styles */
4041
- /* Welcome modal styles */
4042
- .cv-welcome-modal {
4043
- max-width: 32rem;
4044
- width: 90vw;
4045
- background: white;
4046
- border-radius: 0.75rem;
4047
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
4048
- animation: slideIn 0.2s ease;
4049
- display: flex;
4050
- flex-direction: column;
4051
- }
4052
-
4053
- .cv-modal-main {
4054
- padding: 1rem;
4055
- flex: 1;
4056
- display: flex;
4057
- flex-direction: column;
4058
- gap: 1rem;
4059
- overflow-y: auto;
4060
- max-height: calc(80vh - 8rem);
4061
- }
4062
-
4063
- .cv-welcome-message {
4064
- font-size: 0.875rem;
4065
- color: rgba(0, 0, 0, 0.8);
4066
- margin: 0;
4067
- line-height: 1.4;
4068
- text-align: center;
4069
- }
4070
-
4071
- .cv-welcome-message a {
4072
- color: #3e84f4;
4073
- text-align: justify;
4074
- text-decoration: none;
4075
- }
4076
-
4077
- .cv-welcome-message a:hover {
4078
- text-decoration: underline;
4079
- }
4080
-
4081
- .cv-welcome-widget-preview {
4082
- display: flex;
4083
- align-items: center;
4084
- justify-content: center;
4085
- gap: 1rem;
4086
- padding: 1rem;
4087
- background: #f8f9fa;
4088
- border-radius: 0.5rem;
4089
- margin: 1rem 0;
4090
- }
4091
-
4092
- .cv-welcome-widget-icon {
4093
- width: 2rem;
4094
- height: 2rem;
4095
- background: rgba(62, 132, 244, 0.1);
4096
- border-radius: 9999px;
4097
- display: flex;
4098
- align-items: center;
4099
- justify-content: center;
4100
- animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
4101
- color: #3e84f4;
4102
- }
4103
-
4104
- .cv-welcome-widget-label {
4105
- font-size: 0.875rem;
4106
- font-weight: 500;
4107
- color: rgba(0, 0, 0, 0.8);
4108
- margin: 0;
4109
- }
4110
-
4111
- .cv-welcome-got-it {
4112
- width: 100%;
4113
- background: #3e84f4;
4114
- color: white;
4115
- font-weight: 600;
4116
- padding: 0.75rem 1rem;
4117
- border-radius: 0.5rem;
4118
- border: none;
4119
- cursor: pointer;
4120
- font-size: 0.875rem;
4121
- transition: background-color 0.2s ease;
4122
- outline: none;
4123
- }
4124
4205
 
4125
- .cv-welcome-got-it:hover {
4126
- background: rgba(62, 132, 244, 0.9);
4127
- }
4128
-
4129
- .cv-welcome-got-it:focus {
4130
- box-shadow: 0 0 0 2px rgba(62, 132, 244, 0.5);
4131
- }
4132
4206
 
4133
4207
  /* Animations */
4134
4208
  @keyframes cv-pulse {
@@ -4140,26 +4214,7 @@ ${TAB_STYLES}
4140
4214
  }
4141
4215
  }
4142
4216
 
4143
- /* Dark theme welcome modal styles */
4144
- .cv-widget-theme-dark .cv-welcome-modal {
4145
- background: #101722;
4146
- }
4147
4217
 
4148
- .cv-widget-theme-dark .cv-welcome-message {
4149
- color: rgba(255, 255, 255, 0.8);
4150
- }
4151
-
4152
- .cv-widget-theme-dark .cv-welcome-message a {
4153
- color: #60a5fa;
4154
- }
4155
-
4156
- .cv-widget-theme-dark .cv-welcome-widget-preview {
4157
- background: rgba(255, 255, 255, 0.1);
4158
- }
4159
-
4160
- .cv-widget-theme-dark .cv-welcome-widget-label {
4161
- color: #e2e8f0;
4162
- }
4163
4218
 
4164
4219
  /* Dark theme logo box */
4165
4220
  .cv-widget-theme-dark .cv-tabgroup-logo-box {
@@ -4334,6 +4389,176 @@ ${TAB_STYLES}
4334
4389
  .cv-widget-theme-dark .cv-share-action-btn.primary:hover {
4335
4390
  background: #2b74e6;
4336
4391
  }
4392
+
4393
+ /* Intro Callout styles */
4394
+ .cv-widget-callout {
4395
+ position: fixed;
4396
+ background: white;
4397
+ border-radius: 8px;
4398
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
4399
+ padding: 12px 16px;
4400
+ width: 260px;
4401
+ z-index: 9999;
4402
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4403
+ animation: cvFadeIn 0.3s ease-out;
4404
+ pointer-events: auto;
4405
+ display: flex;
4406
+ flex-direction: column;
4407
+ gap: 8px;
4408
+ }
4409
+
4410
+ .cv-widget-callout-text {
4411
+ font-size: 0.9rem;
4412
+ color: #333;
4413
+ margin: 0;
4414
+ line-height: 1.4;
4415
+ }
4416
+
4417
+ .cv-widget-callout-close {
4418
+ position: absolute;
4419
+ top: 6px;
4420
+ right: 6px;
4421
+ width: 18px;
4422
+ height: 18px;
4423
+ border: none;
4424
+ background: rgba(0,0,0,0.05);
4425
+ color: #666;
4426
+ cursor: pointer;
4427
+ display: flex;
4428
+ align-items: center;
4429
+ justify-content: center;
4430
+ border-radius: 50%;
4431
+ font-size: 14px;
4432
+ line-height: 1;
4433
+ padding: 0;
4434
+ transition: all 0.2s ease;
4435
+ }
4436
+
4437
+ .cv-widget-callout-close:hover {
4438
+ background: #f0f0f0;
4439
+ color: #333;
4440
+ }
4441
+
4442
+ /* Callout positioning and arrow */
4443
+ .cv-widget-callout::after {
4444
+ content: '';
4445
+ position: absolute;
4446
+ width: 10px;
4447
+ height: 10px;
4448
+ background: white;
4449
+ transform: rotate(45deg);
4450
+ box-shadow: 1px 1px 1px rgba(0,0,0,0.05); /* subtle shadow for arrow */
4451
+ }
4452
+
4453
+ /* Top-Right Widget -> Callout to the left */
4454
+ .cv-widget-callout.cv-pos-top-right {
4455
+ top: 20px;
4456
+ right: 64px;
4457
+ }
4458
+ .cv-widget-callout.cv-pos-top-right::after {
4459
+ top: 13px;
4460
+ right: -5px;
4461
+ box-shadow: 1px -1px 1px rgba(0,0,0,0.05);
4462
+ transform: rotate(45deg);
4463
+ }
4464
+
4465
+ /* Bottom-Right Widget -> Callout to the left */
4466
+ .cv-widget-callout.cv-pos-bottom-right {
4467
+ bottom: 20px;
4468
+ right: 64px;
4469
+ }
4470
+ .cv-widget-callout.cv-pos-bottom-right::after {
4471
+ bottom: 13px;
4472
+ right: -5px;
4473
+ }
4474
+
4475
+ /* Top-Left Widget -> Callout to the right */
4476
+ .cv-widget-callout.cv-pos-top-left {
4477
+ top: 20px;
4478
+ left: 64px;
4479
+ }
4480
+ .cv-widget-callout.cv-pos-top-left::after {
4481
+ top: 13px;
4482
+ left: -5px;
4483
+ }
4484
+
4485
+ /* Bottom-Left Widget -> Callout to the right */
4486
+ .cv-widget-callout.cv-pos-bottom-left {
4487
+ bottom: 20px;
4488
+ left: 64px;
4489
+ }
4490
+ .cv-widget-callout.cv-pos-bottom-left::after {
4491
+ bottom: 13px;
4492
+ left: -5px;
4493
+ }
4494
+
4495
+ /* Middle-Right Widget -> Callout to the left */
4496
+ .cv-widget-callout.cv-pos-middle-right {
4497
+ top: 50%;
4498
+ right: 64px;
4499
+ transform: translateY(-50%);
4500
+ }
4501
+ .cv-widget-callout.cv-pos-middle-right::after {
4502
+ top: 50%;
4503
+ right: -5px;
4504
+ transform: translateY(-50%) rotate(45deg);
4505
+ }
4506
+
4507
+ /* Middle-Left Widget -> Callout to the right */
4508
+ .cv-widget-callout.cv-pos-middle-left {
4509
+ top: 50%;
4510
+ left: 64px;
4511
+ transform: translateY(-50%);
4512
+ }
4513
+ .cv-widget-callout.cv-pos-middle-left::after {
4514
+ top: 50%;
4515
+ left: -5px;
4516
+ transform: translateY(-50%) rotate(45deg);
4517
+ }
4518
+
4519
+ /* Pulse animation utility */
4520
+ .cv-widget-icon.cv-pulse {
4521
+ animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
4522
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
4523
+ }
4524
+
4525
+ @keyframes cv-pulse {
4526
+ 0% {
4527
+ transform: scale(1);
4528
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
4529
+ }
4530
+ 70% {
4531
+ transform: scale(1.05);
4532
+ box-shadow: 0 0 0 10px rgba(62, 132, 244, 0);
4533
+ }
4534
+ 100% {
4535
+ transform: scale(1);
4536
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0);
4537
+ }
4538
+ }
4539
+
4540
+ /* Dark Theme */
4541
+ .cv-widget-theme-dark .cv-widget-callout {
4542
+ background: #1f2937; /* Tailwind gray-800 mostly */
4543
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
4544
+ border: 1px solid rgba(255,255,255,0.1);
4545
+ }
4546
+ .cv-widget-theme-dark .cv-widget-callout::after {
4547
+ background: #1f2937;
4548
+ border-top: 1px solid rgba(255,255,255,0.1);
4549
+ border-right: 1px solid rgba(255,255,255,0.1);
4550
+ }
4551
+ .cv-widget-theme-dark .cv-widget-callout-text {
4552
+ color: #e5e7eb;
4553
+ }
4554
+ .cv-widget-theme-dark .cv-widget-callout-close {
4555
+ background: rgba(255,255,255,0.1);
4556
+ color: #9ca3af;
4557
+ }
4558
+ .cv-widget-theme-dark .cv-widget-callout-close:hover {
4559
+ background: rgba(255,255,255,0.2);
4560
+ color: #fff;
4561
+ }
4337
4562
  `;
4338
4563
  /**
4339
4564
  * Inject widget styles into the document head
@@ -4352,6 +4577,7 @@ ${TAB_STYLES}
4352
4577
  core;
4353
4578
  container;
4354
4579
  widgetIcon = null;
4580
+ introCallout = null;
4355
4581
  options;
4356
4582
  _hasVisibleConfig = false;
4357
4583
  pageToggleIds = new Set();
@@ -4372,8 +4598,7 @@ ${TAB_STYLES}
4372
4598
  title: options.title || 'Customize View',
4373
4599
  description: options.description || '',
4374
4600
  showWelcome: options.showWelcome ?? false,
4375
- welcomeTitle: options.welcomeTitle || 'Site Customization',
4376
- welcomeMessage: options.welcomeMessage || 'This site is powered by Custom Views. Use the widget on the side (⚙) to customize your experience. Your preferences will be saved and can be shared via URL.<br><br>Learn more at <a href="https://github.com/customviews-js/customviews" target="_blank">customviews GitHub</a>.',
4601
+ welcomeMessage: options.welcomeMessage || 'Customize your reading experience (theme, toggles, tabs) here.',
4377
4602
  showTabGroups: options.showTabGroups ?? true
4378
4603
  };
4379
4604
  // Determine if there are any configurations to show
@@ -4415,9 +4640,9 @@ ${TAB_STYLES}
4415
4640
  this.attachEventListeners();
4416
4641
  // Always append to body since it's a floating icon
4417
4642
  document.body.appendChild(this.widgetIcon);
4418
- // Show welcome modal on first visit if enabled
4643
+ // Show intro callout on first visit if enabled
4419
4644
  if (this.options.showWelcome) {
4420
- this.showWelcomeModalIfFirstVisit();
4645
+ this.showIntroCalloutIfFirstVisit();
4421
4646
  }
4422
4647
  return this.widgetIcon;
4423
4648
  }
@@ -4447,6 +4672,11 @@ ${TAB_STYLES}
4447
4672
  this.stateModal.remove();
4448
4673
  this.stateModal = null;
4449
4674
  }
4675
+ // Clean up callout
4676
+ if (this.introCallout) {
4677
+ this.introCallout.remove();
4678
+ this.introCallout = null;
4679
+ }
4450
4680
  }
4451
4681
  attachEventListeners() {
4452
4682
  if (!this.widgetIcon)
@@ -4462,10 +4692,48 @@ ${TAB_STYLES}
4462
4692
  this.stateModal.classList.add('cv-hidden');
4463
4693
  }
4464
4694
  }
4695
+ /**
4696
+ * Dismiss the intro callout
4697
+ */
4698
+ dismissIntroCallout() {
4699
+ if (!this.introCallout)
4700
+ return;
4701
+ const callout = this.introCallout;
4702
+ // Clear reference immediately from class to prevent re-use
4703
+ this.introCallout = null;
4704
+ callout.remove();
4705
+ // Stop pulsing the widget icon
4706
+ if (this.widgetIcon) {
4707
+ this.widgetIcon.classList.remove('cv-pulse');
4708
+ }
4709
+ // Mark as shown in localStorage
4710
+ try {
4711
+ localStorage.setItem('cv-intro-shown', 'true');
4712
+ }
4713
+ catch (e) {
4714
+ // Ignore localStorage errors
4715
+ }
4716
+ }
4465
4717
  /**
4466
4718
  * Open the custom state creator
4467
4719
  */
4468
4720
  openStateModal() {
4721
+ // Dismiss intro callout if valid
4722
+ if (this.introCallout) {
4723
+ this.dismissIntroCallout();
4724
+ }
4725
+ else {
4726
+ // Even if no callout is shown (e.g. page had no content), opening the widget
4727
+ // should count as "seen", preventing future callouts.
4728
+ try {
4729
+ if (!localStorage.getItem('cv-intro-shown')) {
4730
+ localStorage.setItem('cv-intro-shown', 'true');
4731
+ }
4732
+ }
4733
+ catch (e) {
4734
+ // Ignore localStorage errors
4735
+ }
4736
+ }
4469
4737
  if (!this.stateModal) {
4470
4738
  this._createStateModal();
4471
4739
  }
@@ -4506,10 +4774,20 @@ ${TAB_STYLES}
4506
4774
  <div>
4507
4775
  <p class="cv-toggle-title">${toggle.label || toggle.id}</p>
4508
4776
  </div>
4509
- <label class="cv-toggle-label">
4510
- <input class="cv-toggle-input" type="checkbox" data-toggle="${toggle.id}"/>
4511
- <span class="cv-toggle-slider"></span>
4512
- </label>
4777
+ <div class="cv-toggle-radios">
4778
+ <label class="cv-radio-label" title="Hide">
4779
+ <input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="hide" data-toggle="${toggle.id}"/>
4780
+ <span>Hide</span>
4781
+ </label>
4782
+ <label class="cv-radio-label" title="Peek">
4783
+ <input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="peek" data-toggle="${toggle.id}"/>
4784
+ <span>Peek</span>
4785
+ </label>
4786
+ <label class="cv-radio-label" title="Show">
4787
+ <input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="show" data-toggle="${toggle.id}"/>
4788
+ <span>Show</span>
4789
+ </label>
4790
+ </div>
4513
4791
  </div>
4514
4792
  </div>
4515
4793
  `).join('');
@@ -4710,10 +4988,11 @@ ${TAB_STYLES}
4710
4988
  if (groupId && tabId) {
4711
4989
  const currentTabs = this.core.getCurrentActiveTabs();
4712
4990
  currentTabs[groupId] = tabId;
4713
- const currentToggles = this.core.getCurrentActiveToggles();
4991
+ const currentState = this.core.getCurrentState();
4714
4992
  const newState = {
4715
- toggles: currentToggles,
4716
- tabs: currentTabs
4993
+ shownToggles: currentState.shownToggles || [],
4994
+ peekToggles: currentState.peekToggles || [], // Preserve peek state, fallback to empty array
4995
+ tabs: currentTabs,
4717
4996
  };
4718
4997
  this.core.applyState(newState, { source: 'widget' });
4719
4998
  }
@@ -4811,25 +5090,33 @@ ${TAB_STYLES}
4811
5090
  }
4812
5091
  // Collect toggle values
4813
5092
  const toggles = [];
4814
- const toggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
4815
- toggleInputs.forEach(toggleInput => {
4816
- const toggle = toggleInput.dataset.toggle;
4817
- if (toggle && toggleInput.checked) {
4818
- toggles.push(toggle);
4819
- }
4820
- });
4821
- // Collect tab selections
4822
- const tabGroupSelects = this.stateModal.querySelectorAll('.cv-tabgroup-select');
4823
- const tabs = {};
4824
- tabGroupSelects.forEach(select => {
4825
- const groupId = select.dataset.groupId;
4826
- if (groupId) {
4827
- tabs[groupId] = select.value;
5093
+ const peekToggles = [];
5094
+ // Get all radio inputs
5095
+ const radios = this.stateModal.querySelectorAll('input[type="radio"]:checked');
5096
+ radios.forEach(radio => {
5097
+ const input = radio;
5098
+ const toggleId = input.getAttribute('data-toggle');
5099
+ if (toggleId) {
5100
+ if (input.value === 'show') {
5101
+ toggles.push(toggleId);
5102
+ }
5103
+ else if (input.value === 'peek') {
5104
+ peekToggles.push(toggleId);
5105
+ }
4828
5106
  }
4829
5107
  });
4830
- const result = { toggles };
4831
- if (Object.keys(tabs).length > 0) {
4832
- result.tabs = tabs;
5108
+ const result = { shownToggles: toggles, peekToggles };
5109
+ // Get active tabs from selects
5110
+ const selects = this.stateModal.querySelectorAll('select[data-group-id]');
5111
+ if (selects.length > 0) {
5112
+ result.tabs = {};
5113
+ selects.forEach(select => {
5114
+ const el = select;
5115
+ const groupId = el.getAttribute('data-group-id');
5116
+ if (groupId) {
5117
+ result.tabs[groupId] = el.value;
5118
+ }
5119
+ });
4833
5120
  }
4834
5121
  return result;
4835
5122
  }
@@ -4849,18 +5136,29 @@ ${TAB_STYLES}
4849
5136
  loadCurrentStateIntoForm() {
4850
5137
  if (!this.stateModal)
4851
5138
  return;
4852
- // Get currently active toggles (from custom state or default configuration)
4853
- const activeToggles = this.core.getCurrentActiveToggles();
4854
- // First, uncheck all toggle inputs
5139
+ // We need complete state for both shown and peek toggles
5140
+ const currentState = this.core.getCurrentState();
5141
+ const currentToggles = currentState.shownToggles || [];
5142
+ const currentPeekToggles = currentState.peekToggles || [];
5143
+ // Reset all inputs first (optional, but good for clarity)
4855
5144
  const allToggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
4856
- allToggleInputs.forEach(toggleInput => {
4857
- toggleInput.checked = false;
5145
+ // Identify unique toggles present in the modal
5146
+ const uniqueToggles = new Set();
5147
+ allToggleInputs.forEach(input => {
5148
+ if (input.dataset.toggle)
5149
+ uniqueToggles.add(input.dataset.toggle);
4858
5150
  });
4859
- // Then check the ones that should be active
4860
- activeToggles.forEach(toggle => {
4861
- const toggleInput = this.stateModal?.querySelector(`[data-toggle="${toggle}"]`);
4862
- if (toggleInput) {
4863
- toggleInput.checked = true;
5151
+ uniqueToggles.forEach(toggleId => {
5152
+ let valueToSelect = 'hide';
5153
+ if (currentToggles.includes(toggleId)) {
5154
+ valueToSelect = 'show';
5155
+ }
5156
+ else if (currentPeekToggles.includes(toggleId)) {
5157
+ valueToSelect = 'peek';
5158
+ }
5159
+ const input = this.stateModal.querySelector(`input[name="cv-toggle-${toggleId}"][value="${valueToSelect}"]`);
5160
+ if (input) {
5161
+ input.checked = true;
4864
5162
  }
4865
5163
  });
4866
5164
  // Load tab group selections
@@ -4893,88 +5191,70 @@ ${TAB_STYLES}
4893
5191
  }
4894
5192
  }
4895
5193
  /**
4896
- * Check if this is the first visit and show welcome modal
5194
+ * Check if this is the first visit and show intro callout
4897
5195
  */
4898
- showWelcomeModalIfFirstVisit() {
5196
+ showIntroCalloutIfFirstVisit() {
4899
5197
  if (!this._hasVisibleConfig)
4900
5198
  return;
4901
- const STORAGE_KEY = 'cv-welcome-shown';
4902
- // Check if welcome has been shown before
4903
- const hasSeenWelcome = localStorage.getItem(STORAGE_KEY);
4904
- if (!hasSeenWelcome) {
4905
- // Show welcome modal after a short delay to let the page settle
5199
+ // Strict check: Only show callout if there is actual content on the page to customize.
5200
+ // We check the core registry for any active toggles or tab groups.
5201
+ if (!this.core.hasActiveComponents()) {
5202
+ return;
5203
+ }
5204
+ const STORAGE_KEY = 'cv-intro-shown';
5205
+ // Check if intro has been shown before
5206
+ let hasSeenIntro = null;
5207
+ try {
5208
+ hasSeenIntro = localStorage.getItem(STORAGE_KEY);
5209
+ }
5210
+ catch (e) {
5211
+ // Ignore localStorage errors (e.g. private mode)
5212
+ }
5213
+ if (!hasSeenIntro) {
5214
+ // Show callout after a short delay
4906
5215
  setTimeout(() => {
4907
- this.createWelcomeModal();
4908
- }, 500);
4909
- // Mark as shown
4910
- localStorage.setItem(STORAGE_KEY, 'true');
5216
+ this.createCallout();
5217
+ }, 1000);
4911
5218
  }
4912
5219
  }
4913
5220
  /**
4914
- * Create and show the welcome modal
5221
+ * Create and show the intro callout
4915
5222
  */
4916
- createWelcomeModal() {
4917
- // Don't show if there's already a modal open
4918
- if (this.stateModal && !this.stateModal.classList.contains('cv-hidden'))
5223
+ createCallout() {
5224
+ // Avoid duplicates
5225
+ if (this.introCallout || document.querySelector('.cv-widget-callout'))
4919
5226
  return;
4920
- const welcomeModal = document.createElement('div');
4921
- welcomeModal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
5227
+ this.introCallout = document.createElement('div');
5228
+ const callout = this.introCallout;
5229
+ callout.className = `cv-widget-callout cv-pos-${this.options.position}`;
4922
5230
  if (this.options.theme === 'dark') {
4923
- welcomeModal.classList.add('cv-widget-theme-dark');
5231
+ callout.classList.add('cv-widget-theme-dark');
5232
+ }
5233
+ // Close button
5234
+ const closeBtn = document.createElement('button');
5235
+ closeBtn.className = 'cv-widget-callout-close';
5236
+ closeBtn.innerHTML = '×';
5237
+ closeBtn.setAttribute('aria-label', 'Dismiss intro');
5238
+ closeBtn.addEventListener('click', (e) => {
5239
+ e.stopPropagation();
5240
+ this.dismissIntroCallout();
5241
+ });
5242
+ // Message
5243
+ const msg = document.createElement('p');
5244
+ msg.className = 'cv-widget-callout-text';
5245
+ msg.textContent = this.options.welcomeMessage;
5246
+ callout.appendChild(closeBtn);
5247
+ callout.appendChild(msg);
5248
+ document.body.appendChild(callout);
5249
+ // Add pulse to widget icon to draw attention
5250
+ if (this.widgetIcon) {
5251
+ this.widgetIcon.classList.add('cv-pulse');
4924
5252
  }
4925
- welcomeModal.innerHTML = `
4926
- <div class="cv-widget-modal cv-welcome-modal">
4927
- <header class="cv-modal-header">
4928
- <div class="cv-modal-header-content">
4929
- <div class="cv-modal-icon">
4930
- ${getGearIcon()}
4931
- </div>
4932
- <h1 class="cv-modal-title">${this.options.welcomeTitle}</h1>
4933
- </div>
4934
- </header>
4935
- <div class="cv-modal-main">
4936
- <p class="cv-welcome-message">${this.options.welcomeMessage}</p>
4937
-
4938
- <div class="cv-welcome-widget-preview">
4939
- <div class="cv-welcome-widget-icon">
4940
- ${getGearIcon()}
4941
- </div>
4942
- <p class="cv-welcome-widget-label">Look for this widget</p>
4943
- </div>
4944
-
4945
- <button class="cv-welcome-got-it">Got it!</button>
4946
- </div>
4947
- </div>
4948
- `;
4949
- document.body.appendChild(welcomeModal);
4950
- this.attachWelcomeModalEventListeners(welcomeModal);
4951
- }
4952
- /**
4953
- * Attach event listeners for welcome modal
4954
- */
4955
- attachWelcomeModalEventListeners(welcomeModal) {
4956
- const closeModal = () => {
4957
- welcomeModal.remove();
4958
- document.removeEventListener('keydown', handleEscape);
4959
- };
4960
- // Got it button
4961
- const gotItBtn = welcomeModal.querySelector('.cv-welcome-got-it');
4962
- if (gotItBtn) {
4963
- gotItBtn.addEventListener('click', closeModal);
4964
- }
4965
- // Overlay click to close
4966
- welcomeModal.addEventListener('click', (e) => {
4967
- if (e.target === welcomeModal) {
4968
- closeModal();
4969
- }
5253
+ // Auto-dismiss and open widget on click anywhere on callout
5254
+ callout.addEventListener('click', () => {
5255
+ this.dismissIntroCallout();
5256
+ this.openStateModal();
4970
5257
  });
4971
- // Escape key to close
4972
- const handleEscape = (e) => {
4973
- if (e.key === 'Escape') {
4974
- closeModal();
4975
- }
4976
- };
4977
- document.addEventListener('keydown', handleEscape);
4978
5258
  }
4979
5259
  }
4980
5260