@openvoxproject/voxblocks 0.9.0 → 0.11.0

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 (49) hide show
  1. package/README.md +17 -9
  2. package/dist/cdn/voxblocks.css +1 -1
  3. package/dist/cdn/voxblocks.js +1987 -1279
  4. package/dist/types/components/accordion/vox-accordion.d.ts +2 -0
  5. package/dist/types/components/alert/vox-alert.d.ts +2 -1
  6. package/dist/types/components/callout/vox-callout.d.ts +2 -1
  7. package/dist/types/components/disclosure/vox-disclosure.d.ts +2 -0
  8. package/dist/types/components/dropdown/vox-dropdown.d.ts +3 -0
  9. package/dist/types/components/header/vox-header.d.ts +11 -1
  10. package/dist/types/components/icon/icon-paths.d.ts +30 -0
  11. package/dist/types/components/icon/vox-icon.d.ts +22 -0
  12. package/dist/types/components/link-hub/vox-link-hub.d.ts +3 -0
  13. package/dist/types/components/pagination/vox-pagination.d.ts +6 -4
  14. package/dist/types/components/sidenav/vox-sidenav.d.ts +20 -0
  15. package/dist/types/components/toc/vox-toc.d.ts +43 -0
  16. package/dist/types/index.d.ts +4 -0
  17. package/dist/types/internal/field.d.ts +4 -0
  18. package/dist/voxblocks.css +1 -1
  19. package/dist/voxblocks.js +1705 -997
  20. package/package.json +9 -6
  21. package/src/components/accordion/vox-accordion.ts +6 -1
  22. package/src/components/alert/vox-alert.ts +46 -2
  23. package/src/components/avatar/vox-avatar.ts +9 -2
  24. package/src/components/callout/vox-callout.ts +53 -3
  25. package/src/components/card/vox-card.ts +6 -1
  26. package/src/components/checkbox/vox-checkbox.ts +4 -3
  27. package/src/components/dialog/vox-dialog.ts +2 -2
  28. package/src/components/disclosure/vox-disclosure.ts +6 -1
  29. package/src/components/dropdown/vox-dropdown.ts +32 -8
  30. package/src/components/empty-state/vox-empty-state.ts +1 -1
  31. package/src/components/file-input/vox-file-input.ts +4 -1
  32. package/src/components/header/vox-header.ts +142 -7
  33. package/src/components/icon/icon-paths.ts +287 -0
  34. package/src/components/icon/vox-icon.ts +78 -0
  35. package/src/components/input/vox-input.ts +2 -0
  36. package/src/components/link-hub/vox-link-hub.ts +21 -0
  37. package/src/components/pagination/vox-pagination.ts +9 -6
  38. package/src/components/radio/vox-radio-group.ts +17 -6
  39. package/src/components/select/vox-select.ts +3 -0
  40. package/src/components/sidenav/vox-sidenav.ts +164 -4
  41. package/src/components/step-indicator/vox-step-indicator.ts +16 -2
  42. package/src/components/tabs/vox-tabs.ts +26 -2
  43. package/src/components/textarea/vox-textarea.ts +2 -0
  44. package/src/components/toc/vox-toc.ts +140 -0
  45. package/src/index.ts +6 -0
  46. package/src/internal/field.ts +20 -2
  47. package/src/styles/utilities.css +158 -0
  48. package/src/tokens/palette.css +95 -0
  49. package/src/tokens/tokens.css +7 -7
@@ -1,10 +1,12 @@
1
1
  import { LitElement, html, css, nothing } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { ICON_PATHS } from '../icon/icon-paths.js';
3
4
 
4
5
  /**
5
6
  * A site header with brand, primary navigation, and an actions area.
6
7
  * Nav entries are plain links; mark the current page with
7
- * `aria-current="page"`.
8
+ * `aria-current="page"`. Below 768px, nav and actions collapse behind a
9
+ * menu button.
8
10
  *
9
11
  * @slot logo - Brand mark shown before the site title.
10
12
  * @slot - Navigation links.
@@ -18,6 +20,8 @@ export class VoxHeader extends LitElement {
18
20
  /** Where the brand links to. */
19
21
  @property() href = '/';
20
22
 
