@vmz/ui 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +2 -0
  2. package/contracts/token-requirements.v0.json +58 -1
  3. package/package.json +5 -2
  4. package/presets/web-surface.v0.json +14 -12
  5. package/src/components/Accordion.vmz +49 -16
  6. package/src/components/Alert.vmz +125 -28
  7. package/src/components/AppShell.vmz +121 -56
  8. package/src/components/Autocomplete.vmz +136 -29
  9. package/src/components/Avatar.vmz +20 -12
  10. package/src/components/Badge.vmz +18 -26
  11. package/src/components/Breadcrumb.vmz +28 -14
  12. package/src/components/BulkActions.vmz +13 -8
  13. package/src/components/Button.vmz +68 -44
  14. package/src/components/Callout.vmz +128 -27
  15. package/src/components/Card.vmz +67 -46
  16. package/src/components/Checkbox.vmz +81 -5
  17. package/src/components/CodeBlock.vmz +59 -27
  18. package/src/components/Collapse.vmz +64 -11
  19. package/src/components/ConsoleShell.vmz +249 -69
  20. package/src/components/Content.vmz +15 -1
  21. package/src/components/DataTable.vmz +123 -23
  22. package/src/components/DatePicker.vmz +171 -79
  23. package/src/components/Descriptions.vmz +90 -0
  24. package/src/components/Dialog.vmz +133 -50
  25. package/src/components/Divider.vmz +8 -1
  26. package/src/components/Drawer.vmz +131 -40
  27. package/src/components/Dropdown.vmz +147 -49
  28. package/src/components/Empty.vmz +56 -14
  29. package/src/components/Field.vmz +78 -23
  30. package/src/components/FilterBar.vmz +34 -4
  31. package/src/components/Flex.vmz +3 -1
  32. package/src/components/Footer.vmz +6 -3
  33. package/src/components/Form.vmz +41 -0
  34. package/src/components/FormItem.vmz +2 -3
  35. package/src/components/Grid.vmz +6 -3
  36. package/src/components/Header.vmz +193 -6
  37. package/src/components/Layout.vmz +3 -1
  38. package/src/components/Link.vmz +20 -9
  39. package/src/components/List.vmz +35 -13
  40. package/src/components/Menu.vmz +198 -25
  41. package/src/components/Message.vmz +167 -0
  42. package/src/components/Notification.vmz +132 -37
  43. package/src/components/Pagination.vmz +186 -20
  44. package/src/components/Popconfirm.vmz +287 -0
  45. package/src/components/Popover.vmz +152 -34
  46. package/src/components/Progress.vmz +16 -5
  47. package/src/components/Prose.vmz +131 -32
  48. package/src/components/QueryForm.vmz +31 -9
  49. package/src/components/RadioGroup.vmz +91 -11
  50. package/src/components/Result.vmz +60 -27
  51. package/src/components/Segmented.vmz +46 -11
  52. package/src/components/Select.vmz +174 -87
  53. package/src/components/Sider.vmz +120 -7
  54. package/src/components/Skeleton.vmz +9 -7
  55. package/src/components/Space.vmz +9 -3
  56. package/src/components/Spinner.vmz +5 -4
  57. package/src/components/Steps.vmz +40 -25
  58. package/src/components/Switch.vmz +48 -15
  59. package/src/components/Table.vmz +130 -25
  60. package/src/components/Tabs.vmz +28 -13
  61. package/src/components/Tag.vmz +29 -12
  62. package/src/components/TextArea.vmz +32 -25
  63. package/src/components/Timeline.vmz +65 -30
  64. package/src/components/Toc.vmz +45 -29
  65. package/src/components/Tooltip.vmz +70 -21
  66. package/src/components/Tree.vmz +48 -25
  67. package/src/components/Typography.vmz +23 -8
  68. package/src/components/Upload.vmz +191 -96
@@ -1,12 +1,34 @@
1
1
  <template>
2
2
  <div data-vmz-ui="menu" :data-vmz-overlay-open="open" class="vmz-ui-menu">
