@forcecalendar/interface 1.0.10 → 1.0.12

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.
@@ -349,11 +349,147 @@ export class ForceCalendar extends BaseComponent {
349
349
  justify-content: space-between;
350
350
  width: 100%;
351
351
  }
352
-
352
+
353
353
  #create-event-btn {
354
354
  flex: 1;
355
355
  }
356
356
  }
357
+
358
+ /* Month View Styles (inline rendering for Locker Service compatibility) */
359
+ .fc-month-view {
360
+ display: flex;
361
+ flex-direction: column;
362
+ height: 100%;
363
+ background: var(--fc-background);
364
+ }
365
+
366
+ .fc-month-header {
367
+ display: grid;
368
+ grid-template-columns: repeat(7, 1fr);
369
+ border-bottom: 1px solid var(--fc-border-color);
370
+ background: var(--fc-background-alt);
371
+ }
372
+
373
+ .fc-month-header-cell {
374
+ padding: 12px 8px;
375
+ text-align: center;
376
+ font-size: 11px;
377
+ font-weight: 600;
378
+ color: var(--fc-text-light);
379
+ text-transform: uppercase;
380
+ letter-spacing: 0.05em;
381
+ }
382
+
383
+ .fc-month-body {
384
+ display: flex;
385
+ flex-direction: column;
386
+ flex: 1;
387
+ }
388
+
389
+ .fc-month-week {
390
+ display: grid;
391
+ grid-template-columns: repeat(7, 1fr);
392
+ flex: 1;
393
+ min-height: 100px;
394
+ }
395
+
396
+ .fc-month-day {
397
+ background: var(--fc-background);
398
+ border-right: 1px solid var(--fc-border-color);
399
+ border-bottom: 1px solid var(--fc-border-color);
400
+ padding: 4px;
401
+ min-height: 80px;
402
+ cursor: pointer;
403
+ transition: background-color 0.15s ease;
404
+ display: flex;
405
+ flex-direction: column;
406
+ }
407
+
408
+ .fc-month-day:hover {
409
+ background: var(--fc-background-hover);
410
+ }
411
+
412
+ .fc-month-day:last-child {
413
+ border-right: none;
414
+ }
415
+
416
+ .fc-month-day.other-month {
417
+ background: var(--fc-background-alt);
418
+ }
419
+
420
+ .fc-month-day.other-month .fc-day-number {
421
+ color: var(--fc-text-light);
422
+ }
423
+
424
+ .fc-month-day.today {
425
+ background: rgba(37, 99, 235, 0.05);
426
+ }
427
+
428
+ .fc-month-day.today .fc-day-number {
429
+ background: var(--fc-primary-color);
430
+ color: white;
431
+ border-radius: 50%;
432
+ width: 24px;
433
+ height: 24px;
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ }
438
+
439
+ .fc-day-number {
440
+ font-size: 13px;
441
+ font-weight: 500;
442
+ color: var(--fc-text-color);
443
+ padding: 2px 4px;
444
+ margin-bottom: 4px;
445
+ }
446
+
447
+ .fc-day-events {
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 2px;
451
+ flex: 1;
452
+ overflow: hidden;
453
+ }
454
+
455
+ .fc-event {
456
+ font-size: 11px;
457
+ padding: 2px 6px;
458
+ border-radius: 3px;
459
+ color: white;
460
+ white-space: nowrap;
461
+ overflow: hidden;
462
+ text-overflow: ellipsis;
463
+ cursor: pointer;
464
+ transition: transform 0.1s ease;
465
+ }
466
+
467
+ .fc-event:hover {
468
+ transform: scale(1.02);
469
+ }
470
+
471
+ .fc-more-events {
472
+ font-size: 10px;
473
+ color: var(--fc-text-light);
474
+ padding: 2px 4px;
475
+ font-weight: 500;
476
+ }
477
+
478
+ /* Week View Styles (inline rendering for Locker Service compatibility) */
479
+ .fc-week-view {
480
+ display: flex;
481
+ flex-direction: column;
482
+ height: 100%;
483
+ background: var(--fc-background);
484
+ }
485
+
486
+ /* Day View Styles (inline rendering for Locker Service compatibility) */
487
+ .fc-day-view {
488
+ display: flex;
489
+ flex-direction: column;
490
+ height: 100%;
491
+ background: var(--fc-background);
492
+ }
357
493
  `;
358
494
  }
359
495
 
@@ -436,30 +572,53 @@ export class ForceCalendar extends BaseComponent {
436
572
  const container = this.$('#calendar-view-container');
437
573
  console.log('[ForceCalendar] afterRender - container:', !!container, 'stateManager:', !!this.stateManager, 'currentView:', this.currentView);
438
574
 
575
+ // Only create view once per view type change
439
576
  if (container && this.stateManager && this.currentView) {
577
+ // Check if container actually has content (render() clears shadow DOM)
578
+ if (this._currentViewInstance && this._currentViewInstance._viewType === this.currentView && container.children.length > 0) {
579
+ console.log('[ForceCalendar] View already exists with content, skipping creation');
580
+ return;
581
+ }
582
+
440
583
  // Clean up previous view if exists
441
584
  if (this._currentViewInstance) {
442
585
  if (this._currentViewInstance.cleanup) {
443
586
  this._currentViewInstance.cleanup();
444
587
  }
588
+ if (this._viewUnsubscribe) {
589
+ this._viewUnsubscribe();
590
+ this._viewUnsubscribe = null;
591
+ }
445
592
  }
446
593
 
447
594
  console.log('[ForceCalendar] Creating view for:', this.currentView);
448
595
 
449
596
  // Create a simple view renderer that doesn't use custom elements
450
- const viewRenderer = this._createViewRenderer(this.currentView);
451
- if (viewRenderer) {
452
- this._currentViewInstance = viewRenderer;
453
- viewRenderer.stateManager = this.stateManager;
454
- viewRenderer.container = container;
455
- viewRenderer.render();
456
-
457
- // Subscribe to state changes
458
- this.stateManager.subscribe((newState, oldState) => {
459
- if (viewRenderer && viewRenderer.render) {
460
- viewRenderer.render();
461
- }
462
- });
597
+ try {
598
+ const viewRenderer = this._createViewRenderer(this.currentView);
599
+ if (viewRenderer) {
600
+ viewRenderer._viewType = this.currentView;
601
+ this._currentViewInstance = viewRenderer;
602
+ viewRenderer.stateManager = this.stateManager;
603
+ viewRenderer.container = container;
604
+
605
+ console.log('[ForceCalendar] Calling viewRenderer.render()');
606
+ viewRenderer.render();
607
+ console.log('[ForceCalendar] viewRenderer.render() completed');
608
+
609
+ // Subscribe to state changes (store unsubscribe function)
610
+ this._viewUnsubscribe = this.stateManager.subscribe((newState, oldState) => {
611
+ // Only re-render on data changes, not view changes
612
+ if (newState.events !== oldState?.events ||
613
+ newState.currentDate !== oldState?.currentDate) {
614
+ if (viewRenderer && viewRenderer.render) {
615
+ viewRenderer.render();
616
+ }
617
+ }
618
+ });
619
+ }
620
+ } catch (err) {
621
+ console.error('[ForceCalendar] Error creating/rendering view:', err);
463
622
  }
464
623
  }
465
624
 
@@ -510,11 +669,13 @@ export class ForceCalendar extends BaseComponent {
510
669
  // Create a simple view renderer that bypasses custom elements
511
670
  // This is necessary for Salesforce Locker Service compatibility
512
671
  const self = this;
672
+ const currentViewName = viewName;
513
673
 
514
674
  return {
515
675
  stateManager: null,
516
676
  container: null,
517
677
  _listeners: [],
678
+ _scrolled: false,
518
679
 
519
680
  cleanup() {
520
681
  this._listeners.forEach(({ element, event, handler }) => {
@@ -532,16 +693,34 @@ export class ForceCalendar extends BaseComponent {
532
693
  if (!this.container || !this.stateManager) return;
533
694
 
534
695
  const viewData = this.stateManager.getViewData();
535
- if (!viewData || !viewData.weeks) {
536
- this.container.innerHTML = '<div style="padding: 20px; text-align: center;">Loading calendar...</div>';
696
+ if (!viewData) {
697
+ this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Loading...</div>';
537
698
  return;
538
699
  }
539
700
 
540
701
  this.cleanup();
541
702
  const config = this.stateManager.getState().config;
542
- const html = this._renderMonthView(viewData, config);
703
+ let html = '';
704
+
705
+ switch (currentViewName) {
706
+ case 'week':
707
+ html = this._renderWeekView(viewData, config);
708
+ break;
709
+ case 'day':
710
+ html = this._renderDayView(viewData, config);
711
+ break;
712
+ case 'month':
713
+ default:
714
+ if (!viewData.weeks) {
715
+ this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No data available for month view.</div>';
716
+ return;
717
+ }
718
+ html = this._renderMonthView(viewData, config);
719
+ break;
720
+ }
721
+
543
722
  this.container.innerHTML = html;
544
- this._attachEventHandlers();
723
+ this._attachEventHandlers(currentViewName);
545
724
  },
546
725
 
547
726
  _renderMonthView(viewData, config) {
@@ -553,54 +732,37 @@ export class ForceCalendar extends BaseComponent {
553
732
  }
554
733
 
555
734
  let html = `