23
+ @state() private mobileOpen = false;
24
+
21
25
  static styles = css`
22
26
  :host {
23
27
  display: block;
@@ -52,6 +56,39 @@ export class VoxHeader extends LitElement {
52
56
  width: auto;
53
57
  }
54
58
 
59
+ .menu-toggle {
60
+ display: none;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 36px;
64
+ height: 36px;
65
+ margin-left: auto;
66
+ padding: 0;
67
+ background: none;
68
+ border: none;
69
+ border-radius: var(--vox-radius-sm);
70
+ color: var(--vox-color-text-1);
71
+ cursor: pointer;
72
+ }
73
+
74
+ .menu-toggle:hover {
75
+ color: var(--vox-color-brand-1);
76
+ }
77
+
78
+ .menu-toggle:focus-visible {
79
+ outline: 2px solid var(--vox-color-brand-1);
80
+ outline-offset: 2px;
81
+ }
82
+
83
+ .menu-icon {
84
+ width: 22px;
85
+ height: 22px;
86
+ }
87
+
88
+ .nav-wrap {
89
+ display: contents;
90
+ }
91
+
55
92
  nav {
56
93
  display: flex;
57
94
  align-items: center;
@@ -82,8 +119,83 @@ export class VoxHeader extends LitElement {
82
119
  align-items: center;
83
120
  gap: var(--vox-space-3);
84
121
  }
122
+
123
+ @media (max-width: 768px) {
124
+ .menu-toggle {
125
+ display: inline-flex;
126
+ }
127
+
128
+ .nav-wrap {
129
+ display: none;
130
+ width: 100%;
131
+ }
132
+
133
+ .nav-wrap.open {
134
+ display: flex;
135
+ flex-direction: column;
136
+ align-items: stretch;
137
+ gap: var(--vox-space-4);
138
+ margin-top: var(--vox-space-3);
139
+ padding-top: var(--vox-space-4);
140
+ border-top: 1px solid var(--vox-color-divider);
141
+ }
142
+
143
+ nav {
144
+ flex-direction: column;
145
+ align-items: flex-start;
146
+ gap: var(--vox-space-3);
147
+ }
148
+
149
+ .actions {
150
+ flex-direction: column;
151
+ align-items: stretch;
152
+ }
153
+ }
85
154
  `;
86
155
 
