@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,224 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime, LayoutItem } from '@bimetal/calendar-core';
3
+ import {
4
+ isSameDay, getDayOfMonth, getMonth, getHours, getMinutes,
5
+ calendarWeekNumber, now, maxLanes, lanesPerDay, startOfDay, formatDayLabel,
6
+ getRecurrenceInstanceMetadata,
7
+ } from '@bimetal/calendar-core';
8
+ import { getColFromX, getCalendarContext, getCalendarConfig } from '../../binding/index.js';
9
+ import type { DragInfo } from '../../binding/index.js';
10
+ import { createClickIntent, maxVisibleTimedInMonth } from '@bimetal/calendar-headless';
11
+ import EventBar from '../../components/EventBar.svelte';
12
+ import TimedEntry from './TimedEntry.svelte';
13
+
14
+ let {
15
+ weekDates,
16
+ items,
17
+ timedByDay,
18
+ currentYear,
19
+ currentMonth,
20
+ }: {
21
+ weekDates: CalendarDateTime[];
22
+ items: LayoutItem[];
23
+ timedByDay: Map<number, CalendarEvent[]>;
24
+ currentYear: number;
25
+ currentMonth: number;
26
+ } = $props();
27
+
28
+ const config = getCalendarConfig();
29
+ const ctx = getCalendarContext<{
30
+ onEventClick: (eventId: string, anchorRect: DOMRect, instanceDate?: number) => void;
31
+ onSelectEvent: (eventId: string, instanceDate?: number) => void;
32
+ onClearSelection: () => void;
33
+ selectedEventId: string | null;
34
+ selectedInstanceDate: number | undefined;
35
+ onDropEvent: (e: DragEvent, dropDate: CalendarDateTime) => void;
36
+ onDayCellClick: (date: CalendarDateTime, anchorRect: DOMRect) => void;
37
+ onDragOverDate: (date: CalendarDateTime) => void;
38
+ onHidePreview: () => void;
39
+ previewEvent: CalendarEvent | null;
40
+ // B-CAL-3: hover-expandierte Monatszelle (Tag-epochMs) + Intents, Eigentum
41
+ // des Controllers — KEIN lokales expand-`$state`.
42
+ expandedMonthDay: number | null;
43
+ onExpandMonthCell: (dayEpochMs: number) => void;
44
+ onCollapseMonthCell: () => void;
45
+ }>();
46
+
47
+ let rowEl: HTMLDivElement | undefined = $state();
48
+ const click = createClickIntent();
49
+ const timedClick = createClickIntent();
50
+
51
+ const lanes = $derived(maxLanes(items));
52
+ const lanesByCol = $derived(lanesPerDay(items, weekDates.length));
53
+ const weekNumber = $derived(calendarWeekNumber(weekDates[0]));
54
+ const today = $derived(now(config.core.timezone, config.core.clock));
55
+
56
+ function handleDragOver(e: DragEvent) {
57
+ e.preventDefault();
58
+ e.dataTransfer!.dropEffect = 'move';
59
+ if (!rowEl) return;
60
+ const col = getColFromX(e.clientX, rowEl.getBoundingClientRect());
61
+ ctx.onDragOverDate(weekDates[col]);
62
+ }
63
+
64
+ function handleDragLeave(e: DragEvent) {
65
+ const row = e.currentTarget as HTMLElement;
66
+ const rect = row.getBoundingClientRect();
67
+ if (e.clientX < rect.left || e.clientX > rect.right ||
68
+ e.clientY < rect.top || e.clientY > rect.bottom) {
69
+ ctx.onHidePreview();
70
+ }
71
+ }
72
+
73
+ function handleDrop(e: DragEvent) {
74
+ e.preventDefault();
75
+ if (!rowEl) return;
76
+ const col = getColFromX(e.clientX, rowEl.getBoundingClientRect());
77
+ ctx.onDropEvent(e, weekDates[col]);
78
+ }
79
+
80
+ function handleDayCellMouseDown() {
81
+ click.markMouseDown();
82
+ }
83
+
84
+ function handleDayCellClick(e: MouseEvent, date: CalendarDateTime) {
85
+ if (!click.accept(config.clickThresholdMs)) return;
86
+ // Single click on an empty cell deselects when something is selected,
87
+ // otherwise it is a no-op. Create-dialog moves to dblclick.
88
+ if (ctx.selectedEventId !== null) ctx.onClearSelection();
89
+ }
90
+
91
+ function handleDayCellDoubleClick(e: MouseEvent, date: CalendarDateTime) {
92
+ if (!click.accept(config.clickThresholdMs)) return;
93
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
94
+ ctx.onDayCellClick(date, rect);
95
+ }
96
+
97
+ function handleTimedEventMouseDown(e: MouseEvent) {
98
+ e.stopPropagation();
99
+ timedClick.markMouseDown();
100
+ }
101
+
102
+ function handleTimedEventClick(e: MouseEvent, eventId: string, instanceDate?: number) {
103
+ e.stopPropagation();
104
+ if (!timedClick.accept(config.clickThresholdMs)) return;
105
+ ctx.onSelectEvent(eventId, instanceDate);
106
+ }
107
+
108
+ function handleTimedEventDoubleClick(e: MouseEvent, eventId: string, instanceDate?: number) {
109
+ e.stopPropagation();
110
+ if (!timedClick.accept(config.clickThresholdMs)) return;
111
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
112
+ ctx.onEventClick(eventId, rect, instanceDate);
113
+ }
114
+
115
+ function isTimedSelected(ev: CalendarEvent): boolean {
116
+ const id = getRecurrenceInstanceMetadata(ev)?.instanceDate;
117
+ return ctx.selectedEventId === ev.id && ctx.selectedInstanceDate === id;
118
+ }
119
+ </script>
120
+
121
+ <div
122
+ bind:this={rowEl}
123
+ class="week-row"
124
+ style="--lane-count: {lanes};"
125
+ role="row"
126
+ tabindex="-1"
127
+ ondragover={handleDragOver}
128
+ ondragleave={handleDragLeave}
129
+ ondrop={handleDrop}
130
+ >
131
+ <div class="week-number">{weekNumber}</div>
132
+
133
+ {#each weekDates as date, colIdx (colIdx)}
134
+ {@const isToday = isSameDay(date, today)}
135
+ {@const inMonth = getMonth(date) === currentMonth}
136
+ {@const dayNum = getDayOfMonth(date)}
137
+ {@const dayMs = startOfDay(date).epochMs}
138
+ {@const timedEvents = timedByDay.get(dayMs) ?? []}
139
+ {@const cellLanes = lanesByCol[colIdx]}
140
+ {@const maxVisible = maxVisibleTimedInMonth(cellLanes)}
141
+ {@const hasOverflow = timedEvents.length > maxVisible}
142
+ {@const isExpanded = ctx.expandedMonthDay === dayMs}
143
+ {@const showEvents = isExpanded ? timedEvents : timedEvents.slice(0, maxVisible)}
144
+ {@const overflow = timedEvents.length - maxVisible}
145
+
146
+ <div
147
+ class="day-cell"
148
+ class:outside-month={!inMonth}
149
+ style="grid-column: {colIdx + 2}; grid-row: 1 / -1; --lane-count: {cellLanes};"
150
+ role="gridcell"
151
+ tabindex="0"
152
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleDayCellDoubleClick(e as any, date); }}
153
+ onmousedown={handleDayCellMouseDown}
154
+ onclick={(e) => handleDayCellClick(e, date)}
155
+ ondblclick={(e) => handleDayCellDoubleClick(e, date)}
156
+ >
157
+ <div class="day-number-container">
158
+ <span class="day-number" class:today={isToday}>
159
+ {formatDayLabel(date, config.locale.code)}
160
+ </span>
161
+ </div>
162
+
163
+ {#if timedEvents.length > 0}
164
+ <div class="day-cell__timed">
165
+ {#each showEvents as ev (ev.id)}
166
+ <TimedEntry
167
+ {ev}
168
+ isSelected={isTimedSelected(ev)}
169
+ onEntryMouseDown={handleTimedEventMouseDown}
170
+ onEntryClick={handleTimedEventClick}
171
+ onEntryDoubleClick={handleTimedEventDoubleClick}
172
+ />
173
+ {/each}
174
+ {#if !isExpanded && hasOverflow}
175
+ <div
176
+ class="timed-overflow"
177
+ role="button"
178
+ tabindex="0"
179
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') ctx.onExpandMonthCell(dayMs); }}
180
+ onmouseenter={() => ctx.onExpandMonthCell(dayMs)}
181
+ >
182
+ +{overflow} {config.locale.moreEvents}
183
+ </div>
184
+ {/if}
185
+ </div>
186
+
187
+ {#if isExpanded && hasOverflow}
188
+ <div
189
+ class="day-cell__expand-overlay"
190
+ role="tooltip"
191
+ onmouseleave={() => ctx.onCollapseMonthCell()}
192
+ >
193
+ <div class="day-number-container">
194
+ <span class="day-number" class:today={isToday}>{dayNum}</span>
195
+ </div>
196
+ {#each timedEvents as ev (ev.id)}
197
+ <TimedEntry
198
+ {ev}
199
+ isSelected={isTimedSelected(ev)}
200
+ onEntryMouseDown={handleTimedEventMouseDown}
201
+ onEntryClick={handleTimedEventClick}
202
+ onEntryDoubleClick={handleTimedEventDoubleClick}
203
+ />
204
+ {/each}
205
+ </div>
206
+ {/if}
207
+ {/if}
208
+ </div>
209
+ {/each}
210
+
211
+ <!-- Events overlay -->
212
+ <div class="events-overlay">
213
+ <div class="events-grid">
214
+ {#each items as layoutItem, idx (`${layoutItem.segment.event.id}-${idx}`)}
215
+ {@const isDragged = ctx.previewEvent?.id === layoutItem.segment.event.id}
216
+ <EventBar
217
+ item={layoutItem}
218
+ {weekDates}
219
+ ghost={isDragged}
220
+ />
221
+ {/each}
222
+ </div>
223
+ </div>
224
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { CalendarEvent, CalendarDateTime, LayoutItem } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ weekDates: CalendarDateTime[];
4
+ items: LayoutItem[];
5
+ timedByDay: Map<number, CalendarEvent[]>;
6
+ currentYear: number;
7
+ currentMonth: number;
8
+ };
9
+ declare const WeekRow: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type WeekRow = ReturnType<typeof WeekRow>;
11
+ export default WeekRow;
12
+ //# sourceMappingURL=WeekRow.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WeekRow.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/month/WeekRow.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAYzF,KAAK,gBAAgB,GAAI;IACtB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AA6KJ,QAAA,MAAM,OAAO,sDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,227 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime, Resource } from '@bimetal/calendar-core';
3
+ import {
4
+ isSameDay, now, startOfDay, addDays,
5
+ getHours, getMinutes,
6
+ createTimeRange, overlaps, formatDateTime,
7
+ segmentTimed, layoutTimed,
8
+ } from '@bimetal/calendar-core';
9
+ import { getCalendarConfig, getCalendarContext, snapTimeFromMouseY } from '../../binding/index.js';
10
+ import { DEFAULT_HOUR_HEIGHT, DEFAULT_TIME_LABEL_WIDTH_DAY, DEFAULT_SCROLL_HOUR, createClickIntent } from '@bimetal/calendar-headless';
11
+ import { onMount, tick } from 'svelte';
12
+ import TimeEvent from '../week/TimeEvent.svelte';
13
+
14
+ let {
15
+ currentDate,
16
+ visibleResources,
17
+ onResourceCellClick,
18
+ onResourceDrop,
19
+ }: {
20
+ currentDate: CalendarDateTime;
21
+ visibleResources: readonly Resource[];
22
+ onResourceCellClick: (date: CalendarDateTime, time: string, resourceId: string, rect: DOMRect) => void;
23
+ onResourceDrop: (e: DragEvent, date: CalendarDateTime, time: string, resourceId: string) => void;
24
+ } = $props();
25
+
26
+ const config = getCalendarConfig();
27
+ const ctx = getCalendarContext<{
28
+ events: CalendarEvent[];
29
+ previewEvent: CalendarEvent | null;
30
+ onDragOverDateTime: (date: CalendarDateTime, time: string) => void;
31
+ onHidePreview: () => void;
32
+ selectedEventId: string | null;
33
+ onClearSelection: () => void;
34
+ }>();
35
+
36
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
37
+
38
+ let timeGridEl: HTMLDivElement | undefined = $state();
39
+ let gridInnerEl: HTMLDivElement | undefined = $state();
40
+ const click = createClickIntent();
41
+ const timeSnapMinutes = config.core.granularity.day.minutes;
42
+
43
+ onMount(async () => {
44
+ await tick();
45
+ if (timeGridEl) timeGridEl.scrollTop = DEFAULT_SCROLL_HOUR * DEFAULT_HOUR_HEIGHT;
46
+ });
47
+
48
+ function getTimeFromMouseY(clientY: number, gridInner: HTMLElement): string {
49
+ return snapTimeFromMouseY(clientY, gridInner.getBoundingClientRect(), DEFAULT_HOUR_HEIGHT, timeSnapMinutes);
50
+ }
51
+
52
+ const dayStart = $derived(startOfDay(currentDate));
53
+ const dayEnd = $derived(addDays(dayStart, 1));
54
+ const dayRange = $derived(createTimeRange(dayStart, dayEnd));
55
+
56
+ // B-CAL-1: das vom Controller neu gebaute Event per id eintauschen, damit der
57
+ // Ghost dem Cursor folgt — die Geometrie kommt aus dem Controller, die View
58
+ // parst nichts.
59
+ const displayEvents = $derived.by((): CalendarEvent[] => {
60
+ const pv = ctx.previewEvent;
61
+ if (!pv) return ctx.events;
62
+ return ctx.events.map(ev => ev.id === pv.id ? { ...ev, timeRange: pv.timeRange } : ev);
63
+ });
64
+
65
+ // Lane-Layout pro Resource durch den Core (Pilot-Direktive 2026-06-21).
66
+ const timedLayoutByResource = $derived.by(() => {
67
+ const map = new Map<string, ReturnType<typeof layoutTimed>>();
68
+ for (const r of visibleResources) {
69
+ const own = displayEvents.filter(ev =>
70
+ !ev.allDay && ev.resourceId === r.id && overlaps(ev.timeRange, dayRange),
71
+ );
72
+ map.set(r.id, layoutTimed(segmentTimed(own, currentDate)));
73
+ }
74
+ return map;
75
+ });
76
+
77
+ function resourceFromX(clientX: number, gridInner: HTMLElement): string | null {
78
+ if (visibleResources.length === 0) return null;
79
+ const rect = gridInner.getBoundingClientRect();
80
+ const x = clientX - rect.left - DEFAULT_TIME_LABEL_WIDTH_DAY;
81
+ if (x < 0) return null;
82
+ const cw = (rect.width - DEFAULT_TIME_LABEL_WIDTH_DAY) / visibleResources.length;
83
+ const idx = Math.min(visibleResources.length - 1, Math.max(0, Math.floor(x / cw)));
84
+ return visibleResources[idx].id;
85
+ }
86
+
87
+ const today = $derived(now(config.core.timezone, config.core.clock));
88
+ const isToday = $derived(isSameDay(currentDate, today));
89
+ const dayName = $derived(formatDateTime(currentDate, { locale: config.locale.code, dateStyle: 'full' }));
90
+ const nowHours = $derived(getHours(today) + getMinutes(today) / 60);
91
+ const colWidth = $derived(`calc((100% - ${DEFAULT_TIME_LABEL_WIDTH_DAY}px) / ${visibleResources.length})`);
92
+
93
+ function handleColumnMouseDown() { click.markMouseDown(); }
94
+
95
+ function handleGridDragOver(e: DragEvent) {
96
+ e.preventDefault();
97
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
98
+ if (!gridInnerEl) return;
99
+ const time = getTimeFromMouseY(e.clientY, gridInnerEl);
100
+ ctx.onDragOverDateTime(currentDate, time);
101
+ }
102
+
103
+ function handleGridDrop(e: DragEvent) {
104
+ e.preventDefault();
105
+ if (!gridInnerEl) return;
106
+ const resourceId = resourceFromX(e.clientX, gridInnerEl);
107
+ if (!resourceId) return;
108
+ const time = getTimeFromMouseY(e.clientY, gridInnerEl);
109
+ onResourceDrop(e, currentDate, time, resourceId);
110
+ }
111
+
112
+ function handleGridDragLeave(e: DragEvent) {
113
+ const el = e.currentTarget as HTMLElement;
114
+ const rect = el.getBoundingClientRect();
115
+ if (e.clientX < rect.left || e.clientX > rect.right
116
+ || e.clientY < rect.top || e.clientY > rect.bottom) {
117
+ ctx.onHidePreview();
118
+ }
119
+ }
120
+
121
+ function handleColumnClick(e: MouseEvent) {
122
+ if (!click.accept(config.clickThresholdMs)) return;
123
+ if ((e.target as HTMLElement).closest('.time-event')) return;
124
+ // Single click deselects; create-dialog moves to dblclick.
125
+ if (ctx.selectedEventId !== null) ctx.onClearSelection();
126
+ }
127
+
128
+ function handleColumnDoubleClick(e: MouseEvent, resourceId: string) {
129
+ if (!click.accept(config.clickThresholdMs)) return;
130
+ if (!gridInnerEl) return;
131
+ if ((e.target as HTMLElement).closest('.time-event')) return;
132
+ const time = getTimeFromMouseY(e.clientY, gridInnerEl);
133
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
134
+ onResourceCellClick(currentDate, time, resourceId, rect);
135
+ }
136
+ </script>
137
+
138
+ {#if visibleResources.length === 0}
139
+ <div class="resource-view resource-view--empty">{config.locale.unassigned ?? 'No resource selected'}</div>
140
+ {:else}
141
+ <div class="resource-view">
142
+ <div class="resource-view__header">
143
+ <div class="resource-view__day-name">{dayName}</div>
144
+ <div class="resource-view__resources">
145
+ <div class="resource-view__time-spacer" style:width={`${DEFAULT_TIME_LABEL_WIDTH_DAY}px`}></div>
146
+ {#each visibleResources as r (r.id)}
147
+ <div class="resource-view__resource-header" style="flex: 1 1 0; min-width: 0;">
148
+ {#if r.color}
149
+ <span class="resource-view__resource-dot" style:background-color={r.color}></span>
150
+ {/if}
151
+ <span class="resource-view__resource-name">{r.name}</span>
152
+ </div>
153
+ {/each}
154
+ </div>
155
+ </div>
156
+
157
+ <div class="resource-view__time-grid" bind:this={timeGridEl}>
158
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
159
+ <div
160
+ class="resource-view__time-grid-inner"
161
+ bind:this={gridInnerEl}
162
+ style:height={`${24 * DEFAULT_HOUR_HEIGHT}px`}
163
+ ondragover={handleGridDragOver}
164
+ ondrop={handleGridDrop}
165
+ ondragleave={handleGridDragLeave}
166
+ >
167
+ {#each HOURS as hr (hr)}
168
+ {@const wh = config.core.workingHours}
169
+ {@const isOff = wh ? (hr < wh.start || hr >= wh.end) : false}
170
+ <div
171
+ class="resource-view__hour-row{isOff ? ' hour-slot--off' : ''}"
172
+ style:top={`${hr * DEFAULT_HOUR_HEIGHT}px`}
173
+ style:height={`${DEFAULT_HOUR_HEIGHT}px`}
174
+ >
175
+ <div class="resource-view__hour-label">
176
+ {hr > 0 ? `${String(hr).padStart(2, '0')}:00` : ''}
177
+ </div>
178
+ </div>
179
+ {/each}
180
+
181
+ {#each visibleResources as r, idx (`col-${r.id}`)}
182
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
183
+ <div
184
+ class="resource-view__resource-col"
185
+ role="gridcell"
186
+ tabindex="-1"
187
+ style:left={`calc(${DEFAULT_TIME_LABEL_WIDTH_DAY}px + ${idx} * ${colWidth})`}
188
+ style:width={colWidth}
189
+ onmousedown={handleColumnMouseDown}
190
+ onclick={handleColumnClick}
191
+ ondblclick={(e) => handleColumnDoubleClick(e, r.id)}
192
+ ></div>
193
+ {/each}
194
+
195
+ {#each visibleResources as r, idx (`evcol-${r.id}`)}
196
+ {#each (timedLayoutByResource.get(r.id) ?? []) as item (`${item.segment.event.id}-${item.column}`)}
197
+ <TimeEvent
198
+ event={item.segment.event}
199
+ layout={{
200
+ dayCol: idx,
201
+ dayCount: visibleResources.length,
202
+ lane: item.column,
203
+ laneCount: item.totalColumns,
204
+ startOffset: item.startOffset,
205
+ endOffset: item.endOffset,
206
+ }}
207
+ dayDate={currentDate}
208
+ hourHeight={DEFAULT_HOUR_HEIGHT}
209
+ timeLabelWidth={DEFAULT_TIME_LABEL_WIDTH_DAY}
210
+ ghost={ctx.previewEvent?.id === item.segment.event.id}
211
+ />
212
+ {/each}
213
+ {/each}
214
+
215
+ {#if isToday}
216
+ <div
217
+ class="resource-view__now-line"
218
+ style:top={`${nowHours * DEFAULT_HOUR_HEIGHT}px`}
219
+ style:left={`${DEFAULT_TIME_LABEL_WIDTH_DAY}px`}
220
+ >
221
+ <div class="resource-view__now-dot"></div>
222
+ </div>
223
+ {/if}
224
+ </div>
225
+ </div>
226
+ </div>
227
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { CalendarDateTime, Resource } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ currentDate: CalendarDateTime;
4
+ visibleResources: readonly Resource[];
5
+ onResourceCellClick: (date: CalendarDateTime, time: string, resourceId: string, rect: DOMRect) => void;
6
+ onResourceDrop: (e: DragEvent, date: CalendarDateTime, time: string, resourceId: string) => void;
7
+ };
8
+ declare const ResourceView: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type ResourceView = ReturnType<typeof ResourceView>;
10
+ export default ResourceView;
11
+ //# sourceMappingURL=ResourceView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/resource/ResourceView.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAYvF,KAAK,gBAAgB,GAAI;IACtB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,gBAAgB,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,mBAAmB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvG,cAAc,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAClG,CAAC;AA8LJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import type { CalendarEvent, CalendarDateTime, Resource, LayoutItem } from '@bimetal/calendar-core';
3
+ import {
4
+ startOfDay, addDays, isSameDay, now,
5
+ segmentInRange, layoutAllDay, maxLanes,
6
+ } from '@bimetal/calendar-core';
7
+ import { getCalendarConfig, getCalendarContext } from '../../binding/index.js';
8
+ import { DEFAULT_RESOURCE_LABEL_WIDTH, createClickIntent } from '@bimetal/calendar-headless';
9
+ import EventBar from '../../components/EventBar.svelte';
10
+
11
+
12
+ let {
13
+ currentDate,
14
+ days,
15
+ visibleResources,
16
+ onTimelineCellClick,
17
+ onTimelineDrop,
18
+ }: {
19
+ currentDate: CalendarDateTime;
20
+ days: number;
21
+ visibleResources: readonly Resource[];
22
+ onTimelineCellClick: (date: CalendarDateTime, resourceId: string, rect: DOMRect) => void;
23
+ onTimelineDrop: (e: DragEvent, date: CalendarDateTime, resourceId: string) => void;
24
+ } = $props();
25
+
26
+ const config = getCalendarConfig();
27
+ const ctx = getCalendarContext<{
28
+ events: CalendarEvent[];
29
+ previewEvent: CalendarEvent | null;
30
+ onDragOverDate: (date: CalendarDateTime) => void;
31
+ onHidePreview: () => void;
32
+ selectedEventId: string | null;
33
+ onClearSelection: () => void;
34
+ }>();
35
+
36
+ const cellClickIntent = createClickIntent();
37
+
38
+ const dates = $derived.by<CalendarDateTime[]>(() => {
39
+ const start = startOfDay(currentDate);
40
+ return Array.from({ length: days }, (_, i) => addDays(start, i));
41
+ });
42
+
43
+ const dateFmt = $derived(new Intl.DateTimeFormat(config.locale.code, {
44
+ weekday: 'short', day: 'numeric', month: 'short', timeZone: config.core.timezone,
45
+ }));
46
+
47
+ // B-CAL-1: Der Tag-Shift-Tanz, der die Uhrzeit erhielt, lebt jetzt im
48
+ // Controller (`computeMoveRange` ist ein reiner addDays). Einfach das neu
49
+ // gebaute Event per id eintauschen — die Timeline rekonstruiert nichts mehr.
50
+ const displayEvents = $derived.by<CalendarEvent[]>(() => {
51
+ const pv = ctx.previewEvent;
52
+ if (!pv) return ctx.events;
53
+ return ctx.events.map(ev => ev.id === pv.id ? { ...ev, timeRange: pv.timeRange } : ev);
54
+ });
55
+
56
+ const lanesByResource = $derived.by(() => {
57
+ const map = new Map<string, LayoutItem[]>();
58
+ for (const r of visibleResources) {
59
+ const own = displayEvents.filter(e => e.resourceId === r.id);
60
+ const segments = segmentInRange(own, dates);
61
+ map.set(r.id, layoutAllDay(segments, dates));
62
+ }
63
+ return map;
64
+ });
65
+
66
+ const today = $derived(now(config.core.timezone, config.core.clock));
67
+ const colTemplate = $derived(`${DEFAULT_RESOURCE_LABEL_WIDTH}px repeat(${days}, 1fr)`);
68
+
69
+ function dayFromX(clientX: number, laneRect: DOMRect): CalendarDateTime {
70
+ const x = Math.max(0, clientX - laneRect.left);
71
+ const colWidth = laneRect.width / dates.length;
72
+ const idx = Math.min(dates.length - 1, Math.floor(x / colWidth));
73
+ return dates[idx];
74
+ }
75
+
76
+ function cellMouseDown() {
77
+ cellClickIntent.markMouseDown();
78
+ }
79
+
80
+ function cellClick() {
81
+ if (!cellClickIntent.accept(config.clickThresholdMs)) return;
82
+ // Single click deselects; dblclick opens the dialog.
83
+ if (ctx.selectedEventId !== null) ctx.onClearSelection();
84
+ }
85
+
86
+ function cellDoubleClick(e: MouseEvent, date: CalendarDateTime, resourceId: string) {
87
+ if (!cellClickIntent.accept(config.clickThresholdMs)) return;
88
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
89
+ onTimelineCellClick(date, resourceId, rect);
90
+ }
91
+
92
+ function handleLaneDragOver(e: DragEvent) {
93
+ e.preventDefault();
94
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
95
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
96
+ ctx.onDragOverDate(dayFromX(e.clientX, rect));
97
+ }
98
+
99
+ function handleLaneDrop(e: DragEvent, resourceId: string) {
100
+ e.preventDefault();
101
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
102
+ onTimelineDrop(e, dayFromX(e.clientX, rect), resourceId);
103
+ }
104
+
105
+ function handleLaneDragLeave(e: DragEvent) {
106
+ const el = e.currentTarget as HTMLElement;
107
+ const rect = el.getBoundingClientRect();
108
+ if (e.clientX < rect.left || e.clientX > rect.right
109
+ || e.clientY < rect.top || e.clientY > rect.bottom) {
110
+ ctx.onHidePreview();
111
+ }
112
+ }
113
+ </script>
114
+
115
+ {#if visibleResources.length === 0}
116
+ <div class="timeline-view timeline-view--empty">
117
+ {config.locale.unassigned ?? 'No resource selected'}
118
+ </div>
119
+ {:else}
120
+ <div class="timeline-view">
121
+ <div class="timeline-view__header" style:grid-template-columns={colTemplate}>
122
+ <div class="timeline-view__corner"></div>
123
+ {#each dates as d (d.epochMs)}
124
+ {@const isToday = isSameDay(d, today)}
125
+ <div class="timeline-view__date-header{isToday ? ' timeline-view__date-header--today' : ''}">
126
+ {dateFmt.format(new Date(d.epochMs))}
127
+ </div>
128
+ {/each}
129
+ </div>
130
+
131
+ <div class="timeline-view__body">
132
+ {#each visibleResources as r (r.id)}
133
+ {@const items = lanesByResource.get(r.id) ?? []}
134
+ {@const lanes = Math.max(1, maxLanes(items))}
135
+ <div class="timeline-view__lane">
136
+ <div class="timeline-view__resource-label" style:width={`${DEFAULT_RESOURCE_LABEL_WIDTH}px`}>
137
+ {#if r.color}
138
+ <span class="timeline-view__resource-dot" style:background-color={r.color}></span>
139
+ {/if}
140
+ <span class="timeline-view__resource-name">{r.name}</span>
141
+ </div>
142
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
143
+ <div
144
+ class="timeline-view__lane-content"
145
+ style:display="grid"
146
+ style:grid-template-columns={`repeat(${days}, 1fr)`}
147
+ style:grid-auto-rows="32px"
148
+ style:min-height={`${lanes * 32}px`}
149
+ ondragover={handleLaneDragOver}
150
+ ondrop={(e) => handleLaneDrop(e, r.id)}
151
+ ondragleave={handleLaneDragLeave}
152
+ >
153
+ {#each dates as d, i (`cell-${d.epochMs}`)}
154
+ {@const isToday = isSameDay(d, today)}
155
+ <button
156
+ type="button"
157
+ class="timeline-view__cell{isToday ? ' timeline-view__cell--today' : ''}"
158
+ style:grid-column={i + 1}
159
+ style:grid-row="1 / -1"
160
+ aria-label="{r.name}, {dateFmt.format(new Date(d.epochMs))}"
161
+ onmousedown={cellMouseDown}
162
+ onclick={cellClick}
163
+ ondblclick={(e) => cellDoubleClick(e, d, r.id)}
164
+ ></button>
165
+ {/each}
166
+ {#each items as item, idx (`${item.segment.event.id}-${idx}`)}
167
+ <EventBar
168
+ item={item}
169
+ weekDates={dates}
170
+ ghost={ctx.previewEvent?.id === item.segment.event.id}
171
+ />
172
+ {/each}
173
+ </div>
174
+ </div>
175
+ {/each}
176
+ </div>
177
+ </div>
178
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { CalendarDateTime, Resource } from '@bimetal/calendar-core';
2
+ type $$ComponentProps = {
3
+ currentDate: CalendarDateTime;
4
+ days: number;
5
+ visibleResources: readonly Resource[];
6
+ onTimelineCellClick: (date: CalendarDateTime, resourceId: string, rect: DOMRect) => void;
7
+ onTimelineDrop: (e: DragEvent, date: CalendarDateTime, resourceId: string) => void;
8
+ };
9
+ declare const TimelineView: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type TimelineView = ReturnType<typeof TimelineView>;
11
+ export default TimelineView;
12
+ //# sourceMappingURL=TimelineView.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimelineView.svelte.d.ts","sourceRoot":"","sources":["../../../src/views/timeline/TimelineView.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAiB,gBAAgB,EAAE,QAAQ,EAAc,MAAM,wBAAwB,CAAC;AASnG,KAAK,gBAAgB,GAAI;IACtB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,mBAAmB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACzF,cAAc,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACpF,CAAC;AAuJJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}