@luckydye/calendar 1.3.0 → 1.3.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.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@luckydye/calendar",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "author": "Tim Havlicek",
5
5
  "contributors": [],
6
6
  "description": "",
7
7
  "type": "module",
8
8
  "main": "dist/calendar.js",
9
- "types": "src/calendar.ts",
9
+ "types": "src/lib.ts",
10
10
  "scripts": {
11
11
  "build:app": "APP=true bunx --bun vite build --outDir=dist/calendar --base=./",
12
12
  "build": "bunx --bun vite build",
@@ -44,7 +44,20 @@ interface InhouseSource extends CalendarSource {
44
44
  locked?: boolean;
45
45
  }
46
46
 
47
- type ConfigurableSource = CalDAVSourceConfig | ICalSource | GoogleSource | InhouseSource;
47
+ interface TimeseriesJsonSource extends CalendarSource {
48
+ type: "timeseries-json";
49
+ credentials: {
50
+ url: string;
51
+ };
52
+ locked?: boolean;
53
+ }
54
+
55
+ type ConfigurableSource =
56
+ | CalDAVSourceConfig
57
+ | ICalSource
58
+ | GoogleSource
59
+ | InhouseSource
60
+ | TimeseriesJsonSource;
48
61
 
49
62
  interface SidebarCalendar {
50
63
  id: string;
@@ -621,6 +634,22 @@ export class CalDAVConfigElement extends LitElement {
621
634
  enabled: this.formData.enabled ?? true,
622
635
  locked: this.formData.locked ?? false,
623
636
  } as ICalSource;
637
+ } else if (this.formData.type === "timeseries-json") {
638
+ if (!this.formData.credentials?.url) {
639
+ return;
640
+ }
641
+
642
+ source = {
643
+ id: this.editingId || crypto.randomUUID(),
644
+ name: this.formData.name,
645
+ type: "timeseries-json",
646
+ credentials: {
647
+ url: this.formData.credentials.url,
648
+ },
649
+ color: this.formData.color || "#06B6D4",
650
+ enabled: this.formData.enabled ?? true,
651
+ locked: this.formData.locked ?? false,
652
+ } as TimeseriesJsonSource;
624
653
  } else if (this.formData.type === "google") {
625
654
  if (!this.formData.credentials?.accessToken) {
626
655
  alert('Please authenticate with Google before adding the calendar');
@@ -899,12 +928,15 @@ export class CalDAVConfigElement extends LitElement {
899
928
  this.updateForm("color", "#4285F4");
900
929
  } else if (type === "caldav" && !this.formData.color) {
901
930
  this.updateForm("color", "#FF6E68");
931
+ } else if (type === "timeseries-json" && !this.formData.color) {
932
+ this.updateForm("color", "#06B6D4");
902
933
  }
903
934
  }}
904
935
  ?disabled=${isEditing}
905
936
  >
906
937
  <option value="caldav">CalDAV (with credentials)</option>
907
938
  <option value="ical">iCal URL</option>
939
+ <option value="timeseries-json">Timeseries JSON (URL)</option>
908
940
  <option value="google">Google Calendar</option>
909
941
  <option value="inhouse">Inhouse Booking System</option>
910
942
  </select>
@@ -972,6 +1004,23 @@ export class CalDAVConfigElement extends LitElement {
972
1004
  />
973
1005
  </div>
974
1006
  `
1007
+ : sourceType === "timeseries-json"
1008
+ ? html`
1009
+ <div class="form-group">
1010
+ <label class="form-label">Timeseries JSON URL</label>
1011
+ <input
1012
+ class="form-input"
1013
+ type="text"
1014
+ placeholder="https://example.com/data.json"
1015
+ .value=${this.formData.credentials?.url || ""}
1016
+ @input=${(e: Event) =>
1017
+ this.updateForm("url", (e.target as HTMLInputElement).value)}
1018
+ />
1019
+ <small style="color: var(--text-muted, rgba(255, 255, 255, 0.5)); font-size: 11px;">
1020
+ Expected format: JSON array of objects with a <code>timestamp</code> field. Rendered as a heatmap backdrop.
1021
+ </small>
1022
+ </div>
1023
+ `
975
1024
  : sourceType === "google"
976
1025
  ? html`
977
1026
  <div class="google-auth-section">
@@ -49,6 +49,7 @@ export interface CalendarEvent {
49
49
  status?: 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED';
50
50
  reminders?: NotificationConfig[];
51
51
  isAllDay?: boolean;
52
+ visualStyle?: 'heatmap';
52
53
  }
53
54
 
54
55
  export interface WeekInfo {
@@ -72,8 +73,9 @@ export interface EventSegment {
72
73
  }
73
74
 
74
75
  // Configuration for the sliding window buffer
75
- const BUFFER_WEEKS = 12; // Weeks to keep as buffer outside viewport
76
- const EXTEND_WEEKS = 12; // Weeks to add when extending
76
+ const BUFFER_WEEKS = 4; // Trigger extension when within this many weeks of the edge
77
+ const EXTEND_WEEKS = 26; // Weeks to add per extension (~6 months); must be >> BUFFER_WEEKS so
78
+ // the scroll thumb lands well past the buffer entry after each extension, preventing cascade
77
79
  const MAX_WEEKS = 96; // Maximum weeks to keep in memory (trim beyond this)
78
80
 
79
81
  export class CalendarInternal {
@@ -258,18 +260,21 @@ export class CalendarInternal {
258
260
 
259
261
  getFilteredEvents(filter?: string) {
260
262
  const baseEvents = Array.from(this.calendarEvents.values());
263
+ return this.filterEvents(baseEvents, filter);
264
+ }
261
265
 
266
+ filterEvents(events: CalendarEvent[], filter?: string): CalendarEvent[] {
262
267
  // Filter by enabled calendars
263
268
  // Note: enabledCalendars contains calendar IDs, not sourceIds
264
269
  const enabledEvents = this.enabledCalendars.size > 0
265
- ? baseEvents.filter(e => {
270
+ ? events.filter(e => {
266
271
  // For backwards compatibility: check both sourceId and calendarId
267
272
  // CalDAV events: match via calendarId
268
273
  // Other sources: match via sourceId
269
274
  return (e.calendarId && this.enabledCalendars.has(e.calendarId)) ||
270
275
  (e.sourceId && this.enabledCalendars.has(e.sourceId));
271
276
  })
272
- : baseEvents;
277
+ : events;
273
278
 
274
279
  // Mark events from locked calendars as read-only
275
280
  const lockedEvents = this.lockedCalendars.size > 0
@@ -0,0 +1,28 @@
1
+ import type { WeekInfo } from "./CalendarInternal.js";
2
+
3
+ // Minimum dayHeight at which timed events are rendered at their actual time-of-day position.
4
+ export const TIME_SCALE_DAY_HEIGHT = 300;
5
+
6
+ export interface LayerContext {
7
+ ctx: CanvasRenderingContext2D;
8
+ width: number;
9
+ height: number;
10
+ scrollTop: number;
11
+ dayWidth: number;
12
+ dayHeight: number;
13
+ leftGutterWidth: number;
14
+ columnsPerRow: number;
15
+ rowsPerWeek: number;
16
+ visibleWeeks: WeekInfo[];
17
+ allWeeks: WeekInfo[];
18
+ fontFamily: string;
19
+ styles: Record<string, string>;
20
+ getDayVisualPosition: (dayIndex: number) => { row: number; col: number };
21
+ filter: string | null;
22
+ }
23
+
24
+ export interface CalendarLayer {
25
+ name: string;
26
+ enabled: boolean;
27
+ render(lc: LayerContext): void;
28
+ }