@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
@@ -0,0 +1,105 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A centered, full-width call-to-action panel: heading, supporting copy,
6
+ * and a row of buttons — the closing pitch at the bottom of a page.
7
+ *
8
+ * @slot - Supporting copy under the heading.
9
+ * @slot actions - Buttons or CTA links.
10
+ */
11
+ @customElement('vox-cta-band')
12
+ export class VoxCtaBand extends LitElement {
13
+ @property() heading = '';
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: block;
18
+ font-family: var(--vox-font-family-base);
19
+ }
20
+
21
+ .band {
22
+ text-align: center;
23
+ padding: var(--vox-space-8);
24
+ background-color: var(--vox-color-bg-soft);
25
+ border-radius: var(--vox-radius-lg);
26
+ }
27
+
28
+ .heading {
29
+ margin: 0;
30
+ font-family: var(--vox-font-family-display);
31
+ font-size: 28px;
32
+ font-weight: 600;
33
+ line-height: 1.3;
34
+ color: var(--vox-color-text-1);
35
+ }
36
+
37
+ .body {
38
+ max-width: 36rem;
39
+ margin: var(--vox-space-3) auto 0;
40
+ font-size: 16px;
41
+ line-height: 1.6;
42
+ color: var(--vox-color-text-2);
43
+ }
44
+
45
+ .body:not(.has-content) {
46
+ display: none;
47
+ }
48
+
49
+ .body ::slotted(a) {
50
+ color: var(--vox-color-brand-1);
51
+ text-decoration: underline;
52
+ }
53
+
54
+ .body ::slotted(a:hover) {
55
+ color: var(--vox-color-brand-2);
56
+ }
57
+
58
+ .actions {
59
+ display: flex;
60
+ gap: var(--vox-space-3);
61
+ justify-content: center;
62
+ flex-wrap: wrap;
63
+ margin-top: var(--vox-space-6);
64
+ }
65
+
66
+ .actions:not(.has-content) {
67
+ display: none;
68
+ }
69
+ `;
70
+
71
+ private hasBody = false;
72
+ private hasActions = false;
73
+
74
+ private handleBodySlotChange(event: Event) {
75
+ const slot = event.target as HTMLSlotElement;
76
+ this.hasBody = slot.assignedNodes({ flatten: true }).length > 0;
77
+ this.requestUpdate();
78
+ }
79
+
80
+ private handleActionsSlotChange(event: Event) {
81
+ const slot = event.target as HTMLSlotElement;
82
+ this.hasActions = slot.assignedNodes({ flatten: true }).length > 0;
83
+ this.requestUpdate();
84
+ }
85
+
86
+ render() {
87
+ return html`
88
+ <div class="band">
89
+ ${this.heading ? html`<h2 class="heading">${this.heading}</h2>` : nothing}
90
+ <div class="body ${this.hasBody ? 'has-content' : ''}">
91
+ <slot @slotchange=${this.handleBodySlotChange}></slot>
92
+ </div>
93
+ <div class="actions ${this.hasActions ? 'has-content' : ''}">
94
+ <slot name="actions" @slotchange=${this.handleActionsSlotChange}></slot>
95
+ </div>
96
+ </div>
97
+ `;
98
+ }
99
+ }
100
+
101
+ declare global {
102
+ interface HTMLElementTagNameMap {
103
+ 'vox-cta-band': VoxCtaBand;
104
+ }
105
+ }
@@ -0,0 +1,176 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property, query } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A modal dialog built on the native `<dialog>` element, which provides
6
+ * focus trapping and Escape-to-close for free.
7
+ *
8
+ * @slot - Dialog body.
9
+ * @slot footer - Action buttons.
10
+ * @fires vox-close - When the dialog closes.
11
+ */
12
+ @customElement('vox-dialog')
13
+ export class VoxDialog extends LitElement {
14
+ @property() heading = '';
15
+ @property({ type: Boolean }) open = false;
16
+
17
+ /** Close when the backdrop is clicked. */
18
+ @property({ type: Boolean, attribute: 'light-dismiss' }) lightDismiss = false;
19
+
20
+ @query('dialog') private dialogEl!: HTMLDialogElement;
21
+
22
+ static styles = css`
23
+ dialog {
24
+ box-sizing: border-box;
25
+ width: min(90vw, 480px);
26
+ padding: 0;
27
+ border: 1px solid var(--vox-color-divider);
28
+ border-radius: var(--vox-radius-lg);
29
+ background-color: var(--vox-color-bg-elv);
30
+ color: var(--vox-color-text-1);
31
+ font-family: var(--vox-font-family-base);
32
+ box-shadow: var(--vox-shadow-2);
33
+ }
34
+
35
+ dialog::backdrop {
36
+ background-color: rgba(0, 0, 0, 0.5);
37
+ }
38
+
39
+ .header {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ gap: var(--vox-space-3);
44
+ padding: var(--vox-space-4) var(--vox-space-6);
45
+ border-bottom: 1px solid var(--vox-color-divider);
46
+ }
47
+
48
+ .heading {
49
+ margin: 0;
50
+ font-size: 16px;
51
+ font-weight: 600;
52
+ }
53
+
54
+ .close {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ width: 28px;
59
+ height: 28px;
60
+ padding: 0;
61
+ background: none;
62
+ border: none;
63
+ border-radius: var(--vox-radius-sm);
64
+ color: var(--vox-color-text-2);
65
+ cursor: pointer;
66
+ }
67
+
68
+ .close:hover {
69
+ color: var(--vox-color-text-1);
70
+ background-color: var(--vox-color-bg-soft);
71
+ }
72
+
73
+ .close:focus-visible {
74
+ outline: 2px solid var(--vox-color-brand-1);
75
+ }
76
+
77
+ .close svg {
78
+ width: 16px;
79
+ height: 16px;
80
+ }
81
+
82
+ .body {
83
+ padding: var(--vox-space-6);
84
+ font-size: 14px;
85
+ line-height: 1.7;
86
+ color: var(--vox-color-text-2);
87
+ }
88
+
89
+ .footer {
90
+ display: flex;
91
+ justify-content: flex-end;
92
+ gap: var(--vox-space-3);
93
+ padding: var(--vox-space-4) var(--vox-space-6);
94
+ border-top: 1px solid var(--vox-color-divider);
95
+ }
96
+
97
+ .footer:not(.has-content) {
98
+ display: none;
99
+ }
100
+
101
+ ::slotted(p:first-child) {
102
+ margin-top: 0;
103
+ }
104
+
105
+ ::slotted(p:last-child) {
106
+ margin-bottom: 0;
107
+ }
108
+ `;
109
+
110
+ private hasFooter = false;
111
+
112
+ show() {
113
+ this.open = true;
114
+ }
115
+
116
+ close() {
117
+ this.open = false;
118
+ }
119
+
120
+ updated() {
121
+ if (this.open && !this.dialogEl.open) {
122
+ this.dialogEl.showModal();
123
+ } else if (!this.open && this.dialogEl.open) {
124
+ this.dialogEl.close();
125
+ }
126
+ }
127
+
128
+ private handleNativeClose() {
129
+ this.open = false;
130
+ this.dispatchEvent(
131
+ new CustomEvent('vox-close', { bubbles: true, composed: true }),
132
+ );
133
+ }
134
+
135
+ private handleClick(event: MouseEvent) {
136
+ // A click on the backdrop targets the <dialog> element itself.
137
+ if (this.lightDismiss && event.target === this.dialogEl) {
138
+ this.close();
139
+ }
140
+ }
141
+
142
+ private handleFooterSlotChange(event: Event) {
143
+ const slot = event.target as HTMLSlotElement;
144
+ this.hasFooter = slot.assignedNodes({ flatten: true }).length > 0;
145
+ this.requestUpdate();
146
+ }
147
+
148
+ render() {
149
+ return html`
150
+ <dialog
151
+ aria-label=${this.heading || nothing}
152
+ @close=${this.handleNativeClose}
153
+ @click=${this.handleClick}
154
+ >
155
+ <div class="header">
156
+ <h2 class="heading">${this.heading}</h2>
157
+ <button class="close" aria-label="Close dialog" @click=${this.close}>
158
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
159
+ <path d="M18 6 6 18M6 6l12 12" />
160
+ </svg>
161
+ </button>
162
+ </div>
163
+ <div class="body"><slot></slot></div>
164
+ <div class="footer ${this.hasFooter ? 'has-content' : ''}">
165
+ <slot name="footer" @slotchange=${this.handleFooterSlotChange}></slot>
166
+ </div>
167
+ </dialog>
168
+ `;
169
+ }
170
+ }
171
+
172
+ declare global {
173
+ interface HTMLElementTagNameMap {
174
+ 'vox-dialog': VoxDialog;
175
+ }
176
+ }
@@ -0,0 +1,98 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A single show/hide toggle for supplementary content.
6
+ *
7
+ * @slot - Hidden content.
8
+ * @fires vox-toggle - When the content is shown or hidden.
9
+ */
10
+ @customElement('vox-disclosure')
11
+ export class VoxDisclosure extends LitElement {
12
+ @property() summary = 'Show details';
13
+ @property({ type: Boolean, reflect: true }) open = false;
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: block;
18
+ font-family: var(--vox-font-family-base);
19
+ }
20
+
21
+ .trigger {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ gap: var(--vox-space-2);
25
+ padding: 0;
26
+ background: none;
27
+ border: none;
28
+ color: var(--vox-color-brand-1);
29
+ font-family: inherit;
30
+ font-size: 14px;
31
+ font-weight: 600;
32
+ cursor: pointer;
33
+ }
34
+
35
+ .trigger:hover {
36
+ color: var(--vox-color-brand-2);
37
+ text-decoration: underline;
38
+ }
39
+
40
+ .trigger:focus-visible {
41
+ outline: 2px solid var(--vox-color-brand-1);
42
+ outline-offset: 2px;
43
+ border-radius: var(--vox-radius-sm);
44
+ }
45
+
46
+ .chevron {
47
+ width: 14px;
48
+ height: 14px;
49
+ transition: transform var(--vox-transition-fast);
50
+ }
51
+
52
+ :host([open]) .chevron {
53
+ transform: rotate(90deg);
54
+ }
55
+
56
+ .panel {
57
+ margin-top: var(--vox-space-3);
58
+ font-size: 14px;
59
+ line-height: 1.7;
60
+ color: var(--vox-color-text-2);
61
+ }
62
+
63
+ .panel[hidden] {
64
+ display: none;
65
+ }
66
+ `;
67
+
68
+ toggle() {
69
+ this.open = !this.open;
70
+ this.dispatchEvent(
71
+ new CustomEvent('vox-toggle', { bubbles: true, composed: true }),
72
+ );
73
+ }
74
+
75
+ render() {
76
+ return html`
77
+ <button
78
+ class="trigger"
79
+ aria-expanded=${this.open ? 'true' : 'false'}
80
+ @click=${this.toggle}
81
+ >
82
+ <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">
83
+ <path d="m9 6 6 6-6 6" />
84
+ </svg>
85
+ ${this.summary}
86
+ </button>
87
+ <div class="panel" ?hidden=${!this.open}>
88
+ <slot></slot>
89
+ </div>
90
+ `;
91
+ }
92
+ }
93
+
94
+ declare global {
95
+ interface HTMLElementTagNameMap {
96
+ 'vox-disclosure': VoxDisclosure;
97
+ }
98
+ }
@@ -0,0 +1,186 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A menu button. Menu entries are light-DOM `<a>` or `<button>` children:
6
+ *
7
+ * ```html
8
+ * <vox-dropdown label="Resources">
9
+ * <a href="/docs">Documentation</a>
10
+ * <button type="button">Copy install command</button>
11
+ * </vox-dropdown>
12
+ * ```
13
+ *
14
+ * @slot - Menu entries (`<a>`, `<button>`, `<hr>` for separators).
15
+ */
16
+ @customElement('vox-dropdown')
17
+ export class VoxDropdown extends LitElement {
18
+ @property() label = 'Menu';
19
+ @property({ type: Boolean, reflect: true }) open = false;
20
+
21
+ static styles = css`
22
+ :host {
23
+ position: relative;
24
+ display: inline-block;
25
+ font-family: var(--vox-font-family-base);
26
+ }
27
+
28
+ .trigger {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ gap: var(--vox-space-2);
32
+ padding: 0 var(--vox-space-4);
33
+ height: 38px;
34
+ background-color: var(--vox-color-bg-soft);
35
+ border: 1px solid var(--vox-color-divider);
36
+ border-radius: var(--vox-radius-md);
37
+ color: var(--vox-color-text-1);
38
+ font-family: inherit;
39
+ font-size: 14px;
40
+ font-weight: 600;
41
+ cursor: pointer;
42
+ transition:
43
+ border-color var(--vox-transition-fast),
44
+ color var(--vox-transition-fast);
45
+ }
46
+
47
+ .trigger:hover {
48
+ border-color: var(--vox-color-brand-1);
49
+ color: var(--vox-color-brand-1);
50
+ }
51
+
52
+ .trigger:focus-visible {
53
+ outline: 2px solid var(--vox-color-brand-1);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ .chevron {
58
+ width: 14px;
59
+ height: 14px;
60
+ transition: transform var(--vox-transition-fast);
61
+ }
62
+
63
+ :host([open]) .chevron {
64
+ transform: rotate(180deg);
65
+ }
66
+
67
+ .menu {
68
+ position: absolute;
69
+ top: calc(100% + 4px);
70
+ left: 0;
71
+ z-index: 10;
72
+ min-width: max(100%, 180px);
73
+ display: none;
74
+ flex-direction: column;
75
+ padding: var(--vox-space-2);
76
+ background-color: var(--vox-color-bg-elv);
77
+ border: 1px solid var(--vox-color-divider);
78
+ border-radius: var(--vox-radius-md);
79
+ box-shadow: var(--vox-shadow-2);
80
+ }
81
+
82
+ :host([open]) .menu {
83
+ display: flex;
84
+ }
85
+
86
+ ::slotted(a),
87
+ ::slotted(button) {
88
+ display: block;
89
+ width: 100%;
90
+ box-sizing: border-box;
91
+ padding: var(--vox-space-2) var(--vox-space-3);
92
+ background: none;
93
+ border: none;
94
+ border-radius: var(--vox-radius-sm);
95
+ color: var(--vox-color-text-1);
96
+ font-family: inherit;
97
+ font-size: 14px;
98
+ text-align: left;
99
+ text-decoration: none;
100
+ cursor: pointer;
101
+ white-space: nowrap;
102
+ }
103
+
104
+ ::slotted(a:hover),
105
+ ::slotted(button:hover),
106
+ ::slotted(a:focus-visible),
107
+ ::slotted(button:focus-visible) {
108
+ background-color: var(--vox-color-brand-soft);
109
+ color: var(--vox-color-brand-1);
110
+ outline: none;
111
+ }
112
+
113
+ ::slotted(hr) {
114
+ width: 100%;
115
+ margin: var(--vox-space-1) 0;
116
+ border: none;
117
+ border-top: 1px solid var(--vox-color-divider);
118
+ }
119
+ `;
120
+
121
+ connectedCallback() {
122
+ super.connectedCallback();
123
+ document.addEventListener('click', this.handleOutsideClick);
124
+ this.addEventListener('keydown', this.handleKeydown);
125
+ }
126
+
127
+ disconnectedCallback() {
128
+ super.disconnectedCallback();
129
+ document.removeEventListener('click', this.handleOutsideClick);
130
+ this.removeEventListener('keydown', this.handleKeydown);
131
+ }
132
+
133
+ private handleOutsideClick = (event: MouseEvent) => {
134
+ if (this.open && !event.composedPath().includes(this)) {
135
+ this.open = false;
136
+ }
137
+ };
138
+
139
+ private handleKeydown = (event: KeyboardEvent) => {
140
+ if (event.key === 'Escape' && this.open) {
141
+ this.open = false;
142
+ this.renderRoot.querySelector<HTMLElement>('.trigger')?.focus();
143
+ return;
144
+ }
145
+
146
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && this.open) {
147
+ event.preventDefault();
148
+ const items = [...this.querySelectorAll<HTMLElement>('a, button')];
149
+ if (items.length === 0) return;
150
+ const active = document.activeElement as HTMLElement;
151
+ const index = items.indexOf(active);
152
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
153
+ const next = items[(Math.max(index, 0) + delta + items.length) % items.length];
154
+ next.focus();
155
+ }
156
+ };
157
+
158
+ private toggle() {
159
+ this.open = !this.open;
160
+ }
161
+
162
+ render() {
163
+ return html`
164
+ <button
165
+ class="trigger"
166
+ aria-expanded=${this.open ? 'true' : 'false'}
167
+ aria-haspopup="true"
168
+ @click=${this.toggle}
169
+ >
170
+ ${this.label}
171
+ <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">
172
+ <path d="m6 9 6 6 6-6" />
173
+ </svg>
174
+ </button>
175
+ <div class="menu">
176
+ <slot @click=${() => (this.open = false)}></slot>
177
+ </div>
178
+ `;
179
+ }
180
+ }
181
+
182
+ declare global {
183
+ interface HTMLElementTagNameMap {
184
+ 'vox-dropdown': VoxDropdown;
185
+ }
186
+ }
@@ -0,0 +1,76 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A placeholder for views with nothing to show yet.
6
+ *
7
+ * @slot icon - Icon or emoji above the heading.
8
+ * @slot - Explanatory text.
9
+ * @slot actions - Buttons or links suggesting a next step.
10
+ */
11
+ @customElement('vox-empty-state')
12
+ export class VoxEmptyState extends LitElement {
13
+ @property() heading = '';
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: block;
18
+ font-family: var(--vox-font-family-base);
19
+ }
20
+
21
+ .empty {
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ text-align: center;
26
+ padding: var(--vox-space-8);
27
+ border: 1px dashed var(--vox-color-border);
28
+ border-radius: var(--vox-radius-lg);
29
+ }
30
+
31
+ .icon {
32
+ font-size: 32px;
33
+ margin-bottom: var(--vox-space-3);
34
+ }
35
+
36
+ .heading {
37
+ margin: 0;
38
+ font-size: 18px;
39
+ font-weight: 600;
40
+ color: var(--vox-color-text-1);
41
+ }
42
+
43
+ .body {
44
+ max-width: 32rem;
45
+ margin-top: var(--vox-space-2);
46
+ font-size: 14px;
47
+ line-height: 1.6;
48
+ color: var(--vox-color-text-2);
49
+ }
50
+
51
+ .actions {
52
+ display: flex;
53
+ gap: var(--vox-space-3);
54
+ flex-wrap: wrap;
55
+ justify-content: center;
56
+ margin-top: var(--vox-space-4);
57
+ }
58
+ `;
59
+
60
+ render() {
61
+ return html`
62
+ <div class="empty">
63
+ <div class="icon"><slot name="icon"></slot></div>
64
+ <h3 class="heading">${this.heading}</h3>
65
+ <div class="body"><slot></slot></div>
66
+ <div class="actions"><slot name="actions"></slot></div>
67
+ </div>
68
+ `;
69
+ }
70
+ }
71
+
72
+ declare global {
73
+ interface HTMLElementTagNameMap {
74
+ 'vox-empty-state': VoxEmptyState;
75
+ }
76
+ }