@open-rlb/ng-bootstrap 3.3.24 → 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 CHANGED
@@ -18,7 +18,9 @@ The schematic will:
18
18
  - register the Bootstrap and Bootstrap Icons stylesheets in `angular.json`;
19
19
  - add `provideRlbBootstrap()` to your application providers;
20
20
  - scaffold a `RlbStarterComponent` (`src/app/rlb-starter/`) you can render to verify the setup
21
- (pass `--skip-starter` to opt out).
21
+ (pass `--skip-starter` to opt out);
22
+ - copy the bundled Claude skills into `.claude/skills/` — `date-tz` plus the `rlb-*` component
23
+ guides (pass `--skip-skills` to opt out).
22
24
 
23
25
  ### Manual
24
26
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@open-rlb/ng-bootstrap",
3
- "version": "3.3.24",
3
+ "version": "3.3.26",
4
4
  "peerDependencies": {
5
5
  "@angular/cdk": "^21.0.0",
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
8
  "@angular/forms": "^21.0.0",
9
9
  "@angular/router": "^21.0.0",
10
- "@ngx-translate/core": "^16.0.0",
10
+ "@ngx-translate/core": "^17.0.0",
11
11
  "@open-rlb/date-tz": ">=2.1.1",
12
12
  "bootstrap": ">=5.3.0",
13
13
  "@types/bootstrap": ">=5.2.0",
@@ -0,0 +1,375 @@
1
+ ---
2
+ name: date-tz
3
+ description: Rules and API reference for the @open-rlb/date-tz library used for ALL date/time handling in this project. Use whenever writing or reviewing TypeScript/JavaScript that creates or manipulates dates or times: any new Date(...), DateTz/IDateTz usage, timezone handling, parsing or formatting dates, or date arithmetic. Bans native Date, enforces IDateTz typing, and covers the timezone-aware-getters vs UTC-naive-mutators gotcha.
4
+ ---
5
+
6
+ # date-tz skill
7
+
8
+ You are working in a project that uses the **date-tz** library for all date/time handling. Apply the rules below to every piece of TypeScript/JavaScript code you write or review.
9
+
10
+ ---
11
+
12
+ ## Core rule: never use native `Date` for business logic
13
+
14
+ The `Date` object is **banned** for creating or manipulating dates in application code. The only permitted use is `Date.now()` inside the library itself. In application code:
15
+
16
+ - Do **not** write `new Date(...)`, `Date.parse(...)`, `new Date().getTime()`, etc.
17
+ - Use `DateTz.now(tz?)` to get the current instant.
18
+ - Use `DateTz.parse(str, pattern?, tz?)` to parse a string.
19
+ - Use `new DateTz(timestamp, tz?)` when you already have a millisecond timestamp.
20
+ - Use `new DateTz(iDateTz)` to materialise an `IDateTz` value into a concrete instance.
21
+
22
+ ---
23
+
24
+ ## Interface vs implementation
25
+
26
+ | Context | Type to use |
27
+ | ----------------------------------------- | ----------------------------------------- |
28
+ | Function/method parameter | `IDateTz` |
29
+ | Interface property | `IDateTz` |
30
+ | Return type of a public function | `IDateTz` |
31
+ | Local variable that needs to call methods | `IDateTz` (assign from `new DateTz(...)`) |
32
+ | Constructing a new value | `new DateTz(...)` |
33
+ | Static factory calls | `DateTz.now()`, `DateTz.parse()` |
34
+
35
+ **Rule:** use `IDateTz` everywhere you declare a type. Use `new DateTz(param)` at the top of any function that receives an `IDateTz` and needs to call methods on it.
36
+
37
+ ```typescript
38
+ import { DateTz, IDateTz } from 'date-tz';
39
+
40
+ // CORRECT – interface in signature, concrete at the start of the body
41
+ function formatAppointment(date: IDateTz, tz: string): string {
42
+ const d: IDateTz = new DateTz(date); // materialise once
43
+ return d.toString!('DD/MM/YYYY HH:mm', 'it');
44
+ }
45
+
46
+ // WRONG – using DateTz as the parameter type
47
+ function formatAppointment(date: DateTz, tz: string): string { ... }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Imports
53
+
54
+ ```typescript
55
+ import { DateTz, IDateTz } from 'date-tz';
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Constructors
61
+
62
+ ```typescript
63
+ // From a timestamp (ms since Unix epoch) + optional IANA timezone
64
+ const d: IDateTz = new DateTz(1700000000000, 'Europe/Rome');
65
+
66
+ // From another IDateTz (copies timestamp and timezone)
67
+ const copy: IDateTz = new DateTz(existingIDateTz);
68
+
69
+ // Defaults to 'Etc/UTC' when timezone is omitted
70
+ const utc: IDateTz = new DateTz(1700000000000);
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Static factory methods
76
+
77
+ ```typescript
78
+ // Current instant
79
+ const now: IDateTz = DateTz.now('Europe/Rome');
80
+ const nowUtc: IDateTz = DateTz.now(); // Etc/UTC
81
+
82
+ // Parse a formatted string
83
+ const d: IDateTz = DateTz.parse('2024-01-15 09:30:00', 'YYYY-MM-DD HH:mm:ss', 'America/New_York');
84
+ const d2: IDateTz = DateTz.parse('15/01/2024', 'DD/MM/YYYY', 'Europe/Rome');
85
+
86
+ // 12-hour format requires aa or AA
87
+ const d3: IDateTz = DateTz.parse('01/15/2024 09:30 AM', 'MM/DD/YYYY hh:mm AA', 'Etc/UTC');
88
+
89
+ // List available timezones
90
+ const allTz: string[] = DateTz.timezones();
91
+ const supported: string[] = DateTz.supportedTimeZones();
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Properties (all read-only getters)
97
+
98
+ ```typescript
99
+ const d: IDateTz = new DateTz(ts, 'Europe/Rome');
100
+
101
+ // Local (timezone-aware) components
102
+ d.year // full year, e.g. 2024
103
+ d.month // 0-based month index (0 = January … 11 = December)
104
+ d.day // day of month, 1–31
105
+ d.hour // 0–23
106
+ d.minute // 0–59
107
+ d.dayOfWeek // 0 = Sunday … 6 = Saturday
108
+
109
+ // UTC components
110
+ d.yearUTC
111
+ d.monthUTC
112
+ d.dayUTC
113
+ d.hourUTC
114
+ d.minuteUTC
115
+ d.dayOfWeekUTC
116
+
117
+ // Metadata
118
+ d.timestamp // ms since Unix epoch
119
+ d.timezone // IANA string, e.g. 'Europe/Rome'
120
+ d.timezoneOffset // UTC offset in milliseconds
121
+ d.isDst // true when DST is active
122
+ d.isLeapYear // true when current year is a leap year
123
+ ```
124
+
125
+ > **Note:** `month` and `monthUTC` are **0-based** (January = 0, December = 11).
126
+
127
+ ---
128
+
129
+ ## `toString` – formatting
130
+
131
+ ```typescript
132
+ const d: IDateTz = new DateTz(ts, 'Europe/Rome');
133
+
134
+ d.toString!() // '2024-01-15 09:30:00' (default)
135
+ d.toString!('DD/MM/YYYY') // '15/01/2024'
136
+ d.toString!('DD LM YYYY', 'it') // '15 gennaio 2024'
137
+ d.toString!('WL, DD MM YYYY', 'en') // 'Monday, 15 01 2024'
138
+ d.toString!('hh:mm AA') // '09:30 AM'
139
+ ```
140
+
141
+ **Format tokens:**
142
+
143
+ | Token | Meaning |
144
+ | --------------- | --------------------------------- |
145
+ | `YYYY` / `yyyy` | 4-digit year |
146
+ | `YY` / `yy` | 2-digit year |
147
+ | `MM` | 2-digit month (01–12) |
148
+ | `LM` | Long month name (locale-aware) |
149
+ | `SM` | Short month name (locale-aware) |
150
+ | `DD` | 2-digit day (01–31) |
151
+ | `HH` | 24-hour hour (00–23) |
152
+ | `hh` | 12-hour hour (01–12) |
153
+ | `mm` | Minutes (00–59) |
154
+ | `ss` | Seconds (00–59) |
155
+ | `AA` | AM/PM uppercase |
156
+ | `aa` | am/pm lowercase |
157
+ | `WL` | Long weekday name (locale-aware) |
158
+ | `WS` | Short weekday name (locale-aware) |
159
+ | `tz` | Timezone identifier |
160
+
161
+ ---
162
+
163
+ ## ⚠️ Critical: getters are timezone-aware, mutators are NOT
164
+
165
+ This is the single most important gotcha in this library. In the current `@open-rlb/date-tz` implementation:
166
+
167
+ - **Timezone-aware** (compute on `timestamp + timezoneOffset`): the getters `.year`, `.month`, `.day`, `.hour`, `.minute`, `.dayOfWeek`, and `toString()`.
168
+ - **Timezone-NAIVE** (operate on the raw UTC `timestamp`, ignoring the offset): the mutators `set()`, `add()`, `stripSecMillis()`.
169
+
170
+ So `d.set(0, 'hour')` zeroes the **UTC** hour, **not** the local one. For an event at 09:00 Europe/Rome (07:00 UTC), `d.toString('HH:mm')` correctly returns `'09:00'`, but `d.set(0,'hour')` lands on UTC midnight, so:
171
+
172
+ ```typescript
173
+ // WRONG – yields the UTC time-of-day (off by the tz offset), NOT the local one
174
+ const minutesFromMidnight =
175
+ (d.timestamp - new DateTz(d).set(0,'hour').set(0,'minute').timestamp) / 60000;
176
+ // for 09:00 Rome this returns 420 (07:00), while the label shows 09:00 → mismatch
177
+ ```
178
+
179
+ **Consequence:** never use `set('hour'/'minute'/'day')` or `add('day')` to derive a **local** day boundary or time-of-day. Use the timezone-aware getters instead:
180
+
181
+ ```typescript
182
+ // CORRECT – tz-aware, matches what toString() shows
183
+ const minutesFromMidnight = d.hour! * 60 + d.minute!;
184
+
185
+ // CORRECT – epoch ms of LOCAL midnight (the day containing d, in its own tz)
186
+ const MS_PER_DAY = 86_400_000;
187
+ const localMidnightTs =
188
+ Math.floor((d.timestamp + d.timezoneOffset!) / MS_PER_DAY) * MS_PER_DAY - d.timezoneOffset!;
189
+ ```
190
+
191
+ `add('hour'/'minute'/'second'/'millisecond')` is fine (a fixed ms delta is timezone-independent). The trap is specifically: `set` of any calendar field, `add('day'/'month'/'year')`, and `stripSecMillis` when you expect them to respect the local wall clock — they don't, they act in UTC.
192
+
193
+ > In this repo the calendar already wraps this correctly: see
194
+ > `projects/rlb/ng-bootstrap/src/lib/components/calendar/utils/calendar-date-utils.ts`
195
+ > (`startOfDayTs`, `minutesSinceMidnight`, `dayAt`). Reuse those instead of re-deriving the math.
196
+
197
+ ---
198
+
199
+ ## `add` – arithmetic
200
+
201
+ Returns `IDateTz` (mutates the instance in place). **`add('day'/'month'/'year')` is timezone-naive** — it shifts the raw UTC timestamp, so it does not respect local-midnight/DST. See the critical section above.
202
+
203
+ ```typescript
204
+ let d: IDateTz = new DateTz(ts, 'Europe/Rome');
205
+
206
+ d = d.add!(1, 'hour');
207
+ d = d.add!(30, 'minute');
208
+ d = d.add!(1, 'day');
209
+ d = d.add!(2, 'month');
210
+ d = d.add!(1, 'year');
211
+ d = d.add!(500, 'millisecond');
212
+ d = d.add!(10, 'second');
213
+ ```
214
+
215
+ ---
216
+
217
+ ## `set` – override a component
218
+
219
+ Returns `IDateTz` (mutates the instance in place). **`set` is timezone-naive** — it sets the component in **UTC**, not in the instance's local timezone. `set(0,'hour')` is UTC midnight, not local midnight. See the critical section above before using it for day/time boundaries.
220
+
221
+ ```typescript
222
+ let d: IDateTz = new DateTz(ts, 'Europe/Rome');
223
+
224
+ d = d.set!(2025, 'year');
225
+ d = d.set!(6, 'month'); // 1-based: 1 = January … 12 = December
226
+ d = d.set!(15, 'day'); // 1–31
227
+ d = d.set!(9, 'hour'); // 0–23
228
+ d = d.set!(0, 'minute'); // 0–59
229
+ d = d.set!(0, 'second'); // 0–59
230
+ d = d.set!(0, 'millisecond'); // 0–999
231
+ ```
232
+
233
+ > **Note:** `set('month', …)` is **1-based** (pass `6` for June), unlike the `month` getter which is 0-based.
234
+
235
+ ---
236
+
237
+ ## `stripSecMillis` – truncate to the minute
238
+
239
+ ```typescript
240
+ let d: IDateTz = new DateTz(ts, 'Europe/Rome');
241
+ d = d.stripSecMillis!(); // seconds and milliseconds become 0
242
+ ```
243
+
244
+ > Like `set`/`add`, this truncates on the raw UTC timestamp. Seconds/millis are the same in every timezone, so the result is fine — but don't assume any *hour/day* alignment from it.
245
+
246
+ ---
247
+
248
+ ## `cloneToTimezone` – immutable timezone conversion
249
+
250
+ Creates a **new** instance at the same absolute instant, displayed in a different timezone.
251
+
252
+ ```typescript
253
+ const rome: IDateTz = new DateTz(ts, 'Europe/Rome');
254
+ const ny: IDateTz = rome.cloneToTimezone!('America/New_York');
255
+ // rome and ny share the same timestamp; only timezone (and derived components) differ
256
+ ```
257
+
258
+ ---
259
+
260
+ ## `setTimezone` – mutate the timezone in place
261
+
262
+ Changes the timezone of an existing instance. The UTC timestamp is **preserved**; offset and DST are recomputed.
263
+
264
+ ```typescript
265
+ let d: IDateTz = new DateTz(ts, 'Europe/Rome');
266
+ d = d.setTimezone('Asia/Tokyo');
267
+ ```
268
+
269
+ ---
270
+
271
+ ## `compare` and `isComparable`
272
+
273
+ `compare` throws if the two instances are in different timezones. Always check `isComparable` first, or ensure both dates share a timezone.
274
+
275
+ ```typescript
276
+ function sortDates(a: IDateTz, b: IDateTz): number {
277
+ const da: IDateTz = new DateTz(a);
278
+ const db: IDateTz = new DateTz(b);
279
+ if (!da.isComparable!(db)) {
280
+ throw new Error(`Cannot compare ${da.timezone} with ${db.timezone}`);
281
+ }
282
+ return da.compare!(db); // negative / 0 / positive
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Full worked example
289
+
290
+ ```typescript
291
+ import { DateTz, IDateTz } from 'date-tz';
292
+
293
+ interface Meeting {
294
+ title: string;
295
+ start: IDateTz;
296
+ end: IDateTz;
297
+ }
298
+
299
+ function scheduleMeeting(title: string, startTs: number, durationMinutes: number, tz: string): Meeting {
300
+ const start: IDateTz = new DateTz(startTs, tz);
301
+ const end: IDateTz = new DateTz(startTs, tz).add!(durationMinutes, 'minute');
302
+ return { title, start, end };
303
+ }
304
+
305
+ function formatMeeting(meeting: Meeting, locale: string): string {
306
+ const s: IDateTz = new DateTz(meeting.start);
307
+ const e: IDateTz = new DateTz(meeting.end);
308
+ const date = s.toString!('WL DD LM YYYY', locale);
309
+ const from = s.toString!('HH:mm');
310
+ const to = e.toString!('HH:mm tz');
311
+ return `${meeting.title} — ${date}, ${from}–${to}`;
312
+ }
313
+
314
+ function isTodayMeeting(meeting: Meeting): boolean {
315
+ const now: IDateTz = DateTz.now(meeting.start.timezone);
316
+ const s: IDateTz = new DateTz(meeting.start);
317
+ return s.year === now.year && s.month === now.month && s.day === now.day;
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Common mistakes to avoid
324
+
325
+ ```typescript
326
+ // WRONG – native Date
327
+ const now = new Date();
328
+ const ts = new Date('2024-01-15').getTime();
329
+
330
+ // CORRECT
331
+ const now: IDateTz = DateTz.now('Europe/Rome');
332
+ const d: IDateTz = DateTz.parse('2024-01-15', 'YYYY-MM-DD', 'Europe/Rome');
333
+
334
+ // WRONG – DateTz as parameter type
335
+ function fn(d: DateTz) { ... }
336
+
337
+ // CORRECT
338
+ function fn(d: IDateTz) { const inst: IDateTz = new DateTz(d); ... }
339
+
340
+ // WRONG – comparing dates in different timezones without cloning
341
+ function diff(a: IDateTz, b: IDateTz): number {
342
+ return a.compare!(b); // may throw
343
+ }
344
+
345
+ // CORRECT – normalise to same timezone first
346
+ function diff(a: IDateTz, b: IDateTz): number {
347
+ const da: IDateTz = new DateTz(a);
348
+ const db: IDateTz = da.isComparable!(b) ? new DateTz(b) : b.cloneToTimezone!(a.timezone!);
349
+ return da.compare!(db);
350
+ }
351
+
352
+ // WRONG – forgetting that month getter is 0-based
353
+ if (d.month === 6) { ... } // this is July, not June!
354
+
355
+ // CORRECT – remember month getter is 0-based (0 = January)
356
+ if (d.month === 5) { ... } // June
357
+
358
+ // WRONG – using set('month') with 0-based value
359
+ d.set!(5, 'month'); // would set to May (set expects 1-based)
360
+
361
+ // CORRECT – set('month') is 1-based
362
+ d.set!(6, 'month'); // June
363
+
364
+ // WRONG – set/add are UTC-naive: this is the UTC hour, not the local one
365
+ const minutes = (d.timestamp - new DateTz(d).set!(0,'hour').set!(0,'minute').timestamp) / 60000;
366
+
367
+ // CORRECT – tz-aware getters match toString()
368
+ const minutesLocal = d.hour! * 60 + d.minute!;
369
+
370
+ // WRONG – building from a raw timestamp without a tz silently defaults to Etc/UTC
371
+ const end = new DateTz(someTimestampNumber); // label/getters will be UTC!
372
+
373
+ // CORRECT – pass the intended timezone explicitly
374
+ const endTz = new DateTz(someTimestampNumber, 'Europe/Rome');
375
+ ```