@vmz/ui 0.3.6 → 0.3.7

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 (42) hide show
  1. package/README.md +51 -51
  2. package/package.json +2 -2
  3. package/src/components/Accordion.vmz +94 -94
  4. package/src/components/AppShell.vmz +5 -2
  5. package/src/components/Autocomplete.vmz +154 -154
  6. package/src/components/Badge.vmz +70 -70
  7. package/src/components/Breadcrumb.vmz +72 -72
  8. package/src/components/BulkActions.vmz +51 -51
  9. package/src/components/Button.vmz +213 -211
  10. package/src/components/Callout.vmz +77 -77
  11. package/src/components/Card.vmz +120 -120
  12. package/src/components/Checkbox.vmz +55 -55
  13. package/src/components/CodeBlock.vmz +21 -6
  14. package/src/components/ConsoleShell.vmz +251 -251
  15. package/src/components/DataTable.vmz +194 -194
  16. package/src/components/DatePicker.vmz +674 -674
  17. package/src/components/Dialog.vmz +17 -1
  18. package/src/components/Drawer.vmz +427 -427
  19. package/src/components/Dropdown.vmz +51 -5
  20. package/src/components/Empty.vmz +54 -54
  21. package/src/components/Field.vmz +115 -115
  22. package/src/components/FormItem.vmz +79 -79
  23. package/src/components/Link.vmz +5 -1
  24. package/src/components/List.vmz +102 -102
  25. package/src/components/Menu.vmz +243 -243
  26. package/src/components/Pagination.vmz +71 -69
  27. package/src/components/Popover.vmz +223 -223
  28. package/src/components/Prose.vmz +81 -81
  29. package/src/components/RadioGroup.vmz +124 -124
  30. package/src/components/Segmented.vmz +51 -51
  31. package/src/components/Select.vmz +533 -533
  32. package/src/components/Steps.vmz +103 -103
  33. package/src/components/Switch.vmz +98 -98
  34. package/src/components/Table.vmz +165 -165
  35. package/src/components/Tabs.vmz +110 -110
  36. package/src/components/Tag.vmz +43 -43
  37. package/src/components/TextArea.vmz +118 -118
  38. package/src/components/Timeline.vmz +76 -76
  39. package/src/components/Toc.vmz +74 -74
  40. package/src/components/Tooltip.vmz +58 -58
  41. package/src/components/Tree.vmz +191 -191
  42. package/src/components/Upload.vmz +1299 -1289
