@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,74 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime, LayoutItem } from '@bimetal/calendar-core';
3
+ import { maxLanes } from '@bimetal/calendar-core';
4
+ import { getCalendarContext, getCalendarConfig } from '../../binding/index.js';
5
+ import { colFromTimeGridX, DEFAULT_TIME_LABEL_WIDTH_WEEK } from '@bimetal/calendar-headless';
6
+ import EventBar from '../../components/EventBar.svelte';
7
+
8
+ let {
9
+ weekDates,
10
+ items,
11
+ }: {
12
+ weekDates: CalendarDateTime[];
13
+ items: LayoutItem[];
14
+ } = $props();
15
+
16
+ const config = getCalendarConfig();
17
+ const ctx = getCalendarContext<{
18
+ onDropEvent: (e: DragEvent, dropDate: CalendarDateTime) => void;
19
+ onDragOverDate: (date: CalendarDateTime) => void;
20
+ onHidePreview: () => void;
21
+ previewEvent: CalendarEvent | null;
22
+ }>();
23
+
24
+ let rowEl: HTMLDivElement | undefined = $state();
25
+
26
+ const lanes = $derived(maxLanes(items));
27
+
28
+ function handleDragOver(e: DragEvent) {
29
+ e.preventDefault();
30
+ e.dataTransfer!.dropEffect = 'move';
31
+ if (!rowEl) return;
32
+ const col = colFromTimeGridX(e.clientX, rowEl.getBoundingClientRect(), DEFAULT_TIME_LABEL_WIDTH_WEEK);
33
+ ctx.onDragOverDate(weekDates[col]);
34
+ }
35
+
36
+ function handleDrop(e: DragEvent) {
37
+ e.preventDefault();
38
+ if (!rowEl) return;
39
+ const col = colFromTimeGridX(e.clientX, rowEl.getBoundingClientRect(), DEFAULT_TIME_LABEL_WIDTH_WEEK);
40
+ ctx.onDropEvent(e, weekDates[col]);
41
+ }
42
+
43
+ function handleDragLeave(e: DragEvent) {
44
+ const row = e.currentTarget as HTMLElement;
45
+ const rect = row.getBoundingClientRect();
46
+ if (e.clientX < rect.left || e.clientX > rect.right ||
47
+ e.clientY < rect.top || e.clientY > rect.bottom) {
48
+ ctx.onHidePreview();
49
+ }
50
+ }
51
+ </script>
52
+
53
+ {#if items.length > 0}
54
+ <div
55
+ bind:this={rowEl}
56
+ class="week-view__allday"
57
+ role="application"
58
+ ondragover={handleDragOver}
59
+ ondrop={handleDrop}
60
+ ondragleave={handleDragLeave}
61
+ >
62
+ <div class="week-view__allday-label">{config.locale.allDayLabel}</div>
63
+ <div class="week-view__allday-grid" style="--lane-count: {lanes};">
64
+ {#each items as layoutItem, idx (`${layoutItem.segment.event.id}-${idx}`)}
65
+ {@const isDragged = ctx.previewEvent?.id === layoutItem.segment.event.id}
66
+ <EventBar
67
+ item={layoutItem}
68
+ {weekDates}
69
+ ghost={isDragged}
70
+ />
71
+ {/each}
72
+ </div>
73
+ </div>
74
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { CalendarDateTime, LayoutItem } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ weekDates: CalendarDateTime[];
4
+ items: LayoutItem[];
5
+ };
6
+ declare const AllDayRow: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type AllDayRow = ReturnType<typeof AllDayRow>;
8
+ export default AllDayRow;
9
+ //# sourceMappingURL=AllDayRow.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AllDayRow.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/week/AllDayRow.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzF,KAAK,gBAAgB,GAAI;IACtB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB,CAAC;AAgEJ,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,192 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
3
+ import { isSameDay, getRecurrenceInstanceMetadata } from '@bimetal/calendar-core';
4
+ import { getCalendarContext, getCalendarConfig, toTimeStr, buildDragInfo, getEventSlots } from '../../binding/index.js';
5
+ import type { DragInfo } from '../../binding/index.js';
6
+ import { createClickIntent, writeCalendarDragData, installTransparentDragImage } from '@bimetal/calendar-headless';
7
+
8
+ /**
9
+ * Layout-Position eines Termins (Pilot-Direktive 2026-06-21). Zwei
10
+ * orthogonale Achsen — Begründung in der React-Variante.
11
+ */
12
+ export interface TimeEventLayout {
13
+ readonly dayCol: number;
14
+ readonly dayCount: number;
15
+ readonly lane: number;
16
+ readonly laneCount: number;
17
+ readonly startOffset: number;
18
+ readonly endOffset: number;
19
+ }
20
+
21
+ let {
22
+ event,
23
+ layout,
24
+ dayDate,
25
+ hourHeight,
26
+ timeLabelWidth = 50,
27
+ ghost = false,
28
+ }: {
29
+ event: CalendarEvent;
30
+ layout: TimeEventLayout;
31
+ dayDate: CalendarDateTime;
32
+ hourHeight: number;
33
+ timeLabelWidth?: number;
34
+ ghost?: boolean;
35
+ } = $props();
36
+
37
+ const config = getCalendarConfig();
38
+ const ctx = getCalendarContext<{
39
+ onEventClick: (eventId: string, anchorRect: DOMRect, instanceDate?: number) => void;
40
+ onSelectEvent: (eventId: string, instanceDate?: number) => void;
41
+ selectedEventId: string | null;
42
+ selectedInstanceDate: number | undefined;
43
+ onDragStart: (info: DragInfo) => void;
44
+ onDragEnd: () => void;
45
+ }>();
46
+
47
+ const click = createClickIntent();
48
+
49
+ function handleMouseDown() {
50
+ click.markMouseDown();
51
+ }
52
+
53
+ function handleDragStart(e: DragEvent) {
54
+ click.markDragged();
55
+ const el = e.currentTarget as HTMLElement;
56
+ const rect = el.getBoundingClientRect();
57
+ const yInEl = e.clientY - rect.top;
58
+ const minutesIntoVisible = (yInEl / hourHeight) * 60;
59
+ const snap = config.core.granularity.day.minutes;
60
+ const minuteOffset = Math.round(minutesIntoVisible / snap) * snap;
61
+
62
+ const info: DragInfo = buildDragInfo(event, 'move-timed', { minuteOffset });
63
+ writeCalendarDragData(e.dataTransfer!, info);
64
+ ctx.onDragStart(info);
65
+ }
66
+
67
+ function handleResizeDragStart(e: DragEvent, edge: 'start' | 'end') {
68
+ e.stopPropagation();
69
+ click.markDragged();
70
+ installTransparentDragImage(e.dataTransfer!);
71
+ const info: DragInfo = buildDragInfo(event, 'resize-timed', { edge });
72
+ writeCalendarDragData(e.dataTransfer!, info);
73
+ ctx.onDragStart(info);
74
+ }
75
+
76
+ function handleClick(e: MouseEvent) {
77
+ e.stopPropagation();
78
+ if (!click.accept(config.clickThresholdMs)) return;
79
+ const instanceDate = getRecurrenceInstanceMetadata(event)?.instanceDate;
80
+ ctx.onSelectEvent(event.id, instanceDate);
81
+ }
82
+
83
+ function handleDoubleClick(e: MouseEvent) {
84
+ e.stopPropagation();
85
+ if (!click.accept(config.clickThresholdMs)) return;
86
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
87
+ const instanceDate = getRecurrenceInstanceMetadata(event)?.instanceDate;
88
+ ctx.onEventClick(event.id, rect, instanceDate);
89
+ }
90
+
91
+ const isFirstDay = $derived(isSameDay(event.timeRange.start, dayDate));
92
+ const isLastDay = $derived(isSameDay(event.timeRange.end, dayDate));
93
+ // Zeit-Geometrie aus layoutTimed (Pilot-Direktive 2026-06-21).
94
+ const dayHeight = $derived(24 * hourHeight);
95
+ const top = $derived(layout.startOffset * dayHeight);
96
+ const rawHeight = $derived((layout.endOffset - layout.startOffset) * dayHeight);
97
+ const height = $derived(Math.max(rawHeight, 0.25 * hourHeight));
98
+ const duration = $derived(Math.max((layout.endOffset - layout.startOffset) * 24, 0.25));
99
+ const color = $derived(event.metadata.color ?? '#007AFF');
100
+
101
+ const startTimeStr = $derived(toTimeStr(event.timeRange.start));
102
+ const endTimeStr = $derived(toTimeStr(event.timeRange.end));
103
+
104
+ const timeLabel = $derived.by(() => {
105
+ if (isFirstDay && isLastDay) return `${startTimeStr} \u2013 ${endTimeStr}`;
106
+ if (isFirstDay) return `${config.locale.timeFrom} ${startTimeStr}`;
107
+ if (isLastDay) return `${config.locale.timeUntil} ${endTimeStr}`;
108
+ return '';
109
+ });
110
+
111
+ // Vorgerechnete Br\u00fcche f\u00fcr die Horizontal-Geometrie.
112
+ const leftFraction = $derived(
113
+ layout.dayCol / layout.dayCount + layout.lane / (layout.dayCount * layout.laneCount),
114
+ );
115
+ const widthFraction = $derived(1 / (layout.dayCount * layout.laneCount));
116
+
117
+ const baseStyle = $derived(
118
+ `top: ${top}px; height: ${height}px; ` +
119
+ `left: calc(${timeLabelWidth}px + (100% - ${timeLabelWidth}px) * ${leftFraction} + 2px); ` +
120
+ `width: calc((100% - ${timeLabelWidth}px) * ${widthFraction} - 4px); ` +
121
+ `background-color: ${color}; ` +
122
+ `border-radius: ${isFirstDay ? 4 : 0}px ${isFirstDay ? 4 : 0}px ${isLastDay ? 4 : 0}px ${isLastDay ? 4 : 0}px;`
123
+ );
124
+
125
+ const instanceDate = $derived(getRecurrenceInstanceMetadata(event)?.instanceDate);
126
+ const isSelected = $derived(ctx.selectedEventId === event.id && ctx.selectedInstanceDate === instanceDate);
127
+
128
+ const slots = $derived(getEventSlots(event));
129
+ const renderedTitle = $derived(slots.title ?? event.title);
130
+ const extraClass = $derived(slots.className ? ' ' + slots.className : '');
131
+ const slotStyleStr = $derived(slots.style
132
+ ? Object.entries(slots.style).map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`).join('; ')
133
+ : '');
134
+ const style = $derived(slotStyleStr ? `${baseStyle} ${slotStyleStr}` : baseStyle);
135
+ </script>
136
+
137
+ <div
138
+ class="time-event{ghost ? ' time-event--ghost' : ''}{isSelected ? ' time-event--selected' : ''}{extraClass}"
139
+ {style}
140
+ draggable={!ghost}
141
+ role="button"
142
+ tabindex="0"
143
+ data-day-col={layout.dayCol}
144
+ data-day-count={layout.dayCount}
145
+ data-lane={layout.lane}
146
+ data-lane-count={layout.laneCount}
147
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(e as any); }}
148
+ onmousedown={handleMouseDown}
149
+ ondragstart={handleDragStart}
150
+ ondragend={ctx.onDragEnd}
151
+ onclick={handleClick}
152
+ ondblclick={handleDoubleClick}
153
+ title={renderedTitle}
154
+ >
155
+ {#if isFirstDay && !ghost}
156
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex a11y_no_static_element_interactions -->
157
+ <div
158
+ class="time-event__resize time-event__resize--top"
159
+ role="separator"
160
+ aria-label="Resize start"
161
+ draggable={true}
162
+ ondragstart={(e) => handleResizeDragStart(e, 'start')}
163
+ ondragend={ctx.onDragEnd}
164
+ onmousedown={(e) => e.stopPropagation()}
165
+ ></div>
166
+ {/if}
167
+
168
+ {#if slots.content}
169
+ {#if typeof slots.content === 'string'}{slots.content}{:else}{@render slots.content()}{/if}
170
+ {:else}
171
+ <div class="time-event__title">{renderedTitle}</div>
172
+ {#if duration >= 0.75 && timeLabel}
173
+ <div class="time-event__time">{timeLabel}</div>
174
+ {/if}
175
+ {/if}
176
+ {#if slots.badges}
177
+ {#if typeof slots.badges === 'string'}{slots.badges}{:else}{@render slots.badges()}{/if}
178
+ {/if}
179
+
180
+ {#if isLastDay && !ghost}
181
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_no_noninteractive_tabindex a11y_no_static_element_interactions -->
182
+ <div
183
+ class="time-event__resize time-event__resize--bottom"
184
+ role="separator"
185
+ aria-label="Resize end"
186
+ draggable={true}
187
+ ondragstart={(e) => handleResizeDragStart(e, 'end')}
188
+ ondragend={ctx.onDragEnd}
189
+ onmousedown={(e) => e.stopPropagation()}
190
+ ></div>
191
+ {/if}
192
+ </div>
@@ -0,0 +1,25 @@
1
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
2
+ /**
3
+ * Layout-Position eines Termins (Pilot-Direktive 2026-06-21). Zwei
4
+ * orthogonale Achsen — Begründung in der React-Variante.
5
+ */
6
+ export interface TimeEventLayout {
7
+ readonly dayCol: number;
8
+ readonly dayCount: number;
9
+ readonly lane: number;
10
+ readonly laneCount: number;
11
+ readonly startOffset: number;
12
+ readonly endOffset: number;
13
+ }
14
+ type $$ComponentProps = {
15
+ event: CalendarEvent;
16
+ layout: TimeEventLayout;
17
+ dayDate: CalendarDateTime;
18
+ hourHeight: number;
19
+ timeLabelWidth?: number;
20
+ ghost?: boolean;
21
+ };
22
+ declare const TimeEvent: import("svelte").Component<$$ComponentProps, {}, "">;
23
+ type TimeEvent = ReturnType<typeof TimeEvent>;
24
+ export default TimeEvent;
25
+ //# sourceMappingURL=TimeEvent.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeEvent.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/week/TimeEvent.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAO5E;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AACF,KAAK,gBAAgB,GAAI;IACtB,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAgJJ,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,232 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
3
+ import {
4
+ weekGrid, calendarWeekNumber, isSameDay, now,
5
+ segmentAllDay, segmentTimed, layoutAllDay, layoutTimed,
6
+ getHours, getMinutes, getDayOfMonth,
7
+ } from '@bimetal/calendar-core';
8
+ import { getCalendarContext, getCalendarConfig, toDateStr, parseDateTimeStr } from '../../binding/index.js';
9
+ import { DEFAULT_HOUR_HEIGHT, DEFAULT_TIME_LABEL_WIDTH_WEEK, DEFAULT_SCROLL_HOUR, createClickIntent, timeGridHit } from '@bimetal/calendar-headless';
10
+ import { onMount } from 'svelte';
11
+ import AllDayRow from './AllDayRow.svelte';
12
+ import TimeEvent from './TimeEvent.svelte';
13
+
14
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
15
+
16
+ let {
17
+ currentDate,
18
+ }: {
19
+ currentDate: CalendarDateTime;
20
+ } = $props();
21
+
22
+ const config = getCalendarConfig();
23
+ const ctx = getCalendarContext<{
24
+ events: CalendarEvent[];
25
+ previewEvent: CalendarEvent | null;
26
+ onDragOverDateTime: (date: CalendarDateTime, time: string) => void;
27
+ onDropTimedEvent: (e: DragEvent, dropDate: CalendarDateTime, dropTime: string) => void;
28
+ onHidePreview: () => void;
29
+ onDayCellClick: (date: CalendarDateTime, anchorRect: DOMRect) => void;
30
+ selectedEventId: string | null;
31
+ onClearSelection: () => void;
32
+ }>();
33
+
34
+ let timeGridEl: HTMLDivElement | undefined = $state();
35
+ let gridInnerEl: HTMLDivElement | undefined = $state();
36
+ const click = createClickIntent();
37
+ const timeSnapMinutes = config.core.granularity.week.minutes;
38
+
39
+ const weekDates = $derived(weekGrid(currentDate, config.core));
40
+ const weekNumber = $derived(calendarWeekNumber(weekDates[0]));
41
+
42
+ function scrollTo8AM() {
43
+ if (timeGridEl) {
44
+ timeGridEl.scrollTop = DEFAULT_SCROLL_HOUR * DEFAULT_HOUR_HEIGHT;
45
+ }
46
+ }
47
+
48
+ onMount(scrollTo8AM);
49
+
50
+ let lastDateEpoch = 0;
51
+ $effect(() => {
52
+ const dateEpoch = currentDate.epochMs;
53
+ if (lastDateEpoch !== 0 && lastDateEpoch !== dateEpoch) {
54
+ scrollTo8AM();
55
+ }
56
+ lastDateEpoch = dateEpoch;
57
+ });
58
+
59
+ // B-CAL-1: der Controller hat die Geometrie des gezogenen Events bereits neu
60
+ // gebaut (`previewEvent`). Per id eintauschen — kein String-Parsing / +1 /
61
+ // Validity in der View; diese Policy lebt einmal im Headless.
62
+ const displayEvents = $derived.by((): CalendarEvent[] => {
63
+ const pv = ctx.previewEvent;
64
+ const evts = ctx.events;
65
+ if (!pv) return evts;
66
+ return evts.map(ev => ev.id === pv.id ? { ...ev, timeRange: pv.timeRange } : ev);
67
+ });
68
+
69
+ const allDayEvents = $derived(displayEvents.filter(ev => ev.allDay));
70
+ const timedEvents = $derived(displayEvents.filter(ev => !ev.allDay));
71
+
72
+ const allDaySegments = $derived(segmentAllDay(allDayEvents, weekDates));
73
+ const allDayItems = $derived(layoutAllDay(allDaySegments, weekDates));
74
+
75
+ // Lane-Layout pro Tag durch den Core (Pilot-Direktive 2026-06-21):
76
+ // segmentTimed + layoutTimed bauen Collision-Groups + Lane-Belegung.
77
+ const timedLayoutByDay = $derived.by(() => {
78
+ const map = new Map<number, ReturnType<typeof layoutTimed>>();
79
+ for (let col = 0; col < 7; col++) {
80
+ const segments = segmentTimed(timedEvents, weekDates[col]);
81
+ map.set(col, layoutTimed(segments));
82
+ }
83
+ return map;
84
+ });
85
+
86
+ const currentNow = $derived(now(config.core.timezone, config.core.clock));
87
+ const nowCol = $derived(weekDates.findIndex(d => isSameDay(d, currentNow)));
88
+ const nowHours = $derived(getHours(currentNow) + getMinutes(currentNow) / 60);
89
+
90
+ // Hit-Test lebt im Headless (`timeGridHit`); das Binding misst nur den Rect.
91
+ function handleGridDragOver(e: DragEvent) {
92
+ e.preventDefault();
93
+ e.dataTransfer!.dropEffect = 'move';
94
+ if (!gridInnerEl) return;
95
+ const rect = gridInnerEl.getBoundingClientRect();
96
+ const hit = timeGridHit({ x: e.clientX, y: e.clientY }, rect, { labelWidth: DEFAULT_TIME_LABEL_WIDTH_WEEK, hourHeight: DEFAULT_HOUR_HEIGHT, snapMinutes: timeSnapMinutes });
97
+ ctx.onDragOverDateTime(weekDates[hit.col], hit.time);
98
+ }
99
+
100
+ function handleGridDrop(e: DragEvent) {
101
+ e.preventDefault();
102
+ if (!gridInnerEl) return;
103
+ const rect = gridInnerEl.getBoundingClientRect();
104
+ const hit = timeGridHit({ x: e.clientX, y: e.clientY }, rect, { labelWidth: DEFAULT_TIME_LABEL_WIDTH_WEEK, hourHeight: DEFAULT_HOUR_HEIGHT, snapMinutes: timeSnapMinutes });
105
+ ctx.onDropTimedEvent(e, weekDates[hit.col], hit.time);
106
+ }
107
+
108
+ function handleGridDragLeave(e: DragEvent) {
109
+ const el = e.currentTarget as HTMLElement;
110
+ const rect = el.getBoundingClientRect();
111
+ if (e.clientX < rect.left || e.clientX > rect.right ||
112
+ e.clientY < rect.top || e.clientY > rect.bottom) {
113
+ ctx.onHidePreview();
114
+ }
115
+ }
116
+
117
+ function handleGridMouseDown() {
118
+ click.markMouseDown();
119
+ }
120
+
121
+ function handleGridClick(e: MouseEvent) {
122
+ if (!click.accept(config.clickThresholdMs)) return;
123
+ if ((e.target as HTMLElement).closest('.time-event')) return;
124
+ // Single click on empty grid deselects; create-dialog moves to dblclick.
125
+ if (ctx.selectedEventId !== null) ctx.onClearSelection();
126
+ }
127
+
128
+ function handleGridDoubleClick(e: MouseEvent) {
129
+ if (!click.accept(config.clickThresholdMs)) return;
130
+ if ((e.target as HTMLElement).closest('.time-event')) return;
131
+ if (!gridInnerEl) return;
132
+ const gridRect = gridInnerEl.getBoundingClientRect();
133
+ const hit = timeGridHit({ x: e.clientX, y: e.clientY }, gridRect, { labelWidth: DEFAULT_TIME_LABEL_WIDTH_WEEK, hourHeight: DEFAULT_HOUR_HEIGHT, snapMinutes: timeSnapMinutes });
134
+ const clickedDate = weekDates[hit.col];
135
+ // Klick-Datum + gesnapte Zeit über den geteilten Parser (kein split).
136
+ const clickedDt = parseDateTimeStr(toDateStr(clickedDate), hit.time, clickedDate.timezone);
137
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
138
+ ctx.onDayCellClick(clickedDt, rect);
139
+ }
140
+ </script>
141
+
142
+ <div class="week-view">
143
+ <!-- Header -->
144
+ <div class="week-view__header">
145
+ <div class="week-view__corner">{config.locale.calendarWeek} {weekNumber}</div>
146
+ {#each weekDates as date, i (i)}
147
+ {@const isToday = isSameDay(date, currentNow)}
148
+ <div class="week-view__day-header{isToday ? ' week-view__day-header--today' : ''}">
149
+ <span class="week-view__day-name">{config.locale.dayNamesShort[i]}</span>
150
+ <span class="week-view__day-number{isToday ? ' week-view__day-number--today' : ''}">
151
+ {getDayOfMonth(date)}
152
+ </span>
153
+ </div>
154
+ {/each}
155
+ </div>
156
+
157
+ <!-- All-day row -->
158
+ <AllDayRow {weekDates} items={allDayItems} />
159
+
160
+ <!-- Time grid -->
161
+ <div
162
+ bind:this={timeGridEl}
163
+ class="week-view__time-grid"
164
+ role="grid"
165
+ tabindex="0"
166
+ ondragover={handleGridDragOver}
167
+ ondrop={handleGridDrop}
168
+ ondragleave={handleGridDragLeave}
169
+ onmousedown={handleGridMouseDown}
170
+ onclick={handleGridClick}
171
+ ondblclick={handleGridDoubleClick}
172
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleGridDoubleClick(e as any); }}
173
+ >
174
+ <div
175
+ bind:this={gridInnerEl}
176
+ class="week-view__time-grid-inner"
177
+ style="height: {24 * DEFAULT_HOUR_HEIGHT}px;"
178
+ >
179
+ <!-- Hour rows -->
180
+ {#each HOURS as hr (hr)}
181
+ {@const wh = config.core.workingHours}
182
+ {@const isOff = wh ? (hr < wh.start || hr >= wh.end) : false}
183
+ <div
184
+ class="week-view__hour-row{isOff ? ' hour-slot--off' : ''}"
185
+ style="top: {hr * DEFAULT_HOUR_HEIGHT}px; height: {DEFAULT_HOUR_HEIGHT}px;"
186
+ >
187
+ <div class="week-view__hour-label">
188
+ {hr > 0 ? `${String(hr).padStart(2, '0')}:00` : ''}
189
+ </div>
190
+ </div>
191
+ {/each}
192
+
193
+ <!-- Day columns -->
194
+ {#each weekDates as _, col (col)}
195
+ <div
196
+ class="week-view__day-col"
197
+ style="left: calc({DEFAULT_TIME_LABEL_WIDTH_WEEK}px + {col} * (100% - {DEFAULT_TIME_LABEL_WIDTH_WEEK}px) / 7); width: calc((100% - {DEFAULT_TIME_LABEL_WIDTH_WEEK}px) / 7);"
198
+ ></div>
199
+ {/each}
200
+
201
+ <!-- Timed events -->
202
+ {#each Array.from(timedLayoutByDay.entries()) as [col, items] (col)}
203
+ {#each items as item (`${item.segment.event.id}-${col}-${item.column}`)}
204
+ <TimeEvent
205
+ event={item.segment.event}
206
+ layout={{
207
+ dayCol: col,
208
+ dayCount: 7,
209
+ lane: item.column,
210
+ laneCount: item.totalColumns,
211
+ startOffset: item.startOffset,
212
+ endOffset: item.endOffset,
213
+ }}
214
+ dayDate={weekDates[col]}
215
+ hourHeight={DEFAULT_HOUR_HEIGHT}
216
+ ghost={ctx.previewEvent?.id === item.segment.event.id}
217
+ />
218
+ {/each}
219
+ {/each}
220
+
221
+ <!-- Now line -->
222
+ {#if nowCol >= 0}
223
+ <div
224
+ class="week-view__now-line"
225
+ style="top: {nowHours * DEFAULT_HOUR_HEIGHT}px; left: calc({DEFAULT_TIME_LABEL_WIDTH_WEEK}px + {nowCol} * (100% - {DEFAULT_TIME_LABEL_WIDTH_WEEK}px) / 7); width: calc((100% - {DEFAULT_TIME_LABEL_WIDTH_WEEK}px) / 7);"
226
+ >
227
+ <div class="week-view__now-dot"></div>
228
+ </div>
229
+ {/if}
230
+ </div>
231
+ </div>
232
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { CalendarDateTime } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ currentDate: CalendarDateTime;
4
+ };
5
+ declare const WeekView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type WeekView = ReturnType<typeof WeekView>;
7
+ export default WeekView;
8
+ //# sourceMappingURL=WeekView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WeekView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/week/WeekView.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAY7E,KAAK,gBAAgB,GAAI;IACtB,WAAW,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAuMJ,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime } from '@bimetal/calendar-core';
3
+ import {
4
+ monthGrid, getMonth, getDayOfMonth, isSameDay, startOfDay, addDays, now,
5
+ } from '@bimetal/calendar-core';
6
+ import { getCalendarConfig, getCalendarContext } from '../../binding/index.js';
7
+
8
+ let {
9
+ year,
10
+ onMonthClick = undefined,
11
+ }: {
12
+ year: number;
13
+ onMonthClick?: (year: number, month: number) => void;
14
+ } = $props();
15
+
16
+ const config = getCalendarConfig();
17
+ const ctx = getCalendarContext<{
18
+ events: CalendarEvent[];
19
+ onDayCellClick: (date: CalendarDateTime, rect: DOMRect) => void;
20
+ }>();
21
+
22
+ const today = $derived(now(config.core.timezone, config.core.clock));
23
+
24
+ const eventDays = $derived.by(() => {
25
+ const set = new Set<number>();
26
+ for (const ev of ctx.events) {
27
+ let cursor = startOfDay(ev.timeRange.start);
28
+ const last = ev.timeRange.end;
29
+ while (cursor.epochMs < last.epochMs) {
30
+ set.add(cursor.epochMs);
31
+ cursor = addDays(cursor, 1);
32
+ }
33
+ }
34
+ return set;
35
+ });
36
+
37
+ const months = $derived.by(() => {
38
+ const monthFmt = new Intl.DateTimeFormat(config.locale.code, {
39
+ month: 'long', timeZone: config.core.timezone,
40
+ });
41
+ return Array.from({ length: 12 }, (_, m) => ({
42
+ month: m,
43
+ label: monthFmt.format(new Date(Date.UTC(year, m, 15, 12))),
44
+ weeks: monthGrid(year, m, config.core),
45
+ }));
46
+ });
47
+
48
+ function dayClick(e: MouseEvent, date: CalendarDateTime) {
49
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
50
+ ctx.onDayCellClick(date, rect);
51
+ }
52
+ </script>
53
+
54
+ <div class="year-grid">
55
+ {#each months as { month, label, weeks } (month)}
56
+ <div class="year-month">
57
+ {#if onMonthClick}
58
+ <button type="button" class="year-month__header" onclick={() => onMonthClick?.(year, month)}>
59
+ {label}
60
+ </button>
61
+ {:else}
62
+ <div class="year-month__header">{label}</div>
63
+ {/if}
64
+ <div class="year-month__day-headers">
65
+ {#each config.locale.dayNamesShort as n (n)}
66
+ <div class="year-month__day-header">{n[0]}</div>
67
+ {/each}
68
+ </div>
69
+ <div class="year-month__days">
70
+ {#each weeks as week, wIdx (wIdx)}
71
+ {#each week as date (date.epochMs)}
72
+ {@const inMonth = getMonth(date) === month}
73
+ {@const isToday = isSameDay(date, today)}
74
+ {@const hasEvents = eventDays.has(startOfDay(date).epochMs)}
75
+ <button
76
+ type="button"
77
+ class={
78
+ 'year-month__day'
79
+ + (inMonth ? '' : ' year-month__day--outside')
80
+ + (isToday ? ' year-month__day--today' : '')
81
+ + (hasEvents ? ' year-month__day--has-events' : '')
82
+ }
83
+ onclick={(e) => dayClick(e, date)}
84
+ >
85
+ <span class="year-month__day-num">{getDayOfMonth(date)}</span>
86
+ </button>
87
+ {/each}
88
+ {/each}
89
+ </div>
90
+ </div>
91
+ {/each}
92
+ </div>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ year: number;
3
+ onMonthClick?: (year: number, month: number) => void;
4
+ };
5
+ declare const YearView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type YearView = ReturnType<typeof YearView>;
7
+ export default YearView;
8
+ //# sourceMappingURL=YearView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YearView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/year/YearView.svelte.ts"],"names":[],"mappings":"AASC,KAAK,gBAAgB,GAAI;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD,CAAC;AAqFJ,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}