@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,143 +1,110 @@
1
1
  <template>
2
- <div
3
- :class="'vmz-ui-select' + (open ? ' is-opened' : '') + (isMultiple ? ' is-multiple' : '')"
4
- data-vmz-ui="select"
5
- :data-mode="mode"
6
- :data-searchable="searchableAttr"
7
- :data-invalid="invalidAttr"
8
- >
9
- <label v-if="label" class="vmz-ui-select__label" :for="controlId">{{ label }}</label>
10
- <button
11
- type="button"
12
- :id="controlId"
13
- class="vmz-ui-select__control"
14
- role="combobox"
15
- :disabled="disabled"
16
- aria-haspopup="listbox"
17
- :aria-expanded="open ? 'true' : 'false'"
18
- :aria-controls="listId"
19
- :aria-invalid="error ? 'true' : 'false'"
20
- :aria-describedby="describedBy"
21
- :aria-multiselectable="isMultiple ? 'true' : 'false'"
22
- @click="toggle"
23
- :onKeyDown="onTriggerKeyDown"
24
- >
25
- <span class="vmz-ui-select__value">{{ displayLabel }}</span>
26
- <span class="vmz-ui-select__chev" aria-hidden="true"></span>
27
- </button>
28
- <div
29
- v-if="open"
30
- :id="listId"
31
- class="vmz-ui-select__panel"
32
- data-vmz-overlay="select"
33
- data-vmz-overlay-owner="select"
34
- data-vmz-focus="enter"
35
- >
36
- <div v-if="searchable" class="vmz-ui-select__search">
37
- <input
38
- type="search"
39
- class="vmz-ui-select__search-input"
40
- data-vmz-select="search"
41
- :value="searchQuery"
42
- :placeholder="searchPlaceholder"
43
- :aria-label="searchPlaceholder"
44
- @input="onSearchInput"
45
- :onKeyDown="onSearchKeyDown"
46
- />
2
+ <div data-vmz-ui="select" :data-mode="mode" :data-searchable="searchableAttr" :data-invalid="invalidAttr" :class='"vmz-ui-select" + (open ? " is-opened" : "") + (isMultiple ? " is-multiple" : "")'>
3
+ <label v-if="label" :for="controlId" class="vmz-ui-select__label">
4
+ {{ label }}
5
+ </label>
6
+ <button type="button" :id="controlId" role="combobox" :disabled="disabled" aria-haspopup="listbox" :aria-expanded='open ? "true" : "false"' :aria-controls="listId" :aria-invalid='error ? "true" : "false"' :aria-describedby="describedBy" :aria-multiselectable='isMultiple ? "true" : "false"' @click="toggle" :onKeyDown="onTriggerKeyDown" class="vmz-ui-select__control">
7
+ <span class="vmz-ui-select__value">
8
+ {{ displayLabel }}
9
+ </span>
10
+ <span aria-hidden="true" class="vmz-ui-select__chev" />
11
+ </button>
12
+ <div v-if="open" :id="listId" data-vmz-overlay="select" data-vmz-overlay-owner="select" data-vmz-focus="enter" class="vmz-ui-select__panel">
13
+ <div v-if="searchable" class="vmz-ui-select__search">
14
+ <input type="search" data-vmz-select="search" :value="searchQuery" :placeholder="searchPlaceholder" :aria-label="searchPlaceholder" @input="onSearchInput" :onKeyDown="onSearchKeyDown" class="vmz-ui-select__search-input" />
15
+ </div>
16
+ <ul role="listbox" :aria-multiselectable='isMultiple ? "true" : "false"' tabindex="-1" :onKeyDown="onListKeyDown" class="vmz-ui-select__list">
17
+ <li v-if="showEmpty" role="presentation" class="vmz-ui-select__empty">
18
+ {{ emptyText }}
19
+ </li>
20
+ <li v-for="opt in visibleOptions" :key="opt.value" role="presentation">
21
+ <button type="button" role="option" :data-vmz-option="opt.value" :aria-selected='opt.selected ? "true" : "false"' @click="onOptionClick" :class='"vmz-ui-select__option" + (opt.selected ? " is-on" : "")'>
22
+ {{ opt.label }}
23
+ </button>
24
+ </li>
25
+ </ul>
26
+ <div v-if="isMultiple" class="vmz-ui-select__footer">
27
+ <button type="button" data-vmz-select="done" @click="close" class="vmz-ui-select__done">
28
+ Done
29
+ </button>
30
+ </div>
47
31
  </div>
