@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,115 +1,170 @@
1
- <template>
2
- <div data-vmz-ui="field" :data-invalid="invalidAttr" class="vmz-ui-field">
3
- <label v-if="label" :for="controlId" class="vmz-ui-field__label">
4
- {{ label }}
5
- </label>
6
- <input :id="controlId" :type="type" :value="value" :min="min" :max="max" :step="step" :disabled="disabled" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" @input="onDomInput" class="vmz-ui-field__control" />
7
- <p v-if="description" :id="descriptionId" class="vmz-ui-field__description">
8
- {{ description }}
9
- </p>
10
- <p v-if="error" :id="errorId" role="alert" class="vmz-ui-field__error">
11
- {{ error }}
12
- </p>
13
- </div>
14
- </template>
15
- <style>
16
- .vmz-ui-field {
17
- display: flex;
18
- flex-direction: column;
19
- gap: 0.375rem;
20
- max-width: 28rem;
21
- }
22
-
23
- .vmz-ui-field__label {
24
- font: inherit;
25
- font-weight: 500;
26
- font-size: 0.875rem;
27
- color: var(--vmz-text-ink, inherit);
28
- }
29
-
30
- .vmz-ui-field__control {
31
- box-sizing: border-box;
32
- width: 100%;
33
- min-height: var(--vmz-control-height, 2rem);
34
- font-family: var(--vmz-font-sans, inherit);
35
- font-size: 0.875rem;
36
- line-height: 1.5715;
37
- padding: var(--vmz-density-control-padding-y, 0.25rem)
38
- var(--vmz-density-control-padding-x, 0.75rem);
39
- border: 1px solid var(--vmz-line-default, #d9d9d9);
40
- border-radius: var(--vmz-radius-md, 6px);
41
- background: var(--vmz-surface-paper, #fff);
42
- color: var(--vmz-text-ink, inherit);
43
- outline: none;
44
- transition:
45
- border-color var(--vmz-motion-control-duration, 120ms)
46
- var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1)),
47
- box-shadow var(--vmz-motion-control-duration, 120ms)
48
- var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
49
- }
50
-
51
- .vmz-ui-field__control::placeholder {
52
- color: var(--vmz-text-steel, #8c8c8c);
53
- opacity: 1;
54
- }
55
-
56
- .vmz-ui-field[data-invalid="true"] .vmz-ui-field__control {
57
- border-color: var(--vmz-status-danger-accent);
58
- }
59
-
60
- .vmz-ui-field__control:hover:not(:disabled):not(:focus-visible) {
61
- border-color: var(--vmz-action-primary-background);
62
- }
63
-
64
- .vmz-ui-field__control:focus-visible {
65
- border-color: var(--vmz-action-primary-background);
66
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
67
- }
68
-
69
- .vmz-ui-field__control:disabled {
70
- opacity: 0.55;
71
- cursor: not-allowed;
72
- background: var(--vmz-surface-mist, #fafafa);
73
- }
74
-
75
- .vmz-ui-field__description {
76
- margin: 0;
77
- color: var(--vmz-text-steel, #8c8c8c);
78
- font-size: 0.875rem;
79
- line-height: 1.4;
80
- }
81
-
82
- .vmz-ui-field__error {
83
- margin: 0;
84
- color: var(--vmz-status-danger-foreground);
85
- font-size: 0.875rem;
86
- }
87
- </style>
88
- <script client>
89
- /**
90
- * VMZ UI — Field (UI1).
91
- * Label / description / error association via for + aria-describedby.
92
- * Colors only from Style Theme semantic tokens.
93
- */
94
- export default class Field {
95
- public label: string = "";
96
- public description: string = "";
97
- public error: string = "";
98
- public value: string = "";
99
- public type: string = "text";
100
- public min: string = "";
101
- public max: string = "";
102
- public step: string = "";
103
- public disabled: boolean = false;
104
- public invalidAttr: string = "false";
105
- public controlId: string = "vmz-field";
106
- public descriptionId: string = "vmz-field-desc";
107
- public errorId: string = "vmz-field-err";
108
- public describedBy: string = "";
109
- public onInput: ((e: Event) => void) | null = null;
110
- onDomInput(e: Event) {
111
- this.emit("input", e);
112
- if (typeof this.onInput === "function") this.onInput(e);
113
- }
114
- }
115
- </script>
1
+ <template>
2
+ <div data-vmz-ui="field" :data-invalid="error ? 'true' : 'false'" class="vmz-ui-field">
3
+ <label v-if="label" :for="controlId" class="vmz-ui-field__label">
4
+ {{ label }}
5
+ </label>
6
+ <div :data-prefix="prefix ? 'true' : 'false'" :data-suffix="suffix ? 'true' : 'false'" class="vmz-ui-field__control-wrap">
7
+ <Icon v-if="prefix" :name="prefix" size="md" decorative class="vmz-ui-field__icon vmz-ui-field__icon--prefix" />
8
+ <input :id="controlId" :type="type" :value="value" :min="min" :max="max" :step="step" :placeholder="placeholder" :disabled="disabled" :readonly="readonly" :aria-invalid="error ? 'true' : 'false'" :aria-describedby="describedBy" @input="onDomInput" class="vmz-ui-field__control" />
9
+ <Icon v-if="suffix" :name="suffix" size="md" decorative class="vmz-ui-field__icon vmz-ui-field__icon--suffix" />
10
+ </div>
11
+ <p v-if="description" :id="descriptionId" class="vmz-ui-field__description">
12
+ {{ description }}
13
+ </p>
14
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-field__error">
15
+ {{ error }}
16
+ </p>
17
+ </div>
18
+ </template>
19
+ <style>
20
+ .vmz-ui-field {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--vmz-space-2);
24
+ width: 100%;
25
+ max-width: 28rem;
26
+ }
27
+
28
+ .vmz-ui-field__label {
29
+ font: inherit;
30
+ font-weight: 500;
31
+ font-size: 0.875rem;
32
+ line-height: 1.5715;
33
+ color: var(--vmz-text-ink);
34
+ }
35
+
36
+ .vmz-ui-field__control-wrap {
37
+ position: relative;
38
+ display: flex;
39
+ align-items: center;
40
+ width: 100%;
41
+ }
42
+
43
+ .vmz-ui-field__control {
44
+ box-sizing: border-box;
45
+ width: 100%;
46
+ min-height: var(--vmz-control-height);
47
+ font-family: inherit;
48
+ font-size: 0.875rem;
49
+ line-height: 1.5715;
50
+ padding: var(--vmz-density-control-padding-y) var(--vmz-density-control-padding-x);
51
+ border: 1px solid var(--vmz-line-default);
52
+ border-radius: var(--vmz-radius-lg);
53
+ background: var(--vmz-surface-paper);
54
+ color: var(--vmz-text-ink);
55
+ outline: none;
56
+ transition:
57
+ border-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
58
+ box-shadow var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
59
+ background-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
60
+ }
61
+
62
+ .vmz-ui-field__control-wrap[data-prefix="true"] .vmz-ui-field__control {
63
+ padding-left: calc(
64
+ var(--vmz-density-control-padding-x) + var(--vmz-space-4) + var(--vmz-space-1)
65
+ );
66
+ }
67
+
68
+ .vmz-ui-field__control-wrap[data-suffix="true"] .vmz-ui-field__control {
69
+ padding-right: calc(
70
+ var(--vmz-density-control-padding-x) + var(--vmz-space-4) + var(--vmz-space-1)
71
+ );
72
+ }
73
+
74
+ .vmz-ui-field__icon {
75
+ position: absolute;
76
+ top: 50%;
77
+ z-index: 1;
78
+ color: var(--vmz-text-tertiary);
79
+ transform: translateY(-50%);
80
+ pointer-events: none;
81
+ }
82
+
83
+ .vmz-ui-field__icon--prefix {
84
+ left: var(--vmz-density-control-padding-x);
85
+ }
86
+
87
+ .vmz-ui-field__icon--suffix {
88
+ right: var(--vmz-density-control-padding-x);
89
+ }
90
+
91
+ .vmz-ui-field__control::placeholder {
92
+ color: var(--vmz-text-tertiary);
93
+ opacity: 1;
94
+ }
95
+
96
+ .vmz-ui-field[data-invalid="true"] .vmz-ui-field__control {
97
+ border-color: var(--vmz-status-danger-accent);
98
+ }
99
+
100
+ .vmz-ui-field[data-invalid="true"] .vmz-ui-field__control:hover:not(:disabled),
101
+ .vmz-ui-field[data-invalid="true"] .vmz-ui-field__control:focus-visible {
102
+ border-color: var(--vmz-status-danger-foreground);
103
+ }
104
+
105
+ .vmz-ui-field__control:hover:not(:disabled):not(:focus-visible) {
106
+ border-color: var(--vmz-action-primary-hover);
107
+ }
108
+
109
+ .vmz-ui-field__control:focus-visible {
110
+ border-color: var(--vmz-action-primary-background);
111
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
112
+ }
113
+
114
+ .vmz-ui-field__control:disabled {
115
+ opacity: 0.6;
116
+ color: var(--vmz-text-muted);
117
+ cursor: not-allowed;
118
+ background: var(--vmz-surface-mist);
119
+ }
120
+
121
+ .vmz-ui-field__control:read-only {
122
+ cursor: default;
123
+ background: var(--vmz-surface-mist);
124
+ }
125
+
126
+ .vmz-ui-field__description {
127
+ margin: 0;
128
+ color: var(--vmz-text-steel);
129
+ font-size: 0.875rem;
130
+ line-height: 1.5715;
131
+ }
132
+
133
+ .vmz-ui-field__error {
134
+ margin: 0;
135
+ color: var(--vmz-status-danger-foreground);
136
+ font-size: 0.875rem;
137
+ line-height: 1.5715;
138
+ }
139
+ </style>
140
+ <script client>
141
+ /**
142
+ * VMZ UI — Field (UI1).
143
+ * Label / description / error association via for + aria-describedby.
144
+ * Colors only from Style Theme semantic tokens.
145
+ */
146
+ export default class Field {
147
+ public label: string = "";
148
+ public description: string = "";
149
+ public error: string = "";
150
+ public value: string = "";
151
+ public type: string = "text";
152
+ public placeholder: string = "";
153
+ public prefix: string = "";
154
+ public suffix: string = "";
155
+ public min: string = "";
156
+ public max: string = "";
157
+ public step: string = "";
158
+ public disabled: boolean = false;
159
+ public readonly: boolean = false;
160
+ public controlId: string = "vmz-field";
161
+ public descriptionId: string = "vmz-field-desc";
162
+ public errorId: string = "vmz-field-err";
163
+ public describedBy: string = "";
164
+ public onInput: ((e: Event) => void) | null = null;
165
+ onDomInput(e: Event) {
166
+ this.emit("input", e);
167
+ if (typeof this.onInput === "function") this.onInput(e);
168
+ }
169
+ }
170
+ </script>
@@ -8,10 +8,40 @@
8
8
  display: flex;
9
9
  flex-wrap: wrap;
10
10
  align-items: flex-end;
11
- gap: 0.75rem 1rem;
12
- padding: 0.75rem 0.85rem;
11
+ gap: 0.75rem;
12
+ padding: 1rem;
13
13
  border: 1px solid var(--vmz-line-default);
14
- border-radius: var(--vmz-radius-md, 6px);
14
+ border-radius: var(--vmz-radius-lg, 8px);
15
+ background: var(--vmz-surface-paper);
16
+ }
17
+
18
+ /* Action controls (Field + buttons) keep 32px height and hug the right edge. */
19
+ .vmz-ui-filter-bar .vmz-ui-field__control,
20
+ .vmz-ui-filter-bar .vmz-ui-select__control,
21
+ .vmz-ui-filter-bar .vmz-ui-btn {
22
+ min-height: 2rem;
23
+ }
24
+
25
+ .vmz-ui-filter-bar > :has(.vmz-ui-btn),
26
+ .vmz-ui-filter-bar > button.vmz-ui-btn {
27
+ margin-left: auto;
28
+ }
29
+
30
+ @media (max-width: 40rem) {
31
+ .vmz-ui-filter-bar {
32
+ flex-direction: column;
33
+ align-items: flex-start;
34
+ gap: 0.5rem;
35
+ }
36
+
37
+ .vmz-ui-filter-bar > * {
38
+ width: 100%;
39
+ }
40
+
41
+ .vmz-ui-filter-bar > :has(.vmz-ui-btn),
42
+ .vmz-ui-filter-bar > button.vmz-ui-btn {
43
+ margin-left: 0;
44
+ }
15
45
  }
16
46
  </style>
17
47
  <script client>
@@ -20,6 +50,6 @@
20
50
  * Dense filter/query landmark; parent projects Field + action controls.
21
51
  */
22
52
  export default class FilterBar {
23
- public label: string = "Filters";
53
+ public label: string = "";
24
54
  }
25
55
  </script>
@@ -8,13 +8,15 @@
8
8
  display: flex;
9
9
  align-items: center;
10
10
  justify-content: flex-start;
11
- gap: 0.75rem;
11
+ gap: var(--vmz-space-3);
12
12
  min-width: 0;
13
13
  }
