@bimetal/calendar-svelte-components 0.29.0

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.
Files changed (87) hide show
  1. package/LICENSE +129 -0
  2. package/dist/binding/config.d.ts +6 -0
  3. package/dist/binding/config.d.ts.map +1 -0
  4. package/dist/binding/config.js +5 -0
  5. package/dist/binding/context.d.ts +31 -0
  6. package/dist/binding/context.d.ts.map +1 -0
  7. package/dist/binding/context.js +55 -0
  8. package/dist/binding/controller.d.ts +24 -0
  9. package/dist/binding/controller.d.ts.map +1 -0
  10. package/dist/binding/controller.js +9 -0
  11. package/dist/binding/index.d.ts +23 -0
  12. package/dist/binding/index.d.ts.map +1 -0
  13. package/dist/binding/index.js +24 -0
  14. package/dist/binding/types.d.ts +95 -0
  15. package/dist/binding/types.d.ts.map +1 -0
  16. package/dist/binding/types.js +11 -0
  17. package/dist/binding/view-configs.d.ts +6 -0
  18. package/dist/binding/view-configs.d.ts.map +1 -0
  19. package/dist/binding/view-configs.js +5 -0
  20. package/dist/components/Calendar.svelte +263 -0
  21. package/dist/components/Calendar.svelte.d.ts +24 -0
  22. package/dist/components/Calendar.svelte.d.ts.map +1 -0
  23. package/dist/components/CalendarHeader.svelte +47 -0
  24. package/dist/components/CalendarHeader.svelte.d.ts +15 -0
  25. package/dist/components/CalendarHeader.svelte.d.ts.map +1 -0
  26. package/dist/components/CategoryFilter.svelte +29 -0
  27. package/dist/components/CategoryFilter.svelte.d.ts +10 -0
  28. package/dist/components/CategoryFilter.svelte.d.ts.map +1 -0
  29. package/dist/components/EventBar.svelte +187 -0
  30. package/dist/components/EventBar.svelte.d.ts +10 -0
  31. package/dist/components/EventBar.svelte.d.ts.map +1 -0
  32. package/dist/components/EventDialog.svelte +312 -0
  33. package/dist/components/EventDialog.svelte.d.ts +15 -0
  34. package/dist/components/EventDialog.svelte.d.ts.map +1 -0
  35. package/dist/components/EventHistory.svelte +89 -0
  36. package/dist/components/EventHistory.svelte.d.ts +10 -0
  37. package/dist/components/EventHistory.svelte.d.ts.map +1 -0
  38. package/dist/components/ResourceFilter.svelte +28 -0
  39. package/dist/components/ResourceFilter.svelte.d.ts +10 -0
  40. package/dist/components/ResourceFilter.svelte.d.ts.map +1 -0
  41. package/dist/components/UndoToast.svelte +47 -0
  42. package/dist/components/UndoToast.svelte.d.ts +4 -0
  43. package/dist/components/UndoToast.svelte.d.ts.map +1 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +23 -0
  47. package/dist/styles/index.css +1 -0
  48. package/dist/views/agenda/AgendaEvent.svelte +55 -0
  49. package/dist/views/agenda/AgendaEvent.svelte.d.ts +14 -0
  50. package/dist/views/agenda/AgendaEvent.svelte.d.ts.map +1 -0
  51. package/dist/views/agenda/AgendaView.svelte +155 -0
  52. package/dist/views/agenda/AgendaView.svelte.d.ts +8 -0
  53. package/dist/views/agenda/AgendaView.svelte.d.ts.map +1 -0
  54. package/dist/views/day/AllDayEvent.svelte +49 -0
  55. package/dist/views/day/AllDayEvent.svelte.d.ts +12 -0
  56. package/dist/views/day/AllDayEvent.svelte.d.ts.map +1 -0
  57. package/dist/views/day/DayView.svelte +252 -0
  58. package/dist/views/day/DayView.svelte.d.ts +8 -0
  59. package/dist/views/day/DayView.svelte.d.ts.map +1 -0
  60. package/dist/views/month/MonthView.svelte +66 -0
  61. package/dist/views/month/MonthView.svelte.d.ts +8 -0
  62. package/dist/views/month/MonthView.svelte.d.ts.map +1 -0
  63. package/dist/views/month/TimedEntry.svelte +51 -0
  64. package/dist/views/month/TimedEntry.svelte.d.ts +12 -0
  65. package/dist/views/month/TimedEntry.svelte.d.ts.map +1 -0
  66. package/dist/views/month/WeekRow.svelte +224 -0
  67. package/dist/views/month/WeekRow.svelte.d.ts +12 -0
  68. package/dist/views/month/WeekRow.svelte.d.ts.map +1 -0
  69. package/dist/views/resource/ResourceView.svelte +227 -0
  70. package/dist/views/resource/ResourceView.svelte.d.ts +11 -0
  71. package/dist/views/resource/ResourceView.svelte.d.ts.map +1 -0
  72. package/dist/views/timeline/TimelineView.svelte +178 -0
  73. package/dist/views/timeline/TimelineView.svelte.d.ts +12 -0
  74. package/dist/views/timeline/TimelineView.svelte.d.ts.map +1 -0
  75. package/dist/views/week/AllDayRow.svelte +74 -0
  76. package/dist/views/week/AllDayRow.svelte.d.ts +9 -0
  77. package/dist/views/week/AllDayRow.svelte.d.ts.map +1 -0
  78. package/dist/views/week/TimeEvent.svelte +192 -0
  79. package/dist/views/week/TimeEvent.svelte.d.ts +25 -0
  80. package/dist/views/week/TimeEvent.svelte.d.ts.map +1 -0
  81. package/dist/views/week/WeekView.svelte +232 -0
  82. package/dist/views/week/WeekView.svelte.d.ts +8 -0
  83. package/dist/views/week/WeekView.svelte.d.ts.map +1 -0
  84. package/dist/views/year/YearView.svelte +92 -0
  85. package/dist/views/year/YearView.svelte.d.ts +8 -0
  86. package/dist/views/year/YearView.svelte.d.ts.map +1 -0
  87. package/package.json +56 -0
