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