@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,87 +1,10 @@
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
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
8
  /**
86
9
  * Manages persistence of custom views state using browser localStorage
87
10
  */
@@ -342,6 +265,98 @@ class VisibilityManager {
342
265
  }
343
266
  }
344
267
 
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
+
345
360
  // Constants for selectors
346
361
  const TABGROUP_SELECTOR = 'cv-tabgroup';
347
362
  const TAB_SELECTOR = 'cv-tab';
@@ -363,8 +378,8 @@ class TabManager {
363
378
  return;
364
379
  // Determine the active tab for this group
365
380
  const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
366
- // Apply visibility to child cv-tab elements
367
- const tabElements = groupEl.querySelectorAll(TAB_SELECTOR);
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);
368
383
  tabElements.forEach((tabEl) => {
369
384
  const tabId = tabEl.getAttribute('id');
370
385
  if (!tabId)
@@ -396,8 +411,8 @@ class TabManager {
396
411
  }
397
412
  }
398
413
  }
399
- // 3. Fallback to first cv-tab child in DOM
400
- const firstTab = groupEl.querySelector(TAB_SELECTOR);
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);
401
416
  if (firstTab) {
402
417
  return firstTab.getAttribute('id');
403
418
  }
@@ -422,6 +437,28 @@ class TabManager {
422
437
  static buildNavs(rootEl, cfgGroups, onTabClick) {
423
438
  // Find all cv-tabgroup elements with nav="auto" or no nav attribute
424
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
+ }
425
462
  tabGroups.forEach((groupEl) => {
426
463
  const groupId = groupEl.getAttribute('id');
427
464
  if (!groupId)
@@ -430,8 +467,8 @@ class TabManager {
430
467
  let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
431
468
  if (navContainer)
432
469
  return; // Already built
433
- // Get all child tabs
434
- const tabElements = Array.from(groupEl.querySelectorAll(TAB_SELECTOR));
470
+ // Get only direct child tabs (not nested ones)
471
+ const tabElements = Array.from(groupEl.children).filter((child) => child.tagName.toLowerCase() === TAB_SELECTOR);
435
472
  if (tabElements.length === 0)
436
473
  return;
437
474
  // Create nav container
@@ -449,7 +486,8 @@ class TabManager {
449
486
  listItem.className = 'nav-item';
450
487
  const navLink = document.createElement('a');
451
488
  navLink.className = 'nav-link';
452
- navLink.textContent = header;
489
+ // Replace icon shortcodes in header
490
+ navLink.innerHTML = replaceIconShortcodes(header);
453
491
  navLink.href = '#';
454
492
  navLink.setAttribute('data-tab-id', tabId);
455
493
  navLink.setAttribute('data-group-id', groupId);
@@ -541,157 +579,267 @@ class TabManager {
541
579
  }
542
580
  }
543
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}`;
544
632
  /**
545
- * Styles for toggle visibility and animations
633
+ * ToggleManager handles discovery, visibility, and asset rendering for toggle elements
546
634
  */
547
- const TOGGLE_STYLES = `
548
- /* Core toggle visibility transitions */
549
- [data-cv-toggle], [data-customviews-toggle] {
550
- transition: opacity 150ms ease,
551
- transform 150ms ease,
552
- max-height 200ms ease,
553
- margin 150ms ease;
554
- will-change: opacity, transform, max-height, margin;
555
- }
556
-
557
- .cv-visible {
558
- opacity: 1 !important;
559
- transform: translateY(0) !important;
560
- max-height: var(--cv-max-height, 9999px) !important;
561
- }
562
-
563
- .cv-hidden {
564
- opacity: 0 !important;
565
- transform: translateY(-4px) !important;
566
- pointer-events: none !important;
567
- padding-top: 0 !important;
568
- padding-bottom: 0 !important;
569
- border-top-width: 0 !important;
570
- border-bottom-width: 0 !important;
571
- max-height: 0 !important;
572
- margin-top: 0 !important;
573
- margin-bottom: 0 !important;
574
- overflow: hidden !important;
575
- }
576
- `;
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
+ }
577
691
 
578
692
  /**
579
- * Styles for tab groups and tab navigation
693
+ * Styles for toggle visibility and animations
580
694
  */
581
- const TAB_STYLES = `
582
- /* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
583
- .cv-tabs-nav {
584
- display: flex;
585
- flex-wrap: wrap;
586
- padding-left: 0;
587
- margin-top: 0.5rem;
588
- margin-bottom: 1rem;
589
- list-style: none;
590
- border-bottom: 1px solid #dee2e6;
591
- }
592
-
593
- .cv-tabs-nav .nav-item {
594
- margin-bottom: -1px;
595
- list-style: none;
596
- display: inline-block;
597
- }
598
-
599
- .cv-tabs-nav .nav-link {
600
- display: block;
601
- padding: 0.5rem 1rem;
602
- color: #495057;
603
- text-decoration: none;
604
- background-color: transparent;
605
- border: 1px solid transparent;
606
- border-top-left-radius: 0.25rem;
607
- border-top-right-radius: 0.25rem;
608
- transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
609
- cursor: pointer;
610
- }
611
-
612
- .cv-tabs-nav .nav-link:hover,
613
- .cv-tabs-nav .nav-link:focus {
614
- border-color: #e9ecef #e9ecef #dee2e6;
615
- isolation: isolate;
616
- }
617
-
618
- .cv-tabs-nav .nav-link.active {
619
- color: #495057;
620
- background-color: #fff;
621
- border-color: #dee2e6 #dee2e6 #fff;
622
- }
623
-
624
- .cv-tabs-nav .nav-link:focus {
625
- outline: 0;
626
- box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
627
- }
628
-
629
- /* Legacy button-based nav (deprecated, kept for compatibility) */
630
- .cv-tabs-nav-item {
631
- background: none;
632
- border: none;
633
- border-bottom: 2px solid transparent;
634
- padding: 0.5rem 1rem;
635
- cursor: pointer;
636
- font-size: 1rem;
637
- color: #6c757d;
638
- transition: color 150ms ease, border-color 150ms ease;
639
- }
640
-
641
- .cv-tabs-nav-item:hover {
642
- color: #495057;
643
- border-bottom-color: #dee2e6;
644
- }
645
-
646
- .cv-tabs-nav-item.active {
647
- color: #007bff;
648
- border-bottom-color: #007bff;
649
- font-weight: 500;
650
- }
651
-
652
- .cv-tabs-nav-item:focus {
653
- outline: 2px solid #007bff;
654
- outline-offset: 2px;
655
- }
656
-
657
- /* Tab panel base styles */
658
- cv-tab {
659
- display: block;
660
- }
661
-
662
- /* Override visibility for tab panels - use display instead of collapse animation */
663
- cv-tab.cv-hidden {
664
- display: none !important;
665
- }
666
-
667
- cv-tab.cv-visible {
668
- display: block !important;
669
- }
670
-
671
- cv-tabgroup {
672
- display: block;
673
- margin-bottom: 1.5rem;
674
- }
675
-
676
- /* Bottom border line for tab groups */
677
- .cv-tabgroup-bottom-border {
678
- border-bottom: 1px solid #dee2e6;
679
- margin-top: 1rem;
680
- }
681
-
682
- /* Tab content wrapper */
683
- .cv-tab-content {
684
- padding: 1rem 0;
685
- }
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
+ }
686
834
  `;
687
835
 
688
836
  /**
689
837
  * Combined core styles for toggles and tabs
690
838
  */
691
- const CORE_STYLES = `
692
- ${TOGGLE_STYLES}
693
-
694
- ${TAB_STYLES}
839
+ const CORE_STYLES = `
840
+ ${TOGGLE_STYLES}
841
+
842
+ ${TAB_STYLES}
695
843
  `;
696
844
  /**
697
845
  * Add styles for hiding and showing toggles animations and transitions to the document head
@@ -821,23 +969,10 @@ class CustomViewsCore {
821
969
  this.lastAppliedState = this.cloneState(state);
822
970
  const toggles = state.toggles || [];
823
971
  const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
824
- // Toggles hide or show relevant toggles
825
- this.rootEl.querySelectorAll("[data-cv-toggle], [data-customviews-toggle]").forEach(el => {
826
- const category = el.dataset.cvToggle || el.dataset.customviewsToggle;
827
- const shouldShow = !!category && finalToggles.includes(category);
828
- this.visibilityManager.applyElementVisibility(el, shouldShow);
829
- });
830
- // Render toggles
831
- for (const category of finalToggles) {
832
- this.rootEl.querySelectorAll(`[data-cv-toggle="${category}"], [data-customviews-toggle="${category}"]`).forEach(el => {
833
- // if it has an id, then we should render the asset into it
834
- // Support both (data-cv-id) and (data-customviews-id) attributes
835
- const toggleId = el.dataset.cvId || el.dataset.customviewsId;
836
- if (toggleId) {
837
- renderAssetInto(el, toggleId, this.assetsManager);
838
- }
839
- });
840
- }
972
+ // Apply toggle visibility
973
+ ToggleManager.applyToggles(this.rootEl, finalToggles);
974
+ // Render assets into toggles
975
+ ToggleManager.renderAssets(this.rootEl, finalToggles, this.assetsManager);
841
976
  // Apply tab selections
842
977
  TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
843
978
  // Update nav active states (without rebuilding)
@@ -950,52 +1085,6 @@ class CustomViewsCore {
950
1085
  }
951
1086
  }
952
1087
 
953
- class AssetsManager {
954
- assets;
955
- baseURL;
956
- constructor(assets, baseURL = '') {
957
- this.assets = assets;
958
- this.baseURL = baseURL;
959
- if (!this.validate()) {
960
- console.warn('Invalid assets:', this.assets);
961
- }
962
- }
963
- // Check each asset has content or src
964
- validate() {
965
- return Object.values(this.assets).every(a => a.src || a.content);
966
- }
967
- get(assetId) {
968
- const asset = this.assets[assetId];
969
- if (!asset)
970
- return undefined;
971
- // If there's a baseURL and the asset has a src property, prepend the baseURL
972
- if (this.baseURL && asset.src) {
973
- // Create a shallow copy to avoid mutating the original asset
974
- return {
975
- ...asset,
976
- src: this.prependBaseURL(asset.src)
977
- };
978
- }
979
- return asset;
980
- }
981
- prependBaseURL(path) {
982
- // Don't prepend if the path is already absolute (starts with http:// or https://)
983
- if (path.startsWith('http://') || path.startsWith('https://')) {
984
- return path;
985
- }
986
- // Ensure baseURL doesn't end with / and path starts with /
987
- const cleanBaseURL = this.baseURL.endsWith('/') ? this.baseURL.slice(0, -1) : this.baseURL;
988
- const cleanPath = path.startsWith('/') ? path : '/' + path;
989
- return cleanBaseURL + cleanPath;
990
- }
991
- loadFromJSON(json) {
992
- this.assets = json;
993
- }
994
- loadAdditionalAssets(additionalAssets) {
995
- this.assets = { ...this.assets, ...additionalAssets };
996
- }
997
- }
998
-
999
1088
  /**
1000
1089
  * Helper function to prepend baseUrl to a path
1001
1090
  * @param path The path to prepend the baseUrl to
@@ -1015,6 +1104,57 @@ function prependBaseUrl(path, baseUrl) {
1015
1104
  return cleanbaseUrl + cleanPath;
1016
1105
  }
1017
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
1116
+ }
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);
1132
+ }
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
1140
+ }
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
+
1018
1158
  /**
1019
1159
  * Main CustomViews class for initializing and managing custom views
1020
1160
  */
@@ -1025,6 +1165,8 @@ class CustomViews {
1025
1165
  * @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
1026
1166
  */
1027
1167
  static async init(opts) {
1168
+ // Register custom elements
1169
+ registerCustomElements();
1028
1170
  // Load assets JSON if provided
1029
1171
  let assetsManager;
1030
1172
  const baseURL = opts.baseURL || '';
@@ -1067,627 +1209,644 @@ class CustomViews {
1067
1209
  * Note: Styles are kept as a TypeScript string for compatibility with the build system.
1068
1210
  * This approach ensures the styles are properly bundled and don't require separate CSS file handling.
1069
1211
  */
1070
- const WIDGET_STYLES = `
1071
- /* Rounded rectangle widget icon styles */
1072
- .cv-widget-icon {
1073
- position: fixed;
1074
- /* Slightly transparent by default so the widget is subtle at the page edge */
1075
- background: rgba(255, 255, 255, 0.92);
1076
- color: rgba(0, 0, 0, 0.9);
1077
- opacity: 0.6;
1078
- display: flex;
1079
- align-items: center;
1080
- justify-content: center;
1081
- font-size: 18px;
1082
- font-weight: bold;
1083
- cursor: pointer;
1084
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1085
- z-index: 9998;
1086
- transition: all 0.3s ease;
1087
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1088
- }
1089
-
1090
- .cv-widget-icon:hover {
1091
- /* Become fully opaque on hover to improve readability */
1092
- background: rgba(255, 255, 255, 1);
1093
- color: rgba(0, 0, 0, 1);
1094
- opacity: 1;
1095
- }
1096
-
1097
- /* Top-right: rounded end on left, sticks out leftward on hover */
1098
- .cv-widget-top-right {
1099
- top: 20px;
1100
- right: 0;
1101
- border-radius: 18px 0 0 18px;
1102
- padding-left: 8px;
1103
- justify-content: flex-start;
1104
- }
1105
-
1106
- /* Top-left: rounded end on right, sticks out rightward on hover */
1107
- .cv-widget-top-left {
1108
- top: 20px;
1109
- left: 0;
1110
- border-radius: 0 18px 18px 0;
1111
- padding-right: 8px;
1112
- justify-content: flex-end;
1113
- }
1114
-
1115
- /* Bottom-right: rounded end on left, sticks out leftward on hover */
1116
- .cv-widget-bottom-right {
1117
- bottom: 20px;
1118
- right: 0;
1119
- border-radius: 18px 0 0 18px;
1120
- padding-left: 8px;
1121
- justify-content: flex-start;
1122
- }
1123
-
1124
- /* Bottom-left: rounded end on right, sticks out rightward on hover */
1125
- .cv-widget-bottom-left {
1126
- bottom: 20px;
1127
- left: 0;
1128
- border-radius: 0 18px 18px 0;
1129
- padding-right: 8px;
1130
- justify-content: flex-end;
1131
- }
1132
-
1133
- /* Middle-left: rounded end on right, sticks out rightward on hover */
1134
- .cv-widget-middle-left {
1135
- top: 50%;
1136
- left: 0;
1137
- transform: translateY(-50%);
1138
- border-radius: 0 18px 18px 0;
1139
- padding-right: 8px;
1140
- justify-content: flex-end;
1141
- }
1142
-
1143
- /* Middle-right: rounded end on left, sticks out leftward on hover */
1144
- .cv-widget-middle-right {
1145
- top: 50%;
1146
- right: 0;
1147
- transform: translateY(-50%);
1148
- border-radius: 18px 0 0 18px;
1149
- padding-left: 8px;
1150
- justify-content: flex-start;
1151
- }
1152
-
1153
- .cv-widget-top-right,
1154
- .cv-widget-middle-right,
1155
- .cv-widget-bottom-right,
1156
- .cv-widget-top-left,
1157
- .cv-widget-middle-left,
1158
- .cv-widget-bottom-left {
1159
- height: 36px;
1160
- width: 36px;
1161
- }
1162
-
1163
- .cv-widget-middle-right:hover,
1164
- .cv-widget-top-right:hover,
1165
- .cv-widget-bottom-right:hover,
1166
- .cv-widget-top-left:hover,
1167
- .cv-widget-middle-left:hover,
1168
- .cv-widget-bottom-left:hover {
1169
- width: 55px;
1170
- }
1171
-
1172
- /* Modal content styles */
1173
- .cv-widget-section {
1174
- margin-bottom: 16px;
1175
- }
1176
-
1177
- .cv-widget-section:last-child {
1178
- margin-bottom: 0;
1179
- }
1180
-
1181
- .cv-widget-section label {
1182
- display: block;
1183
- margin-bottom: 4px;
1184
- font-weight: 500;
1185
- color: #555;
1186
- }
1187
-
1188
- .cv-widget-profile-select,
1189
- .cv-widget-state-select {
1190
- width: 100%;
1191
- padding: 8px 12px;
1192
- border: 1px solid #ddd;
1193
- border-radius: 4px;
1194
- background: white;
1195
- font-size: 14px;
1196
- }
1197
-
1198
- .cv-widget-profile-select:focus,
1199
- .cv-widget-state-select:focus {
1200
- outline: none;
1201
- border-color: #007bff;
1202
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1203
- }
1204
-
1205
- .cv-widget-profile-select:disabled,
1206
- .cv-widget-state-select:disabled {
1207
- background: #f8f9fa;
1208
- color: #6c757d;
1209
- cursor: not-allowed;
1210
- }
1211
-
1212
- .cv-widget-current {
1213
- margin: 16px 0;
1214
- padding: 12px;
1215
- background: #f8f9fa;
1216
- border-radius: 4px;
1217
- border-left: 4px solid #007bff;
1218
- }
1219
-
1220
- .cv-widget-current label {
1221
- font-size: 12px;
1222
- text-transform: uppercase;
1223
- letter-spacing: 0.5px;
1224
- color: #666;
1225
- margin-bottom: 4px;
1226
- }
1227
-
1228
- .cv-widget-current-view {
1229
- font-weight: 500;
1230
- color: #333;
1231
- }
1232
-
1233
- .cv-widget-reset {
1234
- width: 100%;
1235
- padding: 8px 16px;
1236
- background: #dc3545;
1237
- color: white;
1238
- border: none;
1239
- border-radius: 4px;
1240
- cursor: pointer;
1241
- font-size: 14px;
1242
- font-weight: 500;
1243
- }
1244
-
1245
- .cv-widget-reset:hover {
1246
- background: #c82333;
1247
- }
1248
-
1249
- .cv-widget-reset:active {
1250
- background: #bd2130;
1251
- }
1252
-
1253
- /* Responsive design for mobile */
1254
- @media (max-width: 768px) {
1255
- .cv-widget-top-right,
1256
- .cv-widget-top-left {
1257
- top: 10px;
1258
- }
1259
-
1260
- .cv-widget-bottom-right,
1261
- .cv-widget-bottom-left {
1262
- bottom: 10px;
1263
- }
1264
-
1265
- /* All widgets stay flush with screen edges */
1266
- .cv-widget-top-right,
1267
- .cv-widget-bottom-right,
1268
- .cv-widget-middle-right {
1269
- right: 0;
1270
- }
1271
-
1272
- .cv-widget-top-left,
1273
- .cv-widget-bottom-left,
1274
- .cv-widget-middle-left {
1275
- left: 0;
1276
- }
1277
-
1278
- /* Slightly smaller on mobile */
1279
- .cv-widget-icon {
1280
- width: 60px;
1281
- height: 32px;
1282
- }
1283
-
1284
- .cv-widget-icon:hover {
1285
- width: 75px;
1286
- }
1287
- }
1288
-
1289
- /* Modal styles */
1290
- .cv-widget-modal-overlay {
1291
- position: fixed;
1292
- top: 0;
1293
- left: 0;
1294
- right: 0;
1295
- bottom: 0;
1296
- background: rgba(0, 0, 0, 0.5);
1297
- display: flex;
1298
- align-items: center;
1299
- justify-content: center;
1300
- z-index: 10002;
1301
- animation: fadeIn 0.2s ease;
1302
- }
1303
-
1304
- @keyframes fadeIn {
1305
- from { opacity: 0; }
1306
- to { opacity: 1; }
1307
- }
1308
-
1309
- .cv-widget-modal {
1310
- background: white;
1311
- border-radius: 8px;
1312
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
1313
- max-width: 400px;
1314
- width: 90vw;
1315
- max-height: 80vh;
1316
- overflow-y: auto;
1317
- animation: slideIn 0.2s ease;
1318
- }
1319
-
1320
- @keyframes slideIn {
1321
- from {
1322
- opacity: 0;
1323
- transform: scale(0.9) translateY(-20px);
1324
- }
1325
- to {
1326
- opacity: 1;
1327
- transform: scale(1) translateY(0);
1328
- }
1329
- }
1330
-
1331
- .cv-widget-modal-header {
1332
- display: flex;
1333
- justify-content: space-between;
1334
- align-items: center;
1335
- padding: 16px 20px;
1336
- border-bottom: 1px solid #e9ecef;
1337
- background: #f8f9fa;
1338
- border-radius: 8px 8px 0 0;
1339
- }
1340
-
1341
- .cv-widget-modal-header h3 {
1342
- margin: 0;
1343
- font-size: 18px;
1344
- font-weight: 600;
1345
- color: #333;
1346
- }
1347
-
1348
- .cv-widget-modal-close {
1349
- background: none;
1350
- border: none;
1351
- font-size: 24px;
1352
- cursor: pointer;
1353
- padding: 0;
1354
- width: 32px;
1355
- height: 32px;
1356
- display: flex;
1357
- align-items: center;
1358
- justify-content: center;
1359
- border-radius: 4px;
1360
- color: #666;
1361
- }
1362
-
1363
- .cv-widget-modal-close:hover {
1364
- background: #e9ecef;
1365
- }
1366
-
1367
- .cv-widget-modal-content {
1368
- padding: 20px;
1369
- }
1370
-
1371
- .cv-widget-modal-actions {
1372
- margin-top: 20px;
1373
- padding-top: 16px;
1374
- border-top: 1px solid #e9ecef;
1375
- }
1376
-
1377
- .cv-widget-restore {
1378
- width: 100%;
1379
- padding: 10px 16px;
1380
- background: #28a745;
1381
- color: white;
1382
- border: none;
1383
- border-radius: 4px;
1384
- cursor: pointer;
1385
- font-size: 14px;
1386
- font-weight: 500;
1387
- }
1388
-
1389
- .cv-widget-restore:hover {
1390
- background: #218838;
1391
- }
1392
-
1393
- .cv-widget-create-state {
1394
- width: 100%;
1395
- padding: 10px 16px;
1396
- background: #007bff;
1397
- color: white;
1398
- border: none;
1399
- border-radius: 4px;
1400
- cursor: pointer;
1401
- font-size: 14px;
1402
- font-weight: 500;
1403
- margin-bottom: 10px;
1404
- }
1405
-
1406
- .cv-widget-create-state:hover {
1407
- background: #0056b3;
1408
- }
1409
-
1410
- /* Dark theme modal styles */
1411
- .cv-widget-theme-dark .cv-widget-modal {
1412
- background: #2d3748;
1413
- color: #e2e8f0;
1414
- }
1415
-
1416
- .cv-widget-theme-dark .cv-widget-modal-header {
1417
- background: #1a202c;
1418
- border-color: #4a5568;
1419
- }
1420
-
1421
- .cv-widget-theme-dark .cv-widget-modal-header h3 {
1422
- color: #e2e8f0;
1423
- }
1424
-
1425
- .cv-widget-theme-dark .cv-widget-modal-close {
1426
- color: #a0aec0;
1427
- }
1428
-
1429
- .cv-widget-theme-dark .cv-widget-modal-close:hover {
1430
- background: #4a5568;
1431
- }
1432
-
1433
- .cv-widget-theme-dark .cv-widget-modal-actions {
1434
- border-color: #4a5568;
1435
- }
1436
-
1437
- /* Custom state creator styles */
1438
- .cv-custom-state-modal {
1439
- max-width: 500px;
1440
- }
1441
-
1442
- .cv-custom-state-form h4 {
1443
- margin: 20px 0 10px 0;
1444
- font-size: 16px;
1445
- font-weight: 600;
1446
- color: #333;
1447
- border-bottom: 1px solid #e9ecef;
1448
- padding-bottom: 5px;
1449
- }
1450
-
1451
- .cv-custom-state-section {
1452
- margin-bottom: 16px;
1453
- }
1454
-
1455
- .cv-custom-state-section label {
1456
- display: block;
1457
- margin-bottom: 4px;
1458
- font-weight: 500;
1459
- color: #555;
1460
- }
1461
-
1462
- .cv-custom-state-input {
1463
- width: 100%;
1464
- padding: 8px 12px;
1465
- border: 1px solid #ddd;
1466
- border-radius: 4px;
1467
- background: white;
1468
- font-size: 14px;
1469
- }
1470
-
1471
- .cv-custom-state-input:focus {
1472
- outline: none;
1473
- border-color: #007bff;
1474
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1475
- }
1476
-
1477
- .cv-custom-toggles {
1478
- display: grid;
1479
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1480
- gap: 10px;
1481
- }
1482
-
1483
- .cv-custom-state-toggle {
1484
- display: flex;
1485
- align-items: center;
1486
- }
1487
-
1488
- .cv-custom-state-toggle label {
1489
- display: flex;
1490
- align-items: center;
1491
- cursor: pointer;
1492
- font-weight: normal;
1493
- margin: 0;
1494
- }
1495
-
1496
- .cv-custom-toggle-checkbox {
1497
- margin-right: 8px;
1498
- width: auto;
1499
- }
1500
-
1501
- .cv-tab-groups {
1502
- margin-top: 20px;
1503
- }
1504
-
1505
- .cv-tab-group-control {
1506
- margin-bottom: 15px;
1507
- }
1508
-
1509
- .cv-tab-group-control label {
1510
- display: block;
1511
- margin-bottom: 5px;
1512
- font-weight: 500;
1513
- font-size: 14px;
1514
- }
1515
-
1516
- .cv-tab-group-select {
1517
- width: 100%;
1518
- padding: 8px 12px;
1519
- border: 1px solid #ced4da;
1520
- border-radius: 4px;
1521
- font-size: 14px;
1522
- background-color: white;
1523
- cursor: pointer;
1524
- }
1525
-
1526
- .cv-tab-group-select:focus {
1527
- outline: none;
1528
- border-color: #007bff;
1529
- box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
1530
- }
1531
-
1532
- .cv-widget-theme-dark .cv-tab-group-select {
1533
- background-color: #2d3748;
1534
- border-color: #4a5568;
1535
- color: #e2e8f0;
1536
- }
1537
-
1538
- .cv-custom-state-actions {
1539
- display: flex;
1540
- gap: 10px;
1541
- margin-top: 20px;
1542
- padding-top: 16px;
1543
- border-top: 1px solid #e9ecef;
1544
- }
1545
-
1546
- .cv-custom-state-cancel,
1547
- .cv-custom-state-copy-url {
1548
- flex: 1;
1549
- padding: 10px 16px;
1550
- border: none;
1551
- border-radius: 4px;
1552
- cursor: pointer;
1553
- font-size: 14px;
1554
- font-weight: 500;
1555
- }
1556
-
1557
- .cv-custom-state-reset {
1558
- flex: 1;
1559
- padding: 10px 16px;
1560
- border: none;
1561
- border-radius: 4px;
1562
- cursor: pointer;
1563
- font-size: 14px;
1564
- font-weight: 500;
1565
- background: #dc3545;
1566
- color: white;
1567
- }
1568
-
1569
- .cv-custom-state-reset:hover {
1570
- background: #c82333;
1571
- }
1572
-
1573
- .cv-custom-state-cancel {
1574
- background: #6c757d;
1575
- color: white;
1576
- }
1577
-
1578
- .cv-custom-state-cancel:hover {
1579
- background: #5a6268;
1580
- }
1581
-
1582
- .cv-custom-state-copy-url {
1583
- background: #28a745;
1584
- color: white;
1585
- }
1586
-
1587
- .cv-custom-state-copy-url:hover {
1588
- background: #218838;
1589
- }
1590
-
1591
- /* Dark theme custom state styles */
1592
- .cv-widget-theme-dark .cv-custom-state-form h4 {
1593
- color: #e2e8f0;
1594
- border-color: #4a5568;
1595
- }
1596
-
1597
- .cv-widget-theme-dark .cv-custom-state-section label {
1598
- color: #a0aec0;
1599
- }
1600
-
1601
- .cv-widget-theme-dark .cv-custom-state-input {
1602
- background: #1a202c;
1603
- border-color: #4a5568;
1604
- color: #e2e8f0;
1605
- }
1606
-
1607
- .cv-widget-theme-dark .cv-custom-state-actions {
1608
- border-color: #4a5568;
1609
- }
1610
-
1611
- /* Welcome modal styles */
1612
- .cv-welcome-modal {
1613
- max-width: 500px;
1614
- }
1615
-
1616
- .cv-welcome-content {
1617
- text-align: center;
1618
- }
1619
-
1620
- .cv-welcome-content p {
1621
- font-size: 15px;
1622
- line-height: 1.6;
1623
- color: #555;
1624
- margin-bottom: 24px;
1625
- }
1626
-
1627
- .cv-welcome-widget-preview {
1628
- display: flex;
1629
- flex-direction: column;
1630
- align-items: center;
1631
- gap: 12px;
1632
- padding: 20px;
1633
- background: #f8f9fa;
1634
- border-radius: 8px;
1635
- margin-bottom: 24px;
1636
- }
1637
-
1638
- .cv-welcome-widget-icon {
1639
- width: 36px;
1640
- height: 36px;
1641
- background: white;
1642
- color: black;
1643
- border-radius: 0 18px 18px 0;
1644
- display: flex;
1645
- align-items: center;
1646
- justify-content: center;
1647
- font-size: 18px;
1648
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1649
- }
1650
-
1651
- .cv-welcome-widget-label {
1652
- font-size: 14px;
1653
- color: #666;
1654
- margin: 0;
1655
- font-weight: 500;
1656
- }
1657
-
1658
- .cv-welcome-got-it {
1659
- width: 100%;
1660
- padding: 12px 24px;
1661
- background: #007bff;
1662
- color: white;
1663
- border: none;
1664
- border-radius: 4px;
1665
- cursor: pointer;
1666
- font-size: 16px;
1667
- font-weight: 600;
1668
- transition: background 0.2s ease;
1669
- }
1670
-
1671
- .cv-welcome-got-it:hover {
1672
- background: #0056b3;
1673
- }
1674
-
1675
- .cv-welcome-got-it:active {
1676
- background: #004494;
1677
- }
1678
-
1679
- /* Dark theme welcome modal styles */
1680
- .cv-widget-theme-dark .cv-welcome-content p {
1681
- color: #cbd5e0;
1682
- }
1683
-
1684
- .cv-widget-theme-dark .cv-welcome-widget-preview {
1685
- background: #1a202c;
1686
- }
1687
-
1688
- .cv-widget-theme-dark .cv-welcome-widget-label {
1689
- color: #a0aec0;
1690
- }
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
+ }
1691
1850
  `;
1692
1851
  /**
1693
1852
  * Inject widget styles into the document head
@@ -1722,8 +1881,8 @@ class CustomViewsWidget {
1722
1881
  title: options.title || 'Custom Views',
1723
1882
  description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1724
1883
  showWelcome: options.showWelcome ?? false,
1725
- welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1726
- 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.',
1727
1886
  showTabGroups: options.showTabGroups ?? true
1728
1887
  };
1729
1888
  // No external state manager to initialize
@@ -1803,13 +1962,13 @@ class CustomViewsWidget {
1803
1962
  this.modal.className = 'cv-widget-modal-overlay';
1804
1963
  this.applyThemeToModal();
1805
1964
  const toggleControls = toggles.length
1806
- ? toggles.map(toggle => `
1807
- <div class="cv-custom-state-toggle">
1808
- <label>
1809
- <input type="checkbox" class="cv-custom-toggle-checkbox" data-toggle="${toggle}" />
1810
- ${this.formatToggleName(toggle)}
1811
- </label>
1812
- </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>
1813
1972
  `).join('')
1814
1973
  : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1815
1974
  // Get tab groups
@@ -1818,46 +1977,46 @@ class CustomViewsWidget {
1818
1977
  if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
1819
1978
  const tabGroupControls = tabGroups.map(group => {
1820
1979
  const options = group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('');
1821
- return `
1822
- <div class="cv-tab-group-control">
1823
- <label for="tab-group-${group.id}">${group.label || group.id}</label>
1824
- <select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
1825
- ${options}
1826
- </select>
1827
- </div>
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>
1828
1987
  `;
1829
1988
  }).join('');
1830
- tabGroupsHTML = `
1831
- <h4>Tab Groups</h4>
1832
- <div class="cv-tab-groups">
1833
- ${tabGroupControls}
1834
- </div>
1989
+ tabGroupsHTML = `
1990
+ <h4>Tab Groups</h4>
1991
+ <div class="cv-tab-groups">
1992
+ ${tabGroupControls}
1993
+ </div>
1835
1994
  `;
1836
1995
  }
1837
- this.modal.innerHTML = `
1838
- <div class="cv-widget-modal cv-custom-state-modal">
1839
- <div class="cv-widget-modal-header">
1840
- <h3>Customize View</h3>
1841
- <button class="cv-widget-modal-close" aria-label="Close modal">X</button>
1842
- </div>
1843
- <div class="cv-widget-modal-content">
1844
- <div class="cv-custom-state-form">
1845
- <p>${this.options.description}</p>
1846
-
1847
- <h4>Content Sections</h4>
1848
- <div class="cv-custom-toggles">
1849
- ${toggleControls}
1850
- </div>
1851
-
1852
- ${tabGroupsHTML}
1853
-
1854
- <div class="cv-custom-state-actions">
1855
- ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1856
- <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
1857
- </div>
1858
- </div>
1859
- </div>
1860
- </div>
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>
1861
2020
  `;
1862
2021
  document.body.appendChild(this.modal);
1863
2022
  this.attachStateModalEventListeners();
@@ -2042,25 +2201,25 @@ class CustomViewsWidget {
2042
2201
  this.modal = document.createElement('div');
2043
2202
  this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
2044
2203
  this.applyThemeToModal();
2045
- this.modal.innerHTML = `
2046
- <div class="cv-widget-modal cv-welcome-modal">
2047
- <div class="cv-widget-modal-header">
2048
- <h3>${this.options.welcomeTitle}</h3>
2049
- <button class="cv-widget-modal-close" aria-label="Close modal">×</button>
2050
- </div>
2051
- <div class="cv-widget-modal-content">
2052
- <div class="cv-welcome-content">
2053
- <p>${this.options.welcomeMessage}</p>
2054
-
2055
- <div class="cv-welcome-widget-preview">
2056
- <div class="cv-welcome-widget-icon">⚙</div>
2057
- <p class="cv-welcome-widget-label">Look for this widget on the side of the screen</p>
2058
- </div>
2059
-
2060
- <button class="cv-welcome-got-it">Got it!</button>
2061
- </div>
2062
- </div>
2063
- </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>
2064
2223
  `;
2065
2224
  document.body.appendChild(this.modal);
2066
2225
  this.attachWelcomeModalEventListeners();
@@ -2102,46 +2261,6 @@ class CustomViewsWidget {
2102
2261
  }
2103
2262
  }
2104
2263
 
2105
- /**
2106
- * Custom Elements for Tab Groups and Tabs
2107
- */
2108
- /**
2109
- * <cv-tab> element - represents a single tab panel
2110
- */
2111
- class CVTab extends HTMLElement {
2112
- connectedCallback() {
2113
- // Element is managed by TabManager
2114
- }
2115
- }
2116
- /**
2117
- * <cv-tabgroup> element - represents a group of tabs
2118
- */
2119
- class CVTabgroup extends HTMLElement {
2120
- connectedCallback() {
2121
- // Element is managed by TabManager
2122
- // Emit ready event after a brief delay to ensure children are parsed
2123
- setTimeout(() => {
2124
- const event = new CustomEvent('cv:tabgroup-ready', {
2125
- bubbles: true,
2126
- detail: { groupId: this.getAttribute('id') }
2127
- });
2128
- this.dispatchEvent(event);
2129
- }, 0);
2130
- }
2131
- }
2132
- /**
2133
- * Register custom elements
2134
- */
2135
- function registerCustomElements() {
2136
- // Only register if not already defined
2137
- if (!customElements.get('cv-tab')) {
2138
- customElements.define('cv-tab', CVTab);
2139
- }
2140
- if (!customElements.get('cv-tabgroup')) {
2141
- customElements.define('cv-tabgroup', CVTabgroup);
2142
- }
2143
- }
2144
-
2145
2264
  /**
2146
2265
  * Initialize CustomViews from script tag attributes and config file
2147
2266
  * This function handles the automatic initialization of CustomViews when included via script tag
@@ -2170,22 +2289,20 @@ function initializeFromScript() {
2170
2289
  return;
2171
2290
  }
2172
2291
  window.__customViewsInitInProgress = true;
2173
- // Register custom elements early
2174
- registerCustomElements();
2175
2292
  try {
2176
2293
  // Find the script tag
2177
2294
  let scriptTag = document.currentScript;
2178
2295
  // Fallback if currentScript is not available (executed after page load)
2179
2296
  if (!scriptTag) {
2180
2297
  // Try to find the script tag by looking for our script
2181
- const scripts = document.querySelectorAll('script[src*="custom-views"]');
2298
+ const scripts = document.querySelectorAll('script[src*="@customviews-js"]');
2182
2299
  if (scripts.length > 0) {
2183
2300
  // Find the most specific match (to avoid matching other custom-views scripts)
2184
2301
  for (let i = 0; i < scripts.length; i++) {
2185
2302
  const script = scripts[i];
2186
2303
  const src = script.getAttribute('src') || '';
2187
- // Look for .min.js or .js at the end
2188
- if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
2304
+ // Look for .min.js or .js at the end, or the package root
2305
+ if (src.match(/@customviews-js\/customviews(\.min)?\.js($|\?)/) || src.includes('@customviews-js/customviews')) {
2189
2306
  scriptTag = script;
2190
2307
  break;
2191
2308
  }