@customviews-js/customviews 1.0.2 → 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 +164 -30
  2. package/dist/{custom-views.cjs.js → custom-views.core.cjs.js} +814 -99
  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 +814 -98
  7. package/dist/custom-views.esm.js.map +1 -1
  8. package/dist/{custom-views.umd.js → custom-views.js} +814 -99
  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 -20
  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
  */
@@ -80,18 +80,6 @@ function renderAssetInto(el, assetId, assetsManager) {
80
80
  }
81
81
  }
82
82
 
83
- /**
84
- * Configuration for the site, has default state and list of toggles
85
- */
86
- class Config {
87
- defaultState;
88
- allToggles;
89
- constructor(defaultState, allToggles) {
90
- this.defaultState = defaultState;
91
- this.allToggles = allToggles;
92
- }
93
- }
94
-
95
83
  /**
96
84
  * Manages persistence of custom views state using browser localStorage
97
85
  */
@@ -221,6 +209,10 @@ class URLStateManager {
221
209
  const compact = {
222
210
  t: state.toggles
223
211
  };
212
+ // Add tab groups if present
213
+ if (state.tabs && Object.keys(state.tabs).length > 0) {
214
+ compact.g = Object.entries(state.tabs);
215
+ }
224
216
  // Convert to JSON and encode
225
217
  const json = JSON.stringify(compact);
226
218
  let encoded;
@@ -267,9 +259,19 @@ class URLStateManager {
267
259
  if (!compact || typeof compact !== 'object') {
268
260
  throw new Error('Invalid compact state structure');
269
261
  }
270
- return {
262
+ const state = {
271
263
  toggles: Array.isArray(compact.t) ? compact.t : []
272
264
  };
265
+ // Reconstruct tabs from compact format
266
+ if (Array.isArray(compact.g)) {
267
+ state.tabs = {};
268
+ for (const [groupId, tabId] of compact.g) {
269
+ if (typeof groupId === 'string' && typeof tabId === 'string') {
270
+ state.tabs[groupId] = tabId;
271
+ }
272
+ }
273
+ }
274
+ return state;
273
275
  }
274
276
  catch (error) {
275
277
  console.warn('Failed to decode view state:', error);
@@ -324,7 +326,7 @@ class VisibilityManager {
324
326
  }
325
327
  /**
326
328
  * Apply simple class-based visibility to a toggle element.
327
- * The element is assumed to have data-customviews-toggle.
329
+ * The element is assumed to have data-cv-toggle or data-customviews-toggle.
328
330
  */
329
331
  applyElementVisibility(el, visible) {
330
332
  if (visible) {
@@ -338,8 +340,211 @@ class VisibilityManager {
338
340
  }
339
341
  }
340
342
 
341
- const CORE_STYLES = `
342
- [data-customviews-toggle] {
343
+ // Constants for selectors
344
+ const TABGROUP_SELECTOR = 'cv-tabgroup';
345
+ const TAB_SELECTOR = 'cv-tab';
346
+ const NAV_AUTO_SELECTOR = 'cv-tabgroup[nav="auto"], cv-tabgroup:not([nav])';
347
+ const NAV_CONTAINER_CLASS = 'cv-tabs-nav';
348
+ /**
349
+ * TabManager handles discovery, visibility, and navigation for tab groups and tabs
350
+ */
351
+ class TabManager {
352
+ /**
353
+ * Apply tab selections to all tab groups in the DOM
354
+ */
355
+ static applySelections(rootEl, tabs, cfgGroups) {
356
+ // Find all cv-tabgroup elements
357
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
358
+ tabGroups.forEach((groupEl) => {
359
+ const groupId = groupEl.getAttribute('id');
360
+ if (!groupId)
361
+ return;
362
+ // Determine the active tab for this group
363
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
364
+ // Apply visibility to child cv-tab elements
365
+ const tabElements = groupEl.querySelectorAll(TAB_SELECTOR);
366
+ tabElements.forEach((tabEl) => {
367
+ const tabId = tabEl.getAttribute('id');
368
+ if (!tabId)
369
+ return;
370
+ const isActive = tabId === activeTabId;
371
+ this.applyTabVisibility(tabEl, isActive);
372
+ });
373
+ });
374
+ }
375
+ /**
376
+ * Resolve the active tab for a group based on state, config, and DOM
377
+ */
378
+ static resolveActiveTab(groupId, tabs, cfgGroups, groupEl) {
379
+ // 1. Check state
380
+ if (tabs[groupId]) {
381
+ return tabs[groupId];
382
+ }
383
+ // 2. Check config default
384
+ if (cfgGroups) {
385
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
386
+ if (groupCfg) {
387
+ if (groupCfg.default) {
388
+ return groupCfg.default;
389
+ }
390
+ // Fallback to first tab in config
391
+ const firstConfigTab = groupCfg.tabs[0];
392
+ if (firstConfigTab) {
393
+ return firstConfigTab.id;
394
+ }
395
+ }
396
+ }
397
+ // 3. Fallback to first cv-tab child in DOM
398
+ const firstTab = groupEl.querySelector(TAB_SELECTOR);
399
+ if (firstTab) {
400
+ return firstTab.getAttribute('id');
401
+ }
402
+ return null;
403
+ }
404
+ /**
405
+ * Apply visibility classes to a tab element
406
+ */
407
+ static applyTabVisibility(tabEl, isActive) {
408
+ if (isActive) {
409
+ tabEl.classList.remove('cv-hidden');
410
+ tabEl.classList.add('cv-visible');
411
+ }
412
+ else {
413
+ tabEl.classList.add('cv-hidden');
414
+ tabEl.classList.remove('cv-visible');
415
+ }
416
+ }
417
+ /**
418
+ * Build navigation for tab groups with nav="auto" (one-time setup)
419
+ */
420
+ static buildNavs(rootEl, cfgGroups, onTabClick) {
421
+ // Find all cv-tabgroup elements with nav="auto" or no nav attribute
422
+ const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
423
+ tabGroups.forEach((groupEl) => {
424
+ const groupId = groupEl.getAttribute('id');
425
+ if (!groupId)
426
+ return;
427
+ // Check if nav already exists - if so, skip building
428
+ let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
429
+ if (navContainer)
430
+ return; // Already built
431
+ // Get all child tabs
432
+ const tabElements = Array.from(groupEl.querySelectorAll(TAB_SELECTOR));
433
+ if (tabElements.length === 0)
434
+ return;
435
+ // Create nav container
436
+ navContainer = document.createElement('ul');
437
+ navContainer.className = `${NAV_CONTAINER_CLASS} nav-tabs`;
438
+ navContainer.setAttribute('role', 'tablist');
439
+ groupEl.insertBefore(navContainer, groupEl.firstChild);
440
+ // Build nav items
441
+ tabElements.forEach((tabEl) => {
442
+ const tabId = tabEl.getAttribute('id');
443
+ if (!tabId)
444
+ return;
445
+ const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
446
+ const listItem = document.createElement('li');
447
+ listItem.className = 'nav-item';
448
+ const navLink = document.createElement('a');
449
+ navLink.className = 'nav-link';
450
+ navLink.textContent = header;
451
+ navLink.href = '#';
452
+ navLink.setAttribute('data-tab-id', tabId);
453
+ navLink.setAttribute('data-group-id', groupId);
454
+ navLink.setAttribute('role', 'tab');
455
+ // Check if this tab is currently active
456
+ const isActive = tabEl.classList.contains('cv-visible');
457
+ if (isActive) {
458
+ navLink.classList.add('active');
459
+ navLink.setAttribute('aria-selected', 'true');
460
+ }
461
+ else {
462
+ navLink.setAttribute('aria-selected', 'false');
463
+ }
464
+ // Add click handler
465
+ if (onTabClick) {
466
+ navLink.addEventListener('click', (e) => {
467
+ e.preventDefault();
468
+ onTabClick(groupId, tabId);
469
+ });
470
+ }
471
+ listItem.appendChild(navLink);
472
+ navContainer.appendChild(listItem);
473
+ });
474
+ // Add bottom border line at the end of the tab group
475
+ const bottomBorder = document.createElement('div');
476
+ bottomBorder.className = 'cv-tabgroup-bottom-border';
477
+ groupEl.appendChild(bottomBorder);
478
+ });
479
+ }
480
+ /**
481
+ * Get tab label from config
482
+ */
483
+ static getTabLabel(tabId, groupId, cfgGroups) {
484
+ if (!cfgGroups)
485
+ return null;
486
+ const groupCfg = cfgGroups.find(g => g.id === groupId);
487
+ if (!groupCfg)
488
+ return null;
489
+ const tabCfg = groupCfg.tabs.find(t => t.id === tabId);
490
+ return tabCfg?.label || null;
491
+ }
492
+ /**
493
+ * Update active state in navs after selection change (single group)
494
+ */
495
+ static updateNavActiveState(rootEl, groupId, activeTabId) {
496
+ const tabGroups = rootEl.querySelectorAll(`${TABGROUP_SELECTOR}[id="${groupId}"]`);
497
+ tabGroups.forEach((groupEl) => {
498
+ const navLinks = groupEl.querySelectorAll('.nav-link');
499
+ navLinks.forEach((link) => {
500
+ const tabId = link.getAttribute('data-tab-id');
501
+ if (tabId === activeTabId) {
502
+ link.classList.add('active');
503
+ link.setAttribute('aria-selected', 'true');
504
+ }
505
+ else {
506
+ link.classList.remove('active');
507
+ link.setAttribute('aria-selected', 'false');
508
+ }
509
+ });
510
+ });
511
+ }
512
+ /**
513
+ * Update active states for all tab groups based on current state
514
+ */
515
+ static updateAllNavActiveStates(rootEl, tabs, cfgGroups) {
516
+ const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
517
+ tabGroups.forEach((groupEl) => {
518
+ const groupId = groupEl.getAttribute('id');
519
+ if (!groupId)
520
+ return;
521
+ // Determine the active tab for this group
522
+ const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
523
+ if (!activeTabId)
524
+ return;
525
+ // Update nav links for this group
526
+ const navLinks = groupEl.querySelectorAll('.nav-link');
527
+ navLinks.forEach((link) => {
528
+ const tabId = link.getAttribute('data-tab-id');
529
+ if (tabId === activeTabId) {
530
+ link.classList.add('active');
531
+ link.setAttribute('aria-selected', 'true');
532
+ }
533
+ else {
534
+ link.classList.remove('active');
535
+ link.setAttribute('aria-selected', 'false');
536
+ }
537
+ });
538
+ });
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Styles for toggle visibility and animations
544
+ */
545
+ const TOGGLE_STYLES = `
546
+ /* Core toggle visibility transitions */
547
+ [data-cv-toggle], [data-customviews-toggle] {
343
548
  transition: opacity 150ms ease,
344
549
  transform 150ms ease,
345
550
  max-height 200ms ease,
@@ -367,6 +572,125 @@ const CORE_STYLES = `
367
572
  overflow: hidden !important;
368
573
  }
369
574
  `;
575
+
576
+ /**
577
+ * Styles for tab groups and tab navigation
578
+ */
579
+ const TAB_STYLES = `
580
+ /* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
581
+ .cv-tabs-nav {
582
+ display: flex;
583
+ flex-wrap: wrap;
584
+ padding-left: 0;
585
+ margin-top: 0.5rem;
586
+ margin-bottom: 1rem;
587
+ list-style: none;
588
+ border-bottom: 1px solid #dee2e6;
589
+ }
590
+
591
+ .cv-tabs-nav .nav-item {
592
+ margin-bottom: -1px;
593
+ list-style: none;
594
+ display: inline-block;
595
+ }
596
+
597
+ .cv-tabs-nav .nav-link {
598
+ display: block;
599
+ padding: 0.5rem 1rem;
600
+ color: #495057;
601
+ text-decoration: none;
602
+ background-color: transparent;
603
+ border: 1px solid transparent;
604
+ border-top-left-radius: 0.25rem;
605
+ border-top-right-radius: 0.25rem;
606
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
607
+ cursor: pointer;
608
+ }
609
+
610
+ .cv-tabs-nav .nav-link:hover,
611
+ .cv-tabs-nav .nav-link:focus {
612
+ border-color: #e9ecef #e9ecef #dee2e6;
613
+ isolation: isolate;
614
+ }
615
+
616
+ .cv-tabs-nav .nav-link.active {
617
+ color: #495057;
618
+ background-color: #fff;
619
+ border-color: #dee2e6 #dee2e6 #fff;
620
+ }
621
+
622
+ .cv-tabs-nav .nav-link:focus {
623
+ outline: 0;
624
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
625
+ }
626
+
627
+ /* Legacy button-based nav (deprecated, kept for compatibility) */
628
+ .cv-tabs-nav-item {
629
+ background: none;
630
+ border: none;
631
+ border-bottom: 2px solid transparent;
632
+ padding: 0.5rem 1rem;
633
+ cursor: pointer;
634
+ font-size: 1rem;
635
+ color: #6c757d;
636
+ transition: color 150ms ease, border-color 150ms ease;
637
+ }
638
+
639
+ .cv-tabs-nav-item:hover {
640
+ color: #495057;
641
+ border-bottom-color: #dee2e6;
642
+ }
643
+
644
+ .cv-tabs-nav-item.active {
645
+ color: #007bff;
646
+ border-bottom-color: #007bff;
647
+ font-weight: 500;
648
+ }
649
+
650
+ .cv-tabs-nav-item:focus {
651
+ outline: 2px solid #007bff;
652
+ outline-offset: 2px;
653
+ }
654
+
655
+ /* Tab panel base styles */
656
+ cv-tab {
657
+ display: block;
658
+ }
659
+
660
+ /* Override visibility for tab panels - use display instead of collapse animation */
661
+ cv-tab.cv-hidden {
662
+ display: none !important;
663
+ }
664
+
665
+ cv-tab.cv-visible {
666
+ display: block !important;
667
+ }
668
+
669
+ cv-tabgroup {
670
+ display: block;
671
+ margin-bottom: 1.5rem;
672
+ }
673
+
674
+ /* Bottom border line for tab groups */
675
+ .cv-tabgroup-bottom-border {
676
+ border-bottom: 1px solid #dee2e6;
677
+ margin-top: 1rem;
678
+ }
679
+
680
+ /* Tab content wrapper */
681
+ .cv-tab-content {
682
+ padding: 1rem 0;
683
+ }
684
+ `;
685
+
686
+ /**
687
+ * Combined core styles for toggles and tabs
688
+ */
689
+ const CORE_STYLES = `
690
+ ${TOGGLE_STYLES}
691
+
692
+ ${TAB_STYLES}
693
+ `;
370
694
  /**
371
695
  * Add styles for hiding and showing toggles animations and transitions to the document head
372
696
  */
@@ -386,36 +710,85 @@ class CustomViewsCore {
386
710
  assetsManager;
387
711
  persistenceManager;
388
712
  visibilityManager;
389
- stateFromUrl = null;
390
- localConfig;
713
+ config;
391
714
  stateChangeListeners = [];
715
+ showUrlEnabled;
716
+ lastAppliedState = null;
392
717
  constructor(opt) {
393
718
  this.assetsManager = opt.assetsManager;
394
- this.localConfig = opt.config;
719
+ this.config = opt.config;
395
720
  this.rootEl = opt.rootEl || document.body;
396
721
  this.persistenceManager = new PersistenceManager();
397
722
  this.visibilityManager = new VisibilityManager();
723
+ this.showUrlEnabled = opt.showUrl ?? true;
724
+ this.lastAppliedState = this.cloneState(this.config?.defaultState);
725
+ }
726
+ getConfig() {
727
+ return this.config;
728
+ }
729
+ /**
730
+ * Get tab groups from config
731
+ */
732
+ getTabGroups() {
733
+ return this.config.tabGroups;
734
+ }
735
+ /**
736
+ * Get currently active tabs (from URL > persisted (localStorage) > defaults)
737
+ */
738
+ getCurrentActiveTabs() {
739
+ if (this.lastAppliedState?.tabs) {
740
+ return { ...this.lastAppliedState.tabs };
741
+ }
742
+ const persistedState = this.persistenceManager.getPersistedState();
743
+ if (persistedState?.tabs) {
744
+ return { ...persistedState.tabs };
745
+ }
746
+ return this.config?.defaultState?.tabs ? { ...this.config.defaultState.tabs } : {};
398
747
  }
399
- getLocalConfig() {
400
- return this.localConfig;
748
+ /**
749
+ * Set active tab for a group and apply state
750
+ */
751
+ setActiveTab(groupId, tabId) {
752
+ // Get current state
753
+ const currentToggles = this.getCurrentActiveToggles();
754
+ const currentTabs = this.getCurrentActiveTabs();
755
+ // Merge new tab selection
756
+ const newTabs = { ...currentTabs, [groupId]: tabId };
757
+ // Create new state
758
+ const newState = {
759
+ toggles: currentToggles,
760
+ tabs: newTabs
761
+ };
762
+ // Apply the state
763
+ this.applyState(newState);
764
+ // Emit custom event
765
+ const event = new CustomEvent('customviews:tab-change', {
766
+ detail: { groupId, tabId },
767
+ bubbles: true
768
+ });
769
+ document.dispatchEvent(event);
401
770
  }
402
771
  // Inject styles, setup listeners and call rendering logic
403
772
  async init() {
404
773
  injectCoreStyles();
774
+ // Build navigation once (with click handlers)
775
+ TabManager.buildNavs(this.rootEl, this.config.tabGroups, (groupId, tabId) => {
776
+ this.setActiveTab(groupId, tabId);
777
+ });
405
778
  // For session history, clicks on back/forward button
406
779
  window.addEventListener("popstate", () => {
407
- this.loadAndRenderState();
780
+ this.loadAndCallApplyState();
408
781
  });
409
- this.loadAndRenderState();
782
+ this.loadAndCallApplyState();
410
783
  }
411
784
  // Priority: URL state > persisted state > default
412
785
  // Also filters using the visibility manager to persist selection
413
786
  // across back/forward button clicks
414
- async loadAndRenderState() {
787
+ async loadAndCallApplyState() {
415
788
  // 1. URL State
416
- this.stateFromUrl = URLStateManager.parseURL();
417
- if (this.stateFromUrl) {
418
- this.applyState(this.stateFromUrl);
789
+ const urlState = URLStateManager.parseURL();
790
+ if (urlState) {
791
+ this.applyState(urlState);
419
792
  return;
420
793
  }
421
794
  // 2. Persisted State
@@ -425,38 +798,48 @@ class CustomViewsCore {
425
798
  return;
426
799
  }
427
800
  // 3. Local Config Fallback
428
- this.renderState(this.localConfig.defaultState);
801
+ this.renderState(this.config.defaultState);
429
802
  }
430
803
  /**
431
- * Apply a custom state, saves to localStorage and updates the URL
432
- */
804
+ * Apply a custom state, saves to localStorage and updates the URL
805
+ */
433
806
  applyState(state) {
434
- this.renderState(state);
435
- this.persistenceManager.persistState(state);
436
- this.stateFromUrl = state;
437
- URLStateManager.updateURL(state);
807
+ const snapshot = this.cloneState(state);
808
+ this.renderState(snapshot);
809
+ this.persistenceManager.persistState(snapshot);
810
+ if (this.showUrlEnabled) {
811
+ URLStateManager.updateURL(snapshot);
812
+ }
813
+ else {
814
+ URLStateManager.clearURL();
815
+ }
438
816
  }
439
817
  /** Render all toggles for the current state */
440
818
  renderState(state) {
819
+ this.lastAppliedState = this.cloneState(state);
441
820
  const toggles = state.toggles || [];
442
821
  const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
443
822
  // Toggles hide or show relevant toggles
444
- this.rootEl.querySelectorAll("[data-customviews-toggle]").forEach(el => {
445
- const category = el.dataset.customviewsToggle;
823
+ this.rootEl.querySelectorAll("[data-cv-toggle], [data-customviews-toggle]").forEach(el => {
824
+ const category = el.dataset.cvToggle || el.dataset.customviewsToggle;
446
825
  const shouldShow = !!category && finalToggles.includes(category);
447
826
  this.visibilityManager.applyElementVisibility(el, shouldShow);
448
827
  });
449
828
  // Render toggles
450
829
  for (const category of finalToggles) {
451
- this.rootEl.querySelectorAll(`[data-customviews-toggle="${category}"]`).forEach(el => {
452
- // if it has an id, then we render the asset into it
453
- // if it has no id, then we assume it's a container
454
- const toggleId = el.dataset.customviewsId;
830
+ this.rootEl.querySelectorAll(`[data-cv-toggle="${category}"], [data-customviews-toggle="${category}"]`).forEach(el => {
831
+ // if it has an id, then we should render the asset into it
832
+ // Support both (data-cv-id) and (data-customviews-id) attributes
833
+ const toggleId = el.dataset.cvId || el.dataset.customviewsId;
455
834
  if (toggleId) {
456
835
  renderAssetInto(el, toggleId, this.assetsManager);
457
836
  }
458
837
  });
459
838
  }
839
+ // Apply tab selections
840
+ TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
841
+ // Update nav active states (without rebuilding)
842
+ TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
460
843
  // Notify state change listeners (like widgets)
461
844
  this.notifyStateChangeListeners();
462
845
  }
@@ -464,10 +847,9 @@ class CustomViewsCore {
464
847
  * Reset to default state
465
848
  */
466
849
  resetToDefault() {
467
- this.stateFromUrl = null;
468
850
  this.persistenceManager.clearAll();
469
- if (this.localConfig) {
470
- this.renderState(this.localConfig.defaultState);
851
+ if (this.config) {
852
+ this.renderState(this.config.defaultState);
471
853
  }
472
854
  else {
473
855
  console.warn("No configuration loaded, cannot reset to default state");
@@ -479,15 +861,12 @@ class CustomViewsCore {
479
861
  * Get the currently active toggles regardless of whether they come from custom state or default configuration
480
862
  */
481
863
  getCurrentActiveToggles() {
482
- // If we have a custom state, return its toggles
483
- if (this.stateFromUrl) {
484
- return this.stateFromUrl.toggles || [];
864
+ if (this.lastAppliedState) {
865
+ return this.lastAppliedState.toggles || [];
485
866
  }
486
- // Otherwise, if we have local config, return its default state toggles
487
- if (this.localConfig) {
488
- return this.localConfig.defaultState.toggles || [];
867
+ if (this.config) {
868
+ return this.config.defaultState.toggles || [];
489
869
  }
490
- // No configuration or state
491
870
  return [];
492
871
  }
493
872
  /**
@@ -495,15 +874,35 @@ class CustomViewsCore {
495
874
  */
496
875
  clearPersistence() {
497
876
  this.persistenceManager.clearAll();
498
- this.stateFromUrl = null;
499
- if (this.localConfig) {
500
- this.renderState(this.localConfig.defaultState);
877
+ if (this.config) {
878
+ this.renderState(this.config.defaultState);
501
879
  }
502
880
  else {
503
881
  console.warn("No configuration loaded, cannot reset to default state");
504
882
  }
505
883
  URLStateManager.clearURL();
506
884
  }
885
+ setOption(flag, value) {
886
+ switch (flag) {
887
+ case 'showUrl': {
888
+ const nextValue = Boolean(value);
889
+ if (this.showUrlEnabled === nextValue) {
890
+ return;
891
+ }
892
+ this.showUrlEnabled = nextValue;
893
+ if (nextValue) {
894
+ const stateForUrl = this.getTrackedStateSnapshot();
895
+ URLStateManager.updateURL(stateForUrl);
896
+ }
897
+ else {
898
+ URLStateManager.clearURL();
899
+ }
900
+ break;
901
+ }
902
+ default:
903
+ console.warn(`[CustomViews] Unknown option '${flag}' passed to setOption`);
904
+ }
905
+ }
507
906
  // === STATE CHANGE LISTENER METHODS ===
508
907
  /**
509
908
  * Add a listener that will be called whenever the state changes
@@ -533,6 +932,20 @@ class CustomViewsCore {
533
932
  }
534
933
  });
535
934
  }
935
+ cloneState(state) {
936
+ const toggles = state?.toggles ? [...state.toggles] : [];
937
+ const tabs = state?.tabs ? { ...state.tabs } : undefined;
938
+ return tabs ? { toggles, tabs } : { toggles };
939
+ }
940
+ getTrackedStateSnapshot() {
941
+ if (this.lastAppliedState) {
942
+ return this.cloneState(this.lastAppliedState);
943
+ }
944
+ if (this.config) {
945
+ return this.cloneState(this.config.defaultState);
946
+ }
947
+ return { toggles: [] };
948
+ }
536
949
  }
537
950
 
538
951
  class AssetsManager {
@@ -581,6 +994,70 @@ class AssetsManager {
581
994
  }
582
995
  }
583
996
 
997
+ /**
998
+ * Helper function to prepend baseUrl to a path
999
+ * @param path The path to prepend the baseUrl to
1000
+ * @param baseUrl The base URL to prepend
1001
+ * @returns The full URL with baseUrl prepended if applicable
1002
+ */
1003
+ function prependBaseUrl(path, baseUrl) {
1004
+ if (!baseUrl)
1005
+ return path;
1006
+ // Don't prepend if the path is already absolute (starts with http:// or https://)
1007
+ if (path.startsWith('http://') || path.startsWith('https://')) {
1008
+ return path;
1009
+ }
1010
+ // Ensure baseUrl doesn't end with / and path starts with /
1011
+ const cleanbaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
1012
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
1013
+ return cleanbaseUrl + cleanPath;
1014
+ }
1015
+
1016
+ /**
1017
+ * Main CustomViews class for initializing and managing custom views
1018
+ */
1019
+ class CustomViews {
1020
+ /**
1021
+ * Entry Point to use CustomViews
1022
+ * @param opts Initialization options including config object and assets path
1023
+ * @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
1024
+ */
1025
+ static async init(opts) {
1026
+ // Load assets JSON if provided
1027
+ let assetsManager;
1028
+ const baseURL = opts.baseURL || '';
1029
+ if (opts.assetsJsonPath) {
1030
+ const assetsPath = prependBaseUrl(opts.assetsJsonPath, baseURL);
1031
+ const assetsJson = await (await fetch(assetsPath)).json();
1032
+ assetsManager = new AssetsManager(assetsJson, baseURL);
1033
+ }
1034
+ else {
1035
+ assetsManager = new AssetsManager({}, baseURL);
1036
+ }
1037
+ // Use provided config or create a minimal default one
1038
+ let config;
1039
+ if (opts.config) {
1040
+ config = opts.config;
1041
+ }
1042
+ else {
1043
+ console.error("No config provided, using minimal default config");
1044
+ // Create a minimal default config
1045
+ config = { allToggles: [], defaultState: { toggles: [] } };
1046
+ }
1047
+ const coreOptions = {
1048
+ assetsManager,
1049
+ config: config,
1050
+ rootEl: opts.rootEl,
1051
+ };
1052
+ if (opts.showUrl !== undefined) {
1053
+ coreOptions.showUrl = opts.showUrl;
1054
+ }
1055
+ const core = new CustomViewsCore(coreOptions);
1056
+ core.init();
1057
+ return core;
1058
+ }
1059
+ }
1060
+
584
1061
  /**
585
1062
  * Widget styles for CustomViews
586
1063
  * Extracted from widget.ts for better maintainability
@@ -592,8 +1069,10 @@ const WIDGET_STYLES = `
592
1069
  /* Rounded rectangle widget icon styles */
593
1070
  .cv-widget-icon {
594
1071
  position: fixed;
595
- background: white;
596
- color: black;
1072
+ /* Slightly transparent by default so the widget is subtle at the page edge */
1073
+ background: rgba(255, 255, 255, 0.92);
1074
+ color: rgba(0, 0, 0, 0.9);
1075
+ opacity: 0.6;
597
1076
  display: flex;
598
1077
  align-items: center;
599
1078
  justify-content: center;
@@ -607,8 +1086,10 @@ const WIDGET_STYLES = `
607
1086
  }
608
1087
 
609
1088
  .cv-widget-icon:hover {
610
- background: white;
611
- color: black;
1089
+ /* Become fully opaque on hover to improve readability */
1090
+ background: rgba(255, 255, 255, 1);
1091
+ color: rgba(0, 0, 0, 1);
1092
+ opacity: 1;
612
1093
  }
613
1094
 
614
1095
  /* Top-right: rounded end on left, sticks out leftward on hover */
@@ -1015,6 +1496,43 @@ const WIDGET_STYLES = `
1015
1496
  width: auto;
1016
1497
  }
1017
1498
 
1499
+ .cv-tab-groups {
1500
+ margin-top: 20px;
1501
+ }
1502
+
1503
+ .cv-tab-group-control {
1504
+ margin-bottom: 15px;
1505
+ }
1506
+
1507
+ .cv-tab-group-control label {
1508
+ display: block;
1509
+ margin-bottom: 5px;
1510
+ font-weight: 500;
1511
+ font-size: 14px;
1512
+ }
1513
+
1514
+ .cv-tab-group-select {
1515
+ width: 100%;
1516
+ padding: 8px 12px;
1517
+ border: 1px solid #ced4da;
1518
+ border-radius: 4px;
1519
+ font-size: 14px;
1520
+ background-color: white;
1521
+ cursor: pointer;
1522
+ }
1523
+
1524
+ .cv-tab-group-select:focus {
1525
+ outline: none;
1526
+ border-color: #007bff;
1527
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
1528
+ }
1529
+
1530
+ .cv-widget-theme-dark .cv-tab-group-select {
1531
+ background-color: #2d3748;
1532
+ border-color: #4a5568;
1533
+ color: #e2e8f0;
1534
+ }
1535
+
1018
1536
  .cv-custom-state-actions {
1019
1537
  display: flex;
1020
1538
  gap: 10px;
@@ -1203,7 +1721,8 @@ class CustomViewsWidget {
1203
1721
  description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
1204
1722
  showWelcome: options.showWelcome ?? false,
1205
1723
  welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
1206
- 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.'
1724
+ welcomeMessage: options.welcomeMessage || 'This website uses Custom Views to let you personalize your experience. Use the widget on the side (⚙) to show or hide different content sections based on your preferences. Your selections will be saved and can be shared via URL.',
1725
+ showTabGroups: options.showTabGroups ?? true
1207
1726
  };
1208
1727
  // No external state manager to initialize
1209
1728
  }
@@ -1268,8 +1787,8 @@ class CustomViewsWidget {
1268
1787
  */
1269
1788
  openStateModal() {
1270
1789
  // Get toggles from current configuration and open the modal regardless of count
1271
- const localConfig = this.core.getLocalConfig();
1272
- const toggles = localConfig?.allToggles || [];
1790
+ const config = this.core.getConfig();
1791
+ const toggles = config?.allToggles || [];
1273
1792
  this.createCustomStateModal(toggles);
1274
1793
  }
1275
1794
  /**
@@ -1291,6 +1810,28 @@ class CustomViewsWidget {
1291
1810
  </div>
1292
1811
  `).join('')
1293
1812
  : `<p class="cv-no-toggles">No configurable sections available.</p>`;
1813
+ // Get tab groups
1814
+ const tabGroups = this.core.getTabGroups();
1815
+ let tabGroupsHTML = '';
1816
+ if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
1817
+ const tabGroupControls = tabGroups.map(group => {
1818
+ const options = group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('');
1819
+ return `
1820
+ <div class="cv-tab-group-control">
1821
+ <label for="tab-group-${group.id}">${group.label || group.id}</label>
1822
+ <select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
1823
+ ${options}
1824
+ </select>
1825
+ </div>
1826
+ `;
1827
+ }).join('');
1828
+ tabGroupsHTML = `
1829
+ <h4>Tab Groups</h4>
1830
+ <div class="cv-tab-groups">
1831
+ ${tabGroupControls}
1832
+ </div>
1833
+ `;
1834
+ }
1294
1835
  this.modal.innerHTML = `
1295
1836
  <div class="cv-widget-modal cv-custom-state-modal">
1296
1837
  <div class="cv-widget-modal-header">
@@ -1306,6 +1847,8 @@ class CustomViewsWidget {
1306
1847
  ${toggleControls}
1307
1848
  </div>
1308
1849
 
1850
+ ${tabGroupsHTML}
1851
+
1309
1852
  <div class="cv-custom-state-actions">
1310
1853
  ${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
1311
1854
  <button class="cv-custom-state-copy-url">Copy Shareable URL</button>
@@ -1355,6 +1898,17 @@ class CustomViewsWidget {
1355
1898
  this.core.applyState(state);
1356
1899
  });
1357
1900
  });
1901
+ // Listen to tab group selects
1902
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
1903
+ tabGroupSelects.forEach(select => {
1904
+ select.addEventListener('change', () => {
1905
+ const groupId = select.dataset.groupId;
1906
+ const tabId = select.value;
1907
+ if (groupId && tabId) {
1908
+ this.core.setActiveTab(groupId, tabId);
1909
+ }
1910
+ });
1911
+ });
1358
1912
  // Overlay click to close
1359
1913
  this.modal.addEventListener('click', (e) => {
1360
1914
  if (e.target === this.modal) {
@@ -1399,7 +1953,16 @@ class CustomViewsWidget {
1399
1953
  toggles.push(toggle);
1400
1954
  }
1401
1955
  });
1402
- return { toggles };
1956
+ // Collect tab selections
1957
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
1958
+ const tabs = {};
1959
+ tabGroupSelects.forEach(select => {
1960
+ const groupId = select.dataset.groupId;
1961
+ if (groupId) {
1962
+ tabs[groupId] = select.value;
1963
+ }
1964
+ });
1965
+ return Object.keys(tabs).length > 0 ? { toggles, tabs } : { toggles };
1403
1966
  }
1404
1967
  /**
1405
1968
  * Copy shareable URL to clipboard
@@ -1435,6 +1998,15 @@ class CustomViewsWidget {
1435
1998
  }
1436
1999
  }
1437
2000
  });
2001
+ // Load tab group selections
2002
+ const activeTabs = this.core.getCurrentActiveTabs();
2003
+ const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
2004
+ tabGroupSelects.forEach(select => {
2005
+ const groupId = select.dataset.groupId;
2006
+ if (groupId && activeTabs[groupId]) {
2007
+ select.value = activeTabs[groupId];
2008
+ }
2009
+ });
1438
2010
  }
1439
2011
  /**
1440
2012
  * Format toggle name for display
@@ -1528,53 +2100,197 @@ class CustomViewsWidget {
1528
2100
  }
1529
2101
  }
1530
2102
 
1531
- class CustomViews {
1532
- // Entry Point to use CustomViews
1533
- static async initFromJson(opts) {
1534
- // Load assets JSON if provided
1535
- let assetsManager;
1536
- const baseURL = opts.baseURL || '';
1537
- if (opts.assetsJsonPath) {
1538
- const assetsJson = await (await fetch(opts.assetsJsonPath)).json();
1539
- assetsManager = new AssetsManager(assetsJson, baseURL);
1540
- }
1541
- else {
1542
- assetsManager = new AssetsManager({}, baseURL);
1543
- }
1544
- // Load config JSON if provided, else just log error and don't load the custom views
1545
- let localConfig;
1546
- if (opts.config) {
1547
- localConfig = opts.config;
2103
+ /**
2104
+ * Custom Elements for Tab Groups and Tabs
2105
+ */
2106
+ /**
2107
+ * <cv-tab> element - represents a single tab panel
2108
+ */
2109
+ class CVTab extends HTMLElement {
2110
+ connectedCallback() {
2111
+ // Element is managed by TabManager
2112
+ }
2113
+ }
2114
+ /**
2115
+ * <cv-tabgroup> element - represents a group of tabs
2116
+ */
2117
+ class CVTabgroup extends HTMLElement {
2118
+ connectedCallback() {
2119
+ // Element is managed by TabManager
2120
+ // Emit ready event after a brief delay to ensure children are parsed
2121
+ setTimeout(() => {
2122
+ const event = new CustomEvent('cv:tabgroup-ready', {
2123
+ bubbles: true,
2124
+ detail: { groupId: this.getAttribute('id') }
2125
+ });
2126
+ this.dispatchEvent(event);
2127
+ }, 0);
2128
+ }
2129
+ }
2130
+ /**
2131
+ * Register custom elements
2132
+ */
2133
+ function registerCustomElements() {
2134
+ // Only register if not already defined
2135
+ if (!customElements.get('cv-tab')) {
2136
+ customElements.define('cv-tab', CVTab);
2137
+ }
2138
+ if (!customElements.get('cv-tabgroup')) {
2139
+ customElements.define('cv-tabgroup', CVTabgroup);
2140
+ }
2141
+ }
2142
+
2143
+ /**
2144
+ * Initialize CustomViews from script tag attributes and config file
2145
+ * This function handles the automatic initialization of CustomViews when included via script tag
2146
+ *
2147
+ * Data attributes supported:
2148
+ * - data-base-url: Base URL for the site (e.g., "/customviews" for subdirectory deployments)
2149
+ * - data-config-path: Path to config file (default: "/customviews.config.json")
2150
+ *
2151
+ * The function fetches the config file and uses it directly to initialize CustomViews.
2152
+ * Widget visibility is controlled via the config file (widget.enabled property).
2153
+ */
2154
+ function initializeFromScript() {
2155
+ // Only run in browser environment
2156
+ if (typeof window === 'undefined')
2157
+ return;
2158
+ // Use the typed global `window` (see src/types/global.d.ts)
2159
+ // Idempotency guard: if already initialized, skip setting up listener again.
2160
+ if (window.__customViewsInitialized) {
2161
+ // Informational for developers; harmless in production.
2162
+ console.info('[CustomViews] Auto-init skipped: already initialized.');
2163
+ return;
2164
+ }
2165
+ document.addEventListener('DOMContentLoaded', async function () {
2166
+ // Prevent concurrent initialization runs (race conditions when script is loaded twice)
2167
+ if (window.__customViewsInitInProgress || window.__customViewsInitialized) {
2168
+ return;
1548
2169
  }
1549
- else {
1550
- if (!opts.configPath) {
1551
- console.error("No config path provided, skipping custom views");
1552
- return null;
2170
+ window.__customViewsInitInProgress = true;
2171
+ // Register custom elements early
2172
+ registerCustomElements();
2173
+ try {
2174
+ // Find the script tag
2175
+ let scriptTag = document.currentScript;
2176
+ // Fallback if currentScript is not available (executed after page load)
2177
+ if (!scriptTag) {
2178
+ // Try to find the script tag by looking for our script
2179
+ const scripts = document.querySelectorAll('script[src*="custom-views"]');
2180
+ if (scripts.length > 0) {
2181
+ // Find the most specific match (to avoid matching other custom-views scripts)
2182
+ for (let i = 0; i < scripts.length; i++) {
2183
+ const script = scripts[i];
2184
+ const src = script.getAttribute('src') || '';
2185
+ // Look for .min.js or .js at the end
2186
+ if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
2187
+ scriptTag = script;
2188
+ break;
2189
+ }
2190
+ }
2191
+ // If no specific match found, use the first one
2192
+ if (!scriptTag) {
2193
+ scriptTag = scripts[0];
2194
+ }
2195
+ }
2196
+ }
2197
+ // Read data attributes from script tag
2198
+ let baseURL = '';
2199
+ let configPath = '/customviews.config.json';
2200
+ if (scriptTag) {
2201
+ baseURL = scriptTag.getAttribute('data-base-url') || '';
2202
+ configPath = scriptTag.getAttribute('data-config-path') || configPath;
1553
2203
  }
2204
+ // Fetch config file
2205
+ let configFile;
1554
2206
  try {
1555
- localConfig = await (await fetch(opts.configPath)).json();
2207
+ const fullConfigPath = prependBaseUrl(configPath, baseURL);
2208
+ console.log(`[CustomViews] Loading config from: ${fullConfigPath}`);
2209
+ const response = await fetch(fullConfigPath);
2210
+ if (!response.ok) {
2211
+ console.warn(`[CustomViews] Config file not found at ${fullConfigPath}. Using defaults.`);
2212
+ // Provide minimal default config structure
2213
+ configFile = {
2214
+ config: {
2215
+ allToggles: [],
2216
+ defaultState: { toggles: [] }
2217
+ },
2218
+ widget: {
2219
+ enabled: true
2220
+ }
2221
+ };
2222
+ }
2223
+ else {
2224
+ configFile = await response.json();
2225
+ console.log('[CustomViews] Config loaded successfully');
2226
+ }
1556
2227
  }
1557
2228
  catch (error) {
1558
- console.error("Error loading config:", error);
1559
- return null;
2229
+ console.error('[CustomViews] Error loading config file:', error);
2230
+ return; // Abort initialization
2231
+ }
2232
+ // Determine effective baseURL (data attribute takes precedence over config)
2233
+ const effectiveBaseURL = baseURL || configFile.baseURL || '';
2234
+ const options = {
2235
+ config: configFile.config,
2236
+ assetsJsonPath: configFile.assetsJsonPath,
2237
+ baseURL: effectiveBaseURL,
2238
+ };
2239
+ if (configFile.showUrl !== undefined) {
2240
+ options.showUrl = configFile.showUrl;
1560
2241
  }
2242
+ // Initialize CustomViews core
2243
+ const core = await CustomViews.init(options);
2244
+ if (!core) {
2245
+ console.error('[CustomViews] Failed to initialize core.');
2246
+ return; // Abort widget creation
2247
+ }
2248
+ // Store instance
2249
+ window.customViewsInstance = { core };
2250
+ // Initialize widget if enabled in config
2251
+ let widget;
2252
+ if (configFile.widget?.enabled !== false) {
2253
+ widget = new CustomViewsWidget({
2254
+ core,
2255
+ ...configFile.widget
2256
+ });
2257
+ widget.render();
2258
+ // Store widget instance
2259
+ window.customViewsInstance.widget = widget;
2260
+ console.log('[CustomViews] Widget initialized and rendered');
2261
+ }
2262
+ else {
2263
+ console.log('[CustomViews] Widget disabled in config - skipping initialization');
2264
+ }
2265
+ // Dispatch ready event
2266
+ const readyEvent = new CustomEvent('customviews:ready', {
2267
+ detail: {
2268
+ core,
2269
+ widget
2270
+ }
2271
+ });
2272
+ document.dispatchEvent(readyEvent);
2273
+ // Mark initialized and clear in-progress flag
2274
+ window.__customViewsInitialized = true;
2275
+ window.__customViewsInitInProgress = false;
1561
2276
  }
1562
- const coreOptions = {
1563
- assetsManager,
1564
- config: localConfig,
1565
- rootEl: opts.rootEl,
1566
- };
1567
- const core = new CustomViewsCore(coreOptions);
1568
- core.init();
1569
- return core;
1570
- }
2277
+ catch (error) {
2278
+ // Clear in-progress flag so a future attempt can retry
2279
+ window.__customViewsInitInProgress = false;
2280
+ console.error('[CustomViews] Auto-initialization error:', error);
2281
+ }
2282
+ });
1571
2283
  }
2284
+
2285
+ // Import from new modules
2286
+ // Set up globals and auto-initialization
1572
2287
  if (typeof window !== "undefined") {
1573
- // @ts-ignore
2288
+ // Expose to window to enable usage (e.g. const app = new window.CustomViews(...))
1574
2289
  window.CustomViews = CustomViews;
1575
- // @ts-ignore
1576
2290
  window.CustomViewsWidget = CustomViewsWidget;
2291
+ // Run auto-initialization
2292
+ initializeFromScript();
1577
2293
  }
1578
2294
 
1579
- export { AssetsManager, CustomViews, CustomViewsCore, CustomViewsWidget, Config as LocalConfig, PersistenceManager, URLStateManager };
2295
+ export { AssetsManager, CustomViews, CustomViewsCore, CustomViewsWidget, PersistenceManager, URLStateManager };
1580
2296
  //# sourceMappingURL=custom-views.esm.js.map