3
3
  <button type="button" data-vmz-menu="trigger" aria-haspopup="menu" :aria-expanded="open ? 'true' : 'false'" :aria-controls="menuId" @click="toggle" :onKeyDown="onTriggerKeyDown" class="vmz-ui-menu__trigger">
4
- {{ label }}
4
+ <span class="vmz-ui-menu__trigger-label">
5
+ {{ label }}
6
+ </span>
7
+ <Icon name="action.chevron-down" size="sm" decorative />
5
8
  </button>
6
9
  <div v-if="open" :id="menuId" role="menu" data-vmz-overlay="menu" data-vmz-overlay-owner="menu" data-vmz-focus="enter" tabindex="-1" :onKeyDown="onMenuKeyDown" class="vmz-ui-menu__panel">
7
- <button v-for="item in items" :key="item.id" type="button" role="menuitem" :data-vmz-menu-item="item.id" @click="onItemClick" class="vmz-ui-menu__item">
8
- {{ item.title }}
9
- </button>
10
+ <div v-for="item in items" :key="item.id" :data-kind="item.kind || 'item'" :data-vmz-menu-row="item.id" class="vmz-ui-menu__row">
11
+ <div v-if="item.kind === 'divider'" role="separator" class="vmz-ui-menu__divider" />
12
+ <div v-else-if="item.kind === 'group'" role="presentation" class="vmz-ui-menu__group">
13
+ {{ item.title }}
14
+ </div>
15
+ <div v-else-if="item.children && item.children.length" :data-open="subOpen(item.id) ? 'true' : 'false'" :data-danger="item.danger ? 'true' : 'false'" class="vmz-ui-menu__submenu">
16
+ <button type="button" role="menuitem" aria-haspopup="menu" :aria-expanded="subOpen(item.id) ? 'true' : 'false'" :disabled="item.disabled" @click="toggleSub(item.id)" class="vmz-ui-menu__item vmz-ui-menu__item--sub">
17
+ <span class="vmz-ui-menu__label">
18
+ {{ item.title }}
19
+ </span>
20
+ <Icon name="action.chevron-right" size="sm" decorative />
21
+ </button>
22
+ <div v-if="subOpen(item.id)" role="menu" class="vmz-ui-menu__subpanel">
23
+ <button v-for="child in item.children" :key="child.id" type="button" role="menuitem" :data-vmz-menu-item="child.id" :data-danger="child.danger ? 'true' : 'false'" :data-active="child.active ? 'true' : 'false'" :disabled="child.disabled" @click="onItemClick" class="vmz-ui-menu__item">
24
+ {{ child.title }}
25
+ </button>
26
+ </div>
27
+ </div>
28
+ <button v-else type="button" role="menuitem" :data-vmz-menu-item="item.id" :data-danger="item.danger ? 'true' : 'false'" :data-active="item.active ? 'true' : 'false'" :disabled="item.disabled" @click="onItemClick" class="vmz-ui-menu__item">
29
+ {{ item.title }}
30
+ </button>
31
+ </div>
10
32
  </div>
11
33
  </div>
12
34
  </template>
@@ -17,17 +39,24 @@
17
39
  }
18
40
 