package/README.md CHANGED
@@ -1,51 +1,51 @@
1
- # @vmz/ui — VMZ UI
2
-
3
- Official **VMZ UI** framework package.
4
-
5
- ```text
6
- VMZ Design ≠ VMZ UI
7
- /designs ≠ this package
8
- @vmz/ui ≠ theme truth source
9
- ```
10
-
11
- - Declares **semantic token requirements** (see `contracts/`).
12
- - Ships ordinary `.vmz` under **`src/components/`** (same hard convention as apps) — **not** `@vmz/plugin-*`, no
13
- `componentsRoot` config.
14
- - **Never** hardcodes brand hex (`#176BFF`, etc.). Applications provide values via `/designs`.
15
-
16
- ## Preview `0.0.4`
17
-
18
- This is a **preview** release. It is **not** `production-ready` and **not** a mature / production UI.
19
-
20
- Headline surface for this cut:
21
-
22
- - Commercial / Console / Document-Product composition on official homepage
23
- - Form depth, Structure, Overlay stacking, DataTable (ordinary HTML table — not `@vmz/ui-data-grid`)
24
- - DatePicker owned calendar overlay (not native `input[type=date]` popup)
25
- - Upload multipart binary (parent-owned FormData → `#server` File/Blob; progress/cancel)
26
- - Density / RTL / preset materialize
27
- - Motion continuity + interrupt/cancel
28
- - Motion compiler IR thin gate (`pnpm verify -- motion-ir`)
29
- - UI7 conformance pack (`@vmz/ui/conformance` + `pnpm verify -- ui7`)
30
- - `@vmz/ui-data-grid` thin gate (virtualization + pinned column — not tree/group/pivot/edit)
31
-
32
- Verify:
33
-
34
- ```bash
35
- pnpm verify -- ui-automation
36
- pnpm verify -- ui7
37
- pnpm verify -- ui-data-grid
38
- pnpm verify -- official-homepage
39
- ```
40
-
41
- Still open relative to Browser Production Profile v1: `browser-production` aggregate core gaps,
42
- `@vmz/ui-data-grid` deep capabilities, Upload cross-page session / multi-file parallel chunks,
43
- `@vmz/test` Browser Host U0–U1, sibling `vmz-panel` product app.
44
-
45
- ## Commercial default surface
46
-
47
- Default web surface targets modern commercial console density: flat 32px-class controls,
48
- quiet borders, restrained radii, layered page/paper surfaces without Bootstrap-era inset
49
- fields, accent bars, or card lift. Applications replace every semantic value through `/designs`.
50
- Additional general-purpose components include layout primitives, typography, segmented controls,
51
- tags, avatars, dropdowns, collapse panels and progress indicators.
1
+ # @vmz/ui — VMZ UI
2
+
3
+ Official **VMZ UI** framework package.
4
+
5
+ ```text
6
+ VMZ Design ≠ VMZ UI
7
+ /designs ≠ this package
8
+ @vmz/ui ≠ theme truth source
9
+ ```
10
+
11
+ - Declares **semantic token requirements** (see `contracts/`).
12
+ - Ships ordinary `.vmz` under **`src/components/`** (same hard convention as apps) — **not** `@vmz/plugin-*`, no
13
+ `componentsRoot` config.
14
+ - **Never** hardcodes brand hex (`#176BFF`, etc.). Applications provide values via `/designs`.
15
+
16
+ ## Preview `0.0.4`
17
+
18
+ This is a **preview** release. It is **not** `production-ready` and **not** a mature / production UI.
19
+
20
+ Headline surface for this cut:
21
+
22
+ - Commercial / Console / Document-Product composition on official homepage
23
+ - Form depth, Structure, Overlay stacking, DataTable (ordinary HTML table — not `@vmz/ui-data-grid`)
24
+ - DatePicker owned calendar overlay (not native `input[type=date]` popup)
25
+ - Upload multipart binary (parent-owned FormData → `#server` File/Blob; progress/cancel)
26
+ - Density / RTL / preset materialize
27
+ - Motion continuity + interrupt/cancel
28
+ - Motion compiler IR thin gate (`pnpm verify -- motion-ir`)
29
+ - UI7 conformance pack (`@vmz/ui/conformance` + `pnpm verify -- ui7`)
30
+ - `@vmz/ui-data-grid` thin gate (virtualization + pinned column — not tree/group/pivot/edit)
31
+
32
+ Verify:
33
+
34
+ ```bash
35
+ pnpm verify -- ui-automation
36
+ pnpm verify -- ui7
37
+ pnpm verify -- ui-data-grid
38
+ pnpm verify -- official-homepage
39
+ ```
40
+
41
+ Still open relative to Browser Production Profile v1: `browser-production` aggregate core gaps,
42
+ `@vmz/ui-data-grid` deep capabilities, Upload cross-page session / multi-file parallel chunks,
43
+ `@vmz/test` Browser Host U0–U1, sibling `vmz-panel` product app.
44
+
45
+ ## Commercial default surface
46
+
47
+ Default web surface targets modern commercial console density: flat 32px-class controls,
48
+ quiet borders, restrained radii, layered page/paper surfaces without Bootstrap-era inset
49
+ fields, accent bars, or card lift. Applications replace every semantic value through `/designs`.
50
+ Additional general-purpose components include layout primitives, typography, segmented controls,
51
+ tags, avatars, dropdowns, collapse panels and progress indicators.
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "license": "MIT",
83
83
  "name": "@vmz/ui",
84
84
  "peerDependencies": {
85
- "@vmz/ui-icons": "0.3.6"
85
+ "@vmz/ui-icons": "0.3.7"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@vmz/ui-icons": {
@@ -98,5 +98,5 @@
98
98
  "url": "git+https://github.com/doki-land/vmz-framework.git"
99
99
  },
100
100
  "type": "module",
101
- "version": "0.3.6"
101
+ "version": "0.3.7"
102
102
  }