156
+ connectedCallback() {
157
+ super.connectedCallback();
158
+ document.addEventListener('click', this.handleOutsideClick);
159
+ this.addEventListener('keydown', this.handleKeydown);
160
+ this.addEventListener('focusout', this.handleFocusOut);
161
+ }
162
+
163
+ disconnectedCallback() {
164
+ super.disconnectedCallback();
165
+ document.removeEventListener('click', this.handleOutsideClick);
166
+ this.removeEventListener('keydown', this.handleKeydown);
167
+ this.removeEventListener('focusout', this.handleFocusOut);
168
+ }
169
+
170
+ private handleOutsideClick = (event: MouseEvent) => {
171
+ if (this.mobileOpen && !event.composedPath().includes(this)) {
172
+ this.mobileOpen = false;
173
+ }
174
+ };
175
+
176
+ /** Close the mobile menu once focus tabs past its last item. */
177
+ private handleFocusOut = (event: FocusEvent) => {
178
+ const next = event.relatedTarget as Node | null;
179
+ if (this.mobileOpen && !(next && this.contains(next))) {
180
+ this.mobileOpen = false;
181
+ }
182
+ };
183
+
184
+ private handleKeydown = (event: KeyboardEvent) => {
185
+ if (event.key === 'Escape' && this.mobileOpen) {
186
+ this.mobileOpen = false;
187
+ this.renderRoot.querySelector<HTMLElement>('.menu-toggle')?.focus();
188
+ }
189
+ };
190
+
191
+ private toggleMobileMenu = () => {
192
+ this.mobileOpen = !this.mobileOpen;
193
+ };
194
+
195
+ private closeMobileMenu = () => {
196
+ this.mobileOpen = false;
197
+ };
198
+
87
199
  render() {
88
200
  return html`
89
201
  <header class="header">
@@ -91,11 +203,34 @@ export class VoxHeader extends LitElement {
91
203
  <slot name="logo"></slot>
92
204
  ${this.siteTitle ? html`<span>${this.siteTitle}</span>` : nothing}
93
205
  </a>
94
- <nav aria-label="Main">
95
- <slot></slot>
96
- </nav>
97
- <div class="actions">
98
- <slot name="actions"></slot>
206
+ <button
207
+ type="button"
208
+ class="menu-toggle"
209
+ aria-expanded=${this.mobileOpen ? 'true' : 'false'}
210
+ aria-controls="nav-wrap"
211
+ aria-label=${this.mobileOpen ? 'Close menu' : 'Open menu'}
212
+ @click=${this.toggleMobileMenu}
213
+ >
214
+ <svg
215
+ class="menu-icon"
216
+ viewBox="0 0 48 48"
217
+ fill="none"
218
+ stroke="currentColor"
219
+ stroke-width="2"
220
+ stroke-linecap="round"
221
+ stroke-linejoin="round"
222
+ aria-hidden="true"
223
+ >
224
+ ${this.mobileOpen ? ICON_PATHS.close : ICON_PATHS.menu}
225
+ </svg>
226
+ </button>
227
+ <div id="nav-wrap" class="nav-wrap${this.mobileOpen ? ' open' : ''}">
228
+ <nav aria-label="Main">
229
+ <slot @click=${this.closeMobileMenu}></slot>
230
+ </nav>
231
+ <div class="actions">
232
+ <slot name="actions"></slot>
233
+ </div>
99
234
  </div>
100
235
  </header>
101
236
  `;
@@ -0,0 +1,287 @@
1
+ import { svg, type SVGTemplateResult } from 'lit';
2
+
3
+ /**
4
+ * The VoxBlocks icon set. Every icon shares one drawing rule: 48x48
5
+ * viewBox, 2px stroke, round caps/joins, `currentColor` — see
6
+ * docs/guide/colors.md and the card/empty-state doc icons this set
7
+ * grew out of. Add new icons here and to `IconName` below; nowhere
8
+ * else needs to change.
9
+ *
10
+ * Grouped by the phases from the icon roadmap:
11
+ * - Phase 1: shared UI baseline — search through refresh, plus eye-slash.
12
+ * - Phase 2: OpenVox marketing site — community through heart.
13
+ * - Phase 3: module registry (Forge replacement) — module through collection.
14
+ * - Phase 4: fleet console (Puppet Enterprise replacement) — dashboard
15
+ * through drift.
16
+ *
17
+ * A few concepts repeat across phases (e.g. a release tag and a topic
18
+ * tag) and deliberately share one icon rather than drawing
19
+ * near-duplicates — `star` also covers "rating", `copy` also covers
20
+ * "copy install command", `terminal` also covers "live management
21
+ * session", and `key` covers both PKI and API keys.
22
+ *
23
+ * `blocks`, `plug`, `ballot`, `sun`, and `accessibility` were added
24
+ * later, consolidating hand-drawn SVGs that had been copy-pasted
25
+ * inline across docs/index.md, card.md, and marketing.md before this
26
+ * component existed — see each one's call site for what it replaced.
27
+ * `settings` was added when components (not just docs) started using
28
+ * icons — e.g. a sidenav "Settings" item needed one that didn't exist yet.
29
+ */
30
+ export type IconName =
31
+ // Phase 1: shared UI baseline
32
+ | 'search'
33
+ | 'close'
34
+ | 'menu'
35
+ | 'chevron-up'
36
+ | 'chevron-down'
37
+ | 'chevron-left'
38
+ | 'chevron-right'
39
+ | 'external-link'
40
+ | 'copy'
41
+ | 'check'
42
+ | 'warning'
43
+ | 'info'
44
+ | 'add'
45
+ | 'remove'
46
+ | 'edit'
47
+ | 'delete'
48
+ | 'filter'
49
+ | 'sort'
50
+ | 'calendar'
51
+ | 'clock'
52
+ | 'person'
53
+ | 'people'
54
+ | 'lock'
55
+ | 'eye'
56
+ | 'eye-slash'
57
+ | 'refresh'
58
+ // Phase 2: OpenVox marketing site
59
+ | 'community'
60
+ | 'book'
61
+ | 'terminal'
62
+ | 'shield'
63
+ | 'roadmap'
64
+ | 'help'
65
+ | 'repository'
66
+ | 'star'
67
+ | 'chat'
68
+ | 'heart'
69
+ // Phase 3: module registry (Forge replacement)
70
+ | 'module'
71
+ | 'tag'
72
+ | 'dependency'
73
+ | 'verified'
74
+ | 'archived'
75
+ | 'publish'
76
+ | 'changelog'
77
+ | 'vulnerability'
78
+ | 'gauge'
79
+ | 'file-tree'
80
+ | 'check-circle'
81
+ | 'x-circle'
82
+ | 'fork'
83
+ | 'trending'
84
+ | 'collection'
85
+ // Phase 4: fleet console (Puppet Enterprise replacement)
86
+ | 'dashboard'
87
+ | 'node'
88
+ | 'node-group'
89
+ | 'pulse'
90
+ | 'compliance'
91
+ | 'report'
92
+ | 'activity-log'
93
+ | 'bell'
94
+ | 'orchestrate'
95
+ | 'plan'
96
+ | 'badge'
97
+ | 'audit'
98
+ | 'key'
99
+ | 'layers'
100
+ | 'classifier'
101
+ | 'deploy'
102
+ | 'metrics'
103
+ | 'schedule'
104
+ | 'backup'
105
+ | 'webhook'
106
+ | 'organization'
107
+ | 'drift'
108
+ // Added later: consolidated from docs pages that predated vox-icon
109
+ | 'blocks'
110
+ | 'plug'
111
+ | 'ballot'
112
+ | 'sun'
113
+ | 'moon'
114
+ | 'accessibility'
115
+ | 'settings'
116
+ | 'github';
117
+
118
+ export const ICON_PATHS: Record<IconName, SVGTemplateResult> = {
119
+ // Phase 1: shared UI baseline -----------------------------------------
120
+
121
+ search: svg`<circle cx="21" cy="21" r="13"/><path d="M30.5 30.5 L41 41"/>`,
122
+
123
+ close: svg`<path d="M14 14 L34 34 M34 14 L14 34"/>`,
124
+
125
+ menu: svg`<path d="M8 14 H40 M8 24 H40 M8 34 H40"/>`,
126
+
127
+ 'chevron-up': svg`<path d="M14 28 L24 18 L34 28"/>`,
128
+ 'chevron-down': svg`<path d="M14 20 L24 30 L34 20"/>`,
129
+ 'chevron-left': svg`<path d="M28 14 L18 24 L28 34"/>`,
130
+ 'chevron-right': svg`<path d="M20 14 L30 24 L20 34"/>`,
131
+
132
+ 'external-link': svg`<path d="M36 26 V38 A4 4 0 0 1 32 42 H10 A4 4 0 0 1 6 38 V16 A4 4 0 0 1 10 12 H22"/><path d="M30 6 H42 V18"/><path d="M20 28 L42 6"/>`,
133
+
134
+ copy: svg`<rect x="8" y="14" width="24" height="24" rx="3"/><path d="M16 14 V10 A2 2 0 0 1 18 8 H38 A2 2 0 0 1 40 10 V30 A2 2 0 0 1 38 32 H34"/>`,
135
+
136
+ check: svg`<path d="M10 25 L20 35 L38 13"/>`,
137
+
138
+ warning: svg`<path d="M24 8 L44 40 H4 Z"/><path d="M24 20 V28"/><circle cx="24" cy="34" r="1.6" fill="currentColor" stroke="none"/>`,
139
+
140
+ info: svg`<circle cx="24" cy="24" r="17"/><path d="M24 22 V33"/><circle cx="24" cy="15.5" r="1.8" fill="currentColor" stroke="none"/>`,
141
+
142
+ add: svg`<path d="M24 10 V38 M10 24 H38"/>`,
143
+
144
+ remove: svg`<path d="M10 24 H38"/>`,
145
+
146
+ edit: svg`<path d="M30 8 L40 18 L18 40 L7 41 L8 30 Z"/><path d="M26 12 L36 22"/>`,
147
+
148
+ delete: svg`<path d="M10 14 H38"/><path d="M17 14 V9 A2 2 0 0 1 19 7 H29 A2 2 0 0 1 31 9 V14"/><path d="M13 14 L15 40 A2 2 0 0 0 17 42 H31 A2 2 0 0 0 33 40 L35 14"/><path d="M20 21 V35 M28 21 V35"/>`,
149
+
150
+ filter: svg`<path d="M6 10 H42 L28 26 V38 L20 42 V26 Z"/>`,
151
+
152
+ sort: svg`<path d="M14 30 V10 M14 10 L8 16 M14 10 L20 16"/><path d="M34 18 V38 M34 38 L28 32 M34 38 L40 32"/>`,
153
+
154
+ calendar: svg`<rect x="6" y="10" width="36" height="32" rx="3"/><path d="M6 20 H42"/><path d="M15 6 V14 M33 6 V14"/><circle cx="15" cy="28" r="1.6" fill="currentColor" stroke="none"/><circle cx="24" cy="28" r="1.6" fill="currentColor" stroke="none"/><circle cx="33" cy="28" r="1.6" fill="currentColor" stroke="none"/>`,
155
+
156
+ clock: svg`<circle cx="24" cy="24" r="17"/><path d="M24 14 V24 L32 29"/>`,
157
+
158
+ person: svg`<circle cx="24" cy="16" r="8"/><path d="M8 42 C8 31 15 26 24 26 C33 26 40 31 40 42"/>`,
159
+
160
+ people: svg`<circle cx="17" cy="16" r="7"/><circle cx="33" cy="18" r="6"/><path d="M4 41 C4 31 10 27 17 27 C21 27 24 28.3 26.3 30.5"/><path d="M24 41 C24 32 29 28 37 28 C43 28 44 32 44 41"/>`,
161
+
162
+ lock: svg`<rect x="10" y="21" width="28" height="21" rx="3"/><path d="M16 21 V15 A8 8 0 0 1 32 15 V21"/><circle cx="24" cy="31" r="2.2" fill="currentColor" stroke="none"/>`,
163
+
164
+ eye: svg`<path d="M4 24 C10 12 20 8 24 8 C28 8 38 12 44 24 C38 36 28 40 24 40 C20 40 10 36 4 24 Z"/><circle cx="24" cy="24" r="6"/>`,
165
+
166
+ 'eye-slash': svg`<path d="M4 24 C10 12 20 8 24 8 C28 8 38 12 44 24 C38 36 28 40 24 40 C20 40 10 36 4 24 Z"/><circle cx="24" cy="24" r="6"/><path d="M6 6 L42 42"/>`,
167
+
168
+ refresh: svg`<path d="M46 8 L46 20 L34 20"/><path d="M2 40 L2 28 L14 28"/><path d="M7.02 18 A18 18 0 0 1 36.72 11.28 L46 20"/><path d="M2 28 L11.28 36.72 A18 18 0 0 0 40.98 30"/>`,
169
+
170
+ // Phase 2: OpenVox marketing site --------------------------------------
171
+
172
+ community: svg`<circle cx="24" cy="10" r="5"/><circle cx="10" cy="34" r="5"/><circle cx="38" cy="34" r="5"/><path d="M24 15 L14 30 M24 15 L34 30 M15 34 H33"/>`,
173
+
174
+ book: svg`<path d="M24 12 C20 8 12 7 6 9 V37 C12 35 20 36 24 40 C28 36 36 35 42 37 V9 C36 7 28 8 24 12 Z"/><path d="M24 12 V40"/>`,
175
+
176
+ terminal: svg`<rect x="6" y="9" width="36" height="30" rx="3"/><path d="M14 19 L21 24 L14 29"/><path d="M25 30 H33"/>`,
177
+
178
+ shield: svg`<path d="M24 6 L40 12 V22 C40 33 33 40 24 43 C15 40 8 33 8 22 V12 Z"/><path d="M17 23 L22 28 L32 17"/>`,
179
+
180
+ roadmap: svg`<path d="M6 38 C14 38 14 26 22 26 C30 26 30 14 38 14"/><circle cx="6" cy="38" r="3" fill="currentColor" stroke="none"/><circle cx="22" cy="26" r="3" fill="currentColor" stroke="none"/><circle cx="38" cy="14" r="3" fill="currentColor" stroke="none"/>`,
181
+
182
+ help: svg`<circle cx="24" cy="24" r="17"/><path d="M18 18 C18 13 30 13 30 19 C30 24 24 23 24 29"/><circle cx="24" cy="35" r="1.8" fill="currentColor" stroke="none"/>`,
183
+
184
+ repository: svg`<rect x="7" y="10" width="34" height="28" rx="3"/><path d="M18 20 L13 24 L18 28 M30 20 L35 24 L30 28"/>`,
185
+
186
+ star: svg`<path d="M24 6 L29 19 L43 19 L32 28 L36 42 L24 34 L12 42 L16 28 L5 19 L19 19 Z"/>`,
187
+
188
+ chat: svg`<path d="M8 10 H40 A2 2 0 0 1 42 12 V30 A2 2 0 0 1 40 32 H20 L12 40 V32 H8 A2 2 0 0 1 6 30 V12 A2 2 0 0 1 8 10 Z"/><path d="M14 18 H34 M14 24 H28"/>`,
189
+
190
+ heart: svg`<path d="M24 41 C10 32 4 23 4 15.5 C4 9 9 5 15 5 C19.5 5 22.5 7.5 24 11 C25.5 7.5 28.5 5 33 5 C39 5 44 9 44 15.5 C44 23 38 32 24 41 Z"/>`,
191
+
192
+ // Phase 3: module registry (Forge replacement) -------------------------
193
+
194
+ module: svg`<rect x="8" y="16" width="32" height="24" rx="3"/><path d="M8 24 H40"/><path d="M20 16 L24 24 L28 16"/>`,
195
+
196
+ tag: svg`<path d="M6 10 H26 L42 26 L26 42 H6 Z"/><circle cx="15" cy="19" r="3" fill="currentColor" stroke="none"/>`,
197
+
198
+ dependency: svg`<rect x="6" y="19" width="20" height="10" rx="5"/><rect x="22" y="19" width="20" height="10" rx="5"/>`,
199
+
200
+ verified: svg`<path d="M24 5 L31 10 L39 9 L40 17 L46 24 L40 31 L39 39 L31 38 L24 43 L17 38 L9 39 L8 31 L2 24 L8 17 L9 9 L17 10 Z"/><path d="M16 24 L21 29 L32 18"/>`,
201
+
202
+ archived: svg`<rect x="6" y="6" width="36" height="10" rx="2"/><rect x="10" y="16" width="28" height="26" rx="2"/><path d="M20 26 H28"/>`,
203
+
204
+ publish: svg`<path d="M24 6 V28 M24 6 L16 14 M24 6 L32 14"/><path d="M8 32 V38 A2 2 0 0 0 10 40 H38 A2 2 0 0 0 40 38 V32"/>`,
205
+
206
+ changelog: svg`<path d="M10 6 H28 L36 14 V42 H10 Z"/><path d="M28 6 V14 H36"/><path d="M16 24 H24"/><circle cx="34" cy="34" r="7"/><path d="M34 30 V34 L37 36"/>`,
207
+
208
+ vulnerability: svg`<path d="M24 6 L40 12 V22 C40 33 33 40 24 43 C15 40 8 33 8 22 V12 Z"/><path d="M24 16 V26"/><circle cx="24" cy="32" r="1.8" fill="currentColor" stroke="none"/>`,
209
+
210
+ gauge: svg`<path d="M6 34 A18 18 0 0 1 42 34"/><path d="M24 34 L34 20"/><circle cx="24" cy="34" r="2.2" fill="currentColor" stroke="none"/>`,
211
+
212
+ 'file-tree': svg`<path d="M6 12 H18 L22 17 H42 V38 H6 Z"/><path d="M26 24 H36 M26 30 H36"/>`,
213
+
214
+ 'check-circle': svg`<circle cx="24" cy="24" r="17"/><path d="M15 24 L21 30 L33 17"/>`,
215
+
216
+ 'x-circle': svg`<circle cx="24" cy="24" r="17"/><path d="M18 18 L30 30 M30 18 L18 30"/>`,
217
+
218
+ fork: svg`<circle cx="14" cy="10" r="4"/><circle cx="34" cy="10" r="4"/><circle cx="24" cy="38" r="4"/><path d="M14 14 V22 L24 32 M34 14 V22 L24 32 M24 32 V34"/>`,
219
+
220
+ trending: svg`<path d="M6 34 L18 22 L26 28 L42 10"/><path d="M32 10 H42 V20"/>`,
221
+
222
+ collection: svg`<rect x="10" y="6" width="28" height="10" rx="2"/><rect x="6" y="19" width="36" height="10" rx="2"/><rect x="10" y="32" width="28" height="10" rx="2"/>`,
223
+
224
+ // Phase 4: fleet console (Puppet Enterprise replacement) ---------------
225
+
226
+ dashboard: svg`<rect x="6" y="6" width="18" height="14" rx="2"/><rect x="28" y="6" width="14" height="8" rx="2"/><rect x="28" y="18" width="14" height="14" rx="2"/><rect x="6" y="24" width="18" height="18" rx="2"/>`,
227
+
228
+ node: svg`<rect x="10" y="14" width="28" height="20" rx="3"/><path d="M16 22 H22 M16 27 H22"/><circle cx="32" cy="24" r="2" fill="currentColor" stroke="none"/>`,
229
+
230
+ 'node-group': svg`<rect x="6" y="10" width="24" height="9" rx="2"/><rect x="12" y="21" width="24" height="9" rx="2"/><rect x="18" y="32" width="24" height="9" rx="2"/>`,
231
+
232
+ pulse: svg`<path d="M4 24 H14 L19 12 L27 36 L32 24 H44"/>`,
233
+
234
+ compliance: svg`<rect x="9" y="6" width="30" height="36" rx="3"/><path d="M16 16 L19 19 L26 12 M16 26 L19 29 L26 22 M16 36 H30"/>`,
235
+
236
+ report: svg`<path d="M10 6 H28 L36 14 V42 H10 Z"/><path d="M28 6 V14 H36"/><path d="M16 34 V28 M22 34 V24 M28 34 V30"/>`,
237
+
238
+ 'activity-log': svg`<path d="M14 8 V40"/><circle cx="14" cy="12" r="3" fill="currentColor" stroke="none"/><circle cx="14" cy="24" r="3" fill="currentColor" stroke="none"/><circle cx="14" cy="36" r="3" fill="currentColor" stroke="none"/><path d="M22 12 H40 M22 24 H40 M22 36 H34"/>`,
239
+
240
+ bell: svg`<path d="M12 32 V22 A12 12 0 0 1 36 22 V32 L40 38 H8 Z"/><path d="M20 38 A4 4 0 0 0 28 38"/>`,
241
+
242
+ orchestrate: svg`<circle cx="24" cy="24" r="17"/><path d="M19 15 L33 24 L19 33 Z"/>`,
243
+
244
+ plan: svg`<rect x="6" y="8" width="24" height="32" rx="3"/><path d="M12 16 H24 M12 22 H24 M12 28 H20"/><path d="M32 24 H44 M38 18 L44 24 L38 30"/>`,
245
+
246
+ badge: svg`<rect x="6" y="10" width="36" height="28" rx="3"/><circle cx="17" cy="21" r="5"/><path d="M10 32 C10 26 13 24 17 24 C21 24 24 26 24 32"/><path d="M30 18 H38 M30 24 H38 M30 30 H36"/>`,
247
+
248
+ audit: svg`<path d="M10 6 H26 L34 14 V42 H10 Z"/><path d="M26 6 V14 H34"/><path d="M16 24 H24 M16 30 H22"/><circle cx="33" cy="33" r="6"/><path d="M37.5 37.5 L43 43"/>`,
249
+
250
+ key: svg`<circle cx="16" cy="24" r="9"/><path d="M23 24 H42 M34 24 V31 M40 24 V29"/>`,
251
+
252
+ layers: svg`<path d="M24 6 L44 16 L24 26 L4 16 Z"/><path d="M4 26 L24 36 L44 26"/><path d="M4 34 L24 44 L44 34"/>`,
253
+
254
+ classifier: svg`<path d="M6 10 H24 L40 26 L24 42 H6 Z"/><circle cx="14" cy="18" r="3" fill="currentColor" stroke="none"/><circle cx="32" cy="26" r="3" fill="currentColor" stroke="none"/>`,
255
+
256
+ deploy: svg`<path d="M24 8 V34"/><path d="M24 8 L15 20 M24 8 L33 20"/><path d="M10 40 H38"/>`,
257
+
258
+ metrics: svg`<path d="M6 40 H42"/><path d="M6 40 V6"/><path d="M10 30 L18 22 L26 27 L38 12"/>`,
259
+
260
+ schedule: svg`<rect x="6" y="10" width="28" height="30" rx="3"/><path d="M6 18 H34"/><path d="M14 6 V14 M26 6 V14"/><circle cx="34" cy="34" r="10"/><path d="M34 28 V34 L38 37"/>`,
261
+
262
+ backup: svg`<path d="M24 6 V26 M24 26 L16 18 M24 26 L32 18"/><rect x="8" y="30" width="32" height="12" rx="2"/><path d="M8 36 H40"/>`,
263
+
264
+ webhook: svg`<circle cx="12" cy="14" r="6"/><circle cx="36" cy="34" r="6"/><path d="M17 17 L31 31"/><path d="M22 22 L20 28 L26 26 L24 32"/>`,
265
+
266
+ organization: svg`<rect x="10" y="10" width="28" height="32" rx="2"/><path d="M17 18 H21 M27 18 H31 M17 26 H21 M27 26 H31 M17 34 H21 M27 34 H31"/>`,
267
+
268
+ drift: svg`<rect x="6" y="12" width="20" height="20" rx="3"/><rect x="22" y="16" width="20" height="20" rx="3"/><path d="M20 24 H28"/>`,
269
+
270
+ // Added later: consolidated from docs pages that predated vox-icon ------
271
+
272
+ blocks: svg`<rect x="7" y="7" width="15" height="15" rx="2.5"/><rect x="26" y="7" width="15" height="15" rx="2.5"/><rect x="7" y="26" width="15" height="15" rx="2.5"/><rect x="26" y="26" width="15" height="15" rx="2.5"/>`,
273
+
274
+ plug: svg`<path d="M16 6 V18 M32 6 V18 M12 18 H36 V28 C36 34 30 38 24 38 C18 38 12 34 12 28 Z"/><path d="M24 38 V44"/>`,
275
+
276
+ ballot: svg`<rect x="8" y="18" width="32" height="24" rx="2.5"/><path d="M8 26 H40"/><path d="M24 10 V26"/><path d="M18 6 L24 12 L30 6"/>`,
277
+
278
+ sun: svg`<circle cx="24" cy="24" r="8"/><path d="M24 4v4M24 40v4M9.86 9.86l2.82 2.82M35.32 35.32l2.82 2.82M4 24h4M40 24h4M12.68 35.32l-2.82 2.82M38.14 9.86l-2.82 2.82"/>`,
279
+
280
+ moon: svg`<path d="M42 25.58A18 18 0 1 1 22.42 6 14 14 0 0 0 42 25.58Z"/>`,
281
+
282
+ github: svg`<g transform="translate(4,4) scale(2.5)" fill="currentColor" stroke="none"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></g>`,
283
+
284
+ accessibility: svg`<circle cx="24" cy="24" r="18"/><circle cx="24" cy="16" r="2.6" fill="currentColor" stroke="none"/><path d="M14 21 H34 M24 21 V32 M24 25 L18 34 M24 25 L30 34"/>`,
285
+
286
+ settings: svg`<circle cx="24" cy="24" r="7"/><path d="M24 6 V12 M24 36 V42 M6 24 H12 M36 24 H42 M11 11 L15.2 15.2 M32.8 32.8 L37 37 M37 11 L32.8 15.2 M15.2 32.8 L11 37"/>`,
287
+ };
@@ -0,0 +1,78 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { ICON_PATHS, type IconName } from './icon-paths.js';
4
+
5
+ export type { IconName };
6
+ export type IconSize = 'sm' | 'md' | 'lg' | 'xl';
7
+
8
+ /**
9
+ * One icon from the shared VoxBlocks icon set, drawn as inline SVG so
10
+ * it inherits `color` from its context like text does. Decorative by
11
+ * default (`aria-hidden`); set `label` when the icon is the only
12
+ * content conveying meaning (e.g. an icon-only button).
13
+ */
14
+ @customElement('vox-icon')
15
+ export class VoxIcon extends LitElement {
16
+ @property() name: IconName = 'info';
17
+ @property({ reflect: true }) size: IconSize = 'md';
18
+ @property() label?: string;
19
+
20
+ static styles = css`
21
+ :host {
22
+ display: inline-flex;
23
+ flex: none;
24
+ color: inherit;
25
+ }
26
+
27
+ svg {
28
+ display: block;
29
+ }
30
+
31
+ :host([size='sm']) svg {
32
+ width: 16px;
33
+ height: 16px;
34
+ }
35
+
36
+ :host([size='md']) svg {
37
+ width: 20px;
38
+ height: 20px;
39
+ }
40
+
41
+ :host([size='lg']) svg {
42
+ width: 24px;
43
+ height: 24px;
44
+ }
45
+
46
+ :host([size='xl']) svg {
47
+ width: 48px;
48
+ height: 48px;
49
+ }
50
+ `;
51
+
52
+ render() {
53
+ const path = ICON_PATHS[this.name];
54
+ if (!path) return nothing;
55
+
56
+ return html`
57
+ <svg
58
+ viewBox="0 0 48 48"
59
+ fill="none"
60
+ stroke="currentColor"
61
+ stroke-width="2"
62
+ stroke-linecap="round"
63
+ stroke-linejoin="round"
64
+ role=${this.label ? 'img' : nothing}
65
+ aria-hidden=${this.label ? nothing : 'true'}
66
+ aria-label=${this.label ?? nothing}
67
+ >
68
+ ${path}
69
+ </svg>
70
+ `;
71
+ }
72
+ }
73
+
74
+ declare global {
75
+ interface HTMLElementTagNameMap {
76
+ 'vox-icon': VoxIcon;
77
+ }
78
+ }
@@ -56,6 +56,8 @@ export class VoxInput extends VoxFieldElement {
56
56
  ?readonly=${this.readonly}
57
57
  ?disabled=${this.disabled}
58
58
  aria-label=${this.label ? nothing : 'text input'}
59
+ aria-describedby=${ifDefined(this.noteId)}
60
+ aria-invalid=${this.invalid ? 'true' : 'false'}
59
61
  @input=${this.handleInput}
60
62
  @change=${this.handleChange}
61
63
  />
@@ -25,6 +25,7 @@ export class VoxLinkHub extends LitElement {
25
25
  * One link in a `<vox-link-hub>`.
26
26
  *
27
27
  * @slot - Short description under the link title.
28
+ * @slot icon - Optional icon before the heading, e.g. `<vox-icon>`.
28
29
  */
29
30
  @customElement('vox-link-hub-item')
30
31
  export class VoxLinkHubItem extends LitElement {
@@ -37,6 +38,15 @@ export class VoxLinkHubItem extends LitElement {
37
38
  font-family: var(--vox-font-family-base);
38
39
  }
39
40
 
41
+ .icon {
42
+ display: flex;
43
+ flex: none;
44
+ }
45
+
46
+ .icon:not(.has-icon) {
47
+ display: none;
48
+ }
49
+
40
50
  a {
41
51
  display: block;
42
52
  height: 100%;
@@ -84,10 +94,21 @@ export class VoxLinkHubItem extends LitElement {
84
94
  }
85
95
  `;
86
96
 
97
+ private hasIcon = false;
98
+
99
+ private handleIconSlotChange(event: Event) {
100
+ const slot = event.target as HTMLSlotElement;
101
+ this.hasIcon = slot.assignedNodes({ flatten: true }).length > 0;
102
+ this.requestUpdate();
103
+ }
104
+
87
105
  render() {
88
106
  return html`
89
107
  <a href=${this.href}>
90
108
  <span class="heading">
109
+ <span class="icon ${this.hasIcon ? 'has-icon' : ''}" aria-hidden="true">
110
+ <slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
111
+ </span>
91
112
  ${this.heading}
92
113
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
93
114
  <path d="M5 12h14" />
@@ -2,18 +2,20 @@ import { LitElement, html, css } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
 
4
4
  /**
5
- * Page-number navigation. Children are plain links; mark the current
6
- * page with `aria-current="page"`:
5
+ * Page-number navigation. Children are plain links, with the current
6
+ * page as a non-interactive `<span aria-current="page">` — like
7
+ * `<vox-breadcrumbs>`, it shouldn't be a link to itself:
7
8
  *
8
9
  * ```html
9
10
  * <vox-pagination>
10
11
  * <a href="?page=1">1</a>
11
- * <a href="?page=2" aria-current="page">2</a>
12
+ * <span aria-current="page">2</span>
12
13
  * <a href="?page=3">3</a>
13
14
  * </vox-pagination>
14
15
  * ```
15
16
  *
16
- * @slot - `<a>` elements (including any prev/next links).
17
+ * @slot - `<a>` elements (including any prev/next links) and the
18
+ * current-page `<span>`.
17
19
  */
18
20
  @customElement('vox-pagination')
19
21
  export class VoxPagination extends LitElement {
@@ -31,7 +33,8 @@ export class VoxPagination extends LitElement {
31
33
  flex-wrap: wrap;
32
34
  }
33
35
 
34
- ::slotted(a) {
36
+ ::slotted(a),
37
+ ::slotted(span) {
35
38
  display: inline-flex;
36
39
  align-items: center;
37
40
  justify-content: center;
@@ -53,7 +56,7 @@ export class VoxPagination extends LitElement {
53
56
  background-color: var(--vox-color-brand-soft);
54
57
  }
55
58
 
56
- ::slotted(a[aria-current='page']) {
59
+ ::slotted([aria-current='page']) {
57
60
  background-color: var(--vox-color-brand-3);
58
61
  color: var(--vox-color-text-inverse);
59
62
  font-weight: 600;