@customviews-js/customviews 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +362 -103
  2. package/dist/{custom-views.cjs.js → custom-views.core.cjs.js} +1708 -891
  3. package/dist/custom-views.core.cjs.js.map +1 -0
  4. package/dist/custom-views.core.esm.js +2413 -0
  5. package/dist/custom-views.core.esm.js.map +1 -0
  6. package/dist/custom-views.esm.js +1708 -890
  7. package/dist/custom-views.esm.js.map +1 -1
  8. package/dist/{custom-views.umd.js → custom-views.js} +1707 -890
  9. package/dist/custom-views.js.map +1 -0
  10. package/dist/custom-views.min.js +7 -0
  11. package/dist/custom-views.min.js.map +1 -0
  12. package/dist/types/{models/AssetsManager.d.ts → core/assets-manager.d.ts} +1 -1
  13. package/dist/types/{models/AssetsManager.d.ts.map → core/assets-manager.d.ts.map} +1 -1
  14. package/dist/types/core/core.d.ts +25 -9
  15. package/dist/types/core/core.d.ts.map +1 -1
  16. package/dist/types/core/custom-elements.d.ts +8 -0
  17. package/dist/types/core/custom-elements.d.ts.map +1 -0
  18. package/dist/types/core/render.d.ts +4 -1
  19. package/dist/types/core/render.d.ts.map +1 -1
  20. package/dist/types/core/tab-manager.d.ts +35 -0
  21. package/dist/types/core/tab-manager.d.ts.map +1 -0
  22. package/dist/types/core/toggle-manager.d.ts +28 -0
  23. package/dist/types/core/toggle-manager.d.ts.map +1 -0
  24. package/dist/types/core/url-state-manager.d.ts.map +1 -1
  25. package/dist/types/core/visibility-manager.d.ts +1 -1
  26. package/dist/types/core/widget.d.ts +2 -0
  27. package/dist/types/core/widget.d.ts.map +1 -1
  28. package/dist/types/entry/browser-entry.d.ts +13 -0
  29. package/dist/types/entry/browser-entry.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +11 -21
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/lib/custom-views.d.ts +29 -0
  33. package/dist/types/lib/custom-views.d.ts.map +1 -0
  34. package/dist/types/styles/styles.d.ts +2 -0
  35. package/dist/types/styles/styles.d.ts.map +1 -1
  36. package/dist/types/styles/tab-styles.d.ts +5 -0
  37. package/dist/types/styles/tab-styles.d.ts.map +1 -0
  38. package/dist/types/styles/toggle-styles.d.ts +5 -0
  39. package/dist/types/styles/toggle-styles.d.ts.map +1 -0
  40. package/dist/types/styles/widget-styles.d.ts +1 -1
  41. package/dist/types/styles/widget-styles.d.ts.map +1 -1
  42. package/dist/types/types/types.d.ts +85 -0
  43. package/dist/types/types/types.d.ts.map +1 -1
  44. package/dist/types/utils/url-utils.d.ts +8 -0
  45. package/dist/types/utils/url-utils.d.ts.map +1 -0
  46. package/package.json +61 -57
  47. package/dist/custom-views.cjs.js.map +0 -1
  48. package/dist/custom-views.umd.js.map +0 -1
  49. package/dist/custom-views.umd.min.js +0 -7
  50. package/dist/custom-views.umd.min.js.map +0 -1
  51. package/dist/types/models/Config.d.ts +0 -10
  52. package/dist/types/models/Config.d.ts.map +0 -1
@@ -1,99 +1,10 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.0.2
2
+ * @customviews-js/customviews v1.1.1
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
6
6
  'use strict';
7
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
8
  /**
98
9
  * Manages persistence of custom views state using browser localStorage
99
10
  */
