@vmz/ui 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +53 -51
  2. package/contracts/token-requirements.v0.json +58 -1
  3. package/package.json +5 -2
  4. package/presets/web-surface.v0.json +14 -12
  5. package/src/components/Accordion.vmz +127 -94
  6. package/src/components/Alert.vmz +125 -28
  7. package/src/components/AppShell.vmz +125 -57
  8. package/src/components/Autocomplete.vmz +261 -154
  9. package/src/components/Avatar.vmz +20 -12
  10. package/src/components/Badge.vmz +62 -70
  11. package/src/components/Breadcrumb.vmz +86 -72
  12. package/src/components/BulkActions.vmz +56 -51
  13. package/src/components/Button.vmz +237 -211
  14. package/src/components/Callout.vmz +178 -77
  15. package/src/components/Card.vmz +141 -120
  16. package/src/components/Checkbox.vmz +131 -55
  17. package/src/components/CodeBlock.vmz +77 -30
  18. package/src/components/Collapse.vmz +64 -11
  19. package/src/components/ConsoleShell.vmz +405 -251
  20. package/src/components/Content.vmz +15 -1
  21. package/src/components/DataTable.vmz +294 -194
  22. package/src/components/DatePicker.vmz +766 -674
  23. package/src/components/Descriptions.vmz +90 -0
  24. package/src/components/Dialog.vmz +137 -38
  25. package/src/components/Divider.vmz +8 -1
  26. package/src/components/Drawer.vmz +518 -427
  27. package/src/components/Dropdown.vmz +195 -51
  28. package/src/components/Empty.vmz +96 -54
  29. package/src/components/Field.vmz +170 -115
  30. package/src/components/FilterBar.vmz +34 -4
  31. package/src/components/Flex.vmz +3 -1
  32. package/src/components/Footer.vmz +6 -3
  33. package/src/components/Form.vmz +41 -0
  34. package/src/components/FormItem.vmz +78 -79
  35. package/src/components/Grid.vmz +6 -3
  36. package/src/components/Header.vmz +193 -6
  37. package/src/components/Layout.vmz +3 -1
  38. package/src/components/Link.vmz +25 -10
  39. package/src/components/List.vmz +124 -102
  40. package/src/components/Menu.vmz +416 -243
  41. package/src/components/Message.vmz +167 -0
  42. package/src/components/Notification.vmz +132 -37
  43. package/src/components/Pagination.vmz +237 -69
  44. package/src/components/Popconfirm.vmz +287 -0
  45. package/src/components/Popover.vmz +341 -223
  46. package/src/components/Progress.vmz +16 -5
  47. package/src/components/Prose.vmz +180 -81
  48. package/src/components/QueryForm.vmz +31 -9
  49. package/src/components/RadioGroup.vmz +204 -124
  50. package/src/components/Result.vmz +60 -27
  51. package/src/components/Segmented.vmz +86 -51
  52. package/src/components/Select.vmz +620 -533
  53. package/src/components/Sider.vmz +120 -7
  54. package/src/components/Skeleton.vmz +9 -7
  55. package/src/components/Space.vmz +9 -3
  56. package/src/components/Spinner.vmz +5 -4
  57. package/src/components/Steps.vmz +118 -103
  58. package/src/components/Switch.vmz +131 -98
  59. package/src/components/Table.vmz +270 -165
  60. package/src/components/Tabs.vmz +125 -110
  61. package/src/components/Tag.vmz +60 -43
  62. package/src/components/TextArea.vmz +125 -118
  63. package/src/components/Timeline.vmz +111 -76
  64. package/src/components/Toc.vmz +90 -74
  65. package/src/components/Tooltip.vmz +107 -58
  66. package/src/components/Tree.vmz +214 -191
  67. package/src/components/Typography.vmz +23 -8
  68. package/src/components/Upload.vmz +1394 -1289
@@ -1,82 +1,179 @@
1
1
  <template>
2
2
  <div data-vmz-ui="alert" :data-tone="tone" :role="roleName" class="vmz-ui-alert">
