@customviews-js/customviews 1.1.0 → 1.1.2

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.
@@ -1,85 +1,8 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.1.0
2
+ * @customviews-js/customviews v1.1.2
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
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
6
  /**
84
7
  * Manages persistence of custom views state using browser localStorage
85
8
  */
@@ -340,6 +263,98 @@ class VisibilityManager {
340
263
  }
341
264
  }
342
265
 
266
+ /** --- Icon utilities --- */
267
+ function ensureFontAwesomeInjected() {
268
+ const isFontAwesomeLoaded = Array.from(document.styleSheets).some(sheet => sheet.href && (sheet.href.includes('font-awesome') || sheet.href.includes('fontawesome')));
269
+ if (isFontAwesomeLoaded)
270
+ return;
271
+ const link = document.createElement('link');
272
+ link.rel = 'stylesheet';
273
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
274
+ link.setAttribute('data-customviews-fontawesome', 'true');
275
+ document.head.appendChild(link);
276
+ }
277
+ function replaceIconShortcodes(text) {
278
+ // Handle Font Awesome shortcodes
279
+ return text.replace(/:fa-([\w-]+):/g, (_, icon) => `<i class="fa fa-${icon}"></i>`);
280
+ }
281
+ /** --- Basic renderers --- */
282
+ function renderImage(el, asset) {
283
+ if (!asset.src)
284
+ return;
285
+ el.innerHTML = '';
286
+ const img = document.createElement('img');
287
+ img.src = asset.src;
288
+ img.alt = asset.alt || '';
289
+ // Apply custom styling if provided
290
+ if (asset.className) {
291
+ img.className = asset.className;
292
+ }
293
+ if (asset.style) {
294
+ img.setAttribute('style', asset.style);
295
+ }
296
+ // Default styles (can be overridden by asset.style)
297
+ img.style.maxWidth = img.style.maxWidth || '100%';
298
+ img.style.height = img.style.height || 'auto';
299
+ img.style.display = img.style.display || 'block';
300
+ el.appendChild(img);
301
+ }
302
+ function renderText(el, asset) {
303
+ if (asset.content != null) {
304
+ el.textContent = asset.content;
305
+ }
306
+ // Apply custom styling if provided
307
+ if (asset.className) {
308
+ el.className = asset.className;
309
+ }
310
+ if (asset.style) {
311
+ el.setAttribute('style', asset.style);
312
+ }
313
+ }
314
+ function renderHtml(el, asset) {
315
+ if (asset.content != null) {
316
+ el.innerHTML = asset.content;
317
+ }
318
+ // Apply custom styling if provided
319
+ if (asset.className) {
320
+ el.className = asset.className;
321
+ }
322
+ if (asset.style) {
323
+ el.setAttribute('style', asset.style);
324
+ }
325
+ }
326
+ /** --- Unified asset renderer --- */
327
+ function detectAssetType(asset) {
328
+ // If src exists, it's an image
329
+ if (asset.src)
330
+ return 'image';
331
+ // If content contains HTML tags, it's HTML
332
+ if (asset.content && /<[^>]+>/.test(asset.content)) {
333
+ return 'html';
334
+ }
335
+ return 'text';
336
+ }
337
+ function renderAssetInto(el, assetId, assetsManager) {
338
+ const asset = assetsManager.get(assetId);
339
+ if (!asset)
340
+ return;
341
+ const type = asset.type || detectAssetType(asset);
342
+ switch (type) {
343
+ case 'image':
344
+ renderImage(el, asset);
345
+ break;
346
+ case 'text':
347
+ renderText(el, asset);
348
+ break;
349
+ case 'html':
350
+ renderHtml(el, asset);
351
+ break;
352
+ default:
353
+ el.innerHTML = asset.content || String(asset);
354
+ console.warn('[CustomViews] Unknown asset type:', type);
355
+ }
356
+ }
357
+
343
358
  // Constants for selectors
344
359
  const TABGROUP_SELECTOR = 'cv-tabgroup';
345
360
  const TAB_SELECTOR = 'cv-tab';
