@oneluiz/dual-datepicker 3.5.0 → 3.6.0
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 +51 -3
- package/core/built-in-presets.d.ts +98 -0
- package/core/date-adapter.d.ts +298 -0
- package/core/date-clock.d.ts +82 -0
- package/core/dual-date-range.store.d.ts +9 -0
- package/core/index.d.ts +15 -0
- package/core/native-date-adapter.d.ts +152 -0
- package/core/preset-providers.d.ts +176 -0
- package/core/preset-registry.d.ts +181 -0
- package/core/preset.engine.d.ts +79 -9
- package/core/range-preset.plugin.d.ts +188 -0
- package/core/system-clock.d.ts +13 -0
- package/dual-datepicker.component.d.ts +17 -11
- package/esm2022/core/built-in-presets.mjs +289 -0
- package/esm2022/core/date-adapter.mjs +77 -0
- package/esm2022/core/date-clock.mjs +65 -0
- package/esm2022/core/dual-date-range.store.mjs +51 -15
- package/esm2022/core/index.mjs +17 -1
- package/esm2022/core/native-date-adapter.mjs +286 -0
- package/esm2022/core/preset-providers.mjs +243 -0
- package/esm2022/core/preset-registry.mjs +277 -0
- package/esm2022/core/preset.engine.mjs +139 -169
- package/esm2022/core/range-preset.plugin.mjs +70 -0
- package/esm2022/core/system-clock.mjs +34 -0
- package/esm2022/dual-datepicker.component.mjs +264 -196
- package/fesm2022/oneluiz-dual-datepicker.mjs +3219 -1822
- package/fesm2022/oneluiz-dual-datepicker.mjs.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, zero-dependency date range picker for Angular 17+. Built with standalone components, Reactive Forms, and Angular Signals. No Angular Material required.
|
|
4
4
|
|
|
5
|
-
> **🆕 NEW in v3.5.
|
|
5
|
+
> **🆕 NEW in v3.5.1**: [**Timezone-Safe Date Adapter**](docs/TIMEZONE_ADAPTER.md) - Fixes enterprise-critical timezone bugs in ERP, BI, POS, and invoicing systems 🛡️
|
|
6
|
+
> **🆕 NEW in v3.5.0**: [**Headless Architecture**](docs/HEADLESS.md) - Use date range state WITHOUT the UI component. Perfect for SSR, services, and global dashboard filters! 🎯
|
|
6
7
|
|
|
7
8
|
[](https://www.npmjs.com/package/@oneluiz/dual-datepicker)
|
|
8
9
|
[](https://www.npmjs.com/package/@oneluiz/dual-datepicker)
|
|
@@ -21,6 +22,25 @@ npm install @oneluiz/dual-datepicker
|
|
|
21
22
|
|
|
22
23
|
## 🌟 What's New
|
|
23
24
|
|
|
25
|
+
### Timezone-Safe Date Adapter (v3.5.1)
|
|
26
|
+
|
|
27
|
+
**Fixed**: Enterprise timezone bugs that caused date ranges to shift by ±1 day.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// ✅ No more timezone shift bugs!
|
|
31
|
+
const store = inject(DualDateRangeStore);
|
|
32
|
+
store.applyPreset('THIS_MONTH');
|
|
33
|
+
const range = store.range(); // { start: "2024-03-01", end: "2024-03-31" }
|
|
34
|
+
// Always correct, even across timezones and DST transitions
|
|
35
|
+
|
|
36
|
+
// ✅ Optional: Use your preferred date library
|
|
37
|
+
providers: [
|
|
38
|
+
{ provide: DATE_ADAPTER, useClass: LuxonDateAdapter }
|
|
39
|
+
]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**[📖 Read the Timezone Adapter Guide →](docs/TIMEZONE_ADAPTER.md)**
|
|
43
|
+
|
|
24
44
|
### Headless Architecture (v3.5.0)
|
|
25
45
|
|
|
26
46
|
Use date range logic **without the UI component**:
|
|
@@ -44,7 +64,7 @@ http.get(`/api/sales?start=${range.start}&end=${range.end}`);
|
|
|
44
64
|
- 🎯 Service-layer filtering
|
|
45
65
|
- 📈 Analytics and BI tools
|
|
46
66
|
|
|
47
|
-
**[📖 Read the Headless Architecture Guide →](HEADLESS.md)**
|
|
67
|
+
**[📖 Read the Headless Architecture Guide →](docs/HEADLESS.md)**
|
|
48
68
|
|
|
49
69
|
---
|
|
50
70
|
|
|
@@ -245,7 +265,35 @@ export class DashboardComponent {
|
|
|
245
265
|
- ✅ Perfect for services and guards
|
|
246
266
|
- ✅ Testable and deterministic
|
|
247
267
|
|
|
248
|
-
|
|
268
|
+
#### SSR-Safe Clock Injection
|
|
269
|
+
|
|
270
|
+
Presets like "Last 7 Days" now use **clock injection** for SSR hydration consistency:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Server (SSR)
|
|
274
|
+
import { DATE_CLOCK } from '@oneluiz/dual-datepicker';
|
|
275
|
+
|
|
276
|
+
const requestTime = new Date();
|
|
277
|
+
|
|
278
|
+
renderApplication(AppComponent, {
|
|
279
|
+
providers: [
|
|
280
|
+
{ provide: DATE_CLOCK, useValue: { now: () => requestTime } }
|
|
281
|
+
]
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Client (Browser)
|
|
287
|
+
bootstrapApplication(AppComponent, {
|
|
288
|
+
providers: [
|
|
289
|
+
{ provide: DATE_CLOCK, useValue: { now: () => new Date(getServerTime()) } }
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Result**: Server and client resolve identical presets ✅ No hydration mismatch!
|
|
295
|
+
|
|
296
|
+
**[📖 Full Headless Architecture Guide →](HEADLESS.md)** | **[💻 Code Examples →](HEADLESS_EXAMPLES.ts)** | **[🚀 SSR Clock Injection Guide →](SSR_CLOCK_INJECTION.md)**
|
|
249
297
|
|
|
250
298
|
---
|
|
251
299
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Date Range Preset Plugins
|
|
3
|
+
*
|
|
4
|
+
* Version: 3.6.0
|
|
5
|
+
*
|
|
6
|
+
* Standard presets provided by the library.
|
|
7
|
+
* All presets use DateClock and DateAdapter for SSR-safety and timezone-safety.
|
|
8
|
+
*
|
|
9
|
+
* PRESETS INCLUDED:
|
|
10
|
+
* - TODAY, YESTERDAY
|
|
11
|
+
* - LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, LAST_60_DAYS, LAST_90_DAYS
|
|
12
|
+
* - THIS_WEEK, LAST_WEEK
|
|
13
|
+
* - THIS_MONTH, LAST_MONTH, MONTH_TO_DATE
|
|
14
|
+
* - THIS_QUARTER, LAST_QUARTER, QUARTER_TO_DATE
|
|
15
|
+
* - THIS_YEAR, LAST_YEAR, YEAR_TO_DATE
|
|
16
|
+
*/
|
|
17
|
+
import { RangePresetPlugin } from './range-preset.plugin';
|
|
18
|
+
/**
|
|
19
|
+
* TODAY - Current day
|
|
20
|
+
*/
|
|
21
|
+
export declare const TODAY_PRESET: RangePresetPlugin;
|
|
22
|
+
/**
|
|
23
|
+
* YESTERDAY - Previous day
|
|
24
|
+
*/
|
|
25
|
+
export declare const YESTERDAY_PRESET: RangePresetPlugin;
|
|
26
|
+
/**
|
|
27
|
+
* LAST_7_DAYS - Last 7 days including today
|
|
28
|
+
*/
|
|
29
|
+
export declare const LAST_7_DAYS_PRESET: RangePresetPlugin;
|
|
30
|
+
/**
|
|
31
|
+
* LAST_14_DAYS - Last 14 days including today
|
|
32
|
+
*/
|
|
33
|
+
export declare const LAST_14_DAYS_PRESET: RangePresetPlugin;
|
|
34
|
+
/**
|
|
35
|
+
* LAST_30_DAYS - Last 30 days including today
|
|
36
|
+
*/
|
|
37
|
+
export declare const LAST_30_DAYS_PRESET: RangePresetPlugin;
|
|
38
|
+
/**
|
|
39
|
+
* LAST_60_DAYS - Last 60 days including today
|
|
40
|
+
*/
|
|
41
|
+
export declare const LAST_60_DAYS_PRESET: RangePresetPlugin;
|
|
42
|
+
/**
|
|
43
|
+
* LAST_90_DAYS - Last 90 days including today
|
|
44
|
+
*/
|
|
45
|
+
export declare const LAST_90_DAYS_PRESET: RangePresetPlugin;
|
|
46
|
+
/**
|
|
47
|
+
* THIS_WEEK - Current week (Monday to Sunday)
|
|
48
|
+
*/
|
|
49
|
+
export declare const THIS_WEEK_PRESET: RangePresetPlugin;
|
|
50
|
+
/**
|
|
51
|
+
* LAST_WEEK - Previous week (Monday to Sunday)
|
|
52
|
+
*/
|
|
53
|
+
export declare const LAST_WEEK_PRESET: RangePresetPlugin;
|
|
54
|
+
/**
|
|
55
|
+
* THIS_MONTH - Current calendar month (1st to last day)
|
|
56
|
+
*/
|
|
57
|
+
export declare const THIS_MONTH_PRESET: RangePresetPlugin;
|
|
58
|
+
/**
|
|
59
|
+
* LAST_MONTH - Previous calendar month
|
|
60
|
+
*/
|
|
61
|
+
export declare const LAST_MONTH_PRESET: RangePresetPlugin;
|
|
62
|
+
/**
|
|
63
|
+
* MONTH_TO_DATE - From start of current month to today
|
|
64
|
+
*/
|
|
65
|
+
export declare const MONTH_TO_DATE_PRESET: RangePresetPlugin;
|
|
66
|
+
/**
|
|
67
|
+
* THIS_QUARTER - Current quarter (Q1: Jan-Mar, Q2: Apr-Jun, Q3: Jul-Sep, Q4: Oct-Dec)
|
|
68
|
+
*/
|
|
69
|
+
export declare const THIS_QUARTER_PRESET: RangePresetPlugin;
|
|
70
|
+
/**
|
|
71
|
+
* LAST_QUARTER - Previous quarter
|
|
72
|
+
*/
|
|
73
|
+
export declare const LAST_QUARTER_PRESET: RangePresetPlugin;
|
|
74
|
+
/**
|
|
75
|
+
* QUARTER_TO_DATE - From start of current quarter to today
|
|
76
|
+
*/
|
|
77
|
+
export declare const QUARTER_TO_DATE_PRESET: RangePresetPlugin;
|
|
78
|
+
/**
|
|
79
|
+
* THIS_YEAR - Current calendar year (Jan 1 to Dec 31)
|
|
80
|
+
*/
|
|
81
|
+
export declare const THIS_YEAR_PRESET: RangePresetPlugin;
|
|
82
|
+
/**
|
|
83
|
+
* LAST_YEAR - Previous calendar year
|
|
84
|
+
*/
|
|
85
|
+
export declare const LAST_YEAR_PRESET: RangePresetPlugin;
|
|
86
|
+
/**
|
|
87
|
+
* YEAR_TO_DATE - From start of current year to today
|
|
88
|
+
*/
|
|
89
|
+
export declare const YEAR_TO_DATE_PRESET: RangePresetPlugin;
|
|
90
|
+
/**
|
|
91
|
+
* All built-in presets as an array
|
|
92
|
+
*
|
|
93
|
+
* Use this to register all built-in presets at once:
|
|
94
|
+
* ```typescript
|
|
95
|
+
* BUILT_IN_PRESETS.forEach(preset => registry.register(preset));
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export declare const BUILT_IN_PRESETS: RangePresetPlugin[];
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Adapter Abstraction for Timezone-Safe Date Operations
|
|
3
|
+
*
|
|
4
|
+
* Problem:
|
|
5
|
+
* Using Date natively in calendar/range logic causes enterprise bugs:
|
|
6
|
+
* - Date ranges shift by 1 day due to timezone/DST
|
|
7
|
+
* - Server (UTC) vs Client (local timezone) discrepancies
|
|
8
|
+
* - Inconsistent reporting in BI/ERP/invoicing/hotel systems
|
|
9
|
+
*
|
|
10
|
+
* Solution:
|
|
11
|
+
* All date calculations go through an adapter layer.
|
|
12
|
+
* This allows:
|
|
13
|
+
* - Timezone-safe operations by default (NativeDateAdapter normalizes to day boundaries)
|
|
14
|
+
* - Optional integration with Luxon/DayJS/date-fns for advanced timezone handling
|
|
15
|
+
* - Consistent behavior across SSR and client
|
|
16
|
+
* - Migration path to timezone-aware libraries without breaking changes
|
|
17
|
+
*
|
|
18
|
+
* Architecture:
|
|
19
|
+
* ```
|
|
20
|
+
* DualDateRangeStore
|
|
21
|
+
* ↓ uses
|
|
22
|
+
* DateAdapter ← Inject via DATE_ADAPTER token
|
|
23
|
+
* ↓ implements
|
|
24
|
+
* NativeDateAdapter (default, zero deps)
|
|
25
|
+
* LuxonDateAdapter (optional, full timezone support)
|
|
26
|
+
* DayJSDateAdapter (optional, lightweight)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Default (NativeDateAdapter)
|
|
32
|
+
* bootstrapApplication(AppComponent);
|
|
33
|
+
*
|
|
34
|
+
* // Advanced (Luxon with timezone)
|
|
35
|
+
* import { LuxonDateAdapter } from '@oneluiz/dual-datepicker/luxon';
|
|
36
|
+
*
|
|
37
|
+
* bootstrapApplication(AppComponent, {
|
|
38
|
+
* providers: [
|
|
39
|
+
* { provide: DATE_ADAPTER, useClass: LuxonDateAdapter }
|
|
40
|
+
* ]
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
import { InjectionToken } from '@angular/core';
|
|
45
|
+
/**
|
|
46
|
+
* Date adapter interface for all calendar/range operations
|
|
47
|
+
*
|
|
48
|
+
* All methods operate on "calendar day" level, ignoring time components.
|
|
49
|
+
* Implementations must ensure timezone-safe behavior.
|
|
50
|
+
*
|
|
51
|
+
* Implementations:
|
|
52
|
+
* - NativeDateAdapter: Default, zero dependencies, uses Date with normalization
|
|
53
|
+
* - LuxonDateAdapter: Optional, full timezone support with Luxon
|
|
54
|
+
* - DayJSDateAdapter: Optional, lightweight timezone support
|
|
55
|
+
* - Custom: Implement for your specific backend/timezone requirements
|
|
56
|
+
*/
|
|
57
|
+
export interface DateAdapter {
|
|
58
|
+
/**
|
|
59
|
+
* Normalize date to start of day (00:00:00.000)
|
|
60
|
+
*
|
|
61
|
+
* Critical for timezone-safe comparisons.
|
|
62
|
+
*
|
|
63
|
+
* Example:
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const date = new Date('2026-02-21T15:30:00');
|
|
66
|
+
* const normalized = adapter.normalize(date);
|
|
67
|
+
* // → 2026-02-21T00:00:00.000 (in local timezone)
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @param date - Date to normalize
|
|
71
|
+
* @returns Date with time set to 00:00:00.000
|
|
72
|
+
*/
|
|
73
|
+
normalize(date: Date): Date;
|
|
74
|
+
/**
|
|
75
|
+
* Check if two dates represent the same calendar day
|
|
76
|
+
*
|
|
77
|
+
* Ignores time component. Timezone-safe.
|
|
78
|
+
*
|
|
79
|
+
* Example:
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const a = new Date('2026-02-21T23:59:59');
|
|
82
|
+
* const b = new Date('2026-02-21T00:00:01');
|
|
83
|
+
* adapter.isSameDay(a, b); // → true
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
isSameDay(a: Date, b: Date): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Check if date A is before date B (calendar day level)
|
|
89
|
+
*
|
|
90
|
+
* Example:
|
|
91
|
+
* ```typescript
|
|
92
|
+
* adapter.isBeforeDay(new Date('2026-02-20'), new Date('2026-02-21')); // → true
|
|
93
|
+
* adapter.isBeforeDay(new Date('2026-02-21'), new Date('2026-02-21')); // → false
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
isBeforeDay(a: Date, b: Date): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Check if date A is after date B (calendar day level)
|
|
99
|
+
*
|
|
100
|
+
* Example:
|
|
101
|
+
* ```typescript
|
|
102
|
+
* adapter.isAfterDay(new Date('2026-02-22'), new Date('2026-02-21')); // → true
|
|
103
|
+
* adapter.isAfterDay(new Date('2026-02-21'), new Date('2026-02-21')); // → false
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
isAfterDay(a: Date, b: Date): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Add days to a date
|
|
109
|
+
*
|
|
110
|
+
* Must handle DST transitions correctly.
|
|
111
|
+
*
|
|
112
|
+
* Example:
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const date = new Date('2026-02-21');
|
|
115
|
+
* const future = adapter.addDays(date, 7);
|
|
116
|
+
* // → 2026-02-28
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
addDays(date: Date, days: number): Date;
|
|
120
|
+
/**
|
|
121
|
+
* Add months to a date
|
|
122
|
+
*
|
|
123
|
+
* Must handle month overflow (e.g., Jan 31 + 1 month → Feb 28).
|
|
124
|
+
*
|
|
125
|
+
* Example:
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const date = new Date('2026-01-31');
|
|
128
|
+
* const next = adapter.addMonths(date, 1);
|
|
129
|
+
* // → 2026-02-28 (not March 3rd)
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
addMonths(date: Date, months: number): Date;
|
|
133
|
+
/**
|
|
134
|
+
* Get start of day (00:00:00.000)
|
|
135
|
+
*
|
|
136
|
+
* Similar to normalize() but explicit intent.
|
|
137
|
+
*/
|
|
138
|
+
startOfDay(date: Date): Date;
|
|
139
|
+
/**
|
|
140
|
+
* Get end of day (23:59:59.999)
|
|
141
|
+
*
|
|
142
|
+
* Useful for range queries that need to include entire day.
|
|
143
|
+
*
|
|
144
|
+
* Example:
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const date = new Date('2026-02-21');
|
|
147
|
+
* const end = adapter.endOfDay(date);
|
|
148
|
+
* // → 2026-02-21T23:59:59.999
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
endOfDay(date: Date): Date;
|
|
152
|
+
/**
|
|
153
|
+
* Get first day of month (00:00:00.000)
|
|
154
|
+
*
|
|
155
|
+
* Example:
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const date = new Date('2026-02-21');
|
|
158
|
+
* const start = adapter.startOfMonth(date);
|
|
159
|
+
* // → 2026-02-01T00:00:00.000
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
startOfMonth(date: Date): Date;
|
|
163
|
+
/**
|
|
164
|
+
* Get last day of month (23:59:59.999)
|
|
165
|
+
*
|
|
166
|
+
* Example:
|
|
167
|
+
* ```typescript
|
|
168
|
+
* const date = new Date('2026-02-21');
|
|
169
|
+
* const end = adapter.endOfMonth(date);
|
|
170
|
+
* // → 2026-02-28T23:59:59.999
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
endOfMonth(date: Date): Date;
|
|
174
|
+
/**
|
|
175
|
+
* Get year (4-digit)
|
|
176
|
+
*
|
|
177
|
+
* Example:
|
|
178
|
+
* ```typescript
|
|
179
|
+
* adapter.getYear(new Date('2026-02-21')); // → 2026
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
getYear(date: Date): number;
|
|
183
|
+
/**
|
|
184
|
+
* Get month (0-11, where 0 = January)
|
|
185
|
+
*
|
|
186
|
+
* Example:
|
|
187
|
+
* ```typescript
|
|
188
|
+
* adapter.getMonth(new Date('2026-02-21')); // → 1 (February)
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
getMonth(date: Date): number;
|
|
192
|
+
/**
|
|
193
|
+
* Get day of month (1-31)
|
|
194
|
+
*
|
|
195
|
+
* Example:
|
|
196
|
+
* ```typescript
|
|
197
|
+
* adapter.getDate(new Date('2026-02-21')); // → 21
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
getDate(date: Date): number;
|
|
201
|
+
/**
|
|
202
|
+
* Get day of week (0-6, where 0 = Sunday)
|
|
203
|
+
*
|
|
204
|
+
* Example:
|
|
205
|
+
* ```typescript
|
|
206
|
+
* adapter.getDay(new Date('2026-02-21')); // → 6 (Saturday)
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
getDay(date: Date): number;
|
|
210
|
+
/**
|
|
211
|
+
* Convert Date to ISO date string (YYYY-MM-DD)
|
|
212
|
+
*
|
|
213
|
+
* CRITICAL: Must be timezone-safe!
|
|
214
|
+
* DO NOT use date.toISOString() as it converts to UTC.
|
|
215
|
+
*
|
|
216
|
+
* Example:
|
|
217
|
+
* ```typescript
|
|
218
|
+
* // Local timezone GMT-6 (CST)
|
|
219
|
+
* const date = new Date('2026-02-21T23:00:00'); // 11 PM CST
|
|
220
|
+
*
|
|
221
|
+
* // ❌ WRONG: date.toISOString().split('T')[0]
|
|
222
|
+
* // Returns "2026-02-22" (shifted to UTC!)
|
|
223
|
+
*
|
|
224
|
+
* // ✅ CORRECT: adapter.toISODate(date)
|
|
225
|
+
* // Returns "2026-02-21" (local date preserved)
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @param date - Date to format (or null)
|
|
229
|
+
* @returns ISO date string in YYYY-MM-DD format (local timezone), empty string if null
|
|
230
|
+
*/
|
|
231
|
+
toISODate(date: Date | null): string;
|
|
232
|
+
/**
|
|
233
|
+
* Parse ISO date string (YYYY-MM-DD) to Date
|
|
234
|
+
*
|
|
235
|
+
* CRITICAL: Must be timezone-safe!
|
|
236
|
+
* DO NOT use new Date(isoString) as it may parse as UTC.
|
|
237
|
+
*
|
|
238
|
+
* Example:
|
|
239
|
+
* ```typescript
|
|
240
|
+
* // ❌ WRONG: new Date('2026-02-21')
|
|
241
|
+
* // May parse as UTC midnight, which is previous day in some timezones
|
|
242
|
+
*
|
|
243
|
+
* // ✅ CORRECT: adapter.parseISODate('2026-02-21')
|
|
244
|
+
* // Returns Date representing 2026-02-21 00:00:00 in local timezone
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* @param isoDate - ISO date string (YYYY-MM-DD) or null
|
|
248
|
+
* @returns Date object or null if invalid
|
|
249
|
+
*/
|
|
250
|
+
parseISODate(isoDate: string | null): Date | null;
|
|
251
|
+
/**
|
|
252
|
+
* Get week start day for locale
|
|
253
|
+
*
|
|
254
|
+
* 0 = Sunday, 1 = Monday, etc.
|
|
255
|
+
*
|
|
256
|
+
* Example:
|
|
257
|
+
* ```typescript
|
|
258
|
+
* adapter.getWeekStart('en-US'); // → 0 (Sunday)
|
|
259
|
+
* adapter.getWeekStart('en-GB'); // → 1 (Monday)
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @param locale - Locale string (e.g., 'en-US', 'es-ES')
|
|
263
|
+
* @returns Day number (0-6)
|
|
264
|
+
*/
|
|
265
|
+
getWeekStart(locale?: string): 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Injection token for DateAdapter
|
|
269
|
+
*
|
|
270
|
+
* Default: NativeDateAdapter (zero dependencies)
|
|
271
|
+
*
|
|
272
|
+
* Override for advanced timezone handling:
|
|
273
|
+
*
|
|
274
|
+
* ```typescript
|
|
275
|
+
* // Luxon with timezone
|
|
276
|
+
* import { LuxonDateAdapter } from '@oneluiz/dual-datepicker/luxon';
|
|
277
|
+
*
|
|
278
|
+
* bootstrapApplication(AppComponent, {
|
|
279
|
+
* providers: [
|
|
280
|
+
* {
|
|
281
|
+
* provide: DATE_ADAPTER,
|
|
282
|
+
* useClass: LuxonDateAdapter
|
|
283
|
+
* }
|
|
284
|
+
* ]
|
|
285
|
+
* });
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* Custom implementation:
|
|
289
|
+
*
|
|
290
|
+
* ```typescript
|
|
291
|
+
* class CustomDateAdapter implements DateAdapter {
|
|
292
|
+
* // Your implementation for backend-specific date handling
|
|
293
|
+
* }
|
|
294
|
+
*
|
|
295
|
+
* provide(DATE_ADAPTER, { useClass: CustomDateAdapter });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export declare const DATE_ADAPTER: InjectionToken<DateAdapter>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Clock Abstraction for SSR-Safe Date Resolution
|
|
3
|
+
*
|
|
4
|
+
* Problem:
|
|
5
|
+
* Presets like "Last 7 Days" or "This Month" use new Date() which causes:
|
|
6
|
+
* - SSR hydration mismatch
|
|
7
|
+
* - Server renders "2026-02-14", client renders "2026-02-15"
|
|
8
|
+
* - Different filters in dashboards
|
|
9
|
+
* - Different queries in ERP/BI
|
|
10
|
+
* - Cache inconsistency
|
|
11
|
+
*
|
|
12
|
+
* Solution:
|
|
13
|
+
* Inject a DateClock to control time deterministically:
|
|
14
|
+
*
|
|
15
|
+
* Server (SSR):
|
|
16
|
+
* provide(DATE_CLOCK, {
|
|
17
|
+
* useValue: { now: () => new Date('2026-02-21T00:00:00Z') }
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* Client (Browser):
|
|
21
|
+
* Uses SystemClock by default (new Date())
|
|
22
|
+
*
|
|
23
|
+
* Testing:
|
|
24
|
+
* provide(DATE_CLOCK, {
|
|
25
|
+
* useValue: { now: () => new Date('2026-01-15T10:30:00Z') }
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* Architecture:
|
|
29
|
+
* - PresetEngine receives DateClock via DI
|
|
30
|
+
* - All preset calculations use clock.now() instead of new Date()
|
|
31
|
+
* - Deterministic: Same clock.now() → Same preset result
|
|
32
|
+
* - SSR-compatible: Server and client resolve identical ranges
|
|
33
|
+
*/
|
|
34
|
+
import { InjectionToken } from '@angular/core';
|
|
35
|
+
/**
|
|
36
|
+
* Clock abstraction for deterministic date resolution
|
|
37
|
+
*
|
|
38
|
+
* Use cases:
|
|
39
|
+
* - SSR: Ensure server and client generate identical presets
|
|
40
|
+
* - Testing: Control time for predictable test results
|
|
41
|
+
* - Replay: Reproduce exact user state from past sessions
|
|
42
|
+
* - Demo: Freeze time for reproducible demos
|
|
43
|
+
*/
|
|
44
|
+
export interface DateClock {
|
|
45
|
+
/**
|
|
46
|
+
* Get current date/time
|
|
47
|
+
*
|
|
48
|
+
* Default implementation returns new Date()
|
|
49
|
+
* Override for SSR, testing, or time-travel scenarios
|
|
50
|
+
*/
|
|
51
|
+
now(): Date;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Injection token for DateClock
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* // Default (uses SystemClock)
|
|
59
|
+
* bootstrapApplication(AppComponent);
|
|
60
|
+
*
|
|
61
|
+
* // SSR Override
|
|
62
|
+
* bootstrapApplication(AppComponent, {
|
|
63
|
+
* providers: [
|
|
64
|
+
* {
|
|
65
|
+
* provide: DATE_CLOCK,
|
|
66
|
+
* useValue: { now: () => new Date('2026-02-21T00:00:00Z') }
|
|
67
|
+
* }
|
|
68
|
+
* ]
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Testing Override
|
|
72
|
+
* TestBed.configureTestingModule({
|
|
73
|
+
* providers: [
|
|
74
|
+
* {
|
|
75
|
+
* provide: DATE_CLOCK,
|
|
76
|
+
* useValue: { now: () => new Date('2026-01-15T12:00:00Z') }
|
|
77
|
+
* }
|
|
78
|
+
* ]
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare const DATE_CLOCK: InjectionToken<DateClock>;
|
|
@@ -14,6 +14,10 @@ export interface DateRangeConfig {
|
|
|
14
14
|
defaultEndTime?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare class DualDateRangeStore {
|
|
17
|
+
private presetEngine;
|
|
18
|
+
private clock;
|
|
19
|
+
private adapter;
|
|
20
|
+
constructor();
|
|
17
21
|
private config;
|
|
18
22
|
private _startDate;
|
|
19
23
|
private _endDate;
|
|
@@ -72,6 +76,11 @@ export declare class DualDateRangeStore {
|
|
|
72
76
|
reset(): void;
|
|
73
77
|
/**
|
|
74
78
|
* Apply a preset by key
|
|
79
|
+
*
|
|
80
|
+
* SSR-Safe: Uses injected DateClock for deterministic resolution
|
|
81
|
+
*
|
|
82
|
+
* @param presetKey - Preset identifier (e.g., 'TODAY', 'LAST_7_DAYS')
|
|
83
|
+
* @param now - Optional date override (defaults to clock.now())
|
|
75
84
|
*/
|
|
76
85
|
applyPreset(presetKey: string, now?: Date): void;
|
|
77
86
|
/**
|
package/core/index.d.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core headless date range logic
|
|
3
|
+
*
|
|
4
|
+
* v3.6.0: Plugin Architecture
|
|
5
|
+
* - RangePresetPlugin interface for extensible presets
|
|
6
|
+
* - PresetRegistry for plugin management
|
|
7
|
+
* - Built-in presets as plugins
|
|
8
|
+
* - Provider functions for custom presets
|
|
9
|
+
*
|
|
3
10
|
* Import from here for clean barrel exports
|
|
4
11
|
*/
|
|
5
12
|
export * from './dual-date-range.store';
|
|
6
13
|
export * from './preset.engine';
|
|
7
14
|
export * from './range.validator';
|
|
15
|
+
export * from './date-clock';
|
|
16
|
+
export * from './system-clock';
|
|
17
|
+
export * from './date-adapter';
|
|
18
|
+
export * from './native-date-adapter';
|
|
19
|
+
export * from './range-preset.plugin';
|
|
20
|
+
export * from './preset-registry';
|
|
21
|
+
export * from './built-in-presets';
|
|
22
|
+
export * from './preset-providers';
|