@open-rlb/ng-bootstrap 3.3.25 → 3.3.26

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.
@@ -0,0 +1,450 @@
1
+ ---
2
+ name: rlb-calendar
3
+ description: Expert guidance for the @open-rlb/ng-bootstrap rlb-calendar component: week/month/day views, event create/edit/delete, drag-and-drop, background intervals, and timezone-aware dates. Use when building, configuring, or debugging the calendar component.
4
+ ---
5
+
6
+ # RLB ng-Bootstrap Calendar Skill
7
+
8
+ You are an expert in the **@open-rlb/ng-bootstrap** calendar component (`rlb-calendar`). It provides week, month, and day views with built-in event management (create/edit/delete via modals), drag-and-drop repositioning, background time intervals, and timezone-aware date handling via `@open-rlb/date-tz`.
9
+
10
+ ## Core Types
11
+
12
+ ```typescript
13
+ import { IDateTz, DateTz } from '@open-rlb/date-tz';
14
+
15
+ type CalendarView = 'week' | 'month' | 'day';
16
+
17
+ // Color type: 'primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'
18
+
19
+ interface CalendarEvent<T = any> {
20
+ id: string | number;
21
+ title: string;
22
+ start: IDateTz;
23
+ end: IDateTz;
24
+ color?: Color;
25
+ allDay?: boolean;
26
+ data?: T; // generic payload for custom data
27
+ }
28
+
29
+ interface CalendarInterval {
30
+ dayWeek: number; // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
31
+ hourStart?: number; // Seconds from midnight (0-86400). Defaults to 0.
32
+ hourStop?: number; // Seconds from midnight (0-86400). Defaults to 86400 (end of day).
33
+ color?: Color; // Bootstrap color, defaults to 'success'
34
+ label?: string;
35
+ }
36
+
37
+ interface CalendarChangeEvent {
38
+ date: IDateTz;
39
+ view: CalendarView;
40
+ }
41
+
42
+ interface CalendarLayout {
43
+ rowHeight: number; // px per hour row, default 110
44
+ maxBodyHeight: number; // rem, scrollable body max height, default 30
45
+ minHeaderHeight: number; // rem, header min height, default 3.5
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Component API
52
+
53
+ ```html
54
+ <rlb-calendar
55
+ [(view)]="calendarView"
56
+ [(events)]="calendarEvents"
57
+ [(current-date)]="currentDate"
58
+ [intervals]="businessHours"
59
+ [loading]="isLoading"
60
+ [show-toolbar]="true"
61
+ [manage-events]="true"
62
+ [layout]="layoutOptions"
63
+ (date-change)="onDateChange($event)"
64
+ (view-change)="onViewChange($event)"
65
+ (event-click)="onEventClick($event)"
66
+ (container-event-click)="onContainerEventClick($event)"
67
+ ></rlb-calendar>
68
+ ```
69
+
70
+ ### Inputs
71
+
72
+ | Input | Type | Default | Description |
73
+ |-------|------|---------|-------------|
74
+ | `view` | `CalendarView` (model) | `'week'` | Active view — two-way bindable |
75
+ | `events` | `CalendarEvent[]` (model) | `[]` | Events array — two-way bindable. The calendar mutates this via built-in CRUD modals. |
76
+ | `current-date` | `IDateTz` (model) | today | Displayed date — two-way bindable |
77
+ | `intervals` | `CalendarInterval[]` | `[]` | Background time intervals rendered behind events (e.g. business hours). Non-interactive, purely visual. |
78
+ | `loading` | `boolean` | `false` | Show loading progress bar |
79
+ | `show-toolbar` | `boolean` | `true` | Show navigation toolbar with prev/today/next and view switcher |
80
+ | `manage-events` | `boolean` | `true` | Enable built-in CRUD (modals, toasts). Set to `false` to disable modals and overflow dialogs — `(event-click)`, `(container-event-click)` and DnD still work. |
81
+ | `layout` | `Partial<CalendarLayout>` | `{ rowHeight: 110, maxBodyHeight: 30, minHeaderHeight: 3.5 }` | Layout dimensions customization |
82
+
83
+ ### Outputs
84
+
85
+ | Output | Type | Description |
86
+ |--------|------|-------------|
87
+ | `date-change` | `CalendarChangeEvent` | Fired when user navigates dates via toolbar |
88
+ | `view-change` | `CalendarChangeEvent` | Fired when view mode changes via toolbar |
89
+ | `event-click` | `CalendarEvent` | Fired when user clicks an existing event (before the edit modal opens) |
90
+ | `container-event-click` | `CalendarEvent[]` | Fired when the "+N more" overflow indicator is clicked, with the list of hidden events. Always fires regardless of `manage-events`. |
91
+
92
+ ---
93
+
94
+ ## Built-in Event Management
95
+
96
+ The calendar has **built-in CRUD** — clicking the grid opens a create modal, clicking an event opens an edit modal. The `events` model is updated internally:
97
+
98
+ - **Create**: Click empty grid area → modal opens → on OK the new event is pushed to `events`
99
+ - **Edit**: Click an event → `event-click` output fires → edit modal opens → on OK the event is replaced in `events`
100
+ - **Delete**: In the edit modal, clicking Cancel on an existing event deletes it from `events`
101
+ - **Drag & Drop**: Dragging an event to a new time/day emits `event-change` internally, which updates `events` and shows a success toast
102
+ - **Overflow**: When events overlap beyond visible columns, a "+N more" indicator appears. Clicking it opens a modal listing hidden events with edit/delete actions.
103
+
104
+ All mutations produce a toast notification (success/warning/info). Since `events` is a model (two-way binding), the parent component is automatically notified of changes via `eventsChange`.
105
+
106
+ > **Important**: Because the calendar manages its own CRUD, you typically only need to provide initial events and reload them on `(date-change)`. You do NOT need to implement create/edit/delete logic yourself unless you want custom behavior.
107
+
108
+ ### Disabling built-in event management
109
+
110
+ Set `[manage-events]="false"` to turn off all built-in CRUD:
111
+
112
+ ```html
113
+ <rlb-calendar
114
+ [(view)]="view"
115
+ [(events)]="events"
116
+ [manage-events]="false"
117
+ (event-click)="onEventClick($event)"
118
+ ></rlb-calendar>
119
+ ```
120
+
121
+ When disabled:
122
+ - Clicking the grid or an event does **not** open any modal
123
+ - The "+N more" overflow indicator does **not** open the overflow modal
124
+ - Drag-and-drop **still works** — it updates the events array and shows toasts
125
+ - `(event-click)` **still fires** — use it to implement your own event handling logic
126
+ - `(container-event-click)` **still fires** — use it to handle overflow events externally
127
+
128
+ ---
129
+
130
+ ## Background Intervals
131
+
132
+ Use `[intervals]` to display non-interactive background highlights — ideal for business hours, availability slots, or recurring time blocks.
133
+
134
+ ```typescript
135
+ // Seconds-from-midnight helpers
136
+ const HOUR = 3600;
137
+
138
+ // Business hours: Mon-Fri 09:00-13:00, 14:00-18:00; Sat 09:00-13:00
139
+ intervals: CalendarInterval[] = [
140
+ { dayWeek: 1, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'success' },
141
+ { dayWeek: 1, hourStart: 14 * HOUR, hourStop: 18 * HOUR, color: 'success' },
142
+ { dayWeek: 2, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'success' },
143
+ { dayWeek: 2, hourStart: 14 * HOUR, hourStop: 18 * HOUR, color: 'success' },
144
+ { dayWeek: 3, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'success' },
145
+ { dayWeek: 3, hourStart: 14 * HOUR, hourStop: 18 * HOUR, color: 'success' },
146
+ { dayWeek: 4, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'success' },
147
+ { dayWeek: 4, hourStart: 14 * HOUR, hourStop: 18 * HOUR, color: 'success' },
148
+ { dayWeek: 5, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'success' },
149
+ { dayWeek: 5, hourStart: 14 * HOUR, hourStop: 18 * HOUR, color: 'success' },
150
+ { dayWeek: 6, hourStart: 9 * HOUR, hourStop: 13 * HOUR, color: 'info' },
151
+ ];
152
+ ```
153
+
154
+ ### Rendering per view
155
+
156
+ | View | Rendering |
157
+ |------|-----------|
158
+ | **Week** | Semi-transparent colored blocks in each matching day column, positioned between grid lines and events |
159
+ | **Day** | Same as week but single column |
160
+ | **Month** | Matching day cells get a colored left border and subtle gradient background |
161
+
162
+ ### Key details
163
+
164
+ - `hourStart` / `hourStop` are in **seconds from midnight** (0–86400), enabling sub-hour precision (e.g. `9.5 * 3600 = 34200` for 09:30)
165
+ - If omitted, `hourStart` defaults to `0` (midnight) and `hourStop` to `86400` (end of day) — highlighting the full day
166
+ - Multiple intervals can target the same `dayWeek` (e.g. morning + afternoon blocks)
167
+ - Intervals have `pointer-events: none` and sit behind events — they never interfere with clicks or drag-and-drop
168
+ - `color` maps to Bootstrap CSS variables (`var(--bs-success)`, etc.)
169
+
170
+ ---
171
+
172
+ ## Basic Usage
173
+
174
+ ```typescript
175
+ import { Component, signal } from '@angular/core';
176
+ import { DateTz, IDateTz } from '@open-rlb/date-tz';
177
+ import {
178
+ CalendarEvent,
179
+ CalendarInterval,
180
+ CalendarView,
181
+ CalendarChangeEvent
182
+ } from '@open-rlb/ng-bootstrap';
183
+
184
+ @Component({
185
+ template: `
186
+ <rlb-calendar
187
+ [(view)]="view"
188
+ [(events)]="events"
189
+ [(current-date)]="currentDate"
190
+ [intervals]="intervals"
191
+ [loading]="loading()"
192
+ (date-change)="onDateChange($event)"
193
+ (event-click)="onEventClick($event)"
194
+ ></rlb-calendar>
195
+ `
196
+ })
197
+ export class MyCalendarComponent {
198
+ view: CalendarView = 'week';
199
+ currentDate: IDateTz = new DateTz();
200
+ loading = signal(false);
201
+
202
+ events: CalendarEvent[] = [
203
+ {
204
+ id: '1',
205
+ title: 'Team Meeting',
206
+ start: new DateTz('2026-05-14T09:00:00', 'Europe/Rome'),
207
+ end: new DateTz('2026-05-14T10:00:00', 'Europe/Rome'),
208
+ color: 'primary'
209
+ }
210
+ ];
211
+
212
+ intervals: CalendarInterval[] = [
213
+ { dayWeek: 1, hourStart: 32400, hourStop: 64800, color: 'success' }, // Mon 09:00-18:00
214
+ ];
215
+
216
+ onDateChange(event: CalendarChangeEvent) {
217
+ this.loadEvents(event.date, event.view);
218
+ }
219
+
220
+ onEventClick(event: CalendarEvent) {
221
+ console.log('Clicked:', event);
222
+ // The built-in edit modal opens automatically after this
223
+ }
224
+
225
+ private loadEvents(date: IDateTz, view: CalendarView) {
226
+ this.loading.set(true);
227
+ this.eventService.getEvents(date, view).subscribe(events => {
228
+ this.events = events;
229
+ this.loading.set(false);
230
+ });
231
+ }
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Working with DateTz
238
+
239
+ The calendar uses `@open-rlb/date-tz` for timezone-aware dates:
240
+
241
+ ```typescript
242
+ import { DateTz, IDateTz } from '@open-rlb/date-tz';
243
+
244
+ // Create dates
245
+ const now = new DateTz(); // current time in UTC
246
+ const rome = new DateTz('2026-05-14T09:00:00', 'Europe/Rome');
247
+ const utc = new DateTz('2026-05-14T09:00:00Z');
248
+
249
+ // Key methods
250
+ rome.toISO(); // ISO string
251
+ rome.toDate(); // native Date
252
+ rome.format('HH:mm'); // formatted string
253
+ rome.startOf('month'); // start of month
254
+ rome.endOf('week'); // end of week
255
+ rome.add(1, 'day'); // date arithmetic
256
+ rome.isBefore(utc); // comparison
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Loading Events on Navigation
262
+
263
+ ```typescript
264
+ onDateChange(event: CalendarChangeEvent) {
265
+ const { date, view } = event;
266
+ let rangeStart: IDateTz;
267
+ let rangeEnd: IDateTz;
268
+
269
+ switch (view) {
270
+ case 'month':
271
+ rangeStart = date.startOf('month');
272
+ rangeEnd = date.endOf('month');
273
+ break;
274
+ case 'week':
275
+ rangeStart = date.startOf('week');
276
+ rangeEnd = date.endOf('week');
277
+ break;
278
+ case 'day':
279
+ rangeStart = date.startOf('day');
280
+ rangeEnd = date.endOf('day');
281
+ break;
282
+ }
283
+
284
+ this.eventService.getRange(rangeStart.toISO(), rangeEnd.toISO())
285
+ .subscribe(events => this.events = events);
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Custom Event Handling (Override Built-in CRUD)
292
+
293
+ If you need to bypass the built-in modals and handle events yourself, listen to `(event-click)` and manage `events` externally:
294
+
295
+ ```typescript
296
+ // Handle event click with your own modal
297
+ onEventClick(event: CalendarEvent) {
298
+ this.myModal.open(event).subscribe(result => {
299
+ if (result.action === 'save') {
300
+ this.events = this.events.map(e =>
301
+ e.id === result.data.id ? result.data : e
302
+ );
303
+ } else if (result.action === 'delete') {
304
+ this.events = this.events.filter(e => e.id !== event.id);
305
+ }
306
+ });
307
+ }
308
+
309
+ // Add new event
310
+ addEvent(title: string, start: IDateTz, end: IDateTz) {
311
+ const newEvent: CalendarEvent = {
312
+ id: crypto.randomUUID(),
313
+ title,
314
+ start,
315
+ end,
316
+ color: 'primary'
317
+ };
318
+ this.events = [...this.events, newEvent];
319
+ }
320
+
321
+ // Update event (e.g. after external drag-drop)
322
+ updateEvent(updated: CalendarEvent) {
323
+ this.events = this.events.map(e =>
324
+ e.id === updated.id ? updated : e
325
+ );
326
+ }
327
+
328
+ // Remove event
329
+ removeEvent(id: string | number) {
330
+ this.events = this.events.filter(e => e.id !== id);
331
+ }
332
+ ```
333
+
334
+ ---
335
+
336
+ ## CalendarEvent Color Mapping
337
+
338
+ Use `color` to visually categorize events:
339
+
340
+ ```typescript
341
+ const colorMap: Record<string, Color> = {
342
+ meeting: 'primary',
343
+ deadline: 'danger',
344
+ holiday: 'success',
345
+ reminder: 'warning',
346
+ training: 'info',
347
+ other: 'secondary'
348
+ };
349
+
350
+ function mapEvent(raw: ApiEvent): CalendarEvent {
351
+ return {
352
+ id: raw.id.toString(),
353
+ title: raw.subject,
354
+ start: new DateTz(raw.startAt, raw.timezone),
355
+ end: new DateTz(raw.endAt, raw.timezone),
356
+ color: colorMap[raw.category] ?? 'secondary',
357
+ data: raw // store original payload in the generic data field
358
+ };
359
+ }
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Layout Customization
365
+
366
+ ```typescript
367
+ // Make rows taller (more space for events)
368
+ layout: Partial<CalendarLayout> = {
369
+ rowHeight: 140, // px per hour (default 110)
370
+ maxBodyHeight: 40, // rem max scroll height (default 30)
371
+ minHeaderHeight: 4, // rem header height (default 3.5)
372
+ };
373
+ ```
374
+
375
+ The `rowHeight` value directly affects:
376
+ - Event height in week/day views (duration in minutes / 60 * rowHeight)
377
+ - Interval block heights (seconds / 3600 * rowHeight)
378
+ - Time slot grid row heights
379
+ - Drag-and-drop snap positioning (15-minute snap intervals)
380
+
381
+ ---
382
+
383
+ ## Toolbar & View Switching
384
+
385
+ The built-in toolbar shows prev/next navigation, a "Today" button, and a view dropdown. To control programmatically:
386
+
387
+ ```typescript
388
+ // Switch view
389
+ this.calendarView = 'week';
390
+
391
+ // Navigate to specific date
392
+ this.currentDate = new DateTz('2026-06-01', 'Europe/Rome');
393
+ ```
394
+
395
+ To hide the toolbar and build a custom one:
396
+ ```html
397
+ <rlb-calendar [show-toolbar]="false" [(view)]="view" [(current-date)]="currentDate" ...>
398
+ </rlb-calendar>
399
+
400
+ <!-- Custom toolbar -->
401
+ <div class="d-flex gap-2 mb-3">
402
+ <button rlb-button color="secondary" (click)="prev()">‹</button>
403
+ <button rlb-button color="secondary" (click)="today()">Today</button>
404
+ <button rlb-button color="secondary" (click)="next()">›</button>
405
+ <div class="ms-auto">
406
+ <rlb-button-group>
407
+ <button rlb-button [color]="view==='day'?'primary':'secondary'" (click)="view='day'">Day</button>
408
+ <button rlb-button [color]="view==='week'?'primary':'secondary'" (click)="view='week'">Week</button>
409
+ <button rlb-button [color]="view==='month'?'primary':'secondary'" (click)="view='month'">Month</button>
410
+ </rlb-button-group>
411
+ </div>
412
+ </div>
413
+ ```
414
+
415
+ ---
416
+
417
+ ## View-specific Behavior
418
+
419
+ ### Week View
420
+ - 7 day columns (Monday → Sunday), 24 hour rows
421
+ - Events split at day boundaries (cross-day events show continuation indicators)
422
+ - Overlapping events arranged in columns (max 4 visible, overflow shows "+N more")
423
+ - Red "now" line on today's column, updates every minute
424
+ - Drag-and-drop with 15-minute snap intervals
425
+ - Horizontal scroll synced between header and body
426
+
427
+ ### Day View
428
+ - Single day column, 24 hour rows
429
+ - Same event rendering as week but more generous column space (max 10 visible columns)
430
+ - Red "now" line if viewing today
431
+
432
+ ### Month View
433
+ - 6-week grid (42 cells), events placed using a "Tetris" algorithm for consistent vertical positioning
434
+ - Multi-day events span cells with continuation styling (rounded corners removed on continued sides)
435
+ - Max 3 events visible per cell, overflow shows "+N more" with click-to-expand modal
436
+ - Drag-and-drop preserves original event duration
437
+
438
+ ---
439
+
440
+ ## Best Practices
441
+
442
+ 1. Always reload events in `(date-change)` to reflect the visible range.
443
+ 2. Use `[loading]="true"` while fetching events — the calendar shows a progress bar.
444
+ 3. Keep `events` as a new array reference when mutating externally (`this.events = [...this.events, newEvent]`) to trigger change detection with OnPush.
445
+ 4. `CalendarEvent.id` can be `string | number`; use `crypto.randomUUID()` for new events.
446
+ 5. `CalendarEvent.end` is **required** — always provide both `start` and `end`.
447
+ 6. Always specify timezone when creating `DateTz` objects; never assume local timezone.
448
+ 7. Use `color` to encode event categories visually — align with the app's semantic color scheme.
449
+ 8. Use `intervals` for static recurring visual blocks (business hours, shifts). They are non-interactive and render behind events.
450
+ 9. Express interval times in **seconds from midnight** for maximum precision (multiply hours by 3600).