@@ -361,8 +376,8 @@ class TabManager {
361
376
  return;
362
377
  // Determine the active tab for this group
363
378
  const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
364
- // Apply visibility to child cv-tab elements
365
- const tabElements = groupEl.querySelectorAll(TAB_SELECTOR);
379
+ // Apply visibility to direct child cv-tab elements only (not nested ones)
380
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
366
381
  tabElements.forEach((tabEl) => {
367
382
  const tabId = tabEl.getAttribute('id');
368
383
  if (!tabId)
@@ -394,8 +409,8 @@ class TabManager {
394
409
  }
395
410
  }
396
411
  }
397
- // 3. Fallback to first cv-tab child in DOM
398
- const firstTab = groupEl.querySelector(TAB_SELECTOR);
412
+ // 3. Fallback to first direct cv-tab child in DOM
413
+ const firstTab = Array.from(groupEl.children).find((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
399
414
  if (firstTab) {
400
415
  return firstTab.getAttribute('id');
401
416
  }
@@ -420,6 +435,28 @@ class TabManager {
420
435
  static buildNavs(rootEl, cfgGroups, onTabClick) {
421
436
  // Find all cv-tabgroup elements with nav="auto" or no nav attribute
422
437
  const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
438
+ // Check if any tab headers contain Font Awesome shortcodes
439
+ // Inject Font Awesome CSS only if needed
440
+ let hasFontAwesomeShortcodes = false;
441
+ tabGroups.forEach((groupEl) => {
442
+ const groupId = groupEl.getAttribute('id');
443
+ if (!groupId)
444
+ return;
445
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === 'cv-tab');
446
+ tabElements.forEach((tabEl) => {
447
+ const tabId = tabEl.getAttribute('id');
448
+ if (!tabId)
449
+ return;
450
+ const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
451
+ if (/:fa-[\w-]+:/.test(header)) {
452
+ hasFontAwesomeShortcodes = true;
453
+ }
454
+ });
455
+ });
456
+ // Inject Font Awesome only if shortcodes are found
457
+ if (hasFontAwesomeShortcodes) {
458
+ ensureFontAwesomeInjected();
459
+ }
423
460
  tabGroups.forEach((groupEl) => {
424
461
  const groupId = groupEl.getAttribute('id');
425
462
  if (!groupId)
@@ -428,8 +465,8 @@ class TabManager {
428
465
  let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
429
466
  if (navContainer)
430
467
  return; // Already built
431
- // Get all child tabs
432
- const tabElements = Array.from(groupEl.querySelectorAll(TAB_SELECTOR));
468
+ // Get only direct child tabs (not nested ones)
469
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
433
470
  if (tabElements.length === 0)
434
471
  return;
435
472
  // Create nav container
@@ -447,7 +484,8 @@ class TabManager {
447
484
  listItem.className = 'nav-item';
448
485
  const navLink = document.createElement('a');
449
486
  navLink.className = 'nav-link';
450
- navLink.textContent = header;
487
+ // Replace icon shortcodes in header
488
+ navLink.innerHTML = replaceIconShortcodes(header);
451
489
  navLink.href = '#';
452
490
  navLink.setAttribute('data-tab-id', tabId);
453
491
  navLink.setAttribute('data-group-id', groupId);
@@ -539,157 +577,267 @@ class TabManager {
539
577
  }
540
578
  }
541
579
 
580
+ class AssetsManager {
581
+ assets;
582
+ baseURL;
583
+ constructor(assets, baseURL = '') {
584
+ this.assets = assets;
585
+ this.baseURL = baseURL;
586
+ if (!this.validate()) {
587
+ console.warn('Invalid assets:', this.assets);
588
+ }
589
+ }
590
+ // Check each asset has content or src
591
+ validate() {
592
+ return Object.values(this.assets).every(a => a.src || a.content);
593
+ }
594
+ get(assetId) {
595
+ const asset = this.assets[assetId];
596
+ if (!asset)
597
+ return undefined;
598
+ // If there's a baseURL and the asset has a src property, prepend the baseURL
599
+ if (this.baseURL && asset.src) {
600
+ // Create a shallow copy to avoid mutating the original asset
601
+ return {
602
+ ...asset,
603
+ src: this.prependBaseURL(asset.src)
604
+ };
605
+ }
606
+ return asset;
607
+ }
608
+ prependBaseURL(path) {
609
+ // Don't prepend if the path is already absolute (starts with http:// or https://)
610
+ if (path.startsWith('http://') || path.startsWith('https://')) {
611
+ return path;
612
+ }
613
+ // Ensure baseURL doesn't end with / and path starts with /
614
+ const cleanBaseURL = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
615
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
616
+ return cleanBaseURL + cleanPath;
617
+ }
618
+ loadFromJSON(json) {
619
+ this.assets = json;
620
+ }
621
+ loadAdditionalAssets(additionalAssets) {
622
+ this.assets = { ...this.assets, ...additionalAssets };
623
+ }
624
+ }
625
+
626
+ // Constants for selectors
627
+ const TOGGLE_DATA_SELECTOR = "[data-cv-toggle], [data-customviews-toggle]";
628
+ const TOGGLE_ELEMENT_SELECTOR = "cv-toggle";
629
+ const TOGGLE_SELECTOR = `${TOGGLE_DATA_SELECTOR}, ${TOGGLE_ELEMENT_SELECTOR}`;
542
630
  /**
543
- * Styles for toggle visibility and animations
631
+ * ToggleManager handles discovery, visibility, and asset rendering for toggle elements
544
632
  */
545
- const TOGGLE_STYLES = `
546
- /* Core toggle visibility transitions */
547
- [data-cv-toggle], [data-customviews-toggle] {
548
- transition: opacity 150ms ease,
549
- transform 150ms ease,
550
- max-height 200ms ease,
551
- margin 150ms ease;
552
- will-change: opacity, transform, max-height, margin;
553
- }
554
-
555
- .cv-visible {
556
- opacity: 1 !important;
557
- transform: translateY(0) !important;
558
- max-height: var(--cv-max-height, 9999px) !important;
559
- }
560
-
561
- .cv-hidden {
562
- opacity: 0 !important;
563
- transform: translateY(-4px) !important;
564
- pointer-events: none !important;
565
- padding-top: 0 !important;
566
- padding-bottom: 0 !important;
567
- border-top-width: 0 !important;
568
- border-bottom-width: 0 !important;
569
- max-height: 0 !important;
570
- margin-top: 0 !important;
571
- margin-bottom: 0 !important;
572
- overflow: hidden !important;
573
- }
574
- `;
633
+ class ToggleManager {
634
+ /**
635
+ * Apply toggle visibility to all toggle elements in the DOM
636
+ */
637
+ static applyToggles(rootEl, activeToggles) {
638
+ rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
639
+ const categories = this.getToggleCategories(el);
640
+ const shouldShow = categories.some(cat => activeToggles.includes(cat));
641
+ this.applyToggleVisibility(el, shouldShow);
642
+ });
643
+ }
644
+ /**
645
+ * Render assets into toggle elements that are currently visible
646
+ */
647
+ static renderAssets(rootEl, activeToggles, assetsManager) {
648
+ rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
649
+ const categories = this.getToggleCategories(el);
650
+ const toggleId = this.getToggleId(el);
651
+ if (toggleId && categories.some(cat => activeToggles.includes(cat))) {
652
+ renderAssetInto(el, toggleId, assetsManager);
653
+ }
654
+ });
655
+ }
656
+ /**
657
+ * Get toggle categories from an element (supports both data attributes and cv-toggle elements)
658
+ */
659
+ static getToggleCategories(el) {
660
+ if (el.tagName.toLowerCase() === 'cv-toggle') {
661
+ const category = el.getAttribute('category');
662
+ return (category || '').split(/\s+/).filter(Boolean);
663
+ }
664
+ else {
665
+ const data = el.dataset.cvToggle || el.dataset.customviewsToggle;
666
+ return (data || '').split(/\s+/).filter(Boolean);
667
+ }
668
+ }
669
+ /**
670
+ * Get toggle ID from an element
671
+ */
672
+ static getToggleId(el) {
673
+ return el.dataset.cvId || el.dataset.customviewsId || el.getAttribute('data-cv-id') || el.getAttribute('data-customviews-id') || undefined;
674
+ }
675
+ /**
676
+ * Apply simple class-based visibility to a toggle element
677
+ */
678
+ static applyToggleVisibility(el, visible) {
679
+ if (visible) {
680
+ el.classList.remove('cv-hidden');
681
+ el.classList.add('cv-visible');
682
+ }
683
+ else {
684
+ el.classList.add('cv-hidden');
685
+ el.classList.remove('cv-visible');
686
+ }
687
+ }
688
+ }
575
689
 
576
690
  /**
577
- * Styles for tab groups and tab navigation
691
+ * Styles for toggle visibility and animations
578
692
  */
579
- const TAB_STYLES = `
580
- /* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
581
- .cv-tabs-nav {
582
- display: flex;
583
- flex-wrap: wrap;
584
- padding-left: 0;
585
- margin-top: 0.5rem;
586
- margin-bottom: 1rem;
587
- list-style: none;
588
- border-bottom: 1px solid #dee2e6;
589
- }
590
-
591
- .cv-tabs-nav .nav-item {
592
- margin-bottom: -1px;
593
- list-style: none;
594
- display: inline-block;
595
- }
596
-
597
- .cv-tabs-nav .nav-link {
598
- display: block;
599
- padding: 0.5rem 1rem;
600
- color: #495057;
601
- text-decoration: none;
602
- background-color: transparent;
603
- border: 1px solid transparent;
604
- border-top-left-radius: 0.25rem;
605
- border-top-right-radius: 0.25rem;
606
- transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
607
- cursor: pointer;
608
- }
609
-
610
- .cv-tabs-nav .nav-link:hover,
611
- .cv-tabs-nav .nav-link:focus {
612
- border-color: #e9ecef #e9ecef #dee2e6;
613
- isolation: isolate;
614
- }
615
-
616
- .cv-tabs-nav .nav-link.active {
617
- color: #495057;
618
- background-color: #fff;
619
- border-color: #dee2e6 #dee2e6 #fff;
620
- }
621
-
622
- .cv-tabs-nav .nav-link:focus {
623
- outline: 0;
624
- box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
625
- }
626
-
627
- /* Legacy button-based nav (deprecated, kept for compatibility) */
628
- .cv-tabs-nav-item {
629
- background: none;
630
- border: none;
631
- border-bottom: 2px solid transparent;
632
- padding: 0.5rem 1rem;
633
- cursor: pointer;
634
- font-size: 1rem;
635
- color: #6c757d;
636
- transition: color 150ms ease, border-color 150ms ease;
637
- }
638
-
639
- .cv-tabs-nav-item:hover {
640
- color: #495057;
641
- border-bottom-color: #dee2e6;
642
- }
643
-
644
- .cv-tabs-nav-item.active {
645
- color: #007bff;
646
- border-bottom-color: #007bff;
647
- font-weight: 500;
648
- }
649
-
650
- .cv-tabs-nav-item:focus {
651
- outline: 2px solid #007bff;
652
- outline-offset: 2px;
653
- }
654
-
655
- /* Tab panel base styles */
656
- cv-tab {
657
- display: block;
658
- }
659
-
660
- /* Override visibility for tab panels - use display instead of collapse animation */
661
- cv-tab.cv-hidden {
662
- display: none !important;
663
- }
664
-
665
- cv-tab.cv-visible {
666
- display: block !important;
667
- }
668
-
669
- cv-tabgroup {
670
- display: block;
671
- margin-bottom: 1.5rem;
672
- }
673
-
674
- /* Bottom border line for tab groups */
675
- .cv-tabgroup-bottom-border {
676
- border-bottom: 1px solid #dee2e6;
677
- margin-top: 1rem;
678
- }
679
-
680
- /* Tab content wrapper */
681
- .cv-tab-content {
682
- padding: 1rem 0;
683
- }
693
+ const TOGGLE_STYLES = `
694
+ /* Core toggle visibility transitions */
695
+ [data-cv-toggle], [data-customviews-toggle], cv-toggle {
696
+ transition: opacity 150ms ease,
697
+ transform 150ms ease,
698
+ max-height 200ms ease,
699
+ margin 150ms ease;
700
+ will-change: opacity, transform, max-height, margin;
701
+ }
702
+
703
+ .cv-visible {
704
+ opacity: 1 !important;
705
+ transform: translateY(0) !important;
706
+ max-height: var(--cv-max-height, 9999px) !important;
707
+ }
708
+
709
+ .cv-hidden {
710
+ opacity: 0 !important;
711
+ transform: translateY(-4px) !important;
712
+ pointer-events: none !important;
713
+ padding-top: 0 !important;
714
+ padding-bottom: 0 !important;
715
+ border-top-width: 0 !important;
716
+ border-bottom-width: 0 !important;
717
+ max-height: 0 !important;
718
+ margin-top: 0 !important;
719
+ margin-bottom: 0 !important;
720
+ overflow: hidden !important;
721
+ }
722
+ `;
723
+
724
+ /**
725
+ * Styles for tab groups and tab navigation
726
+ */
727
+ const TAB_STYLES = `
728
+ /* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
729
+ .cv-tabs-nav {
730
+ display: flex;
731
+ flex-wrap: wrap;
732
+ padding-left: 0;
733
+ margin-top: 0.5rem;
734
+ margin-bottom: 1rem;
735
+ list-style: none;
736
+ border-bottom: 1px solid #dee2e6;
737
+ }
738
+
739
+ .cv-tabs-nav .nav-item {
740
+ margin-bottom: -1px;
741
+ list-style: none;
742
+ display: inline-block;
743
+ }
744
+
745
+ .cv-tabs-nav .nav-link {
746
+ display: block;
747
+ padding: 0.5rem 1rem;
748
+ color: #495057;
749
+ text-decoration: none;
750
+ background-color: transparent;
751
+ border: 1px solid transparent;
752
+ border-top-left-radius: 0.25rem;
753
+ border-top-right-radius: 0.25rem;
754
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
755
+ cursor: pointer;
756
+ }
757
+
758
+ .cv-tabs-nav .nav-link:hover,
759
+ .cv-tabs-nav .nav-link:focus {
760
+ border-color: #e9ecef #e9ecef #dee2e6;
761
+ isolation: isolate;
762
+ }
763
+
764
+ .cv-tabs-nav .nav-link.active {
765
+ color: #495057;
766
+ background-color: #fff;
767
+ border-color: #dee2e6 #dee2e6 #fff;
768
+ }
769
+
770
+ .cv-tabs-nav .nav-link:focus {
771
+ outline: 0;
772
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
773
+ }
774
+
775
+ /* Legacy button-based nav (deprecated, kept for compatibility) */
776
+ .cv-tabs-nav-item {
777
+ background: none;
778
+ border: none;
779
+ border-bottom: 2px solid transparent;
780
+ padding: 0.5rem 1rem;
781
+ cursor: pointer;
782
+ font-size: 1rem;
783
+ color: #6c757d;
784
+ transition: color 150ms ease, border-color 150ms ease;
785
+ }
786
+
787
+ .cv-tabs-nav-item:hover {
788
+ color: #495057;
789
+ border-bottom-color: #dee2e6;
790
+ }
791
+
792
+ .cv-tabs-nav-item.active {
793
+ color: #007bff;
794
+ border-bottom-color: #007bff;
795
+ font-weight: 500;
796
+ }
797
+
798
+ .cv-tabs-nav-item:focus {
799
+ outline: 2px solid #007bff;
800
+ outline-offset: 2px;
801
+ }
802
+
803
+ /* Tab panel base styles */
804
+ cv-tab {
805
+ display: block;
806
+ }
807
+
808
+ /* Override visibility for tab panels - use display instead of collapse animation */
809
+ cv-tab.cv-hidden {
810
+ display: none !important;
811
+ }
812
+
813
+ cv-tab.cv-visible {
814
+ display: block !important;
815
+ }
816
+
817
+ cv-tabgroup {
818
+ display: block;
819
+ margin-bottom: 1.5rem;
820
+ }
821
+
822
+ /* Bottom border line for tab groups */
823
+ .cv-tabgroup-bottom-border {
824
+ border-bottom: 1px solid #dee2e6;
825
+ margin-top: 1rem;
826
+ }
827
+
828
+ /* Tab content wrapper */
829
+ .cv-tab-content {
830
+ padding: 1rem 0;
831
+ }
684
832
  `;
685
833
 
686
834
  /**
687
835
  * Combined core styles for toggles and tabs
688
836
  */
689
- const CORE_STYLES = `
690
- ${TOGGLE_STYLES}
691
-
692
- ${TAB_STYLES}
837
+ const CORE_STYLES = `
838
+ ${TOGGLE_STYLES}
839
+
840
+ ${TAB_STYLES}
693
841
  `;
694
842
  /**
695
843
  * Add styles for hiding and showing toggles animations and transitions to the document head
@@ -819,23 +967,10 @@ class CustomViewsCore {
819
967
  this.lastAppliedState = this.cloneState(state);
820
968
  const toggles = state.toggles || [];
821
969
  const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
822
- // Toggles hide or show relevant toggles
823
- this.rootEl.querySelectorAll("[data-cv-toggle], [data-customviews-toggle]").forEach(el => {
824
- const category = el.dataset.cvToggle || el.dataset.customviewsToggle;
825
- const shouldShow = !!category && finalToggles.includes(category);
826
- this.visibilityManager.applyElementVisibility(el, shouldShow);
827
- });
828
- // Render toggles
829
- for (const category of finalToggles) {
830
- this.rootEl.querySelectorAll(`[data-cv-toggle="${category}"], [data-customviews-toggle="${category}"]`).forEach(el => {
831
- // if it has an id, then we should render the asset into it
832
- // Support both (data-cv-id) and (data-customviews-id) attributes
833
- const toggleId = el.dataset.cvId || el.dataset.customviewsId;
834
- if (toggleId) {
835
- renderAssetInto(el, toggleId, this.assetsManager);
836
- }
837
- });
838
- }
970
+ // Apply toggle visibility
971
+ ToggleManager.applyToggles(this.rootEl, finalToggles);
972
+ // Render assets into toggles
973
+ ToggleManager.renderAssets(this.rootEl, finalToggles, this.assetsManager);
839
974
  // Apply tab selections
840
975
  TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
841
976
  // Update nav active states (without rebuilding)
@@ -948,52 +1083,6 @@ class CustomViewsCore {
948
1083
  }
949
1084
  }
950
1085
 
951
- class AssetsManager {
952
- assets;
953
- baseURL;
954
- constructor(assets, baseURL = '') {
955
- this.assets = assets;
956
- this.baseURL = baseURL;
957
- if (!this.validate()) {
958
- console.warn('Invalid assets:', this.assets);
959
- }
960
- }
961
- // Check each asset has content or src
962
- validate() {
963
- return Object.values(this.assets).every(a => a.src || a.content);
964
- }
965
- get(assetId) {
966
- const asset = this.assets[assetId];
967
- if (!asset)
968
- return undefined;
969
- // If there's a baseURL and the asset has a src property, prepend the baseURL
970
- if (this.baseURL && asset.src) {
971
- // Create a shallow copy to avoid mutating the original asset
972
- return {
973
- ...asset,
974
- src: this.prependBaseURL(asset.src)
975
- };
976
- }
977
- return asset;
978
- }
979
- prependBaseURL(path) {
980
- // Don't prepend if the path is already absolute (starts with http:// or https://)
981
- if (path.startsWith('http://') || path.startsWith('https://')) {
982
- return path;
983
- }
984
- // Ensure baseURL doesn't end with / and path starts with /
985
- const cleanBaseURL = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
986
- const cleanPath = path.startsWith('/') ? path : '/' + path;
987
- return cleanBaseURL + cleanPath;
988
- }
989
- loadFromJSON(json) {
990
- this.assets = json;
991
- }
992
- loadAdditionalAssets(additionalAssets) {
993
- this.assets = { ...this.assets, ...additionalAssets };
994
- }
995
- }
996
-
997
1086
  /**
998
1087
  * Helper function to prepend baseUrl to a path
999
1088
  * @param path The path to prepend the baseUrl to
@@ -1013,6 +1102,57 @@ function prependBaseUrl(path, baseUrl) {
1013
1102
  return cleanbaseUrl + cleanPath;
1014
1103
  }
1015
1104
 
1105
+ /**
1106
+ * Custom Elements for Tab Groups and Tabs
1107
+ */
1108
+ /**
1109
+ * <cv-tab> element - represents a single tab panel
1110
+ */
1111
+ class CVTab extends HTMLElement {
1112
+ connectedCallback() {
1113
+ // Element is managed by TabManager
1114
+ }
1115
+ }
1116
+ /**
1117
+ * <cv-tabgroup> element - represents a group of tabs
1118
+ */
1119
+ class CVTabgroup extends HTMLElement {
1120
+ connectedCallback() {
1121
+ // Element is managed by TabManager
1122
+ // Emit ready event after a brief delay to ensure children are parsed
1123
+ setTimeout(() => {
1124
+ const event = new CustomEvent('cv:tabgroup-ready', {
1125
+ bubbles: true,
1126
+ detail: { groupId: this.getAttribute('id') }
1127
+ });
1128
+ this.dispatchEvent(event);
1129
+ }, 0);
1130
+ }
1131
+ }
1132
+ /**
1133
+ * <cv-toggle> element - represents a toggleable content block
1134
+ */
1135
+ class CVToggle extends HTMLElement {
1136
+ connectedCallback() {
1137
+ // Element is managed by Core
1138
+ }
1139
+ }
1140
+ /**
1141
+ * Register custom elements
1142
+ */
1143
+ function registerCustomElements() {
1144
+ // Only register if not already defined
1145
+ if (!customElements.get('cv-tab')) {
1146
+ customElements.define('cv-tab', CVTab);
1147
+ }
1148
+ if (!customElements.get('cv-tabgroup')) {
1149
+ customElements.define('cv-tabgroup', CVTabgroup);
1150
+ }
1151
+ if (!customElements.get('cv-toggle')) {
1152
+ customElements.define('cv-toggle', CVToggle);
1153
+ }
1154
+ }
1155
+
1016
1156
  /**
1017
1157
  * Main CustomViews class for initializing and managing custom views
1018
1158
  */
@@ -1023,6 +1163,8 @@ class CustomViews {
1023
1163
  * @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
1024
1164
  */
1025
1165
  static async init(opts) {
1166
+ // Register custom elements
1167
+ registerCustomElements();
1026
1168
  // Load assets JSON if provided
1027
1169
  let assetsManager;
1028
1170
  const baseURL = opts.baseURL || '';
@@ -1065,627 +1207,644 @@ class CustomViews {
1065
1207
  * Note: Styles are kept as a TypeScript string for compatibility with the build system.
1066
1208
  * This approach ensures the styles are properly bundled and don't require separate CSS file handling.
1067
1209
  */
1068
- const WIDGET_STYLES = `
1069
- /* Rounded rectangle widget icon styles */
1070
- .cv-widget-icon {
1071
- position: fixed;
1072
- /* Slightly transparent by default so the widget is subtle at the page edge */
1073
- background: rgba(255, 255, 255, 0.92);
1074
- color: rgba(0, 0, 0, 0.9);
1075
- opacity: 0.6;
1076
- display: flex;
1077
- align-items: center;
1078
- justify-content: center;
1079
- font-size: 18px;
1080
- font-weight: bold;
1081
- cursor: pointer;
1082
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1083
- z-index: 9998;
1084
- transition: all 0.3s ease;
1085
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1086
- }
1087
-
1088
- .cv-widget-icon:hover {
1089
- /* Become fully opaque on hover to improve readability */
1090
- background: rgba(255, 255, 255, 1);
1091
- color: rgba(0, 0, 0, 1);
1092
- opacity: 1;
1093
- }
1094
-
1095
- /* Top-right: rounded end on left, sticks out leftward on hover */
1096
- .cv-widget-top-right {
1097
- top: 20px;
1098
- right: 0;
1099
- border-radius: 18px 0 0 18px;
1100
- padding-left: 8px;
1101
- justify-content: flex-start;
1102
- }
1103
-
1104
- /* Top-left: rounded end on right, sticks out rightward on hover */
1105
- .cv-widget-top-left {
1106
- top: 20px;
1107
- left: 0;
1108
- border-radius: 0 18px 18px 0;
1109
- padding-right: 8px;
1110
- justify-content: flex-end;
1111
- }
1112
-
1113
- /* Bottom-right: rounded end on left, sticks out leftward on hover */
1114
- .cv-widget-bottom-right {
1115
- bottom: 20px;
1116
- right: 0;
1117
- border-radius: 18px 0 0 18px;
1118
- padding-left: 8px;
1119
- justify-content: flex-start;
1120
- }
1121
-
1122
- /* Bottom-left: rounded end on right, sticks out rightward on hover */
1123
- .cv-widget-bottom-left {
1124
- bottom: 20px;
1125
- left: 0;
1126
- border-radius: 0 18px 18px 0;
1127
- padding-right: 8px;
1128
- justify-content: flex-end;
1129
- }
1130
-
1131
- /* Middle-left: rounded end on right, sticks out rightward on hover */
1132
- .cv-widget-middle-left {
1133
- top: 50%;
1134
- left: 0;
1135
- transform: translateY(-50%);
1136
- border-radius: 0 18px 18px 0;
1137
- padding-right: 8px;
1138
- justify-content: flex-end;
1139
- }
1140
-
1141
- /* Middle-right: rounded end on left, sticks out leftward on hover */
1142
- .cv-widget-middle-right {
1143
- top: 50%;
1144
- right: 0;
1145
- transform: translateY(-50%);
1146
- border-radius: 18px 0 0 18px;
1147
- padding-left: 8px;
1148
- justify-content: flex-start;
1149
- }
1150
-
1151
- .cv-widget-top-right,
1152
- .cv-widget-middle-right,
1153
- .cv-widget-bottom-right,
1154
- .cv-widget-top-left,
1155
- .cv-widget-middle-left,
1156
- .cv-widget-bottom-left {
1157
- height: 36px;
1158
- width: 36px;
1159
- }
1160
-
1161
- .cv-widget-middle-right:hover,
1162
- .cv-widget-top-right:hover,
1163
- .cv-widget-bottom-right:hover,
1164
- .cv-widget-top-left:hover,
1165
- .cv-widget-middle-left:hover,
1166
- .cv-widget-bottom-left:hover {
1167
- width: 55px;
1168
- }
1169
-
1170
- /* Modal content styles */
1171
- .cv-widget-section {
1172
- margin-bottom: 16px;
1173
- }
1174
-
1175
- .cv-widget-section:last-child {
1176
- margin-bottom: 0;
1177
- }
1178
-
1179
- .cv-widget-section label {
1180
- display: block;
1181
- margin-bottom: 4px;
1182
- font-weight: 500;
1183
- color: #555;
1184
- }
1185
-
1186
- .cv-widget-profile-select,
1187
- .cv-widget-state-select {
1188
- width: 100%;
1189
- padding: 8px 12px;
1190
- border: 1px solid #ddd;
1191
- border-radius: 4px;
1192
- background: white;
1193
- font-size: 14px;
1194
- }
1195
-
1196
- .cv-widget-profile-select:focus,
1197
- .cv-widget-state-select:focus {
1198
- outline: none;
1199
- border-color: #007bff;
1200
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1201
- }
1202
-
1203
- .cv-widget-profile-select:disabled,
1204
- .cv-widget-state-select:disabled {
1205
- background: #f8f9fa;
1206
- color: #6c757d;
1207
- cursor: not-allowed;
1208
- }
1209
-
1210
- .cv-widget-current {
1211
- margin: 16px 0;
1212
- padding: 12px;
1213
- background: #f8f9fa;
1214
- border-radius: 4px;
1215
- border-left: 4px solid #007bff;
1216
- }
1217
-
1218
- .cv-widget-current label {
1219
- font-size: 12px;
1220
- text-transform: uppercase;
1221
- letter-spacing: 0.5px;
1222
- color: #666;
1223
- margin-bottom: 4px;
1224
- }
1225
-
1226
- .cv-widget-current-view {
1227
- font-weight: 500;
1228
- color: #333;
1229
- }
1230
-
1231
- .cv-widget-reset {
1232
- width: 100%;
1233
- padding: 8px 16px;
1234
- background: #dc3545;
1235
- color: white;
1236
- border: none;
1237
- border-radius: 4px;
1238
- cursor: pointer;
1239
- font-size: 14px;
1240
- font-weight: 500;
1241
- }
1242
-
1243
- .cv-widget-reset:hover {
1244
- background: #c82333;
1245
- }
1246
-
1247
- .cv-widget-reset:active {
1248
- background: #bd2130;
1249
- }
1250
-
1251
- /* Responsive design for mobile */
1252
- @media (max-width: 768px) {
1253
- .cv-widget-top-right,
1254
- .cv-widget-top-left {
1255
- top: 10px;
1256
- }
1257
-
1258
- .cv-widget-bottom-right,
1259
- .cv-widget-bottom-left {
1260
- bottom: 10px;
1261
- }
1262
-
1263
- /* All widgets stay flush with screen edges */
1264
- .cv-widget-top-right,
1265
- .cv-widget-bottom-right,
1266
- .cv-widget-middle-right {
1267
- right: 0;
1268
- }
1269
-
1270
- .cv-widget-top-left,
1271
- .cv-widget-bottom-left,
1272
- .cv-widget-middle-left {
1273
- left: 0;
1274
- }
1275
-
1276
- /* Slightly smaller on mobile */
1277
- .cv-widget-icon {
1278
- width: 60px;
1279
- height: 32px;
1280
- }
1281
-
1282
- .cv-widget-icon:hover {
1283
- width: 75px;
1284
- }
1285
- }
1286
-
1287
- /* Modal styles */
1288
- .cv-widget-modal-overlay {
1289
- position: fixed;
1290
- top: 0;
1291
- left: 0;
1292
- right: 0;
1293
- bottom: 0;
1294
- background: rgba(0, 0, 0, 0.5);
1295
- display: flex;
1296
- align-items: center;
1297
- justify-content: center;
1298
- z-index: 10002;
1299
- animation: fadeIn 0.2s ease;
1300
- }
1301
-
1302
- @keyframes fadeIn {
1303
- from { opacity: 0; }
1304
- to { opacity: 1; }
1305
- }
1306
-
1307
- .cv-widget-modal {
1308
- background: white;
1309
- border-radius: 8px;
1310
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
1311
- max-width: 400px;
1312
- width: 90vw;
1313
- max-height: 80vh;
1314
- overflow-y: auto;
1315
- animation: slideIn 0.2s ease;
1316
- }
1317
-
1318
- @keyframes slideIn {
1319
- from {
1320
- opacity: 0;
1321
- transform: scale(0.9) translateY(-20px);
1322
- }
1323
- to {
1324
- opacity: 1;
1325
- transform: scale(1) translateY(0);
1326
- }
1327
- }
1328
-
1329
- .cv-widget-modal-header {
1330
- display: flex;
1331
- justify-content: space-between;
1332
- align-items: center;
1333
- padding: 16px 20px;
1334
- border-bottom: 1px solid #e9ecef;
1335
- background: #f8f9fa;
1336
- border-radius: 8px 8px 0 0;
1337
- }
1338
-
1339
- .cv-widget-modal-header h3 {
1340
- margin: 0;
1341
- font-size: 18px;
1342
- font-weight: 600;
1343
- color: #333;
1344
- }
1345
-
1346
- .cv-widget-modal-close {
1347
- background: none;
1348
- border: none;
1349
- font-size: 24px;
1350
- cursor: pointer;
1351
- padding: 0;
1352
- width: 32px;
1353
- height: 32px;
1354
- display: flex;
1355
- align-items: center;
1356
- justify-content: center;
1357
- border-radius: 4px;
1358
- color: #666;
1359
- }
1360
-
1361
- .cv-widget-modal-close:hover {
1362
- background: #e9ecef;
1363
- }
1364
-
1365
- .cv-widget-modal-content {
1366
- padding: 20px;
1367
- }
1368
-
1369
- .cv-widget-modal-actions {
1370
- margin-top: 20px;
1371
- padding-top: 16px;
1372
- border-top: 1px solid #e9ecef;
1373
- }
1374
-
1375
- .cv-widget-restore {
1376
- width: 100%;
1377
- padding: 10px 16px;
1378
- background: #28a745;
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-restore:hover {
1388
- background: #218838;
1389
- }
1390
-
1391
- .cv-widget-create-state {
1392
- width: 100%;
1393
- padding: 10px 16px;
1394
- background: #007bff;
1395
- color: white;
1396
- border: none;
1397
- border-radius: 4px;
1398
- cursor: pointer;
1399
- font-size: 14px;
1400
- font-weight: 500;
1401
- margin-bottom: 10px;
1402
- }
1403
-
1404
- .cv-widget-create-state:hover {
1405
- background: #0056b3;
1406
- }
1407
-
1408
- /* Dark theme modal styles */
1409
- .cv-widget-theme-dark .cv-widget-modal {
1410
- background: #2d3748;
1411
- color: #e2e8f0;
1412
- }
1413
-
1414
- .cv-widget-theme-dark .cv-widget-modal-header {
1415
- background: #1a202c;
1416
- border-color: #4a5568;
1417
- }
1418
-
1419
- .cv-widget-theme-dark .cv-widget-modal-header h3 {
1420
- color: #e2e8f0;
1421
- }
1422
-
1423
- .cv-widget-theme-dark .cv-widget-modal-close {
1424
- color: #a0aec0;
1425
- }
1426
-
1427
- .cv-widget-theme-dark .cv-widget-modal-close:hover {
1428
- background: #4a5568;
1429
- }
1430
-
1431
- .cv-widget-theme-dark .cv-widget-modal-actions {
1432
- border-color: #4a5568;
1433
- }
1434
-
1435
- /* Custom state creator styles */
1436
- .cv-custom-state-modal {
1437
- max-width: 500px;
1438
- }
1439
-
1440
- .cv-custom-state-form h4 {
1441
- margin: 20px 0 10px 0;
1442
- font-size: 16px;
1443
- font-weight: 600;
1444
- color: #333;
1445
- border-bottom: 1px solid #e9ecef;
1446
- padding-bottom: 5px;
1447
- }
1448
-
1449
- .cv-custom-state-section {
1450
- margin-bottom: 16px;
1451
- }
1452
-
1453
- .cv-custom-state-section label {
1454
- display: block;
1455
- margin-bottom: 4px;
1456
- font-weight: 500;
1457
- color: #555;
1458
- }
1459
-
1460
- .cv-custom-state-input {
1461
- width: 100%;
1462
- padding: 8px 12px;
1463
- border: 1px solid #ddd;
1464
- border-radius: 4px;
1465
- background: white;
1466
- font-size: 14px;
1467
- }
1468
-
1469
- .cv-custom-state-input:focus {
1470
- outline: none;
1471
- border-color: #007bff;
1472
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1473
- }
1474
-
1475
- .cv-custom-toggles {
1476
- display: grid;
1477
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1478
- gap: 10px;
1479
- }
1480
-
1481
- .cv-custom-state-toggle {
1482
- display: flex;
1483
- align-items: center;
1484
- }
1485
-
1486
- .cv-custom-state-toggle label {
1487
- display: flex;
1488
- align-items: center;
1489
- cursor: pointer;
1490
- font-weight: normal;
1491
- margin: 0;
1492
- }
1493
-
1494
- .cv-custom-toggle-checkbox {
1495
- margin-right: 8px;
1496
- width: auto;
1497
- }
1498
-
1499
- .cv-tab-groups {
1500
- margin-top: 20px;
1501
- }
1502
-
1503
- .cv-tab-group-control {
1504
- margin-bottom: 15px;
1505
- }
1506
-
1507
- .cv-tab-group-control label {
1508
- display: block;
1509
- margin-bottom: 5px;
1510
- font-weight: 500;
1511
- font-size: 14px;
1512
- }
1513
-
1514
- .cv-tab-group-select {
1515
- width: 100%;
1516
- padding: 8px 12px;
1517
- border: 1px solid #ced4da;
1518
- border-radius: 4px;
1519
- font-size: 14px;
1520
- background-color: white;
1521
- cursor: pointer;
1522
- }
1523
-
1524
- .cv-tab-group-select:focus {
1525
- outline: none;
1526
- border-color: #007bff;
1527
- box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
1528
- }
1529
-
1530
- .cv-widget-theme-dark .cv-tab-group-select {
1531
- background-color: #2d3748;
1532
- border-color: #4a5568;
1533
- color: #e2e8f0;
1534
- }
1535
-
1536
- .cv-custom-state-actions {
1537
- display: flex;
1538
- gap: 10px;
1539
- margin-top: 20px;
1540
- padding-top: 16px;
1541
- border-top: 1px solid #e9ecef;
1542
- }
1543
-
1544
- .cv-custom-state-cancel,
1545
- .cv-custom-state-copy-url {
1546
- flex: 1;
1547
- padding: 10px 16px;
1548
- border: none;
1549
- border-radius: 4px;
1550
- cursor: pointer;
1551
- font-size: 14px;
1552
- font-weight: 500;
1553
- }
1554
-
1555
- .cv-custom-state-reset {
1556
- flex: 1;
1557
- padding: 10px 16px;
1558
- border: none;
1559
- border-radius: 4px;
1560
- cursor: pointer;
1561
- font-size: 14px;
1562
- font-weight: 500;
1563
- background: #dc3545;
1564
- color: white;
1565
- }
1566
-
1567
- .cv-custom-state-reset:hover {
1568
- background: #c82333;
1569
- }
1570
-
1571
- .cv-custom-state-cancel {
1572
- background: #6c757d;
1573
- color: white;
1574
- }
1575
-
1576
- .cv-custom-state-cancel:hover {
1577
- background: #5a6268;
1578
- }
1579
-
1580
- .cv-custom-state-copy-url {
1581
- background: #28a745;
1582
- color: white;
1583
- }
1584
-
1585
- .cv-custom-state-copy-url:hover {
1586
- background: #218838;
1587
- }
1588
-
1589
- /* Dark theme custom state styles */
1590
- .cv-widget-theme-dark .cv-custom-state-form h4 {
1591
- color: #e2e8f0;
1592
- border-color: #4a5568;
1593
- }
1594
-
1595
- .cv-widget-theme-dark .cv-custom-state-section label {
1596
- color: #a0aec0;
1597
- }
1598
-
1599
- .cv-widget-theme-dark .cv-custom-state-input {
1600
- background: #1a202c;
1601
- border-color: #4a5568;
1602
- color: #e2e8f0;
1603
- }
1604
-
1605
- .cv-widget-theme-dark .cv-custom-state-actions {
1606
- border-color: #4a5568;
1607
- }
1608
-
1609
- /* Welcome modal styles */
1610
- .cv-welcome-modal {
1611
- max-width: 500px;
1612
- }
1613
-
1614
- .cv-welcome-content {
1615
- text-align: center;
1616
- }
1617
-
1618
- .cv-welcome-content p {
1619
- font-size: 15px;
1620
- line-height: 1.6;
1621
- color: #555;
1622
- margin-bottom: 24px;
1623
- }
1624
-
1625
- .cv-welcome-widget-preview {
1626
- display: flex;
1627
- flex-direction: column;
1628
- align-items: center;
1629
- gap: 12px;
1630
- padding: 20px;
1631
- background: #f8f9fa;
1632
- border-radius: 8px;
1633
- margin-bottom: 24px;
1634
- }
1635
-
1636
- .cv-welcome-widget-icon {
1637
- width: 36px;
1638
- height: 36px;
1639
- background: white;
1640
- color: black;
1641
- border-radius: 0 18px 18px 0;
1642
- display: flex;
1643
- align-items: center;
1644
- justify-content: center;
1645
- font-size: 18px;
1646
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1647
- }
1648
-
1649
- .cv-welcome-widget-label {
1650
- font-size: 14px;
1651
- color: #666;
1652
- margin: 0;
1653
- font-weight: 500;
1654
- }
1655
-
1656
- .cv-welcome-got-it {
1657
- width: 100%;
1658
- padding: 12px 24px;
1659
- background: #007bff;
1660
- color: white;
1661
- border: none;
1662
- border-radius: 4px;
1663
- cursor: pointer;
1664
- font-size: 16px;
1665
- font-weight: 600;
1666
- transition: background 0.2s ease;
1667
- }
1668
-
1669
- .cv-welcome-got-it:hover {
1670
- background: #0056b3;
1671
- }
1672
-
1673
- .cv-welcome-got-it:active {
1674
- background: #004494;
1675
- }
1676
-
1677
- /* Dark theme welcome modal styles */
1678
- .cv-widget-theme-dark .cv-welcome-content p {
1679
- color: #cbd5e0;
1680
- }
1681
-
1682
- .cv-widget-theme-dark .cv-welcome-widget-preview {
1683
- background: #1a202c;
1684
- }
1685
-
1686
- .cv-widget-theme-dark .cv-welcome-widget-label {
1687
- color: #a0aec0;
1688
- }
1210
+ const WIDGET_STYLES = `
1211
+ /* Rounded rectangle widget icon styles */
1212
+ .cv-widget-icon {
1213
+ position: fixed;
1214
+ /* Slightly transparent by default so the widget is subtle at the page edge */
1215
+ background: rgba(255, 255, 255, 0.92);
1216
+ color: rgba(0, 0, 0, 0.9);
1217
+ opacity: 0.6;
1218
+ display: flex;
1219
+ align-items: center;
1220
+ justify-content: center;
1221
+ font-size: 18px;
1222
+ font-weight: bold;
1223
+ cursor: pointer;
1224
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1225
+ z-index: 9998;
1226
+ transition: all 0.3s ease;
1227
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1228
+ }
1229
+
1230
+ .cv-widget-icon:hover {
1231
+ /* Become fully opaque on hover to improve readability */
1232
+ background: rgba(255, 255, 255, 1);
1233
+ color: rgba(0, 0, 0, 1);
1234
+ opacity: 1;
1235
+ }
1236
+
1237
+ /* Top-right: rounded end on left, sticks out leftward on hover */
1238
+ .cv-widget-top-right {
1239
+ top: 20px;
1240
+ right: 0;
1241
+ border-radius: 18px 0 0 18px;
1242
+ padding-left: 8px;
1243
+ justify-content: flex-start;
1244
+ }
1245
+
1246
+ /* Top-left: rounded end on right, sticks out rightward on hover */
1247
+ .cv-widget-top-left {
1248
+ top: 20px;
1249
+ left: 0;
1250
+ border-radius: 0 18px 18px 0;
1251
+ padding-right: 8px;
1252
+ justify-content: flex-end;
1253
+ }
1254
+
1255
+ /* Bottom-right: rounded end on left, sticks out leftward on hover */
1256
+ .cv-widget-bottom-right {
1257
+ bottom: 20px;
1258
+ right: 0;
1259
+ border-radius: 18px 0 0 18px;
1260
+ padding-left: 8px;
1261
+ justify-content: flex-start;
1262
+ }
1263
+
1264
+ /* Bottom-left: rounded end on right, sticks out rightward on hover */
1265
+ .cv-widget-bottom-left {
1266
+ bottom: 20px;
1267
+ left: 0;
1268
+ border-radius: 0 18px 18px 0;
1269
+ padding-right: 8px;
1270
+ justify-content: flex-end;
1271
+ }
1272
+
1273
+ /* Middle-left: rounded end on right, sticks out rightward on hover */
1274
+ .cv-widget-middle-left {
1275
+ top: 50%;
1276
+ left: 0;
1277
+ transform: translateY(-50%);
1278
+ border-radius: 0 18px 18px 0;
1279
+ padding-right: 8px;
1280
+ justify-content: flex-end;
1281
+ }
1282
+
1283
+ /* Middle-right: rounded end on left, sticks out leftward on hover */
1284
+ .cv-widget-middle-right {
1285
+ top: 50%;
1286
+ right: 0;
1287
+ transform: translateY(-50%);
1288
+ border-radius: 18px 0 0 18px;
1289
+ padding-left: 8px;
1290
+ justify-content: flex-start;
1291
+ }
1292
+
1293
+ .cv-widget-top-right,
1294
+ .cv-widget-middle-right,
1295
+ .cv-widget-bottom-right,
1296
+ .cv-widget-top-left,
1297
+ .cv-widget-middle-left,
1298
+ .cv-widget-bottom-left {
1299
+ height: 36px;
1300
+ width: 36px;
1301
+ }
1302
+
1303
+ .cv-widget-middle-right:hover,
1304
+ .cv-widget-top-right:hover,
1305
+ .cv-widget-bottom-right:hover,
1306
+ .cv-widget-top-left:hover,
1307
+ .cv-widget-middle-left:hover,
1308
+ .cv-widget-bottom-left:hover {
1309
+ width: 55px;
1310
+ }
1311
+
1312
+ /* Modal content styles */
1313
+ .cv-widget-section {
1314
+ margin-bottom: 16px;
1315
+ }
1316
+
1317
+ .cv-widget-section:last-child {
1318
+ margin-bottom: 0;
1319
+ }
1320
+
1321
+ .cv-widget-section label {
1322
+ display: block;
1323
+ margin-bottom: 4px;
1324
+ font-weight: 500;
1325
+ color: #555;
1326
+ }
1327
+
1328
+ .cv-widget-profile-select,
1329
+ .cv-widget-state-select {
1330
+ width: 100%;
1331
+ padding: 8px 12px;
1332
+ border: 1px solid #ddd;
1333
+ border-radius: 4px;
1334
+ background: white;
1335
+ font-size: 14px;
1336
+ }
1337
+
1338
+ .cv-widget-profile-select:focus,
1339
+ .cv-widget-state-select:focus {
1340
+ outline: none;
1341
+ border-color: #007bff;
1342
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1343
+ }
1344
+
1345
+ .cv-widget-profile-select:disabled,
1346
+ .cv-widget-state-select:disabled {
1347
+ background: #f8f9fa;
1348
+ color: #6c757d;
1349
+ cursor: not-allowed;
1350
+ }
1351
+
1352
+ .cv-widget-current {
1353
+ margin: 16px 0;
1354
+ padding: 12px;
1355
+ background: #f8f9fa;
1356
+ border-radius: 4px;
1357
+ border-left: 4px solid #007bff;
1358
+ }
1359
+
1360
+ .cv-widget-current label {
1361
+ font-size: 12px;
1362
+ text-transform: uppercase;
1363
+ letter-spacing: 0.5px;
1364
+ color: #666;
1365
+ margin-bottom: 4px;
1366
+ }
1367
+
1368
+ .cv-widget-current-view {
1369
+ font-weight: 500;
1370
+ color: #333;
1371
+ }
1372
+
1373
+ .cv-widget-reset {
1374
+ width: 100%;
1375
+ padding: 8px 16px;
1376
+ background: #dc3545;
1377
+ color: white;
1378
+ border: none;
1379
+ border-radius: 4px;
1380
+ cursor: pointer;
1381
+ font-size: 14px;
1382
+ font-weight: 500;
1383
+ }
1384
+
1385
+ .cv-widget-reset:hover {
1386
+ background: #c82333;
1387
+ }
1388
+
1389
+ .cv-widget-reset:active {
1390
+ background: #bd2130;
1391
+ }
1392
+
1393
+ /* Responsive design for mobile */
1394
+ @media (max-width: 768px) {
1395
+ .cv-widget-top-right,
1396
+ .cv-widget-top-left {
1397
+ top: 10px;
1398
+ }
1399
+
1400
+ .cv-widget-bottom-right,
1401
+ .cv-widget-bottom-left {
1402
+ bottom: 10px;
1403
+ }
1404
+
1405
+ /* All widgets stay flush with screen edges */
1406
+ .cv-widget-top-right,
1407
+ .cv-widget-bottom-right,
1408
+ .cv-widget-middle-right {
1409
+ right: 0;
1410
+ }
1411
+
1412
+ .cv-widget-top-left,
1413
+ .cv-widget-bottom-left,
1414
+ .cv-widget-middle-left {
1415
+ left: 0;
1416
+ }
1417
+
1418
+ /* Slightly smaller on mobile */
1419
+ .cv-widget-icon {
1420
+ width: 60px;
1421
+ height: 32px;
1422
+ }
1423
+
1424
+ .cv-widget-icon:hover {
1425
+ width: 75px;
1426
+ }
1427
+ }
1428
+
1429
+ /* Modal styles */
1430
+ .cv-widget-modal-overlay {
1431
+ position: fixed;
1432
+ top: 0;
1433
+ left: 0;
1434
+ right: 0;
1435
+ bottom: 0;
1436
+ background: rgba(0, 0, 0, 0.5);
1437
+ display: flex;
1438
+ align-items: center;
1439
+ justify-content: center;
1440
+ z-index: 10002;
1441
+ animation: fadeIn 0.2s ease;
1442
+ }
1443
+
1444
+ @keyframes fadeIn {
1445
+ from { opacity: 0; }
1446
+ to { opacity: 1; }
1447
+ }
1448
+
1449
+ .cv-widget-modal {
1450
+ background: white;
1451
+ border-radius: 8px;
1452
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
1453
+ max-width: 400px;
1454
+ width: 90vw;
1455
+ max-height: 80vh;
1456
+ overflow-y: auto;
1457
+ animation: slideIn 0.2s ease;
1458
+ }
1459
+
1460
+ @keyframes slideIn {
1461
+ from {
1462
+ opacity: 0;
1463
+ transform: scale(0.9) translateY(-20px);
1464
+ }
1465
+ to {
1466
+ opacity: 1;
1467
+ transform: scale(1) translateY(0);
1468
+ }
1469
+ }
1470
+
1471
+ .cv-widget-modal-header {
1472
+ display: flex;
1473
+ justify-content: space-between;
1474
+ align-items: center;
1475
+ padding: 16px 20px;
1476
+ border-bottom: 1px solid #e9ecef;
1477
+ background: #f8f9fa;
1478
+ border-radius: 8px 8px 0 0;
1479
+ }
1480
+
1481
+ .cv-widget-modal-header h3 {
1482
+ margin: 0;
1483
+ font-size: 18px;
1484
+ font-weight: 600;
1485
+ color: #333;
1486
+ }
1487
+
1488
+ .cv-widget-modal-close {
1489
+ background: none;
1490
+ border: none;
1491
+ font-size: 20px;
1492
+ cursor: pointer;
1493
+ padding: 0;
1494
+ width: 32px;
1495
+ height: 32px;
1496
+ display: flex;
1497
+ align-items: center;
1498
+ justify-content: center;
1499
+ border-radius: 4px;
1500
+ color: #666;
1501
+ line-height: 1;
1502
+ transition: all 0.2s ease;
1503
+ }
1504
+
1505
+ .cv-widget-modal-close:hover {
1506
+ background: #e9ecef;
1507
+ color: #333;
1508
+ }
1509
+
1510
+ .cv-widget-modal-content {
1511
+ padding: 20px;
1512
+ }
1513
+
1514
+ .cv-widget-modal-actions {
1515
+ margin-top: 20px;
1516
+ padding-top: 16px;
1517
+ border-top: 1px solid #e9ecef;
1518
+ }
1519
+
1520
+ .cv-widget-restore {
1521
+ width: 100%;
1522
+ padding: 10px 16px;
1523
+ background: #28a745;
1524
+ color: white;
1525
+ border: none;
1526
+ border-radius: 4px;
1527
+ cursor: pointer;
1528
+ font-size: 14px;
1529
+ font-weight: 500;
1530
+ }
1531
+
1532
+ .cv-widget-restore:hover {
1533
+ background: #218838;
1534
+ }
1535
+
1536
+ .cv-widget-create-state {
1537
+ width: 100%;
1538
+ padding: 10px 16px;
1539
+ background: #007bff;
1540
+ color: white;
1541
+ border: none;
1542
+ border-radius: 4px;
1543
+ cursor: pointer;
1544
+ font-size: 14px;
1545
+ font-weight: 500;
1546
+ margin-bottom: 10px;
1547
+ }
1548
+
1549
+ .cv-widget-create-state:hover {
1550
+ background: #0056b3;
1551
+ }
1552
+
1553
+ /* Dark theme modal styles */
1554
+ .cv-widget-theme-dark .cv-widget-modal {
1555
+ background: #2d3748;
1556
+ color: #e2e8f0;
1557
+ }
1558
+
1559
+ .cv-widget-theme-dark .cv-widget-modal-header {
1560
+ background: #1a202c;
1561
+ border-color: #4a5568;
1562
+ }
1563
+
1564
+ .cv-widget-theme-dark .cv-widget-modal-header h3 {
1565
+ color: #e2e8f0;
1566
+ }
1567
+
1568
+ .cv-widget-theme-dark .cv-widget-modal-close {
1569
+ color: #a0aec0;
1570
+ }
1571
+
1572
+ .cv-widget-theme-dark .cv-widget-modal-close:hover {
1573
+ background: #4a5568;
1574
+ color: #e2e8f0;
1575
+ }
1576
+
1577
+ .cv-widget-theme-dark .cv-widget-modal-actions {
1578
+ border-color: #4a5568;
1579
+ }
1580
+
1581
+ /* Custom state creator styles */
1582
+ .cv-custom-state-modal {
1583
+ max-width: 500px;
1584
+ }
1585
+
1586
+ .cv-custom-state-form h4 {
1587
+ margin: 20px 0 10px 0;
1588
+ font-size: 16px;
1589
+ font-weight: 600;
1590
+ color: #333;
1591
+ border-bottom: 1px solid #e9ecef;
1592
+ padding-bottom: 5px;
1593
+ }
1594
+
1595
+ .cv-custom-state-form p {
1596
+ font-size: 15px;
1597
+ line-height: 1.6;
1598
+ color: #555;
1599
+ margin-bottom: 24px;
1600
+ text-align: justify;
1601
+ }
1602
+
1603
+ .cv-custom-state-section {
1604
+ margin-bottom: 16px;
1605
+ }
1606
+
1607
+ .cv-custom-state-section label {
1608
+ display: block;
1609
+ margin-bottom: 4px;
1610
+ font-weight: 500;
1611
+ color: #555;
1612
+ }
1613
+
1614
+ .cv-custom-state-input {
1615
+ width: 100%;
1616
+ padding: 8px 12px;
1617
+ border: 1px solid #ddd;
1618
+ border-radius: 4px;
1619
+ background: white;
1620
+ font-size: 14px;
1621
+ }
1622
+
1623
+ .cv-custom-state-input:focus {
1624
+ outline: none;
1625
+ border-color: #007bff;
1626
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1627
+ }
1628
+
1629
+ .cv-custom-toggles {
1630
+ display: grid;
1631
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1632
+ gap: 10px;
1633
+ }
1634
+
1635
+ .cv-custom-state-toggle {
1636
+ display: flex;
1637
+ align-items: center;
1638
+ }
1639
+
1640
+ .cv-custom-state-toggle label {
1641
+ display: flex;
1642
+ align-items: center;
1643
+ cursor: pointer;
1644
+ font-weight: normal;
1645
+ margin: 0;
1646
+ }
1647
+
1648
+ .cv-custom-toggle-checkbox {
1649
+ margin-right: 8px;
1650
+ width: auto;
1651
+ }
1652
+
1653
+ .cv-tab-groups {
1654
+ margin-top: 20px;
1655
+ }
1656
+
1657
+ .cv-tab-group-control {
1658
+ margin-bottom: 15px;
1659
+ }
1660
+
1661
+ .cv-tab-group-control label {
1662
+ display: block;
1663
+ margin-bottom: 5px;
1664
+ font-weight: 500;
1665
+ font-size: 14px;
1666
+ }
1667
+
1668
+ .cv-tab-group-select {
1669
+ width: 100%;
1670
+ padding: 8px 12px;
1671
+ border: 1px solid #ced4da;
1672
+ border-radius: 4px;
1673
+ font-size: 14px;
1674
+ background-color: white;
1675
+ cursor: pointer;
1676
+ }
1677
+
1678
+ .cv-tab-group-select:focus {
1679
+ outline: none;
1680
+ border-color: #007bff;
1681
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
1682
+ }
1683
+
1684
+ .cv-widget-theme-dark .cv-tab-group-select {
1685
+ background-color: #2d3748;
1686
+ border-color: #4a5568;
1687
+ color: #e2e8f0;
1688
+ }
1689
+
1690
+ .cv-custom-state-actions {
1691
+ display: flex;
1692
+ gap: 10px;
1693
+ margin-top: 20px;
1694
+ padding-top: 16px;
1695
+ border-top: 1px solid #e9ecef;
1696
+ }
1697
+
1698
+ .cv-custom-state-cancel,
1699
+ .cv-custom-state-copy-url {
1700
+ flex: 1;
1701
+ padding: 10px 16px;
1702
+ border: none;
1703
+ border-radius: 4px;
1704
+ cursor: pointer;
1705
+ font-size: 14px;
1706
+ font-weight: 500;
1707
+ }
1708
+
1709
+ .cv-custom-state-reset {
1710
+ flex: 1;
1711
+ padding: 10px 16px;
1712
+ border: none;
1713
+ border-radius: 4px;
1714
+ cursor: pointer;
1715
+ font-size: 14px;
1716
+ font-weight: 500;
1717
+ background: #dc3545;
1718
+ color: white;
1719
+ }
1720
+
1721
+ .cv-custom-state-reset:hover {
1722
+ background: #c82333;
1723
+ }
1724
+
1725
+ .cv-custom-state-cancel {
1726
+ background: #6c757d;
1727
+ color: white;
1728
+ }
1729
+
1730
+ .cv-custom-state-cancel:hover {
1731
+ background: #5a6268;
1732
+ }
1733
+
1734
+ .cv-custom-state-copy-url {
1735
+ background: #28a745;
1736
+ color: white;
1737
+ }
1738
+
1739
+ .cv-custom-state-copy-url:hover {
1740
+ background: #218838;
1741
+ }
1742
+
1743
+ /* Dark theme custom state styles */
1744
+ .cv-widget-theme-dark .cv-custom-state-form h4 {
1745
+ color: #e2e8f0;
1746
+ border-color: #4a5568;
1747
+ }
1748
+
1749
+ .cv-widget-theme-dark .cv-custom-state-form p {
1750
+ color: #cbd5e0;
1751
+ }
1752
+
1753
+ .cv-widget-theme-dark .cv-custom-state-section label {
1754
+ color: #a0aec0;
1755
+ }
1756
+
1757
+ .cv-widget-theme-dark .cv-custom-state-input {
1758
+ background: #1a202c;
1759
+ border-color: #4a5568;
1760
+ color: #e2e8f0;
1761
+ }
1762
+
1763
+ .cv-widget-theme-dark .cv-custom-state-actions {
1764
+ border-color: #4a5568;
1765
+ }
1766
+
1767
+ /* Welcome modal styles */
1768
+ .cv-welcome-modal {
1769
+ max-width: 500px;
1770
+ }
1771
+
1772
+ .cv-welcome-content {
1773
+ text-align: center;
1774
+ }
1775
+
1776
+ .cv-welcome-content p {
1777
+ font-size: 15px;
1778
+ line-height: 1.6;
1779
+ color: #555;
1780
+ margin-bottom: 24px;
1781
+ text-align: justify;
1782
+ }
1783
+
1784
+ .cv-welcome-widget-preview {
1785
+ display: flex;
1786
+ flex-direction: column;
1787
+ align-items: center;
1788
+ gap: 12px;
1789
+ padding: 20px;
1790
+ background: #f8f9fa;
1791
+ border-radius: 8px;
1792
+ margin-bottom: 24px;
1793
+ }
1794
+
1795
+ .cv-welcome-widget-icon {
1796
+ width: 36px;
1797
+ height: 36px;
1798
+ background: white;
1799
+ color: black;
1800
+ border-radius: 0 18px 18px 0;
1801
+ display: flex;
1802
+ align-items: center;
1803
+ justify-content: center;
1804
+ font-size: 18px;
1805
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1806
+ }
1807
+
1808
+ .cv-welcome-widget-label {
1809
+ font-size: 14px;
1810
+ color: #666;
1811
+ margin: 0;
1812
+ font-weight: 500;
1813
+ }
1814
+
1815
+ .cv-welcome-got-it {
1816
+ width: 100%;
1817
+ padding: 12px 24px;
1818
+ background: #007bff;
1819
+ color: white;
1820
+ border: none;
1821
+ border-radius: 4px;
1822
+ cursor: pointer;
1823
+ font-size: 16px;
1824
+ font-weight: 600;
1825
+ transition: background 0.2s ease;
1826
+ }
1827
+
1828
+ .cv-welcome-got-it:hover {
1829
+ background: #0056b3;
1830
+ }
1831
+
1832
+ .cv-welcome-got-it:active {
1833
+ background: #004494;
1834
+ }
1835
+
1836
+ /* Dark theme welcome modal styles */
1837
+ .cv-widget-theme-dark .cv-welcome-content p {
1838
+ color: #cbd5e0;
1839
+ }
1840
+
1841
+ .cv-widget-theme-dark .cv-welcome-widget-preview {
1842
+ background: #1a202c;
1843
+ }
1844
+
1845
+ .cv-widget-theme-dark .cv-welcome-widget-label {
1846
+ color: #a0aec0;
1847
+ }
1689
1848
  `;
1690
1849
  /**
1691
1850
  * Inject widget styles into the document head
@@ -1720,8 +1879,8 @@ class CustomViewsWidget {
1720
1879
  title: options.title || 'Custom Views',
1721
1880
  description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1722
1881
  showWelcome: options.showWelcome ?? false,
1723
- welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1724
- 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.',
1882
+ welcomeTitle: options.welcomeTitle || 'This website uses Custom Views',
1883
+ 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.',
1725
1884
  showTabGroups: options.showTabGroups ?? true
1726
1885
  };
1727
1886
  // No external state manager to initialize
@@ -1801,13 +1960,13 @@ class CustomViewsWidget {
1801
1960
  this.modal.className = 'cv-widget-modal-overlay';
1802
1961
  this.applyThemeToModal();
1803
1962
  const toggleControls = toggles.length
1804
- ? toggles.map(toggle => `
1805
- <div class="cv-custom-state-toggle">
1806
- <label>
1807
- <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1808
- ${this.formatToggleName(toggle)}
1809
- </label>
1810
- </div>
1963
+ ? toggles.map(toggle => `
1964
+ <div class="cv-custom-state-toggle">
1965
+ <label>
1966
+ <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1967
+ ${this.formatToggleName(toggle)}
1968
+ </label>
1969
+ </div>
1811
1970
  `).join('')
1812
1971
  : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1813
1972
  // Get tab groups
@@ -1816,46 +1975,46 @@ class CustomViewsWidget {
1816
1975
  if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
1817
1976
  const tabGroupControls = tabGroups.map(group => {
1818
1977
  const options = group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('');
1819
- return `
1820
- <div class="cv-tab-group-control">
1821
- <label for="tab-group-${group.id}">${group.label || group.id}</label>
1822
- <select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
1823
- ${options}
1824
- </select>
1825
- </div>
1978
+ return `
1979
+ <div class="cv-tab-group-control">
1980
+ <label for="tab-group-${group.id}">${group.label || group.id}</label>
1981
+ <select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
1982
+ ${options}
1983
+ </select>
1984
+ </div>
1826
1985
  `;
1827
1986
  }).join('');
1828
- tabGroupsHTML = `
1829
- <h4>Tab Groups</h4>
1830
- <div class="cv-tab-groups">
1831
- ${tabGroupControls}
1832
- </div>
1987
+ tabGroupsHTML = `
1988
+ <h4>Tab Groups</h4>
1989
+ <div class="cv-tab-groups">
1990
+ ${tabGroupControls}
1991
+ </div>
1833
1992
  `;
1834
1993
  }
1835
- this.modal.innerHTML = `
1836
- <div class="cv-widget-modal cv-custom-state-modal">
1837
- <div class="cv-widget-modal-header">
1838
- <h3>Customize View</h3>
1839
- <button class="cv-widget-modal-close" aria-label="Close modal">X</button>
1840
- </div>
1841
- <div class="cv-widget-modal-content">
1842
- <div class="cv-custom-state-form">
1843
- <p>${this.options.description}</p>
1844
-
1845
- <h4>Content Sections</h4>
1846
- <div class="cv-custom-toggles">
1847
- ${toggleControls}
1848
- </div>
1849
-
1850
- ${tabGroupsHTML}
1851
-
1852
- <div class="cv-custom-state-actions">
1853
- ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1854
- <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
1855
- </div>
1856
- </div>
1857
- </div>
1858
- </div>
1994
+ this.modal.innerHTML = `
1995
+ <div class="cv-widget-modal cv-custom-state-modal">
1996
+ <div class="cv-widget-modal-header">
1997
+ <h3>Customize View</h3>
1998
+ <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
1999
+ </div>
2000
+ <div class="cv-widget-modal-content">
2001
+ <div class="cv-custom-state-form">
2002
+ <p>${this.options.description}</p>
2003
+
2004
+ <h4>Content Sections</h4>
2005
+ <div class="cv-custom-toggles">
2006
+ ${toggleControls}
2007
+ </div>
2008
+
2009
+ ${tabGroupsHTML}
2010
+
2011
+ <div class="cv-custom-state-actions">
2012
+ ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
2013
+ <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
2014
+ </div>
2015
+ </div>
2016
+ </div>
2017
+ </div>
1859
2018
  `;
1860
2019
  document.body.appendChild(this.modal);
1861
2020
  this.attachStateModalEventListeners();
@@ -2040,25 +2199,25 @@ class CustomViewsWidget {
2040
2199
  this.modal = document.createElement('div');
2041
2200
  this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
2042
2201
  this.applyThemeToModal();
2043
- this.modal.innerHTML = `
2044
- <div class="cv-widget-modal cv-welcome-modal">
2045
- <div class="cv-widget-modal-header">
2046
- <h3>${this.options.welcomeTitle}</h3>
2047
- <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
2048
- </div>
2049
- <div class="cv-widget-modal-content">
2050
- <div class="cv-welcome-content">
2051
- <p>${this.options.welcomeMessage}</p>
2052
-
2053
- <div class="cv-welcome-widget-preview">
2054
- <div class="cv-welcome-widget-icon">⚙</div>
2055
- <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
2056
- </div>
2057
-
2058
- <button class="cv-welcome-got-it">Got it!</button>
2059
- </div>
2060
- </div>
2061
- </div>
2202
+ this.modal.innerHTML = `
2203
+ <div class="cv-widget-modal cv-welcome-modal">
2204
+ <div class="cv-widget-modal-header">
2205
+ <h3>${this.options.welcomeTitle}</h3>
2206
+ <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
2207
+ </div>
2208
+ <div class="cv-widget-modal-content">
2209
+ <div class="cv-welcome-content">
2210
+ <p>${this.options.welcomeMessage}</p>
2211
+
2212
+ <div class="cv-welcome-widget-preview">
2213
+ <div class="cv-welcome-widget-icon">⚙</div>
2214
+ <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
2215
+ </div>
2216
+
2217
+ <button class="cv-welcome-got-it">Got it!</button>
2218
+ </div>
2219
+ </div>
2220
+ </div>
2062
2221
  `;
2063
2222
  document.body.appendChild(this.modal);
2064
2223
  this.attachWelcomeModalEventListeners();
@@ -2100,46 +2259,6 @@ class CustomViewsWidget {
2100
2259
  }
2101
2260
  }
2102
2261
 
2103
- /**
2104
- * Custom Elements for Tab Groups and Tabs
2105
- */
2106
- /**
2107
- * <cv-tab> element - represents a single tab panel
2108
- */
2109
- class CVTab extends HTMLElement {
2110
- connectedCallback() {
2111
- // Element is managed by TabManager
2112
- }
2113
- }
2114
- /**
2115
- * <cv-tabgroup> element - represents a group of tabs
2116
- */
2117
- class CVTabgroup extends HTMLElement {
2118
- connectedCallback() {
2119
- // Element is managed by TabManager
2120
- // Emit ready event after a brief delay to ensure children are parsed
2121
- setTimeout(() => {
2122
- const event = new CustomEvent('cv:tabgroup-ready', {
2123
- bubbles: true,
2124
- detail: { groupId: this.getAttribute('id') }
2125
- });
2126
- this.dispatchEvent(event);
2127
- }, 0);
2128
- }
2129
- }
2130
- /**
2131
- * Register custom elements
2132
- */
2133
- function registerCustomElements() {
2134
- // Only register if not already defined
2135
- if (!customElements.get('cv-tab')) {
2136
- customElements.define('cv-tab', CVTab);
2137
- }
2138
- if (!customElements.get('cv-tabgroup')) {
2139
- customElements.define('cv-tabgroup', CVTabgroup);
2140
- }
2141
- }
2142
-
2143
2262
  /**
2144
2263
  * Initialize CustomViews from script tag attributes and config file
2145
2264
  * This function handles the automatic initialization of CustomViews when included via script tag
@@ -2168,22 +2287,20 @@ function initializeFromScript() {
2168
2287
  return;
2169
2288
  }
2170
2289
  window.__customViewsInitInProgress = true;
2171
- // Register custom elements early
2172
- registerCustomElements();
2173
2290
  try {
2174
2291
  // Find the script tag
2175
2292
  let scriptTag = document.currentScript;
2176
2293
  // Fallback if currentScript is not available (executed after page load)
2177
2294
  if (!scriptTag) {
2178
2295
  // Try to find the script tag by looking for our script
2179
- const scripts = document.querySelectorAll('script[src*="custom-views"]');
2296
+ const scripts = document.querySelectorAll('script[src*="@customviews-js"]');
2180
2297
  if (scripts.length > 0) {
2181
2298
  // Find the most specific match (to avoid matching other custom-views scripts)
2182
2299
  for (let i = 0; i < scripts.length; i++) {
2183
2300
  const script = scripts[i];
2184
2301
  const src = script.getAttribute('src') || '';
2185
- // Look for .min.js or .js at the end
2186
- if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
2302
+ // Look for .min.js or .js at the end, or the package root
2303
+ if (src.match(/@customviews-js\/customviews(\.min)?\.js($|\?)/) || src.includes('@customviews-js/customviews')) {
2187
2304
  scriptTag = script;
2188
2305
  break;
2189
2306
  }