@customviews-js/customviews 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +78 -0
  2. package/dist/custom-views.cjs.js +1564 -0
  3. package/dist/custom-views.cjs.js.map +1 -0
  4. package/dist/custom-views.esm.js +1556 -0
  5. package/dist/custom-views.esm.js.map +1 -0
  6. package/dist/custom-views.umd.js +1570 -0
  7. package/dist/custom-views.umd.js.map +1 -0
  8. package/dist/custom-views.umd.min.js +7 -0
  9. package/dist/custom-views.umd.min.js.map +1 -0
  10. package/dist/types/core/core.d.ts +52 -0
  11. package/dist/types/core/core.d.ts.map +1 -0
  12. package/dist/types/core/persistence.d.ts +22 -0
  13. package/dist/types/core/persistence.d.ts.map +1 -0
  14. package/dist/types/core/render.d.ts +3 -0
  15. package/dist/types/core/render.d.ts.map +1 -0
  16. package/dist/types/core/url-state-manager.d.ts +32 -0
  17. package/dist/types/core/url-state-manager.d.ts.map +1 -0
  18. package/dist/types/core/visibility-manager.d.ts +28 -0
  19. package/dist/types/core/visibility-manager.d.ts.map +1 -0
  20. package/dist/types/core/widget.d.ts +93 -0
  21. package/dist/types/core/widget.d.ts.map +1 -0
  22. package/dist/types/index.d.ts +20 -0
  23. package/dist/types/index.d.ts.map +1 -0
  24. package/dist/types/models/AssetsManager.d.ts +10 -0
  25. package/dist/types/models/AssetsManager.d.ts.map +1 -0
  26. package/dist/types/models/Config.d.ts +10 -0
  27. package/dist/types/models/Config.d.ts.map +1 -0
  28. package/dist/types/styles/styles.d.ts +5 -0
  29. package/dist/types/styles/styles.d.ts.map +1 -0
  30. package/dist/types/styles/widget-styles.d.ts +13 -0
  31. package/dist/types/styles/widget-styles.d.ts.map +1 -0
  32. package/dist/types/types/types.d.ts +27 -0
  33. package/dist/types/types/types.d.ts.map +1 -0
  34. package/package.json +57 -0