@@ -1,94 +1,94 @@
1
- <template>
2
- <div data-vmz-ui="accordion" :aria-label="label" class="vmz-ui-accordion">
3
- <div v-for="item in items" :key="item.id" :data-vmz-accordion="item.id" class="vmz-ui-accordion__item">
4
- <h3 class="vmz-ui-accordion__heading">
5
- <button type="button" :id="item.triggerId" :data-vmz-accordion-trigger="item.id" :aria-expanded="openId === item.id ? 'true' : 'false'" :aria-controls="item.panelId" @click="(() => toggle(item.id))" class="vmz-ui-accordion__trigger">
6
- {{ item.title }}
7
- </button>
8
- </h3>
9
- <div v-if="openId === item.id" :id="item.panelId" role="region" :data-vmz-accordion-panel="item.id" :aria-labelledby="item.triggerId" class="vmz-ui-accordion__panel">
10
- {{ item.body }}
11
- </div>
12
- </div>
13
- </div>
14
- </template>
15
- <style>
16
- .vmz-ui-accordion {
17
- display: flex;
18
- flex-direction: column;
19
- gap: 0.375rem;
20
- max-width: 36rem;
21
- border: 1px solid var(--vmz-line-default);
22
- border-radius: var(--vmz-radius-md, 6px);
23
- background: var(--vmz-surface-paper);
24
- color: var(--vmz-text-ink);
25
- }
26
-
27
- .vmz-ui-accordion__item {
28
- border-bottom: 1px solid var(--vmz-line-default);
29
- }
30
-
31
- .vmz-ui-accordion__item:last-child {
32
- border-bottom: none;
33
- }
34
-
35
- .vmz-ui-accordion__heading {
36
- margin: 0;
37
- font: inherit;
38
- }
39
-
40
- .vmz-ui-accordion__trigger {
41
- display: block;
42
- width: 100%;
43
- text-align: start;
44
- font: inherit;
45
- font-weight: 600;
46
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
47
- border: 0;
48
- background: transparent;
49
- color: inherit;
50
- cursor: pointer;
51
- outline: none;
52
- }
53
-
54
- .vmz-ui-accordion__trigger:hover {
55
- background: color-mix(in srgb, var(--vmz-surface-mist) 85%, transparent);
56
- }
57
-
58
- .vmz-ui-accordion__trigger:focus-visible {
59
- box-shadow: inset 0 0 0 3px var(--vmz-focus-ring);
60
- }
61
-
62
- .vmz-ui-accordion__trigger[aria-expanded="true"] {
63
- color: var(--vmz-action-primary-active);
64
- }
65
-
66
- .vmz-ui-accordion__panel {
67
- padding: 0 var(--vmz-density-control-padding-x) var(--vmz-density-control-padding-y);
68
- color: var(--vmz-text-steel);
69
- line-height: 1.5;
70
- }
71
- </style>
72
- <script client>
73
- /**
74
- * VMZ UI — Accordion.
75
- * Exclusive open: parent owns `openId`. Toggle same id closes.
76
- * Panel body lives on items (no per-panel slot runtime).
77
- */
78
- export default class Accordion {
79
- public label: string = "Accordion";
80
- public openId: string = "";
81
- /** @type {{ id: string, title: string, body: string, triggerId: string, panelId: string }[]} */
82
- public items: {
83
- id: string;
84
- title: string;
85
- body: string;
86
- triggerId: string;
87
- panelId: string;
88
- }[] = [];
89
- public onToggle: ((id: string) => void) | null = null;
90
- toggle(id) {
91
- if (typeof this.onToggle === "function") this.onToggle(id);
92
- }
93
- }
94
- </script>
1
+ <template>
2
+ <div data-vmz-ui="accordion" :aria-label="label" class="vmz-ui-accordion">
3
+ <div v-for="item in items" :key="item.id" :data-vmz-accordion="item.id" class="vmz-ui-accordion__item">
4
+ <h3 class="vmz-ui-accordion__heading">
5
+ <button type="button" :id="item.triggerId" :data-vmz-accordion-trigger="item.id" :aria-expanded="openId === item.id ? 'true' : 'false'" :aria-controls="item.panelId" @click="(() => toggle(item.id))" class="vmz-ui-accordion__trigger">
6
+ {{ item.title }}
7
+ </button>
8
+ </h3>
9
+ <div v-if="openId === item.id" :id="item.panelId" role="region" :data-vmz-accordion-panel="item.id" :aria-labelledby="item.triggerId" class="vmz-ui-accordion__panel">
10
+ {{ item.body }}
11
+ </div>
12
+ </div>
13
+ </div>
14
+ </template>
15
+ <style>
16
+ .vmz-ui-accordion {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 0.375rem;
20
+ max-width: 36rem;
21
+ border: 1px solid var(--vmz-line-default);
22
+ border-radius: var(--vmz-radius-md, 6px);
23
+ background: var(--vmz-surface-paper);
24
+ color: var(--vmz-text-ink);
25
+ }
26
+
27
+ .vmz-ui-accordion__item {
28
+ border-bottom: 1px solid var(--vmz-line-default);
29
+ }
30
+
31
+ .vmz-ui-accordion__item:last-child {
32
+ border-bottom: none;
33
+ }
34
+
35
+ .vmz-ui-accordion__heading {
36
+ margin: 0;
37
+ font: inherit;
38
+ }
39
+
40
+ .vmz-ui-accordion__trigger {
41
+ display: block;
42
+ width: 100%;
43
+ text-align: start;
44
+ font: inherit;
45
+ font-weight: 600;
46
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
47
+ border: 0;
48
+ background: transparent;
49
+ color: inherit;
50
+ cursor: pointer;
51
+ outline: none;
52
+ }
53
+
54
+ .vmz-ui-accordion__trigger:hover {
55
+ background: color-mix(in srgb, var(--vmz-surface-mist) 85%, transparent);
56
+ }
57
+
58
+ .vmz-ui-accordion__trigger:focus-visible {
59
+ box-shadow: inset 0 0 0 3px var(--vmz-focus-ring);
60
+ }
61
+
62
+ .vmz-ui-accordion__trigger[aria-expanded="true"] {
63
+ color: var(--vmz-action-primary-active);
64
+ }
65
+
66
+ .vmz-ui-accordion__panel {
67
+ padding: 0 var(--vmz-density-control-padding-x) var(--vmz-density-control-padding-y);
68
+ color: var(--vmz-text-steel);
69
+ line-height: 1.5;
70
+ }
71
+ </style>
72
+ <script client>
73
+ /**
74
+ * VMZ UI — Accordion.
75
+ * Exclusive open: parent owns `openId`. Toggle same id closes.
76
+ * Panel body lives on items (no per-panel slot runtime).
77
+ */
78
+ export default class Accordion {
79
+ public label: string = "Accordion";
80
+ public openId: string = "";
81
+ /** @type {{ id: string, title: string, body: string, triggerId: string, panelId: string }[]} */
82
+ public items: {
83
+ id: string;
84
+ title: string;
85
+ body: string;
86
+ triggerId: string;
87
+ panelId: string;
88
+ }[] = [];
89
+ public onToggle: ((id: string) => void) | null = null;
90
+ toggle(id) {
91
+ if (typeof this.onToggle === "function") this.onToggle(id);
92
+ }
93
+ }
94
+ </script>
@@ -6,7 +6,9 @@
6
6
  {{ brand }}