3
- <p v-if="title" class="vmz-ui-alert__title">
4
- {{ title }}
5
- </p>
6
- <p v-if="description" class="vmz-ui-alert__description">
7
- {{ description }}
8
- </p>
9
- <div class="vmz-ui-alert__actions">
10
- <slot />
3
+ <span aria-hidden="true" class="vmz-ui-alert__icon">
4
+ <Icon :name="toneIcon()" size="md" decorative />
5
+ </span>
6
+ <div class="vmz-ui-alert__content">
7
+ <p v-if="title" class="vmz-ui-alert__title">
8
+ {{ title }}
9
+ </p>
10
+ <p v-if="description" class="vmz-ui-alert__description">
11
+ {{ description }}
12
+ </p>
13
+ <div class="vmz-ui-alert__actions">
14
+ <slot />
15
+ </div>
11
16
  </div>
17
+ <button v-if="closable" type="button" :aria-label="closeLabel" @click="dismiss" class="vmz-ui-alert__close">
18
+ <Icon name="action.close" size="md" decorative />
19
+ </button>
12
20
  </div>
13
21
  </template>
14
22
  <style>
15
23
  .vmz-ui-alert {
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-left-width: 1px;
22
- border-radius: var(--vmz-radius-md, 6px);
23
- background: color-mix(in srgb, var(--vmz-status-info-accent) 12%, transparent);
25
+ align-items: flex-start;
26
+ gap: var(--vmz-space-2, 0.5rem);
27
+ padding: var(--vmz-space-3, 0.75rem) var(--vmz-space-4, 1rem);
28
+ border: 1px solid var(--vmz-status-info-border, var(--vmz-line-default));
29
+ border-radius: var(--vmz-radius-lg, 8px);
30
+ background: var(--vmz-status-info-background, var(--vmz-surface-mist));
24
31
  color: var(--vmz-status-info-foreground);
25
32
  }
26
33
 
27
34
  .vmz-ui-alert[data-tone="info"] {
28
- border-color: var(--vmz-status-info-accent);
29
- background: color-mix(in srgb, var(--vmz-status-info-accent) 12%, transparent);
35
+ border-color: var(--vmz-status-info-border, var(--vmz-line-default));
36
+ background: var(--vmz-status-info-background, var(--vmz-surface-mist));
30
37
  color: var(--vmz-status-info-foreground);
31
38
  }
32
39
 
33
40
  .vmz-ui-alert[data-tone="success"] {
34
- border-color: var(--vmz-status-success-accent);
35
- background: color-mix(in srgb, var(--vmz-status-success-accent) 12%, transparent);
41
+ border-color: var(--vmz-status-success-border, var(--vmz-line-default));
42
+ background: var(--vmz-status-success-background, var(--vmz-surface-mist));
36
43
  color: var(--vmz-status-success-foreground);
37
44
  }
38
45
 
39
46
  .vmz-ui-alert[data-tone="warning"] {
40
- border-color: var(--vmz-status-warning-accent);
41
- background: color-mix(in srgb, var(--vmz-status-warning-accent) 12%, transparent);
47
+ border-color: var(--vmz-status-warning-border, var(--vmz-line-default));
48
+ background: var(--vmz-status-warning-background, var(--vmz-surface-mist));
42
49
  color: var(--vmz-status-warning-foreground);
43
50
  }
44
51
 
45
52
  .vmz-ui-alert[data-tone="danger"] {
46
- border-color: var(--vmz-status-danger-accent);
47
- background: color-mix(in srgb, var(--vmz-status-danger-accent) 12%, transparent);
53
+ border-color: var(--vmz-status-danger-border, var(--vmz-line-default));
54
+ background: var(--vmz-status-danger-background, var(--vmz-surface-mist));
48
55
  color: var(--vmz-status-danger-foreground);
49
56
  }
50
57
 
