@openvoxproject/voxblocks 0.9.0 → 0.10.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openvoxproject/voxblocks",
3
- "version": "0.9.0",
4
- "description": "Web components for Vox Pupuli OpenVox community web sites and apps",
3
+ "version": "0.10.0",
4
+ "description": "Web components for OpenVox community web sites and apps",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -35,9 +35,11 @@
35
35
  "scripts": {
36
36
  "prepare": "npm run build",
37
37
  "build": "vite build && vite build --config vite.cdn.config.ts && tsc -p tsconfig.build.json",
38
- "docs:dev": "vitepress dev docs",
39
- "docs:build": "vitepress build docs",
40
- "docs:preview": "vitepress preview docs",
38
+ "docs:dev": "rm -rf docs/assets docs/_data/manifest.json && concurrently -n vite,jekyll -c blue,magenta \"npm:docs:assets:watch\" \"npm:docs:jekyll:wait\"",
39
+ "docs:assets:watch": "vite build --config docs/vite.config.js --watch",
40
+ "docs:jekyll:wait": "wait-on docs/_data/manifest.json && npm run docs:jekyll",
41
+ "docs:jekyll": "BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll serve --livereload --source docs --destination docs/_site",
42
+ "docs:build": "vite build --config docs/vite.config.js && BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll build --source docs --destination docs/_site --config docs/_config.yml,docs/_config_production.yml",
41
43
  "storybook": "storybook dev -p 6006",
42
44
  "build-storybook": "storybook build"
43
45
  },
@@ -47,10 +49,11 @@
47
49
  "devDependencies": {
48
50
  "@storybook/web-components": "^10.5.5",
49
51
  "@storybook/web-components-vite": "^10.5.5",
52
+ "concurrently": "^9.1.0",
50
53
  "storybook": "^10.5.5",
51
54
  "typescript": "^5.6.0",
52
55
  "vite": "^6.0.0",
53
- "vitepress": "^1.5.0"
56
+ "wait-on": "^9.1.0"
54
57
  },