7
7
  </a>
8
8
  <nav :aria-label="navLabel" class="vmz-ui-app-shell__nav">
9
- <a v-for="item in nav" :key="item.id" :href="item.href" :data-vmz-nav="item.id" :data-active="item.id === activeId ? 'true' : 'false'" :aria-current="item.id === activeId ? 'page' : null" class="vmz-ui-app-shell__nav-item">{{ item.title }}</a>
9
+ <a v-for="item in nav" :key="item.id" :href="item.href" :data-vmz-nav="item.id" :data-active="item.id === activeId ? 'true' : 'false'" :aria-current="item.id === activeId ? 'page' : null" class="vmz-ui-app-shell__nav-item">
10
+ {{ item.title }}
11
+ </a>
10
12
  </nav>
11
13
  <div class="vmz-ui-app-shell__actions">
12
14
  <slot name="actions" />
@@ -159,7 +161,8 @@
159
161
  <script client>
160
162
  /**
161
163
  * VMZ UI — AppShell (Commercial composition).
162
- * Landmark header/nav + main slot. Parent supplies nav items; no private theme.
164
+ * Landmark header/nav + main slot. `nav` stays flat link-only; non-nav controls
165
+ * go into the named `actions` slot. Parent supplies copy; no private theme.
163
166
  */