48
- <ul
49
- class="vmz-ui-select__list"
50
- role="listbox"
51
- :aria-multiselectable="isMultiple ? 'true' : 'false'"
52
- tabindex="-1"
53
- :onKeyDown="onListKeyDown"
54
- >
55
- <li v-if="showEmpty" class="vmz-ui-select__empty" role="presentation">{{ emptyText }}</li>
56
- <li v-for="opt in visibleOptions" :key="opt.value" role="presentation">
57
- <button
58
- type="button"
59
- :class="'vmz-ui-select__option' + (opt.selected ? ' is-on' : '')"
60
- role="option"
61
- :data-vmz-option="opt.value"
62
- :aria-selected="opt.selected ? 'true' : 'false'"
63
- @click="onOptionClick"
64
- >{{ opt.label }}</button>
65
- </li>
66
- </ul>
67
- <div v-if="isMultiple" class="vmz-ui-select__footer">
68
- <button type="button" class="vmz-ui-select__done" data-vmz-select="done" @click="close">Done</button>
69
- </div>
70
- </div>
71
- <p v-if="description" :id="descriptionId" class="vmz-ui-select__description">{{ description }}</p>
72
- <p v-if="error" :id="errorId" class="vmz-ui-select__error" role="alert">{{ error }}</p>
32
+ <p v-if="description" :id="descriptionId" class="vmz-ui-select__description">
33
+ {{ description }}
34
+ </p>
35
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-select__error">
36
+ {{ error }}
37
+ </p>
73
38
  </div>
74
39
  </template>
75
-
76
40
  <style>
77
- .vmz-ui-select {
41
+ .vmz-ui-select {
78
42
  position: relative;
79
43
  display: flex;
80
44
  flex-direction: column;
81
45
  gap: 0.35rem;
82
46
  max-width: 28rem;
83
- }
47
+ }
84
48
 
85
49
  .vmz-ui-select__label {
86
- font: inherit;
87
- font-weight: 500;
88
- font-size: 0.875rem;
50
+ font: inherit;
51
+ font-weight: 500;
52
+ font-size: 0.875rem;
89
53
  }
90
54
 
