@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.
@@ -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 class=${g({
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-space-3) var(--ml-space-6);
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-space-2) var(--ml-space-4);
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-space-4) var(--ml-space-6);
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-space-2-5) var(--ml-space-4);
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,iDAuSvB,CAAC"}
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-space-3) var(--ml-space-6);
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-space-2) var(--ml-space-4);
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-space-4) var(--ml-space-6);
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-space-2-5) var(--ml-space-4);
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,6CA0H9C"}
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 class=${classMap({
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
- * @example
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
- * @fires ml:change - Emitted when value changes. Detail: { value: string, date: string, time: string }
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
- /** Selected datetime in ISO format (YYYY-MM-DDTHH:mm) */
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
- /** Internal date portion */
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;AAczE,OAAO,yCAAyC,CAAC;AACjD,OAAO,yCAAyC,CAAC;AAiCjD;;;;;;;;;;;;;GAaG;AACH,qBAMa,uBAAwB,YAAW,WAAW,EAAE,QAAQ,EAAE,SAAS;IACxE,UAAU,EAAG,WAAW,CAAC;IAEhC,yDAAyD;IAClD,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,4BAA4B;IACrB,SAAS,SAAM;IAEtB,4BAA4B;IACrB,SAAS,SAAM;IAEtB,OAAO,CAAC,UAAU,CAAyB;IAE3C,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED,IAAW,YAAY,IAAI,MAAM,CAEhC;IAEM,QAAQ,IAAI,IAAI;IAKhB,SAAS,IAAI,IAAI;IAKxB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,oBAAoB;IA+E5B,OAAO,CAAC,UAAU;CAqBlB"}
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
- * @example
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
- * @fires ml:change - Emitted when value changes. Detail: { value: string, date: string, time: string }
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
- /** Selected datetime in ISO format (YYYY-MM-DDTHH:mm) */
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
- /** Internal date portion */
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.value, this.use12Hour);
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
- if (!this.value)
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
- const [datePart, timePart] = this.value.split('T');
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
- if (this.dateValue && this.timeValue) {
201
- this.value = `${this.dateValue}T${this.timeValue}`;
202
- }
203
- else if (this.dateValue) {
204
- this.value = this.dateValue;
205
- }
206
- else if (this.timeValue) {
207
- this.value = this.timeValue;
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,iDAgIhC,CAAC"}
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,6CA+ChE"}
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
  `;