@@ -223,6 +134,10 @@ class URLStateManager {
223
134
  const compact = {
224
135
  t: state.toggles
225
136
  };
137
+ // Add tab groups if present
138
+ if (state.tabs && Object.keys(state.tabs).length > 0) {
139
+ compact.g = Object.entries(state.tabs);
140
+ }
226
141
  // Convert to JSON and encode
227
142
  const json = JSON.stringify(compact);
228
143
  let encoded;
@@ -269,9 +184,19 @@ class URLStateManager {
269
184
  if (!compact || typeof compact !== 'object') {
270
185
  throw new Error('Invalid compact state structure');
271
186
  }
272
- return {
187
+ const state = {
273
188
  toggles: Array.isArray(compact.t) ? compact.t : []
274
189
  };
190
+ // Reconstruct tabs from compact format
191
+ if (Array.isArray(compact.g)) {
192
+ state.tabs = {};
193
+ for (const [groupId, tabId] of compact.g) {
194
+ if (typeof groupId === 'string' && typeof tabId === 'string') {
195
+ state.tabs[groupId] = tabId;
196
+ }
197
+ }
198
+ }
199
+ return state;
275
200
  }
276
201
  catch (error) {
277
202
  console.warn('Failed to decode view state:', error);
@@ -326,7 +251,7 @@ class VisibilityManager {
326
251
  }
327
252
  /**
328
253
  * Apply simple class-based visibility to a toggle element.
329
- * The element is assumed to have data-customviews-toggle.
254
+ * The element is assumed to have data-cv-toggle or data-customviews-toggle.
330
255
  */
331
256
  applyElementVisibility(el, visible) {
332
257
  if (visible) {
@@ -340,34 +265,581 @@ class VisibilityManager {
340
265
  }
341
266
  }
342
267
 
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
- }
268
+ /** --- Icon utilities --- */
269
+ function ensureFontAwesomeInjected() {
270
+ const isFontAwesomeLoaded = Array.from(document.styleSheets).some(sheet => sheet.href && (sheet.href.includes('font-awesome') || sheet.href.includes('fontawesome')));
271
+ if (isFontAwesomeLoaded)
272
+ return;
273
+ const link = document.createElement('link');
274
+ link.rel = 'stylesheet';
275
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
276
+ link.setAttribute('data-customviews-fontawesome', 'true');
277
+ document.head.appendChild(link);
278
+ }
279
+ function replaceIconShortcodes(text) {
280
+ // Handle Font Awesome shortcodes
281
+ return text.replace(/:fa-([\w-]+):/g, (_, icon) => `<i class="fa fa-${icon}"></i>`);
282
+ }
283
+ /** --- Basic renderers --- */
284
+ function renderImage(el, asset) {
285
+ if (!asset.src)
286
+ return;
287
+ el.innerHTML = '';
288
+ const img = document.createElement('img');
289
+ img.src = asset.src;
290
+ img.alt = asset.alt || '';
291
+ // Apply custom styling if provided
292
+ if (asset.className) {
293
+ img.className = asset.className;
294
+ }
295
+ if (asset.style) {
296
+ img.setAttribute('style', asset.style);
297
+ }
298
+ // Default styles (can be overridden by asset.style)
299
+ img.style.maxWidth = img.style.maxWidth || '100%';
300
+ img.style.height = img.style.height || 'auto';
301
+ img.style.display = img.style.display || 'block';
302
+ el.appendChild(img);
303
+ }
304
+ function renderText(el, asset) {
305
+ if (asset.content != null) {
306
+ el.textContent = asset.content;
307
+ }
308
+ // Apply custom styling if provided
309
+ if (asset.className) {
310
+ el.className = asset.className;
311
+ }
312
+ if (asset.style) {
313
+ el.setAttribute('style', asset.style);
314
+ }
315
+ }
316
+ function renderHtml(el, asset) {
317
+ if (asset.content != null) {
318
+ el.innerHTML = asset.content;
319
+ }
320
+ // Apply custom styling if provided
321
+ if (asset.className) {
322
+ el.className = asset.className;
323
+ }
324
+ if (asset.style) {
325
+ el.setAttribute('style', asset.style);
326
+ }
327
+ }
328
+ /** --- Unified asset renderer --- */
329
+ function detectAssetType(asset) {
330
+ // If src exists, it's an image
331
+ if (asset.src)
332
+ return 'image';
333
+ // If content contains HTML tags, it's HTML
334
+ if (asset.content && /<[^>]+>/.test(asset.content)) {
335
+ return 'html';
336
+ }
337
+ return 'text';
338
+ }
339
+ function renderAssetInto(el, assetId, assetsManager) {
340
+ const asset = assetsManager.get(assetId);
341
+ if (!asset)
342
+ return;
343
+ const type = asset.type || detectAssetType(asset);
344
+ switch (type) {
345
+ case 'image':
346
+ renderImage(el, asset);
347
+ break;
348
+ case 'text':
349
+ renderText(el, asset);
350
+ break;
351
+ case 'html':
352
+ renderHtml(el, asset);
353
+ break;
354
+ default:
355
+ el.innerHTML = asset.content || String(asset);
356
+ console.warn('[CustomViews] Unknown asset type:', type);
357
+ }
358
+ }
359
+
360
+ // Constants for selectors
361
+ const TABGROUP_SELECTOR = 'cv-tabgroup';
362
+ const TAB_SELECTOR = 'cv-tab';
363
+ const NAV_AUTO_SELECTOR = 'cv-tabgroup[nav="auto"], cv-tabgroup:not([nav])';
364
+ const NAV_CONTAINER_CLASS = 'cv-tabs-nav';
365
+ /**
366
+ * TabManager handles discovery, visibility, and navigation for tab groups and tabs
367
+ */
368
+ class TabManager {
369
+ /**
370
+ * Apply tab selections to all tab groups in the DOM
371
+ */
372
+ static applySelections(rootEl, tabs, cfgGroups) {
373
+ // Find all cv-tabgroup elements
374
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
375
+ tabGroups.forEach((groupEl) => {
376
+ const groupId = groupEl.getAttribute('id');
377
+ if (!groupId)
378
+ return;
379
+ // Determine the active tab for this group
380
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
381
+ // Apply visibility to direct child cv-tab elements only (not nested ones)
382
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
383
+ tabElements.forEach((tabEl) => {
384
+ const tabId = tabEl.getAttribute('id');
385
+ if (!tabId)
386
+ return;
387
+ const isActive = tabId === activeTabId;
388
+ this.applyTabVisibility(tabEl, isActive);
389
+ });
390
+ });
391
+ }
392
+ /**
393
+ * Resolve the active tab for a group based on state, config, and DOM
394
+ */
395
+ static resolveActiveTab(groupId, tabs, cfgGroups, groupEl) {
396
+ // 1. Check state
397
+ if (tabs[groupId]) {
398
+ return tabs[groupId];
399
+ }
400
+ // 2. Check config default
401
+ if (cfgGroups) {
402
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
403
+ if (groupCfg) {
404
+ if (groupCfg.default) {
405
+ return groupCfg.default;
406
+ }
407
+ // Fallback to first tab in config
408
+ const firstConfigTab = groupCfg.tabs[0];
409
+ if (firstConfigTab) {
410
+ return firstConfigTab.id;
411
+ }
412
+ }
413
+ }
414
+ // 3. Fallback to first direct cv-tab child in DOM
415
+ const firstTab = Array.from(groupEl.children).find((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
416
+ if (firstTab) {
417
+ return firstTab.getAttribute('id');
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * Apply visibility classes to a tab element
423
+ */
424
+ static applyTabVisibility(tabEl, isActive) {
425
+ if (isActive) {
426
+ tabEl.classList.remove('cv-hidden');
427
+ tabEl.classList.add('cv-visible');
428
+ }
429
+ else {
430
+ tabEl.classList.add('cv-hidden');
431
+ tabEl.classList.remove('cv-visible');
432
+ }
433
+ }
434
+ /**
435
+ * Build navigation for tab groups with nav="auto" (one-time setup)
436
+ */
437
+ static buildNavs(rootEl, cfgGroups, onTabClick) {
438
+ // Find all cv-tabgroup elements with nav="auto" or no nav attribute
439
+ const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
440
+ // Check if any tab headers contain Font Awesome shortcodes
441
+ // Inject Font Awesome CSS only if needed
442
+ let hasFontAwesomeShortcodes = false;
443
+ tabGroups.forEach((groupEl) => {
444
+ const groupId = groupEl.getAttribute('id');
445
+ if (!groupId)
446
+ return;
447
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === 'cv-tab');
448
+ tabElements.forEach((tabEl) => {
449
+ const tabId = tabEl.getAttribute('id');
450
+ if (!tabId)
451
+ return;
452
+ const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
453
+ if (/:fa-[\w-]+:/.test(header)) {
454
+ hasFontAwesomeShortcodes = true;
455
+ }
456
+ });
457
+ });
458
+ // Inject Font Awesome only if shortcodes are found
459
+ if (hasFontAwesomeShortcodes) {
460
+ ensureFontAwesomeInjected();
461
+ }
462
+ tabGroups.forEach((groupEl) => {
463
+ const groupId = groupEl.getAttribute('id');
464
+ if (!groupId)
465
+ return;
466
+ // Check if nav already exists - if so, skip building
467
+ let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
468
+ if (navContainer)
469
+ return; // Already built
470
+ // Get only direct child tabs (not nested ones)
471
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
472
+ if (tabElements.length === 0)
473
+ return;
474
+ // Create nav container
475
+ navContainer = document.createElement('ul');
476
+ navContainer.className = `${NAV_CONTAINER_CLASS} nav-tabs`;
477
+ navContainer.setAttribute('role', 'tablist');
478
+ groupEl.insertBefore(navContainer, groupEl.firstChild);
479
+ // Build nav items
480
+ tabElements.forEach((tabEl) => {
481
+ const tabId = tabEl.getAttribute('id');
482
+ if (!tabId)
483
+ return;
484
+ const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
485
+ const listItem = document.createElement('li');
486
+ listItem.className = 'nav-item';
487
+ const navLink = document.createElement('a');
488
+ navLink.className = 'nav-link';
489
+ // Replace icon shortcodes in header
490
+ navLink.innerHTML = replaceIconShortcodes(header);
491
+ navLink.href = '#';
492
+ navLink.setAttribute('data-tab-id', tabId);
493
+ navLink.setAttribute('data-group-id', groupId);
494
+ navLink.setAttribute('role', 'tab');
495
+ // Check if this tab is currently active
496
+ const isActive = tabEl.classList.contains('cv-visible');
497
+ if (isActive) {
498
+ navLink.classList.add('active');
499
+ navLink.setAttribute('aria-selected', 'true');
500
+ }
501
+ else {
502
+ navLink.setAttribute('aria-selected', 'false');
503
+ }
504
+ // Add click handler
505
+ if (onTabClick) {
506
+ navLink.addEventListener('click', (e) => {
507
+ e.preventDefault();
508
+ onTabClick(groupId, tabId);
509
+ });
510
+ }
511
+ listItem.appendChild(navLink);
512
+ navContainer.appendChild(listItem);
513
+ });
514
+ // Add bottom border line at the end of the tab group
515
+ const bottomBorder = document.createElement('div');
516
+ bottomBorder.className = 'cv-tabgroup-bottom-border';
517
+ groupEl.appendChild(bottomBorder);
518
+ });
519
+ }
520
+ /**
521
+ * Get tab label from config
522
+ */
523
+ static getTabLabel(tabId, groupId, cfgGroups) {
524
+ if (!cfgGroups)
525
+ return null;
526
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
527
+ if (!groupCfg)
528
+ return null;
529
+ const tabCfg = groupCfg.tabs.find(t => t.id === tabId);
530
+ return tabCfg?.label || null;
531
+ }
532
+ /**
533
+ * Update active state in navs after selection change (single group)
534
+ */
535
+ static updateNavActiveState(rootEl, groupId, activeTabId) {
536
+ const tabGroups = rootEl.querySelectorAll(`${TABGROUP_SELECTOR}[id="${groupId}"]`);
537
+ tabGroups.forEach((groupEl) => {
538
+ const navLinks = groupEl.querySelectorAll('.nav-link');
539
+ navLinks.forEach((link) => {
540
+ const tabId = link.getAttribute('data-tab-id');
541
+ if (tabId === activeTabId) {
542
+ link.classList.add('active');
543
+ link.setAttribute('aria-selected', 'true');
544
+ }
545
+ else {
546
+ link.classList.remove('active');
547
+ link.setAttribute('aria-selected', 'false');
548
+ }
549
+ });
550
+ });
551
+ }
552
+ /**
553
+ * Update active states for all tab groups based on current state
554
+ */
555
+ static updateAllNavActiveStates(rootEl, tabs, cfgGroups) {
556
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
557
+ tabGroups.forEach((groupEl) => {
558
+ const groupId = groupEl.getAttribute('id');
559
+ if (!groupId)
560
+ return;
561
+ // Determine the active tab for this group
562
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
563
+ if (!activeTabId)
564
+ return;
565
+ // Update nav links for this group
566
+ const navLinks = groupEl.querySelectorAll('.nav-link');
567
+ navLinks.forEach((link) => {
568
+ const tabId = link.getAttribute('data-tab-id');
569
+ if (tabId === activeTabId) {
570
+ link.classList.add('active');
571
+ link.setAttribute('aria-selected', 'true');
572
+ }
573
+ else {
574
+ link.classList.remove('active');
575
+ link.setAttribute('aria-selected', 'false');
576
+ }
577
+ });
578
+ });
579
+ }
580
+ }
581
+
582
+ class AssetsManager {
583
+ assets;
584
+ baseURL;
585
+ constructor(assets, baseURL = '') {
586
+ this.assets = assets;
587
+ this.baseURL = baseURL;
588
+ if (!this.validate()) {
589
+ console.warn('Invalid assets:', this.assets);
590
+ }
591
+ }
592
+ // Check each asset has content or src
593
+ validate() {
594
+ return Object.values(this.assets).every(a => a.src || a.content);
595
+ }
596
+ get(assetId) {
597
+ const asset = this.assets[assetId];
598
+ if (!asset)
599
+ return undefined;
600
+ // If there's a baseURL and the asset has a src property, prepend the baseURL
601
+ if (this.baseURL && asset.src) {
602
+ // Create a shallow copy to avoid mutating the original asset
603
+ return {
604
+ ...asset,
605
+ src: this.prependBaseURL(asset.src)
606
+ };
607
+ }
608
+ return asset;
609
+ }
610
+ prependBaseURL(path) {
611
+ // Don't prepend if the path is already absolute (starts with http:// or https://)
612
+ if (path.startsWith('http://') || path.startsWith('https://')) {
613
+ return path;
614
+ }
615
+ // Ensure baseURL doesn't end with / and path starts with /
616
+ const cleanBaseURL = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
617
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
618
+ return cleanBaseURL + cleanPath;
619
+ }
620
+ loadFromJSON(json) {
621
+ this.assets = json;
622
+ }
623
+ loadAdditionalAssets(additionalAssets) {
624
+ this.assets = { ...this.assets, ...additionalAssets };
625
+ }
626
+ }
627
+
628
+ // Constants for selectors
629
+ const TOGGLE_DATA_SELECTOR = "[data-cv-toggle], [data-customviews-toggle]";
630
+ const TOGGLE_ELEMENT_SELECTOR = "cv-toggle";
631
+ const TOGGLE_SELECTOR = `${TOGGLE_DATA_SELECTOR}, ${TOGGLE_ELEMENT_SELECTOR}`;
632
+ /**
633
+ * ToggleManager handles discovery, visibility, and asset rendering for toggle elements
634
+ */
635
+ class ToggleManager {
636
+ /**
637
+ * Apply toggle visibility to all toggle elements in the DOM
638
+ */
639
+ static applyToggles(rootEl, activeToggles) {
640
+ rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
641
+ const categories = this.getToggleCategories(el);
642
+ const shouldShow = categories.some(cat => activeToggles.includes(cat));
643
+ this.applyToggleVisibility(el, shouldShow);
644
+ });
645
+ }
646
+ /**
647
+ * Render assets into toggle elements that are currently visible
648
+ */
649
+ static renderAssets(rootEl, activeToggles, assetsManager) {
650
+ rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
651
+ const categories = this.getToggleCategories(el);
652
+ const toggleId = this.getToggleId(el);
653
+ if (toggleId && categories.some(cat => activeToggles.includes(cat))) {
654
+ renderAssetInto(el, toggleId, assetsManager);
655
+ }
656
+ });
657
+ }
658
+ /**
659
+ * Get toggle categories from an element (supports both data attributes and cv-toggle elements)
660
+ */
661
+ static getToggleCategories(el) {
662
+ if (el.tagName.toLowerCase() === 'cv-toggle') {
663
+ const category = el.getAttribute('category');
664
+ return (category || '').split(/\s+/).filter(Boolean);
665
+ }
666
+ else {
667
+ const data = el.dataset.cvToggle || el.dataset.customviewsToggle;
668
+ return (data || '').split(/\s+/).filter(Boolean);
669
+ }
670
+ }
671
+ /**
672
+ * Get toggle ID from an element
673
+ */
674
+ static getToggleId(el) {
675
+ return el.dataset.cvId || el.dataset.customviewsId || el.getAttribute('data-cv-id') || el.getAttribute('data-customviews-id') || undefined;
676
+ }
677
+ /**
678
+ * Apply simple class-based visibility to a toggle element
679
+ */
680
+ static applyToggleVisibility(el, visible) {
681
+ if (visible) {
682
+ el.classList.remove('cv-hidden');
683
+ el.classList.add('cv-visible');
684
+ }
685
+ else {
686
+ el.classList.add('cv-hidden');
687
+ el.classList.remove('cv-visible');
688
+ }
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Styles for toggle visibility and animations
694
+ */
695
+ const TOGGLE_STYLES = `
696
+ /* Core toggle visibility transitions */
697
+ [data-cv-toggle], [data-customviews-toggle], cv-toggle {
698
+ transition: opacity 150ms ease,
699
+ transform 150ms ease,
700
+ max-height 200ms ease,
701
+ margin 150ms ease;
702
+ will-change: opacity, transform, max-height, margin;
703
+ }
704
+
705
+ .cv-visible {
706
+ opacity: 1 !important;
707
+ transform: translateY(0) !important;
708
+ max-height: var(--cv-max-height, 9999px) !important;
709
+ }
710
+
711
+ .cv-hidden {
712
+ opacity: 0 !important;
713
+ transform: translateY(-4px) !important;
714
+ pointer-events: none !important;
715
+ padding-top: 0 !important;
716
+ padding-bottom: 0 !important;
717
+ border-top-width: 0 !important;
718
+ border-bottom-width: 0 !important;
719
+ max-height: 0 !important;
720
+ margin-top: 0 !important;
721
+ margin-bottom: 0 !important;
722
+ overflow: hidden !important;
723
+ }
724
+ `;
725
+
726
+ /**
727
+ * Styles for tab groups and tab navigation
728
+ */
729
+ const TAB_STYLES = `
730
+ /* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
731
+ .cv-tabs-nav {
732
+ display: flex;
733
+ flex-wrap: wrap;
734
+ padding-left: 0;
735
+ margin-top: 0.5rem;
736
+ margin-bottom: 1rem;
737
+ list-style: none;
738
+ border-bottom: 1px solid #dee2e6;
739
+ }
740
+
741
+ .cv-tabs-nav .nav-item {
742
+ margin-bottom: -1px;
743
+ list-style: none;
744
+ display: inline-block;
745
+ }
746
+
747
+ .cv-tabs-nav .nav-link {
748
+ display: block;
749
+ padding: 0.5rem 1rem;
750
+ color: #495057;
751
+ text-decoration: none;
752
+ background-color: transparent;
753
+ border: 1px solid transparent;
754
+ border-top-left-radius: 0.25rem;
755
+ border-top-right-radius: 0.25rem;
756
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
757
+ cursor: pointer;
758
+ }
759
+
760
+ .cv-tabs-nav .nav-link:hover,
761
+ .cv-tabs-nav .nav-link:focus {
762
+ border-color: #e9ecef #e9ecef #dee2e6;
763
+ isolation: isolate;
764
+ }
765
+
766
+ .cv-tabs-nav .nav-link.active {
767
+ color: #495057;
768
+ background-color: #fff;
769
+ border-color: #dee2e6 #dee2e6 #fff;
770
+ }
771
+
772
+ .cv-tabs-nav .nav-link:focus {
773
+ outline: 0;
774
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
775
+ }
776
+
777
+ /* Legacy button-based nav (deprecated, kept for compatibility) */
778
+ .cv-tabs-nav-item {
779
+ background: none;
780
+ border: none;
781
+ border-bottom: 2px solid transparent;
782
+ padding: 0.5rem 1rem;
783
+ cursor: pointer;
784
+ font-size: 1rem;
785
+ color: #6c757d;
786
+ transition: color 150ms ease, border-color 150ms ease;
787
+ }
788
+
789
+ .cv-tabs-nav-item:hover {
790
+ color: #495057;
791
+ border-bottom-color: #dee2e6;
792
+ }
793
+
794
+ .cv-tabs-nav-item.active {
795
+ color: #007bff;
796
+ border-bottom-color: #007bff;
797
+ font-weight: 500;
798
+ }
799
+
800
+ .cv-tabs-nav-item:focus {
801
+ outline: 2px solid #007bff;
802
+ outline-offset: 2px;
803
+ }
804
+
805
+ /* Tab panel base styles */
806
+ cv-tab {
807
+ display: block;
808
+ }
809
+
810
+ /* Override visibility for tab panels - use display instead of collapse animation */
811
+ cv-tab.cv-hidden {
812
+ display: none !important;
813
+ }
814
+
815
+ cv-tab.cv-visible {
816
+ display: block !important;
817
+ }
818
+
819
+ cv-tabgroup {
820
+ display: block;
821
+ margin-bottom: 1.5rem;
822
+ }
823
+
824
+ /* Bottom border line for tab groups */
825
+ .cv-tabgroup-bottom-border {
826
+ border-bottom: 1px solid #dee2e6;
827
+ margin-top: 1rem;
828
+ }
829
+
830
+ /* Tab content wrapper */
831
+ .cv-tab-content {
832
+ padding: 1rem 0;
833
+ }
834
+ `;
835
+
836
+ /**
837
+ * Combined core styles for toggles and tabs
838
+ */
839
+ const CORE_STYLES = `
840
+ ${TOGGLE_STYLES}
841
+
842
+ ${TAB_STYLES}
371
843
  `;
372
844
  /**
373
845
  * Add styles for hiding and showing toggles animations and transitions to the document head
@@ -388,36 +860,85 @@ class CustomViewsCore {
388
860
  assetsManager;
389
861
  persistenceManager;
390
862
  visibilityManager;
391
- stateFromUrl = null;
392
- localConfig;
863
+ config;
393
864
  stateChangeListeners = [];
865
+ showUrlEnabled;
866
+ lastAppliedState = null;
394
867
  constructor(opt) {
395
868
  this.assetsManager = opt.assetsManager;
396
- this.localConfig = opt.config;
869
+ this.config = opt.config;
397
870
  this.rootEl = opt.rootEl || document.body;
398
871
  this.persistenceManager = new PersistenceManager();
399
872
  this.visibilityManager = new VisibilityManager();
873
+ this.showUrlEnabled = opt.showUrl ?? true;
874
+ this.lastAppliedState = this.cloneState(this.config?.defaultState);
875
+ }
876
+ getConfig() {
877
+ return this.config;
400
878
  }
401
- getLocalConfig() {
402
- return this.localConfig;
879
+ /**
880
+ * Get tab groups from config
881
+ */
882
+ getTabGroups() {
883
+ return this.config.tabGroups;
884
+ }
885
+ /**
886
+ * Get currently active tabs (from URL > persisted (localStorage) > defaults)
887
+ */
888
+ getCurrentActiveTabs() {
889
+ if (this.lastAppliedState?.tabs) {
890
+ return { ...this.lastAppliedState.tabs };
891
+ }
892
+ const persistedState = this.persistenceManager.getPersistedState();
893
+ if (persistedState?.tabs) {
894
+ return { ...persistedState.tabs };
895
+ }
896
+ return this.config?.defaultState?.tabs ? { ...this.config.defaultState.tabs } : {};
897
+ }
898
+ /**
899
+ * Set active tab for a group and apply state
900
+ */
901
+ setActiveTab(groupId, tabId) {
902
+ // Get current state
903
+ const currentToggles = this.getCurrentActiveToggles();
904
+ const currentTabs = this.getCurrentActiveTabs();
905
+ // Merge new tab selection
906
+ const newTabs = { ...currentTabs, [groupId]: tabId };
907
+ // Create new state
908
+ const newState = {
909
+ toggles: currentToggles,
910
+ tabs: newTabs
911
+ };
912
+ // Apply the state
913
+ this.applyState(newState);
914
+ // Emit custom event
915
+ const event = new CustomEvent('customviews:tab-change', {
916
+ detail: { groupId, tabId },
917
+ bubbles: true
918
+ });
919
+ document.dispatchEvent(event);
403
920
  }
404
921
  // Inject styles, setup listeners and call rendering logic
405
922
  async init() {
406
923
  injectCoreStyles();
924
+ // Build navigation once (with click handlers)
925
+ TabManager.buildNavs(this.rootEl, this.config.tabGroups, (groupId, tabId) => {
926
+ this.setActiveTab(groupId, tabId);
927
+ });
407
928
  // For session history, clicks on back/forward button
408
929
  window.addEventListener("popstate", () => {
409
- this.loadAndRenderState();
930
+ this.loadAndCallApplyState();
410
931
  });
411
- this.loadAndRenderState();
932
+ this.loadAndCallApplyState();
412
933
  }
413
934
  // Priority: URL state > persisted state > default
414
935
  // Also filters using the visibility manager to persist selection
415
936
  // across back/forward button clicks
416
- async loadAndRenderState() {
937
+ async loadAndCallApplyState() {
417
938
  // 1. URL State
418
- this.stateFromUrl = URLStateManager.parseURL();
419
- if (this.stateFromUrl) {
420
- this.applyState(this.stateFromUrl);
939
+ const urlState = URLStateManager.parseURL();
940
+ if (urlState) {
941
+ this.applyState(urlState);
421
942
  return;
422
943
  }
423
944
  // 2. Persisted State
@@ -427,38 +948,35 @@ class CustomViewsCore {
427
948
  return;
428
949
  }
429
950
  // 3. Local Config Fallback
430
- this.renderState(this.localConfig.defaultState);
951
+ this.renderState(this.config.defaultState);
431
952
  }
432
953
  /**
433
- * Apply a custom state, saves to localStorage and updates the URL
434
- */
954
+ * Apply a custom state, saves to localStorage and updates the URL
955
+ */
435
956
  applyState(state) {
436
- this.renderState(state);
437
- this.persistenceManager.persistState(state);
438
- this.stateFromUrl = state;
439
- URLStateManager.updateURL(state);
957
+ const snapshot = this.cloneState(state);
958
+ this.renderState(snapshot);
959
+ this.persistenceManager.persistState(snapshot);
960
+ if (this.showUrlEnabled) {
961
+ URLStateManager.updateURL(snapshot);
962
+ }
963
+ else {
964
+ URLStateManager.clearURL();
965
+ }
440
966
  }
441
967
  /** Render all toggles for the current state */
442
968
  renderState(state) {
969
+ this.lastAppliedState = this.cloneState(state);
443
970
  const toggles = state.toggles || [];
444
971
  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
- }
972
+ // Apply toggle visibility
973
+ ToggleManager.applyToggles(this.rootEl, finalToggles);
974
+ // Render assets into toggles
975
+ ToggleManager.renderAssets(this.rootEl, finalToggles, this.assetsManager);
976
+ // Apply tab selections
977
+ TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
978
+ // Update nav active states (without rebuilding)
979
+ TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
462
980
  // Notify state change listeners (like widgets)
463
981
  this.notifyStateChangeListeners();
464
982
  }
@@ -466,10 +984,9 @@ class CustomViewsCore {
466
984
  * Reset to default state
467
985
  */
468
986
  resetToDefault() {
469
- this.stateFromUrl = null;
470
987
  this.persistenceManager.clearAll();
471
- if (this.localConfig) {
472
- this.renderState(this.localConfig.defaultState);
988
+ if (this.config) {
989
+ this.renderState(this.config.defaultState);
473
990
  }
474
991
  else {
475
992
  console.warn("No configuration loaded, cannot reset to default state");
@@ -481,15 +998,12 @@ class CustomViewsCore {
481
998
  * Get the currently active toggles regardless of whether they come from custom state or default configuration
482
999
  */
483
1000
  getCurrentActiveToggles() {
484
- // If we have a custom state, return its toggles
485
- if (this.stateFromUrl) {
486
- return this.stateFromUrl.toggles || [];
1001
+ if (this.lastAppliedState) {
1002
+ return this.lastAppliedState.toggles || [];
487
1003
  }
488
- // Otherwise, if we have local config, return its default state toggles
489
- if (this.localConfig) {
490
- return this.localConfig.defaultState.toggles || [];
1004
+ if (this.config) {
1005
+ return this.config.defaultState.toggles || [];
491
1006
  }
492
- // No configuration or state
493
1007
  return [];
494
1008
  }
495
1009
  /**
@@ -497,15 +1011,35 @@ class CustomViewsCore {
497
1011
  */
498
1012
  clearPersistence() {
499
1013
  this.persistenceManager.clearAll();
500
- this.stateFromUrl = null;
501
- if (this.localConfig) {
502
- this.renderState(this.localConfig.defaultState);
1014
+ if (this.config) {
1015
+ this.renderState(this.config.defaultState);
503
1016
  }
504
1017
  else {
505
1018
  console.warn("No configuration loaded, cannot reset to default state");
506
1019
  }
507
1020
  URLStateManager.clearURL();
508
1021
  }
1022
+ setOption(flag, value) {
1023
+ switch (flag) {
1024
+ case 'showUrl': {
1025
+ const nextValue = Boolean(value);
1026
+ if (this.showUrlEnabled === nextValue) {
1027
+ return;
1028
+ }
1029
+ this.showUrlEnabled = nextValue;
1030
+ if (nextValue) {
1031
+ const stateForUrl = this.getTrackedStateSnapshot();
1032
+ URLStateManager.updateURL(stateForUrl);
1033
+ }
1034
+ else {
1035
+ URLStateManager.clearURL();
1036
+ }
1037
+ break;
1038
+ }
1039
+ default:
1040
+ console.warn(`[CustomViews] Unknown option '${flag}' passed to setOption`);
1041
+ }
1042
+ }
509
1043
  // === STATE CHANGE LISTENER METHODS ===
510
1044
  /**
511
1045
  * Add a listener that will be called whenever the state changes
@@ -535,51 +1069,136 @@ class CustomViewsCore {
535
1069
  }
536
1070
  });
537
1071
  }
1072
+ cloneState(state) {
1073
+ const toggles = state?.toggles ? [...state.toggles] : [];
1074
+ const tabs = state?.tabs ? { ...state.tabs } : undefined;
1075
+ return tabs ? { toggles, tabs } : { toggles };
1076
+ }
1077
+ getTrackedStateSnapshot() {
1078
+ if (this.lastAppliedState) {
1079
+ return this.cloneState(this.lastAppliedState);
1080
+ }
1081
+ if (this.config) {
1082
+ return this.cloneState(this.config.defaultState);
1083
+ }
1084
+ return { toggles: [] };
1085
+ }
538
1086
  }
539
1087
 
540
- class AssetsManager {
541
- assets;
542
- baseURL;
543
- constructor(assets, baseURL = '') {
544
- this.assets = assets;
545
- this.baseURL = baseURL;
546
- if (!this.validate()) {
547
- console.warn('Invalid assets:', this.assets);
548
- }
1088
+ /**
1089
+ * Helper function to prepend baseUrl to a path
1090
+ * @param path The path to prepend the baseUrl to
1091
+ * @param baseUrl The base URL to prepend
1092
+ * @returns The full URL with baseUrl prepended if applicable
1093
+ */
1094
+ function prependBaseUrl(path, baseUrl) {
1095
+ if (!baseUrl)
1096
+ return path;
1097
+ // Don't prepend if the path is already absolute (starts with http:// or https://)
1098
+ if (path.startsWith('http://') || path.startsWith('https://')) {
1099
+ return path;
1100
+ }
1101
+ // Ensure baseUrl doesn't end with / and path starts with /
1102
+ const cleanbaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
1103
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
1104
+ return cleanbaseUrl + cleanPath;
1105
+ }
1106
+
1107
+ /**
1108
+ * Custom Elements for Tab Groups and Tabs
1109
+ */
1110
+ /**
1111
+ * <cv-tab> element - represents a single tab panel
1112
+ */
1113
+ class CVTab extends HTMLElement {
1114
+ connectedCallback() {
1115
+ // Element is managed by TabManager
549
1116
  }
550
- // Check each asset has content or src
551
- validate() {
552
- return Object.values(this.assets).every(a => a.src || a.content);
1117
+ }
1118
+ /**
1119
+ * <cv-tabgroup> element - represents a group of tabs
1120
+ */
1121
+ class CVTabgroup extends HTMLElement {
1122
+ connectedCallback() {
1123
+ // Element is managed by TabManager
1124
+ // Emit ready event after a brief delay to ensure children are parsed
1125
+ setTimeout(() => {
1126
+ const event = new CustomEvent('cv:tabgroup-ready', {
1127
+ bubbles: true,
1128
+ detail: { groupId: this.getAttribute('id') }
1129
+ });
1130
+ this.dispatchEvent(event);
1131
+ }, 0);
553
1132
  }
554
- get(assetId) {
555
- const asset = this.assets[assetId];
556
- if (!asset)
557
- return undefined;
558
- // If there's a baseURL and the asset has a src property, prepend the baseURL
559
- if (this.baseURL && asset.src) {
560
- // Create a shallow copy to avoid mutating the original asset
561
- return {
562
- ...asset,
563
- src: this.prependBaseURL(asset.src)
564
- };
565
- }
566
- return asset;
1133
+ }
1134
+ /**
1135
+ * <cv-toggle> element - represents a toggleable content block
1136
+ */
1137
+ class CVToggle extends HTMLElement {
1138
+ connectedCallback() {
1139
+ // Element is managed by Core
567
1140
  }
568
- prependBaseURL(path) {
569
- // Don't prepend if the path is already absolute (starts with http:// or https://)
570
- if (path.startsWith('http://') || path.startsWith('https://')) {
571
- return path;
1141
+ }
1142
+ /**
1143
+ * Register custom elements
1144
+ */
1145
+ function registerCustomElements() {
1146
+ // Only register if not already defined
1147
+ if (!customElements.get('cv-tab')) {
1148
+ customElements.define('cv-tab', CVTab);
1149
+ }
1150
+ if (!customElements.get('cv-tabgroup')) {
1151
+ customElements.define('cv-tabgroup', CVTabgroup);
1152
+ }
1153
+ if (!customElements.get('cv-toggle')) {
1154
+ customElements.define('cv-toggle', CVToggle);
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Main CustomViews class for initializing and managing custom views
1160
+ */
1161
+ class CustomViews {
1162
+ /**
1163
+ * Entry Point to use CustomViews
1164
+ * @param opts Initialization options including config object and assets path
1165
+ * @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
1166
+ */
1167
+ static async init(opts) {
1168
+ // Register custom elements
1169
+ registerCustomElements();
1170
+ // Load assets JSON if provided
1171
+ let assetsManager;
1172
+ const baseURL = opts.baseURL || '';
1173
+ if (opts.assetsJsonPath) {
1174
+ const assetsPath = prependBaseUrl(opts.assetsJsonPath, baseURL);
1175
+ const assetsJson = await (await fetch(assetsPath)).json();
1176
+ assetsManager = new AssetsManager(assetsJson, baseURL);
1177
+ }
1178
+ else {
1179
+ assetsManager = new AssetsManager({}, baseURL);
572
1180
  }
573
- // Ensure baseURL doesn't end with / and path starts with /
574
- const cleanBaseURL = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
575
- const cleanPath = path.startsWith('/') ? path : '/' + path;
576
- return cleanBaseURL + cleanPath;
577
- }
578
- loadFromJSON(json) {
579
- this.assets = json;
580
- }
581
- loadAdditionalAssets(additionalAssets) {
582
- this.assets = { ...this.assets, ...additionalAssets };
1181
+ // Use provided config or create a minimal default one
1182
+ let config;
1183
+ if (opts.config) {
1184
+ config = opts.config;
1185
+ }
1186
+ else {
1187
+ console.error("No config provided, using minimal default config");
1188
+ // Create a minimal default config
1189
+ config = { allToggles: [], defaultState: { toggles: [] } };
1190
+ }
1191
+ const coreOptions = {
1192
+ assetsManager,
1193
+ config: config,
1194
+ rootEl: opts.rootEl,
1195
+ };
1196
+ if (opts.showUrl !== undefined) {
1197
+ coreOptions.showUrl = opts.showUrl;
1198
+ }
1199
+ const core = new CustomViewsCore(coreOptions);
1200
+ core.init();
1201
+ return core;
583
1202
  }
584
1203
  }
585
1204
 
@@ -590,586 +1209,644 @@ class AssetsManager {
590
1209
  * Note: Styles are kept as a TypeScript string for compatibility with the build system.
591
1210
  * This approach ensures the styles are properly bundled and don't require separate CSS file handling.
592
1211
  */
593
- const WIDGET_STYLES = `
594
- /* Rounded rectangle widget icon styles */
595
- .cv-widget-icon {
596
- position: fixed;
597
- background: white;
598
- color: black;
599
- display: flex;
600
- align-items: center;
601
- justify-content: center;
602
- font-size: 18px;
603
- font-weight: bold;
604
- cursor: pointer;
605
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
606
- z-index: 9998;
607
- transition: all 0.3s ease;
608
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
609
- }
610
-
611
- .cv-widget-icon:hover {
612
- background: white;
613
- color: black;
614
- }
615
-
616
- /* Top-right: rounded end on left, sticks out leftward on hover */
617
- .cv-widget-top-right {
618
- top: 20px;
619
- right: 0;
620
- border-radius: 18px 0 0 18px;
621
- padding-left: 8px;
622
- justify-content: flex-start;
623
- }
624
-
625
- /* Top-left: rounded end on right, sticks out rightward on hover */
626
- .cv-widget-top-left {
627
- top: 20px;
628
- left: 0;
629
- border-radius: 0 18px 18px 0;
630
- padding-right: 8px;
631
- justify-content: flex-end;
632
- }
633
-
634
- /* Bottom-right: rounded end on left, sticks out leftward on hover */
635
- .cv-widget-bottom-right {
636
- bottom: 20px;
637
- right: 0;
638
- border-radius: 18px 0 0 18px;
639
- padding-left: 8px;
640
- justify-content: flex-start;
641
- }
642
-
643
- /* Bottom-left: rounded end on right, sticks out rightward on hover */
644
- .cv-widget-bottom-left {
645
- bottom: 20px;
646
- left: 0;
647
- border-radius: 0 18px 18px 0;
648
- padding-right: 8px;
649
- justify-content: flex-end;
650
- }
651
-
652
- /* Middle-left: rounded end on right, sticks out rightward on hover */
653
- .cv-widget-middle-left {
654
- top: 50%;
655
- left: 0;
656
- transform: translateY(-50%);
657
- border-radius: 0 18px 18px 0;
658
- padding-right: 8px;
659
- justify-content: flex-end;
660
- }
661
-
662
- /* Middle-right: rounded end on left, sticks out leftward on hover */
663
- .cv-widget-middle-right {
664
- top: 50%;
665
- right: 0;
666
- transform: translateY(-50%);
667
- border-radius: 18px 0 0 18px;
668
- padding-left: 8px;
669
- justify-content: flex-start;
670
- }
671
-
672
- .cv-widget-top-right,
673
- .cv-widget-middle-right,
674
- .cv-widget-bottom-right,
675
- .cv-widget-top-left,
676
- .cv-widget-middle-left,
677
- .cv-widget-bottom-left {
678
- height: 36px;
679
- width: 36px;
680
- }
681
-
682
- .cv-widget-middle-right:hover,
683
- .cv-widget-top-right:hover,
684
- .cv-widget-bottom-right:hover,
685
- .cv-widget-top-left:hover,
686
- .cv-widget-middle-left:hover,
687
- .cv-widget-bottom-left:hover {
688
- width: 55px;
689
- }
690
-
691
- /* Modal content styles */
692
- .cv-widget-section {
693
- margin-bottom: 16px;
694
- }
695
-
696
- .cv-widget-section:last-child {
697
- margin-bottom: 0;
698
- }
699
-
700
- .cv-widget-section label {
701
- display: block;
702
- margin-bottom: 4px;
703
- font-weight: 500;
704
- color: #555;
705
- }
706
-
707
- .cv-widget-profile-select,
708
- .cv-widget-state-select {
709
- width: 100%;
710
- padding: 8px 12px;
711
- border: 1px solid #ddd;
712
- border-radius: 4px;
713
- background: white;
714
- font-size: 14px;
715
- }
716
-
717
- .cv-widget-profile-select:focus,
718
- .cv-widget-state-select:focus {
719
- outline: none;
720
- border-color: #007bff;
721
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
722
- }
723
-
724
- .cv-widget-profile-select:disabled,
725
- .cv-widget-state-select:disabled {
726
- background: #f8f9fa;
727
- color: #6c757d;
728
- cursor: not-allowed;
729
- }
730
-
731
- .cv-widget-current {
732
- margin: 16px 0;
733
- padding: 12px;
734
- background: #f8f9fa;
735
- border-radius: 4px;
736
- border-left: 4px solid #007bff;
737
- }
738
-
739
- .cv-widget-current label {
740
- font-size: 12px;
741
- text-transform: uppercase;
742
- letter-spacing: 0.5px;
743
- color: #666;
744
- margin-bottom: 4px;
745
- }
746
-
747
- .cv-widget-current-view {
748
- font-weight: 500;
749
- color: #333;
750
- }
751
-
752
- .cv-widget-reset {
753
- width: 100%;
754
- padding: 8px 16px;
755
- background: #dc3545;
756
- color: white;
757
- border: none;
758
- border-radius: 4px;
759
- cursor: pointer;
760
- font-size: 14px;
761
- font-weight: 500;
762
- }
763
-
764
- .cv-widget-reset:hover {
765
- background: #c82333;
766
- }
767
-
768
- .cv-widget-reset:active {
769
- background: #bd2130;
770
- }
771
-
772
- /* Responsive design for mobile */
773
- @media (max-width: 768px) {
774
- .cv-widget-top-right,
775
- .cv-widget-top-left {
776
- top: 10px;
777
- }
778
-
779
- .cv-widget-bottom-right,
780
- .cv-widget-bottom-left {
781
- bottom: 10px;
782
- }
783
-
784
- /* All widgets stay flush with screen edges */
785
- .cv-widget-top-right,
786
- .cv-widget-bottom-right,
787
- .cv-widget-middle-right {
788
- right: 0;
789
- }
790
-
791
- .cv-widget-top-left,
792
- .cv-widget-bottom-left,
793
- .cv-widget-middle-left {
794
- left: 0;
795
- }
796
-
797
- /* Slightly smaller on mobile */
798
- .cv-widget-icon {
799
- width: 60px;
800
- height: 32px;
801
- }
802
-
803
- .cv-widget-icon:hover {
804
- width: 75px;
805
- }
806
- }
807
-
808
- /* Modal styles */
809
- .cv-widget-modal-overlay {
810
- position: fixed;
811
- top: 0;
812
- left: 0;
813
- right: 0;
814
- bottom: 0;
815
- background: rgba(0, 0, 0, 0.5);
816
- display: flex;
817
- align-items: center;
818
- justify-content: center;
819
- z-index: 10002;
820
- animation: fadeIn 0.2s ease;
821
- }
822
-
823
- @keyframes fadeIn {
824
- from { opacity: 0; }
825
- to { opacity: 1; }
826
- }
827
-
828
- .cv-widget-modal {
829
- background: white;
830
- border-radius: 8px;
831
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
832
- max-width: 400px;
833
- width: 90vw;
834
- max-height: 80vh;
835
- overflow-y: auto;
836
- animation: slideIn 0.2s ease;
837
- }
838
-
839
- @keyframes slideIn {
840
- from {
841
- opacity: 0;
842
- transform: scale(0.9) translateY(-20px);
843
- }
844
- to {
845
- opacity: 1;
846
- transform: scale(1) translateY(0);
847
- }
848
- }
849
-
850
- .cv-widget-modal-header {
851
- display: flex;
852
- justify-content: space-between;
853
- align-items: center;
854
- padding: 16px 20px;
855
- border-bottom: 1px solid #e9ecef;
856
- background: #f8f9fa;
857
- border-radius: 8px 8px 0 0;
858
- }
859
-
860
- .cv-widget-modal-header h3 {
861
- margin: 0;
862
- font-size: 18px;
863
- font-weight: 600;
864
- color: #333;
865
- }
866
-
867
- .cv-widget-modal-close {
868
- background: none;
869
- border: none;
870
- font-size: 24px;
871
- cursor: pointer;
872
- padding: 0;
873
- width: 32px;
874
- height: 32px;
875
- display: flex;
876
- align-items: center;
877
- justify-content: center;
878
- border-radius: 4px;
879
- color: #666;
880
- }
881
-
882
- .cv-widget-modal-close:hover {
883
- background: #e9ecef;
884
- }
885
-
886
- .cv-widget-modal-content {
887
- padding: 20px;
888
- }
889
-
890
- .cv-widget-modal-actions {
891
- margin-top: 20px;
892
- padding-top: 16px;
893
- border-top: 1px solid #e9ecef;
894
- }
895
-
896
- .cv-widget-restore {
897
- width: 100%;
898
- padding: 10px 16px;
899
- background: #28a745;
900
- color: white;
901
- border: none;
902
- border-radius: 4px;
903
- cursor: pointer;
904
- font-size: 14px;
905
- font-weight: 500;
906
- }
907
-
908
- .cv-widget-restore:hover {
909
- background: #218838;
910
- }
911
-
912
- .cv-widget-create-state {
913
- width: 100%;
914
- padding: 10px 16px;
915
- background: #007bff;
916
- color: white;
917
- border: none;
918
- border-radius: 4px;
919
- cursor: pointer;
920
- font-size: 14px;
921
- font-weight: 500;
922
- margin-bottom: 10px;
923
- }
924
-
925
- .cv-widget-create-state:hover {
926
- background: #0056b3;
927
- }
928
-
929
- /* Dark theme modal styles */
930
- .cv-widget-theme-dark .cv-widget-modal {
931
- background: #2d3748;
932
- color: #e2e8f0;
933
- }
934
-
935
- .cv-widget-theme-dark .cv-widget-modal-header {
936
- background: #1a202c;
937
- border-color: #4a5568;
938
- }
939
-
940
- .cv-widget-theme-dark .cv-widget-modal-header h3 {
941
- color: #e2e8f0;
942
- }
943
-
944
- .cv-widget-theme-dark .cv-widget-modal-close {
945
- color: #a0aec0;
946
- }
947
-
948
- .cv-widget-theme-dark .cv-widget-modal-close:hover {
949
- background: #4a5568;
950
- }
951
-
952
- .cv-widget-theme-dark .cv-widget-modal-actions {
953
- border-color: #4a5568;
954
- }
955
-
956
- /* Custom state creator styles */
957
- .cv-custom-state-modal {
958
- max-width: 500px;
959
- }
960
-
961
- .cv-custom-state-form h4 {
962
- margin: 20px 0 10px 0;
963
- font-size: 16px;
964
- font-weight: 600;
965
- color: #333;
966
- border-bottom: 1px solid #e9ecef;
967
- padding-bottom: 5px;
968
- }
969
-
970
- .cv-custom-state-section {
971
- margin-bottom: 16px;
972
- }
973
-
974
- .cv-custom-state-section label {
975
- display: block;
976
- margin-bottom: 4px;
977
- font-weight: 500;
978
- color: #555;
979
- }
980
-
981
- .cv-custom-state-input {
982
- width: 100%;
983
- padding: 8px 12px;
984
- border: 1px solid #ddd;
985
- border-radius: 4px;
986
- background: white;
987
- font-size: 14px;
988
- }
989
-
990
- .cv-custom-state-input:focus {
991
- outline: none;
992
- border-color: #007bff;
993
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
994
- }
995
-
996
- .cv-custom-toggles {
997
- display: grid;
998
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
999
- gap: 10px;
1000
- }
1001
-
1002
- .cv-custom-state-toggle {
1003
- display: flex;
1004
- align-items: center;
1005
- }
1006
-
1007
- .cv-custom-state-toggle label {
1008
- display: flex;
1009
- align-items: center;
1010
- cursor: pointer;
1011
- font-weight: normal;
1012
- margin: 0;
1013
- }
1014
-
1015
- .cv-custom-toggle-checkbox {
1016
- margin-right: 8px;
1017
- width: auto;
1018
- }
1019
-
1020
- .cv-custom-state-actions {
1021
- display: flex;
1022
- gap: 10px;
1023
- margin-top: 20px;
1024
- padding-top: 16px;
1025
- border-top: 1px solid #e9ecef;
1026
- }
1027
-
1028
- .cv-custom-state-cancel,
1029
- .cv-custom-state-copy-url {
1030
- flex: 1;
1031
- padding: 10px 16px;
1032
- border: none;
1033
- border-radius: 4px;
1034
- cursor: pointer;
1035
- font-size: 14px;
1036
- font-weight: 500;
1037
- }
1038
-
1039
- .cv-custom-state-reset {
1040
- flex: 1;
1041
- padding: 10px 16px;
1042
- border: none;
1043
- border-radius: 4px;
1044
- cursor: pointer;
1045
- font-size: 14px;
1046
- font-weight: 500;
1047
- background: #dc3545;
1048
- color: white;
1049
- }
1050
-
1051
- .cv-custom-state-reset:hover {
1052
- background: #c82333;
1053
- }
1054
-
1055
- .cv-custom-state-cancel {
1056
- background: #6c757d;
1057
- color: white;
1058
- }
1059
-
1060
- .cv-custom-state-cancel:hover {
1061
- background: #5a6268;
1062
- }
1063
-
1064
- .cv-custom-state-copy-url {
1065
- background: #28a745;
1066
- color: white;
1067
- }
1068
-
1069
- .cv-custom-state-copy-url:hover {
1070
- background: #218838;
1071
- }
1072
-
1073
- /* Dark theme custom state styles */
1074
- .cv-widget-theme-dark .cv-custom-state-form h4 {
1075
- color: #e2e8f0;
1076
- border-color: #4a5568;
1077
- }
1078
-
1079
- .cv-widget-theme-dark .cv-custom-state-section label {
1080
- color: #a0aec0;
1081
- }
1082
-
1083
- .cv-widget-theme-dark .cv-custom-state-input {
1084
- background: #1a202c;
1085
- border-color: #4a5568;
1086
- color: #e2e8f0;
1087
- }
1088
-
1089
- .cv-widget-theme-dark .cv-custom-state-actions {
1090
- border-color: #4a5568;
1091
- }
1092
-
1093
- /* Welcome modal styles */
1094
- .cv-welcome-modal {
1095
- max-width: 500px;
1096
- }
1097
-
1098
- .cv-welcome-content {
1099
- text-align: center;
1100
- }
1101
-
1102
- .cv-welcome-content p {
1103
- font-size: 15px;
1104
- line-height: 1.6;
1105
- color: #555;
1106
- margin-bottom: 24px;
1107
- }
1108
-
1109
- .cv-welcome-widget-preview {
1110
- display: flex;
1111
- flex-direction: column;
1112
- align-items: center;
1113
- gap: 12px;
1114
- padding: 20px;
1115
- background: #f8f9fa;
1116
- border-radius: 8px;
1117
- margin-bottom: 24px;
1118
- }
1119
-
1120
- .cv-welcome-widget-icon {
1121
- width: 36px;
1122
- height: 36px;
1123
- background: white;
1124
- color: black;
1125
- border-radius: 0 18px 18px 0;
1126
- display: flex;
1127
- align-items: center;
1128
- justify-content: center;
1129
- font-size: 18px;
1130
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1131
- }
1132
-
1133
- .cv-welcome-widget-label {
1134
- font-size: 14px;
1135
- color: #666;
1136
- margin: 0;
1137
- font-weight: 500;
1138
- }
1139
-
1140
- .cv-welcome-got-it {
1141
- width: 100%;
1142
- padding: 12px 24px;
1143
- background: #007bff;
1144
- color: white;
1145
- border: none;
1146
- border-radius: 4px;
1147
- cursor: pointer;
1148
- font-size: 16px;
1149
- font-weight: 600;
1150
- transition: background 0.2s ease;
1151
- }
1152
-
1153
- .cv-welcome-got-it:hover {
1154
- background: #0056b3;
1155
- }
1156
-
1157
- .cv-welcome-got-it:active {
1158
- background: #004494;
1159
- }
1160
-
1161
- /* Dark theme welcome modal styles */
1162
- .cv-widget-theme-dark .cv-welcome-content p {
1163
- color: #cbd5e0;
1164
- }
1165
-
1166
- .cv-widget-theme-dark .cv-welcome-widget-preview {
1167
- background: #1a202c;
1168
- }
1169
-
1170
- .cv-widget-theme-dark .cv-welcome-widget-label {
1171
- color: #a0aec0;
1172
- }
1212
+ const WIDGET_STYLES = `
1213
+ /* Rounded rectangle widget icon styles */
1214
+ .cv-widget-icon {
1215
+ position: fixed;
1216
+ /* Slightly transparent by default so the widget is subtle at the page edge */
1217
+ background: rgba(255, 255, 255, 0.92);
1218
+ color: rgba(0, 0, 0, 0.9);
1219
+ opacity: 0.6;
1220
+ display: flex;
1221
+ align-items: center;
1222
+ justify-content: center;
1223
+ font-size: 18px;
1224
+ font-weight: bold;
1225
+ cursor: pointer;
1226
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1227
+ z-index: 9998;
1228
+ transition: all 0.3s ease;
1229
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1230
+ }
1231
+
1232
+ .cv-widget-icon:hover {
1233
+ /* Become fully opaque on hover to improve readability */
1234
+ background: rgba(255, 255, 255, 1);
1235
+ color: rgba(0, 0, 0, 1);
1236
+ opacity: 1;
1237
+ }
1238
+
1239
+ /* Top-right: rounded end on left, sticks out leftward on hover */
1240
+ .cv-widget-top-right {
1241
+ top: 20px;
1242
+ right: 0;
1243
+ border-radius: 18px 0 0 18px;
1244
+ padding-left: 8px;
1245
+ justify-content: flex-start;
1246
+ }
1247
+
1248
+ /* Top-left: rounded end on right, sticks out rightward on hover */
1249
+ .cv-widget-top-left {
1250
+ top: 20px;
1251
+ left: 0;
1252
+ border-radius: 0 18px 18px 0;
1253
+ padding-right: 8px;
1254
+ justify-content: flex-end;
1255
+ }
1256
+
1257
+ /* Bottom-right: rounded end on left, sticks out leftward on hover */
1258
+ .cv-widget-bottom-right {
1259
+ bottom: 20px;
1260
+ right: 0;
1261
+ border-radius: 18px 0 0 18px;
1262
+ padding-left: 8px;
1263
+ justify-content: flex-start;
1264
+ }
1265
+
1266
+ /* Bottom-left: rounded end on right, sticks out rightward on hover */
1267
+ .cv-widget-bottom-left {
1268
+ bottom: 20px;
1269
+ left: 0;
1270
+ border-radius: 0 18px 18px 0;
1271
+ padding-right: 8px;
1272
+ justify-content: flex-end;
1273
+ }
1274
+
1275
+ /* Middle-left: rounded end on right, sticks out rightward on hover */
1276
+ .cv-widget-middle-left {
1277
+ top: 50%;
1278
+ left: 0;
1279
+ transform: translateY(-50%);
1280
+ border-radius: 0 18px 18px 0;
1281
+ padding-right: 8px;
1282
+ justify-content: flex-end;
1283
+ }
1284
+
1285
+ /* Middle-right: rounded end on left, sticks out leftward on hover */
1286
+ .cv-widget-middle-right {
1287
+ top: 50%;
1288
+ right: 0;
1289
+ transform: translateY(-50%);
1290
+ border-radius: 18px 0 0 18px;
1291
+ padding-left: 8px;
1292
+ justify-content: flex-start;
1293
+ }
1294
+
1295
+ .cv-widget-top-right,
1296
+ .cv-widget-middle-right,
1297
+ .cv-widget-bottom-right,
1298
+ .cv-widget-top-left,
1299
+ .cv-widget-middle-left,
1300
+ .cv-widget-bottom-left {
1301
+ height: 36px;
1302
+ width: 36px;
1303
+ }
1304
+
1305
+ .cv-widget-middle-right:hover,
1306
+ .cv-widget-top-right:hover,
1307
+ .cv-widget-bottom-right:hover,
1308
+ .cv-widget-top-left:hover,
1309
+ .cv-widget-middle-left:hover,
1310
+ .cv-widget-bottom-left:hover {
1311
+ width: 55px;
1312
+ }
1313
+
1314
+ /* Modal content styles */
1315
+ .cv-widget-section {
1316
+ margin-bottom: 16px;
1317
+ }
1318
+
1319
+ .cv-widget-section:last-child {
1320
+ margin-bottom: 0;
1321
+ }
1322
+
1323
+ .cv-widget-section label {
1324
+ display: block;
1325
+ margin-bottom: 4px;
1326
+ font-weight: 500;
1327
+ color: #555;
1328
+ }
1329
+
1330
+ .cv-widget-profile-select,
1331
+ .cv-widget-state-select {
1332
+ width: 100%;
1333
+ padding: 8px 12px;
1334
+ border: 1px solid #ddd;
1335
+ border-radius: 4px;
1336
+ background: white;
1337
+ font-size: 14px;
1338
+ }
1339
+
1340
+ .cv-widget-profile-select:focus,
1341
+ .cv-widget-state-select:focus {
1342
+ outline: none;
1343
+ border-color: #007bff;
1344
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1345
+ }
1346
+
1347
+ .cv-widget-profile-select:disabled,
1348
+ .cv-widget-state-select:disabled {
1349
+ background: #f8f9fa;
1350
+ color: #6c757d;
1351
+ cursor: not-allowed;
1352
+ }
1353
+
1354
+ .cv-widget-current {
1355
+ margin: 16px 0;
1356
+ padding: 12px;
1357
+ background: #f8f9fa;
1358
+ border-radius: 4px;
1359
+ border-left: 4px solid #007bff;
1360
+ }
1361
+
1362
+ .cv-widget-current label {
1363
+ font-size: 12px;
1364
+ text-transform: uppercase;
1365
+ letter-spacing: 0.5px;
1366
+ color: #666;
1367
+ margin-bottom: 4px;
1368
+ }
1369
+
1370
+ .cv-widget-current-view {
1371
+ font-weight: 500;
1372
+ color: #333;
1373
+ }
1374
+
1375
+ .cv-widget-reset {
1376
+ width: 100%;
1377
+ padding: 8px 16px;
1378
+ background: #dc3545;
1379
+ color: white;
1380
+ border: none;
1381
+ border-radius: 4px;
1382
+ cursor: pointer;
1383
+ font-size: 14px;
1384
+ font-weight: 500;
1385
+ }
1386
+
1387
+ .cv-widget-reset:hover {
1388
+ background: #c82333;
1389
+ }
1390
+
1391
+ .cv-widget-reset:active {
1392
+ background: #bd2130;
1393
+ }
1394
+
1395
+ /* Responsive design for mobile */
1396
+ @media (max-width: 768px) {
1397
+ .cv-widget-top-right,
1398
+ .cv-widget-top-left {
1399
+ top: 10px;
1400
+ }
1401
+
1402
+ .cv-widget-bottom-right,
1403
+ .cv-widget-bottom-left {
1404
+ bottom: 10px;
1405
+ }
1406
+
1407
+ /* All widgets stay flush with screen edges */
1408
+ .cv-widget-top-right,
1409
+ .cv-widget-bottom-right,
1410
+ .cv-widget-middle-right {
1411
+ right: 0;
1412
+ }
1413
+
1414
+ .cv-widget-top-left,
1415
+ .cv-widget-bottom-left,
1416
+ .cv-widget-middle-left {
1417
+ left: 0;
1418
+ }
1419
+
1420
+ /* Slightly smaller on mobile */
1421
+ .cv-widget-icon {
1422
+ width: 60px;
1423
+ height: 32px;
1424
+ }
1425
+
1426
+ .cv-widget-icon:hover {
1427
+ width: 75px;
1428
+ }
1429
+ }
1430
+
1431
+ /* Modal styles */
1432
+ .cv-widget-modal-overlay {
1433
+ position: fixed;
1434
+ top: 0;
1435
+ left: 0;
1436
+ right: 0;
1437
+ bottom: 0;
1438
+ background: rgba(0, 0, 0, 0.5);
1439
+ display: flex;
1440
+ align-items: center;
1441
+ justify-content: center;
1442
+ z-index: 10002;
1443
+ animation: fadeIn 0.2s ease;
1444
+ }
1445
+
1446
+ @keyframes fadeIn {
1447
+ from { opacity: 0; }
1448
+ to { opacity: 1; }
1449
+ }
1450
+
1451
+ .cv-widget-modal {
1452
+ background: white;
1453
+ border-radius: 8px;
1454
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
1455
+ max-width: 400px;
1456
+ width: 90vw;
1457
+ max-height: 80vh;
1458
+ overflow-y: auto;
1459
+ animation: slideIn 0.2s ease;
1460
+ }
1461
+
1462
+ @keyframes slideIn {
1463
+ from {
1464
+ opacity: 0;
1465
+ transform: scale(0.9) translateY(-20px);
1466
+ }
1467
+ to {
1468
+ opacity: 1;
1469
+ transform: scale(1) translateY(0);
1470
+ }
1471
+ }
1472
+
1473
+ .cv-widget-modal-header {
1474
+ display: flex;
1475
+ justify-content: space-between;
1476
+ align-items: center;
1477
+ padding: 16px 20px;
1478
+ border-bottom: 1px solid #e9ecef;
1479
+ background: #f8f9fa;
1480
+ border-radius: 8px 8px 0 0;
1481
+ }
1482
+
1483
+ .cv-widget-modal-header h3 {
1484
+ margin: 0;
1485
+ font-size: 18px;
1486
+ font-weight: 600;
1487
+ color: #333;
1488
+ }
1489
+
1490
+ .cv-widget-modal-close {
1491
+ background: none;
1492
+ border: none;
1493
+ font-size: 20px;
1494
+ cursor: pointer;
1495
+ padding: 0;
1496
+ width: 32px;
1497
+ height: 32px;
1498
+ display: flex;
1499
+ align-items: center;
1500
+ justify-content: center;
1501
+ border-radius: 4px;
1502
+ color: #666;
1503
+ line-height: 1;
1504
+ transition: all 0.2s ease;
1505
+ }
1506
+
1507
+ .cv-widget-modal-close:hover {
1508
+ background: #e9ecef;
1509
+ color: #333;
1510
+ }
1511
+
1512
+ .cv-widget-modal-content {
1513
+ padding: 20px;
1514
+ }
1515
+
1516
+ .cv-widget-modal-actions {
1517
+ margin-top: 20px;
1518
+ padding-top: 16px;
1519
+ border-top: 1px solid #e9ecef;
1520
+ }
1521
+
1522
+ .cv-widget-restore {
1523
+ width: 100%;
1524
+ padding: 10px 16px;
1525
+ background: #28a745;
1526
+ color: white;
1527
+ border: none;
1528
+ border-radius: 4px;
1529
+ cursor: pointer;
1530
+ font-size: 14px;
1531
+ font-weight: 500;
1532
+ }
1533
+
1534
+ .cv-widget-restore:hover {
1535
+ background: #218838;
1536
+ }
1537
+
1538
+ .cv-widget-create-state {
1539
+ width: 100%;
1540
+ padding: 10px 16px;
1541
+ background: #007bff;
1542
+ color: white;
1543
+ border: none;
1544
+ border-radius: 4px;
1545
+ cursor: pointer;
1546
+ font-size: 14px;
1547
+ font-weight: 500;
1548
+ margin-bottom: 10px;
1549
+ }
1550
+
1551
+ .cv-widget-create-state:hover {
1552
+ background: #0056b3;
1553
+ }
1554
+
1555
+ /* Dark theme modal styles */
1556
+ .cv-widget-theme-dark .cv-widget-modal {
1557
+ background: #2d3748;
1558
+ color: #e2e8f0;
1559
+ }
1560
+
1561
+ .cv-widget-theme-dark .cv-widget-modal-header {
1562
+ background: #1a202c;
1563
+ border-color: #4a5568;
1564
+ }
1565
+
1566
+ .cv-widget-theme-dark .cv-widget-modal-header h3 {
1567
+ color: #e2e8f0;
1568
+ }
1569
+
1570
+ .cv-widget-theme-dark .cv-widget-modal-close {
1571
+ color: #a0aec0;
1572
+ }
1573
+
1574
+ .cv-widget-theme-dark .cv-widget-modal-close:hover {
1575
+ background: #4a5568;
1576
+ color: #e2e8f0;
1577
+ }
1578
+
1579
+ .cv-widget-theme-dark .cv-widget-modal-actions {
1580
+ border-color: #4a5568;
1581
+ }
1582
+
1583
+ /* Custom state creator styles */
1584
+ .cv-custom-state-modal {
1585
+ max-width: 500px;
1586
+ }
1587
+
1588
+ .cv-custom-state-form h4 {
1589
+ margin: 20px 0 10px 0;
1590
+ font-size: 16px;
1591
+ font-weight: 600;
1592
+ color: #333;
1593
+ border-bottom: 1px solid #e9ecef;
1594
+ padding-bottom: 5px;
1595
+ }
1596
+
1597
+ .cv-custom-state-form p {
1598
+ font-size: 15px;
1599
+ line-height: 1.6;
1600
+ color: #555;
1601
+ margin-bottom: 24px;
1602
+ text-align: justify;
1603
+ }
1604
+
1605
+ .cv-custom-state-section {
1606
+ margin-bottom: 16px;
1607
+ }
1608
+
1609
+ .cv-custom-state-section label {
1610
+ display: block;
1611
+ margin-bottom: 4px;
1612
+ font-weight: 500;
1613
+ color: #555;
1614
+ }
1615
+
1616
+ .cv-custom-state-input {
1617
+ width: 100%;
1618
+ padding: 8px 12px;
1619
+ border: 1px solid #ddd;
1620
+ border-radius: 4px;
1621
+ background: white;
1622
+ font-size: 14px;
1623
+ }
1624
+
1625
+ .cv-custom-state-input:focus {
1626
+ outline: none;
1627
+ border-color: #007bff;
1628
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1629
+ }
1630
+
1631
+ .cv-custom-toggles {
1632
+ display: grid;
1633
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1634
+ gap: 10px;
1635
+ }
1636
+
1637
+ .cv-custom-state-toggle {
1638
+ display: flex;
1639
+ align-items: center;
1640
+ }
1641
+
1642
+ .cv-custom-state-toggle label {
1643
+ display: flex;
1644
+ align-items: center;
1645
+ cursor: pointer;
1646
+ font-weight: normal;
1647
+ margin: 0;
1648
+ }
1649
+
1650
+ .cv-custom-toggle-checkbox {
1651
+ margin-right: 8px;
1652
+ width: auto;
1653
+ }
1654
+
1655
+ .cv-tab-groups {
1656
+ margin-top: 20px;
1657
+ }
1658
+
1659
+ .cv-tab-group-control {
1660
+ margin-bottom: 15px;
1661
+ }
1662
+
1663
+ .cv-tab-group-control label {
1664
+ display: block;
1665
+ margin-bottom: 5px;
1666
+ font-weight: 500;
1667
+ font-size: 14px;
1668
+ }
1669
+
1670
+ .cv-tab-group-select {
1671
+ width: 100%;
1672
+ padding: 8px 12px;
1673
+ border: 1px solid #ced4da;
1674
+ border-radius: 4px;
1675
+ font-size: 14px;
1676
+ background-color: white;
1677
+ cursor: pointer;
1678
+ }
1679
+
1680
+ .cv-tab-group-select:focus {
1681
+ outline: none;
1682
+ border-color: #007bff;
1683
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
1684
+ }
1685
+
1686
+ .cv-widget-theme-dark .cv-tab-group-select {
1687
+ background-color: #2d3748;
1688
+ border-color: #4a5568;
1689
+ color: #e2e8f0;
1690
+ }
1691
+
1692
+ .cv-custom-state-actions {
1693
+ display: flex;
1694
+ gap: 10px;
1695
+ margin-top: 20px;
1696
+ padding-top: 16px;
1697
+ border-top: 1px solid #e9ecef;
1698
+ }
1699
+
1700
+ .cv-custom-state-cancel,
1701
+ .cv-custom-state-copy-url {
1702
+ flex: 1;
1703
+ padding: 10px 16px;
1704
+ border: none;
1705
+ border-radius: 4px;
1706
+ cursor: pointer;
1707
+ font-size: 14px;
1708
+ font-weight: 500;
1709
+ }
1710
+
1711
+ .cv-custom-state-reset {
1712
+ flex: 1;
1713
+ padding: 10px 16px;
1714
+ border: none;
1715
+ border-radius: 4px;
1716
+ cursor: pointer;
1717
+ font-size: 14px;
1718
+ font-weight: 500;
1719
+ background: #dc3545;
1720
+ color: white;
1721
+ }
1722
+
1723
+ .cv-custom-state-reset:hover {
1724
+ background: #c82333;
1725
+ }
1726
+
1727
+ .cv-custom-state-cancel {
1728
+ background: #6c757d;
1729
+ color: white;
1730
+ }
1731
+
1732
+ .cv-custom-state-cancel:hover {
1733
+ background: #5a6268;
1734
+ }
1735
+
1736
+ .cv-custom-state-copy-url {
1737
+ background: #28a745;
1738
+ color: white;
1739
+ }
1740
+
1741
+ .cv-custom-state-copy-url:hover {
1742
+ background: #218838;
1743
+ }
1744
+
1745
+ /* Dark theme custom state styles */
1746
+ .cv-widget-theme-dark .cv-custom-state-form h4 {
1747
+ color: #e2e8f0;
1748
+ border-color: #4a5568;
1749
+ }
1750
+
1751
+ .cv-widget-theme-dark .cv-custom-state-form p {
1752
+ color: #cbd5e0;
1753
+ }
1754
+
1755
+ .cv-widget-theme-dark .cv-custom-state-section label {
1756
+ color: #a0aec0;
1757
+ }
1758
+
1759
+ .cv-widget-theme-dark .cv-custom-state-input {
1760
+ background: #1a202c;
1761
+ border-color: #4a5568;
1762
+ color: #e2e8f0;
1763
+ }
1764
+
1765
+ .cv-widget-theme-dark .cv-custom-state-actions {
1766
+ border-color: #4a5568;
1767
+ }
1768
+
1769
+ /* Welcome modal styles */
1770
+ .cv-welcome-modal {
1771
+ max-width: 500px;
1772
+ }
1773
+
1774
+ .cv-welcome-content {
1775
+ text-align: center;
1776
+ }
1777
+
1778
+ .cv-welcome-content p {
1779
+ font-size: 15px;
1780
+ line-height: 1.6;
1781
+ color: #555;
1782
+ margin-bottom: 24px;
1783
+ text-align: justify;
1784
+ }
1785
+
1786
+ .cv-welcome-widget-preview {
1787
+ display: flex;
1788
+ flex-direction: column;
1789
+ align-items: center;
1790
+ gap: 12px;
1791
+ padding: 20px;
1792
+ background: #f8f9fa;
1793
+ border-radius: 8px;
1794
+ margin-bottom: 24px;
1795
+ }
1796
+
1797
+ .cv-welcome-widget-icon {
1798
+ width: 36px;
1799
+ height: 36px;
1800
+ background: white;
1801
+ color: black;
1802
+ border-radius: 0 18px 18px 0;
1803
+ display: flex;
1804
+ align-items: center;
1805
+ justify-content: center;
1806
+ font-size: 18px;
1807
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1808
+ }
1809
+
1810
+ .cv-welcome-widget-label {
1811
+ font-size: 14px;
1812
+ color: #666;
1813
+ margin: 0;
1814
+ font-weight: 500;
1815
+ }
1816
+
1817
+ .cv-welcome-got-it {
1818
+ width: 100%;
1819
+ padding: 12px 24px;
1820
+ background: #007bff;
1821
+ color: white;
1822
+ border: none;
1823
+ border-radius: 4px;
1824
+ cursor: pointer;
1825
+ font-size: 16px;
1826
+ font-weight: 600;
1827
+ transition: background 0.2s ease;
1828
+ }
1829
+
1830
+ .cv-welcome-got-it:hover {
1831
+ background: #0056b3;
1832
+ }
1833
+
1834
+ .cv-welcome-got-it:active {
1835
+ background: #004494;
1836
+ }
1837
+
1838
+ /* Dark theme welcome modal styles */
1839
+ .cv-widget-theme-dark .cv-welcome-content p {
1840
+ color: #cbd5e0;
1841
+ }
1842
+
1843
+ .cv-widget-theme-dark .cv-welcome-widget-preview {
1844
+ background: #1a202c;
1845
+ }
1846
+
1847
+ .cv-widget-theme-dark .cv-welcome-widget-label {
1848
+ color: #a0aec0;
1849
+ }
1173
1850
  `;
1174
1851
  /**
1175
1852
  * Inject widget styles into the document head
@@ -1204,8 +1881,9 @@ class CustomViewsWidget {
1204
1881
  title: options.title || 'Custom Views',
1205
1882
  description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1206
1883
  showWelcome: options.showWelcome ?? false,
1207
- welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1208
- 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.'
1884
+ welcomeTitle: options.welcomeTitle || 'This website uses Custom Views',
1885
+ welcomeMessage: options.welcomeMessage || 'This site 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.',
1886
+ showTabGroups: options.showTabGroups ?? true
1209
1887
  };
1210
1888
  // No external state manager to initialize
1211
1889
  }
@@ -1270,8 +1948,8 @@ class CustomViewsWidget {
1270
1948
  */
1271
1949
  openStateModal() {
1272
1950
  // Get toggles from current configuration and open the modal regardless of count
1273
- const localConfig = this.core.getLocalConfig();
1274
- const toggles = localConfig?.allToggles || [];
1951
+ const config = this.core.getConfig();
1952
+ const toggles = config?.allToggles || [];
1275
1953
  this.createCustomStateModal(toggles);
1276
1954
  }
1277
1955
  /**
@@ -1284,37 +1962,61 @@ class CustomViewsWidget {
1284
1962
  this.modal.className = 'cv-widget-modal-overlay';
1285
1963
  this.applyThemeToModal();
1286
1964
  const toggleControls = toggles.length
1287
- ? toggles.map(toggle => `
1288
- <div class="cv-custom-state-toggle">
1289
- <label>
1290
- <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1291
- ${this.formatToggleName(toggle)}
1292
- </label>
1293
- </div>
1965
+ ? toggles.map(toggle => `
1966
+ <div class="cv-custom-state-toggle">
1967
+ <label>
1968
+ <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1969
+ ${this.formatToggleName(toggle)}
1970
+ </label>
1971
+ </div>
1294
1972
  `).join('')
1295
1973
  : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1296
- this.modal.innerHTML = `
1297
- <div class="cv-widget-modal cv-custom-state-modal">
1298
- <div class="cv-widget-modal-header">
1299
- <h3>Customize View</h3>
1300
- <button class="cv-widget-modal-close" aria-label="Close modal">X</button>
1301
- </div>
1302
- <div class="cv-widget-modal-content">
1303
- <div class="cv-custom-state-form">
1304
- <p>${this.options.description}</p>
1305
-
1306
- <h4>Content Sections</h4>
1307
- <div class="cv-custom-toggles">
1308
- ${toggleControls}
1309
- </div>
1310
-
1311
- <div class="cv-custom-state-actions">
1312
- ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1313
- <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
1314
- </div>
1315
- </div>
1316
- </div>
1317
- </div>
1974
+ // Get tab groups
1975
+ const tabGroups = this.core.getTabGroups();
1976
+ let tabGroupsHTML = '';
1977
+ if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
1978
+ const tabGroupControls = tabGroups.map(group => {
1979
+ const options = group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('');
1980
+ return `
1981
+ <div class="cv-tab-group-control">
1982
+ <label for="tab-group-${group.id}">${group.label || group.id}</label>
1983
+ <select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
1984
+ ${options}
1985
+ </select>
1986
+ </div>
1987
+ `;
1988
+ }).join('');
1989
+ tabGroupsHTML = `
1990
+ <h4>Tab Groups</h4>
1991
+ <div class="cv-tab-groups">
1992
+ ${tabGroupControls}
1993
+ </div>
1994
+ `;
1995
+ }
1996
+ this.modal.innerHTML = `
1997
+ <div class="cv-widget-modal cv-custom-state-modal">
1998
+ <div class="cv-widget-modal-header">
1999
+ <h3>Customize View</h3>
2000
+ <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
2001
+ </div>
2002
+ <div class="cv-widget-modal-content">
2003
+ <div class="cv-custom-state-form">
2004
+ <p>${this.options.description}</p>
2005
+
2006
+ <h4>Content Sections</h4>
2007
+ <div class="cv-custom-toggles">
2008
+ ${toggleControls}
2009
+ </div>
2010
+
2011
+ ${tabGroupsHTML}
2012
+
2013
+ <div class="cv-custom-state-actions">
2014
+ ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
2015
+ <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
2016
+ </div>
2017
+ </div>
2018
+ </div>
2019
+ </div>
1318
2020
  `;
1319
2021
  document.body.appendChild(this.modal);
1320
2022
  this.attachStateModalEventListeners();
@@ -1357,6 +2059,17 @@ class CustomViewsWidget {
1357
2059
  this.core.applyState(state);
1358
2060
  });
1359
2061
  });
2062
+ // Listen to tab group selects
2063
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
2064
+ tabGroupSelects.forEach(select => {
2065
+ select.addEventListener('change', () => {
2066
+ const groupId = select.dataset.groupId;
2067
+ const tabId = select.value;
2068
+ if (groupId && tabId) {
2069
+ this.core.setActiveTab(groupId, tabId);
2070
+ }
2071
+ });
2072
+ });
1360
2073
  // Overlay click to close
1361
2074
  this.modal.addEventListener('click', (e) => {
1362
2075
  if (e.target === this.modal) {
@@ -1401,7 +2114,16 @@ class CustomViewsWidget {
1401
2114
  toggles.push(toggle);
1402
2115
  }
1403
2116
  });
1404
- return { toggles };
2117
+ // Collect tab selections
2118
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
2119
+ const tabs = {};
2120
+ tabGroupSelects.forEach(select => {
2121
+ const groupId = select.dataset.groupId;
2122
+ if (groupId) {
2123
+ tabs[groupId] = select.value;
2124
+ }
2125
+ });
2126
+ return Object.keys(tabs).length > 0 ? { toggles, tabs } : { toggles };
1405
2127
  }
1406
2128
  /**
1407
2129
  * Copy shareable URL to clipboard
@@ -1437,6 +2159,15 @@ class CustomViewsWidget {
1437
2159
  }
1438
2160
  }
1439
2161
  });
2162
+ // Load tab group selections
2163
+ const activeTabs = this.core.getCurrentActiveTabs();
2164
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
2165
+ tabGroupSelects.forEach(select => {
2166
+ const groupId = select.dataset.groupId;
2167
+ if (groupId && activeTabs[groupId]) {
2168
+ select.value = activeTabs[groupId];
2169
+ }
2170
+ });
1440
2171
  }
1441
2172
  /**
1442
2173
  * Format toggle name for display
@@ -1470,25 +2201,25 @@ class CustomViewsWidget {
1470
2201
  this.modal = document.createElement('div');
1471
2202
  this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
1472
2203
  this.applyThemeToModal();
1473
- this.modal.innerHTML = `
1474
- <div class="cv-widget-modal cv-welcome-modal">
1475
- <div class="cv-widget-modal-header">
1476
- <h3>${this.options.welcomeTitle}</h3>
1477
- <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
1478
- </div>
1479
- <div class="cv-widget-modal-content">
1480
- <div class="cv-welcome-content">
1481
- <p>${this.options.welcomeMessage}</p>
1482
-
1483
- <div class="cv-welcome-widget-preview">
1484
- <div class="cv-welcome-widget-icon">⚙</div>
1485
- <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
1486
- </div>
1487
-
1488
- <button class="cv-welcome-got-it">Got it!</button>
1489
- </div>
1490
- </div>
1491
- </div>
2204
+ this.modal.innerHTML = `
2205
+ <div class="cv-widget-modal cv-welcome-modal">
2206
+ <div class="cv-widget-modal-header">
2207
+ <h3>${this.options.welcomeTitle}</h3>
2208
+ <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
2209
+ </div>
2210
+ <div class="cv-widget-modal-content">
2211
+ <div class="cv-welcome-content">
2212
+ <p>${this.options.welcomeMessage}</p>
2213
+
2214
+ <div class="cv-welcome-widget-preview">
2215
+ <div class="cv-welcome-widget-icon">⚙</div>
2216
+ <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
2217
+ </div>
2218
+
2219
+ <button class="cv-welcome-got-it">Got it!</button>
2220
+ </div>
2221
+ </div>
2222
+ </div>
1492
2223
  `;
1493
2224
  document.body.appendChild(this.modal);
1494
2225
  this.attachWelcomeModalEventListeners();
@@ -1530,74 +2261,160 @@ class CustomViewsWidget {
1530
2261
  }
1531
2262
  }
1532
2263
 
1533
- class CustomViews {
1534
- // Helper function to prepend baseURL to a path
1535
- static prependBaseURL(path, baseURL) {
1536
- if (!baseURL)
1537
- return path;
1538
- // Don't prepend if the path is already absolute (starts with http:// or https://)
1539
- if (path.startsWith('http://') || path.startsWith('https://')) {
1540
- return path;
1541
- }
1542
- // Ensure baseURL doesn't end with / and path starts with /
1543
- const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
1544
- const cleanPath = path.startsWith('/') ? path : '/' + path;
1545
- return cleanBaseURL + cleanPath;
2264
+ /**
2265
+ * Initialize CustomViews from script tag attributes and config file
2266
+ * This function handles the automatic initialization of CustomViews when included via script tag
2267
+ *
2268
+ * Data attributes supported:
2269
+ * - data-base-url: Base URL for the site (e.g., "/customviews" for subdirectory deployments)
2270
+ * - data-config-path: Path to config file (default: "/customviews.config.json")
2271
+ *
2272
+ * The function fetches the config file and uses it directly to initialize CustomViews.
2273
+ * Widget visibility is controlled via the config file (widget.enabled property).
2274
+ */
2275
+ function initializeFromScript() {
2276
+ // Only run in browser environment
2277
+ if (typeof window === 'undefined')
2278
+ return;
2279
+ // Use the typed global `window` (see src/types/global.d.ts)
2280
+ // Idempotency guard: if already initialized, skip setting up listener again.
2281
+ if (window.__customViewsInitialized) {
2282
+ // Informational for developers; harmless in production.
2283
+ console.info('[CustomViews] Auto-init skipped: already initialized.');
2284
+ return;
1546
2285
  }
1547
- // Entry Point to use CustomViews
1548
- static async initFromJson(opts) {
1549
- // Load assets JSON if provided
1550
- let assetsManager;
1551
- const baseURL = opts.baseURL || '';
1552
- if (opts.assetsJsonPath) {
1553
- const assetsPath = this.prependBaseURL(opts.assetsJsonPath, baseURL);
1554
- const assetsJson = await (await fetch(assetsPath)).json();
1555
- assetsManager = new AssetsManager(assetsJson, baseURL);
1556
- }
1557
- else {
1558
- assetsManager = new AssetsManager({}, baseURL);
1559
- }
1560
- // Load config JSON if provided, else just log error and don't load the custom views
1561
- let localConfig;
1562
- if (opts.config) {
1563
- localConfig = opts.config;
2286
+ document.addEventListener('DOMContentLoaded', async function () {
2287
+ // Prevent concurrent initialization runs (race conditions when script is loaded twice)
2288
+ if (window.__customViewsInitInProgress || window.__customViewsInitialized) {
2289
+ return;
1564
2290
  }
1565
- else {
1566
- if (!opts.configPath) {
1567
- console.error("No config path provided, skipping custom views");
1568
- return null;
2291
+ window.__customViewsInitInProgress = true;
2292
+ try {
2293
+ // Find the script tag
2294
+ let scriptTag = document.currentScript;
2295
+ // Fallback if currentScript is not available (executed after page load)
2296
+ if (!scriptTag) {
2297
+ // Try to find the script tag by looking for our script
2298
+ const scripts = document.querySelectorAll('script[src*="custom-views"]');
2299
+ if (scripts.length > 0) {
2300
+ // Find the most specific match (to avoid matching other custom-views scripts)
2301
+ for (let i = 0; i < scripts.length; i++) {
2302
+ const script = scripts[i];
2303
+ const src = script.getAttribute('src') || '';
2304
+ // Look for .min.js or .js at the end
2305
+ if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
2306
+ scriptTag = script;
2307
+ break;
2308
+ }
2309
+ }
2310
+ // If no specific match found, use the first one
2311
+ if (!scriptTag) {
2312
+ scriptTag = scripts[0];
2313
+ }
2314
+ }
1569
2315
  }
2316
+ // Read data attributes from script tag
2317
+ let baseURL = '';
2318
+ let configPath = '/customviews.config.json';
2319
+ if (scriptTag) {
2320
+ baseURL = scriptTag.getAttribute('data-base-url') || '';
2321
+ configPath = scriptTag.getAttribute('data-config-path') || configPath;
2322
+ }
2323
+ // Fetch config file
2324
+ let configFile;
1570
2325
  try {
1571
- const configPath = this.prependBaseURL(opts.configPath, baseURL);
1572
- localConfig = await (await fetch(configPath)).json();
2326
+ const fullConfigPath = prependBaseUrl(configPath, baseURL);
2327
+ console.log(`[CustomViews] Loading config from: ${fullConfigPath}`);
2328
+ const response = await fetch(fullConfigPath);
2329
+ if (!response.ok) {
2330
+ console.warn(`[CustomViews] Config file not found at ${fullConfigPath}. Using defaults.`);
2331
+ // Provide minimal default config structure
2332
+ configFile = {
2333
+ config: {
2334
+ allToggles: [],
2335
+ defaultState: { toggles: [] }
2336
+ },
2337
+ widget: {
2338
+ enabled: true
2339
+ }
2340
+ };
2341
+ }
2342
+ else {
2343
+ configFile = await response.json();
2344
+ console.log('[CustomViews] Config loaded successfully');
2345
+ }
1573
2346
  }
1574
2347
  catch (error) {
1575
- console.error("Error loading config:", error);
1576
- return null;
2348
+ console.error('[CustomViews] Error loading config file:', error);
2349
+ return; // Abort initialization
2350
+ }
2351
+ // Determine effective baseURL (data attribute takes precedence over config)
2352
+ const effectiveBaseURL = baseURL || configFile.baseURL || '';
2353
+ const options = {
2354
+ config: configFile.config,
2355
+ assetsJsonPath: configFile.assetsJsonPath,
2356
+ baseURL: effectiveBaseURL,
2357
+ };
2358
+ if (configFile.showUrl !== undefined) {
2359
+ options.showUrl = configFile.showUrl;
2360
+ }
2361
+ // Initialize CustomViews core
2362
+ const core = await CustomViews.init(options);
2363
+ if (!core) {
2364
+ console.error('[CustomViews] Failed to initialize core.');
2365
+ return; // Abort widget creation
1577
2366
  }
2367
+ // Store instance
2368
+ window.customViewsInstance = { core };
2369
+ // Initialize widget if enabled in config
2370
+ let widget;
2371
+ if (configFile.widget?.enabled !== false) {
2372
+ widget = new CustomViewsWidget({
2373
+ core,
2374
+ ...configFile.widget
2375
+ });
2376
+ widget.render();
2377
+ // Store widget instance
2378
+ window.customViewsInstance.widget = widget;
2379
+ console.log('[CustomViews] Widget initialized and rendered');
2380
+ }
2381
+ else {
2382
+ console.log('[CustomViews] Widget disabled in config - skipping initialization');
2383
+ }
2384
+ // Dispatch ready event
2385
+ const readyEvent = new CustomEvent('customviews:ready', {
2386
+ detail: {
2387
+ core,
2388
+ widget
2389
+ }
2390
+ });
2391
+ document.dispatchEvent(readyEvent);
2392
+ // Mark initialized and clear in-progress flag
2393
+ window.__customViewsInitialized = true;
2394
+ window.__customViewsInitInProgress = false;
1578
2395
  }
1579
- const coreOptions = {
1580
- assetsManager,
1581
- config: localConfig,
1582
- rootEl: opts.rootEl,
1583
- };
1584
- const core = new CustomViewsCore(coreOptions);
1585
- core.init();
1586
- return core;
1587
- }
2396
+ catch (error) {
2397
+ // Clear in-progress flag so a future attempt can retry
2398
+ window.__customViewsInitInProgress = false;
2399
+ console.error('[CustomViews] Auto-initialization error:', error);
2400
+ }
2401
+ });
1588
2402
  }
2403
+
2404
+ // Import from new modules
2405
+ // Set up globals and auto-initialization
1589
2406
  if (typeof window !== "undefined") {
1590
- // @ts-ignore
2407
+ // Expose to window to enable usage (e.g. const app = new window.CustomViews(...))
1591
2408
  window.CustomViews = CustomViews;
1592
- // @ts-ignore
1593
2409
  window.CustomViewsWidget = CustomViewsWidget;
2410
+ // Run auto-initialization
2411
+ initializeFromScript();
1594
2412
  }
1595
2413
 
1596
2414
  exports.AssetsManager = AssetsManager;
1597
2415
  exports.CustomViews = CustomViews;
1598
2416
  exports.CustomViewsCore = CustomViewsCore;
1599
2417
  exports.CustomViewsWidget = CustomViewsWidget;
1600
- exports.LocalConfig = Config;
1601
2418
  exports.PersistenceManager = PersistenceManager;
1602
2419
  exports.URLStateManager = URLStateManager;
1603
- //# sourceMappingURL=custom-views.cjs.js.map
2420
+ //# sourceMappingURL=custom-views.core.cjs.js.map