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