@dodlhuat/basix 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +56 -1
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/button.scss +48 -0
  5. package/css/calendar.scss +957 -0
  6. package/css/card.scss +65 -65
  7. package/css/chart.scss +270 -157
  8. package/css/chat-bubbles.scss +134 -68
  9. package/css/chips.scss +109 -19
  10. package/css/colors.scss +32 -32
  11. package/css/datepicker.scss +336 -336
  12. package/css/defaults.scss +90 -90
  13. package/css/docs.scss +529 -0
  14. package/css/editor.scss +36 -0
  15. package/css/file-uploader.scss +1 -1
  16. package/css/flyout-menu.scss +361 -361
  17. package/css/form.scss +0 -15
  18. package/css/gallery.scss +65 -6
  19. package/css/grid.scss +41 -40
  20. package/css/group-picker.scss +345 -0
  21. package/css/guitar-chords.css +250 -250
  22. package/css/icons.scss +330 -330
  23. package/css/parameters.scss +3 -3
  24. package/css/placeholder.scss +33 -33
  25. package/css/popover.scss +206 -0
  26. package/css/progress.scss +76 -32
  27. package/css/properties.scss +51 -36
  28. package/css/push-menu.scss +302 -174
  29. package/css/reset.scss +39 -39
  30. package/css/scrollbar.scss +62 -5
  31. package/css/sidebar-nav.scss +92 -0
  32. package/css/spinner.scss +65 -65
  33. package/css/stepper.scss +48 -12
  34. package/css/style.css +3159 -254
  35. package/css/style.css.map +1 -1
  36. package/css/style.min.css +1 -1
  37. package/css/style.scss +51 -45
  38. package/css/table.scss +199 -199
  39. package/css/tabs.scss +154 -123
  40. package/css/timeline.scss +83 -38
  41. package/css/timepicker.scss +100 -5
  42. package/css/toast.scss +81 -81
  43. package/css/virtual-dropdown.scss +35 -29
  44. package/js/calendar.js +532 -0
  45. package/js/calendar.ts +706 -0
  46. package/js/chart.js +573 -257
  47. package/js/chart.ts +692 -0
  48. package/js/code-viewer.js +10 -10
  49. package/js/code-viewer.ts +188 -188
  50. package/js/datepicker.ts +627 -627
  51. package/js/docs-nav.js +204 -0
  52. package/js/dropdown.ts +179 -179
  53. package/js/editor.js +50 -6
  54. package/js/editor.ts +483 -444
  55. package/js/file-uploader.js +1 -0
  56. package/js/file-uploader.ts +1 -0
  57. package/js/flyout-menu.js +14 -14
  58. package/js/flyout-menu.ts +249 -249
  59. package/js/form-builder.js +106 -106
  60. package/js/gallery.js +14 -8
  61. package/js/gallery.ts +245 -236
  62. package/js/group-picker.js +342 -0
  63. package/js/group-picker.ts +447 -0
  64. package/js/guitar-chords.js +268 -268
  65. package/js/lazy-loader.js +121 -121
  66. package/js/modal.ts +166 -166
  67. package/js/popover.js +163 -0
  68. package/js/popover.ts +219 -0
  69. package/js/position.js +108 -0
  70. package/js/position.ts +111 -0
  71. package/js/push-menu.js +113 -0
  72. package/js/push-menu.ts +284 -145
  73. package/js/request.js +50 -50
  74. package/js/scroll.ts +47 -47
  75. package/js/scrollbar.js +13 -0
  76. package/js/scrollbar.ts +324 -307
  77. package/js/select.ts +216 -216
  78. package/js/sidebar-nav.js +41 -0
  79. package/js/sidebar-nav.ts +66 -0
  80. package/js/table.ts +452 -452
  81. package/js/tabs.ts +279 -279
  82. package/js/theme.js +17 -6
  83. package/js/theme.ts +234 -224
  84. package/js/toast.ts +137 -137
  85. package/js/tooltip.js +6 -60
  86. package/js/tooltip.ts +184 -251
  87. package/js/tsconfig.json +18 -18
  88. package/js/utils.ts +83 -83
  89. package/js/virtual-dropdown.js +25 -25
  90. package/js/virtual-dropdown.ts +365 -365
  91. package/package.json +39 -39
  92. package/js/index.js +0 -816
  93. package/js/index.ts +0 -987
