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