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