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