@@ -0,0 +1,1570 @@
1
+ /*!
2
+ * customviews v0.2.0
3
+ * (c) 2025 Chan Ger Teck
4
+ * Released under the MIT License.
5
+ */
6
+ (function (global, factory) {
7
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
8
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CustomViews = {}));
10
+ })(this, (function (exports) { 'use strict';
11
+
12
+ /** --- Basic renderers --- */
13
+ function renderImage(el, asset) {
14
+ if (!asset.src)
15
+ return;
16
+ el.innerHTML = '';
17
+ const img = document.createElement('img');
18
+ img.src = asset.src;
19
+ img.alt = asset.alt || '';
20
+ // Apply custom styling if provided
21
+ if (asset.className) {
22
+ img.className = asset.className;
23
+ }
24
+ if (asset.style) {
25
+ img.setAttribute('style', asset.style);
26
+ }
27
+ // Default styles (can be overridden by asset.style)
28
+ img.style.maxWidth = img.style.maxWidth || '100%';
29
+ img.style.height = img.style.height || 'auto';
30
+ img.style.display = img.style.display || 'block';
31
+ el.appendChild(img);
32
+ }
33
+ function renderText(el, asset) {
34
+ if (asset.content != null) {
35
+ el.textContent = asset.content;
36
+ }
37
+ // Apply custom styling if provided
38
+ if (asset.className) {
39
+ el.className = asset.className;
40
+ }
41
+ if (asset.style) {
42
+ el.setAttribute('style', asset.style);
43
+ }
44
+ }
45
+ function renderHtml(el, asset) {
46
+ if (asset.content != null) {
47
+ el.innerHTML = asset.content;
48
+ }
49
+ // Apply custom styling if provided
50
+ if (asset.className) {
51
+ el.className = asset.className;
52
+ }
53
+ if (asset.style) {
54
+ el.setAttribute('style', asset.style);
55
+ }
56
+ }
57
+ /** --- Unified asset renderer --- */
58
+ function detectAssetType(asset) {
59
+ // If src exists, it's an image
60
+ if (asset.src)
61
+ return 'image';
62
+ // If content contains HTML tags, it's HTML
63
+ if (asset.content && /<[^>]+>/.test(asset.content)) {
64
+ return 'html';
65
+ }
66
+ return 'text';
67
+ }
68
+ function renderAssetInto(el, assetId, assetsManager) {
69
+ const asset = assetsManager.get(assetId);
70
+ if (!asset)
71
+ return;
72
+ const type = asset.type || detectAssetType(asset);
73
+ switch (type) {
74
+ case 'image':
75
+ renderImage(el, asset);
76
+ break;
77
+ case 'text':
78
+ renderText(el, asset);
79
+ break;
80
+ case 'html':
81
+ renderHtml(el, asset);
82
+ break;
83
+ default:
84
+ el.innerHTML = asset.content || String(asset);
85
+ console.warn('[CustomViews] Unknown asset type:', type);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Configuration for the site, has default state and list of toggles
91
+ */
92
+ class Config {
93
+ defaultState;
94
+ allToggles;
95
+ constructor(defaultState, allToggles) {
96
+ this.defaultState = defaultState;
97
+ this.allToggles = allToggles;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Manages persistence of custom views state using browser localStorage
103
+ */
104
+ class PersistenceManager {
105
+ // Storage keys for localStorage
106
+ static STORAGE_KEYS = {
107
+ STATE: 'customviews-state'
108
+ };
109
+ /**
110
+ * Check if localStorage is available in the current environment
111
+ */
112
+ isStorageAvailable() {
113
+ return typeof window !== 'undefined' && window.localStorage !== undefined;
114
+ }
115
+ persistState(state) {
116
+ if (!this.isStorageAvailable())
117
+ return;
118
+ try {
119
+ localStorage.setItem(PersistenceManager.STORAGE_KEYS.STATE, JSON.stringify(state));
120
+ }
121
+ catch (error) {
122
+ console.warn('Failed to persist state:', error);
123
+ }
124
+ }
125
+ getPersistedState() {
126
+ if (!this.isStorageAvailable())
127
+ return null;
128
+ try {
129
+ const raw = localStorage.getItem(PersistenceManager.STORAGE_KEYS.STATE);
130
+ return raw ? JSON.parse(raw) : null;
131
+ }
132
+ catch (error) {
133
+ console.warn('Failed to parse persisted state:', error);
134
+ return null;
135
+ }
136
+ }
137
+ /**
138
+ * Clear persisted state
139
+ */
140
+ clearAll() {
141
+ if (!this.isStorageAvailable())
142
+ return;
143
+ localStorage.removeItem(PersistenceManager.STORAGE_KEYS.STATE);
144
+ }
145
+ /**
146
+ * Check if any persistence data exists
147
+ */
148
+ hasPersistedData() {
149
+ if (!this.isStorageAvailable()) {
150
+ return false;
151
+ }
152
+ return !!this.getPersistedState();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * URL State Manager for CustomViews
158
+ * Handles encoding/decoding of states in URL parameters
159
+ */
160
+ class URLStateManager {
161
+ /**
162
+ * Parse current URL parameters into state object
163
+ */
164
+ static parseURL() {
165
+ const urlParams = new URLSearchParams(window.location.search);
166
+ // Get view state
167
+ const viewParam = urlParams.get('view');
168
+ let decoded = null;
169
+ if (viewParam) {
170
+ try {
171
+ decoded = this.decodeState(viewParam);
172
+ }
173
+ catch (error) {
174
+ console.warn('Failed to decode view state from URL:', error);
175
+ }
176
+ }
177
+ return decoded;
178
+ }
179
+ /**
180
+ * Update URL with current state without triggering navigation
181
+ */
182
+ static updateURL(state) {
183
+ if (typeof window === 'undefined' || !window.history)
184
+ return;
185
+ const url = new URL(window.location.href);
186
+ // Clear existing parameters
187
+ url.searchParams.delete('view');
188
+ // Set view state
189
+ if (state) {
190
+ const encoded = this.encodeState(state);
191
+ if (encoded) {
192
+ url.searchParams.set('view', encoded);
193
+ }
194
+ }
195
+ // Use a relative URL to satisfy stricter environments (e.g., jsdom tests)
196
+ const relative = url.pathname + (url.search || '') + (url.hash || '');
197
+ window.history.replaceState({}, '', relative);
198
+ }
199
+ /**
200
+ * Clear all state parameters from URL
201
+ */
202
+ static clearURL() {
203
+ this.updateURL(null);
204
+ }
205
+ /**
206
+ * Generate shareable URL for current state
207
+ */
208
+ static generateShareableURL(state) {
209
+ const url = new URL(window.location.href);
210
+ // Clear existing parameters
211
+ url.searchParams.delete('view');
212
+ // Set new parameters
213
+ if (state) {
214
+ const encoded = this.encodeState(state);
215
+ if (encoded) {
216
+ url.searchParams.set('view', encoded);
217
+ }
218
+ }
219
+ return url.toString();
220
+ }
221
+ /**
222
+ * Encode state into URL-safe string
223
+ */
224
+ static encodeState(state) {
225
+ try {
226
+ // Create a compact representation
227
+ const compact = {
228
+ t: state.toggles
229
+ };
230
+ // Convert to JSON and encode
231
+ const json = JSON.stringify(compact);
232
+ let encoded;
233
+ if (typeof btoa === 'function') {
234
+ encoded = btoa(json);
235
+ }
236
+ else {
237
+ // Node/test fallback
238
+ // @ts-ignore
239
+ encoded = Buffer.from(json, 'utf-8').toString('base64');
240
+ }
241
+ // Make URL-safe
242
+ const urlSafeString = encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
243
+ return urlSafeString;
244
+ }
245
+ catch (error) {
246
+ console.warn('Failed to encode state:', error);
247
+ return null;
248
+ }
249
+ }
250
+ /**
251
+ * Decode custom state from URL parameter
252
+ */
253
+ static decodeState(encoded) {
254
+ try {
255
+ // Restore base64 padding and characters
256
+ let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
257
+ // Add padding if needed
258
+ while (base64.length % 4) {
259
+ base64 += '=';
260
+ }
261
+ // Decode and parse
262
+ let json;
263
+ if (typeof atob === 'function') {
264
+ json = atob(base64);
265
+ }
266
+ else {
267
+ // Node/test fallback
268
+ // @ts-ignore
269
+ json = Buffer.from(base64, 'base64').toString('utf-8');
270
+ }
271
+ const compact = JSON.parse(json);
272
+ // Validate structure
273
+ if (!compact || typeof compact !== 'object') {
274
+ throw new Error('Invalid compact state structure');
275
+ }
276
+ return {
277
+ toggles: Array.isArray(compact.t) ? compact.t : []
278
+ };
279
+ }
280
+ catch (error) {
281
+ console.warn('Failed to decode view state:', error);
282
+ return null;
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Keeps track of which toggles are hidden and which are visible in memory.
289
+ *
290
+ * This class keeps track of hidden toggles without reading the DOM or URL.
291
+ */
292
+ class VisibilityManager {
293
+ hiddenToggles = new Set();
294
+ /** Marks a toggle as visible or hidden.
295
+ * Returns true if changed.
296
+ * Also updates internal set of hidden toggles.
297
+ */
298
+ setToggleVisibility(toggleId, visible) {
299
+ const wasHidden = this.hiddenToggles.has(toggleId);
300
+ const shouldHide = !visible;
301
+ if (shouldHide && !wasHidden) {
302
+ this.hiddenToggles.add(toggleId);
303
+ return true;
304
+ }
305
+ if (!shouldHide && wasHidden) {
306
+ this.hiddenToggles.delete(toggleId);
307
+ return true;
308
+ }
309
+ return false;
310
+ }
311
+ /** Hide all toggles in the provided set. */
312
+ hideAll(allToggleIds) {
313
+ for (const id of allToggleIds) {
314
+ this.setToggleVisibility(id, false);
315
+ }
316
+ }
317
+ /** Show all toggles in the provided set. */
318
+ showAll(allToggleIds) {
319
+ for (const id of allToggleIds) {
320
+ this.setToggleVisibility(id, true);
321
+ }
322
+ }
323
+ /** Get the globally hidden toggle ids (explicitly hidden via API). */
324
+ getHiddenToggles() {
325
+ return Array.from(this.hiddenToggles);
326
+ }
327
+ /** Filter a list of toggles to only those visible per the hidden set. */
328
+ filterVisibleToggles(toggleIds) {
329
+ return toggleIds.filter(t => !this.hiddenToggles.has(t));
330
+ }
331
+ /**
332
+ * Apply simple class-based visibility to a toggle element.
333
+ * The element is assumed to have data-customviews-toggle.
334
+ */
335
+ applyElementVisibility(el, visible) {
336
+ if (visible) {
337
+ el.classList.remove('cv-hidden');
338
+ el.classList.add('cv-visible');
339
+ }
340
+ else {
341
+ el.classList.add('cv-hidden');
342
+ el.classList.remove('cv-visible');
343
+ }
344
+ }
345
+ }
346
+
347
+ const CORE_STYLES = `
348
+ [data-customviews-toggle] {
349
+ transition: opacity 150ms ease,
350
+ transform 150ms ease,
351
+ max-height 200ms ease,
352
+ margin 150ms ease;
353
+ will-change: opacity, transform, max-height, margin;
354
+ }
355
+
356
+ .cv-visible {
357
+ opacity: 1 !important;
358
+ transform: translateY(0) !important;
359
+ max-height: var(--cv-max-height, 9999px) !important;
360
+ }
361
+
362
+ .cv-hidden {
363
+ opacity: 0 !important;
364
+ transform: translateY(-4px) !important;
365
+ pointer-events: none !important;
366
+ padding-top: 0 !important;
367
+ padding-bottom: 0 !important;
368
+ border-top-width: 0 !important;
369
+ border-bottom-width: 0 !important;
370
+ max-height: 0 !important;
371
+ margin-top: 0 !important;
372
+ margin-bottom: 0 !important;
373
+ overflow: hidden !important;
374
+ }
375
+ `;
376
+ /**
377
+ * Add styles for hiding and showing toggles animations and transitions to the document head
378
+ */
379
+ function injectCoreStyles() {
380
+ if (typeof document === 'undefined')
381
+ return;
382
+ if (document.querySelector('#cv-core-styles'))
383
+ return;
384
+ const style = document.createElement('style');
385
+ style.id = 'cv-core-styles';
386
+ style.textContent = CORE_STYLES;
387
+ document.head.appendChild(style);
388
+ }
389
+
390
+ class CustomViewsCore {
391
+ rootEl;
392
+ assetsManager;
393
+ persistenceManager;
394
+ visibilityManager;
395
+ stateFromUrl = null;
396
+ localConfig;
397
+ stateChangeListeners = [];
398
+ constructor(opt) {
399
+ this.assetsManager = opt.assetsManager;
400
+ this.localConfig = opt.config;
401
+ this.rootEl = opt.rootEl || document.body;
402
+ this.persistenceManager = new PersistenceManager();
403
+ this.visibilityManager = new VisibilityManager();
404
+ }
405
+ getLocalConfig() {
406
+ return this.localConfig;
407
+ }
408
+ // Inject styles, setup listeners and call rendering logic
409
+ async init() {
410
+ injectCoreStyles();
411
+ // For session history, clicks on back/forward button
412
+ window.addEventListener("popstate", () => {
413
+ this.loadAndRenderState();
414
+ });
415
+ this.loadAndRenderState();
416
+ }
417
+ // Priority: URL state > persisted state > default
418
+ // Also filters using the visibility manager to persist selection
419
+ // across back/forward button clicks
420
+ async loadAndRenderState() {
421
+ // 1. URL State
422
+ this.stateFromUrl = URLStateManager.parseURL();
423
+ if (this.stateFromUrl) {
424
+ this.applyState(this.stateFromUrl);
425
+ return;
426
+ }
427
+ // 2. Persisted State
428
+ const persistedState = this.persistenceManager.getPersistedState();
429
+ if (persistedState) {
430
+ this.applyState(persistedState);
431
+ return;
432
+ }
433
+ // 3. Local Config Fallback
434
+ this.renderState(this.localConfig.defaultState);
435
+ }
436
+ /**
437
+ * Apply a custom state, saves to localStorage and updates the URL
438
+ */
439
+ applyState(state) {
440
+ this.renderState(state);
441
+ this.persistenceManager.persistState(state);
442
+ this.stateFromUrl = state;
443
+ URLStateManager.updateURL(state);
444
+ }
445
+ /** Render all toggles for the current state */
446
+ renderState(state) {
447
+ const toggles = state.toggles || [];
448
+ const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
449
+ // Toggles hide or show relevant toggles
450
+ this.rootEl.querySelectorAll("[data-customviews-toggle]").forEach(el => {
451
+ const category = el.dataset.customviewsToggle;
452
+ const shouldShow = !!category && finalToggles.includes(category);
453
+ this.visibilityManager.applyElementVisibility(el, shouldShow);
454
+ });
455
+ // Render toggles
456
+ for (const category of finalToggles) {
457
+ this.rootEl.querySelectorAll(`[data-customviews-toggle="${category}"]`).forEach(el => {
458
+ // if it has an id, then we render the asset into it
459
+ // if it has no id, then we assume it's a container
460
+ const toggleId = el.dataset.customviewsId;
461
+ if (toggleId) {
462
+ renderAssetInto(el, toggleId, this.assetsManager);
463
+ }
464
+ });
465
+ }
466
+ // Notify state change listeners (like widgets)
467
+ this.notifyStateChangeListeners();
468
+ }
469
+ /**
470
+ * Reset to default state
471
+ */
472
+ resetToDefault() {
473
+ this.stateFromUrl = null;
474
+ this.persistenceManager.clearAll();
475
+ if (this.localConfig) {
476
+ this.renderState(this.localConfig.defaultState);
477
+ }
478
+ else {
479
+ console.warn("No configuration loaded, cannot reset to default state");
480
+ }
481
+ // Clear URL
482
+ URLStateManager.clearURL();
483
+ }
484
+ /**
485
+ * Get the currently active toggles regardless of whether they come from custom state or default configuration
486
+ */
487
+ getCurrentActiveToggles() {
488
+ // If we have a custom state, return its toggles
489
+ if (this.stateFromUrl) {
490
+ return this.stateFromUrl.toggles || [];
491
+ }
492
+ // Otherwise, if we have local config, return its default state toggles
493
+ if (this.localConfig) {
494
+ return this.localConfig.defaultState.toggles || [];
495
+ }
496
+ // No configuration or state
497
+ return [];
498
+ }
499
+ /**
500
+ * Clear all persistence and reset to default
501
+ */
502
+ clearPersistence() {
503
+ this.persistenceManager.clearAll();
504
+ this.stateFromUrl = null;
505
+ if (this.localConfig) {
506
+ this.renderState(this.localConfig.defaultState);
507
+ }
508
+ else {
509
+ console.warn("No configuration loaded, cannot reset to default state");
510
+ }
511
+ URLStateManager.clearURL();
512
+ }
513
+ // === STATE CHANGE LISTENER METHODS ===
514
+ /**
515
+ * Add a listener that will be called whenever the state changes
516
+ */
517
+ addStateChangeListener(listener) {
518
+ this.stateChangeListeners.push(listener);
519
+ }
520
+ /**
521
+ * Remove a state change listener
522
+ */
523
+ removeStateChangeListener(listener) {
524
+ const index = this.stateChangeListeners.indexOf(listener);
525
+ if (index > -1) {
526
+ this.stateChangeListeners.splice(index, 1);
527
+ }
528
+ }
529
+ /**
530
+ * Notify all state change listeners
531
+ */
532
+ notifyStateChangeListeners() {
533
+ this.stateChangeListeners.forEach(listener => {
534
+ try {
535
+ listener();
536
+ }
537
+ catch (error) {
538
+ console.warn('Error in state change listener:', error);
539
+ }
540
+ });
541
+ }
542
+ }
543
+
544
+ class AssetsManager {
545
+ assets;
546
+ constructor(assets) {
547
+ this.assets = assets;
548
+ if (!this.validate()) {
549
+ console.warn('Invalid assets:', this.assets);
550
+ }
551
+ }
552
+ // Check each asset has content or src
553
+ validate() {
554
+ return Object.values(this.assets).every(a => a.src || a.content);
555
+ }
556
+ get(assetId) {
557
+ return this.assets[assetId];
558
+ }
559
+ loadFromJSON(json) {
560
+ this.assets = json;
561
+ }
562
+ loadAdditionalAssets(additionalAssets) {
563
+ this.assets = { ...this.assets, ...additionalAssets };
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Widget styles for CustomViews
569
+ * Extracted from widget.ts for better maintainability
570
+ *
571
+ * Note: Styles are kept as a TypeScript string for compatibility with the build system.
572
+ * This approach ensures the styles are properly bundled and don't require separate CSS file handling.
573
+ */
574
+ const WIDGET_STYLES = `
575
+ /* Rounded rectangle widget icon styles */
576
+ .cv-widget-icon {
577
+ position: fixed;
578
+ background: white;
579
+ color: black;
580
+ display: flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ font-size: 18px;
584
+ font-weight: bold;
585
+ cursor: pointer;
586
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
587
+ z-index: 9998;
588
+ transition: all 0.3s ease;
589
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
590
+ }
591
+
592
+ .cv-widget-icon:hover {
593
+ background: white;
594
+ color: black;
595
+ }
596
+
597
+ /* Top-right: rounded end on left, sticks out leftward on hover */
598
+ .cv-widget-top-right {
599
+ top: 20px;
600
+ right: 0;
601
+ border-radius: 18px 0 0 18px;
602
+ padding-left: 8px;
603
+ justify-content: flex-start;
604
+ }
605
+
606
+ /* Top-left: rounded end on right, sticks out rightward on hover */
607
+ .cv-widget-top-left {
608
+ top: 20px;
609
+ left: 0;
610
+ border-radius: 0 18px 18px 0;
611
+ padding-right: 8px;
612
+ justify-content: flex-end;
613
+ }
614
+
615
+ /* Bottom-right: rounded end on left, sticks out leftward on hover */
616
+ .cv-widget-bottom-right {
617
+ bottom: 20px;
618
+ right: 0;
619
+ border-radius: 18px 0 0 18px;
620
+ padding-left: 8px;
621
+ justify-content: flex-start;
622
+ }
623
+
624
+ /* Bottom-left: rounded end on right, sticks out rightward on hover */
625
+ .cv-widget-bottom-left {
626
+ bottom: 20px;
627
+ left: 0;
628
+ border-radius: 0 18px 18px 0;
629
+ padding-right: 8px;
630
+ justify-content: flex-end;
631
+ }
632
+
633
+ /* Middle-left: rounded end on right, sticks out rightward on hover */
634
+ .cv-widget-middle-left {
635
+ top: 50%;
636
+ left: 0;
637
+ transform: translateY(-50%);
638
+ border-radius: 0 18px 18px 0;
639
+ padding-right: 8px;
640
+ justify-content: flex-end;
641
+ }
642
+
643
+ /* Middle-right: rounded end on left, sticks out leftward on hover */
644
+ .cv-widget-middle-right {
645
+ top: 50%;
646
+ right: 0;
647
+ transform: translateY(-50%);
648
+ border-radius: 18px 0 0 18px;
649
+ padding-left: 8px;
650
+ justify-content: flex-start;
651
+ }
652
+
653
+ .cv-widget-top-right,
654
+ .cv-widget-middle-right,
655
+ .cv-widget-bottom-right,
656
+ .cv-widget-top-left,
657
+ .cv-widget-middle-left,
658
+ .cv-widget-bottom-left {
659
+ height: 36px;
660
+ width: 36px;
661
+ }
662
+
663
+ .cv-widget-middle-right:hover,
664
+ .cv-widget-top-right:hover,
665
+ .cv-widget-bottom-right:hover,
666
+ .cv-widget-top-left:hover,
667
+ .cv-widget-middle-left:hover,
668
+ .cv-widget-bottom-left:hover {
669
+ width: 55px;
670
+ }
671
+
672
+ /* Modal content styles */
673
+ .cv-widget-section {
674
+ margin-bottom: 16px;
675
+ }
676
+
677
+ .cv-widget-section:last-child {
678
+ margin-bottom: 0;
679
+ }
680
+
681
+ .cv-widget-section label {
682
+ display: block;
683
+ margin-bottom: 4px;
684
+ font-weight: 500;
685
+ color: #555;
686
+ }
687
+
688
+ .cv-widget-profile-select,
689
+ .cv-widget-state-select {
690
+ width: 100%;
691
+ padding: 8px 12px;
692
+ border: 1px solid #ddd;
693
+ border-radius: 4px;
694
+ background: white;
695
+ font-size: 14px;
696
+ }
697
+
698
+ .cv-widget-profile-select:focus,
699
+ .cv-widget-state-select:focus {
700
+ outline: none;
701
+ border-color: #007bff;
702
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
703
+ }
704
+
705
+ .cv-widget-profile-select:disabled,
706
+ .cv-widget-state-select:disabled {
707
+ background: #f8f9fa;
708
+ color: #6c757d;
709
+ cursor: not-allowed;
710
+ }
711
+
712
+ .cv-widget-current {
713
+ margin: 16px 0;
714
+ padding: 12px;
715
+ background: #f8f9fa;
716
+ border-radius: 4px;
717
+ border-left: 4px solid #007bff;
718
+ }
719
+
720
+ .cv-widget-current label {
721
+ font-size: 12px;
722
+ text-transform: uppercase;
723
+ letter-spacing: 0.5px;
724
+ color: #666;
725
+ margin-bottom: 4px;
726
+ }
727
+
728
+ .cv-widget-current-view {
729
+ font-weight: 500;
730
+ color: #333;
731
+ }
732
+
733
+ .cv-widget-reset {
734
+ width: 100%;
735
+ padding: 8px 16px;
736
+ background: #dc3545;
737
+ color: white;
738
+ border: none;
739
+ border-radius: 4px;
740
+ cursor: pointer;
741
+ font-size: 14px;
742
+ font-weight: 500;
743
+ }
744
+
745
+ .cv-widget-reset:hover {
746
+ background: #c82333;
747
+ }
748
+
749
+ .cv-widget-reset:active {
750
+ background: #bd2130;
751
+ }
752
+
753
+ /* Responsive design for mobile */
754
+ @media (max-width: 768px) {
755
+ .cv-widget-top-right,
756
+ .cv-widget-top-left {
757
+ top: 10px;
758
+ }
759
+
760
+ .cv-widget-bottom-right,
761
+ .cv-widget-bottom-left {
762
+ bottom: 10px;
763
+ }
764
+
765
+ /* All widgets stay flush with screen edges */
766
+ .cv-widget-top-right,
767
+ .cv-widget-bottom-right,
768
+ .cv-widget-middle-right {
769
+ right: 0;
770
+ }
771
+
772
+ .cv-widget-top-left,
773
+ .cv-widget-bottom-left,
774
+ .cv-widget-middle-left {
775
+ left: 0;
776
+ }
777
+
778
+ /* Slightly smaller on mobile */
779
+ .cv-widget-icon {
780
+ width: 60px;
781
+ height: 32px;
782
+ }
783
+
784
+ .cv-widget-icon:hover {
785
+ width: 75px;
786
+ }
787
+ }
788
+
789
+ /* Modal styles */
790
+ .cv-widget-modal-overlay {
791
+ position: fixed;
792
+ top: 0;
793
+ left: 0;
794
+ right: 0;
795
+ bottom: 0;
796
+ background: rgba(0, 0, 0, 0.5);
797
+ display: flex;
798
+ align-items: center;
799
+ justify-content: center;
800
+ z-index: 10002;
801
+ animation: fadeIn 0.2s ease;
802
+ }
803
+
804
+ @keyframes fadeIn {
805
+ from { opacity: 0; }
806
+ to { opacity: 1; }
807
+ }
808
+
809
+ .cv-widget-modal {
810
+ background: white;
811
+ border-radius: 8px;
812
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
813
+ max-width: 400px;
814
+ width: 90vw;
815
+ max-height: 80vh;
816
+ overflow-y: auto;
817
+ animation: slideIn 0.2s ease;
818
+ }
819
+
820
+ @keyframes slideIn {
821
+ from {
822
+ opacity: 0;
823
+ transform: scale(0.9) translateY(-20px);
824
+ }
825
+ to {
826
+ opacity: 1;
827
+ transform: scale(1) translateY(0);
828
+ }
829
+ }
830
+
831
+ .cv-widget-modal-header {
832
+ display: flex;
833
+ justify-content: space-between;
834
+ align-items: center;
835
+ padding: 16px 20px;
836
+ border-bottom: 1px solid #e9ecef;
837
+ background: #f8f9fa;
838
+ border-radius: 8px 8px 0 0;
839
+ }
840
+
841
+ .cv-widget-modal-header h3 {
842
+ margin: 0;
843
+ font-size: 18px;
844
+ font-weight: 600;
845
+ color: #333;
846
+ }
847
+
848
+ .cv-widget-modal-close {
849
+ background: none;
850
+ border: none;
851
+ font-size: 24px;
852
+ cursor: pointer;
853
+ padding: 0;
854
+ width: 32px;
855
+ height: 32px;
856
+ display: flex;
857
+ align-items: center;
858
+ justify-content: center;
859
+ border-radius: 4px;
860
+ color: #666;
861
+ }
862
+
863
+ .cv-widget-modal-close:hover {
864
+ background: #e9ecef;
865
+ }
866
+
867
+ .cv-widget-modal-content {
868
+ padding: 20px;
869
+ }
870
+
871
+ .cv-widget-modal-actions {
872
+ margin-top: 20px;
873
+ padding-top: 16px;
874
+ border-top: 1px solid #e9ecef;
875
+ }
876
+
877
+ .cv-widget-restore {
878
+ width: 100%;
879
+ padding: 10px 16px;
880
+ background: #28a745;
881
+ color: white;
882
+ border: none;
883
+ border-radius: 4px;
884
+ cursor: pointer;
885
+ font-size: 14px;
886
+ font-weight: 500;
887
+ }
888
+
889
+ .cv-widget-restore:hover {
890
+ background: #218838;
891
+ }
892
+
893
+ .cv-widget-create-state {
894
+ width: 100%;
895
+ padding: 10px 16px;
896
+ background: #007bff;
897
+ color: white;
898
+ border: none;
899
+ border-radius: 4px;
900
+ cursor: pointer;
901
+ font-size: 14px;
902
+ font-weight: 500;
903
+ margin-bottom: 10px;
904
+ }
905
+
906
+ .cv-widget-create-state:hover {
907
+ background: #0056b3;
908
+ }
909
+
910
+ /* Dark theme modal styles */
911
+ .cv-widget-theme-dark .cv-widget-modal {
912
+ background: #2d3748;
913
+ color: #e2e8f0;
914
+ }
915
+
916
+ .cv-widget-theme-dark .cv-widget-modal-header {
917
+ background: #1a202c;
918
+ border-color: #4a5568;
919
+ }
920
+
921
+ .cv-widget-theme-dark .cv-widget-modal-header h3 {
922
+ color: #e2e8f0;
923
+ }
924
+
925
+ .cv-widget-theme-dark .cv-widget-modal-close {
926
+ color: #a0aec0;
927
+ }
928
+
929
+ .cv-widget-theme-dark .cv-widget-modal-close:hover {
930
+ background: #4a5568;
931
+ }
932
+
933
+ .cv-widget-theme-dark .cv-widget-modal-actions {
934
+ border-color: #4a5568;
935
+ }
936
+
937
+ /* Custom state creator styles */
938
+ .cv-custom-state-modal {
939
+ max-width: 500px;
940
+ }
941
+
942
+ .cv-custom-state-form h4 {
943
+ margin: 20px 0 10px 0;
944
+ font-size: 16px;
945
+ font-weight: 600;
946
+ color: #333;
947
+ border-bottom: 1px solid #e9ecef;
948
+ padding-bottom: 5px;
949
+ }
950
+
951
+ .cv-custom-state-section {
952
+ margin-bottom: 16px;
953
+ }
954
+
955
+ .cv-custom-state-section label {
956
+ display: block;
957
+ margin-bottom: 4px;
958
+ font-weight: 500;
959
+ color: #555;
960
+ }
961
+
962
+ .cv-custom-state-input {
963
+ width: 100%;
964
+ padding: 8px 12px;
965
+ border: 1px solid #ddd;
966
+ border-radius: 4px;
967
+ background: white;
968
+ font-size: 14px;
969
+ }
970
+
971
+ .cv-custom-state-input:focus {
972
+ outline: none;
973
+ border-color: #007bff;
974
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
975
+ }
976
+
977
+ .cv-custom-toggles {
978
+ display: grid;
979
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
980
+ gap: 10px;
981
+ }
982
+
983
+ .cv-custom-state-toggle {
984
+ display: flex;
985
+ align-items: center;
986
+ }
987
+
988
+ .cv-custom-state-toggle label {
989
+ display: flex;
990
+ align-items: center;
991
+ cursor: pointer;
992
+ font-weight: normal;
993
+ margin: 0;
994
+ }
995
+
996
+ .cv-custom-toggle-checkbox {
997
+ margin-right: 8px;
998
+ width: auto;
999
+ }
1000
+
1001
+ .cv-custom-state-actions {
1002
+ display: flex;
1003
+ gap: 10px;
1004
+ margin-top: 20px;
1005
+ padding-top: 16px;
1006
+ border-top: 1px solid #e9ecef;
1007
+ }
1008
+
1009
+ .cv-custom-state-cancel,
1010
+ .cv-custom-state-copy-url {
1011
+ flex: 1;
1012
+ padding: 10px 16px;
1013
+ border: none;
1014
+ border-radius: 4px;
1015
+ cursor: pointer;
1016
+ font-size: 14px;
1017
+ font-weight: 500;
1018
+ }
1019
+
1020
+ .cv-custom-state-reset {
1021
+ flex: 1;
1022
+ padding: 10px 16px;
1023
+ border: none;
1024
+ border-radius: 4px;
1025
+ cursor: pointer;
1026
+ font-size: 14px;
1027
+ font-weight: 500;
1028
+ background: #dc3545;
1029
+ color: white;
1030
+ }
1031
+
1032
+ .cv-custom-state-reset:hover {
1033
+ background: #c82333;
1034
+ }
1035
+
1036
+ .cv-custom-state-cancel {
1037
+ background: #6c757d;
1038
+ color: white;
1039
+ }
1040
+
1041
+ .cv-custom-state-cancel:hover {
1042
+ background: #5a6268;
1043
+ }
1044
+
1045
+ .cv-custom-state-copy-url {
1046
+ background: #28a745;
1047
+ color: white;
1048
+ }
1049
+
1050
+ .cv-custom-state-copy-url:hover {
1051
+ background: #218838;
1052
+ }
1053
+
1054
+ /* Dark theme custom state styles */
1055
+ .cv-widget-theme-dark .cv-custom-state-form h4 {
1056
+ color: #e2e8f0;
1057
+ border-color: #4a5568;
1058
+ }
1059
+
1060
+ .cv-widget-theme-dark .cv-custom-state-section label {
1061
+ color: #a0aec0;
1062
+ }
1063
+
1064
+ .cv-widget-theme-dark .cv-custom-state-input {
1065
+ background: #1a202c;
1066
+ border-color: #4a5568;
1067
+ color: #e2e8f0;
1068
+ }
1069
+
1070
+ .cv-widget-theme-dark .cv-custom-state-actions {
1071
+ border-color: #4a5568;
1072
+ }
1073
+
1074
+ /* Welcome modal styles */
1075
+ .cv-welcome-modal {
1076
+ max-width: 500px;
1077
+ }
1078
+
1079
+ .cv-welcome-content {
1080
+ text-align: center;
1081
+ }
1082
+
1083
+ .cv-welcome-content p {
1084
+ font-size: 15px;
1085
+ line-height: 1.6;
1086
+ color: #555;
1087
+ margin-bottom: 24px;
1088
+ }
1089
+
1090
+ .cv-welcome-widget-preview {
1091
+ display: flex;
1092
+ flex-direction: column;
1093
+ align-items: center;
1094
+ gap: 12px;
1095
+ padding: 20px;
1096
+ background: #f8f9fa;
1097
+ border-radius: 8px;
1098
+ margin-bottom: 24px;
1099
+ }
1100
+
1101
+ .cv-welcome-widget-icon {
1102
+ width: 36px;
1103
+ height: 36px;
1104
+ background: white;
1105
+ color: black;
1106
+ border-radius: 0 18px 18px 0;
1107
+ display: flex;
1108
+ align-items: center;
1109
+ justify-content: center;
1110
+ font-size: 18px;
1111
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1112
+ }
1113
+
1114
+ .cv-welcome-widget-label {
1115
+ font-size: 14px;
1116
+ color: #666;
1117
+ margin: 0;
1118
+ font-weight: 500;
1119
+ }
1120
+
1121
+ .cv-welcome-got-it {
1122
+ width: 100%;
1123
+ padding: 12px 24px;
1124
+ background: #007bff;
1125
+ color: white;
1126
+ border: none;
1127
+ border-radius: 4px;
1128
+ cursor: pointer;
1129
+ font-size: 16px;
1130
+ font-weight: 600;
1131
+ transition: background 0.2s ease;
1132
+ }
1133
+
1134
+ .cv-welcome-got-it:hover {
1135
+ background: #0056b3;
1136
+ }
1137
+
1138
+ .cv-welcome-got-it:active {
1139
+ background: #004494;
1140
+ }
1141
+
1142
+ /* Dark theme welcome modal styles */
1143
+ .cv-widget-theme-dark .cv-welcome-content p {
1144
+ color: #cbd5e0;
1145
+ }
1146
+
1147
+ .cv-widget-theme-dark .cv-welcome-widget-preview {
1148
+ background: #1a202c;
1149
+ }
1150
+
1151
+ .cv-widget-theme-dark .cv-welcome-widget-label {
1152
+ color: #a0aec0;
1153
+ }
1154
+ `;
1155
+ /**
1156
+ * Inject widget styles into the document head
1157
+ */
1158
+ function injectWidgetStyles() {
1159
+ // Check if styles are already injected
1160
+ if (document.querySelector('#cv-widget-styles'))
1161
+ return;
1162
+ const style = document.createElement('style');
1163
+ style.id = 'cv-widget-styles';
1164
+ style.textContent = WIDGET_STYLES;
1165
+ document.head.appendChild(style);
1166
+ }
1167
+
1168
+ class CustomViewsWidget {
1169
+ core;
1170
+ container;
1171
+ widgetIcon = null;
1172
+ options;
1173
+ // Modal state
1174
+ modal = null;
1175
+ constructor(options) {
1176
+ this.core = options.core;
1177
+ this.container = options.container || document.body;
1178
+ // Set defaults
1179
+ this.options = {
1180
+ core: options.core,
1181
+ container: this.container,
1182
+ position: options.position || 'middle-left',
1183
+ theme: options.theme || 'light',
1184
+ showReset: options.showReset ?? true,
1185
+ title: options.title || 'Custom Views',
1186
+ description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1187
+ showWelcome: options.showWelcome ?? false,
1188
+ welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1189
+ welcomeMessage: options.welcomeMessage || 'This website uses Custom Views to let you personalize your experience. Use the widget on the side (⚙) to show or hide different content sections based on your preferences. Your selections will be saved and can be shared via URL.'
1190
+ };
1191
+ // No external state manager to initialize
1192
+ }
1193
+ /**
1194
+ * Render the widget
1195
+ */
1196
+ render() {
1197
+ this.widgetIcon = this.createWidgetIcon();
1198
+ this.attachEventListeners();
1199
+ // Always append to body since it's a floating icon
1200
+ document.body.appendChild(this.widgetIcon);
1201
+ // Show welcome modal on first visit if enabled
1202
+ if (this.options.showWelcome) {
1203
+ this.showWelcomeModalIfFirstVisit();
1204
+ }
1205
+ return this.widgetIcon;
1206
+ }
1207
+ /**
1208
+ * Create the simple widget icon
1209
+ */
1210
+ createWidgetIcon() {
1211
+ const icon = document.createElement('div');
1212
+ icon.className = `cv-widget-icon cv-widget-${this.options.position}`;
1213
+ icon.innerHTML = '⚙';
1214
+ icon.title = this.options.title;
1215
+ icon.setAttribute('aria-label', 'Open Custom Views');
1216
+ // Add styles
1217
+ injectWidgetStyles();
1218
+ return icon;
1219
+ }
1220
+ /**
1221
+ * Remove the widget from DOM
1222
+ */
1223
+ destroy() {
1224
+ if (this.widgetIcon) {
1225
+ this.widgetIcon.remove();
1226
+ this.widgetIcon = null;
1227
+ }
1228
+ // Clean up modal
1229
+ if (this.modal) {
1230
+ this.modal.remove();
1231
+ this.modal = null;
1232
+ }
1233
+ }
1234
+ attachEventListeners() {
1235
+ if (!this.widgetIcon)
1236
+ return;
1237
+ // Click to open customization modal directly
1238
+ this.widgetIcon.addEventListener('click', () => this.openStateModal());
1239
+ }
1240
+ /**
1241
+ * Close the modal
1242
+ */
1243
+ closeModal() {
1244
+ if (this.modal) {
1245
+ this.modal.remove();
1246
+ this.modal = null;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Open the custom state creator
1251
+ */
1252
+ openStateModal() {
1253
+ // Get toggles from current configuration and open the modal regardless of count
1254
+ const localConfig = this.core.getLocalConfig();
1255
+ const toggles = localConfig?.allToggles || [];
1256
+ this.createCustomStateModal(toggles);
1257
+ }
1258
+ /**
1259
+ * Create the custom state creator modal
1260
+ */
1261
+ createCustomStateModal(toggles) {
1262
+ // Close existing modal
1263
+ this.closeModal();
1264
+ this.modal = document.createElement('div');
1265
+ this.modal.className = 'cv-widget-modal-overlay';
1266
+ this.applyThemeToModal();
1267
+ const toggleControls = toggles.length
1268
+ ? toggles.map(toggle => `
1269
+ <div class="cv-custom-state-toggle">
1270
+ <label>
1271
+ <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1272
+ ${this.formatToggleName(toggle)}
1273
+ </label>
1274
+ </div>
1275
+ `).join('')
1276
+ : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1277
+ this.modal.innerHTML = `
1278
+ <div class="cv-widget-modal cv-custom-state-modal">
1279
+ <div class="cv-widget-modal-header">
1280
+ <h3>Customize View</h3>
1281
+ <button class="cv-widget-modal-close" aria-label="Close modal">X</button>
1282
+ </div>
1283
+ <div class="cv-widget-modal-content">
1284
+ <div class="cv-custom-state-form">
1285
+ <p>${this.options.description}</p>
1286
+
1287
+ <h4>Content Sections</h4>
1288
+ <div class="cv-custom-toggles">
1289
+ ${toggleControls}
1290
+ </div>
1291
+
1292
+ <div class="cv-custom-state-actions">
1293
+ ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1294
+ <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
1295
+ </div>
1296
+ </div>
1297
+ </div>
1298
+ </div>
1299
+ `;
1300
+ document.body.appendChild(this.modal);
1301
+ this.attachStateModalEventListeners();
1302
+ // Load current state into form if we're already in a custom state
1303
+ this.loadCurrentStateIntoForm();
1304
+ }
1305
+ /**
1306
+ * Attach event listeners for custom state creator
1307
+ */
1308
+ attachStateModalEventListeners() {
1309
+ if (!this.modal)
1310
+ return;
1311
+ // Close button
1312
+ const closeBtn = this.modal.querySelector('.cv-widget-modal-close');
1313
+ if (closeBtn) {
1314
+ closeBtn.addEventListener('click', () => {
1315
+ this.closeModal();
1316
+ });
1317
+ }
1318
+ // Copy URL button
1319
+ const copyUrlBtn = this.modal.querySelector('.cv-custom-state-copy-url');
1320
+ if (copyUrlBtn) {
1321
+ copyUrlBtn.addEventListener('click', () => {
1322
+ this.copyShareableURL();
1323
+ });
1324
+ }
1325
+ // Reset to default button
1326
+ const resetBtn = this.modal.querySelector('.cv-custom-state-reset');
1327
+ if (resetBtn) {
1328
+ resetBtn.addEventListener('click', () => {
1329
+ this.core.resetToDefault();
1330
+ this.loadCurrentStateIntoForm();
1331
+ });
1332
+ }
1333
+ // Listen to toggle checkboxes
1334
+ const toggleCheckboxes = this.modal.querySelectorAll('.cv-custom-toggle-checkbox');
1335
+ toggleCheckboxes.forEach(checkbox => {
1336
+ checkbox.addEventListener('change', () => {
1337
+ const state = this.getCurrentCustomStateFromModal();
1338
+ this.core.applyState(state);
1339
+ });
1340
+ });
1341
+ // Overlay click to close
1342
+ this.modal.addEventListener('click', (e) => {
1343
+ if (e.target === this.modal) {
1344
+ this.closeModal();
1345
+ }
1346
+ });
1347
+ // Escape key to close
1348
+ const handleEscape = (e) => {
1349
+ if (e.key === 'Escape') {
1350
+ this.closeModal();
1351
+ document.removeEventListener('keydown', handleEscape);
1352
+ }
1353
+ };
1354
+ document.addEventListener('keydown', handleEscape);
1355
+ }
1356
+ /**
1357
+ * Apply theme class to the modal overlay based on options
1358
+ */
1359
+ applyThemeToModal() {
1360
+ if (!this.modal)
1361
+ return;
1362
+ if (this.options.theme === 'dark') {
1363
+ this.modal.classList.add('cv-widget-theme-dark');
1364
+ }
1365
+ else {
1366
+ this.modal.classList.remove('cv-widget-theme-dark');
1367
+ }
1368
+ }
1369
+ /**
1370
+ * Get current state from form values
1371
+ */
1372
+ getCurrentCustomStateFromModal() {
1373
+ if (!this.modal) {
1374
+ return { toggles: [] };
1375
+ }
1376
+ // Collect toggle values
1377
+ const toggles = [];
1378
+ const toggleCheckboxes = this.modal.querySelectorAll('.cv-custom-toggle-checkbox');
1379
+ toggleCheckboxes.forEach(checkbox => {
1380
+ const toggle = checkbox.dataset.toggle;
1381
+ if (toggle && checkbox.checked) {
1382
+ toggles.push(toggle);
1383
+ }
1384
+ });
1385
+ return { toggles };
1386
+ }
1387
+ /**
1388
+ * Copy shareable URL to clipboard
1389
+ */
1390
+ copyShareableURL() {
1391
+ const customState = this.getCurrentCustomStateFromModal();
1392
+ const url = URLStateManager.generateShareableURL(customState);
1393
+ navigator.clipboard.writeText(url).then(() => {
1394
+ console.log('Shareable URL copied to clipboard!');
1395
+ }).catch(() => { console.error('Failed to copy URL!'); });
1396
+ }
1397
+ /**
1398
+ * Load current state into form based on currently active toggles
1399
+ */
1400
+ loadCurrentStateIntoForm() {
1401
+ if (!this.modal)
1402
+ return;
1403
+ // Get currently active toggles (from custom state or default configuration)
1404
+ const activeToggles = this.core.getCurrentActiveToggles();
1405
+ // First, uncheck all checkboxes
1406
+ const allCheckboxes = this.modal.querySelectorAll('.cv-custom-toggle-checkbox');
1407
+ allCheckboxes.forEach(checkbox => {
1408
+ checkbox.checked = false;
1409
+ checkbox.disabled = false;
1410
+ checkbox.parentElement?.removeAttribute('aria-hidden');
1411
+ });
1412
+ // Then check the ones that should be active
1413
+ activeToggles.forEach(toggle => {
1414
+ const checkbox = this.modal?.querySelector(`[data-toggle="${toggle}"]`);
1415
+ if (checkbox) {
1416
+ if (!checkbox.disabled) {
1417
+ checkbox.checked = true;
1418
+ }
1419
+ }
1420
+ });
1421
+ }
1422
+ /**
1423
+ * Format toggle name for display
1424
+ */
1425
+ formatToggleName(toggle) {
1426
+ return toggle.charAt(0).toUpperCase() + toggle.slice(1);
1427
+ }
1428
+ /**
1429
+ * Check if this is the first visit and show welcome modal
1430
+ */
1431
+ showWelcomeModalIfFirstVisit() {
1432
+ const STORAGE_KEY = 'cv-welcome-shown';
1433
+ // Check if welcome has been shown before
1434
+ const hasSeenWelcome = localStorage.getItem(STORAGE_KEY);
1435
+ if (!hasSeenWelcome) {
1436
+ // Show welcome modal after a short delay to let the page settle
1437
+ setTimeout(() => {
1438
+ this.createWelcomeModal();
1439
+ }, 500);
1440
+ // Mark as shown
1441
+ localStorage.setItem(STORAGE_KEY, 'true');
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Create and show the welcome modal
1446
+ */
1447
+ createWelcomeModal() {
1448
+ // Don't show if there's already a modal open
1449
+ if (this.modal)
1450
+ return;
1451
+ this.modal = document.createElement('div');
1452
+ this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
1453
+ this.applyThemeToModal();
1454
+ this.modal.innerHTML = `
1455
+ <div class="cv-widget-modal cv-welcome-modal">
1456
+ <div class="cv-widget-modal-header">
1457
+ <h3>${this.options.welcomeTitle}</h3>
1458
+ <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
1459
+ </div>
1460
+ <div class="cv-widget-modal-content">
1461
+ <div class="cv-welcome-content">
1462
+ <p>${this.options.welcomeMessage}</p>
1463
+
1464
+ <div class="cv-welcome-widget-preview">
1465
+ <div class="cv-welcome-widget-icon">⚙</div>
1466
+ <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
1467
+ </div>
1468
+
1469
+ <button class="cv-welcome-got-it">Got it!</button>
1470
+ </div>
1471
+ </div>
1472
+ </div>
1473
+ `;
1474
+ document.body.appendChild(this.modal);
1475
+ this.attachWelcomeModalEventListeners();
1476
+ }
1477
+ /**
1478
+ * Attach event listeners for welcome modal
1479
+ */
1480
+ attachWelcomeModalEventListeners() {
1481
+ if (!this.modal)
1482
+ return;
1483
+ // Close button
1484
+ const closeBtn = this.modal.querySelector('.cv-widget-modal-close');
1485
+ if (closeBtn) {
1486
+ closeBtn.addEventListener('click', () => {
1487
+ this.closeModal();
1488
+ });
1489
+ }
1490
+ // Got it button
1491
+ const gotItBtn = this.modal.querySelector('.cv-welcome-got-it');
1492
+ if (gotItBtn) {
1493
+ gotItBtn.addEventListener('click', () => {
1494
+ this.closeModal();
1495
+ });
1496
+ }
1497
+ // Overlay click to close
1498
+ this.modal.addEventListener('click', (e) => {
1499
+ if (e.target === this.modal) {
1500
+ this.closeModal();
1501
+ }
1502
+ });
1503
+ // Escape key to close
1504
+ const handleEscape = (e) => {
1505
+ if (e.key === 'Escape') {
1506
+ this.closeModal();
1507
+ document.removeEventListener('keydown', handleEscape);
1508
+ }
1509
+ };
1510
+ document.addEventListener('keydown', handleEscape);
1511
+ }
1512
+ }
1513
+
1514
+ class CustomViews {
1515
+ // Entry Point to use CustomViews
1516
+ static async initFromJson(opts) {
1517
+ // Load assets JSON if provided
1518
+ let assetsManager;
1519
+ if (opts.assetsJsonPath) {
1520
+ const assetsJson = await (await fetch(opts.assetsJsonPath)).json();
1521
+ assetsManager = new AssetsManager(assetsJson);
1522
+ }
1523
+ else {
1524
+ assetsManager = new AssetsManager({});
1525
+ }
1526
+ // Load config JSON if provided, else just log error and don't load the custom views
1527
+ let localConfig;
1528
+ if (opts.config) {
1529
+ localConfig = opts.config;
1530
+ }
1531
+ else {
1532
+ if (!opts.configPath) {
1533
+ console.error("No config path provided, skipping custom views");
1534
+ return null;
1535
+ }
1536
+ try {
1537
+ localConfig = await (await fetch(opts.configPath)).json();
1538
+ }
1539
+ catch (error) {
1540
+ console.error("Error loading config:", error);
1541
+ return null;
1542
+ }
1543
+ }
1544
+ const coreOptions = {
1545
+ assetsManager,
1546
+ config: localConfig,
1547
+ rootEl: opts.rootEl,
1548
+ };
1549
+ const core = new CustomViewsCore(coreOptions);
1550
+ core.init();
1551
+ return core;
1552
+ }
1553
+ }
1554
+ if (typeof window !== "undefined") {
1555
+ // @ts-ignore
1556
+ window.CustomViews = CustomViews;
1557
+ // @ts-ignore
1558
+ window.CustomViewsWidget = CustomViewsWidget;
1559
+ }
1560
+
1561
+ exports.AssetsManager = AssetsManager;
1562
+ exports.CustomViews = CustomViews;
1563
+ exports.CustomViewsCore = CustomViewsCore;
1564
+ exports.CustomViewsWidget = CustomViewsWidget;
1565
+ exports.LocalConfig = Config;
1566
+ exports.PersistenceManager = PersistenceManager;
1567
+ exports.URLStateManager = URLStateManager;
1568
+
1569
+ }));
1570
+ //# sourceMappingURL=custom-views.umd.js.map