@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
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.
|
|
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": "^
|
|
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
|
+
```
|