@oneluiz/dual-datepicker 3.4.0 → 3.5.1
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 +134 -1
- package/core/date-adapter.d.ts +298 -0
- package/core/date-clock.d.ts +82 -0
- package/core/dual-date-range.store.d.ts +113 -0
- package/core/index.d.ts +11 -0
- package/core/native-date-adapter.d.ts +152 -0
- package/core/preset.engine.d.ts +88 -0
- package/core/range.validator.d.ts +37 -0
- package/core/system-clock.d.ts +13 -0
- package/dual-datepicker.component.d.ts +17 -11
- package/esm2022/core/date-adapter.mjs +77 -0
- package/esm2022/core/date-clock.mjs +65 -0
- package/esm2022/core/dual-date-range.store.mjs +329 -0
- package/esm2022/core/index.mjs +12 -0
- package/esm2022/core/native-date-adapter.mjs +286 -0
- package/esm2022/core/preset.engine.mjs +303 -0
- package/esm2022/core/range.validator.mjs +105 -0
- package/esm2022/core/system-clock.mjs +34 -0
- package/esm2022/dual-datepicker.component.mjs +239 -195
- package/esm2022/public-api.mjs +5 -2
- package/fesm2022/oneluiz-dual-datepicker.mjs +1431 -204
- package/fesm2022/oneluiz-dual-datepicker.mjs.map +1 -1
- package/package.json +5 -3
- package/public-api.d.ts +1 -0
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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.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! 🎯
|
|
7
|
+
|
|
5
8
|
[](https://www.npmjs.com/package/@oneluiz/dual-datepicker)
|
|
6
9
|
[](https://www.npmjs.com/package/@oneluiz/dual-datepicker)
|
|
7
10
|

|
|
@@ -17,6 +20,54 @@ npm install @oneluiz/dual-datepicker
|
|
|
17
20
|
|
|
18
21
|
---
|
|
19
22
|
|
|
23
|
+
## 🌟 What's New
|
|
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
|
+
|
|
44
|
+
### Headless Architecture (v3.5.0)
|
|
45
|
+
|
|
46
|
+
Use date range logic **without the UI component**:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Inject the store anywhere - no UI needed!
|
|
50
|
+
const rangeStore = inject(DualDateRangeStore);
|
|
51
|
+
|
|
52
|
+
// Apply preset
|
|
53
|
+
rangeStore.applyPreset('THIS_MONTH');
|
|
54
|
+
|
|
55
|
+
// Use in API calls
|
|
56
|
+
const range = rangeStore.range();
|
|
57
|
+
http.get(`/api/sales?start=${range.start}&end=${range.end}`);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Perfect for:**
|
|
61
|
+
- 📊 Dashboard filters (control multiple charts)
|
|
62
|
+
- 🏢 SSR applications
|
|
63
|
+
- 🔄 Global state management
|
|
64
|
+
- 🎯 Service-layer filtering
|
|
65
|
+
- 📈 Analytics and BI tools
|
|
66
|
+
|
|
67
|
+
**[📖 Read the Headless Architecture Guide →](docs/HEADLESS.md)**
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
20
71
|
## 📋 Table of Contents
|
|
21
72
|
|
|
22
73
|
- [Features](#-features)
|
|
@@ -26,6 +77,7 @@ npm install @oneluiz/dual-datepicker
|
|
|
26
77
|
- [Basic Usage](#basic-usage)
|
|
27
78
|
- [Reactive Forms](#with-reactive-forms)
|
|
28
79
|
- [Angular Signals](#with-angular-signals)
|
|
80
|
+
- [Headless Usage](#headless-usage-new) ⭐ NEW
|
|
29
81
|
- [Advanced Features](#-advanced-features)
|
|
30
82
|
- [Multi-Range Selection](#multi-range-support)
|
|
31
83
|
- [Disabled Dates](#disabled-dates)
|
|
@@ -169,6 +221,80 @@ dateRange = signal<DateRange | null>(null);
|
|
|
169
221
|
</ngx-dual-datepicker>
|
|
170
222
|
```
|
|
171
223
|
|
|
224
|
+
### Headless Usage (NEW) ⭐
|
|
225
|
+
|
|
226
|
+
**Use date range state WITHOUT the UI component** - perfect for SSR, services, and global filters!
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { Component, inject } from '@angular/core';
|
|
230
|
+
import { DualDateRangeStore } from '@oneluiz/dual-datepicker';
|
|
231
|
+
import { HttpClient } from '@angular/common/http';
|
|
232
|
+
|
|
233
|
+
@Component({
|
|
234
|
+
template: `
|
|
235
|
+
<div class="dashboard">
|
|
236
|
+
<button (click)="setPreset('TODAY')">Today</button>
|
|
237
|
+
<button (click)="setPreset('THIS_MONTH')">This Month</button>
|
|
238
|
+
<p>{{ rangeText() }}</p>
|
|
239
|
+
</div>
|
|
240
|
+
`
|
|
241
|
+
})
|
|
242
|
+
export class DashboardComponent {
|
|
243
|
+
private rangeStore = inject(DualDateRangeStore);
|
|
244
|
+
private http = inject(HttpClient);
|
|
245
|
+
|
|
246
|
+
// Expose signals for template
|
|
247
|
+
rangeText = this.rangeStore.rangeText;
|
|
248
|
+
|
|
249
|
+
setPreset(key: string) {
|
|
250
|
+
this.rangeStore.applyPreset(key);
|
|
251
|
+
|
|
252
|
+
// Use in API call
|
|
253
|
+
const range = this.rangeStore.range();
|
|
254
|
+
this.http.get(`/api/sales`, {
|
|
255
|
+
params: { start: range.start, end: range.end }
|
|
256
|
+
}).subscribe(data => console.log(data));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Benefits:**
|
|
262
|
+
- ✅ No UI component needed
|
|
263
|
+
- ✅ SSR-compatible
|
|
264
|
+
- ✅ Global state management
|
|
265
|
+
- ✅ Perfect for services and guards
|
|
266
|
+
- ✅ Testable and deterministic
|
|
267
|
+
|
|
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)**
|
|
297
|
+
|
|
172
298
|
---
|
|
173
299
|
|
|
174
300
|
## 🎯 Advanced Features
|
|
@@ -1998,6 +2124,12 @@ export class ExampleComponent {
|
|
|
1998
2124
|
|
|
1999
2125
|
Recently shipped:
|
|
2000
2126
|
|
|
2127
|
+
**v3.4.0:**
|
|
2128
|
+
- ✅ **Time Picker** - Select precise datetime ranges with 12h/24h format
|
|
2129
|
+
- ✅ **Configurable Minute Steps** - Choose 1, 5, 15, or 30-minute intervals
|
|
2130
|
+
- ✅ **Default Times** - Set default start/end times
|
|
2131
|
+
- ✅ **Full Theme Support** - Works seamlessly with all built-in themes
|
|
2132
|
+
|
|
2001
2133
|
**v3.3.0:**
|
|
2002
2134
|
- ✅ **Theming System** - Pre-built themes for Bootstrap, Bulma, Foundation, Tailwind CSS, and Custom
|
|
2003
2135
|
- ✅ **CSS Variables Support** - 13 customizable variables for branding
|
|
@@ -2025,9 +2157,10 @@ Recently shipped:
|
|
|
2025
2157
|
|
|
2026
2158
|
Planned features:
|
|
2027
2159
|
|
|
2028
|
-
- ⬜ **Time Picker** - Select date + time ranges
|
|
2029
2160
|
- ⬜ **Mobile Optimizations** - Enhanced touch gestures and responsive layout
|
|
2030
2161
|
- ⬜ **Range Shortcuts** - Quick selection buttons (Today, This Week, etc.)
|
|
2162
|
+
- ⬜ **Time Constraints** - Min/max time validation and business hours
|
|
2163
|
+
- ⬜ **Multi-range + Time Picker** - Combined support for multiple datetime ranges
|
|
2031
2164
|
|
|
2032
2165
|
## 📄 License
|
|
2033
2166
|
|
|
@@ -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>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
export interface DateRangeState {
|
|
3
|
+
start: string;
|
|
4
|
+
end: string;
|
|
5
|
+
startTime?: string;
|
|
6
|
+
endTime?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DateRangeConfig {
|
|
9
|
+
minDate?: Date;
|
|
10
|
+
maxDate?: Date;
|
|
11
|
+
disabledDates?: Date[] | ((date: Date) => boolean);
|
|
12
|
+
enableTimePicker?: boolean;
|
|
13
|
+
defaultStartTime?: string;
|
|
14
|
+
defaultEndTime?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class DualDateRangeStore {
|
|
17
|
+
private presetEngine;
|
|
18
|
+
private clock;
|
|
19
|
+
private adapter;
|
|
20
|
+
constructor();
|
|
21
|
+
private config;
|
|
22
|
+
private _startDate;
|
|
23
|
+
private _endDate;
|
|
24
|
+
private _leftMonth;
|
|
25
|
+
private _rightMonth;
|
|
26
|
+
private _selectingStart;
|
|
27
|
+
private _startTime;
|
|
28
|
+
private _endTime;
|
|
29
|
+
private _pendingStart;
|
|
30
|
+
private _pendingEnd;
|
|
31
|
+
private _hasPendingChanges;
|
|
32
|
+
readonly startDate: import("@angular/core").Signal<Date>;
|
|
33
|
+
readonly endDate: import("@angular/core").Signal<Date>;
|
|
34
|
+
readonly leftMonth: import("@angular/core").Signal<Date>;
|
|
35
|
+
readonly rightMonth: import("@angular/core").Signal<Date>;
|
|
36
|
+
readonly selectingStart: import("@angular/core").Signal<boolean>;
|
|
37
|
+
readonly startTime: import("@angular/core").Signal<string>;
|
|
38
|
+
readonly endTime: import("@angular/core").Signal<string>;
|
|
39
|
+
readonly hasPendingChanges: import("@angular/core").Signal<boolean>;
|
|
40
|
+
readonly range: import("@angular/core").Signal<DateRangeState>;
|
|
41
|
+
readonly isValid: import("@angular/core").Signal<boolean>;
|
|
42
|
+
readonly rangeText: import("@angular/core").Signal<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Configure the store
|
|
45
|
+
*/
|
|
46
|
+
configure(config: Partial<DateRangeConfig>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Set start date (with validation)
|
|
49
|
+
*/
|
|
50
|
+
setStart(date: Date | string | null): void;
|
|
51
|
+
/**
|
|
52
|
+
* Set end date (with validation)
|
|
53
|
+
*/
|
|
54
|
+
setEnd(date: Date | string | null): void;
|
|
55
|
+
/**
|
|
56
|
+
* Set complete range at once
|
|
57
|
+
*/
|
|
58
|
+
setRange(start: Date | string | null, end: Date | string | null): void;
|
|
59
|
+
/**
|
|
60
|
+
* Set pending selection (for requireApply mode)
|
|
61
|
+
*/
|
|
62
|
+
setPendingStart(date: Date | string | null): void;
|
|
63
|
+
setPendingEnd(date: Date | string | null): void;
|
|
64
|
+
/**
|
|
65
|
+
* Apply pending changes
|
|
66
|
+
*/
|
|
67
|
+
applyPending(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Cancel pending changes
|
|
70
|
+
*/
|
|
71
|
+
cancelPending(): void;
|
|
72
|
+
private clearPending;
|
|
73
|
+
/**
|
|
74
|
+
* Reset to empty state
|
|
75
|
+
*/
|
|
76
|
+
reset(): void;
|
|
77
|
+
/**
|
|
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())
|
|
84
|
+
*/
|
|
85
|
+
applyPreset(presetKey: string, now?: Date): void;
|
|
86
|
+
/**
|
|
87
|
+
* Navigate left calendar month
|
|
88
|
+
*/
|
|
89
|
+
setLeftMonth(date: Date): void;
|
|
90
|
+
/**
|
|
91
|
+
* Navigate right calendar month
|
|
92
|
+
*/
|
|
93
|
+
setRightMonth(date: Date): void;
|
|
94
|
+
/**
|
|
95
|
+
* Set time values
|
|
96
|
+
*/
|
|
97
|
+
setStartTime(time: string): void;
|
|
98
|
+
setEndTime(time: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* Get current state as snapshot
|
|
101
|
+
*/
|
|
102
|
+
getSnapshot(): DateRangeState;
|
|
103
|
+
/**
|
|
104
|
+
* Load state from snapshot
|
|
105
|
+
*/
|
|
106
|
+
loadSnapshot(snapshot: DateRangeState): void;
|
|
107
|
+
private parseDate;
|
|
108
|
+
private getNextMonth;
|
|
109
|
+
private getPreviousMonth;
|
|
110
|
+
private formatDateShort;
|
|
111
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DualDateRangeStore, never>;
|
|
112
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<DualDateRangeStore>;
|
|
113
|
+
}
|
package/core/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core headless date range logic
|
|
3
|
+
* Import from here for clean barrel exports
|
|
4
|
+
*/
|
|
5
|
+
export * from './dual-date-range.store';
|
|
6
|
+
export * from './preset.engine';
|
|
7
|
+
export * from './range.validator';
|
|
8
|
+
export * from './date-clock';
|
|
9
|
+
export * from './system-clock';
|
|
10
|
+
export * from './date-adapter';
|
|
11
|
+
export * from './native-date-adapter';
|