@@ -0,0 +1,155 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
3
+ import {
4
+ addDays, isSameDay, startOfDay, isBefore, isAfter,
5
+ getDayOfMonth,
6
+ createTimeRange, overlaps, formatDateTime, now,
7
+ getRecurrenceInstanceMetadata, isSingleDayAllDay,
8
+ } from '@bimetal/calendar-core';
9
+ import { getCalendarContext, getCalendarConfig, toDateStr, toTimeStr } from '../../binding/index.js';
10
+ import { DEFAULT_AGENDA_RANGE_DAYS, createClickIntent } from '@bimetal/calendar-headless';
11
+ import AgendaEvent from './AgendaEvent.svelte';
12
+
13
+ interface DayGroup {
14
+ date: CalendarDateTime;
15
+ dateStr: string;
16
+ events: CalendarEvent[];
17
+ }
18
+
19
+ let {
20
+ currentDate,
21
+ }: {
22
+ currentDate: CalendarDateTime;
23
+ } = $props();
24
+
25
+ const config = getCalendarConfig();
26
+ const ctx = getCalendarContext<{
27
+ events: CalendarEvent[];
28
+ onEventClick: (eventId: string, anchorRect: DOMRect, instanceDate?: number) => void;
29
+ onSelectEvent: (eventId: string, instanceDate?: number) => void;
30
+ selectedEventId: string | null;
31
+ selectedInstanceDate: number | undefined;
32
+ }>();
33
+
34
+ const click = createClickIntent();
35
+
36
+ const rangeStartDt = $derived(startOfDay(currentDate));
37
+ const rangeEndDt = $derived(addDays(rangeStartDt, DEFAULT_AGENDA_RANGE_DAYS));
38
+ const agendaRange = $derived(createTimeRange(rangeStartDt, rangeEndDt));
39
+
40
+ const currentNow = $derived(now(config.core.timezone, config.core.clock));
41
+
42
+ const dayGroups = $derived.by((): DayGroup[] => {
43
+ const groups = new Map<string, CalendarEvent[]>();
44
+
45
+ for (const ev of ctx.events) {
46
+ if (!overlaps(ev.timeRange, agendaRange)) continue;
47
+
48
+ const evStart = isBefore(ev.timeRange.start, rangeStartDt) ? rangeStartDt : ev.timeRange.start;
49
+ const evEnd = isAfter(ev.timeRange.end, rangeEndDt) ? rangeEndDt : ev.timeRange.end;
50
+
51
+ let d = startOfDay(evStart);
52
+ const end = startOfDay(evEnd);
53
+ const limit = addDays(end, 1);
54
+ while (isBefore(d, limit) && isBefore(d, rangeEndDt)) {
55
+ const ds = toDateStr(d);
56
+ const list = groups.get(ds) || [];
57
+ list.push(ev);
58
+ groups.set(ds, list);
59
+ d = addDays(d, 1);
60
+ }
61
+ }
62
+
63
+ const result: DayGroup[] = [];
64
+ for (let i = 0; i < DEFAULT_AGENDA_RANGE_DAYS; i++) {
65
+ const date = addDays(rangeStartDt, i);
66
+ const dateStr = toDateStr(date);
67
+ const dayEvents = groups.get(dateStr);
68
+ if (dayEvents && dayEvents.length > 0) {
69
+ dayEvents.sort((a, b) => {
70
+ if (a.allDay !== b.allDay) return a.allDay ? -1 : 1;
71
+ return a.timeRange.start.epochMs - b.timeRange.start.epochMs;
72
+ });
73
+ result.push({ date, dateStr, events: dayEvents });
74
+ }
75
+ }
76
+ return result;
77
+ });
78
+
79
+ function handleMouseDown() {
80
+ click.markMouseDown();
81
+ }
82
+
83
+ function handleEventClick(e: MouseEvent, eventId: string, instanceDate?: number) {
84
+ if (!click.accept(config.clickThresholdMs)) return;
85
+ ctx.onSelectEvent(eventId, instanceDate);
86
+ }
87
+
88
+ function handleEventDoubleClick(e: MouseEvent, eventId: string, instanceDate?: number) {
89
+ if (!click.accept(config.clickThresholdMs)) return;
90
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
91
+ ctx.onEventClick(eventId, rect, instanceDate);
92
+ }
93
+
94
+ function isEventSelected(ev: CalendarEvent): boolean {
95
+ const id = getRecurrenceInstanceMetadata(ev)?.instanceDate;
96
+ return ctx.selectedEventId === ev.id && ctx.selectedInstanceDate === id;
97
+ }
98
+
99
+ function formatEventTime(ev: CalendarEvent, date: CalendarDateTime): string {
100
+ if (ev.allDay) {
101
+ if (isSingleDayAllDay(ev.timeRange)) {
102
+ return config.locale.allDayEvent;
103
+ }
104
+ const startStr = formatDateTime(ev.timeRange.start, { locale: config.locale.code, dateStyle: 'short' });
105
+ const endStr = formatDateTime(addDays(ev.timeRange.end, -1), { locale: config.locale.code, dateStyle: 'short' });
106
+ return `${config.locale.allDayEvent} (${startStr} \u2013 ${endStr})`;
107
+ }
108
+ const isFirst = isSameDay(ev.timeRange.start, date);
109
+ const isLast = isSameDay(ev.timeRange.end, date);
110
+ const startTime = toTimeStr(ev.timeRange.start);
111
+ const endTime = toTimeStr(ev.timeRange.end);
112
+ if (isFirst && isLast) return `${startTime} \u2013 ${endTime}`;
113
+ if (isFirst) return `${config.locale.timeFrom} ${startTime}`;
114
+ if (isLast) return `${config.locale.timeUntil} ${endTime}`;
115
+ return config.locale.allDayEvent;
116
+ }
117
+ </script>
118
+
119
+ <div class="agenda-view">
120
+ {#if dayGroups.length === 0}
121
+ <div class="agenda-view__empty">{config.locale.noEvents}</div>
122
+ {:else}
123
+ {#each dayGroups as group (group.dateStr)}
124
+ {@const isGroupToday = isSameDay(group.date, currentNow)}
125
+ <div class="agenda-view__day">
126
+ <div class="agenda-view__date-header{isGroupToday ? ' agenda-view__date-header--today' : ''}">
127
+ <span class="agenda-view__date-number{isGroupToday ? ' agenda-view__date-number--today' : ''}">
128
+ {getDayOfMonth(group.date)}
129
+ </span>
130
+ <div class="agenda-view__date-text">
131
+ <span class="agenda-view__date-weekday">
132
+ {formatDateTime(group.date, { locale: config.locale.code, dateStyle: 'full' }).split(',')[0]}
133
+ </span>
134
+ <span class="agenda-view__date-full">
135
+ {formatDateTime(group.date, { locale: config.locale.code, dateStyle: 'long' }).replace(/\d+\.?\s*/, '')}
136
+ </span>
137
+ </div>
138
+ </div>
139
+ <div class="agenda-view__events">
140
+ {#each group.events as ev (`${ev.id}-${group.dateStr}`)}
141
+ <AgendaEvent
142
+ {ev}
143
+ timeLabel={formatEventTime(ev, group.date)}
144
+ category={(ev.metadata.category as string | undefined) ?? 'default'}
145
+ isSelected={isEventSelected(ev)}
146
+ onEntryMouseDown={handleMouseDown}
147
+ onEntryClick={handleEventClick}
148
+ onEntryDoubleClick={handleEventDoubleClick}
149
+ />
150
+ {/each}
151
+ </div>
152
+ </div>
153
+ {/each}
154
+ {/if}
155
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { CalendarDateTime } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ currentDate: CalendarDateTime;
4
+ };
5
+ declare const AgendaView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type AgendaView = ReturnType<typeof AgendaView>;
7
+ export default AgendaView;
8
+ //# sourceMappingURL=AgendaView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgendaView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/agenda/AgendaView.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAiB7E,KAAK,gBAAgB,GAAI;IACtB,WAAW,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AA2IJ,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent } from '@bimetal/calendar-core';
3
+ import { getRecurrenceInstanceMetadata } from '@bimetal/calendar-core';
4
+ import { getEventSlots } from '../../binding/index.js';
5
+
6
+ let {
7
+ ev,
8
+ isSelected,
9
+ onEntryMouseDown,
10
+ onEntryClick,
11
+ onEntryDoubleClick,
12
+ }: {
13
+ ev: CalendarEvent;
14
+ isSelected: boolean;
15
+ onEntryMouseDown: () => void;
16
+ onEntryClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
17
+ onEntryDoubleClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
18
+ } = $props();
19
+
20
+ const slots = $derived(getEventSlots(ev));
21
+ const renderedTitle = $derived(slots.title ?? ev.title);
22
+ const extraClass = $derived(slots.className ? ' ' + slots.className : '');
23
+ const slotStyleStr = $derived(slots.style
24
+ ? Object.entries(slots.style).map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`).join('; ')
25
+ : '');
26
+ const baseStyle = $derived(`background-color: ${ev.metadata.color ?? '#007AFF'};`);
27
+ const fullStyle = $derived(slotStyleStr ? `${baseStyle} ${slotStyleStr}` : baseStyle);
28
+ </script>
29
+
30
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
31
+ <div
32
+ class="day-view__allday-event{isSelected ? ' day-view__allday-event--selected' : ''}{extraClass}"
33
+ style={fullStyle}
34
+ role="button"
35
+ tabindex="0"
36
+ onmousedown={onEntryMouseDown}
37
+ onclick={(e) => onEntryClick(e, ev.id, getRecurrenceInstanceMetadata(ev)?.instanceDate)}
38
+ ondblclick={(e) => onEntryDoubleClick(e, ev.id, getRecurrenceInstanceMetadata(ev)?.instanceDate)}
39
+ title={renderedTitle}
40
+ >
41
+ {#if slots.content}
42
+ {#if typeof slots.content === 'string'}{slots.content}{:else}{@render slots.content()}{/if}
43
+ {:else}
44
+ {renderedTitle}
45
+ {/if}
46
+ {#if slots.badges}
47
+ {#if typeof slots.badges === 'string'}{slots.badges}{:else}{@render slots.badges()}{/if}
48
+ {/if}
49
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { CalendarEvent } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ ev: CalendarEvent;
4
+ isSelected: boolean;
5
+ onEntryMouseDown: () => void;
6
+ onEntryClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
7
+ onEntryDoubleClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
8
+ };
9
+ declare const AllDayEvent: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type AllDayEvent = ReturnType<typeof AllDayEvent>;
11
+ export default AllDayEvent;
12
+ //# sourceMappingURL=AllDayEvent.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AllDayEvent.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/day/AllDayEvent.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAI3D,KAAK,gBAAgB,GAAI;IACtB,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,kBAAkB,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF,CAAC;AAsCJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,252 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
3
+ import {
4
+ isSameDay, now, getDayOfMonth, startOfDay, addDays,
5
+ getHours, getMinutes,
6
+ createTimeRange, overlaps, formatDateTime,
7
+ segmentTimed, layoutTimed,
8
+ getRecurrenceInstanceMetadata,
9
+ } from '@bimetal/calendar-core';
10
+ import { getCalendarContext, getCalendarConfig, snapTimeFromMouseY, toDateStr, parseDateTimeStr } from '../../binding/index.js';
11
+ import { DEFAULT_HOUR_HEIGHT, DEFAULT_TIME_LABEL_WIDTH_DAY, DEFAULT_SCROLL_HOUR, createClickIntent } from '@bimetal/calendar-headless';
12
+ import { onMount } from 'svelte';
13
+ import TimeEvent from '../week/TimeEvent.svelte';
14
+ import AllDayEvent from './AllDayEvent.svelte';
15
+
16
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
17
+
18
+ let {
19
+ currentDate,
20
+ }: {
21
+ currentDate: CalendarDateTime;
22
+ } = $props();
23
+
24
+ const config = getCalendarConfig();
25
+ const ctx = getCalendarContext<{
26
+ events: CalendarEvent[];
27
+ previewEvent: CalendarEvent | null;
28
+ onDragOverDateTime: (date: CalendarDateTime, time: string) => void;
29
+ onDropTimedEvent: (e: DragEvent, dropDate: CalendarDateTime, dropTime: string) => void;
30
+ onHidePreview: () => void;
31
+ onDayCellClick: (date: CalendarDateTime, anchorRect: DOMRect) => void;
32
+ onEventClick: (eventId: string, anchorRect: DOMRect, instanceDate?: number) => void;
33
+ onSelectEvent: (eventId: string, instanceDate?: number) => void;
34
+ onClearSelection: () => void;
35
+ selectedEventId: string | null;
36
+ selectedInstanceDate: number | undefined;
37
+ }>();
38
+
39
+ let timeGridEl: HTMLDivElement | undefined = $state();
40
+ let gridInnerEl: HTMLDivElement | undefined = $state();
41
+ const click = createClickIntent();
42
+ const allDayClick = createClickIntent();
43
+ const timeSnapMinutes = config.core.granularity.day.minutes;
44
+
45
+ function scrollTo8AM() {
46
+ if (timeGridEl) {
47
+ timeGridEl.scrollTop = DEFAULT_SCROLL_HOUR * DEFAULT_HOUR_HEIGHT;
48
+ }
49
+ }
50
+
51
+ onMount(scrollTo8AM);
52
+
53
+ let lastDateEpoch = 0;
54
+ $effect(() => {
55
+ const dateEpoch = currentDate.epochMs;
56
+ if (lastDateEpoch !== 0 && lastDateEpoch !== dateEpoch) {
57
+ scrollTo8AM();
58
+ }
59
+ lastDateEpoch = dateEpoch;
60
+ });
61
+
62
+ const dayStart = $derived(startOfDay(currentDate));
63
+ const dayEnd = $derived(addDays(dayStart, 1));
64
+ const dayRange = $derived(createTimeRange(dayStart, dayEnd));
65
+
66
+ // B-CAL-1: das vom Controller neu gebaute Event per id eintauschen (kein
67
+ // Parsing in der View).
68
+ const displayEvents = $derived.by((): CalendarEvent[] => {
69
+ const pv = ctx.previewEvent;
70
+ const evts = ctx.events;
71
+ if (!pv) return evts;
72
+ return evts.map(ev => ev.id === pv.id ? { ...ev, timeRange: pv.timeRange } : ev);
73
+ });
74
+
75
+ const allDayEvents = $derived(displayEvents.filter(ev => ev.allDay && overlaps(ev.timeRange, dayRange)));
76
+ const timedEvents = $derived(displayEvents.filter(ev => !ev.allDay && overlaps(ev.timeRange, dayRange)));
77
+
78
+ // Lane-Layout durch den Core (Pilot-Direktive 2026-06-21).
79
+ const timedLayout = $derived(layoutTimed(segmentTimed(timedEvents, currentDate)));
80
+
81
+ const currentNow = $derived(now(config.core.timezone, config.core.clock));
82
+ const isToday = $derived(isSameDay(currentDate, currentNow));
83
+ const dayName = $derived(
84
+ formatDateTime(currentDate, { locale: config.locale.code, dateStyle: 'full' }).split(',')[0]
85
+ );
86
+ const nowHours = $derived(getHours(currentNow) + getMinutes(currentNow) / 60);
87
+
88
+ function getTimeFromMouseY(clientY: number): string {
89
+ if (!gridInnerEl) return '00:00';
90
+ return snapTimeFromMouseY(clientY, gridInnerEl.getBoundingClientRect(), DEFAULT_HOUR_HEIGHT, timeSnapMinutes);
91
+ }
92
+
93
+ function handleGridDragOver(e: DragEvent) {
94
+ e.preventDefault();
95
+ e.dataTransfer!.dropEffect = 'move';
96
+ const time = getTimeFromMouseY(e.clientY);
97
+ ctx.onDragOverDateTime(currentDate, time);
98
+ }
99
+
100
+ function handleGridDrop(e: DragEvent) {
101
+ e.preventDefault();
102
+ const time = getTimeFromMouseY(e.clientY);
103
+ ctx.onDropTimedEvent(e, currentDate, time);
104
+ }
105
+
106
+ function handleGridDragLeave(e: DragEvent) {
107
+ const el = e.currentTarget as HTMLElement;
108
+ const rect = el.getBoundingClientRect();
109
+ if (e.clientX < rect.left || e.clientX > rect.right ||
110
+ e.clientY < rect.top || e.clientY > rect.bottom) {
111
+ ctx.onHidePreview();
112
+ }
113
+ }
114
+
115
+ function handleGridMouseDown() {
116
+ click.markMouseDown();
117
+ }
118
+
119
+ function handleGridClick(e: MouseEvent) {
120
+ if (!click.accept(config.clickThresholdMs)) return;
121
+ if ((e.target as HTMLElement).closest('.time-event')) return;
122
+ // Single click on empty grid deselects; create-dialog moves to dblclick.
123
+ if (ctx.selectedEventId !== null) ctx.onClearSelection();
124
+ }
125
+
126
+ function handleGridDoubleClick(e: MouseEvent) {
127
+ if (!click.accept(config.clickThresholdMs)) return;
128
+ if ((e.target as HTMLElement).closest('.time-event')) return;
129
+ const time = getTimeFromMouseY(e.clientY);
130
+ // Tag + gesnapte Zeit über den geteilten Parser (kein split, wie WeekView).
131
+ const clickedDt = parseDateTimeStr(toDateStr(currentDate), time, currentDate.timezone);
132
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
133
+ ctx.onDayCellClick(clickedDt, rect);
134
+ }
135
+
136
+ function handleAllDayEventMouseDown() {
137
+ allDayClick.markMouseDown();
138
+ }
139
+
140
+ function handleAllDayEventClick(e: MouseEvent, eventId: string, instanceDate?: number) {
141
+ e.stopPropagation();
142
+ if (!allDayClick.accept(config.clickThresholdMs)) return;
143
+ ctx.onSelectEvent(eventId, instanceDate);
144
+ }
145
+
146
+ function handleAllDayEventDoubleClick(e: MouseEvent, eventId: string, instanceDate?: number) {
147
+ e.stopPropagation();
148
+ if (!allDayClick.accept(config.clickThresholdMs)) return;
149
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
150
+ ctx.onEventClick(eventId, rect, instanceDate);
151
+ }
152
+
153
+ function isAllDayEventSelected(ev: CalendarEvent): boolean {
154
+ const id = getRecurrenceInstanceMetadata(ev)?.instanceDate;
155
+ return ctx.selectedEventId === ev.id && ctx.selectedInstanceDate === id;
156
+ }
157
+ </script>
158
+
159
+ <div class="day-view">
160
+ <!-- Header -->
161
+ <div class="day-view__header">
162
+ <span class="day-view__day-name">{dayName}</span>
163
+ <span class="day-view__day-number{isToday ? ' day-view__day-number--today' : ''}">
164
+ {getDayOfMonth(currentDate)}
165
+ </span>
166
+ </div>
167
+
168
+ <!-- All-day events -->
169
+ {#if allDayEvents.length > 0}
170
+ <div class="day-view__allday">
171
+ <div class="day-view__allday-label">{config.locale.allDayLabel}</div>
172
+ <div class="day-view__allday-list">
173
+ {#each allDayEvents as ev (ev.id)}
174
+ <AllDayEvent
175
+ {ev}
176
+ isSelected={isAllDayEventSelected(ev)}
177
+ onEntryMouseDown={handleAllDayEventMouseDown}
178
+ onEntryClick={handleAllDayEventClick}
179
+ onEntryDoubleClick={handleAllDayEventDoubleClick}
180
+ />
181
+ {/each}
182
+ </div>
183
+ </div>
184
+ {/if}
185
+
186
+ <!-- Time grid -->
187
+ <div
188
+ bind:this={timeGridEl}
189
+ class="day-view__time-grid"
190
+ role="grid"
191
+ tabindex="0"
192
+ ondragover={handleGridDragOver}
193
+ ondrop={handleGridDrop}
194
+ ondragleave={handleGridDragLeave}
195
+ onmousedown={handleGridMouseDown}
196
+ onclick={handleGridClick}
197
+ ondblclick={handleGridDoubleClick}
198
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleGridDoubleClick(e as any); }}
199
+ >
200
+ <div
201
+ bind:this={gridInnerEl}
202
+ class="day-view__time-grid-inner"
203
+ style="height: {24 * DEFAULT_HOUR_HEIGHT}px;"
204
+ >
205
+ <!-- Hour rows -->
206
+ {#each HOURS as hr (hr)}
207
+ {@const wh = config.core.workingHours}
208
+ {@const isOff = wh ? (hr < wh.start || hr >= wh.end) : false}
209
+ <div
210
+ class="day-view__hour-row{isOff ? ' hour-slot--off' : ''}"
211
+ style="top: {hr * DEFAULT_HOUR_HEIGHT}px; height: {DEFAULT_HOUR_HEIGHT}px;"
212
+ >
213
+ <div class="day-view__hour-label">
214
+ {hr > 0 ? `${String(hr).padStart(2, '0')}:00` : ''}
215
+ </div>
216
+ </div>
217
+ {/each}
218
+
219
+ <!-- Day column -->
220
+ <div
221
+ class="day-view__day-col"
222
+ style="left: {DEFAULT_TIME_LABEL_WIDTH_DAY}px; width: calc(100% - {DEFAULT_TIME_LABEL_WIDTH_DAY}px);"
223
+ ></div>
224
+
225
+ <!-- Timed events -->
226
+ {#each timedLayout as item (`${item.segment.event.id}-${item.column}`)}
227
+ <TimeEvent
228
+ event={item.segment.event}
229
+ layout={{
230
+ dayCol: 0,
231
+ dayCount: 1,
232
+ lane: item.column,
233
+ laneCount: item.totalColumns,
234
+ startOffset: item.startOffset,
235
+ endOffset: item.endOffset,
236
+ }}
237
+ dayDate={currentDate}
238
+ hourHeight={DEFAULT_HOUR_HEIGHT}
239
+ timeLabelWidth={DEFAULT_TIME_LABEL_WIDTH_DAY}
240
+ ghost={ctx.previewEvent?.id === item.segment.event.id}
241
+ />
242
+ {/each}
243
+
244
+ <!-- Now line -->
245
+ {#if isToday}
246
+ <div class="day-view__now-line" style="top: {nowHours * DEFAULT_HOUR_HEIGHT}px;">
247
+ <div class="day-view__now-dot"></div>
248
+ </div>
249
+ {/if}
250
+ </div>
251
+ </div>
252
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { CalendarDateTime } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ currentDate: CalendarDateTime;
4
+ };
5
+ declare const DayView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type DayView = ReturnType<typeof DayView>;
7
+ export default DayView;
8
+ //# sourceMappingURL=DayView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DayView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/day/DayView.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAc7E,KAAK,gBAAgB,GAAI;IACtB,WAAW,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAqNJ,QAAA,MAAM,OAAO,sDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent } from '@bimetal/calendar-core';
3
+ import {
4
+ monthGrid, segmentAllDay, layoutAllDay, startOfDay,
5
+ } from '@bimetal/calendar-core';
6
+ import { getCalendarContext, getCalendarConfig } from '../../binding/index.js';
7
+ import WeekRow from './WeekRow.svelte';
8
+
9
+ let {
10
+ year,
11
+ month,
12
+ }: {
13
+ year: number;
14
+ month: number;
15
+ } = $props();
16
+
17
+ const config = getCalendarConfig();
18
+ const ctx = getCalendarContext<{
19
+ events: CalendarEvent[];
20
+ previewEvent: CalendarEvent | null;
21
+ }>();
22
+
23
+ const weeks = $derived(monthGrid(year, month, config.core));
24
+
25
+ // B-CAL-1: das vom Controller neu gebaute Event per id eintauschen (kein
26
+ // Parsing in der View).
27
+ const displayEvents = $derived.by((): CalendarEvent[] => {
28
+ const pv = ctx.previewEvent;
29
+ const evts = ctx.events;
30
+ if (!pv) return evts;
31
+ return evts.map(ev => ev.id === pv.id ? { ...ev, timeRange: pv.timeRange } : ev);
32
+ });
33
+
34
+ const timedByDay = $derived.by(() => {
35
+ const map = new Map<number, CalendarEvent[]>();
36
+ for (const event of displayEvents) {
37
+ if (event.allDay) continue;
38
+ const dayMs = startOfDay(event.timeRange.start).epochMs;
39
+ const list = map.get(dayMs) ?? [];
40
+ list.push(event);
41
+ map.set(dayMs, list);
42
+ }
43
+ return map;
44
+ });
45
+ </script>
46
+
47
+ <div class="month-grid">
48
+ <div class="day-headers">
49
+ <div class="header-spacer"></div>
50
+ {#each config.locale.dayNamesShort as name (name)}
51
+ <div class="day-header">{name}</div>
52
+ {/each}
53
+ </div>
54
+
55
+ {#each weeks as weekDates, idx (idx)}
56
+ {@const segments = segmentAllDay(displayEvents, weekDates)}
57
+ {@const items = layoutAllDay(segments, weekDates)}
58
+ <WeekRow
59
+ {weekDates}
60
+ {items}
61
+ {timedByDay}
62
+ currentYear={year}
63
+ currentMonth={month}
64
+ />
65
+ {/each}
66
+ </div>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ year: number;
3
+ month: number;
4
+ };
5
+ declare const MonthView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type MonthView = ReturnType<typeof MonthView>;
7
+ export default MonthView;
8
+ //# sourceMappingURL=MonthView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MonthView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/month/MonthView.svelte.ts"],"names":[],"mappings":"AAUC,KAAK,gBAAgB,GAAI;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA2DJ,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent } from '@bimetal/calendar-core';
3
+ import { getRecurrenceInstanceMetadata } from '@bimetal/calendar-core';
4
+ import { getEventSlots, toTimeStr } from '../../binding/index.js';
5
+
6
+ let {
7
+ ev,
8
+ isSelected,
9
+ onEntryMouseDown,
10
+ onEntryClick,
11
+ onEntryDoubleClick,
12
+ }: {
13
+ ev: CalendarEvent;
14
+ isSelected: boolean;
15
+ onEntryMouseDown: (e: MouseEvent) => void;
16
+ onEntryClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
17
+ onEntryDoubleClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
18
+ } = $props();
19
+
20
+ const slots = $derived(getEventSlots(ev));
21
+ const renderedTitle = $derived(slots.title ?? ev.title);
22
+ const extraClass = $derived(slots.className ? ' ' + slots.className : '');
23
+ const slotStyleStr = $derived(slots.style
24
+ ? Object.entries(slots.style).map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`).join('; ')
25
+ : '');
26
+ </script>
27
+
28
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
29
+ <div
30
+ class="timed-entry{isSelected ? ' timed-entry--selected' : ''}{extraClass}"
31
+ style={slotStyleStr}
32
+ role="button"
33
+ tabindex="0"
34
+ onmousedown={onEntryMouseDown}
35
+ onclick={(e) => onEntryClick(e, ev.id, getRecurrenceInstanceMetadata(ev)?.instanceDate)}
36
+ ondblclick={(e) => onEntryDoubleClick(e, ev.id, getRecurrenceInstanceMetadata(ev)?.instanceDate)}
37
+ title={renderedTitle}
38
+ >
39
+ {#if slots.content}
40
+ {#if typeof slots.content === 'string'}{slots.content}{:else}{@render slots.content()}{/if}
41
+ {:else}
42
+ <span class="timed-entry__dot" style:background-color={ev.metadata.color ?? '#007AFF'}></span>
43
+ <span class="timed-entry__time">
44
+ {toTimeStr(ev.timeRange.start)}
45
+ </span>
46
+ <span class="timed-entry__title">{renderedTitle}</span>
47
+ {/if}
48
+ {#if slots.badges}
49
+ {#if typeof slots.badges === 'string'}{slots.badges}{:else}{@render slots.badges()}{/if}
50
+ {/if}
51
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { CalendarEvent } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ ev: CalendarEvent;
4
+ isSelected: boolean;
5
+ onEntryMouseDown: (e: MouseEvent) => void;
6
+ onEntryClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
7
+ onEntryDoubleClick: (e: MouseEvent, id: string, instanceDate?: number) => void;
8
+ };
9
+ declare const TimedEntry: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type TimedEntry = ReturnType<typeof TimedEntry>;
11
+ export default TimedEntry;
12
+ //# sourceMappingURL=TimedEntry.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimedEntry.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/month/TimedEntry.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAI3D,KAAK,gBAAgB,GAAI;IACtB,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,kBAAkB,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF,CAAC;AAwCJ,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}