@vmz/ui 0.3.4 → 0.3.6

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.
Files changed (43) hide show
  1. package/README.md +51 -51
  2. package/package.json +2 -2
  3. package/presets/web-surface.v0.json +19 -7
  4. package/src/components/Accordion.vmz +94 -94
  5. package/src/components/AppShell.vmz +77 -33
  6. package/src/components/Autocomplete.vmz +154 -154
  7. package/src/components/Badge.vmz +70 -70
  8. package/src/components/Breadcrumb.vmz +72 -72
  9. package/src/components/BulkActions.vmz +51 -51
  10. package/src/components/Button.vmz +211 -192
  11. package/src/components/Callout.vmz +77 -77
  12. package/src/components/Card.vmz +120 -106
  13. package/src/components/Checkbox.vmz +55 -55
  14. package/src/components/CodeBlock.vmz +77 -18
  15. package/src/components/ConsoleShell.vmz +251 -251
  16. package/src/components/DataTable.vmz +194 -194
  17. package/src/components/DatePicker.vmz +674 -674
  18. package/src/components/Dialog.vmz +10 -7
  19. package/src/components/Drawer.vmz +427 -427
  20. package/src/components/Dropdown.vmz +135 -16
  21. package/src/components/Empty.vmz +54 -54
  22. package/src/components/Field.vmz +115 -115
  23. package/src/components/FormItem.vmz +79 -79
  24. package/src/components/Link.vmz +34 -4
  25. package/src/components/List.vmz +102 -102
  26. package/src/components/Menu.vmz +243 -243
  27. package/src/components/Pagination.vmz +69 -69
  28. package/src/components/Popover.vmz +223 -223
  29. package/src/components/Prose.vmz +81 -81
  30. package/src/components/RadioGroup.vmz +124 -124
  31. package/src/components/Segmented.vmz +51 -51
  32. package/src/components/Select.vmz +533 -533
  33. package/src/components/Steps.vmz +103 -103
  34. package/src/components/Switch.vmz +98 -98
  35. package/src/components/Table.vmz +165 -133
  36. package/src/components/Tabs.vmz +110 -110
  37. package/src/components/Tag.vmz +43 -40
  38. package/src/components/TextArea.vmz +118 -118
  39. package/src/components/Timeline.vmz +76 -76
  40. package/src/components/Toc.vmz +74 -74
  41. package/src/components/Tooltip.vmz +58 -58
  42. package/src/components/Tree.vmz +191 -191
  43. package/src/components/Upload.vmz +1289 -1289