51
- :where([data-density="compact"]) .vmz-ui-alert {
52
- gap: var(--vmz-density-compact-gap);
53
- padding: var(--vmz-density-compact-padding-y) var(--vmz-density-compact-padding-x);
58
+ .vmz-ui-alert[data-tone="info"] .vmz-ui-alert__icon {
59
+ color: var(--vmz-status-info-accent);
60
+ }
61
+
62
+ .vmz-ui-alert[data-tone="success"] .vmz-ui-alert__icon {
63
+ color: var(--vmz-status-success-accent);
64
+ }
65
+
66
+ .vmz-ui-alert[data-tone="warning"] .vmz-ui-alert__icon {
67
+ color: var(--vmz-status-warning-accent);
68
+ }
69
+
70
+ .vmz-ui-alert[data-tone="danger"] .vmz-ui-alert__icon {
71
+ color: var(--vmz-status-danger-accent);
72
+ }
73
+
74
+ .vmz-ui-alert__icon {
75
+ flex: 0 0 auto;
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ height: 1.5rem;
80
+ }
81
+
82
+ .vmz-ui-alert__content {
83
+ flex: 1 1 auto;
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: var(--vmz-space-1, 0.25rem);
87
+ min-width: 0;
54
88
  }
55
89
 
56
90
  .vmz-ui-alert__title {
57
91
  margin: 0;
58
92
  font-size: 0.875rem;
59
93
  font-weight: 600;
94
+ line-height: 1.5;
95
+ color: inherit;
60
96
  }
61
97
 
62
98
  .vmz-ui-alert__description {
63
99
  margin: 0;
64
- opacity: 0.9;
100
+ font-size: 0.875rem;
101
+ line-height: 1.55;
102
+ color: var(--vmz-text-steel);
103
+ }
104
+
105
+ .vmz-ui-alert__actions {
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ gap: var(--vmz-space-2, 0.5rem);
109
+ margin-top: var(--vmz-space-1, 0.25rem);
65
110
  }
66
111
 
67
112
  .vmz-ui-alert__actions:empty {
68
113
  display: none;
69
114
  }
115
+
116
+ .vmz-ui-alert__close {
117
+ flex: 0 0 auto;
118
+ display: inline-flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ width: 1.5rem;
122
+ height: 1.5rem;
123
+ padding: 0;
124
+ border: 1px solid transparent;
125
+ border-radius: var(--vmz-radius-sm, 4px);
126
+ background: transparent;
127
+ color: var(--vmz-text-tertiary);
128
+ cursor: pointer;
129
+ outline: none;
130
+ transition:
131
+ color var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, cubic-bezier(0.2, 0, 0, 1)),
132
+ background-color var(--vmz-motion-control-duration, 120ms) var(--vmz-motion-control-easing, cubic-bezier(0.2, 0, 0, 1));
133
+ }
134
+
135
+ .vmz-ui-alert__close:hover {
136
+ color: var(--vmz-text-ink);
137
+ background: var(--vmz-surface-mist);
138
+ }
139
+
140
+ .vmz-ui-alert__close:focus-visible {
141
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
142
+ }
143
+
144
+ :where([data-density="compact"]) .vmz-ui-alert {
145
+ gap: var(--vmz-space-2, 0.5rem);
146
+ padding: var(--vmz-space-2, 0.5rem) var(--vmz-space-3, 0.75rem);
147
+ }
148
+
149
+ :where([data-density="dense"]) .vmz-ui-alert {
150
+ gap: var(--vmz-space-1, 0.25rem);
151
+ padding: var(--vmz-space-1, 0.25rem) var(--vmz-space-2, 0.5rem);
152
+ }
70
153
  </style>
71
154
  <script client>
72
155
  /**
73
156
  * VMZ UI — Alert (UI4 status tone matrix).
74
- * tone → status.{info|success|warning|danger}.* tokens from Style Theme.
157
+ * tone → status.{info|success|warning|danger}.{accent|foreground|background|border}.
75
158
  */
76
159
  export default class Alert {
77
160
  public title: string = "";
78
161
  public description: string = "";
79
162
  public tone: string = "info";
80
163
  public roleName: string = "status";
164
+ public closable: boolean = false;
165
+ public closeLabel: string = "";
166
+ public onClose: (() => void) | null = null;
167
+
168
+ toneIcon() {
169
+ const tone = String(this.tone || "").toLowerCase();
170
+ if (tone === "success" || tone === "warning" || tone === "danger") return `status.${tone}`;
171
+ return "status.info";
172
+ }
173
+
174
+ dismiss() {
175
+ this.emit("close");
176
+ if (typeof this.onClose === "function") this.onClose();
177
+ }
81
178
  }
