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