@@ -1,674 +1,674 @@
1
- <template>
2
- <div data-vmz-ui="date-picker" :data-mode="mode" :data-invalid="invalidAttr" :class='"vmz-ui-date-picker" + (open ? " is-opened" : "")'>
3
- <label v-if="label" :for="controlId" class="vmz-ui-date-picker__label">
4
- {{ label }}
5
- </label>
6
- <button type="button" :id="controlId" :disabled="disabled" aria-haspopup="dialog" :aria-expanded="open ? 'true' : 'false'" :aria-controls="panelId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" @click="toggle" :onKeyDown="onTriggerKeyDown" class="vmz-ui-date-picker__control">
7
- <span data-vmz-date="value" class="vmz-ui-date-picker__value">
8
- {{ displayLabel }}
9
- </span>
10
- <span aria-hidden="true" class="vmz-ui-date-picker__chev" />
11
- </button>
12
- <div v-if="open" :id="panelId" role="dialog" aria-modal="false" :aria-label="panelLabel" tabindex="-1" data-vmz-overlay="date-picker" data-vmz-overlay-owner="date-picker" data-vmz-focus="enter" data-vmz-date="panel" :onKeyDown="onPanelKeyDown" class="vmz-ui-date-picker__panel">
13
- <div class="vmz-ui-date-picker__monthbar">
14
- <button type="button" data-vmz-date="prev" aria-label="Previous month" @click="prevMonth" class="vmz-ui-date-picker__nav">
15
- ‹
16
- </button>
17
- <p data-vmz-date="month" class="vmz-ui-date-picker__month">
18
- {{ monthLabel }}
19
- </p>
20
- <button type="button" data-vmz-date="next" aria-label="Next month" @click="nextMonth" class="vmz-ui-date-picker__nav">
21
- ›
22
- </button>
23
- </div>
24
- <div aria-hidden="true" class="vmz-ui-date-picker__weekdays">
25
- <span>
26
- Mo
27
- </span>
28
- <span>
29
- Tu
30
- </span>
31
- <span>
32
- We
33
- </span>
34
- <span>
35
- Th
36
- </span>
37
- <span>
38
- Fr
39
- </span>
40
- <span>
41
- Sa
42
- </span>
43
- <span>
44
- Su
45
- </span>
46
- </div>
47
- <div role="grid" :aria-labelledby="panelId" class="vmz-ui-date-picker__grid">
48
- <button v-for="day in days" :key="day.key" type="button" role="gridcell" :data-vmz-date-iso="day.iso" :aria-selected="day.selected ? 'true' : 'false'" :aria-disabled="day.disabled ? 'true' : 'false'" :disabled="day.disabled" @click="onDayClick" :class='"vmz-ui-date-picker__day" + (day.inMonth ? "" : " is-outside") + (day.selected ? " is-on" : "") + (day.inRange ? " is-in-range" : "") + (day.rangeEdge ? " is-range-edge" : "") + (day.disabled ? " is-disabled" : "")'>
49
- {{ day.label }}
50
- </button>
51
- </div>
52
- </div>
53
- <p v-if="description" :id="descriptionId" class="vmz-ui-date-picker__description">
54
- {{ description }}
55
- </p>
56
- <p v-if="error" :id="errorId" role="alert" class="vmz-ui-date-picker__error">
57
- {{ error }}
58
- </p>
59
- </div>
60
- </template>
61
- <style>
62
- .vmz-ui-date-picker {
63
- position: relative;
64
- display: flex;
65
- flex-direction: column;
66
- gap: 0.35rem;
67
- max-width: 28rem;
68
- }
69
-
70
- .vmz-ui-date-picker__label {
71
- font: inherit;
72
- font-weight: 600;
73
- }
74
-
75
- .vmz-ui-date-picker__control {
76
- display: inline-flex;
77
- align-items: center;
78
- justify-content: space-between;
79
- gap: 0.55rem;
80
- width: 100%;
81
- box-sizing: border-box;
82
- font: inherit;
83
- text-align: left;
84
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
85
- border: 1px solid var(--vmz-line-default);
86
- border-radius: var(--vmz-radius-md, 6px);
87
- background: var(--vmz-surface-paper, transparent);
88
- color: inherit;
89
- outline: none;
90
- cursor: pointer;
91
- }
92
-
93
- .vmz-ui-date-picker[data-invalid="true"] .vmz-ui-date-picker__control {
94
- border-color: var(--vmz-status-danger-accent);
95
- }
96
-
97
- .vmz-ui-date-picker__control:focus-visible,
98
- .vmz-ui-date-picker.is-opened .vmz-ui-date-picker__control {
99
- box-shadow: 0 0 0 2px var(--vmz-focus-ring);
100
- }
101
-
102
- .vmz-ui-date-picker__control:disabled {
103
- opacity: 0.55;
104
- cursor: not-allowed;
105
- }
106
-
107
- .vmz-ui-date-picker__value {
108
- flex: 1 1 auto;
109
- min-width: 0;
110
- overflow: hidden;
111
- text-overflow: ellipsis;
112
- white-space: nowrap;
113
- }
114
-
115
- .vmz-ui-date-picker__chev {
116
- flex: 0 0 auto;
117
- width: 0.45rem;
118
- height: 0.45rem;
119
- border-right: 1.5px solid currentColor;
120
- border-bottom: 1.5px solid currentColor;
121
- transform: rotate(45deg) translate(-1px, -2px);
122
- opacity: 0.7;
123
- }
124
-
125
- .vmz-ui-date-picker.is-opened .vmz-ui-date-picker__chev {
126
- transform: rotate(225deg) translate(-1px, -1px);
127
- }
128
-
129
- .vmz-ui-date-picker__panel {
130
- position: absolute;
131
- z-index: 40;
132
- top: calc(100% + 0.25rem);
133
- left: 0;
134
- right: 0;
135
- box-sizing: border-box;
136
- padding: 0.65rem 0.75rem 0.75rem;
137
- border: 1px solid var(--vmz-line-default);
138
- border-radius: var(--vmz-radius-md, 6px);
139
- background: var(--vmz-surface-paper, #fff);
140
- color: inherit;
141
- box-shadow: 0 10px 24px color-mix(in srgb, #000 16%, transparent);
142
- outline: none;
143
- }
144
-
145
- .vmz-ui-date-picker__monthbar {
146
- display: flex;
147
- align-items: center;
148
- justify-content: space-between;
149
- gap: 0.5rem;
150
- margin-bottom: 0.55rem;
151
- }
152
-
153
- .vmz-ui-date-picker__month {
154
- margin: 0;
155
- font: inherit;
156
- font-weight: 600;
157
- text-align: center;
158
- flex: 1 1 auto;
159
- }
160
-
161
- .vmz-ui-date-picker__nav {
162
- flex: 0 0 auto;
163
- width: 2rem;
164
- height: 2rem;
165
- font: inherit;
166
- font-size: 1.15rem;
167
- line-height: 1;
168
- border: 1px solid transparent;
169
- border-radius: var(--vmz-radius-md, 6px);
170
- background: transparent;
171
- color: inherit;
172
- cursor: pointer;
173
- outline: none;
174
- }
175
-
176
- .vmz-ui-date-picker__nav:hover,
177
- .vmz-ui-date-picker__nav:focus-visible {
178
- border-color: var(--vmz-action-primary-background);
179
- box-shadow: 0 0 0 2px var(--vmz-focus-ring);
180
- }
181
-
182
- .vmz-ui-date-picker__weekdays {
183
- display: grid;
184
- grid-template-columns: repeat(7, 1fr);
185
- gap: 0.15rem;
186
- margin-bottom: 0.25rem;
187
- font-size: 0.75rem;
188
- opacity: 0.7;
189
- text-align: center;
190
- }
191
-
192
- .vmz-ui-date-picker__grid {
193
- display: grid;
194
- grid-template-columns: repeat(7, 1fr);
195
- gap: 0.15rem;
196
- }
197
-
198
- .vmz-ui-date-picker__day {
199
- aspect-ratio: 1;
200
- min-height: 2rem;
201
- font: inherit;
202
- font-size: 0.9rem;
203
- border: 0;
204
- border-radius: var(--vmz-radius-md, 6px);
205
- background: transparent;
206
- color: inherit;
207
- cursor: pointer;
208
- outline: none;
209
- }
210
-
211
- .vmz-ui-date-picker__day:hover,
212
- .vmz-ui-date-picker__day:focus-visible {
213
- background: color-mix(in srgb, var(--vmz-action-primary-background) 18%, transparent);
214
- box-shadow: 0 0 0 2px var(--vmz-focus-ring);
215
- }
216
-
217
- .vmz-ui-date-picker__day.is-outside {
218
- opacity: 0.4;
219
- }
220
-
221
- .vmz-ui-date-picker__day.is-on {
222
- background: color-mix(in srgb, var(--vmz-action-primary-background) 28%, transparent);
223
- font-weight: 600;
224
- }
225
-
226
- .vmz-ui-date-picker__day.is-in-range {
227
- background: color-mix(in srgb, var(--vmz-action-primary-background) 14%, transparent);
228
- }
229
-
230
- .vmz-ui-date-picker__day.is-range-edge {
231
- background: color-mix(in srgb, var(--vmz-action-primary-background) 36%, transparent);
232
- font-weight: 600;
233
- }
234
-
235
- .vmz-ui-date-picker__day.is-disabled,
236
- .vmz-ui-date-picker__day:disabled {
237
- opacity: 0.35;
238
- cursor: not-allowed;
239
- box-shadow: none;
240
- }
241
-
242
- .vmz-ui-date-picker__description {
243
- margin: 0;
244
- opacity: 0.75;
245
- font-size: 0.9em;
246
- }
247
-
248
- .vmz-ui-date-picker__error {
249
- margin: 0;
250
- color: var(--vmz-status-danger-foreground);
251
- font-size: 0.9em;
252
- }
253
- </style>
254
- <script client>
255
- /**
256
- * VMZ UI — DatePicker.
257
- * Owned calendar overlay (trigger + dialog grid). Never native <input type="date"> popup.
258
- * Parent owns ISO value (YYYY-MM-DD) via onChange Event-like { target: { value } }.
259
- * mode=range: parent owns rangeStart + rangeEnd; onChange { target: { value, start, end } }.
260
- * Not @vmz/ui-scheduler — month grid only; no time / recurrence / resource layout.
261
- */
262
- export default class DatePicker {
263
- public label: string = "";
264
- public description: string = "";
265
- public error: string = "";
266
- public value: string = "";
267
- /** "date" | "range" */
268
- public mode: string = "date";
269
- public rangeStart: string = "";
270
- public rangeEnd: string = "";
271
- public min: string = "";
272
- public max: string = "";
273
- public disabled: boolean = false;
274
- public invalidAttr: string = "false";
275
- public controlId: string = "vmz-date";
276
- public descriptionId: string = "vmz-date-desc";
277
- public errorId: string = "vmz-date-err";
278
- public describedBy: string = "";
279
- public onInput:
280
- | ((e: {
281
- target: {
282
- value: string;
283
- start: string;
284
- end: string;
285
- };
286
- }) => void)
287
- | null = null;
288
- public onChange:
289
- | ((e: {
290
- target: {
291
- value: string;
292
- start: string;
293
- end: string;
294
- };
295
- }) => void)
296
- | null = null;
297
- open = false;
298
- displayLabel = "—";
299
- panelId = "vmz-date-panel";
300
- panelLabel = "Choose date";
301
- monthLabel = "";
302
- viewYear = 0;
303
- viewMonth = 0;
304
- /** Internal first click while completing a range. */
305
- draftStart = "";
306
- days: Array<{
307
- key: string;
308
- iso: string;
309
- label: string;
310
- inMonth: boolean;
311
- selected: boolean;
312
- inRange: boolean;
313
- rangeEdge: boolean;
314
- disabled: boolean;
315
- }> = [];
316
- _docKeyHandler = null;
317
- _docPointerHandler = null;
318
- _observer = null;
319
- onMount() {
320
- this.panelId = `${this.controlId}-panel`;
321
- this.syncFromValue(true);
322
- if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
323
- const root = this.__vmzDomRoot;
324
- if (!root || typeof root.querySelector !== "function") return;
325
- this._observer = new MutationObserver(() => this._syncOverlay());
326
- this._observer.observe(root, {
327
- childList: true,
328
- subtree: true,
329
- });
330
- }
331
- onDestroy() {
332
- if (this._observer) {
333
- this._observer.disconnect();
334
- this._observer = null;
335
- }
336
- this._teardownDoc();
337
- }
338
- syncFromValue(resetView: boolean) {
339
- const isRange = String(this.mode) === "range";
340
- const parsed = this._parseIso(
341
- isRange ? this.rangeStart || this.rangeEnd || this.value : this.value,
342
- );
343
- this._syncDisplay();
344
- if (resetView) {
345
- const base = parsed || this._todayParts();
346
- this.viewYear = base.y;
347
- this.viewMonth = base.m;
348
- }
349
- this._rebuildDays();
350
- }
351
- _syncDisplay() {
352
- if (String(this.mode) === "range") {
353
- const a = this._parseIso(this.rangeStart) ? this.rangeStart : "";
354
- const b = this._parseIso(this.rangeEnd) ? this.rangeEnd : "";
355
- if (a && b) this.displayLabel = `${a} → ${b}`;
356
- else if (a) this.displayLabel = `${a} → …`;
357
- else this.displayLabel = "—";
358
- return;
359
- }
360
- this.displayLabel = this._parseIso(this.value) ? this.value : "—";
361
- }
362
- openPanel() {
363
- this.draftStart = "";
364
- this.syncFromValue(true);
365
- this.open = true;
366
- }
367
- toggle() {
368
- if (this.disabled) return;
369
- if (this.open) this.close();
370
- else this.openPanel();
371
- }
372
- close() {
373
- this.open = false;
374
- this.draftStart = "";
375
- }
376
- prevMonth() {
377
- if (this.viewMonth <= 1) {
378
- this.viewYear -= 1;
379
- this.viewMonth = 12;
380
- } else {
381
- this.viewMonth -= 1;
382
- }
383
- this._rebuildDays();
384
- }
385
- nextMonth() {
386
- if (this.viewMonth >= 12) {
387
- this.viewYear += 1;
388
- this.viewMonth = 1;
389
- } else {
390
- this.viewMonth += 1;
391
- }
392
- this._rebuildDays();
393
- }
394
- choose(iso: string) {
395
- if (!iso || this._isOutOfRange(iso)) return;
396
- if (String(this.mode) === "range") {
397
- if (!this.draftStart) {
398
- this.draftStart = iso;
399
- this._rebuildDays();
400
- return;
401
- }
402
- let start = this.draftStart;
403
- let end = iso;
404
- if (end < start) {
405
- const t = start;
406
- start = end;
407
- end = t;
408
- }
409
- this.draftStart = "";
410
- this.close();
411
- const ev = {
412
- target: {
413
- value: `${start}/${end}`,
414
- start,
415
- end,
416
- },
417
- };
418
- this.emit("change", ev);
419
- this.emit("input", ev);
420
- if (typeof this.onChange === "function") this.onChange(ev);
421
- if (typeof this.onInput === "function") this.onInput(ev);
422
- return;
423
- }
424
- this.close();
425
- if (iso === this.value) return;
426
- const ev = {
427
- target: {
428
- value: iso,
429
- start: iso,
430
- end: iso,
431
- },
432
- };
433
- this.emit("change", ev);
434
- this.emit("input", ev);
435
- if (typeof this.onChange === "function") this.onChange(ev);
436
- if (typeof this.onInput === "function") this.onInput(ev);
437
- }
438
- onDayClick(ev: Event) {
439
- const t = ev?.target as HTMLElement | null;
440
- const el =
441
- t && typeof t.closest === "function"
442
- ? (t.closest("[data-vmz-date-iso]") as HTMLElement | null)
443
- : ((ev?.currentTarget as HTMLElement | null) ?? null);
444
- const iso =
445
- el && typeof el.getAttribute === "function"
446
- ? el.getAttribute("data-vmz-date-iso")
447
- : null;
448
- if (iso) this.choose(iso);
449
- }
450
- onTriggerKeyDown(ev: KeyboardEvent) {
451
- if (!ev || this.disabled) return;
452
- if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
453
- ev.preventDefault();
454
- this.openPanel();
455
- } else if (ev.key === "Escape" && this.open) {
456
- ev.preventDefault();
457
- this.close();
458
- }
459
- }
460
- onPanelKeyDown(ev: KeyboardEvent) {
461
- if (!ev) return;
462
- if (ev.key === "Escape") {
463
- ev.preventDefault();
464
- this.close();
465
- return;
466
- }
467
- const root = this.__vmzDomRoot;
468
- const panel =
469
- root && typeof root.querySelector === "function"
470
- ? root.querySelector('[data-vmz-overlay="date-picker"]')
471
- : null;
472
- if (!panel) return;
473
- const nodes = [...panel.querySelectorAll("[data-vmz-date-iso]")].filter(
474
- (n) => n instanceof HTMLElement && !(n as HTMLButtonElement).disabled,
475
- );
476
- if (!nodes.length) return;
477
- const idx = nodes.indexOf(document.activeElement as Element);
478
- if (ev.key === "ArrowRight") {
479
- ev.preventDefault();
480
- (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
481
- } else if (ev.key === "ArrowLeft") {
482
- ev.preventDefault();
483
- (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
484
- } else if (ev.key === "ArrowDown") {
485
- ev.preventDefault();
486
- (nodes[Math.min(idx + 7, nodes.length - 1)] as HTMLElement).focus();
487
- } else if (ev.key === "ArrowUp") {
488
- ev.preventDefault();
489
- (nodes[Math.max(idx - 7, 0)] as HTMLElement).focus();
490
- } else if (ev.key === "Home") {
491
- ev.preventDefault();
492
- (nodes[0] as HTMLElement).focus();
493
- } else if (ev.key === "End") {
494
- ev.preventDefault();
495
- (nodes[nodes.length - 1] as HTMLElement).focus();
496
- } else if (ev.key === "Enter" || ev.key === " ") {
497
- const active = document.activeElement as HTMLElement | null;
498
- const iso = active?.getAttribute?.("data-vmz-date-iso");
499
- if (iso) {
500
- ev.preventDefault();
501
- this.choose(iso);
502
- }
503
- }
504
- }
505
- _rebuildDays() {
506
- const y = this.viewYear || this._todayParts().y;
507
- const m = this.viewMonth || this._todayParts().m;
508
- this.viewYear = y;
509
- this.viewMonth = m;
510
- this.monthLabel = `${this._monthName(m)} ${y}`;
511
- this.panelLabel =
512
- String(this.mode) === "range"
513
- ? `Choose date range — ${this.monthLabel}`
514
- : `Choose date — ${this.monthLabel}`;
515
- const firstDow = (new Date(y, m - 1, 1).getDay() + 6) % 7;
516
- const dim = new Date(y, m, 0).getDate();
517
- const prevDim = new Date(y, m - 1, 0).getDate();
518
- const cells: typeof this.days = [];
519
- const isRange = String(this.mode) === "range";
520
- let start = "";
521
- let end = "";
522
- if (isRange) {
523
- if (this.draftStart) {
524
- start = this.draftStart;
525
- end = this.draftStart;
526
- } else {
527
- start = this._parseIso(this.rangeStart) ? this.rangeStart : "";
528
- end = this._parseIso(this.rangeEnd) ? this.rangeEnd : "";
529
- }
530
- }
531
- for (let i = 0; i < 42; i++) {
532
- let cy = y;
533
- let cm = m;
534
- let cd = 0;
535
- let inMonth = true;
536
- if (i < firstDow) {
537
- inMonth = false;
538
- cd = prevDim - firstDow + i + 1;
539
- cm = m === 1 ? 12 : m - 1;
540
- cy = m === 1 ? y - 1 : y;
541
- } else if (i >= firstDow + dim) {
542
- inMonth = false;
543
- cd = i - firstDow - dim + 1;
544
- cm = m === 12 ? 1 : m + 1;
545
- cy = m === 12 ? y + 1 : y;
546
- } else {
547
- cd = i - firstDow + 1;
548
- }
549
- const iso = this._toIso(cy, cm, cd);
550
- let selected = iso === this.value;
551
- let inRange = false;
552
- let rangeEdge = false;
553
- if (isRange) {
554
- selected = false;
555
- if (start && end) {
556
- rangeEdge = iso === start || iso === end;
557
- inRange = iso >= start && iso <= end;
558
- selected = rangeEdge;
559
- } else if (start) {
560
- rangeEdge = iso === start;
561
- selected = rangeEdge;
562
- }
563
- }
564
- cells.push({
565
- key: iso,
566
- iso,
567
- label: String(cd),
568
- inMonth,
569
- selected,
570
- inRange: inRange && !rangeEdge,
571
- rangeEdge,
572
- disabled: this._isOutOfRange(iso),
573
- });
574
- }
575
- this.days = cells;
576
- this._syncDisplay();
577
- }
578
- _parseIso(iso: string) {
579
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ""));
580
- if (!m) return null;
581
- const y = Number(m[1]);
582
- const mo = Number(m[2]);
583
- const d = Number(m[3]);
584
- if (mo < 1 || mo > 12 || d < 1 || d > 31) return null;
585
- return {
586
- y,
587
- m: mo,
588
- d,
589
- };
590
- }
591
- _toIso(y: number, m: number, d: number) {
592
- return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
593
- }
594
- _todayParts() {
595
- const now = new Date();
596
- return {
597
- y: now.getFullYear(),
598
- m: now.getMonth() + 1,
599
- d: now.getDate(),
600
- };
601
- }
602
- _monthName(m: number) {
603
- const names = [
604
- "January",
605
- "February",
606
- "March",
607
- "April",
608
- "May",
609
- "June",
610
- "July",
611
- "August",
612
- "September",
613
- "October",
614
- "November",
615
- "December",
616
- ];
617
- return names[m - 1] || String(m);
618
- }
619
- _isOutOfRange(iso: string) {
620
- if (this.min && iso < this.min) return true;
621
- if (this.max && iso > this.max) return true;
622
- return false;
623
- }
624
- _syncOverlay() {
625
- const root = this.__vmzDomRoot;
626
- const panel =
627
- root && typeof root.querySelector === "function"
628
- ? root.querySelector('[data-vmz-overlay="date-picker"][data-vmz-focus="enter"]')
629
- : null;
630
- if (panel && this.open) this._bindDoc(panel as HTMLElement);
631
- else if (!panel) this._teardownDoc();
632
- this._syncDisplay();
633
- }
634
- _bindDoc(panel: HTMLElement) {
635
- if (typeof document === "undefined") return;
636
- if (!this._docKeyHandler) {
637
- this._docKeyHandler = (ev: KeyboardEvent) => {
638
- if (ev.key === "Escape") {
639
- ev.preventDefault();
640
- this.close();
641
- }
642
- };
643
- document.addEventListener("keydown", this._docKeyHandler);
644
- }
645
- if (!this._docPointerHandler) {
646
- const root = this.__vmzDomRoot;
647
- this._docPointerHandler = (ev: Event) => {
648
- const t = ev.target;
649
- if (!(t instanceof Node)) return;
650
- if (root && typeof root.contains === "function" && root.contains(t)) return;
651
- this.close();
652
- };
653
- document.addEventListener("pointerdown", this._docPointerHandler, true);
654
- }
655
- const selected = panel.querySelector(
656
- '[data-vmz-date-iso][aria-selected="true"]:not([disabled])',
657
- );
658
- const first = panel.querySelector("[data-vmz-date-iso]:not([disabled])");
659
- const focusTarget = (selected || first) as HTMLElement | null;
660
- if (focusTarget) focusTarget.focus();
661
- else panel.focus();
662
- }
663
- _teardownDoc() {
664
- if (typeof document !== "undefined") {
665
- if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
666
- if (this._docPointerHandler) {
667
- document.removeEventListener("pointerdown", this._docPointerHandler, true);
668
- }
669
- }
670
- this._docKeyHandler = null;
671
- this._docPointerHandler = null;
672
- }
673
- }
674
- </script>
1
+ <template>
2
+ <div data-vmz-ui="date-picker" :data-mode="mode" :data-invalid="invalidAttr" :class='"vmz-ui-date-picker" + (open ? " is-opened" : "")'>
3
+ <label v-if="label" :for="controlId" class="vmz-ui-date-picker__label">
4
+ {{ label }}
5
+ </label>
6
+ <button type="button" :id="controlId" :disabled="disabled" aria-haspopup="dialog" :aria-expanded="open ? 'true' : 'false'" :aria-controls="panelId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" @click="toggle" :onKeyDown="onTriggerKeyDown" class="vmz-ui-date-picker__control">
7
+ <span data-vmz-date="value" class="vmz-ui-date-picker__value">
8
+ {{ displayLabel }}
9
+ </span>
10
+ <span aria-hidden="true" class="vmz-ui-date-picker__chev" />
11
+ </button>
12
+ <div v-if="open" :id="panelId" role="dialog" aria-modal="false" :aria-label="panelLabel" tabindex="-1" data-vmz-overlay="date-picker" data-vmz-overlay-owner="date-picker" data-vmz-focus="enter" data-vmz-date="panel" :onKeyDown="onPanelKeyDown" class="vmz-ui-date-picker__panel">
13
+ <div class="vmz-ui-date-picker__monthbar">
14
+ <button type="button" data-vmz-date="prev" aria-label="Previous month" @click="prevMonth" class="vmz-ui-date-picker__nav">
15
+ ‹
16
+ </button>
17
+ <p data-vmz-date="month" class="vmz-ui-date-picker__month">
18
+ {{ monthLabel }}
19
+ </p>
20
+ <button type="button" data-vmz-date="next" aria-label="Next month" @click="nextMonth" class="vmz-ui-date-picker__nav">
21
+ ›
22
+ </button>
23
+ </div>
24
+ <div aria-hidden="true" class="vmz-ui-date-picker__weekdays">
25
+ <span>
26
+ Mo
27
+ </span>
28
+ <span>
29
+ Tu
30
+ </span>
31
+ <span>
32
+ We
33
+ </span>
34
+ <span>
35
+ Th
36
+ </span>
37
+ <span>
38
+ Fr
39
+ </span>
40
+ <span>
41
+ Sa
42
+ </span>
43
+ <span>
44
+ Su
45
+ </span>
46
+ </div>
47
+ <div role="grid" :aria-labelledby="panelId" class="vmz-ui-date-picker__grid">
48
+ <button v-for="day in days" :key="day.key" type="button" role="gridcell" :data-vmz-date-iso="day.iso" :aria-selected="day.selected ? 'true' : 'false'" :aria-disabled="day.disabled ? 'true' : 'false'" :disabled="day.disabled" @click="onDayClick" :class='"vmz-ui-date-picker__day" + (day.inMonth ? "" : " is-outside") + (day.selected ? " is-on" : "") + (day.inRange ? " is-in-range" : "") + (day.rangeEdge ? " is-range-edge" : "") + (day.disabled ? " is-disabled" : "")'>
49
+ {{ day.label }}
50
+ </button>
51
+ </div>
52
+ </div>
53
+ <p v-if="description" :id="descriptionId" class="vmz-ui-date-picker__description">
54
+ {{ description }}
55
+ </p>
56
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-date-picker__error">
57
+ {{ error }}
58
+ </p>
59
+ </div>
60
+ </template>
61
+ <style>
62
+ .vmz-ui-date-picker {
63
+ position: relative;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: 0.375rem;
67
+ max-width: 28rem;
68
+ }
69
+
70
+ .vmz-ui-date-picker__label {
71
+ font: inherit;
72
+ font-weight: 600;
73
+ }
74
+
75
+ .vmz-ui-date-picker__control {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: space-between;
79
+ gap: 0.5rem;
80
+ width: 100%;
81
+ box-sizing: border-box;
82
+ font: inherit;
83
+ text-align: left;
84
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
85
+ border: 1px solid var(--vmz-line-default);
86
+ border-radius: var(--vmz-radius-md, 6px);
87
+ background: var(--vmz-surface-paper, transparent);
88
+ color: inherit;
89
+ outline: none;
90
+ cursor: pointer;
91
+ }
92
+
93
+ .vmz-ui-date-picker[data-invalid="true"] .vmz-ui-date-picker__control {
94
+ border-color: var(--vmz-status-danger-accent);
95
+ }
96
+
97
+ .vmz-ui-date-picker__control:focus-visible,
98
+ .vmz-ui-date-picker.is-opened .vmz-ui-date-picker__control {
99
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
100
+ }
101
+
102
+ .vmz-ui-date-picker__control:disabled {
103
+ opacity: 0.55;
104
+ cursor: not-allowed;
105
+ }
106
+
107
+ .vmz-ui-date-picker__value {
108
+ flex: 1 1 auto;
109
+ min-width: 0;
110
+ overflow: hidden;
111
+ text-overflow: ellipsis;
112
+ white-space: nowrap;
113
+ }
114
+
115
+ .vmz-ui-date-picker__chev {
116
+ flex: 0 0 auto;
117
+ width: 0.45rem;
118
+ height: 0.45rem;
119
+ border-right: 1.5px solid currentColor;
120
+ border-bottom: 1.5px solid currentColor;
121
+ transform: rotate(45deg) translate(-1px, -2px);
122
+ opacity: 0.7;
123
+ }
124
+
125
+ .vmz-ui-date-picker.is-opened .vmz-ui-date-picker__chev {
126
+ transform: rotate(225deg) translate(-1px, -1px);
127
+ }
128
+
129
+ .vmz-ui-date-picker__panel {
130
+ position: absolute;
131
+ z-index: 40;
132
+ top: calc(100% + 0.25rem);
133
+ left: 0;
134
+ right: 0;
135
+ box-sizing: border-box;
136
+ padding: 0.625rem 0.75rem 0.75rem;
137
+ border: 1px solid var(--vmz-line-default);
138
+ border-radius: var(--vmz-radius-md, 6px);
139
+ background: var(--vmz-surface-paper, #fff);
140
+ color: inherit;
141
+ box-shadow: 0 10px 24px color-mix(in srgb, #000 16%, transparent);
142
+ outline: none;
143
+ }
144
+
145
+ .vmz-ui-date-picker__monthbar {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: space-between;
149
+ gap: 0.5rem;
150
+ margin-bottom: 0.5rem;
151
+ }
152
+
153
+ .vmz-ui-date-picker__month {
154
+ margin: 0;
155
+ font: inherit;
156
+ font-weight: 600;
157
+ text-align: center;
158
+ flex: 1 1 auto;
159
+ }
160
+
161
+ .vmz-ui-date-picker__nav {
162
+ flex: 0 0 auto;
163
+ width: 2rem;
164
+ height: 2rem;
165
+ font: inherit;
166
+ font-size: 1.15rem;
167
+ line-height: 1;
168
+ border: 1px solid transparent;
169
+ border-radius: var(--vmz-radius-md, 6px);
170
+ background: transparent;
171
+ color: inherit;
172
+ cursor: pointer;
173
+ outline: none;
174
+ }
175
+
176
+ .vmz-ui-date-picker__nav:hover,
177
+ .vmz-ui-date-picker__nav:focus-visible {
178
+ border-color: var(--vmz-action-primary-background);
179
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
180
+ }
181
+
182
+ .vmz-ui-date-picker__weekdays {
183
+ display: grid;
184
+ grid-template-columns: repeat(7, 1fr);
185
+ gap: 0.125rem;
186
+ margin-bottom: 0.25rem;
187
+ font-size: 0.75rem;
188
+ opacity: 0.7;
189
+ text-align: center;
190
+ }
191
+
192
+ .vmz-ui-date-picker__grid {
193
+ display: grid;
194
+ grid-template-columns: repeat(7, 1fr);
195
+ gap: 0.125rem;
196
+ }
197
+
198
+ .vmz-ui-date-picker__day {
199
+ aspect-ratio: 1;
200
+ min-height: 2rem;
201
+ font: inherit;
202
+ font-size: 0.9rem;
203
+ border: 0;
204
+ border-radius: var(--vmz-radius-md, 6px);
205
+ background: transparent;
206
+ color: inherit;
207
+ cursor: pointer;
208
+ outline: none;
209
+ }
210
+
211
+ .vmz-ui-date-picker__day:hover,
212
+ .vmz-ui-date-picker__day:focus-visible {
213
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 18%, transparent);
214
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
215
+ }
216
+
217
+ .vmz-ui-date-picker__day.is-outside {
218
+ opacity: 0.4;
219
+ }
220
+
221
+ .vmz-ui-date-picker__day.is-on {
222
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 28%, transparent);
223
+ font-weight: 600;
224
+ }
225
+
226
+ .vmz-ui-date-picker__day.is-in-range {
227
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 14%, transparent);
228
+ }
229
+
230
+ .vmz-ui-date-picker__day.is-range-edge {
231
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 36%, transparent);
232
+ font-weight: 600;
233
+ }
234
+
235
+ .vmz-ui-date-picker__day.is-disabled,
236
+ .vmz-ui-date-picker__day:disabled {
237
+ opacity: 0.35;
238
+ cursor: not-allowed;
239
+ box-shadow: none;
240
+ }
241
+
242
+ .vmz-ui-date-picker__description {
243
+ margin: 0;
244
+ opacity: 0.75;
245
+ font-size: 0.9em;
246
+ }
247
+
248
+ .vmz-ui-date-picker__error {
249
+ margin: 0;
250
+ color: var(--vmz-status-danger-foreground);
251
+ font-size: 0.9em;
252
+ }
253
+ </style>
254
+ <script client>
255
+ /**
256
+ * VMZ UI — DatePicker.
257
+ * Owned calendar overlay (trigger + dialog grid). Never native <input type="date"> popup.
258
+ * Parent owns ISO value (YYYY-MM-DD) via onChange Event-like { target: { value } }.
259
+ * mode=range: parent owns rangeStart + rangeEnd; onChange { target: { value, start, end } }.
260
+ * Not @vmz/ui-scheduler — month grid only; no time / recurrence / resource layout.
261
+ */
262
+ export default class DatePicker {
263
+ public label: string = "";
264
+ public description: string = "";
265
+ public error: string = "";
266
+ public value: string = "";
267
+ /** "date" | "range" */
268
+ public mode: string = "date";
269
+ public rangeStart: string = "";
270
+ public rangeEnd: string = "";
271
+ public min: string = "";
272
+ public max: string = "";
273
+ public disabled: boolean = false;
274
+ public invalidAttr: string = "false";
275
+ public controlId: string = "vmz-date";
276
+ public descriptionId: string = "vmz-date-desc";
277
+ public errorId: string = "vmz-date-err";
278
+ public describedBy: string = "";
279
+ public onInput:
280
+ | ((e: {
281
+ target: {
282
+ value: string;
283
+ start: string;
284
+ end: string;
285
+ };
286
+ }) => void)
287
+ | null = null;
288
+ public onChange:
289
+ | ((e: {
290
+ target: {
291
+ value: string;
292
+ start: string;
293
+ end: string;
294
+ };
295
+ }) => void)
296
+ | null = null;
297
+ open = false;
298
+ displayLabel = "—";
299
+ panelId = "vmz-date-panel";
300
+ panelLabel = "Choose date";
301
+ monthLabel = "";
302
+ viewYear = 0;
303
+ viewMonth = 0;
304
+ /** Internal first click while completing a range. */
305
+ draftStart = "";
306
+ days: Array<{
307
+ key: string;
308
+ iso: string;
309
+ label: string;
310
+ inMonth: boolean;
311
+ selected: boolean;
312
+ inRange: boolean;
313
+ rangeEdge: boolean;
314
+ disabled: boolean;
315
+ }> = [];
316
+ _docKeyHandler = null;
317
+ _docPointerHandler = null;
318
+ _observer = null;
319
+ onMount() {
320
+ this.panelId = `${this.controlId}-panel`;
321
+ this.syncFromValue(true);
322
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
323
+ const root = this.__vmzDomRoot;
324
+ if (!root || typeof root.querySelector !== "function") return;
325
+ this._observer = new MutationObserver(() => this._syncOverlay());
326
+ this._observer.observe(root, {
327
+ childList: true,
328
+ subtree: true,
329
+ });
330
+ }
331
+ onDestroy() {
332
+ if (this._observer) {
333
+ this._observer.disconnect();
334
+ this._observer = null;
335
+ }
336
+ this._teardownDoc();
337
+ }
338
+ syncFromValue(resetView: boolean) {
339
+ const isRange = String(this.mode) === "range";
340
+ const parsed = this._parseIso(
341
+ isRange ? this.rangeStart || this.rangeEnd || this.value : this.value,
342
+ );
343
+ this._syncDisplay();
344
+ if (resetView) {
345
+ const base = parsed || this._todayParts();
346
+ this.viewYear = base.y;
347
+ this.viewMonth = base.m;
348
+ }
349
+ this._rebuildDays();
350
+ }
351
+ _syncDisplay() {
352
+ if (String(this.mode) === "range") {
353
+ const a = this._parseIso(this.rangeStart) ? this.rangeStart : "";
354
+ const b = this._parseIso(this.rangeEnd) ? this.rangeEnd : "";
355
+ if (a && b) this.displayLabel = `${a} → ${b}`;
356
+ else if (a) this.displayLabel = `${a} → …`;
357
+ else this.displayLabel = "—";
358
+ return;
359
+ }
360
+ this.displayLabel = this._parseIso(this.value) ? this.value : "—";
361
+ }
362
+ openPanel() {
363
+ this.draftStart = "";
364
+ this.syncFromValue(true);
365
+ this.open = true;
366
+ }
367
+ toggle() {
368
+ if (this.disabled) return;
369
+ if (this.open) this.close();
370
+ else this.openPanel();
371
+ }
372
+ close() {
373
+ this.open = false;
374
+ this.draftStart = "";
375
+ }
376
+ prevMonth() {
377
+ if (this.viewMonth <= 1) {
378
+ this.viewYear -= 1;
379
+ this.viewMonth = 12;
380
+ } else {
381
+ this.viewMonth -= 1;
382
+ }
383
+ this._rebuildDays();
384
+ }
385
+ nextMonth() {
386
+ if (this.viewMonth >= 12) {
387
+ this.viewYear += 1;
388
+ this.viewMonth = 1;
389
+ } else {
390
+ this.viewMonth += 1;
391
+ }
392
+ this._rebuildDays();
393
+ }
394
+ choose(iso: string) {
395
+ if (!iso || this._isOutOfRange(iso)) return;
396
+ if (String(this.mode) === "range") {
397
+ if (!this.draftStart) {
398
+ this.draftStart = iso;
399
+ this._rebuildDays();
400
+ return;
401
+ }
402
+ let start = this.draftStart;
403
+ let end = iso;
404
+ if (end < start) {
405
+ const t = start;
406
+ start = end;
407
+ end = t;
408
+ }
409
+ this.draftStart = "";
410
+ this.close();
411
+ const ev = {
412
+ target: {
413
+ value: `${start}/${end}`,
414
+ start,
415
+ end,
416
+ },
417
+ };
418
+ this.emit("change", ev);
419
+ this.emit("input", ev);
420
+ if (typeof this.onChange === "function") this.onChange(ev);
421
+ if (typeof this.onInput === "function") this.onInput(ev);
422
+ return;
423
+ }
424
+ this.close();
425
+ if (iso === this.value) return;
426
+ const ev = {
427
+ target: {
428
+ value: iso,
429
+ start: iso,
430
+ end: iso,
431
+ },
432
+ };
433
+ this.emit("change", ev);
434
+ this.emit("input", ev);
435
+ if (typeof this.onChange === "function") this.onChange(ev);
436
+ if (typeof this.onInput === "function") this.onInput(ev);
437
+ }
438
+ onDayClick(ev: Event) {
439
+ const t = ev?.target as HTMLElement | null;
440
+ const el =
441
+ t && typeof t.closest === "function"
442
+ ? (t.closest("[data-vmz-date-iso]") as HTMLElement | null)
443
+ : ((ev?.currentTarget as HTMLElement | null) ?? null);
444
+ const iso =
445
+ el && typeof el.getAttribute === "function"
446
+ ? el.getAttribute("data-vmz-date-iso")
447
+ : null;
448
+ if (iso) this.choose(iso);
449
+ }
450
+ onTriggerKeyDown(ev: KeyboardEvent) {
451
+ if (!ev || this.disabled) return;
452
+ if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
453
+ ev.preventDefault();
454
+ this.openPanel();
455
+ } else if (ev.key === "Escape" && this.open) {
456
+ ev.preventDefault();
457
+ this.close();
458
+ }
459
+ }
460
+ onPanelKeyDown(ev: KeyboardEvent) {
461
+ if (!ev) return;
462
+ if (ev.key === "Escape") {
463
+ ev.preventDefault();
464
+ this.close();
465
+ return;
466
+ }
467
+ const root = this.__vmzDomRoot;
468
+ const panel =
469
+ root && typeof root.querySelector === "function"
470
+ ? root.querySelector('[data-vmz-overlay="date-picker"]')
471
+ : null;
472
+ if (!panel) return;
473
+ const nodes = [...panel.querySelectorAll("[data-vmz-date-iso]")].filter(
474
+ (n) => n instanceof HTMLElement && !(n as HTMLButtonElement).disabled,
475
+ );
476
+ if (!nodes.length) return;
477
+ const idx = nodes.indexOf(document.activeElement as Element);
478
+ if (ev.key === "ArrowRight") {
479
+ ev.preventDefault();
480
+ (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
481
+ } else if (ev.key === "ArrowLeft") {
482
+ ev.preventDefault();
483
+ (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
484
+ } else if (ev.key === "ArrowDown") {
485
+ ev.preventDefault();
486
+ (nodes[Math.min(idx + 7, nodes.length - 1)] as HTMLElement).focus();
487
+ } else if (ev.key === "ArrowUp") {
488
+ ev.preventDefault();
489
+ (nodes[Math.max(idx - 7, 0)] as HTMLElement).focus();
490
+ } else if (ev.key === "Home") {
491
+ ev.preventDefault();
492
+ (nodes[0] as HTMLElement).focus();
493
+ } else if (ev.key === "End") {
494
+ ev.preventDefault();
495
+ (nodes[nodes.length - 1] as HTMLElement).focus();
496
+ } else if (ev.key === "Enter" || ev.key === " ") {
497
+ const active = document.activeElement as HTMLElement | null;
498
+ const iso = active?.getAttribute?.("data-vmz-date-iso");
499
+ if (iso) {
500
+ ev.preventDefault();
501
+ this.choose(iso);
502
+ }
503
+ }
504
+ }
505
+ _rebuildDays() {
506
+ const y = this.viewYear || this._todayParts().y;
507
+ const m = this.viewMonth || this._todayParts().m;
508
+ this.viewYear = y;
509
+ this.viewMonth = m;
510
+ this.monthLabel = `${this._monthName(m)} ${y}`;
511
+ this.panelLabel =
512
+ String(this.mode) === "range"
513
+ ? `Choose date range — ${this.monthLabel}`
514
+ : `Choose date — ${this.monthLabel}`;
515
+ const firstDow = (new Date(y, m - 1, 1).getDay() + 6) % 7;
516
+ const dim = new Date(y, m, 0).getDate();
517
+ const prevDim = new Date(y, m - 1, 0).getDate();
518
+ const cells: typeof this.days = [];
519
+ const isRange = String(this.mode) === "range";
520
+ let start = "";
521
+ let end = "";
522
+ if (isRange) {
523
+ if (this.draftStart) {
524
+ start = this.draftStart;
525
+ end = this.draftStart;
526
+ } else {
527
+ start = this._parseIso(this.rangeStart) ? this.rangeStart : "";
528
+ end = this._parseIso(this.rangeEnd) ? this.rangeEnd : "";
529
+ }
530
+ }
531
+ for (let i = 0; i < 42; i++) {
532
+ let cy = y;
533
+ let cm = m;
534
+ let cd = 0;
535
+ let inMonth = true;
536
+ if (i < firstDow) {
537
+ inMonth = false;
538
+ cd = prevDim - firstDow + i + 1;
539
+ cm = m === 1 ? 12 : m - 1;
540
+ cy = m === 1 ? y - 1 : y;
541
+ } else if (i >= firstDow + dim) {
542
+ inMonth = false;
543
+ cd = i - firstDow - dim + 1;
544
+ cm = m === 12 ? 1 : m + 1;
545
+ cy = m === 12 ? y + 1 : y;
546
+ } else {
547
+ cd = i - firstDow + 1;
548
+ }
549
+ const iso = this._toIso(cy, cm, cd);
550
+ let selected = iso === this.value;
551
+ let inRange = false;
552
+ let rangeEdge = false;
553
+ if (isRange) {
554
+ selected = false;
555
+ if (start && end) {
556
+ rangeEdge = iso === start || iso === end;
557
+ inRange = iso >= start && iso <= end;
558
+ selected = rangeEdge;
559
+ } else if (start) {
560
+ rangeEdge = iso === start;
561
+ selected = rangeEdge;
562
+ }
563
+ }
564
+ cells.push({
565
+ key: iso,
566
+ iso,
567
+ label: String(cd),
568
+ inMonth,
569
+ selected,
570
+ inRange: inRange && !rangeEdge,
571
+ rangeEdge,
572
+ disabled: this._isOutOfRange(iso),
573
+ });
574
+ }
575
+ this.days = cells;
576
+ this._syncDisplay();
577
+ }
578
+ _parseIso(iso: string) {
579
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ""));
580
+ if (!m) return null;
581
+ const y = Number(m[1]);
582
+ const mo = Number(m[2]);
583
+ const d = Number(m[3]);
584
+ if (mo < 1 || mo > 12 || d < 1 || d > 31) return null;
585
+ return {
586
+ y,
587
+ m: mo,
588
+ d,
589
+ };
590
+ }
591
+ _toIso(y: number, m: number, d: number) {
592
+ return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
593
+ }
594
+ _todayParts() {
595
+ const now = new Date();
596
+ return {
597
+ y: now.getFullYear(),
598
+ m: now.getMonth() + 1,
599
+ d: now.getDate(),
600
+ };
601
+ }
602
+ _monthName(m: number) {
603
+ const names = [
604
+ "January",
605
+ "February",
606
+ "March",
607
+ "April",
608
+ "May",
609
+ "June",
610
+ "July",
611
+ "August",
612
+ "September",
613
+ "October",
614
+ "November",
615
+ "December",
616
+ ];
617
+ return names[m - 1] || String(m);
618
+ }
619
+ _isOutOfRange(iso: string) {
620
+ if (this.min && iso < this.min) return true;
621
+ if (this.max && iso > this.max) return true;
622
+ return false;
623
+ }
624
+ _syncOverlay() {
625
+ const root = this.__vmzDomRoot;
626
+ const panel =
627
+ root && typeof root.querySelector === "function"
628
+ ? root.querySelector('[data-vmz-overlay="date-picker"][data-vmz-focus="enter"]')
629
+ : null;
630
+ if (panel && this.open) this._bindDoc(panel as HTMLElement);
631
+ else if (!panel) this._teardownDoc();
632
+ this._syncDisplay();
633
+ }
634
+ _bindDoc(panel: HTMLElement) {
635
+ if (typeof document === "undefined") return;
636
+ if (!this._docKeyHandler) {
637
+ this._docKeyHandler = (ev: KeyboardEvent) => {
638
+ if (ev.key === "Escape") {
639
+ ev.preventDefault();
640
+ this.close();
641
+ }
642
+ };
643
+ document.addEventListener("keydown", this._docKeyHandler);
644
+ }
645
+ if (!this._docPointerHandler) {
646
+ const root = this.__vmzDomRoot;
647
+ this._docPointerHandler = (ev: Event) => {
648
+ const t = ev.target;
649
+ if (!(t instanceof Node)) return;
650
+ if (root && typeof root.contains === "function" && root.contains(t)) return;
651
+ this.close();
652
+ };
653
+ document.addEventListener("pointerdown", this._docPointerHandler, true);
654
+ }
655
+ const selected = panel.querySelector(
656
+ '[data-vmz-date-iso][aria-selected="true"]:not([disabled])',
657
+ );
658
+ const first = panel.querySelector("[data-vmz-date-iso]:not([disabled])");
659
+ const focusTarget = (selected || first) as HTMLElement | null;
660
+ if (focusTarget) focusTarget.focus();
661
+ else panel.focus();
662
+ }
663
+ _teardownDoc() {
664
+ if (typeof document !== "undefined") {
665
+ if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
666
+ if (this._docPointerHandler) {
667
+ document.removeEventListener("pointerdown", this._docPointerHandler, true);
668
+ }
669
+ }
670
+ this._docKeyHandler = null;
671
+ this._docPointerHandler = null;
672
+ }
673
+ }
674
+ </script>