@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,165 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import { ifDefined } from 'lit/directives/if-defined.js';
5
+
6
+ export type ButtonVariant = 'brand' | 'alt' | 'danger' | 'ghost';
7
+ export type ButtonSize = 'sm' | 'md' | 'lg';
8
+
9
+ /**
10
+ * A button styled after the OpenVox docs action buttons.
11
+ * Renders an anchor when `href` is set, a real button otherwise.
12
+ *
13
+ * @slot - Button label.
14
+ */
15
+ @customElement('vox-button')
16
+ export class VoxButton extends LitElement {
17
+ @property() variant: ButtonVariant = 'brand';
18
+ @property() size: ButtonSize = 'md';
19
+ @property() href?: string;
20
+ @property() target?: string;
21
+ @property() type: 'button' | 'submit' | 'reset' = 'button';
22
+ @property({ type: Boolean, reflect: true }) disabled = false;
23
+
24
+ static styles = css`
25
+ :host {
26
+ display: inline-block;
27
+ }
28
+
29
+ :host([disabled]) {
30
+ pointer-events: none;
31
+ }
32
+
33
+ .button {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ gap: var(--vox-space-2);
38
+ border: 1px solid transparent;
39
+ border-radius: var(--vox-radius-full);
40
+ font-family: var(--vox-font-family-base);
41
+ font-weight: 600;
42
+ line-height: 1;
43
+ text-decoration: none;
44
+ cursor: pointer;
45
+ white-space: nowrap;
46
+ transition:
47
+ color var(--vox-transition-base),
48
+ background-color var(--vox-transition-base),
49
+ border-color var(--vox-transition-base);
50
+ }
51
+
52
+ .button:focus-visible {
53
+ outline: 2px solid var(--vox-color-brand-1);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ .button:disabled {
58
+ opacity: 0.5;
59
+ cursor: not-allowed;
60
+ }
61
+
62
+ .sm {
63
+ font-size: 12px;
64
+ padding: 0 var(--vox-space-3);
65
+ height: 28px;
66
+ }
67
+
68
+ .md {
69
+ font-size: 14px;
70
+ padding: 0 20px;
71
+ height: 38px;
72
+ }
73
+
74
+ .lg {
75
+ font-size: 16px;
76
+ padding: 0 var(--vox-space-6);
77
+ height: 48px;
78
+ }
79
+
80
+ .brand {
81
+ background-color: var(--vox-color-brand-3);
82
+ color: var(--vox-color-text-inverse);
83
+ }
84
+
85
+ .brand:hover:not(:disabled) {
86
+ background-color: var(--vox-color-brand-2);
87
+ }
88
+
89
+ .alt {
90
+ background-color: var(--vox-color-bg-soft);
91
+ color: var(--vox-color-text-1);
92
+ border-color: var(--vox-color-divider);
93
+ }
94
+
95
+ .alt:hover:not(:disabled) {
96
+ border-color: var(--vox-color-brand-1);
97
+ color: var(--vox-color-brand-1);
98
+ }
99
+
100
+ .danger {
101
+ background-color: var(--vox-color-danger-3);
102
+ color: var(--vox-color-text-inverse);
103
+ }
104
+
105
+ .danger:hover:not(:disabled) {
106
+ background-color: var(--vox-color-danger-2);
107
+ }
108
+
109
+ .ghost {
110
+ background-color: transparent;
111
+ color: var(--vox-color-brand-1);
112
+ }
113
+
114
+ .ghost:hover:not(:disabled) {
115
+ background-color: var(--vox-color-brand-soft);
116
+ }
117
+
118
+ /* Corner flattening when placed inside a <vox-input-group>. */
119
+ :host([data-vox-group]) .button {
120
+ height: 100%;
121
+ border-radius: 0;
122
+ }
123
+
124
+ :host([data-vox-group='start']) .button {
125
+ border-radius: var(--vox-radius-md) 0 0 var(--vox-radius-md);
126
+ }
127
+
128
+ :host([data-vox-group='end']) .button {
129
+ border-radius: 0 var(--vox-radius-md) var(--vox-radius-md) 0;
130
+ }
131
+ `;
132
+
133
+ render() {
134
+ const classes = classMap({
135
+ button: true,
136
+ [this.variant]: true,
137
+ [this.size]: true,
138
+ });
139
+
140
+ if (this.href !== undefined && !this.disabled) {
141
+ return html`
142
+ <a
143
+ class=${classes}
144
+ href=${this.href}
145
+ target=${ifDefined(this.target)}
146
+ rel=${ifDefined(this.target === '_blank' ? 'noreferrer' : undefined)}
147
+ >
148
+ <slot></slot>
149
+ </a>
150
+ `;
151
+ }
152
+
153
+ return html`
154
+ <button class=${classes} type=${this.type} ?disabled=${this.disabled}>
155
+ <slot></slot>
156
+ </button>
157
+ `;
158
+ }
159
+ }
160
+
161
+ declare global {
162
+ interface HTMLElementTagNameMap {
163
+ 'vox-button': VoxButton;
164
+ }
165
+ }
@@ -0,0 +1,74 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A compact date tile for event listings. Give it an ISO date
6
+ * (`date="2026-09-18"`); month and day render localized.
7
+ */
8
+ @customElement('vox-calendar-tile')
9
+ export class VoxCalendarTile extends LitElement {
10
+ /** ISO date string, e.g. "2026-09-18". */
11
+ @property() date = '';
12
+
13
+ /** BCP 47 locale for month formatting; defaults to the page locale. */
14
+ @property() locale?: string;
15
+
16
+ static styles = css`
17
+ :host {
18
+ display: inline-block;
19
+ font-family: var(--vox-font-family-base);
20
+ }
21
+
22
+ .tile {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ min-width: 56px;
27
+ border: 1px solid var(--vox-color-divider);
28
+ border-radius: var(--vox-radius-md);
29
+ overflow: hidden;
30
+ background-color: var(--vox-color-bg-elv);
31
+ }
32
+
33
+ .month {
34
+ align-self: stretch;
35
+ padding: 2px var(--vox-space-2);
36
+ background-color: var(--vox-color-brand-3);
37
+ color: var(--vox-color-text-inverse);
38
+ font-size: 11px;
39
+ font-weight: 700;
40
+ letter-spacing: 0.08em;
41
+ text-transform: uppercase;
42
+ text-align: center;
43
+ }
44
+
45
+ .day {
46
+ padding: var(--vox-space-1) var(--vox-space-2);
47
+ font-size: 24px;
48
+ font-weight: 700;
49
+ color: var(--vox-color-text-1);
50
+ }
51
+ `;
52
+
53
+ render() {
54
+ const parsed = new Date(`${this.date}T00:00:00`);
55
+ const valid = !Number.isNaN(parsed.getTime());
56
+ const month = valid
57
+ ? parsed.toLocaleString(this.locale, { month: 'short' })
58
+ : '—';
59
+ const day = valid ? parsed.getDate() : '–';
60
+
61
+ return html`
62
+ <time class="tile" datetime=${this.date}>
63
+ <span class="month">${month}</span>
64
+ <span class="day">${day}</span>
65
+ </time>
66
+ `;
67
+ }
68
+ }
69
+
70
+ declare global {
71
+ interface HTMLElementTagNameMap {
72
+ 'vox-calendar-tile': VoxCalendarTile;
73
+ }
74
+ }
@@ -0,0 +1,97 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ export type CalloutVariant = 'info' | 'tip' | 'warning' | 'danger';
5
+
6
+ const DEFAULT_HEADINGS: Record<CalloutVariant, string> = {
7
+ info: 'Info',
8
+ tip: 'Tip',
9
+ warning: 'Warning',
10
+ danger: 'Danger',
11
+ };
12
+
13
+ /**
14
+ * An admonition block matching the custom containers on the OpenVox docs
15
+ * (::: tip, ::: warning, ...).
16
+ *
17
+ * @slot - Callout body content.
18
+ */
19
+ @customElement('vox-callout')
20
+ export class VoxCallout extends LitElement {
21
+ @property() variant: CalloutVariant = 'info';
22
+
23
+ /** Heading text; defaults to the capitalized variant name. */
24
+ @property() heading?: string;
25
+
26
+ static styles = css`
27
+ :host {
28
+ display: block;
29
+ }
30
+
31
+ .callout {
32
+ border-radius: var(--vox-radius-md);
33
+ padding: var(--vox-space-4);
34
+ font-family: var(--vox-font-family-base);
35
+ font-size: 14px;
36
+ line-height: 1.7;
37
+ color: var(--vox-color-text-2);
38
+ }
39
+
40
+ .heading {
41
+ margin: 0 0 var(--vox-space-2);
42
+ font-size: 14px;
43
+ font-weight: 600;
44
+ }
45
+
46
+ .info {
47
+ background-color: var(--vox-color-bg-soft);
48
+ }
49
+ .info .heading {
50
+ color: var(--vox-color-text-1);
51
+ }
52
+
53
+ .tip {
54
+ background-color: var(--vox-color-tip-soft);
55
+ }
56
+ .tip .heading {
57
+ color: var(--vox-color-tip-1);
58
+ }
59
+
60
+ .warning {
61
+ background-color: var(--vox-color-warning-soft);
62
+ }
63
+ .warning .heading {
64
+ color: var(--vox-color-warning-1);
65
+ }
66
+
67
+ .danger {
68
+ background-color: var(--vox-color-danger-soft);
69
+ }
70
+ .danger .heading {
71
+ color: var(--vox-color-danger-1);
72
+ }
73
+
74
+ ::slotted(p:first-child) {
75
+ margin-top: 0;
76
+ }
77
+
78
+ ::slotted(p:last-child) {
79
+ margin-bottom: 0;
80
+ }
81
+ `;
82
+
83
+ render() {
84
+ return html`
85
+ <div class="callout ${this.variant}" role="note">
86
+ <p class="heading">${this.heading ?? DEFAULT_HEADINGS[this.variant]}</p>
87
+ <slot></slot>
88
+ </div>
89
+ `;
90
+ }
91
+ }
92
+
93
+ declare global {
94
+ interface HTMLElementTagNameMap {
95
+ 'vox-callout': VoxCallout;
96
+ }
97
+ }
@@ -0,0 +1,147 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A feature card in the style of the OpenVox docs landing page tiles.
6
+ * Becomes a single large link when `href` is set.
7
+ *
8
+ * @slot icon - Optional icon or emoji shown above the heading.
9
+ * @slot badge - Optional badge or label shown in the top-right corner.
10
+ * @slot - Card body text.
11
+ * @slot footer - Optional content pinned to the bottom of the card, e.g. stats.
12
+ */
13
+ @customElement('vox-card')
14
+ export class VoxCard extends LitElement {
15
+ @property() heading = '';
16
+ @property() href?: string;
17
+ @property() target?: string;
18
+
19
+ static styles = css`
20
+ :host {
21
+ display: block;
22
+ }
23
+
24
+ .card {
25
+ position: relative;
26
+ display: flex;
27
+ flex-direction: column;
28
+ height: 100%;
29
+ box-sizing: border-box;
30
+ background-color: var(--vox-color-bg-soft);
31
+ border: 1px solid var(--vox-color-bg-soft);
32
+ border-radius: var(--vox-radius-lg);
33
+ padding: var(--vox-space-6);
34
+ font-family: var(--vox-font-family-base);
35
+ text-decoration: none;
36
+ transition: border-color var(--vox-transition-base);
37
+ }
38
+
39
+ .badge {
40
+ position: absolute;
41
+ top: var(--vox-space-6);
42
+ right: var(--vox-space-6);
43
+ }
44
+
45
+ .badge:not(.has-badge) {
46
+ display: none;
47
+ }
48
+
49
+ a.card:hover,
50
+ a.card:focus-visible {
51
+ border-color: var(--vox-color-brand-1);
52
+ }
53
+
54
+ a.card:focus-visible {
55
+ outline: 2px solid var(--vox-color-brand-1);
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ .icon {
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 48px;
64
+ height: 48px;
65
+ margin-bottom: var(--vox-space-4);
66
+ background-color: var(--vox-color-bg-elv);
67
+ border-radius: var(--vox-radius-md);
68
+ font-size: 24px;
69
+ }
70
+
71
+ .icon:not(.has-icon) {
72
+ display: none;
73
+ }
74
+
75
+ .heading {
76
+ margin: 0;
77
+ font-size: 16px;
78
+ font-weight: 600;
79
+ line-height: 1.5;
80
+ color: var(--vox-color-text-1);
81
+ }
82
+
83
+ .body {
84
+ margin-top: var(--vox-space-2);
85
+ font-size: 14px;
86
+ line-height: 1.6;
87
+ color: var(--vox-color-text-2);
88
+ }
89
+
90
+ .footer {
91
+ margin-top: auto;
92
+ padding-top: var(--vox-space-4);
93
+ }
94
+
95
+ .footer:not(.has-footer) {
96
+ display: none;
97
+ }
98
+ `;
99
+
100
+ private hasIcon = false;
101
+ private hasBadge = false;
102
+ private hasFooter = false;
103
+
104
+ private handleIconSlotChange(event: Event) {
105
+ const slot = event.target as HTMLSlotElement;
106
+ this.hasIcon = slot.assignedNodes({ flatten: true }).length > 0;
107
+ this.requestUpdate();
108
+ }
109
+
110
+ private handleBadgeSlotChange(event: Event) {
111
+ const slot = event.target as HTMLSlotElement;
112
+ this.hasBadge = slot.assignedNodes({ flatten: true }).length > 0;
113
+ this.requestUpdate();
114
+ }
115
+
116
+ private handleFooterSlotChange(event: Event) {
117
+ const slot = event.target as HTMLSlotElement;
118
+ this.hasFooter = slot.assignedNodes({ flatten: true }).length > 0;
119
+ this.requestUpdate();
120
+ }
121
+
122
+ render() {
123
+ const inner = html`
124
+ <div class="badge ${this.hasBadge ? 'has-badge' : ''}">
125
+ <slot name="badge" @slotchange=${this.handleBadgeSlotChange}></slot>
126
+ </div>
127
+ <div class="icon ${this.hasIcon ? 'has-icon' : ''}">
128
+ <slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
129
+ </div>
130
+ <h3 class="heading">${this.heading}</h3>
131
+ <div class="body"><slot></slot></div>
132
+ <div class="footer ${this.hasFooter ? 'has-footer' : ''}">
133
+ <slot name="footer" @slotchange=${this.handleFooterSlotChange}></slot>
134
+ </div>
135
+ `;
136
+
137
+ return this.href !== undefined
138
+ ? html`<a class="card" href=${this.href} target=${this.target ?? nothing}>${inner}</a>`
139
+ : html`<div class="card">${inner}</div>`;
140
+ }
141
+ }
142
+
143
+ declare global {
144
+ interface HTMLElementTagNameMap {
145
+ 'vox-card': VoxCard;
146
+ }
147
+ }
@@ -0,0 +1,101 @@
1
+ import { html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { VoxFieldElement, checkableStyles } from '../../internal/field.js';
4
+
5
+ /**
6
+ * A checkbox with a custom visual that follows the VoxBlocks theme.
7
+ *
8
+ * @slot - Checkbox label.
9
+ */
10
+ @customElement('vox-checkbox')
11
+ export class VoxCheckbox extends VoxFieldElement {
12
+ @property({ type: Boolean, reflect: true }) checked = false;
13
+ @property() value = 'on';
14
+
15
+ static styles = [
16
+ checkableStyles,
17
+ css`
18
+ .box {
19
+ flex: none;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ width: 18px;
24
+ height: 18px;
25
+ margin-top: 2px;
26
+ border: 1px solid var(--vox-color-border);
27
+ border-radius: var(--vox-radius-sm);
28
+ background-color: var(--vox-color-bg);
29
+ color: var(--vox-color-text-inverse);
30
+ transition:
31
+ background-color var(--vox-transition-fast),
32
+ border-color var(--vox-transition-fast);
33
+ }
34
+
35
+ .box svg {
36
+ width: 12px;
37
+ height: 12px;
38
+ opacity: 0;
39
+ }
40
+
41
+ input:checked + .box {
42
+ background-color: var(--vox-color-brand-3);
43
+ border-color: var(--vox-color-brand-3);
44
+ }
45
+
46
+ input:checked + .box svg {
47
+ opacity: 1;
48
+ }
49
+
50
+ input:focus-visible + .box {
51
+ outline: 2px solid var(--vox-color-brand-1);
52
+ outline-offset: 2px;
53
+ }
54
+ `,
55
+ ];
56
+
57
+ formResetCallback() {
58
+ this.checked = false;
59
+ }
60
+
61
+ updated() {
62
+ this.internals.setFormValue(this.checked ? this.value : null);
63
+ this.internals.setValidity(
64
+ this.required && !this.checked
65
+ ? { valueMissing: true }
66
+ : {},
67
+ 'Please check this box.',
68
+ this.renderRoot.querySelector('input') ?? undefined,
69
+ );
70
+ }
71
+
72
+ private handleChange(event: Event) {
73
+ this.checked = (event.target as HTMLInputElement).checked;
74
+ this.dispatchEvent(new Event('change', { bubbles: true }));
75
+ }
76
+
77
+ render() {
78
+ return html`
79
+ <label class="check">
80
+ <input
81
+ type="checkbox"
82
+ .checked=${this.checked}
83
+ ?disabled=${this.disabled}
84
+ @change=${this.handleChange}
85
+ />
86
+ <span class="box" aria-hidden="true">
87
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
88
+ <path d="m4 12 5 5L20 6" />
89
+ </svg>
90
+ </span>
91
+ <span class="text"><slot></slot></span>
92
+ </label>
93
+ `;
94
+ }
95
+ }
96
+
97
+ declare global {
98
+ interface HTMLElementTagNameMap {
99
+ 'vox-checkbox': VoxCheckbox;
100
+ }
101
+ }
@@ -0,0 +1,69 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A call-to-action link with a trailing arrow that nudges on hover.
6
+ *
7
+ * @slot - Link text.
8
+ */
9
+ @customElement('vox-cta')
10
+ export class VoxCta extends LitElement {
11
+ @property() href = '#';
12
+ @property() target?: string;
13
+
14
+ static styles = css`
15
+ :host {
16
+ display: inline-block;
17
+ font-family: var(--vox-font-family-base);
18
+ }
19
+
20
+ a {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--vox-space-2);
24
+ color: var(--vox-color-brand-1);
25
+ font-size: 15px;
26
+ font-weight: 600;
27
+ text-decoration: none;
28
+ }
29
+
30
+ a:hover {
31
+ color: var(--vox-color-brand-2);
32
+ text-decoration: underline;
33
+ }
34
+
35
+ a:focus-visible {
36
+ outline: 2px solid var(--vox-color-brand-1);
37
+ outline-offset: 2px;
38
+ border-radius: var(--vox-radius-sm);
39
+ }
40
+
41
+ svg {
42
+ width: 16px;
43
+ height: 16px;
44
+ transition: transform var(--vox-transition-fast);
45
+ }
46
+
47
+ a:hover svg {
48
+ transform: translateX(3px);
49
+ }
50
+ `;
51
+
52
+ render() {
53
+ return html`
54
+ <a href=${this.href} target=${this.target ?? ''}>
55
+ <slot></slot>
56
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
57
+ <path d="M5 12h14" />
58
+ <path d="m13 6 6 6-6 6" />
59
+ </svg>
60
+ </a>
61
+ `;
62
+ }
63
+ }
64
+
65
+ declare global {
66
+ interface HTMLElementTagNameMap {
67
+ 'vox-cta': VoxCta;
68
+ }
69
+ }