82
179
  </script>
@@ -1,21 +1,29 @@
1
1
  <template>
2
2
  <div data-vmz-ui="app-shell" class="vmz-ui-app-shell">
3
- <header data-vmz-shell="header" class="vmz-ui-app-shell__header">
3
+ <header data-vmz-shell="header" :onKeyDown="onHeaderKeyDown" class="vmz-ui-app-shell__header">
4
4
  <div class="vmz-ui-app-shell__header-inner">
5
- <a :href="homeHref" class="vmz-ui-app-shell__brand">
5
+ <a v-if="brand" :href="homeHref" class="vmz-ui-app-shell__brand">
6
6
  {{ brand }}
7
7
  </a>
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>
8
+ <nav :id="navId" :aria-label="navLabel" :data-open="menuOpen ? 'true' : null" 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" @click="closeMenu" 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" />
13
15
  </div>
16
+ <button type="button" :aria-label="menuLabel ? menuLabel : navLabel" :aria-expanded="menuOpen ? 'true' : 'false'" :aria-controls="navId" @click="toggleMenu" class="vmz-ui-app-shell__menu">
17
+ <Icon :name="menuOpen ? 'action.close' : 'action.menu'" size="md" decorative />
18
+ </button>
14
19
  </div>
15
20
  </header>
16
21
  <main data-vmz-shell="main" class="vmz-ui-app-shell__main">
17
22
  <slot />
18
23
  </main>
24
+ <footer v-if="footer" data-vmz-shell="footer" class="vmz-ui-app-shell__footer">
25
+ {{ footer }}
26
+ </footer>
19
27
  </div>
20
28
  </template>
21
29
  <style>
@@ -24,26 +32,25 @@
24
32
  flex-direction: column;
25
33
  min-height: 100%;
26
34
  color: var(--vmz-text-ink);
27
- background: transparent;
35
+ background: var(--vmz-surface-page);
36
+ font-family: var(--vmz-font-sans);
28
37
  }
29
38
 
30
39
  .vmz-ui-app-shell__header {
31
40
  position: sticky;
32
41
  top: 0;
33
42
  z-index: 30;
34
- border-bottom: 1px solid var(--vmz-line-subtle, var(--vmz-line-default));
35
- background: color-mix(in srgb, var(--vmz-surface-paper) 88%, transparent);
36
- backdrop-filter: saturate(180%) blur(0.5rem);
37
- -webkit-backdrop-filter: saturate(180%) blur(0.5rem);
43
+ border-bottom: 1px solid var(--vmz-line-subtle);
44
+ background: var(--vmz-surface-paper);
38
45
  }
39
46
 
40
47
  .vmz-ui-app-shell__header-inner {
41
- width: min(var(--vmz-layout-max-width, 75rem), calc(100% - 2rem));
42
- min-height: 3.5rem;
48
+ width: min(var(--vmz-layout-max-width, 75rem), calc(100% - (var(--vmz-layout-gutter, 1rem) * 2)));
49
+ height: var(--vmz-layout-header-height, 4rem);
43
50
  margin: 0 auto;
44
51
  display: flex;
45
52
  align-items: center;
46
- gap: 1.5rem;
53
+ gap: var(--vmz-space-6, 2rem);
47
54
  }
48
55
 
49
56
  .vmz-ui-app-shell__brand {
@@ -51,7 +58,8 @@
51
58
  color: var(--vmz-text-ink);
52
59
  font-family: var(--vmz-font-sans);
53
60
  font-size: 1.0625rem;
54
- font-weight: 650;
61
+ font-weight: 600;
62
+ line-height: 1.25;
55
63
  letter-spacing: -0.01em;
56
64
  text-decoration: none;
57
65
  border-radius: var(--vmz-radius-sm, 4px);
@@ -68,110 +76,170 @@
68
76
  }
69
77
 