91
55
  .vmz-ui-select__control {
92
- display: inline-flex;
93
- align-items: center;
94
- justify-content: space-between;
95
- gap: 0.55rem;
96
- width: 100%;
97
- min-height: var(--vmz-control-height, 2rem);
98
- box-sizing: border-box;
99
- font: inherit;
100
- font-size: 0.875rem;
101
- text-align: left;
102
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
103
- border: 1px solid var(--vmz-line-default, #d9d9d9);
104
- border-radius: var(--vmz-radius-md, 6px);
105
- background: var(--vmz-surface-paper, #fff);
106
- color: inherit;
107
- outline: none;
108
- cursor: pointer;
109
- transition: border-color var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, linear),
110
- box-shadow var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, linear);
56
+ display: inline-flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ gap: 0.55rem;
60
+ width: 100%;
61
+ min-height: var(--vmz-control-height, 2rem);
62
+ box-sizing: border-box;
63
+ font: inherit;
64
+ font-size: 0.875rem;
65
+ text-align: left;
66
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
67
+ border: 1px solid var(--vmz-line-default, #d9d9d9);
68
+ border-radius: var(--vmz-radius-md, 6px);
69
+ background: var(--vmz-surface-paper, #fff);
70
+ color: inherit;
71
+ outline: none;
72
+ cursor: pointer;
73
+ transition:
74
+ border-color var(--vmz-motion-control-duration, 120ms)
75
+ var(--vmz-motion-control-easing, linear),
76
+ box-shadow var(--vmz-motion-control-duration, 120ms)
77
+ var(--vmz-motion-control-easing, linear);
111
78
  }
112
79
 
113
- .vmz-ui-select[data-invalid='true'] .vmz-ui-select__control {
114
- border-color: var(--vmz-status-danger-accent);
80
+ .vmz-ui-select[data-invalid="true"] .vmz-ui-select__control {
81
+ border-color: var(--vmz-status-danger-accent);
115
82
  }
116
83
 
117
84
  .vmz-ui-select__control:hover:not(:disabled) {
118
- border-color: var(--vmz-action-primary-background);
85
+ border-color: var(--vmz-action-primary-background);
119
86
  }
120
87
 
121
88
  .vmz-ui-select__control:focus-visible,
122
89
  .vmz-ui-select.is-opened .vmz-ui-select__control {
123
- border-color: var(--vmz-action-primary-background);
124
- box-shadow: 0 0 0 2px var(--vmz-focus-ring);
90
+ border-color: var(--vmz-action-primary-background);
91
+ box-shadow: 0 0 0 2px var(--vmz-focus-ring);
125
92
  }
126
93
 
127
- .vmz-ui-select__control:disabled {
94
+ .vmz-ui-select__control:disabled {
128
95
  opacity: 0.55;
129
96
  cursor: not-allowed;
130
- }
97
+ }
131
98
 
132
- .vmz-ui-select__value {
99
+ .vmz-ui-select__value {
133
100
  flex: 1 1 auto;
134
101
  min-width: 0;
135
102
  overflow: hidden;
136
103
  text-overflow: ellipsis;
137
104
  white-space: nowrap;
138
- }
105
+ }
139
106
 
140
- .vmz-ui-select__chev {
107
+ .vmz-ui-select__chev {
141
108
  flex: 0 0 auto;
142
109
  width: 0.45rem;
143
110
  height: 0.45rem;
@@ -145,13 +112,13 @@
145
112
  border-bottom: 1.5px solid currentColor;
146
113
  transform: rotate(45deg) translate(-1px, -2px);
147
114
  opacity: 0.7;
148
- }
115
+ }
149
116
 
150
- .vmz-ui-select.is-opened .vmz-ui-select__chev {
117
+ .vmz-ui-select.is-opened .vmz-ui-select__chev {
151
118
  transform: rotate(225deg) translate(-1px, -1px);
152
- }
119
+ }
153
120
 
154
- .vmz-ui-select__panel {
121
+ .vmz-ui-select__panel {
155
122
  position: absolute;
156
123
  z-index: 40;
157
124
  top: calc(100% + 0.25rem);
@@ -163,15 +130,19 @@
163
130
  border-radius: var(--vmz-radius-md, 6px);
164
131
  background: var(--vmz-surface-paper, #fff);
165
132
  color: inherit;
166
- box-shadow: var(--vmz-shadow-popup, 0 6px 16px 0 rgba(0,0,0,.08), 0 3px 6px -4px rgba(0,0,0,.12));
133
+ box-shadow: var(
134
+ --vmz-shadow-popup,
135
+ 0 6px 16px 0 rgba(0, 0, 0, 0.08),
136
+ 0 3px 6px -4px rgba(0, 0, 0, 0.12)
137
+ );
167
138
  outline: none;
168
- }
139
+ }
169
140
 
170
- .vmz-ui-select__search {
141
+ .vmz-ui-select__search {
171
142
  padding: 0.4rem 0.45rem 0.15rem;
172
- }
143
+ }
173
144
 
174
- .vmz-ui-select__search-input {
145
+ .vmz-ui-select__search-input {
175
146
  width: 100%;
176
147
  box-sizing: border-box;
177
148
  font: inherit;
@@ -182,28 +153,28 @@
182
153
  background: var(--vmz-surface-paper, #fff);
183
154
  color: inherit;
184
155
  outline: none;
185
- }
156
+ }
186
157
 
187
- .vmz-ui-select__search-input:focus-visible {
158
+ .vmz-ui-select__search-input:focus-visible {
188
159
  box-shadow: 0 0 0 2px var(--vmz-focus-ring);
189
- }
160
+ }
190
161
 
191
- .vmz-ui-select__list {
162
+ .vmz-ui-select__list {
192
163
  margin: 0;
193
164
  padding: 0.25rem;
194
165
  list-style: none;
195
166
  max-height: 16rem;
196
167
  overflow: auto;
197
168
  outline: none;
198
- }
169
+ }
199
170
 
200
- .vmz-ui-select__empty {
171
+ .vmz-ui-select__empty {
201
172
  padding: 0.55rem 0.65rem;
202
173
  opacity: 0.7;
203
174
  font-size: 0.92em;
204
- }
175
+ }
205
176
 
206
- .vmz-ui-select__option {
177
+ .vmz-ui-select__option {
207
178
  display: block;
208
179
  width: 100%;
209
180
  box-sizing: border-box;
@@ -216,26 +187,26 @@
216
187
  color: inherit;
217
188
  cursor: pointer;
218
189
  outline: none;
219
- }
190
+ }
220
191
 
221
- .vmz-ui-select__option:hover,
222
- .vmz-ui-select__option:focus-visible {
192
+ .vmz-ui-select__option:hover,
193
+ .vmz-ui-select__option:focus-visible {
223
194
  background: color-mix(in srgb, var(--vmz-action-primary-background) 18%, transparent);
224
- }
195
+ }
225
196
 
226
- .vmz-ui-select__option.is-on {
197
+ .vmz-ui-select__option.is-on {
227
198
  background: color-mix(in srgb, var(--vmz-action-primary-background) 28%, transparent);
228
199
  font-weight: 600;
229
- }
200
+ }
230
201
 
231
- .vmz-ui-select__footer {
202
+ .vmz-ui-select__footer {
232
203
  display: flex;
233
204
  justify-content: flex-end;
234
205
  padding: 0.35rem 0.45rem 0.45rem;
235
206
  border-top: 1px solid var(--vmz-line-default, #d9dee3);
236
- }
207
+ }
237
208
 
238
- .vmz-ui-select__done {
209
+ .vmz-ui-select__done {
239
210
  font: inherit;
240
211
  font-weight: 600;
241
212
  padding: 0.3rem 0.7rem;
@@ -244,297 +215,319 @@
244
215
  background: var(--vmz-surface-paper, #fff);
245
216
  color: inherit;
246
217
  cursor: pointer;
247
- }
218
+ }
248
219
 
249
- .vmz-ui-select__description {
220
+ .vmz-ui-select__description {
250
221
  margin: 0;
251
222
  opacity: 0.75;
252
223
  font-size: 0.9em;
253
- }
224
+ }
254
225
 
255
- .vmz-ui-select__error {
226
+ .vmz-ui-select__error {
256
227
  margin: 0;
257
228
  color: var(--vmz-status-danger-foreground);
258
229
  font-size: 0.9em;
259
- }
230
+ }
260
231
  </style>
261
-
262
232
  <script client>
263
233
  /**
264
- * VMZ UI — Select.
265
- * Owned listbox (trigger + panel + role=option). Never native <select> popup.
266
- * Parent owns value (single) or values (multiple) + options.
267
- * mode=multiple keeps panel open until Done/Escape; searchable filters visible options.
268
- */
234
+ * VMZ UI — Select.
235
+ * Owned listbox (trigger + panel + role=option). Never native <select> popup.
236
+ * Parent owns value (single) or values (multiple) + options.
237
+ * mode=multiple keeps panel open until Done/Escape; searchable filters visible options.
238
+ */
269
239
  export default class Select {
270
- public label: string = "";
271
- public description: string = "";
272
- public error: string = "";
273
- /** single mode */
274
- public value: string = "";
275
- /** multiple mode — parent owns array */
276
- public values: string[] = [];
277
- public options: Array<{
278
- value: string;
279
- label: string;
280
- }> = [];
281
- /** "single" | "multiple" */
282
- public mode: string = "single";
283
- public searchable: boolean = false;
284
- public searchPlaceholder: string = "Search";
285
- public emptyText: string = "No matches";
286
- public disabled: boolean = false;
287
- public invalidAttr: string = "false";
288
- public controlId: string = "vmz-select";
289
- public listId: string = "vmz-select-list";
290
- public descriptionId: string = "vmz-select-desc";
291
- public errorId: string = "vmz-select-err";
292
- public describedBy: string = "";
293
- public onChange: ((e: {
294
- target: {
295
- value: string;
296
- values: string[];
297
- };
298
- }) => void) | null = null;
299
- open = false;
300
- displayLabel = "";
301
- searchQuery = "";
302
- isMultiple = false;
303
- searchableAttr = "false";
304
- showEmpty = false;
305
- visibleOptions: Array<{
306
- value: string;
307
- label: string;
308
- selected: boolean;
309
- }> = [];
310
- _docKeyHandler = null;
311
- _docPointerHandler = null;
312
- _observer = null;
313
- onMount() {
314
- this.listId = `${this.controlId}-list`;
315
- this._syncMode();
316
- this._rebuildVisible();
317
- if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
318
- const root = this.__vmzDomRoot;
319
- if (!root || typeof root.querySelector !== "function") return;
320
- this._observer = new MutationObserver(() => this._syncOverlay());
321
- this._observer.observe(root, {
322
- childList: true,
323
- subtree: true
324
- });
325
- }
326
- onDestroy() {
327
- if (this._observer) {
328
- this._observer.disconnect();
329
- this._observer = null;
330
- }
331
- this._teardownDoc();
332
- }
333
- _syncMode() {
334
- this.isMultiple = String(this.mode || "single") === "multiple";
335
- this.searchableAttr = this.searchable ? "true" : "false";
336
- }
337
- _selectedSet() {
338
- if (this.isMultiple) {
339
- return new Set((this.values || []).map((v) => String(v)));
340
- }
341
- return new Set(this.value ? [String(this.value)] : []);
342
- }
343
- _rebuildVisible() {
344
- this._syncMode();
345
- const q = String(this.searchQuery || "").trim().toLowerCase();
346
- const selected = this._selectedSet();
347
- const all = this.options || [];
348
- const filtered = !q ? all : all.filter((o) => {
349
- const label = String(o.label || "").toLowerCase();
350
- const value = String(o.value || "").toLowerCase();
351
- return label.includes(q) || value.includes(q);
352
- });
353
- this.visibleOptions = filtered.map((o) => ({
354
- value: String(o.value),
355
- label: String(o.label),
356
- selected: selected.has(String(o.value))
357
- }));
358
- this.showEmpty = this.visibleOptions.length === 0;
359
- this._syncLabel();
360
- }
361
- _syncLabel() {
362
- const opts = this.options || [];
363
- if (this.isMultiple) {
364
- const selected = this._selectedSet();
365
- const labels = opts.filter((o) => selected.has(String(o.value))).map((o) => o.label);
366
- if (!labels.length) this.displayLabel = "—";
367
- else if (labels.length <= 2) this.displayLabel = labels.join(", ");
368
- else this.displayLabel = `${labels[0]}, ${labels[1]} +${labels.length - 2}`;
369
- return;
370
- }
371
- const hit = opts.find((o) => String(o.value) === String(this.value));
372
- this.displayLabel = hit ? hit.label : this.value || "—";
373
- }
374
- toggle() {
375
- if (this.disabled) return;
376
- if (this.open) this.close();
377
- else this.openPanel();
378
- }
379
- openPanel() {
380
- this.searchQuery = "";
381
- this._rebuildVisible();
382
- this.open = true;
383
- }
384
- close() {
385
- this.open = false;
386
- this.searchQuery = "";
387
- this._rebuildVisible();
388
- }
389
- _emit(nextValue: string, nextValues: string[]) {
390
- const payload = { target: {
391
- value: nextValue,
392
- values: nextValues
393
- } };
394
- this.emit("change", payload);
395
- if (typeof this.onChange === "function") {
396
- this.onChange(payload);
397
- }
398
- }
399
- /** Parent-owned prop writes (bindComponentProp) — keep derived label/options in sync. */
400
- __vmzOnParentProp(name: string) {
401
- if (name === "value" || name === "values" || name === "options") this._rebuildVisible();
402
- }
403
- choose(next: string) {
404
- if (!next && next !== "") return;
405
- if (this.isMultiple) {
406
- const cur = [...this.values || []].map(String);
407
- const idx = cur.indexOf(String(next));
408
- if (idx >= 0) cur.splice(idx, 1);
409
- else cur.push(String(next));
410
- this.values = cur;
411
- this._rebuildVisible();
412
- this._emit(cur.join(","), cur);
413
- return;
414
- }
415
- const nextStr = String(next);
416
- if (nextStr === String(this.value)) {
417
- this.close();
418
- return;
419
- }
420
- this.value = nextStr;
421
- this._rebuildVisible();
422
- this._emit(nextStr, nextStr ? [nextStr] : []);
423
- this.close();
424
- }
425
- onSearchInput(ev: Event) {
426
- const t = ev?.target as HTMLInputElement | null;
427
- this.searchQuery = t ? String(t.value || "") : "";
428
- this._rebuildVisible();
429
- }
430
- onSearchKeyDown(ev: KeyboardEvent) {
431
- if (!ev) return;
432
- if (ev.key === "Escape") {
433
- ev.preventDefault();
434
- this.close();
435
- } else if (ev.key === "ArrowDown") {
436
- ev.preventDefault();
437
- const root = this.__vmzDomRoot;
438
- const first = root && typeof root.querySelector === "function" ? root.querySelector("[data-vmz-overlay=\"select\"] [role=\"option\"]") : null;
439
- if (first instanceof HTMLElement) first.focus();
440
- }
441
- }
442
- onOptionClick(ev: Event) {
443
- const t = ev?.target as Element | null;
444
- const el = (t && typeof (t as HTMLElement).closest === "function" ? (t as HTMLElement).closest("[data-vmz-option]") as HTMLElement | null : null) || ev?.currentTarget as HTMLElement | null;
445
- const id = el && typeof el.getAttribute === "function" ? el.getAttribute("data-vmz-option") : null;
446
- if (id != null) this.choose(id);
447
- }
448
- onTriggerKeyDown(ev: KeyboardEvent) {
449
- if (!ev || this.disabled) return;
450
- if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
451
- ev.preventDefault();
452
- this.openPanel();
453
- } else if (ev.key === "Escape" && this.open) {
454
- ev.preventDefault();
455
- this.close();
456
- }
457
- }
458
- onListKeyDown(ev: KeyboardEvent) {
459
- if (!ev) return;
460
- if (ev.key === "Escape") {
461
- ev.preventDefault();
462
- this.close();
463
- return;
464
- }
465
- const root = this.__vmzDomRoot;
466
- const list = root && typeof root.querySelector === "function" ? root.querySelector(`[data-vmz-overlay="select"] [role="listbox"]`) : null;
467
- if (!list) return;
468
- const nodes = [...list.querySelectorAll("[role=\"option\"]")].filter((n) => n instanceof HTMLElement && n.offsetParent !== null);
469
- if (!nodes.length) return;
470
- const idx = nodes.indexOf(document.activeElement as Element);
471
- if (ev.key === "ArrowDown") {
472
- ev.preventDefault();
473
- (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
474
- } else if (ev.key === "ArrowUp") {
475
- ev.preventDefault();
476
- (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
477
- } else if (ev.key === "Home") {
478
- ev.preventDefault();
479
- (nodes[0] as HTMLElement).focus();
480
- } else if (ev.key === "End") {
481
- ev.preventDefault();
482
- (nodes[nodes.length - 1] as HTMLElement).focus();
483
- } else if (ev.key === "Enter" || ev.key === " ") {
484
- const active = document.activeElement as HTMLElement | null;
485
- const id = active?.getAttribute?.("data-vmz-option");
486
- if (id) {
487
- ev.preventDefault();
488
- this.choose(id);
489
- }
490
- }
491
- }
492
- _syncOverlay() {
493
- const root = this.__vmzDomRoot;
494
- const panel = root && typeof root.querySelector === "function" ? root.querySelector("[data-vmz-overlay=\"select\"][data-vmz-focus=\"enter\"]") : null;
495
- if (panel && this.open) this._bindDoc(panel as HTMLElement);
496
- else if (!panel) this._teardownDoc();
497
- this._rebuildVisible();
498
- }
499
- _bindDoc(panel: HTMLElement) {
500
- if (typeof document === "undefined") return;
501
- if (!this._docKeyHandler) {
502
- this._docKeyHandler = (ev: KeyboardEvent) => {
503
- if (ev.key === "Escape") {
504
- ev.preventDefault();
505
- this.close();
506
- }
507
- };
508
- document.addEventListener("keydown", this._docKeyHandler);
509
- }
510
- if (!this._docPointerHandler) {
511
- const root = this.__vmzDomRoot;
512
- this._docPointerHandler = (ev: Event) => {
513
- const t = ev.target;
514
- if (!(t instanceof Node)) return;
515
- if (root && typeof root.contains === "function" && root.contains(t)) return;
516
- this.close();
517
- };
518
- document.addEventListener("pointerdown", this._docPointerHandler, true);
519
- }
520
- const search = panel.querySelector("[data-vmz-select=\"search\"]");
521
- if (search instanceof HTMLElement) {
522
- search.focus();
523
- return;
524
- }
525
- const first = panel.querySelector("[role=\"option\"]");
526
- if (first instanceof HTMLElement) first.focus();
527
- else panel.focus();
528
- }
529
- _teardownDoc() {
530
- if (typeof document !== "undefined") {
531
- if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
532
- if (this._docPointerHandler) {
533
- document.removeEventListener("pointerdown", this._docPointerHandler, true);
534
- }
535
- }
536
- this._docKeyHandler = null;
537
- this._docPointerHandler = null;
538
- }
240
+ public label: string = "";
241
+ public description: string = "";
242
+ public error: string = "";
243
+ /** single mode */
244
+ public value: string = "";
245
+ /** multiple mode — parent owns array */
246
+ public values: string[] = [];
247
+ public options: Array<{
248
+ value: string;
249
+ label: string;
250
+ }> = [];
251
+ /** "single" | "multiple" */
252
+ public mode: string = "single";
253
+ public searchable: boolean = false;
254
+ public searchPlaceholder: string = "Search";
255
+ public emptyText: string = "No matches";
256
+ public disabled: boolean = false;
257
+ public invalidAttr: string = "false";
258
+ public controlId: string = "vmz-select";
259
+ public listId: string = "vmz-select-list";
260
+ public descriptionId: string = "vmz-select-desc";
261
+ public errorId: string = "vmz-select-err";
262
+ public describedBy: string = "";
263
+ public onChange:
264
+ | ((e: {
265
+ target: {
266
+ value: string;
267
+ values: string[];
268
+ };
269
+ }) => void)
270
+ | null = null;
271
+ open = false;
272
+ displayLabel = "";
273
+ searchQuery = "";
274
+ isMultiple = false;
275
+ searchableAttr = "false";
276
+ showEmpty = false;
277
+ visibleOptions: Array<{
278
+ value: string;
279
+ label: string;
280
+ selected: boolean;
281
+ }> = [];
282
+ _docKeyHandler = null;
283
+ _docPointerHandler = null;
284
+ _observer = null;
285
+ onMount() {
286
+ this.listId = `${this.controlId}-list`;
287
+ this._syncMode();
288
+ this._rebuildVisible();
289
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
290
+ const root = this.__vmzDomRoot;
291
+ if (!root || typeof root.querySelector !== "function") return;
292
+ this._observer = new MutationObserver(() => this._syncOverlay());
293
+ this._observer.observe(root, {
294
+ childList: true,
295
+ subtree: true,
296
+ });
297
+ }
298
+ onDestroy() {
299
+ if (this._observer) {
300
+ this._observer.disconnect();
301
+ this._observer = null;
302
+ }
303
+ this._teardownDoc();
304
+ }
305
+ _syncMode() {
306
+ this.isMultiple = String(this.mode || "single") === "multiple";
307
+ this.searchableAttr = this.searchable ? "true" : "false";
308
+ }
309
+ _selectedSet() {
310
+ if (this.isMultiple) {
311
+ return new Set((this.values || []).map((v) => String(v)));
312
+ }
313
+ return new Set(this.value ? [String(this.value)] : []);
314
+ }
315
+ _rebuildVisible() {
316
+ this._syncMode();
317
+ const q = String(this.searchQuery || "")
318
+ .trim()
319
+ .toLowerCase();
320
+ const selected = this._selectedSet();
321
+ const all = this.options || [];
322
+ const filtered = !q
323
+ ? all
324
+ : all.filter((o) => {
325
+ const label = String(o.label || "").toLowerCase();
326
+ const value = String(o.value || "").toLowerCase();
327
+ return label.includes(q) || value.includes(q);
328
+ });
329
+ this.visibleOptions = filtered.map((o) => ({
330
+ value: String(o.value),
331
+ label: String(o.label),
332
+ selected: selected.has(String(o.value)),
333
+ }));
334
+ this.showEmpty = this.visibleOptions.length === 0;
335
+ this._syncLabel();
336
+ }
337
+ _syncLabel() {
338
+ const opts = this.options || [];
339
+ if (this.isMultiple) {
340
+ const selected = this._selectedSet();
341
+ const labels = opts.filter((o) => selected.has(String(o.value))).map((o) => o.label);
342
+ if (!labels.length) this.displayLabel = "—";
343
+ else if (labels.length <= 2) this.displayLabel = labels.join(", ");
344
+ else this.displayLabel = `${labels[0]}, ${labels[1]} +${labels.length - 2}`;
345
+ return;
346
+ }
347
+ const hit = opts.find((o) => String(o.value) === String(this.value));
348
+ this.displayLabel = hit ? hit.label : this.value || "—";
349
+ }
350
+ toggle() {
351
+ if (this.disabled) return;
352
+ if (this.open) this.close();
353
+ else this.openPanel();
354
+ }
355
+ openPanel() {
356
+ this.searchQuery = "";
357
+ this._rebuildVisible();
358
+ this.open = true;
359
+ }
360
+ close() {
361
+ this.open = false;
362
+ this.searchQuery = "";
363
+ this._rebuildVisible();
364
+ }
365
+ _emit(nextValue: string, nextValues: string[]) {
366
+ const payload = {
367
+ target: {
368
+ value: nextValue,
369
+ values: nextValues,
370
+ },
371
+ };
372
+ this.emit("change", payload);
373
+ if (typeof this.onChange === "function") {
374
+ this.onChange(payload);
375
+ }
376
+ }
377
+ /** Parent-owned prop writes (bindComponentProp) — keep derived label/options in sync. */
378
+ __vmzOnParentProp(name: string) {
379
+ if (name === "value" || name === "values" || name === "options") this._rebuildVisible();
380
+ }
381
+ choose(next: string) {
382
+ if (!next && next !== "") return;
383
+ if (this.isMultiple) {
384
+ const cur = [...(this.values || [])].map(String);
385
+ const idx = cur.indexOf(String(next));
386
+ if (idx >= 0) cur.splice(idx, 1);
387
+ else cur.push(String(next));
388
+ this.values = cur;
389
+ this._rebuildVisible();
390
+ this._emit(cur.join(","), cur);
391
+ return;
392
+ }
393
+ const nextStr = String(next);
394
+ if (nextStr === String(this.value)) {
395
+ this.close();
396
+ return;
397
+ }
398
+ this.value = nextStr;
399
+ this._rebuildVisible();
400
+ this._emit(nextStr, nextStr ? [nextStr] : []);
401
+ this.close();
402
+ }
403
+ onSearchInput(ev: Event) {
404
+ const t = ev?.target as HTMLInputElement | null;
405
+ this.searchQuery = t ? String(t.value || "") : "";
406
+ this._rebuildVisible();
407
+ }
408
+ onSearchKeyDown(ev: KeyboardEvent) {
409
+ if (!ev) return;
410
+ if (ev.key === "Escape") {
411
+ ev.preventDefault();
412
+ this.close();
413
+ } else if (ev.key === "ArrowDown") {
414
+ ev.preventDefault();
415
+ const root = this.__vmzDomRoot;
416
+ const first =
417
+ root && typeof root.querySelector === "function"
418
+ ? root.querySelector('[data-vmz-overlay="select"] [role="option"]')
419
+ : null;
420
+ if (first instanceof HTMLElement) first.focus();
421
+ }
422
+ }
423
+ onOptionClick(ev: Event) {
424
+ const t = ev?.target as Element | null;
425
+ const el =
426
+ (t && typeof (t as HTMLElement).closest === "function"
427
+ ? ((t as HTMLElement).closest("[data-vmz-option]") as HTMLElement | null)
428
+ : null) || (ev?.currentTarget as HTMLElement | null);
429
+ const id =
430
+ el && typeof el.getAttribute === "function" ? el.getAttribute("data-vmz-option") : null;
431
+ if (id != null) this.choose(id);
432
+ }
433
+ onTriggerKeyDown(ev: KeyboardEvent) {
434
+ if (!ev || this.disabled) return;
435
+ if ((ev.key === "ArrowDown" || ev.key === "Enter" || ev.key === " ") && !this.open) {
436
+ ev.preventDefault();
437
+ this.openPanel();
438
+ } else if (ev.key === "Escape" && this.open) {
439
+ ev.preventDefault();
440
+ this.close();
441
+ }
442
+ }
443
+ onListKeyDown(ev: KeyboardEvent) {
444
+ if (!ev) return;
445
+ if (ev.key === "Escape") {
446
+ ev.preventDefault();
447
+ this.close();
448
+ return;
449
+ }
450
+ const root = this.__vmzDomRoot;
451
+ const list =
452
+ root && typeof root.querySelector === "function"
453
+ ? root.querySelector(`[data-vmz-overlay="select"] [role="listbox"]`)
454
+ : null;
455
+ if (!list) return;
456
+ const nodes = [...list.querySelectorAll('[role="option"]')].filter(
457
+ (n) => n instanceof HTMLElement && n.offsetParent !== null,
458
+ );
459
+ if (!nodes.length) return;
460
+ const idx = nodes.indexOf(document.activeElement as Element);
461
+ if (ev.key === "ArrowDown") {
462
+ ev.preventDefault();
463
+ (nodes[(idx + 1 + nodes.length) % nodes.length] as HTMLElement).focus();
464
+ } else if (ev.key === "ArrowUp") {
465
+ ev.preventDefault();
466
+ (nodes[(idx - 1 + nodes.length) % nodes.length] as HTMLElement).focus();
467
+ } else if (ev.key === "Home") {
468
+ ev.preventDefault();
469
+ (nodes[0] as HTMLElement).focus();
470
+ } else if (ev.key === "End") {
471
+ ev.preventDefault();
472
+ (nodes[nodes.length - 1] as HTMLElement).focus();
473
+ } else if (ev.key === "Enter" || ev.key === " ") {
474
+ const active = document.activeElement as HTMLElement | null;
475
+ const id = active?.getAttribute?.("data-vmz-option");
476
+ if (id) {
477
+ ev.preventDefault();
478
+ this.choose(id);
479
+ }
480
+ }
481
+ }
482
+ _syncOverlay() {
483
+ const root = this.__vmzDomRoot;
484
+ const panel =
485
+ root && typeof root.querySelector === "function"
486
+ ? root.querySelector('[data-vmz-overlay="select"][data-vmz-focus="enter"]')
487
+ : null;
488
+ if (panel && this.open) this._bindDoc(panel as HTMLElement);
489
+ else if (!panel) this._teardownDoc();
490
+ this._rebuildVisible();
491
+ }
492
+ _bindDoc(panel: HTMLElement) {
493
+ if (typeof document === "undefined") return;
494
+ if (!this._docKeyHandler) {
495
+ this._docKeyHandler = (ev: KeyboardEvent) => {
496
+ if (ev.key === "Escape") {
497
+ ev.preventDefault();
498
+ this.close();
499
+ }
500
+ };
501
+ document.addEventListener("keydown", this._docKeyHandler);
502
+ }
503
+ if (!this._docPointerHandler) {
504
+ const root = this.__vmzDomRoot;
505
+ this._docPointerHandler = (ev: Event) => {
506
+ const t = ev.target;
507
+ if (!(t instanceof Node)) return;
508
+ if (root && typeof root.contains === "function" && root.contains(t)) return;
509
+ this.close();
510
+ };
511
+ document.addEventListener("pointerdown", this._docPointerHandler, true);
512
+ }
513
+ const search = panel.querySelector('[data-vmz-select="search"]');
514
+ if (search instanceof HTMLElement) {
515
+ search.focus();
516
+ return;
517
+ }
518
+ const first = panel.querySelector('[role="option"]');
519
+ if (first instanceof HTMLElement) first.focus();
520
+ else panel.focus();
521
+ }
522
+ _teardownDoc() {
523
+ if (typeof document !== "undefined") {
524
+ if (this._docKeyHandler) document.removeEventListener("keydown", this._docKeyHandler);
525
+ if (this._docPointerHandler) {
526
+ document.removeEventListener("pointerdown", this._docPointerHandler, true);
527
+ }
528
+ }
529
+ this._docKeyHandler = null;
530
+ this._docPointerHandler = null;
531
+ }
539
532
  }
540
533
  </script>