@customviews-js/customviews 1.4.1-beta.0 → 1.4.1-beta.1

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.4.1-beta.0
2
+ * @customviews-js/customviews v1.4.1-beta.1
3
3
  * (c) 2025 Chan Ger Teck
4
4
  * Released under the MIT License.
5
5
  */
@@ -704,7 +704,7 @@ class TabManager {
704
704
  });
705
705
  }
706
706
  // Add tooltip for UX feedback (use native title attribute)
707
- navLink.setAttribute('title', 'Double click to change switch tabs across all groups');
707
+ navLink.setAttribute('title', "Double-click a tab to 'pin' it in all similar tab groups.");
708
708
  listItem.appendChild(navLink);
709
709
  navContainer.appendChild(listItem);
710
710
  });
@@ -2732,6 +2732,12 @@ class CustomViewsCore {
2732
2732
  this.componentRegistry.tabGroups.delete(tabGroup);
2733
2733
  });
2734
2734
  }
2735
+ /**
2736
+ * Check if there are any active components in the registry
2737
+ */
2738
+ hasActiveComponents() {
2739
+ return this.componentRegistry.toggles.size > 0 || this.componentRegistry.tabGroups.size > 0;
2740
+ }
2735
2741
  getConfig() {
2736
2742
  return this.config;
2737
2743
  }
@@ -4190,97 +4196,7 @@ const WIDGET_STYLES = `
4190
4196
  }
4191
4197
 
4192
4198
  /* Dark theme custom state styles */
4193
- /* Welcome modal styles */
4194
- .cv-welcome-modal {
4195
- max-width: 32rem;
4196
- width: 90vw;
4197
- background: white;
4198
- border-radius: 0.75rem;
4199
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
4200
- animation: slideIn 0.2s ease;
4201
- display: flex;
4202
- flex-direction: column;
4203
- }
4204
-
4205
- .cv-modal-main {
4206
- padding: 1rem;
4207
- flex: 1;
4208
- display: flex;
4209
- flex-direction: column;
4210
- gap: 1rem;
4211
- overflow-y: auto;
4212
- max-height: calc(80vh - 8rem);
4213
- }
4214
-
4215
- .cv-welcome-message {
4216
- font-size: 0.875rem;
4217
- color: rgba(0, 0, 0, 0.8);
4218
- margin: 0;
4219
- line-height: 1.4;
4220
- text-align: center;
4221
- }
4222
-
4223
- .cv-welcome-message a {
4224
- color: #3e84f4;
4225
- text-align: justify;
4226
- text-decoration: none;
4227
- }
4228
-
4229
- .cv-welcome-message a:hover {
4230
- text-decoration: underline;
4231
- }
4232
-
4233
- .cv-welcome-widget-preview {
4234
- display: flex;
4235
- align-items: center;
4236
- justify-content: center;
4237
- gap: 1rem;
4238
- padding: 1rem;
4239
- background: #f8f9fa;
4240
- border-radius: 0.5rem;
4241
- margin: 1rem 0;
4242
- }
4243
4199
 
4244
- .cv-welcome-widget-icon {
4245
- width: 2rem;
4246
- height: 2rem;
4247
- background: rgba(62, 132, 244, 0.1);
4248
- border-radius: 9999px;
4249
- display: flex;
4250
- align-items: center;
4251
- justify-content: center;
4252
- animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
4253
- color: #3e84f4;
4254
- }
4255
-
4256
- .cv-welcome-widget-label {
4257
- font-size: 0.875rem;
4258
- font-weight: 500;
4259
- color: rgba(0, 0, 0, 0.8);
4260
- margin: 0;
4261
- }
4262
-
4263
- .cv-welcome-got-it {
4264
- width: 100%;
4265
- background: #3e84f4;
4266
- color: white;
4267
- font-weight: 600;
4268
- padding: 0.75rem 1rem;
4269
- border-radius: 0.5rem;
4270
- border: none;
4271
- cursor: pointer;
4272
- font-size: 0.875rem;
4273
- transition: background-color 0.2s ease;
4274
- outline: none;
4275
- }
4276
-
4277
- .cv-welcome-got-it:hover {
4278
- background: rgba(62, 132, 244, 0.9);
4279
- }
4280
-
4281
- .cv-welcome-got-it:focus {
4282
- box-shadow: 0 0 0 2px rgba(62, 132, 244, 0.5);
4283
- }
4284
4200
 
4285
4201
  /* Animations */
4286
4202
  @keyframes cv-pulse {
@@ -4292,26 +4208,7 @@ const WIDGET_STYLES = `
4292
4208
  }