70
78
  .vmz-ui-app-shell__nav {
71
- display: flex;
72
- flex: 1 1 auto;
73
- flex-wrap: wrap;
74
- gap: 0.125rem;
79
+ flex: 0 1 auto;
75
80
  min-width: 0;
81
+ height: 100%;
82
+ display: flex;
83
+ align-items: center;
84
+ gap: var(--vmz-space-5, 1.5rem);
76
85
  }
77
86
 
78
87
  .vmz-ui-app-shell__nav-item {
79
88
  position: relative;
80
- padding: 0.375rem 0.75rem;
81
- border-radius: var(--vmz-radius-sm, 4px);
89
+ display: inline-flex;
90
+ align-items: center;
91
+ height: 100%;
92
+ box-sizing: border-box;
93
+ border-bottom: 2px solid transparent;
82
94
  color: var(--vmz-text-steel);
83
- font: inherit;
84
95
  font-size: 0.875rem;
85
96
  font-weight: 500;
86
- line-height: 1.4286;
97
+ line-height: 1;
87
98
  text-decoration: none;
88
99
  white-space: nowrap;
89
100
  outline: none;
90
101
  transition:
91
102
  color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1)),
92
- background-color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
103
+ border-color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
93
104
  }
94
105
 
95
106
  .vmz-ui-app-shell__nav-item:hover {
96
- color: var(--vmz-text-ink);
97
- background: var(--vmz-surface-mist, #fafafa);
98
- }
99
-
100
- .vmz-ui-app-shell__nav-item:focus-visible {
101
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
107
+ color: var(--vmz-action-primary-background);
102
108
  }
103
109
 
104
- /* Current section: ink emphasis plus a hairline marker, no brand fill. */
105
110
  .vmz-ui-app-shell__nav-item[data-active="true"] {
106
- color: var(--vmz-text-ink);
107
- font-weight: 600;
111
+ color: var(--vmz-action-primary-background);
112
+ border-bottom-color: var(--vmz-action-primary-background);
108
113
  }
109
114
 
110
- .vmz-ui-app-shell__nav-item[data-active="true"]::after {
111
- content: "";
112
- position: absolute;
113
- inset: auto 0.75rem 0;
114
- height: 2px;
115
- border-radius: var(--vmz-radius-pill, 999px);
116
- background: var(--vmz-action-primary-background);
115
+ .vmz-ui-app-shell__nav-item:focus-visible {
116
+ border-radius: var(--vmz-radius-sm, 4px);
117
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
117
118
  }
118
119
 
119
120
  .vmz-ui-app-shell__actions {
120
121
  flex: none;
121
122
  display: flex;
122
123
  align-items: center;
123
- gap: 0.5rem;
124
+ gap: var(--vmz-space-2, 0.5rem);
124
125
  margin-left: auto;
125
126
  }
126
127
 
128
+ .vmz-ui-app-shell__menu {
129
+ display: none;
130
+ }
131
+
127
132
  .vmz-ui-app-shell__main {
128
133
  flex: 1;
129
- width: min(var(--vmz-layout-max-width, 75rem), calc(100% - 2rem));
134
+ box-sizing: border-box;
135
+ width: min(var(--vmz-layout-max-width, 75rem), calc(100% - (var(--vmz-layout-gutter, 1rem) * 2)));
130
136
  margin: 0 auto;
131
- padding: 1.75rem 0 3rem;
137
+ padding: var(--vmz-space-6, 2rem) 0 var(--vmz-space-8, 4rem);
132
138
  display: flex;
133
139
  flex-direction: column;
134
- gap: 1rem;
140
+ gap: var(--vmz-space-6, 2rem);
141
+ }
142
+
143
+ .vmz-ui-app-shell__footer {
144
+ border-top: 1px solid var(--vmz-line-subtle);
145
+ background: var(--vmz-surface-paper);
146
+ padding: var(--vmz-space-6, 2rem) var(--vmz-space-5, 1.5rem);
147
+ color: var(--vmz-text-tertiary);
148
+ font-size: 0.8125rem;
149
+ line-height: 1.5;
150
+ text-align: center;
135
151
  }
136
152
 
137
153
  @media (max-width: 720px) {
138
154
  .vmz-ui-app-shell__header-inner {
139
- flex-wrap: wrap;
140
- gap: 0.5rem 0.75rem;
141
- padding: 0.625rem 0;
155
+ height: var(--vmz-layout-header-height-compact, 3.5rem);
156
+ gap: var(--vmz-space-3, 0.75rem);
142
157
  }
143
158
  .vmz-ui-app-shell__nav {
144
- order: 3;
145
- flex-basis: 100%;
146
- overflow-x: auto;
147
- flex-wrap: nowrap;
148
- padding-bottom: 0.125rem;
149
- scrollbar-width: none;
150
- }
151
- .vmz-ui-app-shell__nav::-webkit-scrollbar {
152
159
  display: none;
160
+ position: absolute;
161
+ top: 100%;
162
+ left: 0;
163
+ right: 0;
164
+ height: auto;
165
+ flex-direction: column;
166
+ align-items: stretch;
167
+ gap: 0;
168
+ padding: var(--vmz-space-2, 0.5rem) var(--vmz-layout-gutter, 1rem) var(--vmz-space-4, 1rem);
169
+ border-bottom: 1px solid var(--vmz-line-subtle);
170
+ background: var(--vmz-surface-paper);
171
+ box-shadow: var(--vmz-shadow-popup);
172
+ }
173
+ .vmz-ui-app-shell__nav[data-open="true"] {
174
+ display: flex;
175
+ }
176
+ .vmz-ui-app-shell__nav-item {
177
+ height: 2.5rem;
178
+ padding: 0 var(--vmz-space-1, 0.25rem);
179
+ border-bottom: 0;
180
+ }
181
+ .vmz-ui-app-shell__menu {
182
+ flex: none;
183
+ display: inline-flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ width: 2.5rem;
187
+ height: 2.5rem;
188
+ padding: 0;
189
+ border: 0;
190
+ border-radius: var(--vmz-radius-md, 6px);
191
+ background: transparent;
192
+ color: var(--vmz-text-ink);
193
+ cursor: pointer;
194
+ outline: none;
195
+ transition: background-color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
196
+ }
197
+ .vmz-ui-app-shell__menu:hover {
198
+ background: var(--vmz-surface-mist);
199
+ }
200
+ .vmz-ui-app-shell__menu:focus-visible {
201
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
153
202
  }
154
203
  .vmz-ui-app-shell__main {
155
- padding-top: 1.25rem;
204
+ padding: var(--vmz-space-5, 1.5rem) 0 var(--vmz-space-7, 3rem);
205
+ gap: var(--vmz-space-5, 1.5rem);
156
206
  }
157
207
  }
158
208
  </style>
159
209
  <script client>
160
210
  /**
161
211
  * VMZ UI — AppShell (Commercial composition).
162
- * Landmark header/nav + main slot. Parent supplies nav items; no private theme.
212
+ * Landmark header/nav + main slot. `nav` stays flat link-only; non-nav controls
213
+ * go into the named `actions` slot. Parent supplies copy; no private theme.
163
214
  */
164
215
  export default class AppShell {
165
- public brand: string = "VMZ";
216
+ public brand: string = "";
166
217
  public homeHref: string = "/";
167
- public navLabel: string = "Primary";
218
+ public navLabel: string = "";
219
+ public menuLabel: string = "";
168
220
  /** Id of the nav item representing the current page, for `aria-current` + marker. */
169
221
  public activeId: string = "";
222
+ public navId: string = "vmz-ui-app-shell-nav";
223
+ /** Optional footer copy; the footer band only renders when supplied. */
224
+ public footer: string = "";
170
225
  /** @type {{ id: string, title: string, href: string }[]} */
171
226
  public nav: {
172
227
  id: string;
173
228
  title: string;
174
229
  href: string;
175
230
  }[] = [];
231
+ menuOpen = false;
232
+
233
+ toggleMenu() {
234
+ this.menuOpen = !this.menuOpen;
235
+ }
236
+
237
+ closeMenu() {
238
+ if (this.menuOpen) this.menuOpen = false;
239
+ }
240
+
241
+ onHeaderKeyDown(e) {
242
+ if (e && e.key === "Escape") this.closeMenu();
243
+ }
176
244
  }
177
245
  </script>