@openvoxproject/voxblocks 0.9.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 (97) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +66 -0
  3. package/dist/cdn/voxblocks.css +1 -0
  4. package/dist/cdn/voxblocks.js +5057 -0
  5. package/dist/types/components/accordion/vox-accordion.d.ts +32 -0
  6. package/dist/types/components/alert/vox-alert.d.ts +23 -0
  7. package/dist/types/components/avatar/vox-avatar.d.ts +18 -0
  8. package/dist/types/components/badge/vox-badge.d.ts +17 -0
  9. package/dist/types/components/billboard/vox-billboard.d.ts +22 -0
  10. package/dist/types/components/breadcrumbs/vox-breadcrumbs.d.ts +24 -0
  11. package/dist/types/components/button/vox-button.d.ts +24 -0
  12. package/dist/types/components/calendar-tile/vox-calendar-tile.d.ts +18 -0
  13. package/dist/types/components/callout/vox-callout.d.ts +20 -0
  14. package/dist/types/components/card/vox-card.d.ts +28 -0
  15. package/dist/types/components/checkbox/vox-checkbox.d.ts +20 -0
  16. package/dist/types/components/cta/vox-cta.d.ts +17 -0
  17. package/dist/types/components/cta-band/vox-cta-band.d.ts +22 -0
  18. package/dist/types/components/dialog/vox-dialog.d.ts +30 -0
  19. package/dist/types/components/disclosure/vox-disclosure.d.ts +19 -0
  20. package/dist/types/components/dropdown/vox-dropdown.d.ts +29 -0
  21. package/dist/types/components/empty-state/vox-empty-state.d.ts +18 -0
  22. package/dist/types/components/file-input/vox-file-input.d.ts +22 -0
  23. package/dist/types/components/footer/vox-footer.d.ts +28 -0
  24. package/dist/types/components/grid/vox-grid.d.ts +22 -0
  25. package/dist/types/components/header/vox-header.d.ts +23 -0
  26. package/dist/types/components/hero/vox-hero.d.ts +21 -0
  27. package/dist/types/components/input/vox-input.d.ts +24 -0
  28. package/dist/types/components/input-group/vox-input-group.d.ts +25 -0
  29. package/dist/types/components/link-hub/vox-link-hub.d.ts +27 -0
  30. package/dist/types/components/loader/vox-loader.d.ts +16 -0
  31. package/dist/types/components/pagination/vox-pagination.d.ts +25 -0
  32. package/dist/types/components/quote/vox-quote.d.ts +18 -0
  33. package/dist/types/components/radio/vox-radio-group.d.ts +23 -0
  34. package/dist/types/components/radio/vox-radio.d.ts +21 -0
  35. package/dist/types/components/select/vox-select.d.ts +27 -0
  36. package/dist/types/components/series-nav/vox-series-nav.d.ts +18 -0
  37. package/dist/types/components/sidenav/vox-sidenav.d.ts +50 -0
  38. package/dist/types/components/sponsor/vox-sponsor.d.ts +35 -0
  39. package/dist/types/components/stat/vox-stat.d.ts +17 -0
  40. package/dist/types/components/step-indicator/vox-step-indicator.d.ts +28 -0
  41. package/dist/types/components/subnav/vox-subnav.d.ts +18 -0
  42. package/dist/types/components/switch/vox-switch.d.ts +20 -0
  43. package/dist/types/components/tabs/vox-tabs.d.ts +57 -0
  44. package/dist/types/components/textarea/vox-textarea.d.ts +22 -0
  45. package/dist/types/components/theme-toggle/vox-theme-toggle.d.ts +35 -0
  46. package/dist/types/components/timeline/vox-timeline.d.ts +27 -0
  47. package/dist/types/index.d.ts +51 -0
  48. package/dist/types/internal/field.d.ts +27 -0
  49. package/dist/voxblocks.css +1 -0
  50. package/dist/voxblocks.js +4350 -0
  51. package/package.json +59 -0
  52. package/src/components/accordion/vox-accordion.ts +150 -0
  53. package/src/components/alert/vox-alert.ts +155 -0
  54. package/src/components/avatar/vox-avatar.ts +80 -0
  55. package/src/components/badge/vox-badge.ts +67 -0
  56. package/src/components/billboard/vox-billboard.ts +109 -0
  57. package/src/components/breadcrumbs/vox-breadcrumbs.ts +68 -0
  58. package/src/components/button/vox-button.ts +165 -0
  59. package/src/components/calendar-tile/vox-calendar-tile.ts +74 -0
  60. package/src/components/callout/vox-callout.ts +97 -0
  61. package/src/components/card/vox-card.ts +147 -0
  62. package/src/components/checkbox/vox-checkbox.ts +101 -0
  63. package/src/components/cta/vox-cta.ts +69 -0
  64. package/src/components/cta-band/vox-cta-band.ts +105 -0
  65. package/src/components/dialog/vox-dialog.ts +176 -0
  66. package/src/components/disclosure/vox-disclosure.ts +98 -0
  67. package/src/components/dropdown/vox-dropdown.ts +186 -0
  68. package/src/components/empty-state/vox-empty-state.ts +76 -0
  69. package/src/components/file-input/vox-file-input.ts +127 -0
  70. package/src/components/footer/vox-footer.ts +109 -0
  71. package/src/components/grid/vox-grid.ts +54 -0
  72. package/src/components/header/vox-header.ts +109 -0
  73. package/src/components/hero/vox-hero.ts +100 -0
  74. package/src/components/input/vox-input.ts +72 -0
  75. package/src/components/input-group/vox-input-group.ts +85 -0
  76. package/src/components/link-hub/vox-link-hub.ts +108 -0
  77. package/src/components/loader/vox-loader.ts +82 -0
  78. package/src/components/pagination/vox-pagination.ts +76 -0
  79. package/src/components/quote/vox-quote.ts +82 -0
  80. package/src/components/radio/vox-radio-group.ts +117 -0
  81. package/src/components/radio/vox-radio.ts +102 -0
  82. package/src/components/select/vox-select.ts +89 -0
  83. package/src/components/series-nav/vox-series-nav.ts +94 -0
  84. package/src/components/sidenav/vox-sidenav.ts +192 -0
  85. package/src/components/sponsor/vox-sponsor.ts +187 -0
  86. package/src/components/stat/vox-stat.ts +55 -0
  87. package/src/components/step-indicator/vox-step-indicator.ts +152 -0
  88. package/src/components/subnav/vox-subnav.ts +64 -0
  89. package/src/components/switch/vox-switch.ts +90 -0
  90. package/src/components/tabs/vox-tabs.ts +201 -0
  91. package/src/components/textarea/vox-textarea.ts +75 -0
  92. package/src/components/theme-toggle/vox-theme-toggle.ts +164 -0
  93. package/src/components/timeline/vox-timeline.ts +100 -0
  94. package/src/index.ts +65 -0
  95. package/src/internal/field.ts +165 -0
  96. package/src/styles/utilities.css +409 -0
  97. package/src/tokens/tokens.css +161 -0
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@openvoxproject/voxblocks",
3
+ "version": "0.9.0",
4
+ "description": "Web components for Vox Pupuli OpenVox community web sites and apps",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/OpenVoxProject/voxblocks.git"
10
+ },
11
+ "keywords": [
12
+ "web-components",
13
+ "lit",
14
+ "design-system",
15
+ "voxpupuli",
16
+ "openvox"
17
+ ],
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.stories.ts"
22
+ ],
23
+ "main": "./dist/voxblocks.js",
24
+ "module": "./dist/voxblocks.js",
25
+ "types": "./dist/types/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/types/index.d.ts",
29
+ "default": "./dist/voxblocks.js"
30
+ },
31
+ "./voxblocks.css": "./dist/voxblocks.css",
32
+ "./cdn": "./dist/cdn/voxblocks.js",
33
+ "./cdn/voxblocks.css": "./dist/cdn/voxblocks.css"
34
+ },
35
+ "scripts": {
36
+ "prepare": "npm run build",
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",
41
+ "storybook": "storybook dev -p 6006",
42
+ "build-storybook": "storybook build"
43
+ },
44
+ "dependencies": {
45
+ "lit": "^3.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "@storybook/web-components": "^10.5.5",
49
+ "@storybook/web-components-vite": "^10.5.5",
50
+ "storybook": "^10.5.5",
51
+ "typescript": "^5.6.0",
52
+ "vite": "^6.0.0",
53
+ "vitepress": "^1.5.0"
54
+ },
55
+ "allowScripts": {
56
+ "esbuild@0.25.12": true,
57
+ "esbuild@0.21.5": true
58
+ }
59
+ }
@@ -0,0 +1,150 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A stack of expandable panels.
6
+ *
7
+ * @slot - `<vox-accordion-item>` elements.
8
+ */
9
+ @customElement('vox-accordion')
10
+ export class VoxAccordion extends LitElement {
11
+ /** Allow only one panel open at a time. */
12
+ @property({ type: Boolean }) single = false;
13
+
14
+ static styles = css`
15
+ :host {
16
+ display: block;
17
+ border: 1px solid var(--vox-color-divider);
18
+ border-radius: var(--vox-radius-md);
19
+ overflow: hidden;
20
+ }
21
+
22
+ ::slotted(vox-accordion-item:not(:first-child)) {
23
+ border-top: 1px solid var(--vox-color-divider);
24
+ }
25
+ `;
26
+
27
+ private handleToggle(event: Event) {
28
+ if (!this.single) return;
29
+ const opened = event.target as VoxAccordionItem;
30
+ if (!opened.open) return;
31
+ this.querySelectorAll<VoxAccordionItem>('vox-accordion-item').forEach(
32
+ (item) => {
33
+ if (item !== opened) item.open = false;
34
+ },
35
+ );
36
+ }
37
+
38
+ render() {
39
+ return html`<slot @vox-toggle=${this.handleToggle}></slot>`;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * One panel of a `<vox-accordion>`.
45
+ *
46
+ * @slot - Panel content.
47
+ * @fires vox-toggle - When the panel opens or closes.
48
+ */
49
+ @customElement('vox-accordion-item')
50
+ export class VoxAccordionItem extends LitElement {
51
+ @property() heading = '';
52
+ @property({ type: Boolean, reflect: true }) open = false;
53
+
54
+ static styles = css`
55
+ :host {
56
+ display: block;
57
+ font-family: var(--vox-font-family-base);
58
+ }
59
+
60
+ .trigger {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ gap: var(--vox-space-3);
65
+ width: 100%;
66
+ padding: var(--vox-space-4);
67
+ background: none;
68
+ border: none;
69
+ color: var(--vox-color-text-1);
70
+ font-family: inherit;
71
+ font-size: 15px;
72
+ font-weight: 600;
73
+ text-align: left;
74
+ cursor: pointer;
75
+ }
76
+
77
+ .trigger:hover {
78
+ color: var(--vox-color-brand-1);
79
+ }
80
+
81
+ .trigger:focus-visible {
82
+ outline: 2px solid var(--vox-color-brand-1);
83
+ outline-offset: -2px;
84
+ }
85
+
86
+ .chevron {
87
+ flex: none;
88
+ width: 16px;
89
+ height: 16px;
90
+ transition: transform var(--vox-transition-fast);
91
+ }
92
+
93
+ :host([open]) .chevron {
94
+ transform: rotate(180deg);
95
+ }
96
+
97
+ .panel {
98
+ padding: 0 var(--vox-space-4) var(--vox-space-4);
99
+ font-size: 14px;
100
+ line-height: 1.7;
101
+ color: var(--vox-color-text-2);
102
+ }
103
+
104
+ .panel[hidden] {
105
+ display: none;
106
+ }
107
+
108
+ ::slotted(p:first-child) {
109
+ margin-top: 0;
110
+ }
111
+
112
+ ::slotted(p:last-child) {
113
+ margin-bottom: 0;
114
+ }
115
+ `;
116
+
117
+ toggle() {
118
+ this.open = !this.open;
119
+ this.dispatchEvent(
120
+ new CustomEvent('vox-toggle', { bubbles: true, composed: true }),
121
+ );
122
+ }
123
+
124
+ render() {
125
+ return html`
126
+ <h3 style="margin:0">
127
+ <button
128
+ class="trigger"
129
+ aria-expanded=${this.open ? 'true' : 'false'}
130
+ @click=${this.toggle}
131
+ >
132
+ ${this.heading}
133
+ <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
134
+ <path d="m6 9 6 6 6-6" />
135
+ </svg>
136
+ </button>
137
+ </h3>
138
+ <div class="panel" ?hidden=${!this.open}>
139
+ <slot></slot>
140
+ </div>
141
+ `;
142
+ }
143
+ }
144
+
145
+ declare global {
146
+ interface HTMLElementTagNameMap {
147
+ 'vox-accordion': VoxAccordion;
148
+ 'vox-accordion-item': VoxAccordionItem;
149
+ }
150
+ }
@@ -0,0 +1,155 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ export type AlertVariant = 'info' | 'success' | 'warning' | 'danger';
5
+
6
+ /**
7
+ * A page-level notification message, optionally dismissible.
8
+ * For static advisory text inside prose, see `<vox-callout>`.
9
+ *
10
+ * @slot - Alert message.
11
+ * @fires vox-dismiss - When the user dismisses the alert.
12
+ */
13
+ @customElement('vox-alert')
14
+ export class VoxAlert extends LitElement {
15
+ @property() variant: AlertVariant = 'info';
16
+ @property() heading = '';
17
+ @property({ type: Boolean }) dismissible = false;
18
+ @property({ type: Boolean, reflect: true }) open = true;
19
+
20
+ static styles = css`
21
+ :host {
22
+ display: block;
23
+ font-family: var(--vox-font-family-base);
24
+ }
25
+
26
+ :host(:not([open])) {
27
+ display: none;
28
+ }
29
+
30
+ .alert {
31
+ display: flex;
32
+ gap: var(--vox-space-3);
33
+ align-items: flex-start;
34
+ padding: var(--vox-space-4);
35
+ border-radius: var(--vox-radius-md);
36
+ border-left: 4px solid;
37
+ font-size: 14px;
38
+ line-height: 1.6;
39
+ color: var(--vox-color-text-1);
40
+ }
41
+
42
+ .body {
43
+ flex: 1 1 auto;
44
+ }
45
+
46
+ .heading {
47
+ margin: 0 0 var(--vox-space-1);
48
+ font-size: 14px;
49
+ font-weight: 600;
50
+ }
51
+
52
+ .info {
53
+ background-color: var(--vox-color-brand-soft);
54
+ border-color: var(--vox-color-brand-1);
55
+ }
56
+ .info .heading {
57
+ color: var(--vox-color-brand-1);
58
+ }
59
+
60
+ .success {
61
+ background-color: var(--vox-color-tip-soft);
62
+ border-color: var(--vox-color-tip-1);
63
+ }
64
+ .success .heading {
65
+ color: var(--vox-color-tip-1);
66
+ }
67
+
68
+ .warning {
69
+ background-color: var(--vox-color-warning-soft);
70
+ border-color: var(--vox-color-warning-1);
71
+ }
72
+ .warning .heading {
73
+ color: var(--vox-color-warning-1);
74
+ }
75
+
76
+ .danger {
77
+ background-color: var(--vox-color-danger-soft);
78
+ border-color: var(--vox-color-danger-1);
79
+ }
80
+ .danger .heading {
81
+ color: var(--vox-color-danger-1);
82
+ }
83
+
84
+ .close {
85
+ flex: none;
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ width: 24px;
90
+ height: 24px;
91
+ padding: 0;
92
+ background: none;
93
+ border: none;
94
+ border-radius: var(--vox-radius-sm);
95
+ color: var(--vox-color-text-2);
96
+ cursor: pointer;
97
+ }
98
+
99
+ .close:hover {
100
+ color: var(--vox-color-text-1);
101
+ }
102
+
103
+ .close:focus-visible {
104
+ outline: 2px solid var(--vox-color-brand-1);
105
+ }
106
+
107
+ .close svg {
108
+ width: 14px;
109
+ height: 14px;
110
+ }
111
+
112
+ ::slotted(p:first-child) {
113
+ margin-top: 0;
114
+ }
115
+
116
+ ::slotted(p:last-child) {
117
+ margin-bottom: 0;
118
+ }
119
+ `;
120
+
121
+ dismiss() {
122
+ this.open = false;
123
+ this.dispatchEvent(
124
+ new CustomEvent('vox-dismiss', { bubbles: true, composed: true }),
125
+ );
126
+ }
127
+
128
+ render() {
129
+ return html`
130
+ <div class="alert ${this.variant}" role="alert">
131
+ <div class="body">
132
+ ${this.heading
133
+ ? html`<p class="heading">${this.heading}</p>`
134
+ : nothing}
135
+ <slot></slot>
136
+ </div>
137
+ ${this.dismissible
138
+ ? html`
139
+ <button class="close" aria-label="Dismiss" @click=${this.dismiss}>
140
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
141
+ <path d="M18 6 6 18M6 6l12 12" />
142
+ </svg>
143
+ </button>
144
+ `
145
+ : nothing}
146
+ </div>
147
+ `;
148
+ }
149
+ }
150
+
151
+ declare global {
152
+ interface HTMLElementTagNameMap {
153
+ 'vox-alert': VoxAlert;
154
+ }
155
+ }
@@ -0,0 +1,80 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
5
+
6
+ /**
7
+ * A circular avatar showing an image, or initials as a fallback.
8
+ */
9
+ @customElement('vox-avatar')
10
+ export class VoxAvatar extends LitElement {
11
+ @property() src?: string;
12
+ @property() alt = '';
13
+ @property() initials = '';
14
+ @property({ reflect: true }) size: AvatarSize = 'md';
15
+
16
+ static styles = css`
17
+ :host {
18
+ display: inline-block;
19
+ }
20
+
21
+ .avatar {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ overflow: hidden;
26
+ border-radius: 50%;
27
+ background-color: var(--vox-color-brand-soft);
28
+ color: var(--vox-color-brand-1);
29
+ font-family: var(--vox-font-family-base);
30
+ font-weight: 600;
31
+ user-select: none;
32
+ }
33
+
34
+ img {
35
+ width: 100%;
36
+ height: 100%;
37
+ object-fit: cover;
38
+ }
39
+
40
+ :host([size='sm']) .avatar {
41
+ width: 28px;
42
+ height: 28px;
43
+ font-size: 11px;
44
+ }
45
+
46
+ :host([size='md']) .avatar {
47
+ width: 40px;
48
+ height: 40px;
49
+ font-size: 14px;
50
+ }
51
+
52
+ :host([size='lg']) .avatar {
53
+ width: 56px;
54
+ height: 56px;
55
+ font-size: 20px;
56
+ }
57
+
58
+ :host([size='xl']) .avatar {
59
+ width: 80px;
60
+ height: 80px;
61
+ font-size: 28px;
62
+ }
63
+ `;
64
+
65
+ render() {
66
+ return html`
67
+ <span class="avatar" role=${this.src ? 'presentation' : 'img'} aria-label=${this.alt}>
68
+ ${this.src
69
+ ? html`<img src=${this.src} alt=${this.alt} />`
70
+ : this.initials}
71
+ </span>
72
+ `;
73
+ }
74
+ }
75
+
76
+ declare global {
77
+ interface HTMLElementTagNameMap {
78
+ 'vox-avatar': VoxAvatar;
79
+ }
80
+ }
@@ -0,0 +1,67 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ export type BadgeVariant = 'brand' | 'tip' | 'warning' | 'danger' | 'neutral';
5
+
6
+ /**
7
+ * A small status pill, e.g. for version labels or deprecation notices.
8
+ *
9
+ * @slot - Badge label.
10
+ */
11
+ @customElement('vox-badge')
12
+ export class VoxBadge extends LitElement {
13
+ @property() variant: BadgeVariant = 'brand';
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: inline-block;
18
+ }
19
+
20
+ .badge {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ padding: 2px 10px;
24
+ border-radius: var(--vox-radius-full);
25
+ font-family: var(--vox-font-family-base);
26
+ font-size: 12px;
27
+ font-weight: 600;
28
+ line-height: 1.6;
29
+ white-space: nowrap;
30
+ }
31
+
32
+ .brand {
33
+ background-color: var(--vox-color-brand-soft);
34
+ color: var(--vox-color-brand-1);
35
+ }
36
+
37
+ .tip {
38
+ background-color: var(--vox-color-tip-soft);
39
+ color: var(--vox-color-tip-1);
40
+ }
41
+
42
+ .warning {
43
+ background-color: var(--vox-color-warning-soft);
44
+ color: var(--vox-color-warning-1);
45
+ }
46
+
47
+ .danger {
48
+ background-color: var(--vox-color-danger-soft);
49
+ color: var(--vox-color-danger-1);
50
+ }
51
+
52
+ .neutral {
53
+ background-color: var(--vox-color-bg-soft);
54
+ color: var(--vox-color-text-2);
55
+ }
56
+ `;
57
+
58
+ render() {
59
+ return html`<span class="badge ${this.variant}"><slot></slot></span>`;
60
+ }
61
+ }
62
+
63
+ declare global {
64
+ interface HTMLElementTagNameMap {
65
+ 'vox-badge': VoxBadge;
66
+ }
67
+ }
@@ -0,0 +1,109 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A split media-and-content section for landing pages.
6
+ *
7
+ * @slot media - Image or other media shown beside the content.
8
+ * @slot - Body content.
9
+ * @slot actions - Buttons or CTA links below the body.
10
+ */
11
+ @customElement('vox-billboard')
12
+ export class VoxBillboard extends LitElement {
13
+ @property() heading = '';
14
+
15
+ /** Put the media on the right instead of the left. */
16
+ @property({ type: Boolean, reflect: true }) reverse = false;
17
+
18
+ static styles = css`
19
+ :host {
20
+ display: block;
21
+ font-family: var(--vox-font-family-base);
22
+ }
23
+
24
+ .billboard {
25
+ display: flex;
26
+ gap: var(--vox-space-8);
27
+ align-items: center;
28
+ flex-wrap: wrap;
29
+ padding: var(--vox-space-8);
30
+ background-color: var(--vox-color-bg-soft);
31
+ border-radius: var(--vox-radius-lg);
32
+ }
33
+
34
+ :host([reverse]) .billboard {
35
+ flex-direction: row-reverse;
36
+ }
37
+
38
+ .media {
39
+ flex: 1 1 280px;
40
+ min-width: 0;
41
+ }
42
+
43
+ .media ::slotted(img) {
44
+ display: block;
45
+ max-width: 100%;
46
+ border-radius: var(--vox-radius-md);
47
+ }
48
+
49
+ .content {
50
+ flex: 1 1 320px;
51
+ min-width: 0;
52
+ }
53
+
54
+ .heading {
55
+ margin: 0 0 var(--vox-space-3);
56
+ font-family: var(--vox-font-family-display);
57
+ font-size: 28px;
58
+ font-weight: 600;
59
+ line-height: 1.3;
60
+ color: var(--vox-color-text-1);
61
+ }
62
+
63
+ .body {
64
+ font-size: 16px;
65
+ line-height: 1.7;
66
+ color: var(--vox-color-text-2);
67
+ }
68
+
69
+ .actions {
70
+ display: flex;
71
+ gap: var(--vox-space-3);
72
+ flex-wrap: wrap;
73
+ margin-top: var(--vox-space-6);
74
+ }
75
+
76
+ .actions:not(.has-content) {
77
+ display: none;
78
+ }
79
+ `;
80
+
81
+ private hasActions = false;
82
+
83
+ private handleActionsSlotChange(event: Event) {
84
+ const slot = event.target as HTMLSlotElement;
85
+ this.hasActions = slot.assignedNodes({ flatten: true }).length > 0;
86
+ this.requestUpdate();
87
+ }
88
+
89
+ render() {
90
+ return html`
91
+ <div class="billboard">
92
+ <div class="media"><slot name="media"></slot></div>
93
+ <div class="content">
94
+ <h2 class="heading">${this.heading}</h2>
95
+ <div class="body"><slot></slot></div>
96
+ <div class="actions ${this.hasActions ? 'has-content' : ''}">
97
+ <slot name="actions" @slotchange=${this.handleActionsSlotChange}></slot>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ `;
102
+ }
103
+ }
104
+
105
+ declare global {
106
+ interface HTMLElementTagNameMap {
107
+ 'vox-billboard': VoxBillboard;
108
+ }
109
+ }
@@ -0,0 +1,68 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A breadcrumb trail. Children are plain links, with the current page
6
+ * as a `<span aria-current="page">`:
7
+ *
8
+ * ```html
9
+ * <vox-breadcrumbs>
10
+ * <a href="/">Home</a>
11
+ * <a href="/modules">Modules</a>
12
+ * <span aria-current="page">puppet-nginx</span>
13
+ * </vox-breadcrumbs>
14
+ * ```
15
+ *
16
+ * @slot - Links and the current-page span.
17
+ */
18
+ @customElement('vox-breadcrumbs')
19
+ export class VoxBreadcrumbs extends LitElement {
20
+ static styles = css`
21
+ :host {
22
+ display: block;
23
+ font-family: var(--vox-font-family-base);
24
+ }
25
+
26
+ nav {
27
+ display: flex;
28
+ align-items: center;
29
+ flex-wrap: wrap;
30
+ font-size: 13px;
31
+ }
32
+
33
+ ::slotted(*) {
34
+ color: var(--vox-color-text-2);
35
+ text-decoration: none;
36
+ }
37
+
38
+ ::slotted(a:hover) {
39
+ color: var(--vox-color-brand-1);
40
+ text-decoration: underline;
41
+ }
42
+
43
+ ::slotted([aria-current='page']) {
44
+ color: var(--vox-color-text-1);
45
+ font-weight: 600;
46
+ }
47
+
48
+ ::slotted(*:not(:first-child))::before {
49
+ content: '/';
50
+ margin: 0 var(--vox-space-2);
51
+ color: var(--vox-color-text-3);
52
+ }
53
+ `;
54
+
55
+ render() {
56
+ return html`
57
+ <nav aria-label="Breadcrumbs">
58
+ <slot></slot>
59
+ </nav>
60
+ `;
61
+ }
62
+ }
63
+
64
+ declare global {
65
+ interface HTMLElementTagNameMap {
66
+ 'vox-breadcrumbs': VoxBreadcrumbs;
67
+ }
68
+ }