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