55
58
  "allowScripts": {
56
59
  "esbuild@0.25.12": true,
@@ -1,10 +1,19 @@
1
1
  import { LitElement, html, css, nothing } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import { ICON_PATHS, type IconName } from '../icon/icon-paths.js';
3
4
 
4
5
  export type AlertVariant = 'info' | 'success' | 'warning' | 'danger';
5
6
 
7
+ const VARIANT_ICON: Record<AlertVariant, IconName> = {
8
+ info: 'info',
9
+ success: 'check-circle',
10
+ warning: 'warning',
11
+ danger: 'x-circle',
12
+ };
13
+
6
14
  /**
7
- * A page-level notification message, optionally dismissible.
15
+ * A page-level notification message, optionally dismissible. Shows an
16
+ * icon matching `variant` automatically — no icon slot to fill in.
8
17
  * For static advisory text inside prose, see `<vox-callout>`.
9
18
  *
10
19
  * @slot - Alert message.
@@ -43,6 +52,13 @@ export class VoxAlert extends LitElement {
43
52
  flex: 1 1 auto;
44
53
  }
45
54
 
55
+ .icon {
56
+ flex: none;
57
+ width: 20px;
58
+ height: 20px;
59
+ margin-top: 1px;
60
+ }
61
+
46
62
  .heading {
47
63
  margin: 0 0 var(--vox-space-1);
48
64
  font-size: 14px;
@@ -56,6 +72,9 @@ export class VoxAlert extends LitElement {
56
72
  .info .heading {
57
73
  color: var(--vox-color-brand-1);
58
74
  }
75
+ .info .icon {
76
+ color: var(--vox-color-brand-1);
77
+ }
59
78
 
60
79
  .success {
61
80
  background-color: var(--vox-color-tip-soft);
@@ -64,6 +83,9 @@ export class VoxAlert extends LitElement {
64
83
  .success .heading {
65
84
  color: var(--vox-color-tip-1);
66
85
  }
86
+ .success .icon {
87
+ color: var(--vox-color-tip-1);
88
+ }
67
89
 
68
90
  .warning {
69
91
  background-color: var(--vox-color-warning-soft);
@@ -72,6 +94,9 @@ export class VoxAlert extends LitElement {
72
94
  .warning .heading {
73
95
  color: var(--vox-color-warning-1);
74
96
  }
97
+ .warning .icon {
98
+ color: var(--vox-color-warning-1);
99
+ }
75
100
 
76
101
  .danger {
77
102
  background-color: var(--vox-color-danger-soft);
@@ -80,6 +105,9 @@ export class VoxAlert extends LitElement {
80
105
  .danger .heading {
81
106
  color: var(--vox-color-danger-1);
82
107
  }
108
+ .danger .icon {
109
+ color: var(--vox-color-danger-1);
110
+ }
83
111
 
84
112
  .close {
85
113
  flex: none;
@@ -128,6 +156,18 @@ export class VoxAlert extends LitElement {
128
156
  render() {
129
157
  return html`
130
158
  <div class="alert ${this.variant}" role="alert">
159
+ <svg
160
+ class="icon"
161
+ viewBox="0 0 48 48"
162
+ fill="none"
163
+ stroke="currentColor"
164
+ stroke-width="2"
165
+ stroke-linecap="round"
166
+ stroke-linejoin="round"
167
+ aria-hidden="true"
168
+ >
169
+ ${ICON_PATHS[VARIANT_ICON[this.variant]]}
170
+ </svg>
131
171
  <div class="body">
132
172
  ${this.heading
133
173
  ? html`<p class="heading">${this.heading}</p>`
@@ -1,5 +1,6 @@
1
1
  import { LitElement, html, css } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
+ import { ICON_PATHS, type IconName } from '../icon/icon-paths.js';
3
4
 
4
5
  export type CalloutVariant = 'info' | 'tip' | 'warning' | 'danger';
5
6
 
@@ -10,9 +11,17 @@ const DEFAULT_HEADINGS: Record<CalloutVariant, string> = {
10
11
  danger: 'Danger',
11
12
  };
12
13
 
14
+ const VARIANT_ICON: Record<CalloutVariant, IconName> = {
15
+ info: 'info',
16
+ tip: 'check-circle',
17
+ warning: 'warning',
18
+ danger: 'x-circle',
19
+ };
20
+
13
21
  /**
14
22
  * An admonition block matching the custom containers on the OpenVox docs
15
- * (::: tip, ::: warning, ...).
23
+ * (::: tip, ::: warning, ...). Shows an icon matching `variant`
24
+ * automatically — no icon slot to fill in.
16
25
  *
17
26
  * @slot - Callout body content.
18
27
  */
@@ -29,6 +38,9 @@ export class VoxCallout extends LitElement {
29
38
  }
30
39
 
31
40
  .callout {
41
+ display: flex;
42
+ gap: var(--vox-space-3);
43
+ align-items: flex-start;
32
44
  border-radius: var(--vox-radius-md);
33
45
  padding: var(--vox-space-4);
34
46
  font-family: var(--vox-font-family-base);
@@ -37,6 +49,18 @@ export class VoxCallout extends LitElement {
37
49
  color: var(--vox-color-text-2);
38
50
  }
39
51
 
52
+ .icon {
53
+ flex: none;
54
+ width: 18px;
55
+ height: 18px;
56
+ margin-top: 2px;
57
+ }
58
+
59
+ .content {
60
+ flex: 1 1 auto;
61
+ min-width: 0;
62
+ }
63
+
40
64
  .heading {
41
65
  margin: 0 0 var(--vox-space-2);
42
66
  font-size: 14px;
@@ -49,6 +73,9 @@ export class VoxCallout extends LitElement {
49
73
  .info .heading {
50
74
  color: var(--vox-color-text-1);
51
75
  }
76
+ .info .icon {
77
+ color: var(--vox-color-text-1);
78
+ }
52
79
 
53
80
  .tip {
54
81
  background-color: var(--vox-color-tip-soft);
@@ -56,6 +83,9 @@ export class VoxCallout extends LitElement {
56
83
  .tip .heading {
57
84
  color: var(--vox-color-tip-1);
58
85
  }
86
+ .tip .icon {
87
+ color: var(--vox-color-tip-1);
88
+ }
59
89
 
60
90
  .warning {
61
91
  background-color: var(--vox-color-warning-soft);
@@ -63,6 +93,9 @@ export class VoxCallout extends LitElement {
63
93
  .warning .heading {
64
94
  color: var(--vox-color-warning-1);
65
95
  }
96
+ .warning .icon {
97
+ color: var(--vox-color-warning-1);
98
+ }
66
99
 
67
100
  .danger {
68
101
  background-color: var(--vox-color-danger-soft);
@@ -70,6 +103,9 @@ export class VoxCallout extends LitElement {
70
103
  .danger .heading {
71
104
  color: var(--vox-color-danger-1);
72
105
  }
106
+ .danger .icon {
107
+ color: var(--vox-color-danger-1);
108
+ }
73
109
 
74
110
  ::slotted(p:first-child) {
75
111
  margin-top: 0;
@@ -83,8 +119,22 @@ export class VoxCallout extends LitElement {
83
119
  render() {
84
120
  return html`
85
121
  <div class="callout ${this.variant}" role="note">
86
- <p class="heading">${this.heading ?? DEFAULT_HEADINGS[this.variant]}</p>
87
- <slot></slot>
122
+ <svg
123
+ class="icon"
124
+ viewBox="0 0 48 48"
125
+ fill="none"
126
+ stroke="currentColor"
127
+ stroke-width="2"
128
+ stroke-linecap="round"
129
+ stroke-linejoin="round"
130
+ aria-hidden="true"
131
+ >
132
+ ${ICON_PATHS[VARIANT_ICON[this.variant]]}
133
+ </svg>
134
+ <div class="content">
135
+ <p class="heading">${this.heading ?? DEFAULT_HEADINGS[this.variant]}</p>
136
+ <slot></slot>
137
+ </div>
88
138
  </div>
89
139
  `;
90
140
  }
@@ -32,6 +32,11 @@ export class VoxCard extends LitElement {
32
32
  border-radius: var(--vox-radius-lg);
33
33
  padding: var(--vox-space-6);
34
34
  font-family: var(--vox-font-family-base);
35
+ /* Slotted content (e.g. an icon using stroke="currentColor")
36
+ inherits from this element's position in the flat tree — when
37
+ rendered as an <a>, that means the browser's default link color
38
+ without this, since nothing else in .card sets one. */
39
+ color: var(--vox-color-text-1);
35
40
  text-decoration: none;
36
41
  transition: border-color var(--vox-transition-base);
37
42
  }
@@ -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,73 @@ 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
+ }
161
+
162
+ disconnectedCallback() {
163
+ super.disconnectedCallback();
164
+ document.removeEventListener('click', this.handleOutsideClick);
165
+ this.removeEventListener('keydown', this.handleKeydown);
166
+ }
167
+
168
+ private handleOutsideClick = (event: MouseEvent) => {
169
+ if (this.mobileOpen && !event.composedPath().includes(this)) {
170
+ this.mobileOpen = false;
171
+ }
172
+ };
173
+
174
+ private handleKeydown = (event: KeyboardEvent) => {
175
+ if (event.key === 'Escape' && this.mobileOpen) {
176
+ this.mobileOpen = false;
177
+ this.renderRoot.querySelector<HTMLElement>('.menu-toggle')?.focus();
178
+ }
179
+ };
180
+
181
+ private toggleMobileMenu = () => {
182
+ this.mobileOpen = !this.mobileOpen;
183
+ };
184
+
185
+ private closeMobileMenu = () => {
186
+ this.mobileOpen = false;
187
+ };
188
+
87
189
  render() {
88
190
  return html`
89
191
  <header class="header">
@@ -91,11 +193,34 @@ export class VoxHeader extends LitElement {
91
193
  <slot name="logo"></slot>
92
194
  ${this.siteTitle ? html`<span>${this.siteTitle}</span>` : nothing}
93
195
  </a>
94
- <nav aria-label="Main">
95
- <slot></slot>
96
- </nav>
97
- <div class="actions">
98
- <slot name="actions"></slot>
196
+ <button
197
+ type="button"
198
+ class="menu-toggle"
199
+ aria-expanded=${this.mobileOpen ? 'true' : 'false'}
200
+ aria-controls="nav-wrap"
201
+ aria-label=${this.mobileOpen ? 'Close menu' : 'Open menu'}
202
+ @click=${this.toggleMobileMenu}
203
+ >
204
+ <svg
205
+ class="menu-icon"
206
+ viewBox="0 0 48 48"
207
+ fill="none"
208
+ stroke="currentColor"
209
+ stroke-width="2"
210
+ stroke-linecap="round"
211
+ stroke-linejoin="round"
212
+ aria-hidden="true"
213
+ >
214
+ ${this.mobileOpen ? ICON_PATHS.close : ICON_PATHS.menu}
215
+ </svg>
216
+ </button>
217
+ <div id="nav-wrap" class="nav-wrap${this.mobileOpen ? ' open' : ''}">
218
+ <nav aria-label="Main">
219
+ <slot @click=${this.closeMobileMenu}></slot>
220
+ </nav>
221
+ <div class="actions">
222
+ <slot name="actions"></slot>
223
+ </div>
99
224
  </div>
100
225
  </header>
101
226
  `;