@melodicdev/components 1.6.2 → 1.6.3
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 +6 -0
- package/lib/components/forms/date-time-picker/date-time-picker.component.d.ts +81 -9
- package/lib/components/forms/date-time-picker/date-time-picker.component.d.ts.map +1 -1
- package/lib/components/forms/date-time-picker/date-time-picker.component.js +163 -26
- package/lib/components/forms/date-time-picker/date-time-picker.styles.d.ts.map +1 -1
- package/lib/components/forms/date-time-picker/date-time-picker.styles.js +32 -0
- package/lib/components/forms/date-time-picker/date-time-picker.template.d.ts.map +1 -1
- package/lib/components/forms/date-time-picker/date-time-picker.template.js +8 -0
- package/lib/components/forms/date-time-picker/tz-utils.d.ts +61 -0
- package/lib/components/forms/date-time-picker/tz-utils.d.ts.map +1 -0
- package/lib/components/forms/date-time-picker/tz-utils.js +145 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,6 +231,12 @@ type ThemeMode = 'light' | 'dark' | 'system';
|
|
|
231
231
|
|
|
232
232
|
---
|
|
233
233
|
|
|
234
|
+
## Working with timezones
|
|
235
|
+
|
|
236
|
+
For datetime fields that need to round-trip a real UTC instant — multi-tenant SaaS, calendar/scheduling apps, anything stored as UTC server-side — use [`<ml-date-time-picker>`](./docs/components/forms.md#ml-date-time-picker) with its `timezone` attribute. The picker anchors the wall-clock the user types to the named IANA zone, surfaces a `valueUtc` field on the `ml:change` event detail, and renders a trailing zone label so users know what zone they're entering. Without `timezone`, the component is a plain naive datetime input — no behavior change for existing consumers. Elsewhere in the app, the right primitive for displaying the same instant in another zone is `Intl.DateTimeFormat(..., { timeZone })`.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
234
240
|
## Theme System
|
|
235
241
|
|
|
236
242
|
Override tokens globally via CSS:
|
|
@@ -1,23 +1,61 @@
|
|
|
1
1
|
import type { IElementRef, OnCreate, OnDestroy } from '@melodicdev/core';
|
|
2
|
+
import type { TimezoneLabelFormat } from './tz-utils.js';
|
|
2
3
|
import '../date-picker/date-picker.component.js';
|
|
3
4
|
import '../time-picker/time-picker.component.js';
|
|
4
5
|
/**
|
|
5
|
-
* ml-date-time-picker - Combined date and time picker
|
|
6
|
+
* ml-date-time-picker - Combined date and time picker with optional timezone support.
|
|
6
7
|
*
|
|
7
|
-
* Composes ml-date-picker and ml-time-picker into a single control.
|
|
8
|
-
* Value format: ISO datetime string (YYYY-MM-DDTHH:mm or YYYY-MM-DDTHH:mm:ss)
|
|
8
|
+
* Composes `ml-date-picker` and `ml-time-picker` into a single control.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* **Without `timezone`** (default): treats `value` as a naive wall-clock
|
|
11
|
+
* string `YYYY-MM-DDTHH:mm`. Behavior is byte-identical to a plain date+time
|
|
12
|
+
* input — no UTC conversion happens.
|
|
13
|
+
*
|
|
14
|
+
* **With `timezone`** (IANA name, e.g. `America/Detroit`): the picker
|
|
15
|
+
* anchors the wall-clock the user sees to the named zone. If the incoming
|
|
16
|
+
* `value` is a UTC ISO string (`...Z` or `±HH:MM` offset), it is parsed as a
|
|
17
|
+
* real instant and rendered as the equivalent wall-clock in `timezone`. If
|
|
18
|
+
* the incoming value is naive, it is treated as wall-clock in `timezone`.
|
|
19
|
+
* The `ml:change` event detail then includes a `valueUtc` field so consumers
|
|
20
|
+
* can store/round-trip the real UTC instant without zone drift.
|
|
21
|
+
*
|
|
22
|
+
* @example Naive (current behavior — unchanged)
|
|
11
23
|
* ```html
|
|
12
24
|
* <ml-date-time-picker label="Event start" value="2026-02-08T09:30"></ml-date-time-picker>
|
|
13
|
-
* <ml-date-time-picker label="Meeting" use-12-hour></ml-date-time-picker>
|
|
14
25
|
* ```
|
|
15
26
|
*
|
|
16
|
-
* @
|
|
27
|
+
* @example Timezone-anchored
|
|
28
|
+
* ```html
|
|
29
|
+
* <ml-date-time-picker
|
|
30
|
+
* label="Event start"
|
|
31
|
+
* timezone="America/Detroit"
|
|
32
|
+
* value="2026-04-27T13:00:00Z"
|
|
33
|
+
* timezone-label="short"
|
|
34
|
+
* viewer-hint
|
|
35
|
+
* ></ml-date-time-picker>
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Then in the consumer:
|
|
39
|
+
* ```ts
|
|
40
|
+
* picker.addEventListener('ml:change', (e) => {
|
|
41
|
+
* // e.detail.value → naive wall-clock in the picker's zone
|
|
42
|
+
* // e.detail.valueUtc → '2026-04-27T13:00:00.000Z' — POST this to your API
|
|
43
|
+
* // e.detail.timezone → 'America/Detroit'
|
|
44
|
+
* fetch('/api/events', { method: 'POST', body: JSON.stringify({ startsAt: e.detail.valueUtc }) });
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @fires ml:change - Detail: `{ value: string, date: string, time: string, valueUtc?: string, timezone?: string }`
|
|
17
49
|
*/
|
|
18
50
|
export declare class DateTimePickerComponent implements IElementRef, OnCreate, OnDestroy {
|
|
19
51
|
elementRef: HTMLElement;
|
|
20
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Selected datetime.
|
|
54
|
+
*
|
|
55
|
+
* - When `timezone` is unset: a naive `YYYY-MM-DDTHH:mm` string.
|
|
56
|
+
* - When `timezone` is set: either a UTC ISO string (`...Z`/`±HH:MM`)
|
|
57
|
+
* or a naive `YYYY-MM-DDTHH:mm` interpreted as wall-clock in `timezone`.
|
|
58
|
+
*/
|
|
21
59
|
value: string;
|
|
22
60
|
/** Placeholder text */
|
|
23
61
|
placeholder: string;
|
|
@@ -45,15 +83,49 @@ export declare class DateTimePickerComponent implements IElementRef, OnCreate, O
|
|
|
45
83
|
step: number;
|
|
46
84
|
/** Use 12-hour format (default: true) */
|
|
47
85
|
twelveHour: boolean;
|
|
48
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* IANA timezone name (e.g. `America/Detroit`). When set, the picker
|
|
88
|
+
* interprets/emits values anchored to this zone. When unset (default),
|
|
89
|
+
* values are naive wall-clock strings.
|
|
90
|
+
*/
|
|
91
|
+
timezone: string;
|
|
92
|
+
/**
|
|
93
|
+
* How to render the trailing timezone label.
|
|
94
|
+
*
|
|
95
|
+
* - `short` → `EDT`
|
|
96
|
+
* - `long` → `Eastern Daylight Time`
|
|
97
|
+
* - `offset` → `GMT-4`
|
|
98
|
+
* - `none` → don't render a label
|
|
99
|
+
*/
|
|
100
|
+
timezoneLabel: TimezoneLabelFormat;
|
|
101
|
+
/**
|
|
102
|
+
* When true AND the browser's UTC offset differs from `timezone` at the
|
|
103
|
+
* current value's instant, render a small subdued line below the input
|
|
104
|
+
* showing the equivalent wall-clock in the viewer's local zone.
|
|
105
|
+
*/
|
|
106
|
+
viewerHint: boolean;
|
|
107
|
+
/** Internal date portion (always naive YYYY-MM-DD in the picker's zone) */
|
|
49
108
|
dateValue: string;
|
|
50
|
-
/** Internal time portion */
|
|
109
|
+
/** Internal time portion (always naive HH:mm in the picker's zone) */
|
|
51
110
|
timeValue: string;
|
|
52
111
|
private _listeners;
|
|
112
|
+
private _lastSyncedValue;
|
|
53
113
|
get use12Hour(): boolean;
|
|
54
114
|
get displayValue(): string;
|
|
115
|
+
/** The naive wall-clock string the user sees, regardless of input format. */
|
|
116
|
+
get naiveValue(): string;
|
|
117
|
+
/** Derived UTC ISO for the current naive value. Empty string when no timezone. */
|
|
118
|
+
get valueUtc(): string;
|
|
119
|
+
/** Trailing timezone label text (e.g. `EDT`). Empty when no timezone or label='none'. */
|
|
120
|
+
get tzLabelText(): string;
|
|
121
|
+
/** Whether the viewer-hint line should be shown right now. */
|
|
122
|
+
get showViewerHint(): boolean;
|
|
123
|
+
/** Pre-rendered viewer hint text (e.g. `(6:00 AM PDT your time)`). */
|
|
124
|
+
get viewerHintText(): string;
|
|
55
125
|
onCreate(): void;
|
|
56
126
|
onDestroy(): void;
|
|
127
|
+
onPropertyChange(name: string, _oldVal: unknown, _newVal: unknown): void;
|
|
128
|
+
private currentInstant;
|
|
57
129
|
private syncFromValue;
|
|
58
130
|
private attachChildListeners;
|
|
59
131
|
private emitChange;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-time-picker.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"date-time-picker.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAYzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAWzD,OAAO,yCAAyC,CAAC;AACjD,OAAO,yCAAyC,CAAC;AAiCjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,qBAMa,uBAAwB,YAAW,WAAW,EAAE,QAAQ,EAAE,SAAS;IACxE,UAAU,EAAG,WAAW,CAAC;IAEhC;;;;;;OAMG;IACI,KAAK,SAAM;IAElB,uBAAuB;IAChB,WAAW,SAA0B;IAE5C,kBAAkB;IACX,KAAK,SAAM;IAElB,gBAAgB;IACT,IAAI,SAAM;IAEjB,oBAAoB;IACb,KAAK,SAAM;IAElB,iBAAiB;IACV,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAQ;IAEvC,qBAAqB;IACd,QAAQ,UAAS;IAExB,qBAAqB;IACd,QAAQ,UAAS;IAExB,2CAA2C;IACpC,OAAO,SAAM;IAEpB,2CAA2C;IACpC,OAAO,SAAM;IAEpB,sCAAsC;IAC/B,OAAO,SAAM;IAEpB,sCAAsC;IAC/B,OAAO,SAAM;IAEpB,2BAA2B;IACpB,IAAI,SAAM;IAEjB,yCAAyC;IAClC,UAAU,UAAQ;IAEzB;;;;OAIG;IACI,QAAQ,SAAM;IAErB;;;;;;;OAOG;IACI,aAAa,EAAE,mBAAmB,CAAW;IAEpD;;;;OAIG;IACI,UAAU,UAAS;IAE1B,2EAA2E;IACpE,SAAS,SAAM;IAEtB,sEAAsE;IAC/D,SAAS,SAAM;IAEtB,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,gBAAgB,CAAM;IAE9B,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED,IAAW,YAAY,IAAI,MAAM,CAEhC;IAED,6EAA6E;IAC7E,IAAW,UAAU,IAAI,MAAM,CAK9B;IAED,kFAAkF;IAClF,IAAW,QAAQ,IAAI,MAAM,CAK5B;IAED,yFAAyF;IACzF,IAAW,WAAW,IAAI,MAAM,CAI/B;IAED,8DAA8D;IAC9D,IAAW,cAAc,IAAI,OAAO,CAKnC;IAED,sEAAsE;IACtE,IAAW,cAAc,IAAI,MAAM,CAKlC;IAEM,QAAQ,IAAI,IAAI;IAKhB,SAAS,IAAI,IAAI;IAKjB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQ/E,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,oBAAoB;IA+E5B,OAAO,CAAC,UAAU;CA8BlB"}
|
|
@@ -8,6 +8,7 @@ import { MelodicComponent } from '@melodicdev/core';
|
|
|
8
8
|
import { registerAdapter } from '@melodicdev/core/forms';
|
|
9
9
|
import { dateTimePickerTemplate } from './date-time-picker.template.js';
|
|
10
10
|
import { dateTimePickerStyles } from './date-time-picker.styles.js';
|
|
11
|
+
import { formatTimezoneLabel, formatViewerHint, isUtcIsoString, naiveToUtcIso, utcToNaive, viewerOffsetDiffersAt } from './tz-utils.js';
|
|
11
12
|
registerAdapter((el) => el.tagName === 'ML-DATE-TIME-PICKER', {
|
|
12
13
|
inputEvent: 'ml:change',
|
|
13
14
|
blurEvent: 'focusout',
|
|
@@ -51,22 +52,59 @@ function formatDateTimeDisplay(value, use12Hour) {
|
|
|
51
52
|
return display;
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
|
-
* ml-date-time-picker - Combined date and time picker
|
|
55
|
+
* ml-date-time-picker - Combined date and time picker with optional timezone support.
|
|
55
56
|
*
|
|
56
|
-
* Composes ml-date-picker and ml-time-picker into a single control.
|
|
57
|
-
* Value format: ISO datetime string (YYYY-MM-DDTHH:mm or YYYY-MM-DDTHH:mm:ss)
|
|
57
|
+
* Composes `ml-date-picker` and `ml-time-picker` into a single control.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
59
|
+
* **Without `timezone`** (default): treats `value` as a naive wall-clock
|
|
60
|
+
* string `YYYY-MM-DDTHH:mm`. Behavior is byte-identical to a plain date+time
|
|
61
|
+
* input — no UTC conversion happens.
|
|
62
|
+
*
|
|
63
|
+
* **With `timezone`** (IANA name, e.g. `America/Detroit`): the picker
|
|
64
|
+
* anchors the wall-clock the user sees to the named zone. If the incoming
|
|
65
|
+
* `value` is a UTC ISO string (`...Z` or `±HH:MM` offset), it is parsed as a
|
|
66
|
+
* real instant and rendered as the equivalent wall-clock in `timezone`. If
|
|
67
|
+
* the incoming value is naive, it is treated as wall-clock in `timezone`.
|
|
68
|
+
* The `ml:change` event detail then includes a `valueUtc` field so consumers
|
|
69
|
+
* can store/round-trip the real UTC instant without zone drift.
|
|
70
|
+
*
|
|
71
|
+
* @example Naive (current behavior — unchanged)
|
|
60
72
|
* ```html
|
|
61
73
|
* <ml-date-time-picker label="Event start" value="2026-02-08T09:30"></ml-date-time-picker>
|
|
62
|
-
* <ml-date-time-picker label="Meeting" use-12-hour></ml-date-time-picker>
|
|
63
74
|
* ```
|
|
64
75
|
*
|
|
65
|
-
* @
|
|
76
|
+
* @example Timezone-anchored
|
|
77
|
+
* ```html
|
|
78
|
+
* <ml-date-time-picker
|
|
79
|
+
* label="Event start"
|
|
80
|
+
* timezone="America/Detroit"
|
|
81
|
+
* value="2026-04-27T13:00:00Z"
|
|
82
|
+
* timezone-label="short"
|
|
83
|
+
* viewer-hint
|
|
84
|
+
* ></ml-date-time-picker>
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* Then in the consumer:
|
|
88
|
+
* ```ts
|
|
89
|
+
* picker.addEventListener('ml:change', (e) => {
|
|
90
|
+
* // e.detail.value → naive wall-clock in the picker's zone
|
|
91
|
+
* // e.detail.valueUtc → '2026-04-27T13:00:00.000Z' — POST this to your API
|
|
92
|
+
* // e.detail.timezone → 'America/Detroit'
|
|
93
|
+
* fetch('/api/events', { method: 'POST', body: JSON.stringify({ startsAt: e.detail.valueUtc }) });
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @fires ml:change - Detail: `{ value: string, date: string, time: string, valueUtc?: string, timezone?: string }`
|
|
66
98
|
*/
|
|
67
99
|
let DateTimePickerComponent = class DateTimePickerComponent {
|
|
68
100
|
constructor() {
|
|
69
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Selected datetime.
|
|
103
|
+
*
|
|
104
|
+
* - When `timezone` is unset: a naive `YYYY-MM-DDTHH:mm` string.
|
|
105
|
+
* - When `timezone` is set: either a UTC ISO string (`...Z`/`±HH:MM`)
|
|
106
|
+
* or a naive `YYYY-MM-DDTHH:mm` interpreted as wall-clock in `timezone`.
|
|
107
|
+
*/
|
|
70
108
|
this.value = '';
|
|
71
109
|
/** Placeholder text */
|
|
72
110
|
this.placeholder = 'Select date and time';
|
|
@@ -94,17 +132,82 @@ let DateTimePickerComponent = class DateTimePickerComponent {
|
|
|
94
132
|
this.step = 15;
|
|
95
133
|
/** Use 12-hour format (default: true) */
|
|
96
134
|
this.twelveHour = true;
|
|
97
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* IANA timezone name (e.g. `America/Detroit`). When set, the picker
|
|
137
|
+
* interprets/emits values anchored to this zone. When unset (default),
|
|
138
|
+
* values are naive wall-clock strings.
|
|
139
|
+
*/
|
|
140
|
+
this.timezone = '';
|
|
141
|
+
/**
|
|
142
|
+
* How to render the trailing timezone label.
|
|
143
|
+
*
|
|
144
|
+
* - `short` → `EDT`
|
|
145
|
+
* - `long` → `Eastern Daylight Time`
|
|
146
|
+
* - `offset` → `GMT-4`
|
|
147
|
+
* - `none` → don't render a label
|
|
148
|
+
*/
|
|
149
|
+
this.timezoneLabel = 'short';
|
|
150
|
+
/**
|
|
151
|
+
* When true AND the browser's UTC offset differs from `timezone` at the
|
|
152
|
+
* current value's instant, render a small subdued line below the input
|
|
153
|
+
* showing the equivalent wall-clock in the viewer's local zone.
|
|
154
|
+
*/
|
|
155
|
+
this.viewerHint = false;
|
|
156
|
+
/** Internal date portion (always naive YYYY-MM-DD in the picker's zone) */
|
|
98
157
|
this.dateValue = '';
|
|
99
|
-
/** Internal time portion */
|
|
158
|
+
/** Internal time portion (always naive HH:mm in the picker's zone) */
|
|
100
159
|
this.timeValue = '';
|
|
101
160
|
this._listeners = [];
|
|
161
|
+
this._lastSyncedValue = '';
|
|
102
162
|
}
|
|
103
163
|
get use12Hour() {
|
|
104
164
|
return this.twelveHour;
|
|
105
165
|
}
|
|
106
166
|
get displayValue() {
|
|
107
|
-
return formatDateTimeDisplay(this.
|
|
167
|
+
return formatDateTimeDisplay(this.naiveValue, this.use12Hour);
|
|
168
|
+
}
|
|
169
|
+
/** The naive wall-clock string the user sees, regardless of input format. */
|
|
170
|
+
get naiveValue() {
|
|
171
|
+
if (this.dateValue && this.timeValue)
|
|
172
|
+
return `${this.dateValue}T${this.timeValue}`;
|
|
173
|
+
if (this.dateValue)
|
|
174
|
+
return this.dateValue;
|
|
175
|
+
if (this.timeValue)
|
|
176
|
+
return this.timeValue;
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
/** Derived UTC ISO for the current naive value. Empty string when no timezone. */
|
|
180
|
+
get valueUtc() {
|
|
181
|
+
if (!this.timezone)
|
|
182
|
+
return '';
|
|
183
|
+
const naive = this.naiveValue;
|
|
184
|
+
if (!naive || !naive.includes('T'))
|
|
185
|
+
return '';
|
|
186
|
+
return naiveToUtcIso(naive, this.timezone);
|
|
187
|
+
}
|
|
188
|
+
/** Trailing timezone label text (e.g. `EDT`). Empty when no timezone or label='none'. */
|
|
189
|
+
get tzLabelText() {
|
|
190
|
+
if (!this.timezone || this.timezoneLabel === 'none')
|
|
191
|
+
return '';
|
|
192
|
+
const instant = this.currentInstant() ?? new Date();
|
|
193
|
+
return formatTimezoneLabel(instant, this.timezone, this.timezoneLabel);
|
|
194
|
+
}
|
|
195
|
+
/** Whether the viewer-hint line should be shown right now. */
|
|
196
|
+
get showViewerHint() {
|
|
197
|
+
if (!this.viewerHint || !this.timezone)
|
|
198
|
+
return false;
|
|
199
|
+
const instant = this.currentInstant();
|
|
200
|
+
if (!instant)
|
|
201
|
+
return false;
|
|
202
|
+
return viewerOffsetDiffersAt(instant, this.timezone);
|
|
203
|
+
}
|
|
204
|
+
/** Pre-rendered viewer hint text (e.g. `(6:00 AM PDT your time)`). */
|
|
205
|
+
get viewerHintText() {
|
|
206
|
+
const instant = this.currentInstant();
|
|
207
|
+
if (!instant)
|
|
208
|
+
return '';
|
|
209
|
+
const { wallClock, label } = formatViewerHint(instant, 'short');
|
|
210
|
+
return label ? `(${wallClock} ${label} your time)` : `(${wallClock} your time)`;
|
|
108
211
|
}
|
|
109
212
|
onCreate() {
|
|
110
213
|
this.syncFromValue();
|
|
@@ -115,10 +218,44 @@ let DateTimePickerComponent = class DateTimePickerComponent {
|
|
|
115
218
|
cleanup();
|
|
116
219
|
this._listeners = [];
|
|
117
220
|
}
|
|
221
|
+
onPropertyChange(name, _oldVal, _newVal) {
|
|
222
|
+
if (name === 'value' || name === 'timezone') {
|
|
223
|
+
// Re-derive internal date/time when the inbound value (or its
|
|
224
|
+
// anchor zone) changes from outside the component.
|
|
225
|
+
queueMicrotask(() => this.syncFromValue());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
currentInstant() {
|
|
229
|
+
if (!this.timezone)
|
|
230
|
+
return null;
|
|
231
|
+
const utcIso = this.valueUtc;
|
|
232
|
+
if (!utcIso)
|
|
233
|
+
return null;
|
|
234
|
+
const d = new Date(utcIso);
|
|
235
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
236
|
+
}
|
|
118
237
|
syncFromValue() {
|
|
119
|
-
|
|
238
|
+
const incoming = this.value ?? '';
|
|
239
|
+
if (incoming === this._lastSyncedValue)
|
|
240
|
+
return;
|
|
241
|
+
this._lastSyncedValue = incoming;
|
|
242
|
+
if (!incoming) {
|
|
243
|
+
this.dateValue = '';
|
|
244
|
+
this.timeValue = '';
|
|
120
245
|
return;
|
|
121
|
-
|
|
246
|
+
}
|
|
247
|
+
// With a timezone AND a UTC ISO input, project onto the zone's wall-clock.
|
|
248
|
+
if (this.timezone && isUtcIsoString(incoming)) {
|
|
249
|
+
const naive = utcToNaive(incoming, this.timezone);
|
|
250
|
+
const [datePart, timePart] = naive.split('T');
|
|
251
|
+
if (datePart)
|
|
252
|
+
this.dateValue = datePart;
|
|
253
|
+
if (timePart)
|
|
254
|
+
this.timeValue = timePart;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Otherwise treat incoming as naive (zoned or unzoned — both render the same).
|
|
258
|
+
const [datePart, timePart] = incoming.split('T');
|
|
122
259
|
if (datePart)
|
|
123
260
|
this.dateValue = datePart;
|
|
124
261
|
if (timePart)
|
|
@@ -197,23 +334,23 @@ let DateTimePickerComponent = class DateTimePickerComponent {
|
|
|
197
334
|
}
|
|
198
335
|
}
|
|
199
336
|
emitChange() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
337
|
+
this.value = this.naiveValue;
|
|
338
|
+
this._lastSyncedValue = this.value;
|
|
339
|
+
const detail = {
|
|
340
|
+
value: this.value,
|
|
341
|
+
date: this.dateValue,
|
|
342
|
+
time: this.timeValue
|
|
343
|
+
};
|
|
344
|
+
if (this.timezone) {
|
|
345
|
+
detail.timezone = this.timezone;
|
|
346
|
+
const utc = this.valueUtc;
|
|
347
|
+
if (utc)
|
|
348
|
+
detail.valueUtc = utc;
|
|
208
349
|
}
|
|
209
350
|
this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
|
|
210
351
|
bubbles: true,
|
|
211
352
|
composed: true,
|
|
212
|
-
detail
|
|
213
|
-
value: this.value,
|
|
214
|
-
date: this.dateValue,
|
|
215
|
-
time: this.timeValue
|
|
216
|
-
}
|
|
353
|
+
detail
|
|
217
354
|
}));
|
|
218
355
|
}
|
|
219
356
|
};
|
|
@@ -222,7 +359,7 @@ DateTimePickerComponent = __decorate([
|
|
|
222
359
|
selector: 'ml-date-time-picker',
|
|
223
360
|
template: dateTimePickerTemplate,
|
|
224
361
|
styles: dateTimePickerStyles,
|
|
225
|
-
attributes: ['value', 'placeholder', 'label', 'hint', 'error', 'size', 'disabled', 'required', 'min-date', 'max-date', 'min-time', 'max-time', 'step', 'twelve-hour']
|
|
362
|
+
attributes: ['value', 'placeholder', 'label', 'hint', 'error', 'size', 'disabled', 'required', 'min-date', 'max-date', 'min-time', 'max-time', 'step', 'twelve-hour', 'timezone', 'timezone-label', 'viewer-hint']
|
|
226
363
|
})
|
|
227
364
|
], DateTimePickerComponent);
|
|
228
365
|
export { DateTimePickerComponent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-time-picker.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"date-time-picker.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,iDAgKhC,CAAC"}
|
|
@@ -39,6 +39,17 @@ export const dateTimePickerStyles = () => css `
|
|
|
39
39
|
--ml-date-time-picker-hint-color: var(--ml-color-text-muted);
|
|
40
40
|
--ml-date-time-picker-hint-font-size: var(--ml-text-sm);
|
|
41
41
|
|
|
42
|
+
/* --- Timezone label --- */
|
|
43
|
+
--ml-date-time-picker-tz-label-color: var(--ml-color-text-muted);
|
|
44
|
+
--ml-date-time-picker-tz-label-font-size: var(--ml-text-sm);
|
|
45
|
+
--ml-date-time-picker-tz-label-font-weight: var(--ml-font-medium);
|
|
46
|
+
--ml-date-time-picker-tz-label-padding-x: var(--ml-space-2-5);
|
|
47
|
+
|
|
48
|
+
/* --- Viewer hint (subdued line below input) --- */
|
|
49
|
+
--ml-date-time-picker-viewer-hint-color: var(--ml-color-text-muted);
|
|
50
|
+
--ml-date-time-picker-viewer-hint-font-size: var(--ml-text-xs);
|
|
51
|
+
--ml-date-time-picker-viewer-hint-margin-top: var(--ml-space-1);
|
|
52
|
+
|
|
42
53
|
/* --- Transition --- */
|
|
43
54
|
--ml-date-time-picker-transition-duration: var(--ml-duration-150);
|
|
44
55
|
--ml-date-time-picker-transition-easing: var(--ml-ease-in-out);
|
|
@@ -104,6 +115,27 @@ export const dateTimePickerStyles = () => css `
|
|
|
104
115
|
flex-shrink: 0;
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
/* Timezone label (e.g. EDT) trailing the inputs */
|
|
119
|
+
.ml-date-time-picker__tz-label {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
padding: 0 var(--ml-date-time-picker-tz-label-padding-x);
|
|
123
|
+
font-size: var(--ml-date-time-picker-tz-label-font-size);
|
|
124
|
+
font-weight: var(--ml-date-time-picker-tz-label-font-weight);
|
|
125
|
+
color: var(--ml-date-time-picker-tz-label-color);
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
user-select: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Viewer-hint line: shows the same instant in the viewer's local zone */
|
|
131
|
+
.ml-date-time-picker__viewer-hint {
|
|
132
|
+
display: block;
|
|
133
|
+
margin-top: var(--ml-date-time-picker-viewer-hint-margin-top);
|
|
134
|
+
font-size: var(--ml-date-time-picker-viewer-hint-font-size);
|
|
135
|
+
color: var(--ml-date-time-picker-viewer-hint-color);
|
|
136
|
+
line-height: var(--ml-date-time-picker-label-line-height);
|
|
137
|
+
}
|
|
138
|
+
|
|
107
139
|
/* Disabled */
|
|
108
140
|
.ml-date-time-picker--disabled .ml-date-time-picker__row {
|
|
109
141
|
opacity: var(--ml-date-time-picker-disabled-opacity);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-time-picker.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"date-time-picker.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/date-time-picker.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,uBAAuB,6CAuDhE"}
|
|
@@ -37,8 +37,16 @@ export function dateTimePickerTemplate(c) {
|
|
|
37
37
|
.twelveHour=${c.use12Hour}
|
|
38
38
|
placeholder="Select time"
|
|
39
39
|
></ml-time-picker>
|
|
40
|
+
|
|
41
|
+
${when(!!c.tzLabelText, () => html `
|
|
42
|
+
<span class="ml-date-time-picker__tz-label" aria-label="Timezone">${c.tzLabelText}</span>
|
|
43
|
+
`)}
|
|
40
44
|
</div>
|
|
41
45
|
|
|
46
|
+
${when(c.showViewerHint, () => html `
|
|
47
|
+
<span class="ml-date-time-picker__viewer-hint">${c.viewerHintText}</span>
|
|
48
|
+
`)}
|
|
49
|
+
|
|
42
50
|
${when(!!c.error, () => html `<span class="ml-date-time-picker__error">${c.error}</span>`, () => html `${when(!!c.hint, () => html `<span class="ml-date-time-picker__hint">${c.hint}</span>`)}`)}
|
|
43
51
|
</div>
|
|
44
52
|
`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timezone helpers for ml-date-time-picker.
|
|
3
|
+
*
|
|
4
|
+
* These functions translate between a naive wall-clock string
|
|
5
|
+
* (`YYYY-MM-DDTHH:mm[:ss]`) and a real UTC instant, anchored to an IANA
|
|
6
|
+
* timezone name (e.g. `America/Detroit`). All conversions are done via
|
|
7
|
+
* `Intl.DateTimeFormat` offset reflection — there are no hardcoded zone
|
|
8
|
+
* tables.
|
|
9
|
+
*
|
|
10
|
+
* DST policy:
|
|
11
|
+
* - Spring-forward gap (e.g. 2026-03-08 02:30 in America/Detroit doesn't
|
|
12
|
+
* exist) resolves to the post-jump wall clock, i.e. the input is
|
|
13
|
+
* interpreted one hour forward (03:30 EDT in this example).
|
|
14
|
+
* - Fall-back ambiguity (e.g. 2026-11-01 01:30 happens twice) resolves
|
|
15
|
+
* to the FIRST occurrence (still-DST / EDT). This matches RFC 5545
|
|
16
|
+
* VTIMEZONE behavior and the default in major calendar apps.
|
|
17
|
+
*/
|
|
18
|
+
export type TimezoneLabelFormat = 'short' | 'long' | 'offset' | 'none';
|
|
19
|
+
export declare function isUtcIsoString(value: string): boolean;
|
|
20
|
+
export declare function isNaiveDateTime(value: string): boolean;
|
|
21
|
+
/** Offset of `timeZone` relative to UTC at `date`, in milliseconds (positive = ahead of UTC). */
|
|
22
|
+
export declare function getZoneOffsetMs(date: Date, timeZone: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Convert a naive wall-clock string to a real UTC instant, treating the
|
|
25
|
+
* naive value as a wall clock in `timeZone`.
|
|
26
|
+
*
|
|
27
|
+
* Returns the UTC ISO 8601 string with `Z` suffix.
|
|
28
|
+
*/
|
|
29
|
+
export declare function naiveToUtcIso(naive: string, timeZone: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Convert a UTC instant (Date or ISO string) to a naive wall-clock string
|
|
32
|
+
* in `timeZone`. Output format: `YYYY-MM-DDTHH:mm`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function utcToNaive(instant: Date | string, timeZone: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Format a timezone name/abbreviation/offset for display next to the input.
|
|
37
|
+
*
|
|
38
|
+
* - `short` → `EDT`
|
|
39
|
+
* - `long` → `Eastern Daylight Time`
|
|
40
|
+
* - `offset` → `GMT-4`
|
|
41
|
+
* - `none` → `''`
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatTimezoneLabel(instant: Date, timeZone: string, format: TimezoneLabelFormat): string;
|
|
44
|
+
/** Resolves the browser's local IANA timezone, falling back to UTC. */
|
|
45
|
+
export declare function getLocalTimeZone(): string;
|
|
46
|
+
/**
|
|
47
|
+
* True iff `timeZone` produces a different UTC offset from the viewer's
|
|
48
|
+
* local zone at the given instant. Compares offsets, not zone names —
|
|
49
|
+
* `America/Detroit` and `America/New_York` won't trigger this because
|
|
50
|
+
* they share an offset year-round.
|
|
51
|
+
*/
|
|
52
|
+
export declare function viewerOffsetDiffersAt(instant: Date, timeZone: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Render the viewer's local wall-clock + tz label for the given UTC instant.
|
|
55
|
+
* Used by the optional `viewer-hint` line.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatViewerHint(instant: Date, format?: TimezoneLabelFormat): {
|
|
58
|
+
wallClock: string;
|
|
59
|
+
label: string;
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=tz-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tz-utils.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-time-picker/tz-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAKvE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAoCD,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM3E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GACzB,MAAM,CAWR;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI9E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,GAAE,mBAA6B,GAAG;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd,CAYA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timezone helpers for ml-date-time-picker.
|
|
3
|
+
*
|
|
4
|
+
* These functions translate between a naive wall-clock string
|
|
5
|
+
* (`YYYY-MM-DDTHH:mm[:ss]`) and a real UTC instant, anchored to an IANA
|
|
6
|
+
* timezone name (e.g. `America/Detroit`). All conversions are done via
|
|
7
|
+
* `Intl.DateTimeFormat` offset reflection — there are no hardcoded zone
|
|
8
|
+
* tables.
|
|
9
|
+
*
|
|
10
|
+
* DST policy:
|
|
11
|
+
* - Spring-forward gap (e.g. 2026-03-08 02:30 in America/Detroit doesn't
|
|
12
|
+
* exist) resolves to the post-jump wall clock, i.e. the input is
|
|
13
|
+
* interpreted one hour forward (03:30 EDT in this example).
|
|
14
|
+
* - Fall-back ambiguity (e.g. 2026-11-01 01:30 happens twice) resolves
|
|
15
|
+
* to the FIRST occurrence (still-DST / EDT). This matches RFC 5545
|
|
16
|
+
* VTIMEZONE behavior and the default in major calendar apps.
|
|
17
|
+
*/
|
|
18
|
+
const NAIVE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?$/;
|
|
19
|
+
const UTC_RE = /(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
20
|
+
export function isUtcIsoString(value) {
|
|
21
|
+
return !!value && UTC_RE.test(value);
|
|
22
|
+
}
|
|
23
|
+
export function isNaiveDateTime(value) {
|
|
24
|
+
return !!value && NAIVE_RE.test(value);
|
|
25
|
+
}
|
|
26
|
+
function getZonedParts(date, timeZone) {
|
|
27
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
28
|
+
timeZone,
|
|
29
|
+
hourCycle: 'h23',
|
|
30
|
+
year: 'numeric',
|
|
31
|
+
month: '2-digit',
|
|
32
|
+
day: '2-digit',
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit',
|
|
35
|
+
second: '2-digit'
|
|
36
|
+
});
|
|
37
|
+
const parts = {};
|
|
38
|
+
for (const p of fmt.formatToParts(date)) {
|
|
39
|
+
if (p.type !== 'literal')
|
|
40
|
+
parts[p.type] = p.value;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
year: Number(parts.year),
|
|
44
|
+
month: Number(parts.month),
|
|
45
|
+
day: Number(parts.day),
|
|
46
|
+
hour: Number(parts.hour) === 24 ? 0 : Number(parts.hour),
|
|
47
|
+
minute: Number(parts.minute),
|
|
48
|
+
second: Number(parts.second)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Offset of `timeZone` relative to UTC at `date`, in milliseconds (positive = ahead of UTC). */
|
|
52
|
+
export function getZoneOffsetMs(date, timeZone) {
|
|
53
|
+
const p = getZonedParts(date, timeZone);
|
|
54
|
+
const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
55
|
+
return asUtc - date.getTime();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Convert a naive wall-clock string to a real UTC instant, treating the
|
|
59
|
+
* naive value as a wall clock in `timeZone`.
|
|
60
|
+
*
|
|
61
|
+
* Returns the UTC ISO 8601 string with `Z` suffix.
|
|
62
|
+
*/
|
|
63
|
+
export function naiveToUtcIso(naive, timeZone) {
|
|
64
|
+
if (!naive)
|
|
65
|
+
return '';
|
|
66
|
+
const padded = naive.length === 16 ? `${naive}:00` : naive;
|
|
67
|
+
const naiveAsUtc = new Date(`${padded}Z`);
|
|
68
|
+
if (Number.isNaN(naiveAsUtc.getTime()))
|
|
69
|
+
return '';
|
|
70
|
+
const offset = getZoneOffsetMs(naiveAsUtc, timeZone);
|
|
71
|
+
const real = new Date(naiveAsUtc.getTime() - offset);
|
|
72
|
+
return real.toISOString();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Convert a UTC instant (Date or ISO string) to a naive wall-clock string
|
|
76
|
+
* in `timeZone`. Output format: `YYYY-MM-DDTHH:mm`.
|
|
77
|
+
*/
|
|
78
|
+
export function utcToNaive(instant, timeZone) {
|
|
79
|
+
const date = typeof instant === 'string' ? new Date(instant) : instant;
|
|
80
|
+
if (Number.isNaN(date.getTime()))
|
|
81
|
+
return '';
|
|
82
|
+
const p = getZonedParts(date, timeZone);
|
|
83
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
84
|
+
return `${p.year}-${pad(p.month)}-${pad(p.day)}T${pad(p.hour)}:${pad(p.minute)}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Format a timezone name/abbreviation/offset for display next to the input.
|
|
88
|
+
*
|
|
89
|
+
* - `short` → `EDT`
|
|
90
|
+
* - `long` → `Eastern Daylight Time`
|
|
91
|
+
* - `offset` → `GMT-4`
|
|
92
|
+
* - `none` → `''`
|
|
93
|
+
*/
|
|
94
|
+
export function formatTimezoneLabel(instant, timeZone, format) {
|
|
95
|
+
if (format === 'none' || !timeZone)
|
|
96
|
+
return '';
|
|
97
|
+
const timeZoneName = format === 'long' ? 'long' : format === 'offset' ? 'shortOffset' : 'short';
|
|
98
|
+
try {
|
|
99
|
+
const fmt = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName });
|
|
100
|
+
const part = fmt.formatToParts(instant).find((p) => p.type === 'timeZoneName');
|
|
101
|
+
return part?.value ?? '';
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Resolves the browser's local IANA timezone, falling back to UTC. */
|
|
108
|
+
export function getLocalTimeZone() {
|
|
109
|
+
try {
|
|
110
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return 'UTC';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True iff `timeZone` produces a different UTC offset from the viewer's
|
|
118
|
+
* local zone at the given instant. Compares offsets, not zone names —
|
|
119
|
+
* `America/Detroit` and `America/New_York` won't trigger this because
|
|
120
|
+
* they share an offset year-round.
|
|
121
|
+
*/
|
|
122
|
+
export function viewerOffsetDiffersAt(instant, timeZone) {
|
|
123
|
+
const local = getLocalTimeZone();
|
|
124
|
+
if (!timeZone || local === timeZone)
|
|
125
|
+
return false;
|
|
126
|
+
return getZoneOffsetMs(instant, local) !== getZoneOffsetMs(instant, timeZone);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Render the viewer's local wall-clock + tz label for the given UTC instant.
|
|
130
|
+
* Used by the optional `viewer-hint` line.
|
|
131
|
+
*/
|
|
132
|
+
export function formatViewerHint(instant, format = 'short') {
|
|
133
|
+
const local = getLocalTimeZone();
|
|
134
|
+
const naive = utcToNaive(instant, local);
|
|
135
|
+
const [, time] = naive.split('T');
|
|
136
|
+
const [hStr, mStr] = (time ?? '').split(':');
|
|
137
|
+
const h24 = Number(hStr);
|
|
138
|
+
const period = h24 >= 12 ? 'PM' : 'AM';
|
|
139
|
+
let h12 = h24 % 12;
|
|
140
|
+
if (h12 === 0)
|
|
141
|
+
h12 = 12;
|
|
142
|
+
const wallClock = `${h12}:${mStr} ${period}`;
|
|
143
|
+
const label = formatTimezoneLabel(instant, local, format);
|
|
144
|
+
return { wallClock, label };
|
|
145
|
+
}
|