ultimate-jekyll-manager 1.6.4 → 1.6.6

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.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ ---
18
+ ## [1.6.5] - 2026-06-04
19
+
20
+ ### Added
21
+
22
+ - **Dynamic event fitting in month view.** Calendar cells now show as many events as physically fit instead of a hardcoded max of 3; "+N more" only appears when events genuinely overflow.
23
+ - **Local time display on calendar events.** Event pills show local time (data layer remains UTC). Editor modal shows a local date+time badge below the UTC inputs.
24
+ - **Hover states on event pills.** Brightness + box-shadow effect on hover for month, week, and day views.
25
+ - **Now line on month view.** Red progress line shows current time of day in today's cell, with dot rendered above cell borders.
26
+ - **Left tab accent on all event pills.** Consistent dark left border matches week/day view style.
27
+ - **Monthly-weekday (Nth weekday) recurrence pattern.** Calendar supports "2nd Wednesday of every month" style recurring campaigns with calendar-relative date stepping.
28
+
29
+ ### Changed
30
+
31
+ - **Per-string translation caching replaces all-or-nothing page caching.** Each page's cache is now a `hash→translation` map (`es/pages/index.html.json`). Only strings whose content hash changed are sent to the API — unchanged strings are served from cache. Dramatically reduces API calls and cost on incremental builds.
32
+ - **Prompt hash mismatch now wipes page cache files** (not just meta entries) for a clean slate.
33
+ - **Translation stats now track cached vs new strings** instead of whole-page hit/miss.
34
+ - **Outside-month calendar cells** now fade only content (date number + events), keeping borders at full opacity for consistent grid lines.
35
+ - **Imagemin constants renamed** (`MAX_SOURCE_DIMENSION` → `IMAGE_MAX_DIMENSION`) and default max dimension lowered from 4096 to 2048.
36
+
37
+ ### Removed
38
+
39
+ - **`RECHECK_DAYS`** — per-string content hashes make age-based invalidation unnecessary.
40
+ - **All-day row** removed from week view (no all-day event concept in marketing calendar).
41
+
17
42
  ---
18
43
  ## [1.6.4] - 2026-06-03
19
44
 
@@ -79,7 +79,7 @@ $campaign-push-color: #4CAF50;
79
79
  .calendar-month {
80
80
  display: grid;
81
81
  grid-template-columns: repeat(7, 1fr);
82
- grid-template-rows: repeat(auto-fill, 1fr);
82
+ grid-auto-rows: 1fr;
83
83
  flex-grow: 1;
84
84
  min-height: 0;
85
85
  }
@@ -88,12 +88,13 @@ $campaign-push-color: #4CAF50;
88
88
  border-right: 1px solid var(--bs-border-color);
89
89
  border-bottom: 1px solid var(--bs-border-color);
90
90
  padding: 0.25rem;
91
- overflow-y: auto;
92
- overflow-x: hidden;
93
- min-height: 80px;
91
+ overflow: hidden;
92
+ min-height: 0;
94
93
  position: relative;
95
94
  cursor: pointer;
96
95
  transition: background-color 0.15s;
96
+ display: flex;
97
+ flex-direction: column;
97
98
 
98
99
  &:nth-child(7n) {
99
100
  border-right: none;
@@ -120,7 +121,10 @@ $campaign-push-color: #4CAF50;
120
121
  }
121
122
 
122
123
  .calendar-cell--outside {
123
- opacity: 0.35;
124
+ .calendar-cell-date,
125
+ .calendar-cell-events {
126
+ opacity: 0.35;
127
+ }
124
128
  }
125
129
 
126
130
  .calendar-cell--drag-over {
@@ -142,6 +146,9 @@ $campaign-push-color: #4CAF50;
142
146
  display: flex;
143
147
  flex-direction: column;
144
148
  gap: 1px;
149
+ flex: 1;
150
+ min-height: 0;
151
+ overflow: hidden;
145
152
  }
146
153
 
147
154
  .calendar-cell-more {
@@ -253,7 +260,7 @@ $campaign-push-color: #4CAF50;
253
260
  right: 0;
254
261
  height: 2px;
255
262
  background-color: $calendar-now-color;
256
- z-index: 2;
263
+ z-index: 3;
257
264
  pointer-events: none;
258
265
 
259
266
  &::before {
@@ -268,6 +275,7 @@ $campaign-push-color: #4CAF50;
268
275
  }
269
276
  }
270
277
 
278
+
271
279
  .calendar-week-event {
272
280
  position: absolute;
273
281
  left: 2px;
@@ -280,9 +288,11 @@ $campaign-push-color: #4CAF50;
280
288
  z-index: 1;
281
289
  color: #fff;
282
290
  border-left: 3px solid rgba(0, 0, 0, 0.2);
291
+ transition: filter 0.15s, box-shadow 0.15s;
283
292
 
284
293
  &:hover {
285
- opacity: 0.85;
294
+ filter: brightness(1.2);
295
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
286
296
  }
287
297
  }
288
298
 
@@ -402,15 +412,17 @@ $campaign-push-color: #4CAF50;
402
412
  height: 1.25rem;
