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.
@@ -32,7 +32,6 @@ export default class CalendarEvents {
32
32
  this._initTypeToggle();
33
33
  this._initRecurrenceToggle();
34
34
  this._initSendNow();
35
- this._initDeleteButton();
36
35
  this._initCreateButton();
37
36
  this._initPreview();
38
37
  }
@@ -47,7 +46,12 @@ export default class CalendarEvents {
47
46
  this._setType('email');
48
47
  this._resetRecurrence();
49
48
  document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
49
+ document.getElementById('campaign-modal-id').classList.add('d-none');
50
+ document.getElementById('campaign-local-time-row').style.display = 'none';
50
51
  });
52
+
53
+ document.getElementById('campaign-date').addEventListener('input', () => this._updateLocalTimeHint());
54
+ document.getElementById('campaign-time').addEventListener('input', () => this._updateLocalTimeHint());
51
55
  }
52
56
 
53
57
  _initResultsModal() {
@@ -82,15 +86,57 @@ export default class CalendarEvents {
82
86
  allowResubmit: true,
83
87
  });
84
88
 
85
- this.formManager.on('submit', async ({ data }) => {
86
- const payload = this._buildPayload(data);
89
+ this.formManager.on('validation', ({ $submitButton }) => {
90
+ const action = $submitButton?.value || 'save';
91
+ if (action === 'delete' || action === 'test') {
92
+ this.formManager.clearFieldErrors();
93
+ }
94
+ });
95
+
96
+ this.formManager.on('submit', async ({ data, $submitButton }) => {
97
+ const action = $submitButton?.value || 'save';
98
+
99
+ if (action === 'delete') {
100
+ if (!this.editingCampaignId) { return; }
101
+ if (!confirm('Delete this campaign? This cannot be undone.')) {
102
+ this.formManager.ready();
103
+ return;
104
+ }
105
+ await this._deleteCampaign(this.editingCampaignId);
106
+ this._getEditorModal().hide();
107
+ return;
108
+ }
109
+
110
+ if (action === 'test') {
111
+ const payload = this._buildPayload(data);
112
+ payload.test = true;
113
+ payload.sendAt = 'now';
114
+ delete payload.recurrence;
115
+
116
+ if (this.editingCampaignId) {
117
+ payload.recurringId = this.editingCampaignId;
118
+ }
119
+
120
+ const url = `${webManager.getApiUrl()}/backend-manager/marketing/campaign`;
121
+ await authorizedFetch(url, {
122
+ method: 'POST',
123
+ timeout: 60000,
124
+ response: 'json',
125
+ tries: 1,
126
+ log: true,
127
+ body: payload,
128
+ });
129
+ this.formManager.showSuccess('Test sent');
130
+ return;
131
+ }
87
132
 
133
+ // Default: save
134
+ const payload = this._buildPayload(data);
88
135
  if (this.editingCampaignId) {
89
136
  await this._updateCampaign(this.editingCampaignId, payload);
90
137
  } else {
91
138
  await this._createCampaign(payload);
92
139
  }
93
-
94
140
  this._getEditorModal().hide();
95
141
  });
96
142
  }
@@ -105,7 +151,7 @@ export default class CalendarEvents {
105
151
  const isEmail = $radio.value === 'email';
106
152
  $emailFields.classList.toggle('d-none', !isEmail);
107
153
  $pushFields.classList.toggle('d-none', isEmail);
108
- $subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body text)';
154
+ $subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body — campaign name is used as title)';
109
155
  });
110
156
  });
111
157
  }
