@vmz/ui 0.2.3 → 0.2.4

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