403
413
  box-sizing: border-box;
404
414
  border: 1px solid transparent;
415
+ border-left: 3px solid rgba(0, 0, 0, 0.2);
405
416
  white-space: nowrap;
406
417
  overflow: hidden;
407
418
  text-overflow: ellipsis;
408
419
  cursor: pointer;
409
420
  color: #fff;
410
- transition: opacity 0.15s;
421
+ transition: filter 0.15s, box-shadow 0.15s;
411
422
 
412
423
  &:hover {
413
- opacity: 0.85;
424
+ filter: brightness(1.2);
425
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
414
426
  }
415
427
  }
416
428
 
@@ -466,7 +478,9 @@ $campaign-push-color: #4CAF50;
466
478
  // Virtual recurring occurrences: dashed border, slightly transparent
467
479
  .calendar-event--virtual {
468
480
  border-style: dashed;
481
+ border-left-style: solid;
469
482
  border-color: rgba(255, 255, 255, 0.5);
483
+ border-left-color: rgba(0, 0, 0, 0.2);
470
484
  opacity: 0.7;
471
485
  }
472
486
 
@@ -19,6 +19,7 @@ const TOGGLE_ID = 'marketing-emails';
19
19
  const GRANT_DATE_ID = 'marketing-emails-grant-date';
20
20
 
21
21
  let formManager = null;
22
+ let pushFormManager = null;
22
23
 
23
24
  export function init() {
24
25
  const $form = document.getElementById(FORM_ID);
@@ -90,4 +91,91 @@ export function loadData(account) {
90
91
  }
91
92
 
92
93
  formManager.ready();
94
+
95
+ initPushNotifications();
96
+ }
97
+
98
+ function updatePushUI() {
99
+ const $status = document.getElementById('push-notification-status');
100
+ const $tokenInput = document.getElementById('push-token-value');
101
+
102
+ if (!$status) {
103
+ return;
104
+ }
105
+
106
+ const notifications = webManager.notifications();
107
+ const stored = webManager.storage().get('notifications', {});
108
+ const permission = typeof Notification !== 'undefined' ? Notification.permission : 'default';
109
+
110
+ let state;
111
+ if (stored.subscribed && stored.token) {
112
+ state = 'subscribed';
113
+ $status.innerHTML = '<span class="badge bg-success">Subscribed</span>';
114
+ if ($tokenInput) { $tokenInput.value = stored.token; }
115
+ if (pushFormManager) { pushFormManager._setDisabled(true); }
116
+ } else if (!notifications.isSupported()) {
117
+ state = 'not-supported';
118
+ $status.innerHTML = '<span class="badge bg-warning">Not supported</span>';
119
+ if (pushFormManager) { pushFormManager._setDisabled(true); }
120
+ } else if (permission === 'denied') {
121
+ state = 'denied';
122
+ $status.innerHTML = '<span class="badge bg-danger">Denied</span>';
123
+ if (pushFormManager) { pushFormManager._setDisabled(true); }
124
+ } else {
125
+ state = 'not-subscribed';
126
+ $status.innerHTML = '<span class="badge bg-secondary">Not subscribed</span>';
127
+ if ($tokenInput) { $tokenInput.value = ''; }
128
+ if (pushFormManager) { pushFormManager.ready(); }
129
+ }
130
+
131
+ console.log('[Account:push] updatePushUI →', state, { storedSubscribed: stored.subscribed, storedToken: stored.token?.slice(-8), permission });
132
+ }
133
+
134
+ async function initPushNotifications() {
135
+ const $status = document.getElementById('push-notification-status');
136
+ const $form = document.getElementById('push-subscribe-form');
137
+ const $copyBtn = document.getElementById('copy-push-token-btn');
138
+
139
+ if (!$status) {
140
+ return;
141
+ }
142
+
143
+ const notifications = webManager.notifications();
144
+
145
+ // Full sync: validates permission + token + Firestore, then updates localStorage
146
+ await notifications.syncSubscription();
147
+
148
+ // Create the form (always starts disabled), then let updatePushUI control its state
149
+ if (notifications.isSupported() && $form) {
150
+ $form.style.display = '';
151
+
152
+ pushFormManager = new FormManager('#push-subscribe-form', {
153
+ autoReady: false,
154
+ allowResubmit: true,
155
+ });
156
+
157
+ pushFormManager.on('submit', async () => {
158
+ console.log('[Account:push] Subscribe button clicked');
159
+ await notifications.subscribe();
160
+ console.log('[Account:push] Subscribe complete — updating UI');
161
+ setTimeout(() => updatePushUI(), 0);
162
+ });
163
+ }
164
+
165
+ // Now that the form exists, let updatePushUI set the correct state
166
+ updatePushUI();
167
+
168
+ if ($copyBtn) {
169
+ const $tokenInput = document.getElementById('push-token-value');
170
+ const originalHtml = $copyBtn.innerHTML;
171
+ $copyBtn.addEventListener('click', () => {
172
+ if (!$tokenInput?.value) {
173
+ return;
174
+ }
175
+ navigator.clipboard.writeText($tokenInput.value).then(() => {
176
+ $copyBtn.textContent = 'Copied!';
177
+ setTimeout(() => { $copyBtn.innerHTML = originalHtml; }, 2000);
178
+ });
179
+ });
180
+ }
93
181
  }
