@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
@@ -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>
@@ -1,69 +1,237 @@
1
- <template>
2
- <nav data-vmz-ui="pagination" :aria-label="label" class="vmz-ui-pagination">
3
- <button type="button" :disabled="prevDisabled" @click="onPrevClick" class="vmz-ui-pagination__btn">
4
- Previous
5
- </button>
6
- <p data-vmz-fixture="page-status" class="vmz-ui-pagination__status">
7
- {{ status }}
8
- </p>
9
- <button type="button" :disabled="nextDisabled" @click="onNextClick" class="vmz-ui-pagination__btn">
10
- Next
11
- </button>
12
- </nav>
13
- </template>
14
- <style>
15
- .vmz-ui-pagination {
16
- display: flex;
17
- flex-wrap: wrap;
18
- align-items: center;
19
- gap: 0.625rem 1rem;
20
- }
21
-
22
- .vmz-ui-pagination__btn {
23
- font: inherit;
24
- font-weight: 600;
25
- padding: 0.375rem 0.7rem;
26
- border: 1px solid var(--vmz-line-default);
27
- border-radius: var(--vmz-radius-md, 6px);
28
- background: transparent;
29
- color: var(--vmz-action-primary-active);
30
- cursor: pointer;
31
- outline: none;
32
- }
33
-
34
- .vmz-ui-pagination__btn:focus-visible {
35
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
36
- }
37
-
38
- .vmz-ui-pagination__btn:disabled {
39
- opacity: 0.55;
40
- cursor: not-allowed;
41
- }
42
-
43
- .vmz-ui-pagination__status {
44
- margin: 0;
45
- font-weight: 600;
46
- }
47
- </style>
48
- <script client>
49
- /**
50
- * VMZ UI — Pagination (Console composition skeleton).
51
- * Parent owns page / pageCount; buttons call onPrev / onNext.
52
- */
53
- export default class Pagination {
54
- public label: string = "Pagination";
55
- public status: string = "Page 1 / 1";
56
- public prevDisabled: boolean = true;
57
- public nextDisabled: boolean = true;
58
- public onPrev: (() => void) | null = null;
59
- public onNext: (() => void) | null = null;
60
- onPrevClick() {
61
- if (this.prevDisabled) return;
62
- if (typeof this.onPrev === "function") this.onPrev();
63
- }
64
- onNextClick() {
65
- if (this.nextDisabled) return;
66
- if (typeof this.onNext === "function") this.onNext();
67
- }
68
- }
69
- </script>
1
+ <template>
2
+ <nav data-vmz-ui="pagination" :aria-label="label" class="vmz-ui-pagination">
3
+ <button type="button" :disabled="prevDisabled" :aria-label="prevLabel ? prevLabel : null" @click="onPrevClick" class="vmz-ui-pagination__nav">
4
+ <Icon name="action.chevron-left" size="sm" decorative />
5
+ <span v-if="prevLabel" class="vmz-ui-pagination__nav-label">
6
+ {{ prevLabel }}
7
+ </span>
8
+ </button>
9
+ <ul v-if="pageItems.length" class="vmz-ui-pagination__pages">
10
+ <li v-for="item in pageItems" :key="item.key" class="vmz-ui-pagination__page-item">
11
+ <span v-if="item.gap" aria-hidden="true" class="vmz-ui-pagination__ellipsis">
12
+ <Icon name="action.more" size="sm" decorative />
13
+ </span>
14
+ <button v-else type="button" :data-vmz-page="item.page" :aria-current="item.page === currentPage ? 'page' : null" :aria-label="pageAria(item.page)" @click="onPageClick(item.page)" class="vmz-ui-pagination__page">
15
+ {{ item.page }}
16
+ </button>
17
+ </li>
18
+ </ul>
19
+ <p data-vmz-fixture="page-status" class="vmz-ui-pagination__status">
20
+ {{ status }}
21
+ </p>
22
+ <span v-if="pageSizeOptions.length" class="vmz-ui-pagination__size">
23
+ <span v-if="pageSizeLabel" class="vmz-ui-pagination__size-label">
24
+ {{ pageSizeLabel }}
25
+ </span>
26
+ <Select :control-id="pageSizeControlId" :value="pageSize" :options="pageSizeOptions" :on-change="onPageSizeSelect" />
27
+ </span>
28
+ <button type="button" :disabled="nextDisabled" :aria-label="nextLabel ? nextLabel : null" @click="onNextClick" class="vmz-ui-pagination__nav">
29
+ <span v-if="nextLabel" class="vmz-ui-pagination__nav-label">
30
+ {{ nextLabel }}
31
+ </span>
32
+ <Icon name="action.chevron-right" size="sm" decorative />
33
+ </button>
34
+ </nav>
35
+ </template>
36
+ <style>
37
+ .vmz-ui-pagination {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ align-items: center;
41
+ gap: 0.5rem;
42
+ font-size: 0.875rem;
43
+ color: var(--vmz-text-ink);
44
+ }
45
+
46
+ .vmz-ui-pagination__nav,
47
+ .vmz-ui-pagination__page {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ gap: 0.25rem;
52
+ min-width: 2rem;
53
+ height: 2rem;
54
+ padding: 0 0.5rem;
55
+ box-sizing: border-box;
56
+ font: inherit;
57
+ font-size: 0.875rem;
58
+ font-weight: 500;
59
+ border: 1px solid var(--vmz-line-default);
60
+ border-radius: var(--vmz-radius-md, 6px);
61
+ background: var(--vmz-surface-paper);
62
+ color: var(--vmz-text-ink);
63
+ cursor: pointer;
64
+ outline: none;
65
+ transition:
66
+ color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1)),
67
+ border-color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1)),
68
+ background-color var(--vmz-motion-control-duration, 150ms) var(--vmz-motion-control-easing, cubic-bezier(0.4, 0, 0.2, 1));
69
+ }
70
+
71
+ .vmz-ui-pagination__nav:hover:not(:disabled),
72
+ .vmz-ui-pagination__page:hover:not([aria-current="page"]) {
73
+ background: var(--vmz-surface-mist);
74
+ border-color: var(--vmz-line-default);
75
+ color: var(--vmz-action-primary-background);
76
+ }
77
+
78
+ .vmz-ui-pagination__nav:focus-visible,
79
+ .vmz-ui-pagination__page:focus-visible {
80
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
81
+ }
82
+
83
+ .vmz-ui-pagination__nav:disabled {
84
+ opacity: 0.55;
85
+ cursor: not-allowed;
86
+ }
87
+
88
+ .vmz-ui-pagination__pages {
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ align-items: center;
92
+ gap: 0.25rem;
93
+ margin: 0;
94
+ padding: 0;
95
+ list-style: none;
96
+ }
97
+
98
+ .vmz-ui-pagination__page {
99
+ min-width: 2rem;
100
+ padding: 0;
101
+ font-variant-numeric: tabular-nums;
102
+ }
103
+
104
+ .vmz-ui-pagination__page[aria-current="page"] {
105
+ border-color: var(--vmz-action-primary-background);
106
+ background: var(--vmz-action-primary-background);
107
+ color: var(--vmz-action-primary-foreground);
108
+ cursor: default;
109
+ }
110
+
111
+ .vmz-ui-pagination__ellipsis {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ width: 2rem;
116
+ height: 2rem;
117
+ color: var(--vmz-text-tertiary);
118
+ }
119
+
120
+ .vmz-ui-pagination__status {
121
+ margin: 0 0 0 auto;
122
+ font-size: 0.875rem;
123
+ font-weight: 400;
124
+ color: var(--vmz-text-steel);
125
+ }
126
+
127
+ .vmz-ui-pagination__size {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ gap: 0.5rem;
131
+ }
132
+
133
+ .vmz-ui-pagination__size-label {
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ color: var(--vmz-text-steel);
137
+ white-space: nowrap;
138
+ }
139
+
140
+ .vmz-ui-pagination__size .vmz-ui-select {
141
+ max-width: 6rem;
142
+ }
143
+
144
+ .vmz-ui-pagination__size .vmz-ui-select__control {
145
+ min-height: 2rem;
146
+ padding-top: 0;
147
+ padding-bottom: 0;
148
+ }
149
+ </style>
150
+ <script client>
151
+ /**
152
+ * VMZ UI — Pagination.
153
+ * Parent owns page / pageCount; buttons call onPrev / onNext / onPage.
154
+ * Optional page-size Select renders when `pageSizeOptions` is non-empty.
155
+ */
156
+ export default class Pagination {
157
+ public label: string = "";
158
+ public prevLabel: string = "";
159
+ public nextLabel: string = "";
160
+ public status: string = "";
161
+ public prevDisabled: boolean = true;
162
+ public nextDisabled: boolean = true;
163
+ public page: number = 1;
164
+ public pageCount: number = 0;
165
+ /** Normalised numeric page mirror read by the template. */
166
+ public currentPage: number = 1;
167
+ public pageLabel: string = "";
168
+ public pageSize: string = "";
169
+ public pageSizeLabel: string = "";
170
+ public pageSizeControlId: string = "vmz-pagination-size";
171
+ /** @type {{ value: string, label: string }[]} */
172
+ public pageSizeOptions: {
173
+ value: string;
174
+ label: string;
175
+ }[] = [];
176
+ /** @type {{ key: string, page: number, gap: boolean }[]} */
177
+ pageItems: {
178
+ key: string;
179
+ page: number;
180
+ gap: boolean;
181
+ }[] = [];
182
+ public onPrev: (() => void) | null = null;
183
+ public onNext: (() => void) | null = null;
184
+ public onPage: ((page: number) => void) | null = null;
185
+ public onPageSizeChange: ((value: string) => void) | null = null;
186
+ onMount() {
187
+ this._rebuildPages();
188
+ }
189
+ /** Parent-owned prop writes — keep the page window in sync. */
190
+ __vmzOnParentProp(name: string) {
191
+ if (name === "page" || name === "pageCount") this._rebuildPages();
192
+ }
193
+ _rebuildPages() {
194
+ const total = Math.max(0, Math.floor(Number(this.pageCount) || 0));
195
+ const current = Math.min(Math.max(1, Math.floor(Number(this.page) || 1)), Math.max(1, total));
196
+ this.currentPage = current;
197
+ if (total <= 0) {
198
+ this.pageItems = [];
199
+ return;
200
+ }
201
+ const items = [];
202
+ const pageItem = (p) => ({ key: `p-${p}`, page: p, gap: false });
203
+ const gapItem = (k) => ({ key: k, page: 0, gap: true });
204
+ if (total <= 7) {
205
+ for (let p = 1; p <= total; p += 1) items.push(pageItem(p));
206
+ } else {
207
+ items.push(pageItem(1));
208
+ const start = Math.max(2, current - 1);
209
+ const end = Math.min(total - 1, current + 1);
210
+ if (start > 2) items.push(gapItem("gap-start"));
211
+ for (let p = start; p <= end; p += 1) items.push(pageItem(p));
212
+ if (end < total - 1) items.push(gapItem("gap-end"));
213
+ items.push(pageItem(total));
214
+ }
215
+ this.pageItems = items;
216
+ }
217
+ pageAria(p) {
218
+ return this.pageLabel ? `${this.pageLabel} ${p}` : String(p);
219
+ }
220
+ onPageClick(p) {
221
+ if (p === this.currentPage) return;
222
+ if (typeof this.onPage === "function") this.onPage(p);
223
+ }
224
+ onPageSizeSelect(payload) {
225
+ const next = payload && payload.target ? payload.target.value : "";
226
+ if (typeof this.onPageSizeChange === "function") this.onPageSizeChange(next);
227
+ }
228
+ onPrevClick() {
229
+ if (this.prevDisabled) return;
230
+ if (typeof this.onPrev === "function") this.onPrev();
231
+ }
232
+ onNextClick() {
233
+ if (this.nextDisabled) return;
234
+ if (typeof this.onNext === "function") this.onNext();
235
+ }
236
+ }
237
+ </script>