@@ -168,6 +214,8 @@ export default class CalendarEvents {
168
214
  const $patternSelect = document.getElementById('campaign-recurrence-pattern');
169
215
  const $dayHint = document.getElementById('recurrence-day-hint');
170
216
  const $monthRow = document.getElementById('recurrence-month-row');
217
+ const $nthRow = document.getElementById('recurrence-nth-row');
218
+ const $dayInput = document.getElementById('campaign-recurrence-day');
171
219
 
172
220
  $checkbox.addEventListener('change', () => {
173
221
  $fields.classList.toggle('d-none', !$checkbox.checked);
@@ -175,14 +223,21 @@ export default class CalendarEvents {
175
223
 
176
224
  $patternSelect.addEventListener('change', () => {
177
225
  const pattern = $patternSelect.value;
178
- // Update day hint based on pattern
179
- if (pattern === 'weekly') {
226
+
227
+ const isWeekday = pattern === 'weekly' || pattern === 'monthly-weekday';
228
+
229
+ if (isWeekday) {
180
230
  $dayHint.textContent = '(of week, 0=Sun)';
231
+ $dayInput.min = 0;
232
+ $dayInput.max = 6;
181
233
  } else {
182
234
  $dayHint.textContent = '(of month)';
235
+ $dayInput.min = 1;
236
+ $dayInput.max = 31;
183
237
  }
184
- // Show month field only for yearly
238
+
185
239
  $monthRow.classList.toggle('d-none', pattern !== 'yearly');
240
+ $nthRow.classList.toggle('d-none', pattern !== 'monthly-weekday');
186
241
  });
187
242
  }
188
243
 
@@ -191,27 +246,10 @@ export default class CalendarEvents {
191
246
  const now = new Date();
192
247
  document.getElementById('campaign-date').value = formatDateUTC(now);
193
248
  document.getElementById('campaign-time').value = formatTimeUTC(now);
249
+ this._updateLocalTimeHint();
194
250
  });
195
251
  }
196
252
 
197
- _initDeleteButton() {
198
- document.getElementById('btn-delete-campaign').addEventListener('click', async () => {
199
- if (!this.editingCampaignId) {
200
- return;
201
- }
202
-
203
- if (!confirm('Delete this campaign? This cannot be undone.')) {
204
- return;
205
- }
206
-
207
- try {
208
- await this._deleteCampaign(this.editingCampaignId);
209
- this._getEditorModal().hide();
210
- } catch (error) {
211
- this.formManager.showError(`Delete failed: ${error.message}`);
212
- }
213
- });
214
- }
215
253
 
216
254
  // ============================================
217
255
  // Modal Operations
@@ -220,12 +258,14 @@ export default class CalendarEvents {
220
258
  this.editingCampaignId = null;
221
259
  this.formManager.reset();
222
260
  this._toggleDeleteButton(false);
261
+ this._showCampaignId(null);
223
262
  this._setType('email');
224
263
  document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
225
264
 
226
265
  // Pre-fill date and time
227
266
  document.getElementById('campaign-date').value = date || '';
228
267
  document.getElementById('campaign-time').value = time || '09:00';
268
+ this._updateLocalTimeHint();
229
269
 
230
270
  this._getEditorModal().show();
231
271
  }
@@ -246,6 +286,7 @@ export default class CalendarEvents {
246
286
  }
247
287
  this.editingCampaignId = template.id;
248
288
  this._toggleDeleteButton(true);
289
+ this._showCampaignId(template.id);
249
290
  document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
250
291
  this._populateFormFromCampaign(template, true);
251
292
  this._getEditorModal().show();
@@ -260,10 +301,15 @@ export default class CalendarEvents {
260
301
  return;
261
302
  }
262
303
 
263
- // Recurring template: editable (changes apply to all future sends)
304
+ // Recurring template: editable only if still pending
264
305
  if (displayType === DISPLAY_TYPES.RECURRING_TEMPLATE) {
306
+ if (campaign.status === 'sent' || campaign.status === 'failed') {
307
+ this._openResultsModal(campaign);
308
+ return;
309
+ }
265
310
  this.editingCampaignId = campaignId;
266
311
  this._toggleDeleteButton(true);
312
+ this._showCampaignId(campaignId);
267
313
  document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
268
314
  this._populateFormFromCampaign(campaign, true);
269
315
  this._getEditorModal().show();
@@ -273,6 +319,7 @@ export default class CalendarEvents {
273
319
  // One-off pending: fully editable
274
320
  this.editingCampaignId = campaignId;
275
321
  this._toggleDeleteButton(true);
322
+ this._showCampaignId(campaignId);
276
323
  document.getElementById('campaign-modal-title-text').textContent = 'Edit Campaign';
277
324
  this._populateFormFromCampaign(campaign, true);
278
325
  this._getEditorModal().show();
@@ -309,7 +356,8 @@ export default class CalendarEvents {
309
356
  document.getElementById('campaign-subject').value = settings.subject || '';
310
357
  document.getElementById('campaign-date').value = formatDateUTC(d);
311
358
  document.getElementById('campaign-time').value = formatTimeUTC(d);
312
- document.getElementById('campaign-discount-code').value = settings.discountCode || '';
359
+ const contentData = settings.data?.content || {};
360
+ document.getElementById('campaign-discount-code').value = contentData.discountCode || '';
313
361
  document.getElementById('campaign-test').checked = !!settings.test;
314
362
 
315
363
  // Targeting
@@ -319,8 +367,8 @@ export default class CalendarEvents {
319
367
 
320
368
  // Email fields
321
369
  document.getElementById('campaign-preheader').value = settings.preheader || '';
322
- document.getElementById('campaign-content').value = settings.content || '';
323
- document.getElementById('campaign-template').value = settings.template || 'default';
370
+ document.getElementById('campaign-content').value = contentData.message || '';
371
+ document.getElementById('campaign-template').value = settings.template || 'card';
324
372
  document.getElementById('campaign-sender').value = settings.sender || 'marketing';
325
373
 
326
374
  // Push fields
@@ -359,23 +407,32 @@ export default class CalendarEvents {
359
407
  const $recurrenceFields = document.getElementById('recurrence-fields');
360
408
 
361
409
  if (recurrence) {
410
+ const pattern = recurrence.pattern || 'monthly';
411
+ const isWeekday = pattern === 'weekly' || pattern === 'monthly-weekday';
412
+
362
413
  $recurringCheckbox.checked = true;
363
414
  $recurrenceFields.classList.remove('d-none');
364
- document.getElementById('campaign-recurrence-pattern').value = recurrence.pattern || 'monthly';
415
+ document.getElementById('campaign-recurrence-pattern').value = pattern;
365
416
  document.getElementById('campaign-recurrence-hour').value = recurrence.hour || 0;
417
+ document.getElementById('campaign-recurrence-minute').value = recurrence.minute || 0;
366
418
  document.getElementById('campaign-recurrence-day').value = recurrence.day || 1;
419
+ document.getElementById('campaign-recurrence-nth').value = recurrence.nth || 2;
367
420
  document.getElementById('campaign-recurrence-month').value = recurrence.month || 1;
368
421
 
369
- // Show month row if yearly
370
- document.getElementById('recurrence-month-row').classList.toggle('d-none', recurrence.pattern !== 'yearly');
422
+ document.getElementById('recurrence-month-row').classList.toggle('d-none', pattern !== 'yearly');
423
+ document.getElementById('recurrence-nth-row').classList.toggle('d-none', pattern !== 'monthly-weekday');
371
424
 
372
- // Update day hint
373
425
  const $dayHint = document.getElementById('recurrence-day-hint');
374
- $dayHint.textContent = recurrence.pattern === 'weekly' ? '(of week, 0=Sun)' : '(of month)';
426
+ const $dayInput = document.getElementById('campaign-recurrence-day');
427
+ $dayHint.textContent = isWeekday ? '(of week, 0=Sun)' : '(of month)';
428
+ $dayInput.min = isWeekday ? 0 : 1;
429
+ $dayInput.max = isWeekday ? 6 : 31;
375
430
  } else {
376
431
  $recurringCheckbox.checked = false;
377
432
  $recurrenceFields.classList.add('d-none');
378
433
  }
434
+
435
+ this._updateLocalTimeHint();
379
436
  }
380
437
 
381
438
  _setType(type) {
@@ -386,6 +443,45 @@ export default class CalendarEvents {
386
443
  }
387
444
  }
388
445
 
446
+ _showCampaignId(id) {
447
+ const $el = document.getElementById('campaign-modal-id');
448
+ if (id) {
449
+ $el.textContent = id;
450
+ $el.classList.remove('d-none');
451
+ } else {
452
+ $el.classList.add('d-none');
453
+ }
454
+ }
455
+
456
+ _updateLocalTimeHint() {
457
+ const $row = document.getElementById('campaign-local-time-row');
458
+ const $hint = document.getElementById('campaign-time-local');
459
+ const date = document.getElementById('campaign-date').value;
460
+ const time = document.getElementById('campaign-time').value;
461
+
462
+ if (!date || !time) {
463
+ $row.style.display = 'none';
464
+ return;
465
+ }
466
+
467
+ const [y, mo, d] = date.split('-').map(Number);
468
+ const [h, m] = time.split(':').map(Number);
469
+ const utc = new Date(Date.UTC(y, mo - 1, d, h, m));
470
+
471
+ const localStr = utc.toLocaleString('en', {
472
+ weekday: 'short',
473
+ month: 'short',
474
+ day: 'numeric',
475
+ year: 'numeric',
476
+ hour: 'numeric',
477
+ minute: '2-digit',
478
+ timeZoneName: 'short',
479
+ });
480
+
481
+ $hint.textContent = `Local: ${localStr}`;
482
+ $row.style.display = '';
483
+ }
484
+
389
485
  // ============================================
390
486
  // Payload Building
391
487
  // ============================================
@@ -404,9 +500,6 @@ export default class CalendarEvents {
404
500
  };
405
501
 
406
502
  // Config
407
- if (c.discountCode) {
408
- payload.discountCode = c.discountCode.trim();
409
- }
410
503
  if (c.test) {
411
504
  payload.test = true;
412
505
  }
@@ -440,15 +533,23 @@ export default class CalendarEvents {
440
533
  if (c.preheader) {
441
534
  payload.preheader = c.preheader.trim();
442
535
  }
443
- if (c.content) {
444
- payload.content = c.content;
445
- }
446
- if (c.template && c.template !== 'default') {
536
+ if (c.template && c.template !== 'card') {
447
537
  payload.template = c.template;
448
538
  }
449
539
  if (c.sender) {
450
540
  payload.sender = c.sender;
451
541
  }
542
+
543
+ const content = {};
544
+ if (c.content) {
545
+ content.message = c.content;
546
+ }
547
+ if (c.discountCode) {
548
+ content.discountCode = c.discountCode.trim();
549
+ }
550
+ if (Object.keys(content).length) {
551
+ payload.data = { ...payload.data, content };
552
+ }
452
553
  }
453
554
 
454
555
  // Push-specific
@@ -498,12 +599,17 @@ export default class CalendarEvents {
498
599
  // Recurrence
499
600
  if (c.recurring) {
500
601
  const rec = c.recurrence || {};
602
+ const pattern = rec.pattern || 'monthly';
501
603
  payload.recurrence = {
502
- pattern: rec.pattern || 'monthly',
604
+ pattern,
503
605
  hour: parseInt(rec.hour, 10) || 0,
606
+ minute: parseInt(rec.minute, 10) || 0,
504
607
  day: parseInt(rec.day, 10) || 1,
505
608
  };
506
- if (rec.pattern === 'yearly') {
609
+ if (pattern === 'monthly-weekday') {
610
+ payload.recurrence.nth = parseInt(rec.nth, 10) || 1;
611
+ }
612
+ if (pattern === 'yearly') {
507
613
  payload.recurrence.month = parseInt(rec.month, 10) || 1;
508
614
  }
509
615
  }
@@ -616,8 +722,13 @@ export default class CalendarEvents {
616
722
 
617
723
  // Update recurrence metadata for the backend cron
618
724
  recurrence.hour = d.getUTCHours();
725
+ recurrence.minute = d.getUTCMinutes();
619
726
  if (recurrence.pattern === 'weekly') {
620
727
  recurrence.day = d.getUTCDay();
728
+ } else if (recurrence.pattern === 'monthly-weekday') {
729
+ recurrence.day = d.getUTCDay();
730
+ // Calculate which occurrence of this weekday falls on this date (1st, 2nd, 3rd, 4th)
731
+ recurrence.nth = Math.ceil(d.getUTCDate() / 7);
621
732
  } else if (recurrence.pattern === 'monthly' || recurrence.pattern === 'quarterly') {
622
733
  recurrence.day = d.getUTCDate();
623
734
  } else if (recurrence.pattern === 'yearly') {
@@ -670,9 +781,12 @@ export default class CalendarEvents {
670
781
  html += `<span class="badge bg-${campaign.type === 'email' ? 'primary' : 'success'}">${campaign.type === 'email' ? 'Email' : 'Push'}</span>`;
671
782
  html += `</div>`;
672
783
  html += `<table class="table table-sm table-borderless mb-0">`;
673
- html += `<tr><td class="text-muted" style="width:120px">Name</td><td>${webManager.utilities().escapeHTML(settings.name || '')}</td></tr>`;
784
+ html += `<tr><td class="text-muted" style="width:120px">ID</td><td><code>${webManager.utilities().escapeHTML(campaign.id)}</code></td></tr>`;
785
+ html += `<tr><td class="text-muted">Name</td><td>${webManager.utilities().escapeHTML(settings.name || '')}</td></tr>`;
674
786
  html += `<tr><td class="text-muted">Subject</td><td>${webManager.utilities().escapeHTML(settings.subject || '')}</td></tr>`;
675
- html += `<tr><td class="text-muted">Sent At</td><td>${formatDateUTC(d)} ${formatTimeUTC(d)} UTC</td></tr>`;
787
+ html += `<tr><td class="text-muted">Test</td><td>${settings.test ? '<span class="badge bg-warning">Yes</span>' : 'No'}</td></tr>`;
788
+ const localStr = d.toLocaleString('en', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
789
+ html += `<tr><td class="text-muted">Sent At</td><td>${formatDateUTC(d)} ${formatTimeUTC(d)} UTC <span class="text-muted">(${localStr})</span></td></tr>`;
676
790
 
677
791
  if (campaign.type === 'email') {
678
792
  if (settings.preheader) {
@@ -705,10 +819,11 @@ export default class CalendarEvents {
705
819
  html += '</div>';
706
820
 
707
821
  // Content (email only)
708
- if (campaign.type === 'email' && settings.content) {
822
+ const resultContent = settings.data?.content?.message;
823
+ if (campaign.type === 'email' && resultContent) {
709
824
  html += '<div class="mb-4">';
710
825
  html += '<h6>Content</h6>';
711
- html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:200px;overflow-y:auto">${webManager.utilities().escapeHTML(settings.content)}</pre>`;
826
+ html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:200px;overflow-y:auto">${webManager.utilities().escapeHTML(resultContent)}</pre>`;
712
827
  html += '</div>';
713
828
  }
714
829
 
@@ -735,9 +850,12 @@ export default class CalendarEvents {
735
850
  document.getElementById('recurrence-fields').classList.add('d-none');
736
851
  document.getElementById('campaign-recurrence-pattern').value = 'monthly';
737
852
  document.getElementById('campaign-recurrence-hour').value = '14';
853
+ document.getElementById('campaign-recurrence-minute').value = '0';
738
854
  document.getElementById('campaign-recurrence-day').value = '1';
855
+ document.getElementById('campaign-recurrence-nth').value = '2';
739
856
  document.getElementById('campaign-recurrence-month').value = '1';
740
857
  document.getElementById('recurrence-month-row').classList.add('d-none');
858
+ document.getElementById('recurrence-nth-row').classList.add('d-none');
741
859
  }
742
860
 
743
861
  }
@@ -14,6 +14,18 @@ export default class CalendarRenderer {
14
14
  this.$toolbar = document.getElementById('calendar-toolbar');
15
15
  this.$grid = document.getElementById('calendar-grid');
16
16
  this._nowLineInterval = null;
17
+ this._resizeObserver = null;
18
+
19
+ this._initResizeObserver();
20
+ }
21
+
22
+ _initResizeObserver() {
23
+ this._resizeObserver = new ResizeObserver(() => {
24
+ if (this.core.viewMode === 'month') {
25
+ this._fitMonthEvents();
26
+ }
27
+ });
28
+ this._resizeObserver.observe(this.$grid);
17
29
  }
18
30
 
19
31
  // ============================================
@@ -90,6 +102,7 @@ export default class CalendarRenderer {
90
102
  // Month View
91
103
  // ============================================
92
104
  _renderMonthView() {
105
+ this._pillHeight = null;
93
106
  const core = this.core;
94
107
  const cells = core.getMonthGrid();
95
108
 
@@ -110,10 +123,9 @@ export default class CalendarRenderer {
110
123
  html += `<div class="calendar-cell-date">${cell.date.getUTCDate()}</div>`;
111
124
  html += '<div class="calendar-cell-events">';
112
125
 
113
- const maxVisible = 3;
114
- campaigns.slice(0, maxVisible).forEach((c) => { html += this._renderEventPill(c); });
115
- if (campaigns.length > maxVisible) {
116
- html += `<div class="calendar-cell-more" data-date="${dateStr}">+${campaigns.length - maxVisible} more</div>`;
126
+ campaigns.forEach((c) => { html += this._renderEventPill(c); });
127
+ if (campaigns.length > 0) {
128
+ html += `<div class="calendar-cell-more" data-date="${dateStr}" style="display:none"></div>`;
117
129
  }
118
130
 
119
131
  html += '</div></div>';
@@ -121,11 +133,81 @@ export default class CalendarRenderer {
121
133
  html += '</div>';
122
134
 
123
135
  this.$grid.innerHTML = html;
136
+ requestAnimationFrame(() => this._fitMonthEvents());
124
137
  this._bindCellClicks();
125
138
  this._bindCampaignClicks();
126
139
  this._bindDragAndDrop();
127
140
  }
128
141
 
142
+ _fitMonthEvents() {
143
+ // Measure natural pill height once from a detached clone so flex
144
+ // compression doesn't skew the value
145
+ if (!this._pillHeight) {
146
+ const $sample = this.$grid.querySelector('.calendar-event');
147
+ if ($sample) {
148
+ const $clone = $sample.cloneNode(true);
149
+ $clone.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;';
150
+ document.body.appendChild($clone);
151
+ this._pillHeight = $clone.getBoundingClientRect().height;
152
+ $clone.remove();
153
+ }
154
+ }
155
+
156
+ this.$grid.querySelectorAll('.calendar-cell').forEach(($cell) => {
157
+ const $events = $cell.querySelector('.calendar-cell-events');
158
+ if (!$events) { return; }
159
+
160
+ const $pills = $events.querySelectorAll('.calendar-event');
161
+ const $more = $events.querySelector('.calendar-cell-more');
162
+ if ($pills.length === 0) { return; }
163
+
164
+ // Hide everything first so the cell has its natural empty height
165
+ $pills.forEach(($p) => { $p.style.display = 'none'; });
166
+ if ($more) { $more.style.display = 'none'; }
167
+
168
+ const cellRect = $cell.getBoundingClientRect();
169
+ const eventsRect = $events.getBoundingClientRect();
170
+ const available = cellRect.bottom - eventsRect.top;
171
+
172
+ const pillHeight = this._pillHeight || 20;
173
+ const gap = 1;
174
+
175
+ // Measure "+more" line height
176
+ if ($more) {
177
+ $more.textContent = '+1 more';
178
+ $more.style.display = '';
179
+ }
180
+ const moreLineHeight = $more ? $more.getBoundingClientRect().height + gap : 0;
181
+ if ($more) { $more.style.display = 'none'; }
182
+
183
+ // Calculate how many pills fit
184
+ let maxVisible = 0;
185
+ for (let n = 1; n <= $pills.length; n++) {
186
+ const pillsHeight = n * pillHeight + (n - 1) * gap;
187
+ const needsMore = n < $pills.length;
188
+ const total = pillsHeight + (needsMore ? moreLineHeight : 0);
189
+ if (total <= available) {
190
+ maxVisible = n;
191
+ } else {
192
+ break;
193
+ }
194
+ }
195
+
196
+ // Apply visibility
197
+ $pills.forEach(($p, i) => {
198
+ $p.style.display = i < maxVisible ? '' : 'none';
199
+ });
200
+
201
+ if ($more) {
202
+ const hidden = $pills.length - maxVisible;
203
+ if (hidden > 0) {
204
+ $more.textContent = `+${hidden} more`;
205
+ $more.style.display = '';
206
+ }
207
+ }
208
+ });
209
+ }
210
+
129
211
  // ============================================
130
212
  // Week View
131
213
  // ============================================
@@ -141,12 +223,6 @@ export default class CalendarRenderer {
141
223
  });
142
224
  html += '</div>';
143
225
 
144
- html += '<div class="calendar-week-allday"><div class="calendar-week-time-label">all-day</div>';
145
- weekDates.forEach((date) => {
146
- html += `<div class="calendar-cell" data-date="${formatDateUTC(date)}" data-allday="true" style="min-height:auto;border-bottom:none;"></div>`;
147
- });
148
- html += '</div>';
149
-
150
226
  html += '<div class="calendar-week-body"><div class="calendar-week-time-col">';
151
227
  hours.forEach((hour) => { html += `<div class="calendar-week-time-label">${hour.label}</div>`; });
152
228
  html += '</div>';
@@ -277,7 +353,7 @@ export default class CalendarRenderer {
277
353
  html += `<tr class="calendar-list-date-header"><td colspan="4">${dayName}, ${monthName} ${d.getUTCDate()}, ${d.getUTCFullYear()}${todayBadge}</td></tr>`;
278
354
  }
279
355
 
280
- const timeStr = this._formatTime(formatTimeUTC(campaign.sendAt));
356
+ const timeStr = this._formatLocalTime(campaign.sendAt);
281
357
  const name = (campaign.settings && campaign.settings.name) || 'Untitled';
282
358
  const statusStyle = core.campaignStatusStyle(campaign);
283
359
  const isRecurring = core.isRecurring(campaign);
@@ -320,7 +396,7 @@ export default class CalendarRenderer {
320
396
 
321
397
  _renderEventPill(campaign) {
322
398
  const core = this.core;
323
- const timeStr = this._formatTime(formatTimeUTC(campaign.sendAt));
399
+ const timeStr = this._formatLocalTime(campaign.sendAt);
324
400
  const color = core.campaignColor(campaign);
325
401
  const statusStyle = core.campaignStatusStyle(campaign);
326
402
  const name = (campaign.settings && campaign.settings.name) || 'Untitled';
@@ -356,11 +432,11 @@ export default class CalendarRenderer {
356
432
 
357
433
  _renderTimeEvent(campaign, layout) {
358
434
  const core = this.core;
359
- const time = formatTimeUTC(campaign.sendAt);
360
- const [hours, minutes] = time.split(':').map(Number);
435
+ const utcTime = formatTimeUTC(campaign.sendAt);
436
+ const [hours, minutes] = utcTime.split(':').map(Number);
361
437
  const topPx = (hours * 60 + minutes);
362
438
  const heightPx = Math.max(core.campaignDuration(), 15);
363
- const timeStr = this._formatTime(time);
439
+ const timeStr = this._formatLocalTime(campaign.sendAt);
364
440
  const color = core.campaignColor(campaign);
365
441
  const statusStyle = core.campaignStatusStyle(campaign);
366
442
  const name = (campaign.settings && campaign.settings.name) || 'Untitled';
@@ -452,7 +528,7 @@ export default class CalendarRenderer {
452
528
  // ============================================
453
529
  _startNowLine() {
454
530
  clearInterval(this._nowLineInterval);
455
- if (this.core.viewMode !== 'day' && this.core.viewMode !== 'week') {
531
+ if (this.core.viewMode !== 'day' && this.core.viewMode !== 'week' && this.core.viewMode !== 'month') {
456
532
  return;
457
533
  }
458
534
  this._updateNowLine();
@@ -466,6 +542,7 @@ export default class CalendarRenderer {
466
542
  const todayStr = formatDateUTC(now);
467
543
  const minutesSinceMidnight = now.getUTCHours() * 60 + now.getUTCMinutes();
468
544
 
545
+ // Week/day views: absolute position in px (1px per minute)
469
546
  const $cols = this.$grid.querySelectorAll(
470
547
  `.calendar-week-day-col[data-date="${todayStr}"], .calendar-day-col[data-date="${todayStr}"]`
471
548
  );
@@ -476,6 +553,24 @@ export default class CalendarRenderer {
476
553
  $line.style.top = `${minutesSinceMidnight}px`;
477
554
  $col.appendChild($line);
478
555
  });
556
+
557
+ // Month view: position on the grid container so the dot isn't clipped by cell overflow
558
+ if (this.core.viewMode === 'month') {
559
+ const $cell = this.$grid.querySelector(`.calendar-cell[data-date="${todayStr}"]`);
560
+ if ($cell) {
561
+ const gridRect = this.$grid.getBoundingClientRect();
562
+ const cellRect = $cell.getBoundingClientRect();
563
+ const pct = minutesSinceMidnight / 1440;
564
+ const topPx = (cellRect.top - gridRect.top) + (cellRect.height * pct);
565
+
566
+ const $line = document.createElement('div');
567
+ $line.className = 'calendar-now-line';
568
+ $line.style.top = `${topPx}px`;
569
+ $line.style.left = `${cellRect.left - gridRect.left}px`;
570
+ $line.style.width = `${cellRect.width}px`;
571
+ this.$grid.appendChild($line);
572
+ }
573
+ }
479
574
  }
480
575
 
481
576
  // ============================================
@@ -679,5 +774,14 @@ export default class CalendarRenderer {
679
774
  const display = h === 0 ? 12 : h > 12 ? h - 12 : h;
680
775
  return `${display}${m > 0 ? ':' + String(m).padStart(2, '0') : ''}${period}`;
681
776
  }
777
+
778
+ _formatLocalTime(sendAt) {
779
+ const d = new Date(sendAt * 1000);
780
+ const h = d.getHours();
781
+ const m = d.getMinutes();
782
+ const period = h >= 12 ? 'p' : 'a';
783
+ const display = h === 0 ? 12 : h > 12 ? h - 12 : h;
784
+ return `${display}${m > 0 ? ':' + String(m).padStart(2, '0') : ''}${period}`;
785
+ }
682
786
  }
683
787