@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.
- package/README.md +3 -1
- package/package.json +2 -2
- package/schematics/ng-add/claude-skills/date-tz/SKILL.md +375 -0
- package/schematics/ng-add/claude-skills/rlb-calendar/SKILL.md +450 -0
- package/schematics/ng-add/claude-skills/rlb-components/SKILL.md +364 -0
- package/schematics/ng-add/claude-skills/rlb-datatable/SKILL.md +295 -0
- package/schematics/ng-add/claude-skills/rlb-design/SKILL.md +120 -0
- package/schematics/ng-add/claude-skills/rlb-inputs/SKILL.md +326 -0
- package/schematics/ng-add/claude-skills/rlb-modals/SKILL.md +236 -0
- package/schematics/ng-add/index.js +19 -5
- package/schematics/ng-add/schema.json +5 -0
|
@@ -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).
|