@melodicdev/components 1.6.1 → 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/assets/melodic-components.js +21 -6
- package/assets/melodic-components.js.map +1 -1
- package/assets/melodic-components.min.js +21 -6
- package/lib/components/data-display/table/table.styles.d.ts.map +1 -1
- package/lib/components/data-display/table/table.styles.js +14 -4
- package/lib/components/data-display/table/table.template.d.ts.map +1 -1
- package/lib/components/data-display/table/table.template.js +7 -2
- 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
|
@@ -12140,6 +12140,7 @@ function Uo(e) {
|
|
|
12140
12140
|
`)}
|
|
12141
12141
|
${k(e.columns, (t) => t.key, (t) => i`
|
|
12142
12142
|
<th
|
|
12143
|
+
part="header-cell"
|
|
12143
12144
|
class=${g({
|
|
12144
12145
|
"ml-table__th": !0,
|
|
12145
12146
|
"ml-table__th--sortable": !!t.sortable,
|
|
@@ -12172,6 +12173,7 @@ function Uo(e) {
|
|
|
12172
12173
|
const a = e.startIndex + r;
|
|
12173
12174
|
return i`
|
|
12174
12175
|
<tr
|
|
12176
|
+
part="row"
|
|
12175
12177
|
class=${g({
|
|
12176
12178
|
"ml-table__row": !0,
|
|
12177
12179
|
"ml-table__row--selected": e.isRowSelected(a)
|
|
@@ -12191,10 +12193,13 @@ function Uo(e) {
|
|
|
12191
12193
|
</td>
|
|
12192
12194
|
`)}
|
|
12193
12195
|
${k(e.columns, (l) => l.key, (l) => i`
|
|
12194
|
-
<td
|
|
12196
|
+
<td
|
|
12197
|
+
part="cell"
|
|
12198
|
+
class=${g({
|
|
12195
12199
|
"ml-table__td": !0,
|
|
12196
12200
|
[`ml-table__td--${l.align ?? "left"}`]: !0
|
|
12197
|
-
})}
|
|
12201
|
+
})}
|
|
12202
|
+
>
|
|
12198
12203
|
${Ko(l, t, a)}
|
|
12199
12204
|
</td>
|
|
12200
12205
|
`)}
|
|
@@ -12267,6 +12272,16 @@ const Wo = () => _`
|
|
|
12267
12272
|
|
|
12268
12273
|
/* ── Table: cells ── */
|
|
12269
12274
|
--ml-table-cell-color: var(--ml-color-text);
|
|
12275
|
+
--ml-table-cell-padding-y: var(--ml-space-4);
|
|
12276
|
+
--ml-table-cell-padding-x: var(--ml-space-6);
|
|
12277
|
+
--ml-table-cell-padding-y-sm: var(--ml-space-2-5);
|
|
12278
|
+
--ml-table-cell-padding-x-sm: var(--ml-space-4);
|
|
12279
|
+
|
|
12280
|
+
/* ── Table: header cells ── */
|
|
12281
|
+
--ml-table-header-padding-y: var(--ml-space-3);
|
|
12282
|
+
--ml-table-header-padding-x: var(--ml-space-6);
|
|
12283
|
+
--ml-table-header-padding-y-sm: var(--ml-space-2);
|
|
12284
|
+
--ml-table-header-padding-x-sm: var(--ml-space-4);
|
|
12270
12285
|
|
|
12271
12286
|
/* ── Table: checkbox ── */
|
|
12272
12287
|
--ml-table-checkbox-accent: var(--ml-color-primary);
|
|
@@ -12338,7 +12353,7 @@ const Wo = () => _`
|
|
|
12338
12353
|
}
|
|
12339
12354
|
|
|
12340
12355
|
.ml-table__th {
|
|
12341
|
-
padding: var(--ml-
|
|
12356
|
+
padding: var(--ml-table-header-padding-y) var(--ml-table-header-padding-x);
|
|
12342
12357
|
font-size: var(--ml-text-xs);
|
|
12343
12358
|
font-weight: var(--ml-font-medium);
|
|
12344
12359
|
color: var(--ml-table-header-color);
|
|
@@ -12350,7 +12365,7 @@ const Wo = () => _`
|
|
|
12350
12365
|
}
|
|
12351
12366
|
|
|
12352
12367
|
.ml-table--sm .ml-table__th {
|
|
12353
|
-
padding: var(--ml-
|
|
12368
|
+
padding: var(--ml-table-header-padding-y-sm) var(--ml-table-header-padding-x-sm);
|
|
12354
12369
|
}
|
|
12355
12370
|
|
|
12356
12371
|
.ml-table__th--center { text-align: center; }
|
|
@@ -12428,14 +12443,14 @@ const Wo = () => _`
|
|
|
12428
12443
|
|
|
12429
12444
|
/* ── Body cells ── */
|
|
12430
12445
|
.ml-table__td {
|
|
12431
|
-
padding: var(--ml-
|
|
12446
|
+
padding: var(--ml-table-cell-padding-y) var(--ml-table-cell-padding-x);
|
|
12432
12447
|
font-size: var(--ml-text-sm);
|
|
12433
12448
|
color: var(--ml-table-cell-color);
|
|
12434
12449
|
vertical-align: middle;
|
|
12435
12450
|
}
|
|
12436
12451
|
|
|
12437
12452
|
.ml-table--sm .ml-table__td {
|
|
12438
|
-
padding: var(--ml-
|
|
12453
|
+
padding: var(--ml-table-cell-padding-y-sm) var(--ml-table-cell-padding-x-sm);
|
|
12439
12454
|
font-size: var(--ml-text-xs);
|
|
12440
12455
|
}
|
|
12441
12456
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/data-display/table/table.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"table.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/data-display/table/table.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,iDAiTvB,CAAC"}
|
|
@@ -47,6 +47,16 @@ export const tableStyles = () => css `
|
|
|
47
47
|
|
|
48
48
|
/* ── Table: cells ── */
|
|
49
49
|
--ml-table-cell-color: var(--ml-color-text);
|
|
50
|
+
--ml-table-cell-padding-y: var(--ml-space-4);
|
|
51
|
+
--ml-table-cell-padding-x: var(--ml-space-6);
|
|
52
|
+
--ml-table-cell-padding-y-sm: var(--ml-space-2-5);
|
|
53
|
+
--ml-table-cell-padding-x-sm: var(--ml-space-4);
|
|
54
|
+
|
|
55
|
+
/* ── Table: header cells ── */
|
|
56
|
+
--ml-table-header-padding-y: var(--ml-space-3);
|
|
57
|
+
--ml-table-header-padding-x: var(--ml-space-6);
|
|
58
|
+
--ml-table-header-padding-y-sm: var(--ml-space-2);
|
|
59
|
+
--ml-table-header-padding-x-sm: var(--ml-space-4);
|
|
50
60
|
|
|
51
61
|
/* ── Table: checkbox ── */
|
|
52
62
|
--ml-table-checkbox-accent: var(--ml-color-primary);
|
|
@@ -118,7 +128,7 @@ export const tableStyles = () => css `
|
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
.ml-table__th {
|
|
121
|
-
padding: var(--ml-
|
|
131
|
+
padding: var(--ml-table-header-padding-y) var(--ml-table-header-padding-x);
|
|
122
132
|
font-size: var(--ml-text-xs);
|
|
123
133
|
font-weight: var(--ml-font-medium);
|
|
124
134
|
color: var(--ml-table-header-color);
|
|
@@ -130,7 +140,7 @@ export const tableStyles = () => css `
|
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
.ml-table--sm .ml-table__th {
|
|
133
|
-
padding: var(--ml-
|
|
143
|
+
padding: var(--ml-table-header-padding-y-sm) var(--ml-table-header-padding-x-sm);
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
.ml-table__th--center { text-align: center; }
|
|
@@ -208,14 +218,14 @@ export const tableStyles = () => css `
|
|
|
208
218
|
|
|
209
219
|
/* ── Body cells ── */
|
|
210
220
|
.ml-table__td {
|
|
211
|
-
padding: var(--ml-
|
|
221
|
+
padding: var(--ml-table-cell-padding-y) var(--ml-table-cell-padding-x);
|
|
212
222
|
font-size: var(--ml-text-sm);
|
|
213
223
|
color: var(--ml-table-cell-color);
|
|
214
224
|
vertical-align: middle;
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
.ml-table--sm .ml-table__td {
|
|
218
|
-
padding: var(--ml-
|
|
228
|
+
padding: var(--ml-table-cell-padding-y-sm) var(--ml-table-cell-padding-x-sm);
|
|
219
229
|
font-size: var(--ml-text-xs);
|
|
220
230
|
}
|
|
221
231
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.template.d.ts","sourceRoot":"","sources":["../../../../src/components/data-display/table/table.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,wBAAgB,aAAa,CAAC,CAAC,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"table.template.d.ts","sourceRoot":"","sources":["../../../../src/components/data-display/table/table.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,wBAAgB,aAAa,CAAC,CAAC,EAAE,cAAc,6CA+H9C"}
|
|
@@ -44,6 +44,7 @@ export function tableTemplate(c) {
|
|
|
44
44
|
`)}
|
|
45
45
|
${repeat(c.columns, (col) => col.key, (col) => html `
|
|
46
46
|
<th
|
|
47
|
+
part="header-cell"
|
|
47
48
|
class=${classMap({
|
|
48
49
|
'ml-table__th': true,
|
|
49
50
|
'ml-table__th--sortable': !!col.sortable,
|
|
@@ -80,6 +81,7 @@ export function tableTemplate(c) {
|
|
|
80
81
|
const absoluteIndex = c.startIndex + i;
|
|
81
82
|
return html `
|
|
82
83
|
<tr
|
|
84
|
+
part="row"
|
|
83
85
|
class=${classMap({
|
|
84
86
|
'ml-table__row': true,
|
|
85
87
|
'ml-table__row--selected': c.isRowSelected(absoluteIndex)
|
|
@@ -99,10 +101,13 @@ export function tableTemplate(c) {
|
|
|
99
101
|
</td>
|
|
100
102
|
`)}
|
|
101
103
|
${repeat(c.columns, (col) => col.key, (col) => html `
|
|
102
|
-
<td
|
|
104
|
+
<td
|
|
105
|
+
part="cell"
|
|
106
|
+
class=${classMap({
|
|
103
107
|
'ml-table__td': true,
|
|
104
108
|
[`ml-table__td--${col.align ?? 'left'}`]: true
|
|
105
|
-
})}
|
|
109
|
+
})}
|
|
110
|
+
>
|
|
106
111
|
${renderCell(col, row, absoluteIndex)}
|
|
107
112
|
</td>
|
|
108
113
|
`)}
|
|
@@ -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
|
`;
|