@customviews-js/customviews 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +147 -31
  2. package/dist/{custom-views.cjs.js → custom-views.core.cjs.js} +813 -113
  3. package/dist/custom-views.core.cjs.js.map +1 -0
  4. package/dist/custom-views.core.esm.js +2296 -0
  5. package/dist/custom-views.core.esm.js.map +1 -0
  6. package/dist/custom-views.esm.js +813 -112
  7. package/dist/custom-views.esm.js.map +1 -1
  8. package/dist/{custom-views.umd.js → custom-views.js} +813 -113
  9. package/dist/custom-views.js.map +1 -0
  10. package/dist/custom-views.min.js +7 -0
  11. package/dist/custom-views.min.js.map +1 -0
  12. package/dist/types/{models/AssetsManager.d.ts → core/assets-manager.d.ts} +1 -1
  13. package/dist/types/{models/AssetsManager.d.ts.map → core/assets-manager.d.ts.map} +1 -1
  14. package/dist/types/core/core.d.ts +25 -9
  15. package/dist/types/core/core.d.ts.map +1 -1
  16. package/dist/types/core/custom-elements.d.ts +8 -0
  17. package/dist/types/core/custom-elements.d.ts.map +1 -0
  18. package/dist/types/core/render.d.ts +1 -1
  19. package/dist/types/core/render.d.ts.map +1 -1
  20. package/dist/types/core/tab-manager.d.ts +35 -0
  21. package/dist/types/core/tab-manager.d.ts.map +1 -0
  22. package/dist/types/core/url-state-manager.d.ts.map +1 -1
  23. package/dist/types/core/visibility-manager.d.ts +1 -1
  24. package/dist/types/core/widget.d.ts +2 -0
  25. package/dist/types/core/widget.d.ts.map +1 -1
  26. package/dist/types/entry/browser-entry.d.ts +13 -0
  27. package/dist/types/entry/browser-entry.d.ts.map +1 -0
  28. package/dist/types/index.d.ts +11 -21
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/lib/custom-views.d.ts +29 -0
  31. package/dist/types/lib/custom-views.d.ts.map +1 -0
  32. package/dist/types/styles/styles.d.ts +2 -0
  33. package/dist/types/styles/styles.d.ts.map +1 -1
  34. package/dist/types/styles/tab-styles.d.ts +5 -0
  35. package/dist/types/styles/tab-styles.d.ts.map +1 -0
  36. package/dist/types/styles/toggle-styles.d.ts +5 -0
  37. package/dist/types/styles/toggle-styles.d.ts.map +1 -0
  38. package/dist/types/styles/widget-styles.d.ts +1 -1
  39. package/dist/types/styles/widget-styles.d.ts.map +1 -1
  40. package/dist/types/types/types.d.ts +85 -0
  41. package/dist/types/types/types.d.ts.map +1 -1
  42. package/dist/types/utils/url-utils.d.ts +8 -0
  43. package/dist/types/utils/url-utils.d.ts.map +1 -0
  44. package/package.json +13 -9
  45. package/dist/custom-views.cjs.js.map +0 -1
  46. package/dist/custom-views.umd.js.map +0 -1
  47. package/dist/custom-views.umd.min.js +0 -7
  48. package/dist/custom-views.umd.min.js.map +0 -1
  49. package/dist/types/models/Config.d.ts +0 -10
  50. package/dist/types/models/Config.d.ts.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @customviews-js/customviews v1.0.2
2
+ * @customviews-js/customviews v1.1.0
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
@@ -82,18 +82,6 @@ function renderAssetInto(el, assetId, assetsManager) {
82
82
  }
83
83
  }
84
84
 
85
- /**
86
- * Configuration for the site, has default state and list of toggles
87
- */
88
- class Config {
89
- defaultState;
90
- allToggles;
91
- constructor(defaultState, allToggles) {
92
- this.defaultState = defaultState;
93
- this.allToggles = allToggles;
94
- }
95
- }
96
-
97
85
  /**
98
86
  * Manages persistence of custom views state using browser localStorage
99
87
  */