556
- <style>
557
- .fc-month-view { display: flex; flex-direction: column; height: 100%; background: #fff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; }
558
- .fc-month-header { display: grid; grid-template-columns: repeat(7, 1fr); background: #fafafa; border-bottom: 1px solid #e5e7eb; }
559
- .fc-month-header-cell { padding: 8px; text-align: left; font-weight: 600; font-size: 10px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; }
560
- .fc-month-body { flex: 1; display: flex; flex-direction: column; }
561
- .fc-month-week { flex: 1; display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; }
562
- .fc-month-week:last-child { border-bottom: none; }
563
- .fc-month-day { background: #fff; padding: 4px; position: relative; border-right: 1px solid #e5e7eb; min-height: 80px; cursor: pointer; }
564
- .fc-month-day:last-child { border-right: none; }
565
- .fc-month-day:hover { background: #f9fafb; }
566
- .fc-month-day.other-month { background: #f9fafb; }
567
- .fc-month-day.other-month .fc-day-number { color: #d1d5db; }
568
- .fc-month-day.today .fc-day-number { background: #ef4444; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; }
569
- .fc-day-number { font-size: 12px; font-weight: 500; color: #111827; padding: 4px; }
570
- .fc-day-events { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
571
- .fc-event { font-size: 11px; padding: 2px 6px; border-radius: 2px; background: #2563eb; color: white; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
572
- .fc-event:hover { opacity: 0.9; }
573
- .fc-more-events { font-size: 10px; color: #6b7280; padding: 2px 4px; cursor: pointer; }
574
- .fc-more-events:hover { color: #111827; text-decoration: underline; }
575
- </style>
576
- <div class="fc-month-view">
577
- <div class="fc-month-header">
578
- ${dayNames.map(d => `<div class="fc-month-header-cell">${d}</div>`).join('')}
735
+ <div class="fc-month-view" style="display: flex; flex-direction: column; height: 100%; min-height: 400px; background: #fff; border: 1px solid #e5e7eb;">
736
+ <div class="fc-month-header" style="display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #f9fafb;">
737
+ ${dayNames.map(d => `<div class="fc-month-header-cell" style="padding: 12px 8px; text-align: center; font-size: 11px; font-weight: 600; color: #6b7280; text-transform: uppercase;">${d}</div>`).join('')}
579
738
  </div>
580
- <div class="fc-month-body">
739
+ <div class="fc-month-body" style="display: flex; flex-direction: column; flex: 1;">
581
740
  `;
582
741
 
583
742
  viewData.weeks.forEach(week => {
584
- html += '<div class="fc-month-week">';
743
+ html += '<div class="fc-month-week" style="display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; min-height: 80px;">';
585
744
  week.days.forEach(day => {
586
- const classes = ['fc-month-day'];
587
- if (!day.isCurrentMonth) classes.push('other-month');
588
- if (day.isToday) classes.push('today');
745
+ const isOtherMonth = !day.isCurrentMonth;
746
+ const isToday = day.isToday;
747
+
748
+ const dayBg = isOtherMonth ? '#f3f4f6' : '#fff';
749
+ const dayNumColor = isOtherMonth ? '#9ca3af' : '#111827';
750
+ const todayStyle = isToday ? 'background: #2563eb; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;' : '';
589
751
 
590
752
  const events = day.events || [];
591
753
  const visibleEvents = events.slice(0, 3);
592
754
  const moreCount = events.length - 3;
593
755
 
594
756
  html += `
595
- <div class="${classes.join(' ')}" data-date="${day.date}">
596
- <div class="fc-day-number">${day.dayOfMonth}</div>
597
- <div class="fc-day-events">
757
+ <div class="fc-month-day" data-date="${day.date}" style="background: ${dayBg}; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; padding: 4px; min-height: 80px; cursor: pointer;">
758
+ <div class="fc-day-number" style="font-size: 13px; font-weight: 500; color: ${dayNumColor}; padding: 2px 4px; margin-bottom: 4px; ${todayStyle}">${day.dayOfMonth}</div>
759
+ <div class="fc-day-events" style="display: flex; flex-direction: column; gap: 2px;">
598
760
  ${visibleEvents.map(evt => `
599
- <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}">
761
+ <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 11px; padding: 2px 6px; border-radius: 3px; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
600
762
  ${evt.title}
601
763
  </div>
602
764
  `).join('')}
603
- ${moreCount > 0 ? `<div class="fc-more-events">+${moreCount} more</div>` : ''}
765
+ ${moreCount > 0 ? `<div class="fc-more-events" style="font-size: 10px; color: #6b7280; padding: 2px 4px; font-weight: 500;">+${moreCount} more</div>` : ''}
604
766
  </div>
605
767
  </div>
606
768
  `;
@@ -612,10 +774,265 @@ export class ForceCalendar extends BaseComponent {
612
774
  return html;
613
775
  },
614
776
 
615
- _attachEventHandlers() {
777
+ _renderWeekView(viewData, config) {
778
+ const days = viewData.days || [];
779
+ if (days.length === 0) {
780
+ return '<div style="padding: 20px; text-align: center; color: #666;">No data available for week view.</div>';
781
+ }
782
+
783
+ const weekStartsOn = config.weekStartsOn || 0;
784
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
785
+ const hours = Array.from({ length: 24 }, (_, i) => i);
786
+
787
+ // Process days to add events
788
+ const processedDays = days.map(day => {
789
+ const dayDate = new Date(day.date);
790
+ const events = day.events || [];
791
+ return {
792
+ ...day,
793
+ date: dayDate,
794
+ dayName: dayNames[dayDate.getDay()],
795
+ dayOfMonth: dayDate.getDate(),
796
+ isToday: this._isToday(dayDate),
797
+ timedEvents: events.filter(e => !e.allDay),
798
+ allDayEvents: events.filter(e => e.allDay)
799
+ };
800
+ });
801
+
802
+ let html = `
803
+ <div class="fc-week-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
804
+ <!-- Header -->
805
+ <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
806
+ <div style="border-right: 1px solid #e5e7eb;"></div>
807
+ ${processedDays.map(day => `
808
+ <div style="padding: 12px 8px; text-align: center; border-right: 1px solid #e5e7eb;">
809
+ <div style="font-size: 10px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">${day.dayName}</div>
810
+ <div style="font-size: 16px; font-weight: 500; margin-top: 4px; ${day.isToday ? 'background: #dc2626; color: white; border-radius: 50%; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center;' : 'color: #111827;'}">${day.dayOfMonth}</div>
811
+ </div>
812
+ `).join('')}
813
+ </div>
814
+
815
+ <!-- All Day Row -->
816
+ <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 32px; flex-shrink: 0;">
817
+ <div style="font-size: 9px; color: #6b7280; display: flex; align-items: center; justify-content: center; border-right: 1px solid #e5e7eb; text-transform: uppercase; font-weight: 700;">All day</div>
818
+ ${processedDays.map(day => `
819
+ <div style="border-right: 1px solid #e5e7eb; padding: 4px; display: flex; flex-direction: column; gap: 2px;">
820
+ ${day.allDayEvents.map(evt => `
821
+ <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 10px; padding: 2px 4px; border-radius: 2px; color: white; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
822
+ ${evt.title}
823
+ </div>
824
+ `).join('')}
825
+ </div>
826
+ `).join('')}
827
+ </div>
828
+
829
+ <!-- Time Grid Body -->
830
+ <div id="week-scroll-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
831
+ <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); position: relative; height: 1440px;">
832
+ <!-- Time Gutter -->
833
+ <div style="border-right: 1px solid #e5e7eb; background: #fafafa;">
834
+ ${hours.map(h => `
835
+ <div style="height: 60px; font-size: 10px; color: #6b7280; text-align: right; padding-right: 8px; font-weight: 500;">
836
+ ${h === 0 ? '' : this._formatHour(h)}
837
+ </div>
838
+ `).join('')}
839
+ </div>
840
+
841
+ <!-- Day Columns -->
842
+ ${processedDays.map(day => `
843
+ <div class="fc-week-day-column" data-date="${day.date.toISOString()}" style="border-right: 1px solid #e5e7eb; position: relative; cursor: pointer;">
844
+ <!-- Hour grid lines -->
845
+ ${hours.map(() => `<div style="height: 60px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
846
+
847
+ <!-- Now indicator for today -->
848
+ ${day.isToday ? this._renderNowIndicator() : ''}
849
+
850
+ <!-- Timed events -->
851
+ ${day.timedEvents.map(evt => this._renderTimedEvent(evt)).join('')}
852
+ </div>
853
+ `).join('')}
854
+ </div>
855
+ </div>
856
+ </div>
857
+ `;
858
+
859
+ return html;
860
+ },
861
+
862
+ _renderDayView(viewData, config) {
863
+ // Day view from core has: type, date, dayName, isToday, allDayEvents, hours
864
+ // We need to handle both the core structure and enriched structure
865
+ const currentDate = this.stateManager?.getState()?.currentDate || new Date();
866
+
867
+ let dayDate, dayName, isToday, allDayEvents, timedEvents;
868
+
869
+ if (viewData.type === 'day' && viewData.date) {
870
+ // Core day view structure
871
+ dayDate = new Date(viewData.date);
872
+ dayName = viewData.dayName || ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayDate.getDay()];
873
+ isToday = viewData.isToday !== undefined ? viewData.isToday : this._isToday(dayDate);
874
+ allDayEvents = viewData.allDayEvents || [];
875
+
876
+ // Extract timed events from hours array or get from stateManager
877
+ if (viewData.hours && Array.isArray(viewData.hours)) {
878
+ // Collect unique events from hours (events can span multiple hours)
879
+ const eventMap = new Map();
880
+ viewData.hours.forEach(hour => {
881
+ (hour.events || []).forEach(evt => {
882
+ if (!eventMap.has(evt.id)) {
883
+ eventMap.set(evt.id, evt);
884
+ }
885
+ });
886
+ });
887
+ timedEvents = Array.from(eventMap.values());
888
+ } else {
889
+ timedEvents = [];
890
+ }
891
+ } else if (viewData.days && viewData.days.length > 0) {
892
+ // Enriched structure with days array
893
+ const dayData = viewData.days.find(d => this._isSameDay(new Date(d.date), currentDate)) || viewData.days[0];
894
+ dayDate = new Date(dayData.date);
895
+ dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayDate.getDay()];
896
+ isToday = this._isToday(dayDate);
897
+ const events = dayData.events || [];
898
+ allDayEvents = events.filter(e => e.allDay);
899
+ timedEvents = events.filter(e => !e.allDay);
900
+ } else {
901
+ return '<div style="padding: 20px; text-align: center; color: #666;">No data available for day view.</div>';
902
+ }
903
+
904
+ const hours = Array.from({ length: 24 }, (_, i) => i);
905
+
906
+ let html = `
907
+ <div class="fc-day-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
908
+ <!-- Header -->
909
+ <div style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
910
+ <div style="border-right: 1px solid #e5e7eb;"></div>
911
+ <div style="padding: 16px 24px;">
912
+ <div style="font-size: 12px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">${dayName}</div>
913
+ <div style="font-size: 24px; font-weight: 600; margin-top: 4px; ${isToday ? 'color: #dc2626;' : 'color: #111827;'}">${dayDate.getDate()}</div>
914
+ </div>
915
+ </div>
916
+
917
+ <!-- All Day Row -->
918
+ <div style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 36px; flex-shrink: 0;">
919
+ <div style="font-size: 9px; color: #6b7280; display: flex; align-items: center; justify-content: center; border-right: 1px solid #e5e7eb; text-transform: uppercase; font-weight: 700;">All day</div>
920
+ <div style="padding: 6px 12px; display: flex; flex-wrap: wrap; gap: 4px;">
921
+ ${allDayEvents.map(evt => `
922
+ <div class="fc-event" data-event-id="${evt.id}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 12px; padding: 4px 8px; border-radius: 4px; color: white; cursor: pointer; font-weight: 500;">
923
+ ${evt.title}
924
+ </div>
925
+ `).join('')}
926
+ </div>
927
+ </div>
928
+
929
+ <!-- Time Grid Body -->
930
+ <div id="day-scroll-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
931
+ <div style="display: grid; grid-template-columns: 60px 1fr; position: relative; height: 1440px;">
932
+ <!-- Time Gutter -->
933
+ <div style="border-right: 1px solid #e5e7eb; background: #fafafa;">
934
+ ${hours.map(h => `
935
+ <div style="height: 60px; font-size: 11px; color: #6b7280; text-align: right; padding-right: 12px; font-weight: 500;">
936
+ ${h === 0 ? '' : this._formatHour(h)}
937
+ </div>
938
+ `).join('')}
939
+ </div>
940
+
941
+ <!-- Day Column -->
942
+ <div class="fc-day-column" data-date="${dayDate.toISOString()}" style="position: relative; cursor: pointer;">
943
+ <!-- Hour grid lines -->
944
+ ${hours.map(() => `<div style="height: 60px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
945
+
946
+ <!-- Now indicator for today -->
947
+ ${isToday ? this._renderNowIndicator() : ''}
948
+
949
+ <!-- Timed events -->
950
+ ${timedEvents.map(evt => this._renderTimedEventDay(evt)).join('')}
951
+ </div>
952
+ </div>
953
+ </div>
954
+ </div>
955
+ `;
956
+
957
+ return html;
958
+ },
959
+
960
+ _renderTimedEvent(event) {
961
+ const start = new Date(event.start);
962
+ const end = new Date(event.end);
963
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
964
+ const durationMinutes = Math.max((end - start) / (1000 * 60), 20);
965
+ const color = event.backgroundColor || '#2563eb';
966
+
967
+ return `
968
+ <div class="fc-event" data-event-id="${event.id}"
969
+ style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 2px; right: 2px;
970
+ background-color: ${color}; border-radius: 4px; padding: 4px 8px; font-size: 11px;
971
+ font-weight: 500; color: white; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
972
+ cursor: pointer; z-index: 5;">
973
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${event.title}</div>
974
+ <div style="font-size: 10px; opacity: 0.9;">${this._formatTime(start)}</div>
975
+ </div>
976
+ `;
977
+ },
978
+
979
+ _renderTimedEventDay(event) {
980
+ const start = new Date(event.start);
981
+ const end = new Date(event.end);
982
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
983
+ const durationMinutes = Math.max((end - start) / (1000 * 60), 30);
984
+ const color = event.backgroundColor || '#2563eb';
985
+
986
+ return `
987
+ <div class="fc-event" data-event-id="${event.id}"
988
+ style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 12px; right: 24px;
989
+ background-color: ${color}; border-radius: 6px; padding: 8px 12px; font-size: 13px;
990
+ font-weight: 500; color: white; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
991
+ cursor: pointer; z-index: 5;">
992
+ <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${event.title}</div>
993
+ <div style="font-size: 11px; opacity: 0.9;">${this._formatTime(start)} - ${this._formatTime(end)}</div>
994
+ </div>
995
+ `;
996
+ },
997
+
998
+ _renderNowIndicator() {
999
+ const now = new Date();
1000
+ const minutes = now.getHours() * 60 + now.getMinutes();
1001
+ return `<div style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
1002
+ },
1003
+
1004
+ _formatHour(hour) {
1005
+ const period = hour >= 12 ? 'PM' : 'AM';
1006
+ const displayHour = hour % 12 || 12;
1007
+ return `${displayHour} ${period}`;
1008
+ },
1009
+
1010
+ _formatTime(date) {
1011
+ const hours = date.getHours();
1012
+ const minutes = date.getMinutes();
1013
+ const period = hours >= 12 ? 'PM' : 'AM';
1014
+ const displayHour = hours % 12 || 12;
1015
+ return minutes === 0 ? `${displayHour} ${period}` : `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
1016
+ },
1017
+
1018
+ _isToday(date) {
1019
+ const today = new Date();
1020
+ return date.getDate() === today.getDate() &&
1021
+ date.getMonth() === today.getMonth() &&
1022
+ date.getFullYear() === today.getFullYear();
1023
+ },
1024
+
1025
+ _isSameDay(date1, date2) {
1026
+ return date1.getDate() === date2.getDate() &&
1027
+ date1.getMonth() === date2.getMonth() &&
1028
+ date1.getFullYear() === date2.getFullYear();
1029
+ },
1030
+
1031
+ _attachEventHandlers(viewType) {
616
1032
  const stateManager = this.stateManager;
1033
+ const self = this;
617
1034
 
618
- // Day click handlers
1035
+ // Day click handlers (for month view)
619
1036
  this.container.querySelectorAll('.fc-month-day').forEach(dayEl => {
620
1037
  this.addListener(dayEl, 'click', (e) => {
621
1038
  const date = new Date(dayEl.dataset.date);
@@ -623,6 +1040,32 @@ export class ForceCalendar extends BaseComponent {
623
1040
  });
624
1041
  });
625
1042
 
1043
+ // Week view day column click handlers
1044
+ this.container.querySelectorAll('.fc-week-day-column').forEach(dayEl => {
1045
+ this.addListener(dayEl, 'click', (e) => {
1046
+ if (e.target.closest('.fc-event')) return;
1047
+ const date = new Date(dayEl.dataset.date);
1048
+ const rect = dayEl.getBoundingClientRect();
1049
+ const scrollContainer = this.container.querySelector('#week-scroll-container');
1050
+ const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
1051
+ date.setHours(Math.floor(y / 60), Math.floor(y % 60), 0, 0);
1052
+ stateManager.selectDate(date);
1053
+ });
1054
+ });
1055
+
1056
+ // Day view column click handlers
1057
+ this.container.querySelectorAll('.fc-day-column').forEach(dayEl => {
1058
+ this.addListener(dayEl, 'click', (e) => {
1059
+ if (e.target.closest('.fc-event')) return;
1060
+ const date = new Date(dayEl.dataset.date);
1061
+ const rect = dayEl.getBoundingClientRect();
1062
+ const scrollContainer = this.container.querySelector('#day-scroll-container');
1063
+ const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
1064
+ date.setHours(Math.floor(y / 60), Math.floor(y % 60), 0, 0);
1065
+ stateManager.selectDate(date);
1066
+ });
1067
+ });
1068
+
626
1069
  // Event click handlers
627
1070
  this.container.querySelectorAll('.fc-event').forEach(eventEl => {
628
1071
  this.addListener(eventEl, 'click', (e) => {
@@ -634,6 +1077,16 @@ export class ForceCalendar extends BaseComponent {
634
1077
  }
635
1078
  });
636
1079
  });
1080
+
1081
+ // Scroll to 8 AM for week and day views
1082
+ if (viewType === 'week' || viewType === 'day') {
1083
+ const scrollContainerId = viewType === 'week' ? '#week-scroll-container' : '#day-scroll-container';
1084
+ const scrollContainer = this.container.querySelector(scrollContainerId);
1085
+ if (scrollContainer && !this._scrolled) {
1086
+ scrollContainer.scrollTop = 8 * 60 - 50;
1087
+ this._scrolled = true;
1088
+ }
1089
+ }
637
1090
  }
638
1091
  };
639
1092
  }