14
+
14
15
  .vmz-ui-flex[data-vertical="true"] {
15
16
  flex-direction: column;
16
17
  align-items: stretch;
17
18
  }
19
+
18
20
  .vmz-ui-flex[data-wrap="true"] {
19
21
  flex-wrap: wrap;
20
22
  }
@@ -5,9 +5,12 @@
5
5
  </template>
6
6
  <style>
7
7
  .vmz-ui-footer {
8
- padding: 1.5rem;
9
- color: var(--vmz-text-steel, #646a73);
10
- font-size: 0.875rem;
8
+ padding: var(--vmz-space-6, 2rem) var(--vmz-space-5, 1.5rem);
9
+ border-top: 1px solid var(--vmz-line-subtle);
10
+ background: var(--vmz-surface-paper);
11
+ color: var(--vmz-text-tertiary);
12
+ font-size: 0.8125rem;
13
+ line-height: 1.5;
11
14
  text-align: center;
12
15
  }
13
16
  </style>
@@ -34,6 +34,9 @@
34
34
  /**
35
35
  * VMZ UI — Form (hand-written commercial form shell).
36
36
  * Parent owns field values + validation presentation; no parallel form store.
37
+ * `rules` + `values` are evaluated at submit only: failures emit `invalid`
38
+ * (field -> rule key: required | min | max | pattern | custom) and suppress
39
+ * `submit`. Copy stays author-supplied.
37
40
  * Native submit/reset guarded; browser HTML5 validation left off (data-novalidate + preventDefault).
38
41
  */
39
42
  export default class Form {
@@ -43,14 +46,52 @@ export default class Form {
43
46
  public describedBy: string = "";
44
47
  public disabled: boolean = false;
45
48
  public disabledAttr: string = "false";
49
+ public rules: Record<string, {
50
+ required?: boolean;
51
+ minLength?: number;
52
+ maxLength?: number;
53
+ pattern?: string;
54
+ test?: (value: string, values: Record<string, unknown>) => boolean;
55
+ }> = {};
56
+ public values: Record<string, unknown> = {};
46
57
  public onSubmit: (() => void) | null = null;
47
58
  public onReset: (() => void) | null = null;
59
+ public onInvalid: ((errors: Record<string, string>) => void) | null = null;
48
60
  onSubmitGuard(ev) {
49
61
  if (ev && typeof ev.preventDefault === "function") ev.preventDefault();
50
62
  if (this.disabled) return;
63
+ const errors = this.collectErrors();
64
+ if (Object.keys(errors).length > 0) {
65
+ this.emit("invalid", errors);
66
+ if (typeof this.onInvalid === "function") this.onInvalid(errors);
67
+ return;
68
+ }
51
69
  this.emit("submit");
52
70
  if (typeof this.onSubmit === "function") this.onSubmit();
53
71
  }
72
+ collectErrors() {
73
+ const errors: Record<string, string> = {};
74
+ const rules = this.rules || {};
75
+ const values = this.values || {};
76
+ for (const field of Object.keys(rules)) {
77
+ const rule = rules[field] || {};
78
+ const raw = values[field];
79
+ const text = raw == null ? "" : String(raw);
80
+ const filled = text.trim().length > 0;
81
+ if (rule.required && !filled) {
82
+ errors[field] = "required";
83
+ } else if (rule.minLength && filled && text.length < Number(rule.minLength)) {
84
+ errors[field] = "min";
85
+ } else if (rule.maxLength && text.length > Number(rule.maxLength)) {
86
+ errors[field] = "max";
87
+ } else if (rule.pattern && filled && !new RegExp(String(rule.pattern)).test(text)) {
88
+ errors[field] = "pattern";
89
+ } else if (typeof rule.test === "function" && !rule.test(text, values)) {
90
+ errors[field] = "custom";
91
+ }
92
+ }
93
+ return errors;
94
+ }
54
95
  onResetGuard(ev) {
55
96
  if (ev && typeof ev.preventDefault === "function") ev.preventDefault();
56
97
  if (this.disabled) return;
@@ -1,79 +1,78 @@
1
- <template>
2
- <div data-vmz-ui="form-item" :data-required="requiredAttr" :data-invalid="invalidAttr" class="vmz-ui-form-item">
3
- <label v-if="label" :for="controlId" class="vmz-ui-form-item__label">
4
- <span>
5
- {{ label }}
6
- </span>
7
- <span v-if="required" aria-hidden="true" class="vmz-ui-form-item__required">
8
- *
9
- </span>
10
- </label>
11
- <div class="vmz-ui-form-item__control">
12
- <slot />
13
- </div>
14
- <p v-if="description" :id="descriptionId" class="vmz-ui-form-item__description">
15
- {{ description }}
16
- </p>
17
- <p v-if="error" :id="errorId" role="alert" class="vmz-ui-form-item__error">
18
- {{ error }}
19
- </p>
20
- </div>
21
- </template>
22
- <style>
23
- .vmz-ui-form-item {
24
- display: flex;
25
- flex-direction: column;
26
- gap: 0.375rem;
27
- max-width: 28rem;
28
- }
29
-
30
- .vmz-ui-form-item__label {
31
- display: inline-flex;
32
- align-items: baseline;
33
- gap: 0.25rem;
34
- font: inherit;
35
- font-weight: 600;
36
- }
37
-
38
- .vmz-ui-form-item__required {
39
- color: var(--vmz-status-danger-accent);
40
- font-weight: 700;
41
- }
42
-
43
- .vmz-ui-form-item__control {
44
- min-width: 0;
45
- }
46
-
47
- .vmz-ui-form-item__description {
48
- margin: 0;
49
- opacity: 0.75;
50
- font-size: 0.9em;
51
- }
52
-
53
- .vmz-ui-form-item__error {
54
- margin: 0;
55
- color: var(--vmz-status-danger-foreground);
56
- font-size: 0.9em;
57
- }
58
-
59
- .vmz-ui-form-item[data-invalid="true"] .vmz-ui-form-item__control :where(input, select, textarea) {
60
- border-color: var(--vmz-status-danger-accent);
61
- }
62
- </style>
63
- <script client>
64
- /**
65
- * VMZ UI — FormItem.
66
- * Label / description / error shell for slotted controls; parent owns validation strings.
67
- */
68
- export default class FormItem {
69
- public label: string = "";
70
- public description: string = "";
71
- public error: string = "";
72
- public required: boolean = false;
73
- public requiredAttr: string = "false";
74
- public invalidAttr: string = "false";
75
- public controlId: string = "vmz-form-item";
76
- public descriptionId: string = "vmz-form-item-desc";
77
- public errorId: string = "vmz-form-item-err";
78
- }
79
- </script>
1
+ <template>
2
+ <div data-vmz-ui="form-item" :data-required="required ? 'true' : 'false'" :data-invalid="error ? 'true' : 'false'" class="vmz-ui-form-item">
3
+ <label v-if="label" :for="controlId" class="vmz-ui-form-item__label">
4
+ <span>
5
+ {{ label }}
6
+ </span>
7
+ <span v-if="required" aria-hidden="true" class="vmz-ui-form-item__required">
8
+ *
9
+ </span>
10
+ </label>
11
+ <div class="vmz-ui-form-item__control">
12
+ <slot />
13
+ </div>
14
+ <p v-if="description" :id="descriptionId" class="vmz-ui-form-item__description">
15
+ {{ description }}
16
+ </p>
17
+ <p v-if="error" :id="errorId" role="alert" class="vmz-ui-form-item__error">
18
+ {{ error }}
19
+ </p>
20
+ </div>
21
+ </template>
22
+ <style>
23
+ .vmz-ui-form-item {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 0.375rem;
27
+ width: 100%;
28
+ max-width: 28rem;
29
+ }
30
+
31
+ .vmz-ui-form-item__label {
32
+ display: inline-flex;
33
+ align-items: baseline;
34
+ gap: 0.25rem;
35
+ font: inherit;
36
+ font-weight: 600;
37
+ }
38
+
39
+ .vmz-ui-form-item__required {
40
+ color: var(--vmz-status-danger-accent);
41
+ font-weight: 700;
42
+ }
43
+
44
+ .vmz-ui-form-item__control {
45
+ min-width: 0;
46
+ }
47
+
48
+ .vmz-ui-form-item__description {
49
+ margin: 0;
50
+ opacity: 0.75;
51
+ font-size: 0.9em;
52
+ }
53
+
54
+ .vmz-ui-form-item__error {
55
+ margin: 0;
56
+ color: var(--vmz-status-danger-foreground);
57
+ font-size: 0.9em;
58
+ }
59
+
60
+ .vmz-ui-form-item[data-invalid="true"] .vmz-ui-form-item__control :where(input, select, textarea) {
61
+ border-color: var(--vmz-status-danger-accent);
62
+ }
63
+ </style>
64
+ <script client>
65
+ /**
66
+ * VMZ UI — FormItem.
67
+ * Label / description / error shell for slotted controls; parent owns validation strings.
68
+ */
69
+ export default class FormItem {
70
+ public label: string = "";
71
+ public description: string = "";
72
+ public error: string = "";
73
+ public required: boolean = false;
74
+ public controlId: string = "vmz-form-item";
75
+ public descriptionId: string = "vmz-form-item-desc";
76
+ public errorId: string = "vmz-form-item-err";
77
+ }
78
+ </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div data-vmz-ui="grid" class="vmz-ui-grid" :style="'--ui-grid-columns:' + columns + ';--ui-grid-gap:' + gap">
2
+ <div data-vmz-ui="grid" :data-columns="columns" class="vmz-ui-grid" :style="'--ui-grid-columns:' + columns + ';--ui-grid-gap:' + gap">
3
3
  <slot />
4
4
  </div>
5
5
  </template>
@@ -7,13 +7,16 @@
7
7
  .vmz-ui-grid {
8
8
  display: grid;
9
9
  grid-template-columns: repeat(var(--ui-grid-columns, 3), minmax(0, 1fr));
10
- gap: var(--ui-grid-gap, 1rem);
10
+ gap: var(--ui-grid-gap, var(--vmz-space-4));
11
+ align-items: stretch;
11
12
  }
13
+
12
14
  @media (max-width: 900px) {
13
15
  .vmz-ui-grid {
14
16
  grid-template-columns: repeat(2, minmax(0, 1fr));
15
17
  }
16
18
  }
19
+
17
20
  @media (max-width: 600px) {
18
21
  .vmz-ui-grid {
19
22
  grid-template-columns: 1fr;
@@ -23,6 +26,6 @@
23
26
  <script client>
24
27
  export default class Grid {
25
28
  public columns: number = 3;
26
- public gap: string = "1rem";
29
+ public gap: string = "var(--vmz-space-4)";
27
30
  }
28
31
  </script>