@@ -358,15 +358,23 @@ export default class CalendarCore {
358
358
 
359
359
  /**
360
360
  * Generate virtual occurrences using the template's sendAt as seed.
361
- * Pure unix math no Date objects, no timezone issues.
361
+ * Fixed-interval patterns (daily, weekly) use pure unix math.
362
+ * Calendar-relative patterns (monthly, monthly-weekday, quarterly, yearly)
363
+ * step through actual dates so they land correctly every month.
362
364
  */
363
365
  _generateOccurrences(template, startUNIX, endUNIX) {
364
- const interval = this._getIntervalSeconds(template.recurrence.pattern);
366
+ const { pattern } = template.recurrence;
365
367
  const occurrences = [];
366
368
  const seedUNIX = template.sendAt;
367
369
 
368
- // Walk backward from seed to find first occurrence >= startUNIX
370
+ if (pattern === 'monthly-weekday') {
371
+ return this._generateNthWeekdayOccurrences(template, startUNIX, endUNIX);
372
+ }
373
+
374
+ // Fixed-interval patterns
375
+ const interval = this._getIntervalSeconds(pattern);
369
376
  let cursorUNIX = seedUNIX;
377
+
370
378
  while (cursorUNIX > startUNIX + interval) {
371
379
  cursorUNIX -= interval;
372
380
  }
@@ -376,23 +384,81 @@ export default class CalendarCore {
376
384
 
377
385
  let maxIterations = 400;
378
386
  while (cursorUNIX <= endUNIX && maxIterations-- > 0) {
379
- occurrences.push({
380
- id: `${template.id}__virtual__${cursorUNIX}`,
381
- sendAt: cursorUNIX,
382
- status: 'pending',
383
- type: template.type,
384
- settings: template.settings,
385
- recurrence: template.recurrence,
386
- _virtual: true,
387
- _recurringSourceId: template.id,
388
- });
389
-
387
+ occurrences.push(this._buildVirtualEvent(template, cursorUNIX));
390
388
  cursorUNIX += interval;
391
389
  }
392
390
 
393
391
  return occurrences;
394
392
  }
395
393
 
394
+ /**
395
+ * Generate Nth-weekday-of-month occurrences (e.g., 2nd Wednesday).
396
+ * Walks month by month, computing the actual calendar date each time.
397
+ */
398
+ _generateNthWeekdayOccurrences(template, startUNIX, endUNIX) {
399
+ const { nth = 1, day: dayOfWeek = 0, hour = 0, minute = 0 } = template.recurrence;
400
+ const occurrences = [];
401
+
402
+ // Start scanning 2 months before the visible range
403
+ const startDate = new Date(startUNIX * 1000);
404
+ let year = startDate.getUTCFullYear();
405
+ let month = startDate.getUTCMonth() - 2;
406
+
407
+ let maxIterations = 100;
408
+ while (maxIterations-- > 0) {
409
+ const ts = this._nthWeekdayOfMonth(year, month, nth, dayOfWeek, hour, minute);
410
+
411
+ if (ts > endUNIX) {
412
+ break;
413
+ }
414
+
415
+ if (ts >= startUNIX) {
416
+ occurrences.push(this._buildVirtualEvent(template, ts));
417
+ }
418
+
419
+ month++;
420
+ if (month > 11) {
421
+ month = 0;
422
+ year++;
423
+ }
424
+ }
425
+
426
+ return occurrences;
427
+ }
428
+
429
+ /**
430
+ * Find the Nth occurrence of a weekday in a given month (UTC).
431
+ * Returns unix timestamp.
432
+ */
433
+ _nthWeekdayOfMonth(year, month, nth, dayOfWeek, hour, minute) {
434
+ const first = new Date(Date.UTC(year, month, 1));
435
+ let dateNum = 1;
436
+
437
+ // Advance to the first matching weekday
438
+ while (first.getUTCDay() !== dayOfWeek) {
439
+ dateNum++;
440
+ first.setUTCDate(dateNum);
441
+ }
442
+
443
+ // Advance to the Nth occurrence
444
+ dateNum += (nth - 1) * 7;
445
+
446
+ return Date.UTC(year, month, dateNum, hour, minute, 0) / 1000;
447
+ }
448
+
449
+ _buildVirtualEvent(template, sendAtUNIX) {
450
+ return {
451
+ id: `${template.id}__virtual__${sendAtUNIX}`,
452
+ sendAt: sendAtUNIX,
453
+ status: 'pending',
454
+ type: template.type,
455
+ settings: template.settings,
456
+ recurrence: template.recurrence,
457
+ _virtual: true,
458
+ _recurringSourceId: template.id,
459
+ };
460
+ }
461
+
396
462
  _getIntervalSeconds(pattern) {
397
463
  const DAY = 86400;
398
464
  switch (pattern) {