@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.
- package/dist/custom-views.core.cjs.js +569 -289
- package/dist/custom-views.core.cjs.js.map +1 -1
- package/dist/custom-views.core.esm.js +569 -289
- package/dist/custom-views.core.esm.js.map +1 -1
- package/dist/custom-views.esm.js +569 -289
- package/dist/custom-views.esm.js.map +1 -1
- package/dist/custom-views.js +569 -289
- package/dist/custom-views.js.map +1 -1
- package/dist/custom-views.min.js +2 -2
- package/dist/custom-views.min.js.map +1 -1
- package/dist/types/core/core.d.ts +10 -4
- package/dist/types/core/core.d.ts.map +1 -1
- package/dist/types/core/toggle-manager.d.ts +12 -2
- package/dist/types/core/toggle-manager.d.ts.map +1 -1
- package/dist/types/core/url-state-manager.d.ts.map +1 -1
- package/dist/types/core/widget.d.ts +11 -12
- package/dist/types/core/widget.d.ts.map +1 -1
- package/dist/types/styles/toggle-styles.d.ts +1 -1
- package/dist/types/styles/toggle-styles.d.ts.map +1 -1
- package/dist/types/styles/widget-styles.d.ts +1 -1
- package/dist/types/styles/widget-styles.d.ts.map +1 -1
- package/dist/types/types/types.d.ts +4 -4
- package/dist/types/types/types.d.ts.map +1 -1
- package/dist/types/utils/icons.d.ts +2 -0
- package/dist/types/utils/icons.d.ts.map +1 -1
- package/package.json +10 -4
package/dist/custom-views.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @customviews-js/customviews v1.4.
|
|
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
|
*/
|
|
@@ -163,8 +163,12 @@ class URLStateManager {
|
|
|
163
163
|
// Create a compact representation
|
|
164
164
|
const compact = {};
|
|
165
165
|
// Add toggles if present and non-empty
|
|
166
|
-
if (state.
|
|
167
|
-
compact.t = state.
|
|
166
|
+
if (state.shownToggles && state.shownToggles.length > 0) {
|
|
167
|
+
compact.t = state.shownToggles;
|
|
168
|
+
}
|
|
169
|
+
// Add peek toggles if present and non-empty
|
|
170
|
+
if (state.peekToggles && state.peekToggles.length > 0) {
|
|
171
|
+
compact.p = state.peekToggles;
|
|
168
172
|
}
|
|
169
173
|
// Add tab groups if present
|
|
170
174
|
if (state.tabs && Object.keys(state.tabs).length > 0) {
|
|
@@ -225,7 +229,8 @@ class URLStateManager {
|
|
|
225
229
|
// Reconstruct State from compact format
|
|
226
230
|
// Reconstruct Toggles
|
|
227
231
|
const state = {
|
|
228
|
-
|
|
232
|
+
shownToggles: Array.isArray(compact.t) ? compact.t : [],
|
|
233
|
+
peekToggles: Array.isArray(compact.p) ? compact.p : []
|
|
229
234
|
};
|
|
230
235
|
// Reconstruct Tabs
|
|
231
236
|
if (Array.isArray(compact.g)) {
|
|
@@ -438,6 +443,12 @@ function getGitHubIcon() {
|
|
|
438
443
|
<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
444
|
</svg>`;
|
|
440
445
|
}
|
|
446
|
+
function getChevronDownIcon() {
|
|
447
|
+
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>`;
|
|
448
|
+
}
|
|
449
|
+
function getChevronUpIcon() {
|
|
450
|
+
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>`;
|
|
451
|
+
}
|
|
441
452
|
|
|
442
453
|
// Constants for selectors
|
|
443
454
|
const TABGROUP_SELECTOR$1 = 'cv-tabgroup';
|
|
@@ -693,7 +704,7 @@ class TabManager {
|
|
|
693
704
|
});
|
|
694
705
|
}
|
|
695
706
|
// Add tooltip for UX feedback (use native title attribute)
|
|
696
|
-
navLink.setAttribute('title',
|
|
707
|
+
navLink.setAttribute('title', "Double-click a tab to 'pin' it in all similar tab groups.");
|
|
697
708
|
listItem.appendChild(navLink);
|
|
698
709
|
navContainer.appendChild(listItem);
|
|
699
710
|
});
|
|
@@ -1008,20 +1019,33 @@ function renderAssetInto(el, assetId, assetsManager) {
|
|
|
1008
1019
|
* ToggleManager handles discovery, visibility, and asset rendering for toggle elements
|
|
1009
1020
|
*/
|
|
1010
1021
|
class ToggleManager {
|
|
1022
|
+
/**
|
|
1023
|
+
* Track locally expanded elements (that were in peek mode but user expanded them)
|
|
1024
|
+
*/
|
|
1025
|
+
static expandedPeekElements = new WeakSet();
|
|
1011
1026
|
/**
|
|
1012
1027
|
* Apply toggle visibility to a given list of toggle elements
|
|
1013
1028
|
*/
|
|
1014
|
-
static
|
|
1015
|
-
|
|
1029
|
+
static applyTogglesVisibility(allToggleElements, activeToggles, peekToggles = []) {
|
|
1030
|
+
allToggleElements.forEach(el => {
|
|
1016
1031
|
const categories = this.getToggleCategories(el);
|
|
1017
1032
|
const shouldShow = categories.some(cat => activeToggles.includes(cat));
|
|
1018
|
-
|
|
1033
|
+
const shouldPeek = !shouldShow && categories.some(cat => peekToggles.includes(cat));
|
|
1034
|
+
if (!shouldPeek) {
|
|
1035
|
+
this.expandedPeekElements.delete(el);
|
|
1036
|
+
}
|
|
1037
|
+
// If locally expanded, treat as shown (override peek)
|
|
1038
|
+
// Note: If neither show nor peek is active (i.e. hidden), local expansion is ignored/cleared effectively
|
|
1039
|
+
this.applyToggleVisibility(el, shouldShow || (shouldPeek && this.expandedPeekElements.has(el)), shouldPeek && !this.expandedPeekElements.has(el));
|
|
1019
1040
|
});
|
|
1020
1041
|
}
|
|
1021
1042
|
/**
|
|
1022
1043
|
* Render assets into a given list of toggle elements that are currently visible
|
|
1044
|
+
* Toggles that have a toggleId and are currently visible will have their assets rendered (if any)
|
|
1023
1045
|
*/
|
|
1024
|
-
static
|
|
1046
|
+
static renderToggleAssets(elements, activeToggles, assetsManager) {
|
|
1047
|
+
// TO DO: (gerteck) Enable for peek toggles as well
|
|
1048
|
+
// Also, rework the rendering logic again to make it more user friendly.
|
|
1025
1049
|
elements.forEach(el => {
|
|
1026
1050
|
const categories = this.getToggleCategories(el);
|
|
1027
1051
|
const toggleId = this.getToggleId(el);
|
|
@@ -1034,6 +1058,7 @@ class ToggleManager {
|
|
|
1034
1058
|
}
|
|
1035
1059
|
/**
|
|
1036
1060
|
* Get toggle categories from an element (supports both data attributes and cv-toggle elements)
|
|
1061
|
+
* Note: a toggle can have multiple categories.
|
|
1037
1062
|
*/
|
|
1038
1063
|
static getToggleCategories(el) {
|
|
1039
1064
|
if (el.tagName.toLowerCase() === 'cv-toggle') {
|
|
@@ -1054,14 +1079,101 @@ class ToggleManager {
|
|
|
1054
1079
|
/**
|
|
1055
1080
|
* Apply simple class-based visibility to a toggle element
|
|
1056
1081
|
*/
|
|
1057
|
-
static applyToggleVisibility(
|
|
1082
|
+
static applyToggleVisibility(toggleElement, visible, peek = false) {
|
|
1083
|
+
const isLocallyExpanded = this.expandedPeekElements.has(toggleElement);
|
|
1058
1084
|
if (visible) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1085
|
+
toggleElement.classList.remove('cv-hidden', 'cv-peek');
|
|
1086
|
+
toggleElement.classList.add('cv-visible');
|
|
1087
|
+
// Show collapse button ONLY if locally expanded (meaning we are actually in peek mode but expanded).
|
|
1088
|
+
// If globally visible (because of 'Show' state), isLocallyExpanded should have been cleared by applyTogglesVisibility,
|
|
1089
|
+
// so this will be false, and button will be removed.
|
|
1090
|
+
this.manageExpandButton(toggleElement, false, isLocallyExpanded);
|
|
1091
|
+
}
|
|
1092
|
+
else if (peek) {
|
|
1093
|
+
toggleElement.classList.remove('cv-hidden', 'cv-visible');
|
|
1094
|
+
toggleElement.classList.add('cv-peek');
|
|
1095
|
+
// Show/create expand button if peeked
|
|
1096
|
+
this.manageExpandButton(toggleElement, true, false);
|
|
1061
1097
|
}
|
|
1062
1098
|
else {
|
|
1063
|
-
|
|
1064
|
-
|
|
1099
|
+
toggleElement.classList.add('cv-hidden');
|
|
1100
|
+
toggleElement.classList.remove('cv-visible', 'cv-peek');
|
|
1101
|
+
// Ensure button is gone/hidden
|
|
1102
|
+
this.manageExpandButton(toggleElement, false, false);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Manage the presence of the inline Expand/Collapse button using a wrapper approach
|
|
1107
|
+
*/
|
|
1108
|
+
static manageExpandButton(toggleElement, showExpand, showCollapse = false) {
|
|
1109
|
+
// 1. Ensure wrapper exists
|
|
1110
|
+
let wrapper = toggleElement.parentElement;
|
|
1111
|
+
if (!wrapper || !wrapper.classList.contains('cv-wrapper')) {
|
|
1112
|
+
wrapper = document.createElement('div');
|
|
1113
|
+
wrapper.className = 'cv-wrapper';
|
|
1114
|
+
toggleElement.parentNode?.insertBefore(wrapper, toggleElement);
|
|
1115
|
+
wrapper.appendChild(toggleElement);
|
|
1116
|
+
}
|
|
1117
|
+
const btn = wrapper.querySelector('.cv-expand-btn');
|
|
1118
|
+
// 2. Handle "No Button" case (neither expand nor collapse)
|
|
1119
|
+
if (!showExpand && !showCollapse) {
|
|
1120
|
+
if (btn)
|
|
1121
|
+
btn.style.display = 'none';
|
|
1122
|
+
// If content is visible globally (not hidden), ensure wrapper has 'cv-expanded'
|
|
1123
|
+
// to hide the peek fade effect (since fade is for peek state only).
|
|
1124
|
+
if (!toggleElement.classList.contains('cv-hidden')) {
|
|
1125
|
+
wrapper.classList.add('cv-expanded');
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
wrapper.classList.remove('cv-expanded');
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
// 3. Handle Button Needed (Expand or Collapse)
|
|
1133
|
+
const action = showExpand ? 'expand' : 'collapse';
|
|
1134
|
+
// Update Wrapper Class Logic
|
|
1135
|
+
// If showExpand (Peek state) -> remove cv-expanded (show fade)
|
|
1136
|
+
// If showCollapse (Expanded peek) -> add cv-expanded (hide fade)
|
|
1137
|
+
if (showExpand) {
|
|
1138
|
+
wrapper.classList.remove('cv-expanded');
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
if (!wrapper.classList.contains('cv-expanded'))
|
|
1142
|
+
wrapper.classList.add('cv-expanded');
|
|
1143
|
+
}
|
|
1144
|
+
// Check if existing button matches desired state
|
|
1145
|
+
const currentAction = btn?.getAttribute('data-action');
|
|
1146
|
+
if (btn && currentAction === action) {
|
|
1147
|
+
btn.style.display = 'flex';
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
// 4. Create New Button (if missing or state changed)
|
|
1151
|
+
const iconSvg = showExpand ? getChevronDownIcon() : getChevronUpIcon();
|
|
1152
|
+
const newBtn = document.createElement('button');
|
|
1153
|
+
newBtn.className = 'cv-expand-btn';
|
|
1154
|
+
newBtn.innerHTML = iconSvg;
|
|
1155
|
+
newBtn.setAttribute('aria-label', showExpand ? 'Expand content' : 'Collapse content');
|
|
1156
|
+
newBtn.setAttribute('data-action', action); // Track state
|
|
1157
|
+
newBtn.style.display = 'flex';
|
|
1158
|
+
newBtn.addEventListener('click', (e) => {
|
|
1159
|
+
e.stopPropagation();
|
|
1160
|
+
// Logic: Toggle expansion state
|
|
1161
|
+
if (showExpand) {
|
|
1162
|
+
wrapper.classList.add('cv-expanded');
|
|
1163
|
+
this.expandedPeekElements.add(toggleElement);
|
|
1164
|
+
this.applyToggleVisibility(toggleElement, true, false);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
wrapper.classList.remove('cv-expanded');
|
|
1168
|
+
this.expandedPeekElements.delete(toggleElement);
|
|
1169
|
+
this.applyToggleVisibility(toggleElement, false, true);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
if (btn) {
|
|
1173
|
+
btn.replaceWith(newBtn);
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
wrapper.appendChild(newBtn);
|
|
1065
1177
|
}
|
|
1066
1178
|
}
|
|
1067
1179
|
/**
|
|
@@ -1069,15 +1181,15 @@ class ToggleManager {
|
|
|
1069
1181
|
* This includes applying visibility and rendering assets.
|
|
1070
1182
|
*/
|
|
1071
1183
|
static initializeToggles(root, activeToggles, assetsManager) {
|
|
1072
|
-
const
|
|
1184
|
+
const allToggleElements = [];
|
|
1073
1185
|
if (root.matches('[data-cv-toggle], [data-customviews-toggle], cv-toggle')) {
|
|
1074
|
-
|
|
1186
|
+
allToggleElements.push(root);
|
|
1075
1187
|
}
|
|
1076
|
-
root.querySelectorAll('[data-cv-toggle], [data-customviews-toggle], cv-toggle').forEach(el =>
|
|
1077
|
-
if (
|
|
1188
|
+
root.querySelectorAll('[data-cv-toggle], [data-customviews-toggle], cv-toggle').forEach(el => allToggleElements.push(el));
|
|
1189
|
+
if (allToggleElements.length === 0)
|
|
1078
1190
|
return;
|
|
1079
|
-
this.
|
|
1080
|
-
this.
|
|
1191
|
+
this.applyTogglesVisibility(allToggleElements, activeToggles);
|
|
1192
|
+
this.renderToggleAssets(allToggleElements, activeToggles, assetsManager);
|
|
1081
1193
|
}
|
|
1082
1194
|
}
|
|
1083
1195
|
|
|
@@ -1185,17 +1297,16 @@ class ScrollManager {
|
|
|
1185
1297
|
const TOGGLE_STYLES = `
|
|
1186
1298
|
/* Core toggle visibility transitions */
|
|
1187
1299
|
[data-cv-toggle], [data-customviews-toggle], cv-toggle {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
margin 150ms ease;
|
|
1192
|
-
will-change: opacity, transform, max-height, margin;
|
|
1300
|
+
display: block;
|
|
1301
|
+
overflow: hidden;
|
|
1302
|
+
/* Removed transitions for instant toggling */
|
|
1193
1303
|
}
|
|
1194
1304
|
|
|
1305
|
+
/* Open State */
|
|
1195
1306
|
.cv-visible {
|
|
1196
1307
|
opacity: 1 !important;
|
|
1197
1308
|
transform: translateY(0) !important;
|
|
1198
|
-
max-height:
|
|
1309
|
+
max-height: none !important;
|
|
1199
1310
|
}
|
|
1200
1311
|
|
|
1201
1312
|
.cv-hidden {
|
|
@@ -1211,6 +1322,61 @@ const TOGGLE_STYLES = `
|
|
|
1211
1322
|
margin-bottom: 0 !important;
|
|
1212
1323
|
overflow: hidden !important;
|
|
1213
1324
|
}
|
|
1325
|
+
|
|
1326
|
+
/* Close/Peek State */
|
|
1327
|
+
.cv-peek {
|
|
1328
|
+
display: block !important;
|
|
1329
|
+
max-height: 70px !important;
|
|
1330
|
+
overflow: hidden !important;
|
|
1331
|
+
opacity: 1 !important;
|
|
1332
|
+
transform: translateY(0) !important;
|
|
1333
|
+
mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
|
|
1334
|
+
-webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
.cv-wrapper {
|
|
1338
|
+
position: relative;
|
|
1339
|
+
width: 100%;
|
|
1340
|
+
display: block;
|
|
1341
|
+
margin-bottom: 24px; /* Space for the button */
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.cv-expand-btn {
|
|
1345
|
+
position: absolute;
|
|
1346
|
+
bottom: -28px; /* Mostly outside, slight overlap */
|
|
1347
|
+
left: 50%;
|
|
1348
|
+
transform: translateX(-50%);
|
|
1349
|
+
display: flex;
|
|
1350
|
+
background: transparent;
|
|
1351
|
+
border: none;
|
|
1352
|
+
border-radius: 50%;
|
|
1353
|
+
padding: 4px;
|
|
1354
|
+
width: 32px;
|
|
1355
|
+
height: 32px;
|
|
1356
|
+
cursor: pointer;
|
|
1357
|
+
z-index: 100;
|
|
1358
|
+
align-items: center;
|
|
1359
|
+
justify-content: center;
|
|
1360
|
+
color: #888;
|
|
1361
|
+
transition: all 0.2s ease;
|
|
1362
|
+
box-shadow: none;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.cv-expand-btn:hover {
|
|
1366
|
+
background: rgba(0, 0, 0, 0.05);
|
|
1367
|
+
color: #000;
|
|
1368
|
+
transform: translateX(-50%) scale(1.1);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.cv-expand-btn svg {
|
|
1372
|
+
display: block;
|
|
1373
|
+
opacity: 0.6;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.cv-expand-btn:hover svg {
|
|
1377
|
+
opacity: 1;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1214
1380
|
`;
|
|
1215
1381
|
|
|
1216
1382
|
/**
|
|
@@ -2566,6 +2732,12 @@ class CustomViewsCore {
|
|
|
2566
2732
|
this.componentRegistry.tabGroups.delete(tabGroup);
|
|
2567
2733
|
});
|
|
2568
2734
|
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Check if there are any active components in the registry
|
|
2737
|
+
*/
|
|
2738
|
+
hasActiveComponents() {
|
|
2739
|
+
return this.componentRegistry.toggles.size > 0 || this.componentRegistry.tabGroups.size > 0;
|
|
2740
|
+
}
|
|
2569
2741
|
getConfig() {
|
|
2570
2742
|
return this.config;
|
|
2571
2743
|
}
|
|
@@ -2600,7 +2772,7 @@ class CustomViewsCore {
|
|
|
2600
2772
|
});
|
|
2601
2773
|
}
|
|
2602
2774
|
const computedState = {
|
|
2603
|
-
|
|
2775
|
+
shownToggles: this.config.toggles?.map(t => t.id) || [],
|
|
2604
2776
|
tabs
|
|
2605
2777
|
};
|
|
2606
2778
|
return computedState;
|
|
@@ -2672,9 +2844,9 @@ class CustomViewsCore {
|
|
|
2672
2844
|
const initialTop = anchorElement.getBoundingClientRect().top;
|
|
2673
2845
|
const currentTabs = this.getCurrentActiveTabs();
|
|
2674
2846
|
currentTabs[groupId] = tabId;
|
|
2675
|
-
const
|
|
2847
|
+
const currentState = this.getCurrentState();
|
|
2676
2848
|
const newState = {
|
|
2677
|
-
|
|
2849
|
+
...currentState,
|
|
2678
2850
|
tabs: currentTabs,
|
|
2679
2851
|
};
|
|
2680
2852
|
// 2. Apply state with scroll anchor information
|
|
@@ -2730,7 +2902,8 @@ class CustomViewsCore {
|
|
|
2730
2902
|
// 1. URL State
|
|
2731
2903
|
const urlState = URLStateManager.parseURL();
|
|
2732
2904
|
if (urlState) {
|
|
2733
|
-
|
|
2905
|
+
// Apply URL state temporarily (do not persist until interaction)
|
|
2906
|
+
this.applyState(urlState, { persist: false });
|
|
2734
2907
|
return;
|
|
2735
2908
|
}
|
|
2736
2909
|
// 2. Persisted State
|
|
@@ -2744,9 +2917,10 @@ class CustomViewsCore {
|
|
|
2744
2917
|
}
|
|
2745
2918
|
/**
|
|
2746
2919
|
* Apply a custom state, saves to localStorage and updates the URL
|
|
2747
|
-
*
|
|
2920
|
+
* 'source' in options indicates the origin of the state change
|
|
2748
2921
|
* (e.g., 'widget' to trigger scroll behavior)
|
|
2749
|
-
*
|
|
2922
|
+
* 'scrollAnchor' in options indicates the element to maintain scroll position of
|
|
2923
|
+
* 'persist' (default true) to control whether to save to localStorage
|
|
2750
2924
|
*/
|
|
2751
2925
|
applyState(state, options) {
|
|
2752
2926
|
// console.log(`[Core] applyState called with source: ${options?.source}`, state);
|
|
@@ -2756,7 +2930,10 @@ class CustomViewsCore {
|
|
|
2756
2930
|
}
|
|
2757
2931
|
const snapshot = this.cloneState(state);
|
|
2758
2932
|
this.renderState(snapshot);
|
|
2759
|
-
|
|
2933
|
+
// Only persist if explicitly requested (default true)
|
|
2934
|
+
if (options?.persist !== false) {
|
|
2935
|
+
this.persistenceManager.persistState(snapshot);
|
|
2936
|
+
}
|
|
2760
2937
|
if (this.showUrlEnabled) {
|
|
2761
2938
|
URLStateManager.updateURL(snapshot);
|
|
2762
2939
|
}
|
|
@@ -2783,14 +2960,13 @@ class CustomViewsCore {
|
|
|
2783
2960
|
renderState(state) {
|
|
2784
2961
|
this.observer?.disconnect();
|
|
2785
2962
|
this.lastAppliedState = this.cloneState(state);
|
|
2786
|
-
const toggles = state?.
|
|
2963
|
+
const toggles = state?.shownToggles || [];
|
|
2787
2964
|
const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
|
|
2788
|
-
const
|
|
2965
|
+
const allToggleElements = Array.from(this.componentRegistry.toggles);
|
|
2789
2966
|
const tabGroupElements = Array.from(this.componentRegistry.tabGroups);
|
|
2790
|
-
|
|
2791
|
-
ToggleManager.applyToggles(toggleElements, finalToggles);
|
|
2967
|
+
ToggleManager.applyTogglesVisibility(allToggleElements, finalToggles, state.peekToggles);
|
|
2792
2968
|
// Render assets into toggles
|
|
2793
|
-
ToggleManager.
|
|
2969
|
+
ToggleManager.renderToggleAssets(allToggleElements, finalToggles, this.assetsManager);
|
|
2794
2970
|
// Apply tab selections
|
|
2795
2971
|
TabManager.applyTabSelections(tabGroupElements, state.tabs || {}, this.config.tabGroups);
|
|
2796
2972
|
// Update nav active states (without rebuilding)
|
|
@@ -2821,16 +2997,16 @@ class CustomViewsCore {
|
|
|
2821
2997
|
URLStateManager.clearURL();
|
|
2822
2998
|
}
|
|
2823
2999
|
/**
|
|
2824
|
-
* Get the
|
|
3000
|
+
* Get the full current state including active toggles, peek toggles, and tabs
|
|
2825
3001
|
*/
|
|
2826
|
-
|
|
3002
|
+
getCurrentState() {
|
|
2827
3003
|
if (this.lastAppliedState) {
|
|
2828
|
-
return this.lastAppliedState
|
|
3004
|
+
return this.cloneState(this.lastAppliedState);
|
|
2829
3005
|
}
|
|
2830
3006
|
if (this.config) {
|
|
2831
|
-
return this.getComputedDefaultState()
|
|
3007
|
+
return this.cloneState(this.getComputedDefaultState());
|
|
2832
3008
|
}
|
|
2833
|
-
return
|
|
3009
|
+
return {};
|
|
2834
3010
|
}
|
|
2835
3011
|
/**
|
|
2836
3012
|
* Clear all persistence and reset to default
|
|
@@ -3526,10 +3702,6 @@ const WIDGET_STYLES = `
|
|
|
3526
3702
|
color: rgba(255, 255, 255, 0.6);
|
|
3527
3703
|
}
|
|
3528
3704
|
|
|
3529
|
-
.cv-widget-theme-dark .cv-toggle-slider {
|
|
3530
|
-
background: rgba(255, 255, 255, 0.2);
|
|
3531
|
-
}
|
|
3532
|
-
|
|
3533
3705
|
.cv-widget-theme-dark .cv-tab-group-description {
|
|
3534
3706
|
color: rgba(255, 255, 255, 0.8);
|
|
3535
3707
|
}
|
|
@@ -3650,40 +3822,32 @@ const WIDGET_STYLES = `
|
|
|
3650
3822
|
}
|
|
3651
3823
|
|
|
3652
3824
|
.cv-toggle-input {
|
|
3825
|
+
/* Only hide if it is part of a custom slider toggle */
|
|
3826
|
+
}
|
|
3827
|
+
.cv-toggle-label .cv-toggle-input {
|
|
3653
3828
|
opacity: 0;
|
|
3654
3829
|
width: 0;
|
|
3655
3830
|
height: 0;
|
|
3656
3831
|
}
|
|
3657
3832
|
|
|
3658
|
-
.cv-toggle-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
left: 0;
|
|
3662
|
-
right: 0;
|
|
3663
|
-
bottom: 0;
|
|
3664
|
-
background: rgba(0, 0, 0, 0.2);
|
|
3665
|
-
border-radius: 9999px;
|
|
3666
|
-
transition: background-color 0.2s ease;
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
.cv-toggle-slider:before {
|
|
3670
|
-
position: absolute;
|
|
3671
|
-
content: "";
|
|
3672
|
-
height: 1rem;
|
|
3673
|
-
width: 1rem;
|
|
3674
|
-
left: 0.25rem;
|
|
3675
|
-
bottom: 0.25rem;
|
|
3676
|
-
background: white;
|
|
3677
|
-
border-radius: 50%;
|
|
3678
|
-
transition: transform 0.2s ease;
|
|
3833
|
+
.cv-toggle-radios {
|
|
3834
|
+
display: flex;
|
|
3835
|
+
gap: 8px;
|
|
3679
3836
|
}
|
|
3680
3837
|
|
|
3681
|
-
.cv-
|
|
3682
|
-
|
|
3838
|
+
.cv-radio-label {
|
|
3839
|
+
display: flex;
|
|
3840
|
+
align-items: center;
|
|
3841
|
+
gap: 4px;
|
|
3842
|
+
font-size: 0.85rem;
|
|
3843
|
+
cursor: pointer;
|
|
3683
3844
|
}
|
|
3684
3845
|
|
|
3685
|
-
.cv-
|
|
3686
|
-
|
|
3846
|
+
.cv-radio-label input {
|
|
3847
|
+
margin: 0;
|
|
3848
|
+
opacity: 1;
|
|
3849
|
+
width: auto;
|
|
3850
|
+
height: auto;
|
|
3687
3851
|
}
|
|
3688
3852
|
|
|
3689
3853
|
/* Dark theme toggle switch styles */
|
|
@@ -4032,97 +4196,7 @@ const WIDGET_STYLES = `
|
|
|
4032
4196
|
}
|
|
4033
4197
|
|
|
4034
4198
|
/* Dark theme custom state styles */
|
|
4035
|
-
/* Welcome modal styles */
|
|
4036
|
-
.cv-welcome-modal {
|
|
4037
|
-
max-width: 32rem;
|
|
4038
|
-
width: 90vw;
|
|
4039
|
-
background: white;
|
|
4040
|
-
border-radius: 0.75rem;
|
|
4041
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
4042
|
-
animation: slideIn 0.2s ease;
|
|
4043
|
-
display: flex;
|
|
4044
|
-
flex-direction: column;
|
|
4045
|
-
}
|
|
4046
|
-
|
|
4047
|
-
.cv-modal-main {
|
|
4048
|
-
padding: 1rem;
|
|
4049
|
-
flex: 1;
|
|
4050
|
-
display: flex;
|
|
4051
|
-
flex-direction: column;
|
|
4052
|
-
gap: 1rem;
|
|
4053
|
-
overflow-y: auto;
|
|
4054
|
-
max-height: calc(80vh - 8rem);
|
|
4055
|
-
}
|
|
4056
|
-
|
|
4057
|
-
.cv-welcome-message {
|
|
4058
|
-
font-size: 0.875rem;
|
|
4059
|
-
color: rgba(0, 0, 0, 0.8);
|
|
4060
|
-
margin: 0;
|
|
4061
|
-
line-height: 1.4;
|
|
4062
|
-
text-align: center;
|
|
4063
|
-
}
|
|
4064
|
-
|
|
4065
|
-
.cv-welcome-message a {
|
|
4066
|
-
color: #3e84f4;
|
|
4067
|
-
text-align: justify;
|
|
4068
|
-
text-decoration: none;
|
|
4069
|
-
}
|
|
4070
|
-
|
|
4071
|
-
.cv-welcome-message a:hover {
|
|
4072
|
-
text-decoration: underline;
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
.cv-welcome-widget-preview {
|
|
4076
|
-
display: flex;
|
|
4077
|
-
align-items: center;
|
|
4078
|
-
justify-content: center;
|
|
4079
|
-
gap: 1rem;
|
|
4080
|
-
padding: 1rem;
|
|
4081
|
-
background: #f8f9fa;
|
|
4082
|
-
border-radius: 0.5rem;
|
|
4083
|
-
margin: 1rem 0;
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
.cv-welcome-widget-icon {
|
|
4087
|
-
width: 2rem;
|
|
4088
|
-
height: 2rem;
|
|
4089
|
-
background: rgba(62, 132, 244, 0.1);
|
|
4090
|
-
border-radius: 9999px;
|
|
4091
|
-
display: flex;
|
|
4092
|
-
align-items: center;
|
|
4093
|
-
justify-content: center;
|
|
4094
|
-
animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
4095
|
-
color: #3e84f4;
|
|
4096
|
-
}
|
|
4097
|
-
|
|
4098
|
-
.cv-welcome-widget-label {
|
|
4099
|
-
font-size: 0.875rem;
|
|
4100
|
-
font-weight: 500;
|
|
4101
|
-
color: rgba(0, 0, 0, 0.8);
|
|
4102
|
-
margin: 0;
|
|
4103
|
-
}
|
|
4104
|
-
|
|
4105
|
-
.cv-welcome-got-it {
|
|
4106
|
-
width: 100%;
|
|
4107
|
-
background: #3e84f4;
|
|
4108
|
-
color: white;
|
|
4109
|
-
font-weight: 600;
|
|
4110
|
-
padding: 0.75rem 1rem;
|
|
4111
|
-
border-radius: 0.5rem;
|
|
4112
|
-
border: none;
|
|
4113
|
-
cursor: pointer;
|
|
4114
|
-
font-size: 0.875rem;
|
|
4115
|
-
transition: background-color 0.2s ease;
|
|
4116
|
-
outline: none;
|
|
4117
|
-
}
|
|
4118
4199
|
|
|
4119
|
-
.cv-welcome-got-it:hover {
|
|
4120
|
-
background: rgba(62, 132, 244, 0.9);
|
|
4121
|
-
}
|
|
4122
|
-
|
|
4123
|
-
.cv-welcome-got-it:focus {
|
|
4124
|
-
box-shadow: 0 0 0 2px rgba(62, 132, 244, 0.5);
|
|
4125
|
-
}
|
|
4126
4200
|
|
|
4127
4201
|
/* Animations */
|
|
4128
4202
|
@keyframes cv-pulse {
|
|
@@ -4134,26 +4208,7 @@ const WIDGET_STYLES = `
|
|
|
4134
4208
|
}
|
|
4135
4209
|
}
|
|
4136
4210
|
|
|
4137
|
-
/* Dark theme welcome modal styles */
|
|
4138
|
-
.cv-widget-theme-dark .cv-welcome-modal {
|
|
4139
|
-
background: #101722;
|
|
4140
|
-
}
|
|
4141
4211
|
|
|
4142
|
-
.cv-widget-theme-dark .cv-welcome-message {
|
|
4143
|
-
color: rgba(255, 255, 255, 0.8);
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
.cv-widget-theme-dark .cv-welcome-message a {
|
|
4147
|
-
color: #60a5fa;
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
.cv-widget-theme-dark .cv-welcome-widget-preview {
|
|
4151
|
-
background: rgba(255, 255, 255, 0.1);
|
|
4152
|
-
}
|
|
4153
|
-
|
|
4154
|
-
.cv-widget-theme-dark .cv-welcome-widget-label {
|
|
4155
|
-
color: #e2e8f0;
|
|
4156
|
-
}
|
|
4157
4212
|
|
|
4158
4213
|
/* Dark theme logo box */
|
|
4159
4214
|
.cv-widget-theme-dark .cv-tabgroup-logo-box {
|
|
@@ -4328,6 +4383,176 @@ const WIDGET_STYLES = `
|
|
|
4328
4383
|
.cv-widget-theme-dark .cv-share-action-btn.primary:hover {
|
|
4329
4384
|
background: #2b74e6;
|
|
4330
4385
|
}
|
|
4386
|
+
|
|
4387
|
+
/* Intro Callout styles */
|
|
4388
|
+
.cv-widget-callout {
|
|
4389
|
+
position: fixed;
|
|
4390
|
+
background: white;
|
|
4391
|
+
border-radius: 8px;
|
|
4392
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
4393
|
+
padding: 12px 16px;
|
|
4394
|
+
width: 260px;
|
|
4395
|
+
z-index: 9999;
|
|
4396
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
4397
|
+
animation: cvFadeIn 0.3s ease-out;
|
|
4398
|
+
pointer-events: auto;
|
|
4399
|
+
display: flex;
|
|
4400
|
+
flex-direction: column;
|
|
4401
|
+
gap: 8px;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
.cv-widget-callout-text {
|
|
4405
|
+
font-size: 0.9rem;
|
|
4406
|
+
color: #333;
|
|
4407
|
+
margin: 0;
|
|
4408
|
+
line-height: 1.4;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
.cv-widget-callout-close {
|
|
4412
|
+
position: absolute;
|
|
4413
|
+
top: 6px;
|
|
4414
|
+
right: 6px;
|
|
4415
|
+
width: 18px;
|
|
4416
|
+
height: 18px;
|
|
4417
|
+
border: none;
|
|
4418
|
+
background: rgba(0,0,0,0.05);
|
|
4419
|
+
color: #666;
|
|
4420
|
+
cursor: pointer;
|
|
4421
|
+
display: flex;
|
|
4422
|
+
align-items: center;
|
|
4423
|
+
justify-content: center;
|
|
4424
|
+
border-radius: 50%;
|
|
4425
|
+
font-size: 14px;
|
|
4426
|
+
line-height: 1;
|
|
4427
|
+
padding: 0;
|
|
4428
|
+
transition: all 0.2s ease;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
.cv-widget-callout-close:hover {
|
|
4432
|
+
background: #f0f0f0;
|
|
4433
|
+
color: #333;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
/* Callout positioning and arrow */
|
|
4437
|
+
.cv-widget-callout::after {
|
|
4438
|
+
content: '';
|
|
4439
|
+
position: absolute;
|
|
4440
|
+
width: 10px;
|
|
4441
|
+
height: 10px;
|
|
4442
|
+
background: white;
|
|
4443
|
+
transform: rotate(45deg);
|
|
4444
|
+
box-shadow: 1px 1px 1px rgba(0,0,0,0.05); /* subtle shadow for arrow */
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
/* Top-Right Widget -> Callout to the left */
|
|
4448
|
+
.cv-widget-callout.cv-pos-top-right {
|
|
4449
|
+
top: 20px;
|
|
4450
|
+
right: 64px;
|
|
4451
|
+
}
|
|
4452
|
+
.cv-widget-callout.cv-pos-top-right::after {
|
|
4453
|
+
top: 13px;
|
|
4454
|
+
right: -5px;
|
|
4455
|
+
box-shadow: 1px -1px 1px rgba(0,0,0,0.05);
|
|
4456
|
+
transform: rotate(45deg);
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
/* Bottom-Right Widget -> Callout to the left */
|
|
4460
|
+
.cv-widget-callout.cv-pos-bottom-right {
|
|
4461
|
+
bottom: 20px;
|
|
4462
|
+
right: 64px;
|
|
4463
|
+
}
|
|
4464
|
+
.cv-widget-callout.cv-pos-bottom-right::after {
|
|
4465
|
+
bottom: 13px;
|
|
4466
|
+
right: -5px;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
/* Top-Left Widget -> Callout to the right */
|
|
4470
|
+
.cv-widget-callout.cv-pos-top-left {
|
|
4471
|
+
top: 20px;
|
|
4472
|
+
left: 64px;
|
|
4473
|
+
}
|
|
4474
|
+
.cv-widget-callout.cv-pos-top-left::after {
|
|
4475
|
+
top: 13px;
|
|
4476
|
+
left: -5px;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
/* Bottom-Left Widget -> Callout to the right */
|
|
4480
|
+
.cv-widget-callout.cv-pos-bottom-left {
|
|
4481
|
+
bottom: 20px;
|
|
4482
|
+
left: 64px;
|
|
4483
|
+
}
|
|
4484
|
+
.cv-widget-callout.cv-pos-bottom-left::after {
|
|
4485
|
+
bottom: 13px;
|
|
4486
|
+
left: -5px;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
/* Middle-Right Widget -> Callout to the left */
|
|
4490
|
+
.cv-widget-callout.cv-pos-middle-right {
|
|
4491
|
+
top: 50%;
|
|
4492
|
+
right: 64px;
|
|
4493
|
+
transform: translateY(-50%);
|
|
4494
|
+
}
|
|
4495
|
+
.cv-widget-callout.cv-pos-middle-right::after {
|
|
4496
|
+
top: 50%;
|
|
4497
|
+
right: -5px;
|
|
4498
|
+
transform: translateY(-50%) rotate(45deg);
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
/* Middle-Left Widget -> Callout to the right */
|
|
4502
|
+
.cv-widget-callout.cv-pos-middle-left {
|
|
4503
|
+
top: 50%;
|
|
4504
|
+
left: 64px;
|
|
4505
|
+
transform: translateY(-50%);
|
|
4506
|
+
}
|
|
4507
|
+
.cv-widget-callout.cv-pos-middle-left::after {
|
|
4508
|
+
top: 50%;
|
|
4509
|
+
left: -5px;
|
|
4510
|
+
transform: translateY(-50%) rotate(45deg);
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
/* Pulse animation utility */
|
|
4514
|
+
.cv-widget-icon.cv-pulse {
|
|
4515
|
+
animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
4516
|
+
box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
@keyframes cv-pulse {
|
|
4520
|
+
0% {
|
|
4521
|
+
transform: scale(1);
|
|
4522
|
+
box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
|
|
4523
|
+
}
|
|
4524
|
+
70% {
|
|
4525
|
+
transform: scale(1.05);
|
|
4526
|
+
box-shadow: 0 0 0 10px rgba(62, 132, 244, 0);
|
|
4527
|
+
}
|
|
4528
|
+
100% {
|
|
4529
|
+
transform: scale(1);
|
|
4530
|
+
box-shadow: 0 0 0 0 rgba(62, 132, 244, 0);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
/* Dark Theme */
|
|
4535
|
+
.cv-widget-theme-dark .cv-widget-callout {
|
|
4536
|
+
background: #1f2937; /* Tailwind gray-800 mostly */
|
|
4537
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
4538
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
4539
|
+
}
|
|
4540
|
+
.cv-widget-theme-dark .cv-widget-callout::after {
|
|
4541
|
+
background: #1f2937;
|
|
4542
|
+
border-top: 1px solid rgba(255,255,255,0.1);
|
|
4543
|
+
border-right: 1px solid rgba(255,255,255,0.1);
|
|
4544
|
+
}
|
|
4545
|
+
.cv-widget-theme-dark .cv-widget-callout-text {
|
|
4546
|
+
color: #e5e7eb;
|
|
4547
|
+
}
|
|
4548
|
+
.cv-widget-theme-dark .cv-widget-callout-close {
|
|
4549
|
+
background: rgba(255,255,255,0.1);
|
|
4550
|
+
color: #9ca3af;
|
|
4551
|
+
}
|
|
4552
|
+
.cv-widget-theme-dark .cv-widget-callout-close:hover {
|
|
4553
|
+
background: rgba(255,255,255,0.2);
|
|
4554
|
+
color: #fff;
|
|
4555
|
+
}
|
|
4331
4556
|
`;
|
|
4332
4557
|
/**
|
|
4333
4558
|
* Inject widget styles into the document head
|
|
@@ -4346,6 +4571,7 @@ class CustomViewsWidget {
|
|
|
4346
4571
|
core;
|
|
4347
4572
|
container;
|
|
4348
4573
|
widgetIcon = null;
|
|
4574
|
+
introCallout = null;
|
|
4349
4575
|
options;
|
|
4350
4576
|
_hasVisibleConfig = false;
|
|
4351
4577
|
pageToggleIds = new Set();
|
|
@@ -4366,8 +4592,7 @@ class CustomViewsWidget {
|
|
|
4366
4592
|
title: options.title || 'Customize View',
|
|
4367
4593
|
description: options.description || '',
|
|
4368
4594
|
showWelcome: options.showWelcome ?? false,
|
|
4369
|
-
|
|
4370
|
-
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>.',
|
|
4595
|
+
welcomeMessage: options.welcomeMessage || 'Customize your reading experience (theme, toggles, tabs) here.',
|
|
4371
4596
|
showTabGroups: options.showTabGroups ?? true
|
|
4372
4597
|
};
|
|
4373
4598
|
// Determine if there are any configurations to show
|
|
@@ -4409,9 +4634,9 @@ class CustomViewsWidget {
|
|
|
4409
4634
|
this.attachEventListeners();
|
|
4410
4635
|
// Always append to body since it's a floating icon
|
|
4411
4636
|
document.body.appendChild(this.widgetIcon);
|
|
4412
|
-
// Show
|
|
4637
|
+
// Show intro callout on first visit if enabled
|
|
4413
4638
|
if (this.options.showWelcome) {
|
|
4414
|
-
this.
|
|
4639
|
+
this.showIntroCalloutIfFirstVisit();
|
|
4415
4640
|
}
|
|
4416
4641
|
return this.widgetIcon;
|
|
4417
4642
|
}
|
|
@@ -4441,6 +4666,11 @@ class CustomViewsWidget {
|
|
|
4441
4666
|
this.stateModal.remove();
|
|
4442
4667
|
this.stateModal = null;
|
|
4443
4668
|
}
|
|
4669
|
+
// Clean up callout
|
|
4670
|
+
if (this.introCallout) {
|
|
4671
|
+
this.introCallout.remove();
|
|
4672
|
+
this.introCallout = null;
|
|
4673
|
+
}
|
|
4444
4674
|
}
|
|
4445
4675
|
attachEventListeners() {
|
|
4446
4676
|
if (!this.widgetIcon)
|
|
@@ -4456,10 +4686,48 @@ class CustomViewsWidget {
|
|
|
4456
4686
|
this.stateModal.classList.add('cv-hidden');
|
|
4457
4687
|
}
|
|
4458
4688
|
}
|
|
4689
|
+
/**
|
|
4690
|
+
* Dismiss the intro callout
|
|
4691
|
+
*/
|
|
4692
|
+
dismissIntroCallout() {
|
|
4693
|
+
if (!this.introCallout)
|
|
4694
|
+
return;
|
|
4695
|
+
const callout = this.introCallout;
|
|
4696
|
+
// Clear reference immediately from class to prevent re-use
|
|
4697
|
+
this.introCallout = null;
|
|
4698
|
+
callout.remove();
|
|
4699
|
+
// Stop pulsing the widget icon
|
|
4700
|
+
if (this.widgetIcon) {
|
|
4701
|
+
this.widgetIcon.classList.remove('cv-pulse');
|
|
4702
|
+
}
|
|
4703
|
+
// Mark as shown in localStorage
|
|
4704
|
+
try {
|
|
4705
|
+
localStorage.setItem('cv-intro-shown', 'true');
|
|
4706
|
+
}
|
|
4707
|
+
catch (e) {
|
|
4708
|
+
// Ignore localStorage errors
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4459
4711
|
/**
|
|
4460
4712
|
* Open the custom state creator
|
|
4461
4713
|
*/
|
|
4462
4714
|
openStateModal() {
|
|
4715
|
+
// Dismiss intro callout if valid
|
|
4716
|
+
if (this.introCallout) {
|
|
4717
|
+
this.dismissIntroCallout();
|
|
4718
|
+
}
|
|
4719
|
+
else {
|
|
4720
|
+
// Even if no callout is shown (e.g. page had no content), opening the widget
|
|
4721
|
+
// should count as "seen", preventing future callouts.
|
|
4722
|
+
try {
|
|
4723
|
+
if (!localStorage.getItem('cv-intro-shown')) {
|
|
4724
|
+
localStorage.setItem('cv-intro-shown', 'true');
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
catch (e) {
|
|
4728
|
+
// Ignore localStorage errors
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4463
4731
|
if (!this.stateModal) {
|
|
4464
4732
|
this._createStateModal();
|
|
4465
4733
|
}
|
|
@@ -4500,10 +4768,20 @@ class CustomViewsWidget {
|
|
|
4500
4768
|
<div>
|
|
4501
4769
|
<p class="cv-toggle-title">${toggle.label || toggle.id}</p>
|
|
4502
4770
|
</div>
|
|
4503
|
-
<
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4771
|
+
<div class="cv-toggle-radios">
|
|
4772
|
+
<label class="cv-radio-label" title="Hide">
|
|
4773
|
+
<input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="hide" data-toggle="${toggle.id}"/>
|
|
4774
|
+
<span>Hide</span>
|
|
4775
|
+
</label>
|
|
4776
|
+
<label class="cv-radio-label" title="Peek">
|
|
4777
|
+
<input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="peek" data-toggle="${toggle.id}"/>
|
|
4778
|
+
<span>Peek</span>
|
|
4779
|
+
</label>
|
|
4780
|
+
<label class="cv-radio-label" title="Show">
|
|
4781
|
+
<input class="cv-toggle-input" type="radio" name="cv-toggle-${toggle.id}" value="show" data-toggle="${toggle.id}"/>
|
|
4782
|
+
<span>Show</span>
|
|
4783
|
+
</label>
|
|
4784
|
+
</div>
|
|
4507
4785
|
</div>
|
|
4508
4786
|
</div>
|
|
4509
4787
|
`).join('');
|
|
@@ -4704,10 +4982,11 @@ class CustomViewsWidget {
|
|
|
4704
4982
|
if (groupId && tabId) {
|
|
4705
4983
|
const currentTabs = this.core.getCurrentActiveTabs();
|
|
4706
4984
|
currentTabs[groupId] = tabId;
|
|
4707
|
-
const
|
|
4985
|
+
const currentState = this.core.getCurrentState();
|
|
4708
4986
|
const newState = {
|
|
4709
|
-
|
|
4710
|
-
|
|
4987
|
+
shownToggles: currentState.shownToggles || [],
|
|
4988
|
+
peekToggles: currentState.peekToggles || [], // Preserve peek state, fallback to empty array
|
|
4989
|
+
tabs: currentTabs,
|
|
4711
4990
|
};
|
|
4712
4991
|
this.core.applyState(newState, { source: 'widget' });
|
|
4713
4992
|
}
|
|
@@ -4805,25 +5084,33 @@ class CustomViewsWidget {
|
|
|
4805
5084
|
}
|
|
4806
5085
|
// Collect toggle values
|
|
4807
5086
|
const toggles = [];
|
|
4808
|
-
const
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
tabs[groupId] = select.value;
|
|
5087
|
+
const peekToggles = [];
|
|
5088
|
+
// Get all radio inputs
|
|
5089
|
+
const radios = this.stateModal.querySelectorAll('input[type="radio"]:checked');
|
|
5090
|
+
radios.forEach(radio => {
|
|
5091
|
+
const input = radio;
|
|
5092
|
+
const toggleId = input.getAttribute('data-toggle');
|
|
5093
|
+
if (toggleId) {
|
|
5094
|
+
if (input.value === 'show') {
|
|
5095
|
+
toggles.push(toggleId);
|
|
5096
|
+
}
|
|
5097
|
+
else if (input.value === 'peek') {
|
|
5098
|
+
peekToggles.push(toggleId);
|
|
5099
|
+
}
|
|
4822
5100
|
}
|
|
4823
5101
|
});
|
|
4824
|
-
const result = { toggles };
|
|
4825
|
-
|
|
4826
|
-
|
|
5102
|
+
const result = { shownToggles: toggles, peekToggles };
|
|
5103
|
+
// Get active tabs from selects
|
|
5104
|
+
const selects = this.stateModal.querySelectorAll('select[data-group-id]');
|
|
5105
|
+
if (selects.length > 0) {
|
|
5106
|
+
result.tabs = {};
|
|
5107
|
+
selects.forEach(select => {
|
|
5108
|
+
const el = select;
|
|
5109
|
+
const groupId = el.getAttribute('data-group-id');
|
|
5110
|
+
if (groupId) {
|
|
5111
|
+
result.tabs[groupId] = el.value;
|
|
5112
|
+
}
|
|
5113
|
+
});
|
|
4827
5114
|
}
|
|
4828
5115
|
return result;
|
|
4829
5116
|
}
|
|
@@ -4843,18 +5130,29 @@ class CustomViewsWidget {
|
|
|
4843
5130
|
loadCurrentStateIntoForm() {
|
|
4844
5131
|
if (!this.stateModal)
|
|
4845
5132
|
return;
|
|
4846
|
-
//
|
|
4847
|
-
const
|
|
4848
|
-
|
|
5133
|
+
// We need complete state for both shown and peek toggles
|
|
5134
|
+
const currentState = this.core.getCurrentState();
|
|
5135
|
+
const currentToggles = currentState.shownToggles || [];
|
|
5136
|
+
const currentPeekToggles = currentState.peekToggles || [];
|
|
5137
|
+
// Reset all inputs first (optional, but good for clarity)
|
|
4849
5138
|
const allToggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
|
|
4850
|
-
|
|
4851
|
-
|
|
5139
|
+
// Identify unique toggles present in the modal
|
|
5140
|
+
const uniqueToggles = new Set();
|
|
5141
|
+
allToggleInputs.forEach(input => {
|
|
5142
|
+
if (input.dataset.toggle)
|
|
5143
|
+
uniqueToggles.add(input.dataset.toggle);
|
|
4852
5144
|
});
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
5145
|
+
uniqueToggles.forEach(toggleId => {
|
|
5146
|
+
let valueToSelect = 'hide';
|
|
5147
|
+
if (currentToggles.includes(toggleId)) {
|
|
5148
|
+
valueToSelect = 'show';
|
|
5149
|
+
}
|
|
5150
|
+
else if (currentPeekToggles.includes(toggleId)) {
|
|
5151
|
+
valueToSelect = 'peek';
|
|
5152
|
+
}
|
|
5153
|
+
const input = this.stateModal.querySelector(`input[name="cv-toggle-${toggleId}"][value="${valueToSelect}"]`);
|
|
5154
|
+
if (input) {
|
|
5155
|
+
input.checked = true;
|
|
4858
5156
|
}
|
|
4859
5157
|
});
|
|
4860
5158
|
// Load tab group selections
|
|
@@ -4887,88 +5185,70 @@ class CustomViewsWidget {
|
|
|
4887
5185
|
}
|
|
4888
5186
|
}
|
|
4889
5187
|
/**
|
|
4890
|
-
* Check if this is the first visit and show
|
|
5188
|
+
* Check if this is the first visit and show intro callout
|
|
4891
5189
|
*/
|
|
4892
|
-
|
|
5190
|
+
showIntroCalloutIfFirstVisit() {
|
|
4893
5191
|
if (!this._hasVisibleConfig)
|
|
4894
5192
|
return;
|
|
4895
|
-
|
|
4896
|
-
//
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5193
|
+
// Strict check: Only show callout if there is actual content on the page to customize.
|
|
5194
|
+
// We check the core registry for any active toggles or tab groups.
|
|
5195
|
+
if (!this.core.hasActiveComponents()) {
|
|
5196
|
+
return;
|
|
5197
|
+
}
|
|
5198
|
+
const STORAGE_KEY = 'cv-intro-shown';
|
|
5199
|
+
// Check if intro has been shown before
|
|
5200
|
+
let hasSeenIntro = null;
|
|
5201
|
+
try {
|
|
5202
|
+
hasSeenIntro = localStorage.getItem(STORAGE_KEY);
|
|
5203
|
+
}
|
|
5204
|
+
catch (e) {
|
|
5205
|
+
// Ignore localStorage errors (e.g. private mode)
|
|
5206
|
+
}
|
|
5207
|
+
if (!hasSeenIntro) {
|
|
5208
|
+
// Show callout after a short delay
|
|
4900
5209
|
setTimeout(() => {
|
|
4901
|
-
this.
|
|
4902
|
-
},
|
|
4903
|
-
// Mark as shown
|
|
4904
|
-
localStorage.setItem(STORAGE_KEY, 'true');
|
|
5210
|
+
this.createCallout();
|
|
5211
|
+
}, 1000);
|
|
4905
5212
|
}
|
|
4906
5213
|
}
|
|
4907
5214
|
/**
|
|
4908
|
-
* Create and show the
|
|
5215
|
+
* Create and show the intro callout
|
|
4909
5216
|
*/
|
|
4910
|
-
|
|
4911
|
-
//
|
|
4912
|
-
if (this.
|
|
5217
|
+
createCallout() {
|
|
5218
|
+
// Avoid duplicates
|
|
5219
|
+
if (this.introCallout || document.querySelector('.cv-widget-callout'))
|
|
4913
5220
|
return;
|
|
4914
|
-
|
|
4915
|
-
|
|
5221
|
+
this.introCallout = document.createElement('div');
|
|
5222
|
+
const callout = this.introCallout;
|
|
5223
|
+
callout.className = `cv-widget-callout cv-pos-${this.options.position}`;
|
|
4916
5224
|
if (this.options.theme === 'dark') {
|
|
4917
|
-
|
|
5225
|
+
callout.classList.add('cv-widget-theme-dark');
|
|
5226
|
+
}
|
|
5227
|
+
// Close button
|
|
5228
|
+
const closeBtn = document.createElement('button');
|
|
5229
|
+
closeBtn.className = 'cv-widget-callout-close';
|
|
5230
|
+
closeBtn.innerHTML = '×';
|
|
5231
|
+
closeBtn.setAttribute('aria-label', 'Dismiss intro');
|
|
5232
|
+
closeBtn.addEventListener('click', (e) => {
|
|
5233
|
+
e.stopPropagation();
|
|
5234
|
+
this.dismissIntroCallout();
|
|
5235
|
+
});
|
|
5236
|
+
// Message
|
|
5237
|
+
const msg = document.createElement('p');
|
|
5238
|
+
msg.className = 'cv-widget-callout-text';
|
|
5239
|
+
msg.textContent = this.options.welcomeMessage;
|
|
5240
|
+
callout.appendChild(closeBtn);
|
|
5241
|
+
callout.appendChild(msg);
|
|
5242
|
+
document.body.appendChild(callout);
|
|
5243
|
+
// Add pulse to widget icon to draw attention
|
|
5244
|
+
if (this.widgetIcon) {
|
|
5245
|
+
this.widgetIcon.classList.add('cv-pulse');
|
|
4918
5246
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
<div class="cv-modal-icon">
|
|
4924
|
-
${getGearIcon()}
|
|
4925
|
-
</div>
|
|
4926
|
-
<h1 class="cv-modal-title">${this.options.welcomeTitle}</h1>
|
|
4927
|
-
</div>
|
|
4928
|
-
</header>
|
|
4929
|
-
<div class="cv-modal-main">
|
|
4930
|
-
<p class="cv-welcome-message">${this.options.welcomeMessage}</p>
|
|
4931
|
-
|
|
4932
|
-
<div class="cv-welcome-widget-preview">
|
|
4933
|
-
<div class="cv-welcome-widget-icon">
|
|
4934
|
-
${getGearIcon()}
|
|
4935
|
-
</div>
|
|
4936
|
-
<p class="cv-welcome-widget-label">Look for this widget</p>
|
|
4937
|
-
</div>
|
|
4938
|
-
|
|
4939
|
-
<button class="cv-welcome-got-it">Got it!</button>
|
|
4940
|
-
</div>
|
|
4941
|
-
</div>
|
|
4942
|
-
`;
|
|
4943
|
-
document.body.appendChild(welcomeModal);
|
|
4944
|
-
this.attachWelcomeModalEventListeners(welcomeModal);
|
|
4945
|
-
}
|
|
4946
|
-
/**
|
|
4947
|
-
* Attach event listeners for welcome modal
|
|
4948
|
-
*/
|
|
4949
|
-
attachWelcomeModalEventListeners(welcomeModal) {
|
|
4950
|
-
const closeModal = () => {
|
|
4951
|
-
welcomeModal.remove();
|
|
4952
|
-
document.removeEventListener('keydown', handleEscape);
|
|
4953
|
-
};
|
|
4954
|
-
// Got it button
|
|
4955
|
-
const gotItBtn = welcomeModal.querySelector('.cv-welcome-got-it');
|
|
4956
|
-
if (gotItBtn) {
|
|
4957
|
-
gotItBtn.addEventListener('click', closeModal);
|
|
4958
|
-
}
|
|
4959
|
-
// Overlay click to close
|
|
4960
|
-
welcomeModal.addEventListener('click', (e) => {
|
|
4961
|
-
if (e.target === welcomeModal) {
|
|
4962
|
-
closeModal();
|
|
4963
|
-
}
|
|
5247
|
+
// Auto-dismiss and open widget on click anywhere on callout
|
|
5248
|
+
callout.addEventListener('click', () => {
|
|
5249
|
+
this.dismissIntroCallout();
|
|
5250
|
+
this.openStateModal();
|
|
4964
5251
|
});
|
|
4965
|
-
// Escape key to close
|
|
4966
|
-
const handleEscape = (e) => {
|
|
4967
|
-
if (e.key === 'Escape') {
|
|
4968
|
-
closeModal();
|
|
4969
|
-
}
|
|
4970
|
-
};
|
|
4971
|
-
document.addEventListener('keydown', handleEscape);
|
|
4972
5252
|
}
|
|
4973
5253
|
}
|
|
4974
5254
|
|