4293
4209
  }
4294
4210
 
4295
- /* Dark theme welcome modal styles */
4296
- .cv-widget-theme-dark .cv-welcome-modal {
4297
- background: #101722;
4298
- }
4299
4211
 
4300
- .cv-widget-theme-dark .cv-welcome-message {
4301
- color: rgba(255, 255, 255, 0.8);
4302
- }
4303
-
4304
- .cv-widget-theme-dark .cv-welcome-message a {
4305
- color: #60a5fa;
4306
- }
4307
-
4308
- .cv-widget-theme-dark .cv-welcome-widget-preview {
4309
- background: rgba(255, 255, 255, 0.1);
4310
- }
4311
-
4312
- .cv-widget-theme-dark .cv-welcome-widget-label {
4313
- color: #e2e8f0;
4314
- }
4315
4212
 
4316
4213
  /* Dark theme logo box */
4317
4214
  .cv-widget-theme-dark .cv-tabgroup-logo-box {
@@ -4486,6 +4383,176 @@ const WIDGET_STYLES = `
4486
4383
  .cv-widget-theme-dark .cv-share-action-btn.primary:hover {
4487
4384
  background: #2b74e6;
4488
4385
  }
4386
+
4387
+ /* Intro Callout styles */
4388
+ .cv-widget-callout {
4389
+ position: fixed;
4390
+ background: white;
4391
+ border-radius: 8px;
4392
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
4393
+ padding: 12px 16px;
4394
+ width: 260px;
4395
+ z-index: 9999;
4396
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4397
+ animation: cvFadeIn 0.3s ease-out;
4398
+ pointer-events: auto;
4399
+ display: flex;
4400
+ flex-direction: column;
4401
+ gap: 8px;
4402
+ }
4403
+
4404
+ .cv-widget-callout-text {
4405
+ font-size: 0.9rem;
4406
+ color: #333;
4407
+ margin: 0;
4408
+ line-height: 1.4;
4409
+ }
4410
+
4411
+ .cv-widget-callout-close {
4412
+ position: absolute;
4413
+ top: 6px;
4414
+ right: 6px;
4415
+ width: 18px;
4416
+ height: 18px;
4417
+ border: none;
4418
+ background: rgba(0,0,0,0.05);
4419
+ color: #666;
4420
+ cursor: pointer;
4421
+ display: flex;
4422
+ align-items: center;
4423
+ justify-content: center;
4424
+ border-radius: 50%;
4425
+ font-size: 14px;
4426
+ line-height: 1;
4427
+ padding: 0;
4428
+ transition: all 0.2s ease;
4429
+ }
4430
+
4431
+ .cv-widget-callout-close:hover {
4432
+ background: #f0f0f0;
4433
+ color: #333;
4434
+ }
4435
+
4436
+ /* Callout positioning and arrow */
4437
+ .cv-widget-callout::after {
4438
+ content: '';
4439
+ position: absolute;
4440
+ width: 10px;
4441
+ height: 10px;
4442
+ background: white;
4443
+ transform: rotate(45deg);
4444
+ box-shadow: 1px 1px 1px rgba(0,0,0,0.05); /* subtle shadow for arrow */
4445
+ }
4446
+
4447
+ /* Top-Right Widget -> Callout to the left */
4448
+ .cv-widget-callout.cv-pos-top-right {
4449
+ top: 20px;
4450
+ right: 64px;
4451
+ }
4452
+ .cv-widget-callout.cv-pos-top-right::after {
4453
+ top: 13px;
4454
+ right: -5px;
4455
+ box-shadow: 1px -1px 1px rgba(0,0,0,0.05);
4456
+ transform: rotate(45deg);
4457
+ }
4458
+
4459
+ /* Bottom-Right Widget -> Callout to the left */
4460
+ .cv-widget-callout.cv-pos-bottom-right {
4461
+ bottom: 20px;
4462
+ right: 64px;
4463
+ }
4464
+ .cv-widget-callout.cv-pos-bottom-right::after {
4465
+ bottom: 13px;
4466
+ right: -5px;
4467
+ }
4468
+
4469
+ /* Top-Left Widget -> Callout to the right */
4470
+ .cv-widget-callout.cv-pos-top-left {
4471
+ top: 20px;
4472
+ left: 64px;
4473
+ }
4474
+ .cv-widget-callout.cv-pos-top-left::after {
4475
+ top: 13px;
4476
+ left: -5px;
4477
+ }
4478
+
4479
+ /* Bottom-Left Widget -> Callout to the right */
4480
+ .cv-widget-callout.cv-pos-bottom-left {
4481
+ bottom: 20px;
4482
+ left: 64px;
4483
+ }
4484
+ .cv-widget-callout.cv-pos-bottom-left::after {
4485
+ bottom: 13px;
4486
+ left: -5px;
4487
+ }
4488
+
4489
+ /* Middle-Right Widget -> Callout to the left */
4490
+ .cv-widget-callout.cv-pos-middle-right {
4491
+ top: 50%;
4492
+ right: 64px;
4493
+ transform: translateY(-50%);
4494
+ }
4495
+ .cv-widget-callout.cv-pos-middle-right::after {
4496
+ top: 50%;
4497
+ right: -5px;
4498
+ transform: translateY(-50%) rotate(45deg);
4499
+ }
4500
+
4501
+ /* Middle-Left Widget -> Callout to the right */
4502
+ .cv-widget-callout.cv-pos-middle-left {
4503
+ top: 50%;
4504
+ left: 64px;
4505
+ transform: translateY(-50%);
4506
+ }
4507
+ .cv-widget-callout.cv-pos-middle-left::after {
4508
+ top: 50%;
4509
+ left: -5px;
4510
+ transform: translateY(-50%) rotate(45deg);
4511
+ }
4512
+
4513
+ /* Pulse animation utility */
4514
+ .cv-widget-icon.cv-pulse {
4515
+ animation: cv-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
4516
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
4517
+ }
4518
+
4519
+ @keyframes cv-pulse {
4520
+ 0% {
4521
+ transform: scale(1);
4522
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0.7);
4523
+ }
4524
+ 70% {
4525
+ transform: scale(1.05);
4526
+ box-shadow: 0 0 0 10px rgba(62, 132, 244, 0);
4527
+ }
4528
+ 100% {
4529
+ transform: scale(1);
4530
+ box-shadow: 0 0 0 0 rgba(62, 132, 244, 0);
4531
+ }
4532
+ }
4533
+
4534
+ /* Dark Theme */
4535
+ .cv-widget-theme-dark .cv-widget-callout {
4536
+ background: #1f2937; /* Tailwind gray-800 mostly */
4537
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
4538
+ border: 1px solid rgba(255,255,255,0.1);
4539
+ }
4540
+ .cv-widget-theme-dark .cv-widget-callout::after {
4541
+ background: #1f2937;
4542
+ border-top: 1px solid rgba(255,255,255,0.1);
4543
+ border-right: 1px solid rgba(255,255,255,0.1);
4544
+ }
4545
+ .cv-widget-theme-dark .cv-widget-callout-text {
4546
+ color: #e5e7eb;
4547
+ }
4548
+ .cv-widget-theme-dark .cv-widget-callout-close {
4549
+ background: rgba(255,255,255,0.1);
4550
+ color: #9ca3af;
4551
+ }
4552
+ .cv-widget-theme-dark .cv-widget-callout-close:hover {
4553
+ background: rgba(255,255,255,0.2);
4554
+ color: #fff;
4555
+ }
4489
4556
  `;
4490
4557
  /**
4491
4558
  * Inject widget styles into the document head
@@ -4504,6 +4571,7 @@ class CustomViewsWidget {
4504
4571
  core;
4505
4572
  container;
4506
4573
  widgetIcon = null;
4574
+ introCallout = null;
4507
4575
  options;
4508
4576
  _hasVisibleConfig = false;
4509
4577
  pageToggleIds = new Set();
@@ -4524,8 +4592,7 @@ class CustomViewsWidget {
4524
4592
  title: options.title || 'Customize View',
4525
4593
  description: options.description || '',
4526
4594
  showWelcome: options.showWelcome ?? false,
4527
- welcomeTitle: options.welcomeTitle || 'Site Customization',
4528
- welcomeMessage: options.welcomeMessage || 'This site is powered by Custom Views. Use the widget on the side (⚙) to customize your experience. Your preferences will be saved and can be shared via URL.<br><br>Learn more at <a href="https://github.com/customviews-js/customviews" target="_blank">customviews GitHub</a>.',
4595
+ welcomeMessage: options.welcomeMessage || 'Customize your reading experience (theme, toggles, tabs) here.',
4529
4596
  showTabGroups: options.showTabGroups ?? true
4530
4597
  };
4531
4598
  // Determine if there are any configurations to show
@@ -4567,9 +4634,9 @@ class CustomViewsWidget {
4567
4634
  this.attachEventListeners();
4568
4635
  // Always append to body since it's a floating icon
4569
4636
  document.body.appendChild(this.widgetIcon);
4570
- // Show welcome modal on first visit if enabled
4637
+ // Show intro callout on first visit if enabled
4571
4638
  if (this.options.showWelcome) {
4572
- this.showWelcomeModalIfFirstVisit();
4639
+ this.showIntroCalloutIfFirstVisit();
4573
4640
  }
4574
4641
  return this.widgetIcon;
4575
4642
  }
@@ -4599,6 +4666,11 @@ class CustomViewsWidget {
4599
4666
  this.stateModal.remove();
4600
4667
  this.stateModal = null;
4601
4668
  }
4669
+ // Clean up callout
4670
+ if (this.introCallout) {
4671
+ this.introCallout.remove();
4672
+ this.introCallout = null;
4673
+ }
4602
4674
  }
4603
4675
  attachEventListeners() {
4604
4676
  if (!this.widgetIcon)
@@ -4614,10 +4686,48 @@ class CustomViewsWidget {
4614
4686
  this.stateModal.classList.add('cv-hidden');
4615
4687
  }
4616
4688
  }
4689
+ /**
4690
+ * Dismiss the intro callout
4691
+ */
4692
+ dismissIntroCallout() {
4693
+ if (!this.introCallout)
4694
+ return;
4695
+ const callout = this.introCallout;
4696
+ // Clear reference immediately from class to prevent re-use
4697
+ this.introCallout = null;
4698
+ callout.remove();
4699
+ // Stop pulsing the widget icon
4700
+ if (this.widgetIcon) {
4701
+ this.widgetIcon.classList.remove('cv-pulse');
4702
+ }
4703
+ // Mark as shown in localStorage
4704
+ try {
4705
+ localStorage.setItem('cv-intro-shown', 'true');
4706
+ }
4707
+ catch (e) {
4708
+ // Ignore localStorage errors
4709
+ }
4710
+ }
4617
4711
  /**
4618
4712
  * Open the custom state creator
4619
4713
  */
4620
4714
  openStateModal() {
4715
+ // Dismiss intro callout if valid
4716
+ if (this.introCallout) {
4717
+ this.dismissIntroCallout();
4718
+ }
4719
+ else {
4720
+ // Even if no callout is shown (e.g. page had no content), opening the widget
4721
+ // should count as "seen", preventing future callouts.
4722
+ try {
4723
+ if (!localStorage.getItem('cv-intro-shown')) {
4724
+ localStorage.setItem('cv-intro-shown', 'true');
4725
+ }
4726
+ }
4727
+ catch (e) {
4728
+ // Ignore localStorage errors
4729
+ }
4730
+ }
4621
4731
  if (!this.stateModal) {
4622
4732
  this._createStateModal();
4623
4733
  }
@@ -5075,88 +5185,70 @@ class CustomViewsWidget {
5075
5185
  }
5076
5186
  }
5077
5187
  /**
5078
- * Check if this is the first visit and show welcome modal
5188
+ * Check if this is the first visit and show intro callout
5079
5189
  */
5080
- showWelcomeModalIfFirstVisit() {
5190
+ showIntroCalloutIfFirstVisit() {
5081
5191
  if (!this._hasVisibleConfig)
5082
5192
  return;
5083
- const STORAGE_KEY = 'cv-welcome-shown';
5084
- // Check if welcome has been shown before
5085
- const hasSeenWelcome = localStorage.getItem(STORAGE_KEY);
5086
- if (!hasSeenWelcome) {
5087
- // Show welcome modal after a short delay to let the page settle
5193
+ // Strict check: Only show callout if there is actual content on the page to customize.
5194
+ // We check the core registry for any active toggles or tab groups.
5195
+ if (!this.core.hasActiveComponents()) {
5196
+ return;
5197
+ }
5198
+ const STORAGE_KEY = 'cv-intro-shown';
5199
+ // Check if intro has been shown before
5200
+ let hasSeenIntro = null;
5201
+ try {
5202
+ hasSeenIntro = localStorage.getItem(STORAGE_KEY);
5203
+ }
5204
+ catch (e) {
5205
+ // Ignore localStorage errors (e.g. private mode)
5206
+ }
5207
+ if (!hasSeenIntro) {
5208
+ // Show callout after a short delay
5088
5209
  setTimeout(() => {
5089
- this.createWelcomeModal();
5090
- }, 500);
5091
- // Mark as shown
5092
- localStorage.setItem(STORAGE_KEY, 'true');
5210
+ this.createCallout();
5211
+ }, 1000);
5093
5212
  }
5094
5213
  }
5095
5214
  /**
5096
- * Create and show the welcome modal
5215
+ * Create and show the intro callout
5097
5216
  */
5098
- createWelcomeModal() {
5099
- // Don't show if there's already a modal open
5100
- if (this.stateModal && !this.stateModal.classList.contains('cv-hidden'))
5217
+ createCallout() {
5218
+ // Avoid duplicates
5219
+ if (this.introCallout || document.querySelector('.cv-widget-callout'))
5101
5220
  return;
5102
- const welcomeModal = document.createElement('div');
5103
- welcomeModal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
5221
+ this.introCallout = document.createElement('div');
5222
+ const callout = this.introCallout;
5223
+ callout.className = `cv-widget-callout cv-pos-${this.options.position}`;
5104
5224
  if (this.options.theme === 'dark') {
5105
- welcomeModal.classList.add('cv-widget-theme-dark');
5225
+ callout.classList.add('cv-widget-theme-dark');
5226
+ }
5227
+ // Close button
5228
+ const closeBtn = document.createElement('button');
5229
+ closeBtn.className = 'cv-widget-callout-close';
5230
+ closeBtn.innerHTML = '×';
5231
+ closeBtn.setAttribute('aria-label', 'Dismiss intro');
5232
+ closeBtn.addEventListener('click', (e) => {
5233
+ e.stopPropagation();
5234
+ this.dismissIntroCallout();
5235
+ });
5236
+ // Message
5237
+ const msg = document.createElement('p');
5238
+ msg.className = 'cv-widget-callout-text';
5239
+ msg.textContent = this.options.welcomeMessage;
5240
+ callout.appendChild(closeBtn);
5241
+ callout.appendChild(msg);
5242
+ document.body.appendChild(callout);
5243
+ // Add pulse to widget icon to draw attention
5244
+ if (this.widgetIcon) {
5245
+ this.widgetIcon.classList.add('cv-pulse');
5106
5246
  }
5107
- welcomeModal.innerHTML = `
5108
- <div class="cv-widget-modal cv-welcome-modal">
5109
- <header class="cv-modal-header">
5110
- <div class="cv-modal-header-content">
5111
- <div class="cv-modal-icon">
5112
- ${getGearIcon()}
5113
- </div>
5114
- <h1 class="cv-modal-title">${this.options.welcomeTitle}</h1>
5115
- </div>
5116
- </header>
5117
- <div class="cv-modal-main">
5118
- <p class="cv-welcome-message">${this.options.welcomeMessage}</p>
5119
-
5120
- <div class="cv-welcome-widget-preview">
5121
- <div class="cv-welcome-widget-icon">
5122
- ${getGearIcon()}
5123
- </div>
5124
- <p class="cv-welcome-widget-label">Look for this widget</p>
5125
- </div>
5126
-
5127
- <button class="cv-welcome-got-it">Got it!</button>
5128
- </div>
5129
- </div>
5130
- `;
5131
- document.body.appendChild(welcomeModal);
5132
- this.attachWelcomeModalEventListeners(welcomeModal);
5133
- }
5134
- /**
5135
- * Attach event listeners for welcome modal
5136
- */
5137
- attachWelcomeModalEventListeners(welcomeModal) {
5138
- const closeModal = () => {
5139
- welcomeModal.remove();
5140
- document.removeEventListener('keydown', handleEscape);
5141
- };
5142
- // Got it button
5143
- const gotItBtn = welcomeModal.querySelector('.cv-welcome-got-it');
5144
- if (gotItBtn) {
5145
- gotItBtn.addEventListener('click', closeModal);
5146
- }
5147
- // Overlay click to close
5148
- welcomeModal.addEventListener('click', (e) => {
5149
- if (e.target === welcomeModal) {
5150
- closeModal();
5151
- }
5247
+ // Auto-dismiss and open widget on click anywhere on callout
5248
+ callout.addEventListener('click', () => {
5249
+ this.dismissIntroCallout();
5250
+ this.openStateModal();
5152
5251
  });
5153
- // Escape key to close
5154
- const handleEscape = (e) => {
5155
- if (e.key === 'Escape') {
5156
- closeModal();
5157
- }
5158
- };
5159
- document.addEventListener('keydown', handleEscape);
5160
5252
  }
5161
5253
  }
5162
5254