package/js/calendar.js ADDED
@@ -0,0 +1,532 @@
1
+ // ============================================================
2
+ // calendar.ts — Basix Calendar Component
3
+ // Integrates with @dodlhuat/basix design tokens & conventions
4
+ // ============================================================
5
+ // -----------------------------------------------------------
6
+ // Date Logic (pure functions, no side effects)
7
+ // -----------------------------------------------------------
8
+ export const CalendarLogic = {
9
+ /**
10
+ * Returns all days to render for a month grid (including leading/trailing
11
+ * days from adjacent months to fill the 7-column grid).
12
+ */
13
+ getMonthGrid(year, month, firstDayOfWeek) {
14
+ const firstOfMonth = new Date(year, month, 1);
15
+ const lastOfMonth = new Date(year, month + 1, 0);
16
+ // Leading days from previous month
17
+ let startDow = firstOfMonth.getDay() - firstDayOfWeek;
18
+ if (startDow < 0)
19
+ startDow += 7;
20
+ const days = [];
21
+ for (let i = startDow; i > 0; i--) {
22
+ days.push(new Date(year, month, 1 - i));
23
+ }
24
+ for (let d = 1; d <= lastOfMonth.getDate(); d++) {
25
+ days.push(new Date(year, month, d));
26
+ }
27
+ // Trailing days to fill remaining cells (always complete the row)
28
+ const remaining = 7 - (days.length % 7);
29
+ if (remaining < 7) {
30
+ for (let i = 1; i <= remaining; i++) {
31
+ days.push(new Date(year, month + 1, i));
32
+ }
33
+ }
34
+ return days;
35
+ },
36
+ /** Returns the 7 dates of the week containing `date`. */
37
+ getWeekDays(date, firstDayOfWeek) {
38
+ const d = new Date(date);
39
+ const dow = d.getDay();
40
+ let diff = dow - firstDayOfWeek;
41
+ if (diff < 0)
42
+ diff += 7;
43
+ d.setDate(d.getDate() - diff);
44
+ return Array.from({ length: 7 }, (_, i) => {
45
+ const day = new Date(d);
46
+ day.setDate(d.getDate() + i);
47
+ return day;
48
+ });
49
+ },
50
+ isSameDay(a, b) {
51
+ return (a.getFullYear() === b.getFullYear() &&
52
+ a.getMonth() === b.getMonth() &&
53
+ a.getDate() === b.getDate());
54
+ },
55
+ isToday(date) {
56
+ return CalendarLogic.isSameDay(date, new Date());
57
+ },
58
+ isCurrentMonth(date, year, month) {
59
+ return date.getFullYear() === year && date.getMonth() === month;
60
+ },
61
+ /** Returns all events that fall (fully or partially) on a given day. */
62
+ getEventsForDay(events, day) {
63
+ const dayStart = new Date(day);
64
+ dayStart.setHours(0, 0, 0, 0);
65
+ const dayEnd = new Date(day);
66
+ dayEnd.setHours(23, 59, 59, 999);
67
+ return events.filter((e) => e.start <= dayEnd && e.end >= dayStart);
68
+ },
69
+ /** Returns only allDay events for a day. */
70
+ getAllDayEvents(events, day) {
71
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => e.allDay);
72
+ },
73
+ /** Returns only timed events for a day. */
74
+ getTimedEvents(events, day) {
75
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => !e.allDay);
76
+ },
77
+ /** Returns top-offset % and height % for a timed event within a day column. */
78
+ getEventPosition(event, day) {
79
+ const dayStart = new Date(day);
80
+ dayStart.setHours(0, 0, 0, 0);
81
+ const dayEnd = new Date(day);
82
+ dayEnd.setHours(24, 0, 0, 0);
83
+ const totalMs = 24 * 60 * 60 * 1000;
84
+ const startMs = Math.max(event.start.getTime(), dayStart.getTime()) - dayStart.getTime();
85
+ const endMs = Math.min(event.end.getTime(), dayEnd.getTime()) - dayStart.getTime();
86
+ return {
87
+ top: (startMs / totalMs) * 100,
88
+ height: Math.max(((endMs - startMs) / totalMs) * 100, 2), // min 2%
89
+ };
90
+ },
91
+ formatTime(date) {
92
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
93
+ },
94
+ };
95
+ // -----------------------------------------------------------
96
+ // Default Locale
97
+ // -----------------------------------------------------------
98
+ const DEFAULT_LOCALE = {
99
+ monthNames: [
100
+ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
101
+ 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
102
+ ],
103
+ dayNamesShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
104
+ dayNamesFull: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
105
+ firstDayOfWeek: 1,
106
+ today: 'Heute',
107
+ month: 'Monat',
108
+ week: 'Woche',
109
+ agenda: 'Agenda',
110
+ allDay: 'Ganztägig',
111
+ noEvents: 'Keine Termine',
112
+ };
113
+ // -----------------------------------------------------------
114
+ // Renderer — builds DOM from CalendarLogic output
115
+ // -----------------------------------------------------------
116
+ export class CalendarRenderer {
117
+ constructor(locale) {
118
+ this.locale = locale;
119
+ }
120
+ /** Ordered day-name headers respecting firstDayOfWeek */
121
+ renderWeekdayHeaders() {
122
+ const { dayNamesShort, firstDayOfWeek } = this.locale;
123
+ const ordered = [
124
+ ...dayNamesShort.slice(firstDayOfWeek),
125
+ ...dayNamesShort.slice(0, firstDayOfWeek),
126
+ ];
127
+ return ordered
128
+ .map((name) => `<div class="cal__weekday" aria-label="${name}">${name}</div>`)
129
+ .join('');
130
+ }
131
+ renderEvent(event, compact = false) {
132
+ const extraClass = event.className ?? '';
133
+ if (compact) {
134
+ return `<div class="cal__event-pill ${extraClass}"
135
+ data-event-id="${event.id}"
136
+ role="button"
137
+ tabindex="0"
138
+ aria-label="${event.title}"
139
+ title="${event.title}">${event.title}</div>`;
140
+ }
141
+ return `<div class="cal__event-pill ${extraClass}"
142
+ data-event-id="${event.id}"
143
+ role="button"
144
+ tabindex="0"
145
+ aria-label="${event.title}, ${CalendarLogic.formatTime(event.start)} – ${CalendarLogic.formatTime(event.end)}"
146
+ title="${event.title}">
147
+ <span class="cal__event-time">${CalendarLogic.formatTime(event.start)}</span>
148
+ ${event.title}
149
+ </div>`;
150
+ }
151
+ renderMonthDay(date, currentMonth, currentYear, events, selectedDate, showOutsideDays) {
152
+ const dayEvents = CalendarLogic.getEventsForDay(events, date);
153
+ const isToday = CalendarLogic.isToday(date);
154
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(date, selectedDate) : false;
155
+ const isOutside = !CalendarLogic.isCurrentMonth(date, currentYear, currentMonth);
156
+ if (isOutside && !showOutsideDays) {
157
+ return `<div class="cal__day cal__day--empty" aria-hidden="true"></div>`;
158
+ }
159
+ const classes = [
160
+ 'cal__day',
161
+ isToday ? 'is-today' : '',
162
+ isSelected ? 'is-selected' : '',
163
+ isOutside ? 'cal__day--outside' : '',
164
+ dayEvents.length > 0 ? 'has-events' : '',
165
+ ]
166
+ .filter(Boolean)
167
+ .join(' ');
168
+ const eventsHtml = dayEvents
169
+ .slice(0, 3)
170
+ .map((e) => this.renderEvent(e, true))
171
+ .join('');
172
+ const moreCount = dayEvents.length - 3;
173
+ const moreHtml = moreCount > 0
174
+ ? `<div class="cal__event-more">+${moreCount}</div>`
175
+ : '';
176
+ return `<div class="${classes}"
177
+ role="gridcell"
178
+ tabindex="0"
179
+ aria-label="${date.toLocaleDateString()}"
180
+ aria-selected="${isSelected}"
181
+ data-date="${date.toISOString()}">
182
+ <span class="cal__day-num">${date.getDate()}</span>
183
+ <div class="cal__day-events">${eventsHtml}${moreHtml}</div>
184
+ </div>`;
185
+ }
186
+ renderMonthView(year, month, events, selectedDate, showOutsideDays, firstDayOfWeek) {
187
+ const days = CalendarLogic.getMonthGrid(year, month, firstDayOfWeek);
188
+ const cells = days
189
+ .map((d) => this.renderMonthDay(d, month, year, events, selectedDate, showOutsideDays))
190
+ .join('');
191
+ return `<div class="cal__month-grid" role="grid" aria-label="${this.locale.monthNames[month]} ${year}">
192
+ ${this.renderWeekdayHeaders()}
193
+ ${cells}
194
+ </div>`;
195
+ }
196
+ renderWeekView(date, events, selectedDate, firstDayOfWeek) {
197
+ const days = CalendarLogic.getWeekDays(date, firstDayOfWeek);
198
+ // All-day row
199
+ const allDayCols = days
200
+ .map((d) => {
201
+ const adEvents = CalendarLogic.getAllDayEvents(events, d);
202
+ const pills = adEvents.map((e) => this.renderEvent(e, true)).join('');
203
+ return `<div class="cal__allday-col">${pills}</div>`;
204
+ })
205
+ .join('');
206
+ // Day column headers
207
+ const headCols = days
208
+ .map((d) => {
209
+ const isToday = CalendarLogic.isToday(d);
210
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(d, selectedDate) : false;
211
+ const classes = [
212
+ 'cal__week-head-day',
213
+ isToday ? 'is-today' : '',
214
+ isSelected ? 'is-selected' : '',
215
+ ]
216
+ .filter(Boolean)
217
+ .join(' ');
218
+ const dow = this.locale.dayNamesShort[(d.getDay() + 7) % 7];
219
+ return `<div class="${classes}" data-date="${d.toISOString()}">
220
+ ${dow}<span>${d.getDate()}</span>
221
+ </div>`;
222
+ })
223
+ .join('');
224
+ // Hour slots + events
225
+ const hourLabels = Array.from({ length: 24 }, (_, h) => {
226
+ const label = h === 0 ? '' : `${String(h).padStart(2, '0')}:00`;
227
+ return `<div class="cal__time-slot">${label}</div>`;
228
+ }).join('');
229
+ const dayCols = days
230
+ .map((d) => {
231
+ const timedEvents = CalendarLogic.getTimedEvents(events, d);
232
+ const hourCells = Array.from({ length: 24 }, () => `<div class="cal__day-col-hour"></div>`).join('');
233
+ const eventOverlays = timedEvents
234
+ .map((e) => {
235
+ const { top, height } = CalendarLogic.getEventPosition(e, d);
236
+ const extraClass = e.className ?? '';
237
+ return `<div class="cal__week-event ${extraClass}"
238
+ style="top:${top.toFixed(2)}%;height:${height.toFixed(2)}%"
239
+ data-event-id="${e.id}"
240
+ role="button"
241
+ tabindex="0"
242
+ aria-label="${e.title}">
243
+ <span class="cal__event-time">${CalendarLogic.formatTime(e.start)}</span>
244
+ ${e.title}
245
+ </div>`;
246
+ })
247
+ .join('');
248
+ return `<div class="cal__day-col" data-date="${d.toISOString()}">${hourCells}${eventOverlays}</div>`;
249
+ })
250
+ .join('');
251
+ return `<div class="cal__week" role="grid">
252
+ <div class="cal__week-head">
253
+ <div class="cal__week-head-time"></div>
254
+ ${headCols}
255
+ </div>
256
+ <div class="cal__allday">
257
+ <div class="cal__allday-label">${this.locale.allDay}</div>
258
+ ${allDayCols}
259
+ </div>
260
+ <div class="cal__week-body">
261
+ <div class="cal__week-grid">
262
+ <div class="cal__time-col">${hourLabels}</div>
263
+ ${dayCols}
264
+ </div>
265
+ </div>
266
+ </div>`;
267
+ }
268
+ renderAgendaView(year, month, events) {
269
+ // Collect all days in this month that have events
270
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
271
+ let html = '';
272
+ for (let d = 1; d <= daysInMonth; d++) {
273
+ const day = new Date(year, month, d);
274
+ const dayEvents = CalendarLogic.getEventsForDay(events, day);
275
+ if (dayEvents.length === 0)
276
+ continue;
277
+ const isToday = CalendarLogic.isToday(day);
278
+ const dow = this.locale.dayNamesFull[day.getDay()];
279
+ html += `<div class="cal__agenda-day ${isToday ? 'is-today' : ''}">
280
+ <div class="cal__agenda-date">
281
+ <span class="cal__agenda-dow">${dow}</span>
282
+ <span class="cal__agenda-num ${isToday ? 'is-today' : ''}">${d}</span>
283
+ </div>
284
+ <div class="cal__agenda-events">
285
+ ${dayEvents.map((e) => `
286
+ <div class="cal__agenda-event ${e.className ?? ''}"
287
+ data-event-id="${e.id}"
288
+ role="button"
289
+ tabindex="0">
290
+ <span class="cal__agenda-event-time">
291
+ ${e.allDay ? this.locale.allDay : CalendarLogic.formatTime(e.start) + ' – ' + CalendarLogic.formatTime(e.end)}
292
+ </span>
293
+ <span class="cal__agenda-event-title">${e.title}</span>
294
+ </div>`).join('')}
295
+ </div>
296
+ </div>`;
297
+ }
298
+ if (!html) {
299
+ html = `<div class="cal__agenda-empty">${this.locale.noEvents}</div>`;
300
+ }
301
+ return `<div class="cal__agenda">${html}</div>`;
302
+ }
303
+ }
304
+ // -----------------------------------------------------------
305
+ // Calendar — main controller class
306
+ // -----------------------------------------------------------
307
+ export class Calendar {
308
+ constructor(options) {
309
+ this.selectedDate = null;
310
+ this.events = [];
311
+ // Resolve container
312
+ if (typeof options.container === 'string') {
313
+ const el = document.querySelector(options.container);
314
+ if (!el)
315
+ throw new Error(`Calendar: container "${options.container}" not found.`);
316
+ this.container = el;
317
+ }
318
+ else {
319
+ this.container = options.container;
320
+ }
321
+ this.locale = { ...DEFAULT_LOCALE, ...(options.locale ?? {}) };
322
+ this.renderer = new CalendarRenderer(this.locale);
323
+ this.options = {
324
+ container: this.container,
325
+ events: options.events ?? [],
326
+ view: options.view ?? 'month',
327
+ locale: options.locale ?? {},
328
+ showOutsideDays: options.showOutsideDays ?? true,
329
+ onDayClick: options.onDayClick ?? (() => { }),
330
+ onEventClick: options.onEventClick ?? (() => { }),
331
+ onChange: options.onChange ?? (() => { }),
332
+ className: options.className ?? '',
333
+ };
334
+ this.events = [...this.options.events];
335
+ this.currentView = this.options.view;
336
+ this.currentDate = new Date();
337
+ this.render();
338
+ this.attachEvents();
339
+ }
340
+ // ----------------------------------------------------------
341
+ // Public API
342
+ // ----------------------------------------------------------
343
+ setView(view) {
344
+ this.currentView = view;
345
+ this.render();
346
+ this.options.onChange(this.currentDate, this.currentView);
347
+ }
348
+ next() {
349
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
350
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1);
351
+ }
352
+ else {
353
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), this.currentDate.getDate() + 7);
354
+ }
355
+ this.render();
356
+ this.options.onChange(this.currentDate, this.currentView);
357
+ }
358
+ prev() {
359
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
360
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1);
361
+ }
362
+ else {
363
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), this.currentDate.getDate() - 7);
364
+ }
365
+ this.render();
366
+ this.options.onChange(this.currentDate, this.currentView);
367
+ }
368
+ today() {
369
+ this.currentDate = new Date();
370
+ this.render();
371
+ this.options.onChange(this.currentDate, this.currentView);
372
+ }
373
+ addEvent(event) {
374
+ this.events.push(event);
375
+ this.render();
376
+ }
377
+ removeEvent(id) {
378
+ this.events = this.events.filter((e) => e.id !== id);
379
+ this.render();
380
+ }
381
+ setEvents(events) {
382
+ this.events = [...events];
383
+ this.render();
384
+ }
385
+ getEvents() {
386
+ return [...this.events];
387
+ }
388
+ destroy() {
389
+ this.container.innerHTML = '';
390
+ this.container.removeAttribute('data-cal');
391
+ }
392
+ // ----------------------------------------------------------
393
+ // Internal rendering
394
+ // ----------------------------------------------------------
395
+ getTitle() {
396
+ const { monthNames } = this.locale;
397
+ const y = this.currentDate.getFullYear();
398
+ const m = this.currentDate.getMonth();
399
+ if (this.currentView === 'week') {
400
+ const days = CalendarLogic.getWeekDays(this.currentDate, this.locale.firstDayOfWeek);
401
+ const first = days[0];
402
+ const last = days[6];
403
+ if (first.getMonth() === last.getMonth()) {
404
+ return `${monthNames[first.getMonth()]} ${y}`;
405
+ }
406
+ return `${monthNames[first.getMonth()]} – ${monthNames[last.getMonth()]} ${y}`;
407
+ }
408
+ return `${monthNames[m]} ${y}`;
409
+ }
410
+ buildHeader() {
411
+ const activeMonth = this.currentView === 'month' || this.currentView === 'agenda' ? 'cal__btn--active' : '';
412
+ const activeWeek = this.currentView === 'week' ? 'cal__btn--active' : '';
413
+ const activeAgenda = this.currentView === 'agenda' ? 'cal__btn--active' : '';
414
+ return `<div class="cal__header">
415
+ <div class="cal__nav">
416
+ <button class="cal__btn cal__btn--today" data-action="today" aria-label="${this.locale.today}">${this.locale.today}</button>
417
+ <button class="cal__btn" data-action="prev" aria-label="Zurück">
418
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_left"/></svg>
419
+ </button>
420
+ <button class="cal__btn" data-action="next" aria-label="Vor">
421
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_right"/></svg>
422
+ </button>
423
+ </div>
424
+ <h2 class="cal__title" aria-live="polite">${this.getTitle()}</h2>
425
+ <div class="cal__view-toggle" role="group" aria-label="Ansicht wählen">
426
+ <button class="cal__btn ${activeMonth}" data-action="view-month" aria-pressed="${this.currentView === 'month'}">${this.locale.month}</button>
427
+ <button class="cal__btn ${activeWeek}" data-action="view-week" aria-pressed="${this.currentView === 'week'}">${this.locale.week}</button>
428
+ <button class="cal__btn ${activeAgenda}" data-action="view-agenda" aria-pressed="${this.currentView === 'agenda'}">${this.locale.agenda}</button>
429
+ </div>
430
+ </div>`;
431
+ }
432
+ buildBody() {
433
+ const { firstDayOfWeek } = this.locale;
434
+ const y = this.currentDate.getFullYear();
435
+ const m = this.currentDate.getMonth();
436
+ switch (this.currentView) {
437
+ case 'month':
438
+ return this.renderer.renderMonthView(y, m, this.events, this.selectedDate, this.options.showOutsideDays, firstDayOfWeek);
439
+ case 'week':
440
+ return this.renderer.renderWeekView(this.currentDate, this.events, this.selectedDate, firstDayOfWeek);
441
+ case 'agenda':
442
+ return this.renderer.renderAgendaView(y, m, this.events);
443
+ }
444
+ }
445
+ render() {
446
+ const rootClass = ['cal', this.options.className].filter(Boolean).join(' ');
447
+ this.container.setAttribute('data-cal', this.currentView);
448
+ this.container.innerHTML = `<div class="${rootClass}" role="application" aria-label="Kalender">
449
+ ${this.buildHeader()}
450
+ <div class="cal__body">
451
+ ${this.buildBody()}
452
+ </div>
453
+ </div>`;
454
+ }
455
+ // ----------------------------------------------------------
456
+ // Event delegation
457
+ // ----------------------------------------------------------
458
+ attachEvents() {
459
+ this.container.addEventListener('click', (e) => this.handleClick(e));
460
+ this.container.addEventListener('keydown', (e) => this.handleKeydown(e));
461
+ }
462
+ handleClick(e) {
463
+ const target = e.target;
464
+ // Nav / view buttons
465
+ const btn = target.closest('[data-action]');
466
+ if (btn) {
467
+ const action = btn.dataset.action;
468
+ if (action === 'prev')
469
+ this.prev();
470
+ else if (action === 'next')
471
+ this.next();
472
+ else if (action === 'today')
473
+ this.today();
474
+ else if (action === 'view-month')
475
+ this.setView('month');
476
+ else if (action === 'view-week')
477
+ this.setView('week');
478
+ else if (action === 'view-agenda')
479
+ this.setView('agenda');
480
+ return;
481
+ }
482
+ // Event click
483
+ const eventEl = target.closest('[data-event-id]');
484
+ if (eventEl) {
485
+ const id = eventEl.dataset.eventId;
486
+ const event = this.events.find((ev) => ev.id === id);
487
+ if (event) {
488
+ e.stopPropagation();
489
+ this.options.onEventClick(event);
490
+ }
491
+ return;
492
+ }
493
+ // Day click
494
+ const dayEl = target.closest('[data-date]');
495
+ if (dayEl && dayEl.dataset.date) {
496
+ const date = new Date(dayEl.dataset.date);
497
+ this.selectedDate = date;
498
+ this.options.onDayClick(date);
499
+ // Re-render to update selection state
500
+ this.render();
501
+ }
502
+ }
503
+ handleKeydown(e) {
504
+ const target = e.target;
505
+ // Allow Enter/Space to trigger click on focused interactive elements
506
+ if (e.key === 'Enter' || e.key === ' ') {
507
+ if (target.closest('[data-date], [data-event-id], [data-action]')) {
508
+ e.preventDefault();
509
+ target.click();
510
+ }
511
+ }
512
+ // Arrow key navigation within month grid
513
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key))
514
+ return;
515
+ const dayEl = target.closest('.cal__day[data-date]');
516
+ if (!dayEl)
517
+ return;
518
+ e.preventDefault();
519
+ const all = Array.from(this.container.querySelectorAll('.cal__day[data-date]:not(.cal__day--empty)'));
520
+ const idx = all.indexOf(dayEl);
521
+ let next = idx;
522
+ if (e.key === 'ArrowRight')
523
+ next = idx + 1;
524
+ else if (e.key === 'ArrowLeft')
525
+ next = idx - 1;
526
+ else if (e.key === 'ArrowDown')
527
+ next = idx + 7;
528
+ else if (e.key === 'ArrowUp')
529
+ next = idx - 7;
530
+ all[Math.max(0, Math.min(next, all.length - 1))]?.focus();
531
+ }
532
+ }