@@ -223,6 +211,10 @@ class URLStateManager {
223
211
  const compact = {
224
212
  t: state.toggles
225
213
  };
214
+ // Add tab groups if present
215
+ if (state.tabs && Object.keys(state.tabs).length > 0) {
216
+ compact.g = Object.entries(state.tabs);
217
+ }
226
218
  // Convert to JSON and encode
227
219
  const json = JSON.stringify(compact);
228
220
  let encoded;
@@ -269,9 +261,19 @@ class URLStateManager {
269
261
  if (!compact || typeof compact !== 'object') {
270
262
  throw new Error('Invalid compact state structure');
271
263
  }
272
- return {
264
+ const state = {
273
265
  toggles: Array.isArray(compact.t) ? compact.t : []
274
266
  };
267
+ // Reconstruct tabs from compact format
268
+ if (Array.isArray(compact.g)) {
269
+ state.tabs = {};
270
+ for (const [groupId, tabId] of compact.g) {
271
+ if (typeof groupId === 'string' && typeof tabId === 'string') {
272
+ state.tabs[groupId] = tabId;
273
+ }
274
+ }
275
+ }
276
+ return state;
275
277
  }
276
278
  catch (error) {
277
279
  console.warn('Failed to decode view state:', error);
@@ -326,7 +328,7 @@ class VisibilityManager {
326
328
  }
327
329
  /**
328
330
  * Apply simple class-based visibility to a toggle element.
329
- * The element is assumed to have data-customviews-toggle.
331
+ * The element is assumed to have data-cv-toggle or data-customviews-toggle.
330
332
  */
331
333
  applyElementVisibility(el, visible) {
332
334
  if (visible) {
@@ -340,8 +342,211 @@ class VisibilityManager {
340
342
  }
341
343
  }
342
344
 
343
- const CORE_STYLES = `
344
- [data-customviews-toggle] {
345
+ // Constants for selectors
346
+ const TABGROUP_SELECTOR = 'cv-tabgroup';
347
+ const TAB_SELECTOR = 'cv-tab';
348
+ const NAV_AUTO_SELECTOR = 'cv-tabgroup[nav="auto"], cv-tabgroup:not([nav])';
349
+ const NAV_CONTAINER_CLASS = 'cv-tabs-nav';
350
+ /**
351
+ * TabManager handles discovery, visibility, and navigation for tab groups and tabs
352
+ */
353
+ class TabManager {
354
+ /**
355
+ * Apply tab selections to all tab groups in the DOM
356
+ */
357
+ static applySelections(rootEl, tabs, cfgGroups) {
358
+ // Find all cv-tabgroup elements
359
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
360
+ tabGroups.forEach((groupEl) => {
361
+ const groupId = groupEl.getAttribute('id');
362
+ if (!groupId)
363
+ return;
364
+ // Determine the active tab for this group
365
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
366
+ // Apply visibility to child cv-tab elements
367
+ const tabElements = groupEl.querySelectorAll(TAB_SELECTOR);
368
+ tabElements.forEach((tabEl) => {
369
+ const tabId = tabEl.getAttribute('id');
370
+ if (!tabId)
371
+ return;
372
+ const isActive = tabId === activeTabId;
373
+ this.applyTabVisibility(tabEl, isActive);
374
+ });
375
+ });
376
+ }
377
+ /**
378
+ * Resolve the active tab for a group based on state, config, and DOM
379
+ */
380
+ static resolveActiveTab(groupId, tabs, cfgGroups, groupEl) {
381
+ // 1. Check state
382
+ if (tabs[groupId]) {
383
+ return tabs[groupId];
384
+ }
385
+ // 2. Check config default
386
+ if (cfgGroups) {
387
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
388
+ if (groupCfg) {
389
+ if (groupCfg.default) {
390
+ return groupCfg.default;
391
+ }
392
+ // Fallback to first tab in config
393
+ const firstConfigTab = groupCfg.tabs[0];
394
+ if (firstConfigTab) {
395
+ return firstConfigTab.id;
396
+ }
397
+ }
398
+ }
399
+ // 3. Fallback to first cv-tab child in DOM
400
+ const firstTab = groupEl.querySelector(TAB_SELECTOR);
401
+ if (firstTab) {
402
+ return firstTab.getAttribute('id');
403
+ }
404
+ return null;
405
+ }
406
+ /**
407
+ * Apply visibility classes to a tab element
408
+ */
409
+ static applyTabVisibility(tabEl, isActive) {
410
+ if (isActive) {
411
+ tabEl.classList.remove('cv-hidden');
412
+ tabEl.classList.add('cv-visible');
413
+ }
414
+ else {
415
+ tabEl.classList.add('cv-hidden');
416
+ tabEl.classList.remove('cv-visible');
417
+ }
418
+ }
419
+ /**
420
+ * Build navigation for tab groups with nav="auto" (one-time setup)
421
+ */
422
+ static buildNavs(rootEl, cfgGroups, onTabClick) {
423
+ // Find all cv-tabgroup elements with nav="auto" or no nav attribute
424
+ const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
425
+ tabGroups.forEach((groupEl) => {
426
+ const groupId = groupEl.getAttribute('id');
427
+ if (!groupId)
428
+ return;
429
+ // Check if nav already exists - if so, skip building
430
+ let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
431
+ if (navContainer)
432
+ return; // Already built
433
+ // Get all child tabs
434
+ const tabElements = Array.from(groupEl.querySelectorAll(TAB_SELECTOR));
435
+ if (tabElements.length === 0)
436
+ return;
437
+ // Create nav container
438
+ navContainer = document.createElement('ul');
439
+ navContainer.className = `${NAV_CONTAINER_CLASS} nav-tabs`;
440
+ navContainer.setAttribute('role', 'tablist');
441
+ groupEl.insertBefore(navContainer, groupEl.firstChild);
442
+ // Build nav items
443
+ tabElements.forEach((tabEl) => {
444
+ const tabId = tabEl.getAttribute('id');
445
+ if (!tabId)
446
+ return;
447
+ const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
448
+ const listItem = document.createElement('li');
449
+ listItem.className = 'nav-item';
450
+ const navLink = document.createElement('a');
451
+ navLink.className = 'nav-link';
452
+ navLink.textContent = header;
453
+ navLink.href = '#';
454
+ navLink.setAttribute('data-tab-id', tabId);
455
+ navLink.setAttribute('data-group-id', groupId);
456
+ navLink.setAttribute('role', 'tab');
457
+ // Check if this tab is currently active
458
+ const isActive = tabEl.classList.contains('cv-visible');
459
+ if (isActive) {
460
+ navLink.classList.add('active');
461
+ navLink.setAttribute('aria-selected', 'true');
462
+ }
463
+ else {
464
+ navLink.setAttribute('aria-selected', 'false');
465
+ }
466
+ // Add click handler
467
+ if (onTabClick) {
468
+ navLink.addEventListener('click', (e) => {
469
+ e.preventDefault();
470
+ onTabClick(groupId, tabId);
471
+ });
472
+ }
473
+ listItem.appendChild(navLink);
474
+ navContainer.appendChild(listItem);
475
+ });
476
+ // Add bottom border line at the end of the tab group
477
+ const bottomBorder = document.createElement('div');
478
+ bottomBorder.className = 'cv-tabgroup-bottom-border';
479
+ groupEl.appendChild(bottomBorder);
480
+ });
481
+ }
482
+ /**
483
+ * Get tab label from config
484
+ */
485
+ static getTabLabel(tabId, groupId, cfgGroups) {
486
+ if (!cfgGroups)
487
+ return null;
488
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
489
+ if (!groupCfg)
490
+ return null;
491
+ const tabCfg = groupCfg.tabs.find(t => t.id === tabId);
492
+ return tabCfg?.label || null;
493
+ }
494
+ /**
495
+ * Update active state in navs after selection change (single group)
496
+ */
497
+ static updateNavActiveState(rootEl, groupId, activeTabId) {
498
+ const tabGroups = rootEl.querySelectorAll(`${TABGROUP_SELECTOR}[id="${groupId}"]`);
499
+ tabGroups.forEach((groupEl) => {
500
+ const navLinks = groupEl.querySelectorAll('.nav-link');
501
+ navLinks.forEach((link) => {
502
+ const tabId = link.getAttribute('data-tab-id');
503
+ if (tabId === activeTabId) {
504
+ link.classList.add('active');
505
+ link.setAttribute('aria-selected', 'true');
506
+ }
507
+ else {
508
+ link.classList.remove('active');
509
+ link.setAttribute('aria-selected', 'false');
510
+ }
511
+ });
512
+ });
513
+ }
514
+ /**
515
+ * Update active states for all tab groups based on current state
516
+ */
517
+ static updateAllNavActiveStates(rootEl, tabs, cfgGroups) {
518
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
519
+ tabGroups.forEach((groupEl) => {
520
+ const groupId = groupEl.getAttribute('id');
521
+ if (!groupId)
522
+ return;
523
+ // Determine the active tab for this group
524
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
525
+ if (!activeTabId)
526
+ return;
527
+ // Update nav links for this group
528
+ const navLinks = groupEl.querySelectorAll('.nav-link');
529
+ navLinks.forEach((link) => {
530
+ const tabId = link.getAttribute('data-tab-id');
531
+ if (tabId === activeTabId) {
532
+ link.classList.add('active');
533
+ link.setAttribute('aria-selected', 'true');
534
+ }
535
+ else {
536
+ link.classList.remove('active');
537
+ link.setAttribute('aria-selected', 'false');
538
+ }
539
+ });
540
+ });
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Styles for toggle visibility and animations
546
+ */
547
+ const TOGGLE_STYLES = `
548
+ /* Core toggle visibility transitions */
549
+ [data-cv-toggle], [data-customviews-toggle] {
345
550
  transition: opacity 150ms ease,
346
551
  transform 150ms ease,
347
552
  max-height 200ms ease,
@@ -369,6 +574,125 @@ const CORE_STYLES = `
369
574
  overflow: hidden !important;
370
575
  }
371
576
  `;
577
+
578
+ /**
579
+ * Styles for tab groups and tab navigation
580
+ */
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
+ }
686
+ `;
687
+
688
+ /**
689
+ * Combined core styles for toggles and tabs
690
+ */
691
+ const CORE_STYLES = `
692
+ ${TOGGLE_STYLES}
693
+
694
+ ${TAB_STYLES}
695
+ `;
372
696
  /**
373
697
  * Add styles for hiding and showing toggles animations and transitions to the document head
374
698
  */
@@ -388,36 +712,85 @@ class CustomViewsCore {
388
712
  assetsManager;
389
713
  persistenceManager;
390
714
  visibilityManager;
391
- stateFromUrl = null;
392
- localConfig;
715
+ config;
393
716
  stateChangeListeners = [];
717
+ showUrlEnabled;
718
+ lastAppliedState = null;
394
719
  constructor(opt) {
395
720
  this.assetsManager = opt.assetsManager;
396
- this.localConfig = opt.config;
721
+ this.config = opt.config;
397
722
  this.rootEl = opt.rootEl || document.body;
398
723
  this.persistenceManager = new PersistenceManager();
399
724
  this.visibilityManager = new VisibilityManager();
725
+ this.showUrlEnabled = opt.showUrl ?? true;
726
+ this.lastAppliedState = this.cloneState(this.config?.defaultState);
727
+ }
728
+ getConfig() {
729
+ return this.config;
730
+ }
731
+ /**
732
+ * Get tab groups from config
733
+ */
734
+ getTabGroups() {
735
+ return this.config.tabGroups;
400
736
  }
401
- getLocalConfig() {
402
- return this.localConfig;
737
+ /**
738
+ * Get currently active tabs (from URL > persisted (localStorage) > defaults)
739
+ */
740
+ getCurrentActiveTabs() {
741
+ if (this.lastAppliedState?.tabs) {
742
+ return { ...this.lastAppliedState.tabs };
743
+ }
744
+ const persistedState = this.persistenceManager.getPersistedState();
745
+ if (persistedState?.tabs) {
746
+ return { ...persistedState.tabs };
747
+ }
748
+ return this.config?.defaultState?.tabs ? { ...this.config.defaultState.tabs } : {};
749
+ }
750
+ /**
751
+ * Set active tab for a group and apply state
752
+ */
753
+ setActiveTab(groupId, tabId) {
754
+ // Get current state
755
+ const currentToggles = this.getCurrentActiveToggles();
756
+ const currentTabs = this.getCurrentActiveTabs();
757
+ // Merge new tab selection
758
+ const newTabs = { ...currentTabs, [groupId]: tabId };
759
+ // Create new state
760
+ const newState = {
761
+ toggles: currentToggles,
762
+ tabs: newTabs
763
+ };
764
+ // Apply the state
765
+ this.applyState(newState);
766
+ // Emit custom event
767
+ const event = new CustomEvent('customviews:tab-change', {
768
+ detail: { groupId, tabId },
769
+ bubbles: true
770
+ });
771
+ document.dispatchEvent(event);
403
772
  }
404
773
  // Inject styles, setup listeners and call rendering logic
405
774
  async init() {
406
775
  injectCoreStyles();
776
+ // Build navigation once (with click handlers)
777
+ TabManager.buildNavs(this.rootEl, this.config.tabGroups, (groupId, tabId) => {
778
+ this.setActiveTab(groupId, tabId);
779
+ });
407
780
  // For session history, clicks on back/forward button
408
781
  window.addEventListener("popstate", () => {
409
- this.loadAndRenderState();
782
+ this.loadAndCallApplyState();
410
783
  });
411
- this.loadAndRenderState();
784
+ this.loadAndCallApplyState();
412
785
  }
413
786
  // Priority: URL state > persisted state > default
414
787
  // Also filters using the visibility manager to persist selection
415
788
  // across back/forward button clicks
416
- async loadAndRenderState() {
789
+ async loadAndCallApplyState() {
417
790
  // 1. URL State
418
- this.stateFromUrl = URLStateManager.parseURL();
419
- if (this.stateFromUrl) {
420
- this.applyState(this.stateFromUrl);
791
+ const urlState = URLStateManager.parseURL();
792
+ if (urlState) {
793
+ this.applyState(urlState);
421
794
  return;
422
795
  }
423
796
  // 2. Persisted State
@@ -427,38 +800,48 @@ class CustomViewsCore {
427
800
  return;
428
801
  }
429
802
  // 3. Local Config Fallback
430
- this.renderState(this.localConfig.defaultState);
803
+ this.renderState(this.config.defaultState);
431
804
  }
432
805
  /**
433
- * Apply a custom state, saves to localStorage and updates the URL
434
- */
806
+ * Apply a custom state, saves to localStorage and updates the URL
807
+ */
435
808
  applyState(state) {
436
- this.renderState(state);
437
- this.persistenceManager.persistState(state);
438
- this.stateFromUrl = state;
439
- URLStateManager.updateURL(state);
809
+ const snapshot = this.cloneState(state);
810
+ this.renderState(snapshot);
811
+ this.persistenceManager.persistState(snapshot);
812
+ if (this.showUrlEnabled) {
813
+ URLStateManager.updateURL(snapshot);
814
+ }
815
+ else {
816
+ URLStateManager.clearURL();
817
+ }
440
818
  }
441
819
  /** Render all toggles for the current state */
442
820
  renderState(state) {
821
+ this.lastAppliedState = this.cloneState(state);
443
822
  const toggles = state.toggles || [];
444
823
  const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
445
824
  // Toggles hide or show relevant toggles
446
- this.rootEl.querySelectorAll("[data-customviews-toggle]").forEach(el => {
447
- const category = el.dataset.customviewsToggle;
825
+ this.rootEl.querySelectorAll("[data-cv-toggle], [data-customviews-toggle]").forEach(el => {
826
+ const category = el.dataset.cvToggle || el.dataset.customviewsToggle;
448
827
  const shouldShow = !!category && finalToggles.includes(category);
449
828
  this.visibilityManager.applyElementVisibility(el, shouldShow);
450
829
  });
451
830
  // Render toggles
452
831
  for (const category of finalToggles) {
453
- this.rootEl.querySelectorAll(`[data-customviews-toggle="${category}"]`).forEach(el => {
454
- // if it has an id, then we render the asset into it
455
- // if it has no id, then we assume it's a container
456
- const toggleId = el.dataset.customviewsId;
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;
457
836
  if (toggleId) {
458
837
  renderAssetInto(el, toggleId, this.assetsManager);
459
838
  }
460
839
  });
461
840
  }
841
+ // Apply tab selections
842
+ TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
843
+ // Update nav active states (without rebuilding)
844
+ TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
462
845
  // Notify state change listeners (like widgets)
463
846
  this.notifyStateChangeListeners();
464
847
  }
@@ -466,10 +849,9 @@ class CustomViewsCore {
466
849
  * Reset to default state
467
850
  */
468
851
  resetToDefault() {
469
- this.stateFromUrl = null;
470
852
  this.persistenceManager.clearAll();
471
- if (this.localConfig) {
472
- this.renderState(this.localConfig.defaultState);
853
+ if (this.config) {
854
+ this.renderState(this.config.defaultState);
473
855
  }
474
856
  else {
475
857
  console.warn("No configuration loaded, cannot reset to default state");
@@ -481,15 +863,12 @@ class CustomViewsCore {
481
863
  * Get the currently active toggles regardless of whether they come from custom state or default configuration
482
864
  */
483
865
  getCurrentActiveToggles() {
484
- // If we have a custom state, return its toggles
485
- if (this.stateFromUrl) {
486
- return this.stateFromUrl.toggles || [];
866
+ if (this.lastAppliedState) {
867
+ return this.lastAppliedState.toggles || [];
487
868
  }
488
- // Otherwise, if we have local config, return its default state toggles
489
- if (this.localConfig) {
490
- return this.localConfig.defaultState.toggles || [];
869
+ if (this.config) {
870
+ return this.config.defaultState.toggles || [];
491
871
  }
492
- // No configuration or state
493
872
  return [];
494
873
  }
495
874
  /**
@@ -497,15 +876,35 @@ class CustomViewsCore {
497
876
  */
498
877
  clearPersistence() {
499
878
  this.persistenceManager.clearAll();
500
- this.stateFromUrl = null;
501
- if (this.localConfig) {
502
- this.renderState(this.localConfig.defaultState);
879
+ if (this.config) {
880
+ this.renderState(this.config.defaultState);
503
881
  }
504
882
  else {
505
883
  console.warn("No configuration loaded, cannot reset to default state");
506
884
  }
507
885
  URLStateManager.clearURL();
508
886
  }
887
+ setOption(flag, value) {
888
+ switch (flag) {
889
+ case 'showUrl': {
890
+ const nextValue = Boolean(value);
891
+ if (this.showUrlEnabled === nextValue) {
892
+ return;
893
+ }
894
+ this.showUrlEnabled = nextValue;
895
+ if (nextValue) {
896
+ const stateForUrl = this.getTrackedStateSnapshot();
897
+ URLStateManager.updateURL(stateForUrl);
898
+ }
899
+ else {
900
+ URLStateManager.clearURL();
901
+ }
902
+ break;
903
+ }
904
+ default:
905
+ console.warn(`[CustomViews] Unknown option '${flag}' passed to setOption`);
906
+ }
907
+ }
509
908
  // === STATE CHANGE LISTENER METHODS ===
510
909
  /**
511
910
  * Add a listener that will be called whenever the state changes
@@ -535,6 +934,20 @@ class CustomViewsCore {
535
934
  }
536
935
  });
537
936
  }
937
+ cloneState(state) {
938
+ const toggles = state?.toggles ? [...state.toggles] : [];
939
+ const tabs = state?.tabs ? { ...state.tabs } : undefined;
940
+ return tabs ? { toggles, tabs } : { toggles };
941
+ }
942
+ getTrackedStateSnapshot() {
943
+ if (this.lastAppliedState) {
944
+ return this.cloneState(this.lastAppliedState);
945
+ }
946
+ if (this.config) {
947
+ return this.cloneState(this.config.defaultState);
948
+ }
949
+ return { toggles: [] };
950
+ }
538
951
  }
539
952
 
540
953
  class AssetsManager {
@@ -583,6 +996,70 @@ class AssetsManager {
583
996
  }
584
997
  }
585
998
 
999
+ /**
1000
+ * Helper function to prepend baseUrl to a path
1001
+ * @param path The path to prepend the baseUrl to
1002
+ * @param baseUrl The base URL to prepend
1003
+ * @returns The full URL with baseUrl prepended if applicable
1004
+ */
1005
+ function prependBaseUrl(path, baseUrl) {
1006
+ if (!baseUrl)
1007
+ return path;
1008
+ // Don't prepend if the path is already absolute (starts with http:// or https://)
1009
+ if (path.startsWith('http://') || path.startsWith('https://')) {
1010
+ return path;
1011
+ }
1012
+ // Ensure baseUrl doesn't end with / and path starts with /
1013
+ const cleanbaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
1014
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
1015
+ return cleanbaseUrl + cleanPath;
1016
+ }
1017
+
1018
+ /**
1019
+ * Main CustomViews class for initializing and managing custom views
1020
+ */
1021
+ class CustomViews {
1022
+ /**
1023
+ * Entry Point to use CustomViews
1024
+ * @param opts Initialization options including config object and assets path
1025
+ * @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
1026
+ */
1027
+ static async init(opts) {
1028
+ // Load assets JSON if provided
1029
+ let assetsManager;
1030
+ const baseURL = opts.baseURL || '';
1031
+ if (opts.assetsJsonPath) {
1032
+ const assetsPath = prependBaseUrl(opts.assetsJsonPath, baseURL);
1033
+ const assetsJson = await (await fetch(assetsPath)).json();
1034
+ assetsManager = new AssetsManager(assetsJson, baseURL);
1035
+ }
1036
+ else {
1037
+ assetsManager = new AssetsManager({}, baseURL);
1038
+ }
1039
+ // Use provided config or create a minimal default one
1040
+ let config;
1041
+ if (opts.config) {
1042
+ config = opts.config;
1043
+ }
1044
+ else {
1045
+ console.error("No config provided, using minimal default config");
1046
+ // Create a minimal default config
1047
+ config = { allToggles: [], defaultState: { toggles: [] } };
1048
+ }
1049
+ const coreOptions = {
1050
+ assetsManager,
1051
+ config: config,
1052
+ rootEl: opts.rootEl,
1053
+ };
1054
+ if (opts.showUrl !== undefined) {
1055
+ coreOptions.showUrl = opts.showUrl;
1056
+ }
1057
+ const core = new CustomViewsCore(coreOptions);
1058
+ core.init();
1059
+ return core;
1060
+ }
1061
+ }
1062
+
586
1063
  /**
587
1064
  * Widget styles for CustomViews
588
1065
  * Extracted from widget.ts for better maintainability
@@ -594,8 +1071,10 @@ const WIDGET_STYLES = `
594
1071
  /* Rounded rectangle widget icon styles */
595
1072
  .cv-widget-icon {
596
1073
  position: fixed;
597
- background: white;
598
- color: black;
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;
599
1078
  display: flex;
600
1079
  align-items: center;
601
1080
  justify-content: center;
@@ -609,8 +1088,10 @@ const WIDGET_STYLES = `
609
1088
  }
610
1089
 
611
1090
  .cv-widget-icon:hover {
612
- background: white;
613
- color: black;
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;
614
1095
  }
615
1096
 
616
1097
  /* Top-right: rounded end on left, sticks out leftward on hover */
@@ -1017,6 +1498,43 @@ const WIDGET_STYLES = `
1017
1498
  width: auto;
1018
1499
  }
1019
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
+
1020
1538
  .cv-custom-state-actions {
1021
1539
  display: flex;
1022
1540
  gap: 10px;
@@ -1205,7 +1723,8 @@ class CustomViewsWidget {
1205
1723
  description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1206
1724
  showWelcome: options.showWelcome ?? false,
1207
1725
  welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1208
- welcomeMessage: options.welcomeMessage || 'This website uses Custom Views to let you personalize your experience. Use the widget on the side (⚙) to show or hide different content sections based on your preferences. Your selections will be saved and can be shared via URL.'
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.',
1727
+ showTabGroups: options.showTabGroups ?? true
1209
1728
  };
1210
1729
  // No external state manager to initialize
1211
1730
  }
@@ -1270,8 +1789,8 @@ class CustomViewsWidget {
1270
1789
  */
1271
1790
  openStateModal() {
1272
1791
  // Get toggles from current configuration and open the modal regardless of count
1273
- const localConfig = this.core.getLocalConfig();
1274
- const toggles = localConfig?.allToggles || [];
1792
+ const config = this.core.getConfig();
1793
+ const toggles = config?.allToggles || [];
1275
1794
  this.createCustomStateModal(toggles);
1276
1795
  }
1277
1796
  /**
@@ -1293,6 +1812,28 @@ class CustomViewsWidget {
1293
1812
  </div>
1294
1813
  `).join('')
1295
1814
  : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1815
+ // Get tab groups
1816
+ const tabGroups = this.core.getTabGroups();
1817
+ let tabGroupsHTML = '';
1818
+ if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
1819
+ const tabGroupControls = tabGroups.map(group => {
1820
+ 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>
1828
+ `;
1829
+ }).join('');
1830
+ tabGroupsHTML = `
1831
+ <h4>Tab Groups</h4>
1832
+ <div class="cv-tab-groups">
1833
+ ${tabGroupControls}
1834
+ </div>
1835
+ `;
1836
+ }
1296
1837
  this.modal.innerHTML = `
1297
1838
  <div class="cv-widget-modal cv-custom-state-modal">
1298
1839
  <div class="cv-widget-modal-header">
@@ -1308,6 +1849,8 @@ class CustomViewsWidget {
1308
1849
  ${toggleControls}
1309
1850
  </div>
1310
1851
 
1852
+ ${tabGroupsHTML}
1853
+
1311
1854
  <div class="cv-custom-state-actions">
1312
1855
  ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1313
1856
  <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
@@ -1357,6 +1900,17 @@ class CustomViewsWidget {
1357
1900
  this.core.applyState(state);
1358
1901
  });
1359
1902
  });
1903
+ // Listen to tab group selects
1904
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
1905
+ tabGroupSelects.forEach(select => {
1906
+ select.addEventListener('change', () => {
1907
+ const groupId = select.dataset.groupId;
1908
+ const tabId = select.value;
1909
+ if (groupId && tabId) {
1910
+ this.core.setActiveTab(groupId, tabId);
1911
+ }
1912
+ });
1913
+ });
1360
1914
  // Overlay click to close
1361
1915
  this.modal.addEventListener('click', (e) => {
1362
1916
  if (e.target === this.modal) {
@@ -1401,7 +1955,16 @@ class CustomViewsWidget {
1401
1955
  toggles.push(toggle);
1402
1956
  }
1403
1957
  });
1404
- return { toggles };
1958
+ // Collect tab selections
1959
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
1960
+ const tabs = {};
1961
+ tabGroupSelects.forEach(select => {
1962
+ const groupId = select.dataset.groupId;
1963
+ if (groupId) {
1964
+ tabs[groupId] = select.value;
1965
+ }
1966
+ });
1967
+ return Object.keys(tabs).length > 0 ? { toggles, tabs } : { toggles };
1405
1968
  }
1406
1969
  /**
1407
1970
  * Copy shareable URL to clipboard
@@ -1437,6 +2000,15 @@ class CustomViewsWidget {
1437
2000
  }
1438
2001
  }
1439
2002
  });
2003
+ // Load tab group selections
2004
+ const activeTabs = this.core.getCurrentActiveTabs();
2005
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
2006
+ tabGroupSelects.forEach(select => {
2007
+ const groupId = select.dataset.groupId;
2008
+ if (groupId && activeTabs[groupId]) {
2009
+ select.value = activeTabs[groupId];
2010
+ }
2011
+ });
1440
2012
  }
1441
2013
  /**
1442
2014
  * Format toggle name for display
@@ -1530,74 +2102,202 @@ class CustomViewsWidget {
1530
2102
  }
1531
2103
  }
1532
2104
 
1533
- class CustomViews {
1534
- // Helper function to prepend baseURL to a path
1535
- static prependBaseURL(path, baseURL) {
1536
- if (!baseURL)
1537
- return path;
1538
- // Don't prepend if the path is already absolute (starts with http:// or https://)
1539
- if (path.startsWith('http://') || path.startsWith('https://')) {
1540
- return path;
1541
- }
1542
- // Ensure baseURL doesn't end with / and path starts with /
1543
- const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
1544
- const cleanPath = path.startsWith('/') ? path : '/' + path;
1545
- return cleanBaseURL + cleanPath;
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
1546
2114
  }
1547
- // Entry Point to use CustomViews
1548
- static async initFromJson(opts) {
1549
- // Load assets JSON if provided
1550
- let assetsManager;
1551
- const baseURL = opts.baseURL || '';
1552
- if (opts.assetsJsonPath) {
1553
- const assetsPath = this.prependBaseURL(opts.assetsJsonPath, baseURL);
1554
- const assetsJson = await (await fetch(assetsPath)).json();
1555
- assetsManager = new AssetsManager(assetsJson, baseURL);
1556
- }
1557
- else {
1558
- assetsManager = new AssetsManager({}, baseURL);
1559
- }
1560
- // Load config JSON if provided, else just log error and don't load the custom views
1561
- let localConfig;
1562
- if (opts.config) {
1563
- localConfig = opts.config;
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
+ /**
2146
+ * Initialize CustomViews from script tag attributes and config file
2147
+ * This function handles the automatic initialization of CustomViews when included via script tag
2148
+ *
2149
+ * Data attributes supported:
2150
+ * - data-base-url: Base URL for the site (e.g., "/customviews" for subdirectory deployments)
2151
+ * - data-config-path: Path to config file (default: "/customviews.config.json")
2152
+ *
2153
+ * The function fetches the config file and uses it directly to initialize CustomViews.
2154
+ * Widget visibility is controlled via the config file (widget.enabled property).
2155
+ */
2156
+ function initializeFromScript() {
2157
+ // Only run in browser environment
2158
+ if (typeof window === 'undefined')
2159
+ return;
2160
+ // Use the typed global `window` (see src/types/global.d.ts)
2161
+ // Idempotency guard: if already initialized, skip setting up listener again.
2162
+ if (window.__customViewsInitialized) {
2163
+ // Informational for developers; harmless in production.
2164
+ console.info('[CustomViews] Auto-init skipped: already initialized.');
2165
+ return;
2166
+ }
2167
+ document.addEventListener('DOMContentLoaded', async function () {
2168
+ // Prevent concurrent initialization runs (race conditions when script is loaded twice)
2169
+ if (window.__customViewsInitInProgress || window.__customViewsInitialized) {
2170
+ return;
1564
2171
  }
1565
- else {
1566
- if (!opts.configPath) {
1567
- console.error("No config path provided, skipping custom views");
1568
- return null;
2172
+ window.__customViewsInitInProgress = true;
2173
+ // Register custom elements early
2174
+ registerCustomElements();
2175
+ try {
2176
+ // Find the script tag
2177
+ let scriptTag = document.currentScript;
2178
+ // Fallback if currentScript is not available (executed after page load)
2179
+ if (!scriptTag) {
2180
+ // Try to find the script tag by looking for our script
2181
+ const scripts = document.querySelectorAll('script[src*="custom-views"]');
2182
+ if (scripts.length > 0) {
2183
+ // Find the most specific match (to avoid matching other custom-views scripts)
2184
+ for (let i = 0; i < scripts.length; i++) {
2185
+ const script = scripts[i];
2186
+ const src = script.getAttribute('src') || '';
2187
+ // Look for .min.js or .js at the end
2188
+ if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
2189
+ scriptTag = script;
2190
+ break;
2191
+ }
2192
+ }
2193
+ // If no specific match found, use the first one
2194
+ if (!scriptTag) {
2195
+ scriptTag = scripts[0];
2196
+ }
2197
+ }
2198
+ }
2199
+ // Read data attributes from script tag
2200
+ let baseURL = '';
2201
+ let configPath = '/customviews.config.json';
2202
+ if (scriptTag) {
2203
+ baseURL = scriptTag.getAttribute('data-base-url') || '';
2204
+ configPath = scriptTag.getAttribute('data-config-path') || configPath;
1569
2205
  }
2206
+ // Fetch config file
2207
+ let configFile;
1570
2208
  try {
1571
- const configPath = this.prependBaseURL(opts.configPath, baseURL);
1572
- localConfig = await (await fetch(configPath)).json();
2209
+ const fullConfigPath = prependBaseUrl(configPath, baseURL);
2210
+ console.log(`[CustomViews] Loading config from: ${fullConfigPath}`);
2211
+ const response = await fetch(fullConfigPath);
2212
+ if (!response.ok) {
2213
+ console.warn(`[CustomViews] Config file not found at ${fullConfigPath}. Using defaults.`);
2214
+ // Provide minimal default config structure
2215
+ configFile = {
2216
+ config: {
2217
+ allToggles: [],
2218
+ defaultState: { toggles: [] }
2219
+ },
2220
+ widget: {
2221
+ enabled: true
2222
+ }
2223
+ };
2224
+ }
2225
+ else {
2226
+ configFile = await response.json();
2227
+ console.log('[CustomViews] Config loaded successfully');
2228
+ }
1573
2229
  }
1574
2230
  catch (error) {
1575
- console.error("Error loading config:", error);
1576
- return null;
2231
+ console.error('[CustomViews] Error loading config file:', error);
2232
+ return; // Abort initialization
2233
+ }
2234
+ // Determine effective baseURL (data attribute takes precedence over config)
2235
+ const effectiveBaseURL = baseURL || configFile.baseURL || '';
2236
+ const options = {
2237
+ config: configFile.config,
2238
+ assetsJsonPath: configFile.assetsJsonPath,
2239
+ baseURL: effectiveBaseURL,
2240
+ };
2241
+ if (configFile.showUrl !== undefined) {
2242
+ options.showUrl = configFile.showUrl;
1577
2243
  }
2244
+ // Initialize CustomViews core
2245
+ const core = await CustomViews.init(options);
2246
+ if (!core) {
2247
+ console.error('[CustomViews] Failed to initialize core.');
2248
+ return; // Abort widget creation
2249
+ }
2250
+ // Store instance
2251
+ window.customViewsInstance = { core };
2252
+ // Initialize widget if enabled in config
2253
+ let widget;
2254
+ if (configFile.widget?.enabled !== false) {
2255
+ widget = new CustomViewsWidget({
2256
+ core,
2257
+ ...configFile.widget
2258
+ });
2259
+ widget.render();
2260
+ // Store widget instance
2261
+ window.customViewsInstance.widget = widget;
2262
+ console.log('[CustomViews] Widget initialized and rendered');
2263
+ }
2264
+ else {
2265
+ console.log('[CustomViews] Widget disabled in config - skipping initialization');
2266
+ }
2267
+ // Dispatch ready event
2268
+ const readyEvent = new CustomEvent('customviews:ready', {
2269
+ detail: {
2270
+ core,
2271
+ widget
2272
+ }
2273
+ });
2274
+ document.dispatchEvent(readyEvent);
2275
+ // Mark initialized and clear in-progress flag
2276
+ window.__customViewsInitialized = true;
2277
+ window.__customViewsInitInProgress = false;
1578
2278
  }
1579
- const coreOptions = {
1580
- assetsManager,
1581
- config: localConfig,
1582
- rootEl: opts.rootEl,
1583
- };
1584
- const core = new CustomViewsCore(coreOptions);
1585
- core.init();
1586
- return core;
1587
- }
2279
+ catch (error) {
2280
+ // Clear in-progress flag so a future attempt can retry
2281
+ window.__customViewsInitInProgress = false;
2282
+ console.error('[CustomViews] Auto-initialization error:', error);
2283
+ }
2284
+ });
1588
2285
  }
2286
+
2287
+ // Import from new modules
2288
+ // Set up globals and auto-initialization
1589
2289
  if (typeof window !== "undefined") {
1590
- // @ts-ignore
2290
+ // Expose to window to enable usage (e.g. const app = new window.CustomViews(...))
1591
2291
  window.CustomViews = CustomViews;
1592
- // @ts-ignore
1593
2292
  window.CustomViewsWidget = CustomViewsWidget;
2293
+ // Run auto-initialization
2294
+ initializeFromScript();
1594
2295
  }
1595
2296
 
1596
2297
  exports.AssetsManager = AssetsManager;
1597
2298
  exports.CustomViews = CustomViews;
1598
2299
  exports.CustomViewsCore = CustomViewsCore;
1599
2300
  exports.CustomViewsWidget = CustomViewsWidget;
1600
- exports.LocalConfig = Config;
1601
2301
  exports.PersistenceManager = PersistenceManager;
1602
2302
  exports.URLStateManager = URLStateManager;
1603
- //# sourceMappingURL=custom-views.cjs.js.map
2303
+ //# sourceMappingURL=custom-views.core.cjs.js.map