164
167
  export default class AppShell {
165
168
  public brand: string = "VMZ";
@@ -1,154 +1,154 @@
1
- <template>
2
- <div data-vmz-ui="autocomplete" :data-invalid="invalidAttr" :data-open="openAttr" class="vmz-ui-autocomplete">
3
- <label v-if="label" :for="controlId" class="vmz-ui-autocomplete__label">
4
- {{ label }}
5
- </label>
6
- <input :id="controlId" type="text" role="combobox" autocomplete="off" aria-autocomplete="list" :aria-expanded="openAttr" :aria-controls="listId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" :value="value" :disabled="disabled" @input="onDomInput" :onKeyDown="onKeyDown" class="vmz-ui-autocomplete__control" />
7
- <ul v-if="listOpen" :id="listId" role="listbox" data-vmz-autocomplete="list" @click="onListClick" class="vmz-ui-autocomplete__list">
8
- <li v-for="opt in options" :key="opt.value" role="option" :data-vmz-option="opt.value" tabindex="-1" class="vmz-ui-autocomplete__option">
9
- {{ opt.label }}
10
- </li>
11
- </ul>
12
- <p v-if="showEmpty" data-vmz-autocomplete="empty" class="vmz-ui-autocomplete__empty">
13
- {{ emptyText }}
14
- </p>
15
- <p v-if="description" :id="descriptionId" class="vmz-ui-autocomplete__description">
16
- {{ description }}
17
- </p>
18
- <p v-if="error" :id="errorId" role="alert" class="vmz-ui-autocomplete__error">
19
- {{ error }}
20
- </p>
21
- </div>
22
- </template>
23
- <style>
24
- .vmz-ui-autocomplete {
25
- position: relative;
26
- display: flex;
27
- flex-direction: column;
28
- gap: 0.375rem;
29
- max-width: 28rem;
30
- }
31
-
32
- .vmz-ui-autocomplete__label {
33
- font: inherit;
34
- font-weight: 600;
35
- }
36
-
37
- .vmz-ui-autocomplete__control {
38
- font: inherit;
39
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
40
- border: 1px solid var(--vmz-line-default);
41
- border-radius: var(--vmz-radius-md, 6px);
42
- background: transparent;
43
- color: inherit;
44
- outline: none;
45
- }
46
-
47
- .vmz-ui-autocomplete[data-invalid="true"] .vmz-ui-autocomplete__control {
48
- border-color: var(--vmz-status-danger-accent);
49
- }
50
-
51
- .vmz-ui-autocomplete__control:focus-visible {
52
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
53
- }
54
-
55
- .vmz-ui-autocomplete__control:disabled {
56
- opacity: 0.55;
57
- cursor: not-allowed;
58
- }
59
-
60
- .vmz-ui-autocomplete__list {
61
- list-style: none;
62
- margin: 0;
63
- padding: 0.25rem 0;
64
- border: 1px solid var(--vmz-line-default);
65
- border-radius: var(--vmz-radius-md, 6px);
66
- background: var(--vmz-surface-paper);
67
- max-height: 12rem;
68
- overflow: auto;
69
- }
70
-
71
- .vmz-ui-autocomplete__option {
72
- margin: 0;
73
- padding: 0.5rem 0.75rem;
74
- cursor: pointer;
75
- font-weight: 600;
76
- color: var(--vmz-text-ink);
77
- }
78
-
79
- .vmz-ui-autocomplete__option:hover,
80
- .vmz-ui-autocomplete__option:focus {
81
- background: var(--vmz-surface-mist);
82
- outline: none;
83
- }
84
-
85
- .vmz-ui-autocomplete__empty {
86
- margin: 0;
87
- color: var(--vmz-text-steel);
88
- font-size: 0.9em;
89
- }
90
-
91
- .vmz-ui-autocomplete__description {
92
- margin: 0;
93
- opacity: 0.75;
94
- font-size: 0.9em;
95
- }
96
-
97
- .vmz-ui-autocomplete__error {
98
- margin: 0;
99
- color: var(--vmz-status-danger-foreground);
100
- font-size: 0.9em;
101
- }
102
- </style>
103
- <script client>
104
- /**
105
- * VMZ UI — Autocomplete.
106
- * Parent owns value, listOpen, and filtered options — no parallel query/store.
107
- * Selection notifies via onSelect(value); Escape closes via onClose.
108
- */
109
- export default class Autocomplete {
110
- public label: string = "";
111
- public description: string = "";
112
- public error: string = "";
113
- public value: string = "";
114
- public options: Array<{
115
- value: string;
116
- label: string;
117
- }> = [];
118
- public listOpen: boolean = false;
119
- public openAttr: string = "false";
120
- public showEmpty: boolean = false;
121
- public emptyText: string = "No matches";
122
- public disabled: boolean = false;
123
- public invalidAttr: string = "false";
124
- public controlId: string = "vmz-autocomplete";
125
- public listId: string = "vmz-autocomplete-list";
126
- public descriptionId: string = "vmz-autocomplete-desc";
127
- public errorId: string = "vmz-autocomplete-err";
128
- public describedBy: string = "";
129
- public onInput: ((e: Event) => void) | null = null;
130
- public onSelect: ((value: string) => void) | null = null;
131
- public onClose: (() => void) | null = null;
132
- onDomInput(e: Event) {
133
- this.emit("input", e);
134
- if (typeof this.onInput === "function") this.onInput(e);
135
- }
136
- onListClick(ev) {
137
- if (this.disabled) return;
138
- const t = ev && ev.target;
139
- if (!t || typeof t.closest !== "function") return;
140
- const opt = t.closest("[data-vmz-option]");
141
- if (!opt) return;
142
- const next = opt.getAttribute("data-vmz-option") || "";
143
- if (!next) return;
144
- this.emit("select", next);
145
- if (typeof this.onSelect === "function") this.onSelect(next);
146
- }
147
- onKeyDown(ev) {
148
- if (!ev || ev.key !== "Escape") return;
149
- ev.preventDefault();
150
- this.emit("close");
151
- if (typeof this.onClose === "function") this.onClose();
152
- }
153
- }
154
- </script>
1
+ <template>
2
+ <div data-vmz-ui="autocomplete" :data-invalid="invalidAttr" :data-open="openAttr" class="vmz-ui-autocomplete">
3
+ <label v-if="label" :for="controlId" class="vmz-ui-autocomplete__label">
4
+ {{ label }}
5
+ </label>
6
+ <input :id="controlId" type="text" role="combobox" autocomplete="off" aria-autocomplete="list" :aria-expanded="openAttr" :aria-controls="listId" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" :value="value" :disabled="disabled" @input="onDomInput" :onKeyDown="onKeyDown" class="vmz-ui-autocomplete__control" />
7
+ <ul v-if="listOpen" :id="listId" role="listbox" data-vmz-autocomplete="list" @click="onListClick" class="vmz-ui-autocomplete__list">
8
+ <li v-for="opt in options" :key="opt.value" role="option" :data-vmz-option="opt.value" tabindex="-1" class="vmz-ui-autocomplete__option">
9
+ {{ opt.label }}
10
+ </li>
11
+ </ul>
12
+ <p v-if="showEmpty" data-vmz-autocomplete="empty" class="vmz-ui-autocomplete__empty">
13
+ {{ emptyText }}
14
+ </p>
15
+ <p v-if="description" :id="descriptionId" class="vmz-ui-autocomplete__description">
16
+ {{ description }}
17
+ </p>
18
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-autocomplete__error">
19
+ {{ error }}
20
+ </p>
21
+ </div>
22
+ </template>
23
+ <style>
24
+ .vmz-ui-autocomplete {
25
+ position: relative;
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 0.375rem;
29
+ max-width: 28rem;
30
+ }
31
+
32
+ .vmz-ui-autocomplete__label {
33
+ font: inherit;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .vmz-ui-autocomplete__control {
38
+ font: inherit;
39
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
40
+ border: 1px solid var(--vmz-line-default);
41
+ border-radius: var(--vmz-radius-md, 6px);
42
+ background: transparent;
43
+ color: inherit;
44
+ outline: none;
45
+ }
46
+
47
+ .vmz-ui-autocomplete[data-invalid="true"] .vmz-ui-autocomplete__control {
48
+ border-color: var(--vmz-status-danger-accent);
49
+ }
50
+
51
+ .vmz-ui-autocomplete__control:focus-visible {
52
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
53
+ }
54
+
55
+ .vmz-ui-autocomplete__control:disabled {
56
+ opacity: 0.55;
57
+ cursor: not-allowed;
58
+ }
59
+
60
+ .vmz-ui-autocomplete__list {
61
+ list-style: none;
62
+ margin: 0;
63
+ padding: 0.25rem 0;
64
+ border: 1px solid var(--vmz-line-default);
65
+ border-radius: var(--vmz-radius-md, 6px);
66
+ background: var(--vmz-surface-paper);
67
+ max-height: 12rem;
68
+ overflow: auto;
69
+ }
70
+
71
+ .vmz-ui-autocomplete__option {
72
+ margin: 0;
73
+ padding: 0.5rem 0.75rem;
74
+ cursor: pointer;
75
+ font-weight: 600;
76
+ color: var(--vmz-text-ink);
77
+ }
78
+
79
+ .vmz-ui-autocomplete__option:hover,
80
+ .vmz-ui-autocomplete__option:focus {
81
+ background: var(--vmz-surface-mist);
82
+ outline: none;
83
+ }
84
+
85
+ .vmz-ui-autocomplete__empty {
86
+ margin: 0;
87
+ color: var(--vmz-text-steel);
88
+ font-size: 0.9em;
89
+ }
90
+
91
+ .vmz-ui-autocomplete__description {
92
+ margin: 0;
93
+ opacity: 0.75;
94
+ font-size: 0.9em;
95
+ }
96
+
97
+ .vmz-ui-autocomplete__error {
98
+ margin: 0;
99
+ color: var(--vmz-status-danger-foreground);
100
+ font-size: 0.9em;
101
+ }
102
+ </style>
103
+ <script client>
104
+ /**
105
+ * VMZ UI — Autocomplete.
106
+ * Parent owns value, listOpen, and filtered options — no parallel query/store.
107
+ * Selection notifies via onSelect(value); Escape closes via onClose.
108
+ */
109
+ export default class Autocomplete {
110
+ public label: string = "";
111
+ public description: string = "";
112
+ public error: string = "";
113
+ public value: string = "";
114
+ public options: Array<{
115
+ value: string;
116
+ label: string;
117
+ }> = [];
118
+ public listOpen: boolean = false;
119
+ public openAttr: string = "false";
120
+ public showEmpty: boolean = false;
121
+ public emptyText: string = "No matches";
122
+ public disabled: boolean = false;
123
+ public invalidAttr: string = "false";
124
+ public controlId: string = "vmz-autocomplete";
125
+ public listId: string = "vmz-autocomplete-list";
126
+ public descriptionId: string = "vmz-autocomplete-desc";
127
+ public errorId: string = "vmz-autocomplete-err";
128
+ public describedBy: string = "";
129
+ public onInput: ((e: Event) => void) | null = null;
130
+ public onSelect: ((value: string) => void) | null = null;
131
+ public onClose: (() => void) | null = null;
132
+ onDomInput(e: Event) {
133
+ this.emit("input", e);
134
+ if (typeof this.onInput === "function") this.onInput(e);
135
+ }
136
+ onListClick(ev) {
137
+ if (this.disabled) return;
138
+ const t = ev && ev.target;
139
+ if (!t || typeof t.closest !== "function") return;
140
+ const opt = t.closest("[data-vmz-option]");
141
+ if (!opt) return;
142
+ const next = opt.getAttribute("data-vmz-option") || "";
143
+ if (!next) return;
144
+ this.emit("select", next);
145
+ if (typeof this.onSelect === "function") this.onSelect(next);
146
+ }
147
+ onKeyDown(ev) {
148
+ if (!ev || ev.key !== "Escape") return;
149
+ ev.preventDefault();
150
+ this.emit("close");
151
+ if (typeof this.onClose === "function") this.onClose();
152
+ }
153
+ }
154
+ </script>