@vmz/ui 0.3.6 → 0.3.8

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