19
41
  .vmz-ui-menu__trigger {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ gap: 6px;
20
45
  font: inherit;
21
- font-size: 0.875rem;
46
+ font-size: 14px;
22
47
  font-weight: 400;
23
- min-height: var(--vmz-control-height, 2rem);
24
- padding: 0.25rem 0.75rem;
48
+ line-height: 22px;
49
+ min-height: var(--vmz-control-height, 32px);
50
+ padding: 0 12px;
25
51
  border: 1px solid var(--vmz-line-default);
26
- border-radius: var(--vmz-radius-md, 6px);
52
+ border-radius: var(--vmz-radius-md);
27
53
  cursor: pointer;
28
- background: var(--vmz-surface-paper, #fff);
54
+ background: var(--vmz-surface-paper);
29
55
  color: var(--vmz-text-ink);
30
56
  outline: none;
57
+ transition:
58
+ color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
59
+ border-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
31
60
  }
32
61
 
33
62
  .vmz-ui-menu__trigger:hover {
@@ -39,61 +68,196 @@
39
68
  box-shadow: 0 0 0 3px var(--vmz-focus-ring);
40
69
  }
41
70
 
71
+ .vmz-ui-menu__trigger-label:empty {
72
+ display: none;
73
+ }
74
+
75
+ .vmz-ui-menu[data-vmz-overlay-open="true"] .vmz-ui-menu__trigger {
76
+ border-color: var(--vmz-action-primary-background);
77
+ }
78
+
79
+ .vmz-ui-menu[data-vmz-overlay-open="true"] .vmz-ui-menu__trigger .vmz-ui-icon {
80
+ transform: rotate(180deg);
81
+ }
82
+
42
83
  .vmz-ui-menu__panel {
43
84
  position: absolute;
44
85
  z-index: 30;
45
- top: calc(100% + 0.25rem);
86
+ top: calc(100% + 4px);
46
87
  left: 0;
47
- min-width: 10rem;
48
- padding: 0.25rem;
49
- border: 1px solid var(--vmz-line-default);
50
- border-radius: var(--vmz-radius-md, 6px);
51
- background: var(--vmz-surface-paper, #fff);
88
+ min-width: 176px;
89
+ max-height: 320px;
90
+ overflow: auto;
91
+ box-sizing: border-box;
92
+ padding: 4px;
93
+ border: 1px solid var(--vmz-line-subtle);
94
+ border-radius: var(--vmz-radius-lg);
95
+ background: var(--vmz-surface-paper);
52
96
  color: var(--vmz-text-ink);
53
97
  box-shadow: var(--vmz-shadow-popup);
54
98
  outline: none;
99
+ animation: vmz-ui-menu-in var(--vmz-motion-overlay-duration)
100
+ var(--vmz-motion-overlay-easing);
55
101
  }
56
102
 
57
103
  .vmz-ui-menu__panel:focus-visible {
58
104
  box-shadow: 0 0 0 3px var(--vmz-focus-ring);
59
105
  }
60
106
 
107
+ .vmz-ui-menu__row {
108
+ display: contents;
109
+ }
110
+
61
111
  .vmz-ui-menu__item {
62
- display: block;
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 8px;
63
115
  width: 100%;
116
+ min-height: 32px;
117
+ box-sizing: border-box;
64
118
  text-align: left;
65
119
  font: inherit;
66
- padding: 0.375rem 0.625rem;
120
+ font-size: 14px;
121
+ line-height: 22px;
122
+ padding: 0 12px;
67
123
  border: 0;
68
- border-radius: var(--vmz-radius-md, 6px);
124
+ border-radius: var(--vmz-radius-md);
69
125
  background: transparent;
70
- color: inherit;
126
+ color: var(--vmz-text-ink);
71
127
  cursor: pointer;
72
128
  outline: none;
129
+ transition:
130
+ color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
131
+ background-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
73
132
  }
74
133
 
75
- .vmz-ui-menu__item:hover,
76
- .vmz-ui-menu__item:focus-visible {
77
- background: color-mix(in srgb, var(--vmz-action-primary-background) 18%, transparent);
134
+ .vmz-ui-menu__item:hover {
135
+ background: var(--vmz-surface-mist);
78
136
  }
79
137
 
80
138
  .vmz-ui-menu__item:focus-visible {
81
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
139
+ background: var(--vmz-surface-mist);
140
+ box-shadow: 0 0 0 2px var(--vmz-focus-ring) inset;
141
+ }
142
+
143
+ .vmz-ui-menu__item[data-active="true"] {
144
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 10%, transparent);
145
+ box-shadow: inset 2px 0 0 var(--vmz-action-primary-background);
146
+ color: var(--vmz-text-ink);
147
+ }
148
+
149
+ .vmz-ui-menu__item[data-active="true"]:hover,
150
+ .vmz-ui-menu__item[data-active="true"]:focus-visible {
151
+ background: color-mix(in srgb, var(--vmz-action-primary-background) 16%, transparent);
152
+ }
153
+
154
+ .vmz-ui-menu__item[data-danger="true"] {
155
+ color: var(--vmz-status-danger-foreground);
156
+ }
157
+
158
+ .vmz-ui-menu__item[data-danger="true"]:hover,
159
+ .vmz-ui-menu__item[data-danger="true"]:focus-visible {
160
+ background: color-mix(in srgb, var(--vmz-status-danger-accent) 12%, transparent);
161
+ }
162
+
163
+ .vmz-ui-menu__item:disabled {
164
+ opacity: 0.45;
165
+ cursor: not-allowed;
166
+ background: transparent;
167
+ color: var(--vmz-text-tertiary);
168
+ }
169
+
170
+ .vmz-ui-menu__label {
171
+ flex: 1 1 auto;
172
+ min-width: 0;
173
+ overflow: hidden;
174
+ text-overflow: ellipsis;
175
+ white-space: nowrap;
176
+ }
177
+
178
+ .vmz-ui-menu__item--sub .vmz-ui-icon {
179
+ flex: 0 0 auto;
180
+ color: var(--vmz-text-tertiary);
181
+ }
182
+
183
+ .vmz-ui-menu__group {
184
+ padding: 8px 12px 4px;
185
+ font-size: 12px;
186
+ font-weight: 600;
187
+ line-height: 20px;
188
+ color: var(--vmz-text-tertiary);
189
+ }
190
+
191
+ .vmz-ui-menu__divider {
192
+ height: 1px;
193
+ margin: 4px 8px;
194
+ background: var(--vmz-line-subtle);
195
+ }
196
+
197
+ .vmz-ui-menu__submenu {
198
+ position: relative;
199
+ }
200
+
201
+ .vmz-ui-menu__submenu[data-open="true"] > .vmz-ui-menu__item--sub {
202
+ background: var(--vmz-surface-mist);
203
+ }
204
+
205
+ .vmz-ui-menu__subpanel {
206
+ display: flex;
207
+ flex-direction: column;
208
+ margin: 2px 0 4px;
209
+ padding: 4px 0 4px 12px;
210
+ border-left: 1px solid var(--vmz-line-subtle);
211
+ }
212
+
213
+ @keyframes vmz-ui-menu-in {
214
+ from {
215
+ opacity: 0;
216
+ transform: translateY(-4px);
217
+ }
218
+ to {
219
+ opacity: 1;
220
+ transform: none;
221
+ }
222
+ }
223
+
224
+ @media (prefers-reduced-motion: reduce) {
225
+ .vmz-ui-menu__panel,
226
+ .vmz-ui-menu__trigger,
227
+ .vmz-ui-menu__item {
228
+ animation: none;
229
+ transition: none;
230
+ }
82
231
  }
83
232
  </style>
84
233
  <script client>
85
234
  /**
86
235
  * VMZ UI — Menu (UI2).
87
236
  * Parent owns `open`; Escape / choose closes via onClose; focus restore to trigger.
237
+ *
238
+ * Item model: `{ id, title, kind?: "item"|"group"|"divider", danger?, disabled?,
239
+ * active?, children?: MenuItem[] }`. Submenus render a nested panel with a
240
+ * chevron-right affordance.
88
241
  */
89
242
  export default class Menu {
90
- public label: string = "Menu";
243
+ public label: string = "";
91
244
  public open: boolean = false;
92
245
  public menuId: string = "vmz-menu";
93
- /** @type {{ id: string, title: string }[]} */
246
+ /** @type {{ id: string, title: string, kind?: string, danger?: boolean, disabled?: boolean, active?: boolean, children?: { id: string, title: string, danger?: boolean, disabled?: boolean, active?: boolean }[] }[]} */
94
247
  public items: {
95
248
  id: string;
96
249
  title: string;
250
+ kind?: string;
251
+ danger?: boolean;
252
+ disabled?: boolean;
253
+ active?: boolean;
254
+ children?: {
255
+ id: string;
256
+ title: string;
257
+ danger?: boolean;
258
+ disabled?: boolean;
259
+ active?: boolean;
260
+ }[];
97
261
  }[] = [];
98
262
  public onOpenChange: ((open: boolean) => void) | null = null;
99
263
  public onSelect: ((id: string) => void) | null = null;
@@ -102,6 +266,7 @@ export default class Menu {
102
266
  _docKeyHandler = null;
103
267
  _docPointerHandler = null;
104
268
  _observer = null;
269
+ _subOpen: Record<string, boolean> = {};
105
270
  async onMount() {
106
271
  if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
107
272
  const root = this.__vmzDomRoot;
@@ -122,6 +287,12 @@ export default class Menu {
122
287
  this._panel = null;
123
288
  this._restoreFocus = null;
124
289
  }
290
+ subOpen(id) {
291
+ return !!this._subOpen[id];
292
+ }
293
+ toggleSub(id) {
294
+ this._subOpen = { ...this._subOpen, [id]: !this._subOpen[id] };
295
+ }
125
296
  toggle() {
126
297
  if (typeof this.onOpenChange === "function") this.onOpenChange(!this.open);
127
298
  }
@@ -134,6 +305,7 @@ export default class Menu {
134
305
  }
135
306
  onItemClick(ev) {
136
307
  const el = ev?.currentTarget;
308
+ if (el && typeof el.getAttribute === "function" && el.disabled) return;
137
309
  const id =
138
310
  el && typeof el.getAttribute === "function"
139
311
  ? el.getAttribute("data-vmz-menu-item")
@@ -223,6 +395,7 @@ export default class Menu {
223
395
  _exitFocus() {
224
396
  this._teardownDoc();
225
397
  this._panel = null;
398
+ this._subOpen = {};
226
399
  const restore = this._restoreFocus;
227
400
  this._restoreFocus = null;
228
401
  if (restore && typeof restore.focus === "function" && document.contains(restore)) {
@@ -0,0 +1,167 @@
1
+ <template>
2
+ <span data-vmz-ui="message" :data-tone="tone" :role="roleAttr()" class="vmz-ui-message">
3
+ <span aria-hidden="true" class="vmz-ui-message__icon">
4
+ <Icon :name="toneIcon()" size="md" decorative />
5
+ </span>
6
+ <span class="vmz-ui-message__content">
7
+ <span class="vmz-ui-message__title">
8
+ {{ title }}
9
+ </span>
10
+ <span v-if="description" class="vmz-ui-message__description">
11
+ {{ description }}
12
+ </span>
13
+ </span>
14
+ <button v-if="closable" type="button" :aria-label="closeLabel || title" @click="dismiss" class="vmz-ui-message__close">
15
+ <Icon name="action.close" size="sm" decorative />
16
+ </button>
17
+ </span>
18
+ </template>
19
+ <style>
20
+ .vmz-ui-message {
21
+ position: fixed;
22
+ top: 1.25rem;
23
+ left: 50%;
24
+ z-index: 45;
25
+ transform: translateX(-50%);
26
+ box-sizing: border-box;
27
+ display: flex;
28
+ align-items: flex-start;
29
+ gap: var(--vmz-space-2, 0.5rem);
30
+ width: max-content;
31
+ max-width: min(420px, calc(100vw - 2rem));
32
+ padding: 0.6rem 0.85rem;
33
+ border: 1px solid var(--vmz-line-subtle);
34
+ border-radius: var(--vmz-radius-lg);
35
+ background: var(--vmz-surface-paper);
36
+ color: var(--vmz-text-ink);
37
+ box-shadow: var(--vmz-shadow-popup);
38
+ animation: vmz-ui-message-in var(--vmz-motion-overlay-duration) var(--vmz-motion-overlay-easing);
39
+ }
40
+
41
+ .vmz-ui-message__icon {
42
+ flex: 0 0 auto;
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 1.5rem;
47
+ color: var(--vmz-status-info-accent);
48
+ }
49
+
50
+ .vmz-ui-message[data-tone="success"] .vmz-ui-message__icon {
51
+ color: var(--vmz-status-success-accent);
52
+ }
53
+
54
+ .vmz-ui-message[data-tone="warning"] .vmz-ui-message__icon {
55
+ color: var(--vmz-status-warning-accent);
56
+ }
57
+
58
+ .vmz-ui-message[data-tone="danger"] .vmz-ui-message__icon {
59
+ color: var(--vmz-status-danger-accent);
60
+ }
61
+
62
+ .vmz-ui-message__content {
63
+ flex: 1 1 auto;
64
+ display: block;
65
+ min-width: 0;
66
+ padding-top: 0.1rem;
67
+ }
68
+
69
+ .vmz-ui-message__title {
70
+ display: block;
71
+ font-size: 0.875rem;
72
+ font-weight: 500;
73
+ line-height: 1.4;
74
+ color: var(--vmz-text-ink);
75
+ }
76
+
77
+ .vmz-ui-message__description {
78
+ display: block;
79
+ margin-top: 1px;
80
+ font-size: 0.8125rem;
81
+ line-height: 1.4;
82
+ color: var(--vmz-text-steel);
83
+ }
84
+
85
+ .vmz-ui-message__close {
86
+ flex: 0 0 auto;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 1.5rem;
91
+ height: 1.5rem;
92
+ padding: 0;
93
+ border: 1px solid transparent;
94
+ border-radius: var(--vmz-radius-sm);
95
+ background: transparent;
96
+ color: var(--vmz-text-tertiary);
97
+ cursor: pointer;
98
+ outline: none;
99
+ transition: color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
100
+ }
101
+
102
+ .vmz-ui-message__close:hover {
103
+ color: var(--vmz-text-ink);
104
+ }
105
+
106
+ .vmz-ui-message__close:focus-visible {
107
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
108
+ }
109
+
110
+ @keyframes vmz-ui-message-in {
111
+ from {
112
+ opacity: 0;
113
+ transform: translateX(-50%) translateY(-8px);
114
+ }
115
+ to {
116
+ opacity: 1;
117
+ transform: translateX(-50%) translateY(0);
118
+ }
119
+ }
120
+
121
+ @media (prefers-reduced-motion: reduce) {
122
+ .vmz-ui-message {
123
+ animation: none;
124
+ }
125
+ }
126
+ </style>
127
+ <script client>
128
+ /**
129
+ * VMZ UI — Message.
130
+ * Transient confirmation toast for completed operations (render with `v-if`).
131
+ * Parent owns visibility. `duration > 0` auto-dismisses and emits `close`;
132
+ * the caller decides what side effects follow.
133
+ */
134
+ export default class Message {
135
+ public title: string = "";
136
+ public description: string = "";
137
+ public tone: string = "success";
138
+ public closable: boolean = false;
139
+ public closeLabel: string = "";
140
+ public duration: number = 3200;
141
+ public onClose: (() => void) | null = null;
142
+ _timer = null;
143
+ onMount() {
144
+ const ms = Number(this.duration);
145
+ if (!Number.isFinite(ms) || ms <= 0) return;
146
+ this._timer = setTimeout(() => this.dismiss(), ms);
147
+ }
148
+ onDestroy() {
149
+ if (this._timer) clearTimeout(this._timer);
150
+ this._timer = null;
151
+ }
152
+ roleAttr() {
153
+ return String(this.tone).toLowerCase() === "danger" ? "alert" : "status";
154
+ }
155
+ toneIcon() {
156
+ const tone = String(this.tone || "").toLowerCase();
157
+ if (tone === "success" || tone === "warning" || tone === "danger") return `status.${tone}`;
158
+ return "status.info";
159
+ }
160
+ dismiss() {
161
+ if (this._timer) clearTimeout(this._timer);
162
+ this._timer = null;
163
+ this.emit("close");
164
+ if (typeof this.onClose === "function") this.onClose();
165
+ }
166
+ }
167
+ </script>
@@ -1,80 +1,175 @@
1
1
  <template>
2
2
  <div data-vmz-ui="notification" :data-tone="tone" :role="roleName" class="vmz-ui-notification">
3
- <p v-if="title" class="vmz-ui-notification__title">
4
- {{ title }}
5
- </p>
6
- <p v-if="description" class="vmz-ui-notification__description">
7
- {{ description }}
8
- </p>
9
- <div class="vmz-ui-notification__actions">
10
- <slot />
3
+ <span aria-hidden="true" class="vmz-ui-notification__icon">
4
+ <Icon :name="toneIcon()" size="md" decorative />
5
+ </span>
6
+ <div class="vmz-ui-notification__content">
7
+ <p v-if="title" class="vmz-ui-notification__title">
8
+ {{ title }}
9
+ </p>
10
+ <p v-if="description" class="vmz-ui-notification__description">
11
+ {{ description }}
12
+ </p>
13
+ <div class="vmz-ui-notification__actions">
14
+ <slot />
15
+ </div>
11
16
  </div>
17
+ <button v-if="closable" type="button" :aria-label="closeLabel" @click="dismiss" class="vmz-ui-notification__close">
18
+ <Icon name="action.close" size="md" decorative />
19
+ </button>
12
20
  </div>
13
21
  </template>
14
22
  <style>
15
23
  .vmz-ui-notification {
16
24
  display: flex;
17
- flex-direction: column;
18
- gap: var(--vmz-density-control-gap);
19
- padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
20
- border: 1px solid var(--vmz-status-info-accent);
21
- border-radius: var(--vmz-radius-md, 6px);
22
- background: color-mix(in srgb, var(--vmz-status-info-accent) 10%, transparent);
23
- color: var(--vmz-status-info-foreground);
25
+ align-items: flex-start;
26
+ gap: var(--vmz-space-2, 0.5rem);
27
+ padding: var(--vmz-space-4, 1rem);
28
+ border: 1px solid var(--vmz-line-default, var(--vmz-line-strong));
29
+ border-radius: var(--vmz-radius-lg, 8px);
30
+ background: var(--vmz-surface-paper);
31
+ color: var(--vmz-text-ink);
32
+ box-shadow: var(--vmz-shadow-popup, none);
33
+ animation: vmz-ui-notification-in var(--vmz-motion-overlay-duration, 160ms)
34
+ var(--vmz-motion-overlay-easing, cubic-bezier(0.2, 0, 0, 1));
24
35
  }
25
36
 
26
- .vmz-ui-notification[data-tone="info"] {
27
- border-color: var(--vmz-status-info-accent);
28
- background: color-mix(in srgb, var(--vmz-status-info-accent) 10%, transparent);
29
- color: var(--vmz-status-info-foreground);
37
+ .vmz-ui-notification[data-tone="info"] .vmz-ui-notification__icon {
38
+ color: var(--vmz-status-info-accent);
30
39
  }
31
40
 
32
- .vmz-ui-notification[data-tone="success"] {
33
- border-color: var(--vmz-status-success-accent);
34
- background: color-mix(in srgb, var(--vmz-status-success-accent) 10%, transparent);
35
- color: var(--vmz-status-success-foreground);
41
+ .vmz-ui-notification[data-tone="success"] .vmz-ui-notification__icon {
42
+ color: var(--vmz-status-success-accent);
36
43
  }
37
44
 
38
- .vmz-ui-notification[data-tone="warning"] {
39
- border-color: var(--vmz-status-warning-accent);
40
- background: color-mix(in srgb, var(--vmz-status-warning-accent) 10%, transparent);
41
- color: var(--vmz-status-warning-foreground);
45
+ .vmz-ui-notification[data-tone="warning"] .vmz-ui-notification__icon {
46
+ color: var(--vmz-status-warning-accent);
42
47
  }
43
48
 
44
- .vmz-ui-notification[data-tone="danger"] {
45
- border-color: var(--vmz-status-danger-accent);
46
- background: color-mix(in srgb, var(--vmz-status-danger-accent) 10%, transparent);
47
- color: var(--vmz-status-danger-foreground);
49
+ .vmz-ui-notification[data-tone="danger"] .vmz-ui-notification__icon {
50
+ color: var(--vmz-status-danger-accent);
48
51
  }
49
52
 
50
- :where([data-density="compact"]) .vmz-ui-notification {
51
- gap: var(--vmz-density-compact-gap);
52
- padding: var(--vmz-density-compact-padding-y) var(--vmz-density-compact-padding-x);
53
+ .vmz-ui-notification__icon {
54
+ flex: 0 0 auto;
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ height: 1.5rem;
59
+ }
60
+
61
+ .vmz-ui-notification__content {
62
+ flex: 1 1 auto;
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--vmz-space-1, 0.25rem);
66
+ min-width: 0;
53
67
  }
54
68
 
55
69
  .vmz-ui-notification__title {
56
70
  margin: 0;
57
- font-weight: 700;
71
+ font-size: 0.9375rem;
72
+ font-weight: 600;
73
+ line-height: 1.4;
74
+ color: var(--vmz-text-ink);
58
75
  }
59
76
 
60
77
  .vmz-ui-notification__description {
61
78
  margin: 0;
62
- opacity: 0.9;
79
+ font-size: 0.875rem;
80
+ line-height: 1.55;
81
+ color: var(--vmz-text-steel);
82
+ }
83
+
84
+ .vmz-ui-notification__actions {
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ gap: var(--vmz-space-2, 0.5rem);
88
+ margin-top: var(--vmz-space-1, 0.25rem);
63
89
  }
64
90
 
65
91
  .vmz-ui-notification__actions:empty {
66
92
  display: none;
67
93
  }
94
+
95
+ .vmz-ui-notification__close {
96
+ flex: 0 0 auto;
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 1.5rem;
101
+ height: 1.5rem;
102
+ padding: 0;
103
+ border: 1px solid transparent;
104
+ border-radius: var(--vmz-radius-sm, 4px);
105
+ background: transparent;
106
+ color: var(--vmz-text-tertiary);
107
+ cursor: pointer;
108
+ outline: none;
109
+ transition:
110
+ color var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, cubic-bezier(0.2, 0, 0, 1)),
111
+ background-color var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, cubic-bezier(0.2, 0, 0, 1));
112
+ }
113
+
114
+ .vmz-ui-notification__close:hover {
115
+ color: var(--vmz-text-ink);
116
+ background: var(--vmz-surface-mist);
117
+ }
118
+
119
+ .vmz-ui-notification__close:focus-visible {
120
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
121
+ }
122
+
123
+ @keyframes vmz-ui-notification-in {
124
+ from {
125
+ opacity: 0;
126
+ transform: translateY(-0.5rem);
127
+ }
128
+ to {
129
+ opacity: 1;
130
+ transform: none;
131
+ }
132
+ }
133
+
134
+ @media (prefers-reduced-motion: reduce) {
135
+ .vmz-ui-notification {
136
+ animation: none;
137
+ }
138
+ }
139
+
140
+ :where([data-density="compact"]) .vmz-ui-notification {
141
+ gap: var(--vmz-space-2, 0.5rem);
142
+ padding: var(--vmz-space-3, 0.75rem);
143
+ }
144
+
145
+ :where([data-density="dense"]) .vmz-ui-notification {
146
+ gap: var(--vmz-space-1, 0.25rem);
147
+ padding: var(--vmz-space-3, 0.75rem);
148
+ }
68
149
  </style>
69
150
  <script client>
70
151
  /**
71
152
  * VMZ UI — Notification (UI4 Commercial Surface).
72
- * Ephemeral feedback surface; tones share status.* tokens with Alert.
153
+ * Ephemeral feedback surface: paper + popup shadow + tone accent icon; 160ms enter.
73
154
  */
74
155
  export default class Notification {
75
156
  public title: string = "";
76
157
  public description: string = "";
77
158
  public tone: string = "info";
78
159
  public roleName: string = "status";
160
+ public closable: boolean = false;
161
+ public closeLabel: string = "";
162
+ public onClose: (() => void) | null = null;
163
+
164
+ toneIcon() {
165
+ const tone = String(this.tone || "").toLowerCase();
166
+ if (tone === "success" || tone === "warning" || tone === "danger") return `status.${tone}`;
167
+ return "status.info";
168
+ }
169
+
170
+ dismiss() {
171
+ this.emit("close");
172
+ if (typeof this.onClose === "function") this.onClose();
173
+ }
79
174
  }
80
175
  </script>