@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.
- package/LICENSE +202 -0
- package/README.md +66 -0
- package/dist/cdn/voxblocks.css +1 -0
- package/dist/cdn/voxblocks.js +5057 -0
- package/dist/types/components/accordion/vox-accordion.d.ts +32 -0
- package/dist/types/components/alert/vox-alert.d.ts +23 -0
- package/dist/types/components/avatar/vox-avatar.d.ts +18 -0
- package/dist/types/components/badge/vox-badge.d.ts +17 -0
- package/dist/types/components/billboard/vox-billboard.d.ts +22 -0
- package/dist/types/components/breadcrumbs/vox-breadcrumbs.d.ts +24 -0
- package/dist/types/components/button/vox-button.d.ts +24 -0
- package/dist/types/components/calendar-tile/vox-calendar-tile.d.ts +18 -0
- package/dist/types/components/callout/vox-callout.d.ts +20 -0
- package/dist/types/components/card/vox-card.d.ts +28 -0
- package/dist/types/components/checkbox/vox-checkbox.d.ts +20 -0
- package/dist/types/components/cta/vox-cta.d.ts +17 -0
- package/dist/types/components/cta-band/vox-cta-band.d.ts +22 -0
- package/dist/types/components/dialog/vox-dialog.d.ts +30 -0
- package/dist/types/components/disclosure/vox-disclosure.d.ts +19 -0
- package/dist/types/components/dropdown/vox-dropdown.d.ts +29 -0
- package/dist/types/components/empty-state/vox-empty-state.d.ts +18 -0
- package/dist/types/components/file-input/vox-file-input.d.ts +22 -0
- package/dist/types/components/footer/vox-footer.d.ts +28 -0
- package/dist/types/components/grid/vox-grid.d.ts +22 -0
- package/dist/types/components/header/vox-header.d.ts +23 -0
- package/dist/types/components/hero/vox-hero.d.ts +21 -0
- package/dist/types/components/input/vox-input.d.ts +24 -0
- package/dist/types/components/input-group/vox-input-group.d.ts +25 -0
- package/dist/types/components/link-hub/vox-link-hub.d.ts +27 -0
- package/dist/types/components/loader/vox-loader.d.ts +16 -0
- package/dist/types/components/pagination/vox-pagination.d.ts +25 -0
- package/dist/types/components/quote/vox-quote.d.ts +18 -0
- package/dist/types/components/radio/vox-radio-group.d.ts +23 -0
- package/dist/types/components/radio/vox-radio.d.ts +21 -0
- package/dist/types/components/select/vox-select.d.ts +27 -0
- package/dist/types/components/series-nav/vox-series-nav.d.ts +18 -0
- package/dist/types/components/sidenav/vox-sidenav.d.ts +50 -0
- package/dist/types/components/sponsor/vox-sponsor.d.ts +35 -0
- package/dist/types/components/stat/vox-stat.d.ts +17 -0
- package/dist/types/components/step-indicator/vox-step-indicator.d.ts +28 -0
- package/dist/types/components/subnav/vox-subnav.d.ts +18 -0
- package/dist/types/components/switch/vox-switch.d.ts +20 -0
- package/dist/types/components/tabs/vox-tabs.d.ts +57 -0
- package/dist/types/components/textarea/vox-textarea.d.ts +22 -0
- package/dist/types/components/theme-toggle/vox-theme-toggle.d.ts +35 -0
- package/dist/types/components/timeline/vox-timeline.d.ts +27 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/internal/field.d.ts +27 -0
- package/dist/voxblocks.css +1 -0
- package/dist/voxblocks.js +4350 -0
- package/package.json +59 -0
- package/src/components/accordion/vox-accordion.ts +150 -0
- package/src/components/alert/vox-alert.ts +155 -0
- package/src/components/avatar/vox-avatar.ts +80 -0
- package/src/components/badge/vox-badge.ts +67 -0
- package/src/components/billboard/vox-billboard.ts +109 -0
- package/src/components/breadcrumbs/vox-breadcrumbs.ts +68 -0
- package/src/components/button/vox-button.ts +165 -0
- package/src/components/calendar-tile/vox-calendar-tile.ts +74 -0
- package/src/components/callout/vox-callout.ts +97 -0
- package/src/components/card/vox-card.ts +147 -0
- package/src/components/checkbox/vox-checkbox.ts +101 -0
- package/src/components/cta/vox-cta.ts +69 -0
- package/src/components/cta-band/vox-cta-band.ts +105 -0
- package/src/components/dialog/vox-dialog.ts +176 -0
- package/src/components/disclosure/vox-disclosure.ts +98 -0
- package/src/components/dropdown/vox-dropdown.ts +186 -0
- package/src/components/empty-state/vox-empty-state.ts +76 -0
- package/src/components/file-input/vox-file-input.ts +127 -0
- package/src/components/footer/vox-footer.ts +109 -0
- package/src/components/grid/vox-grid.ts +54 -0
- package/src/components/header/vox-header.ts +109 -0
- package/src/components/hero/vox-hero.ts +100 -0
- package/src/components/input/vox-input.ts +72 -0
- package/src/components/input-group/vox-input-group.ts +85 -0
- package/src/components/link-hub/vox-link-hub.ts +108 -0
- package/src/components/loader/vox-loader.ts +82 -0
- package/src/components/pagination/vox-pagination.ts +76 -0
- package/src/components/quote/vox-quote.ts +82 -0
- package/src/components/radio/vox-radio-group.ts +117 -0
- package/src/components/radio/vox-radio.ts +102 -0
- package/src/components/select/vox-select.ts +89 -0
- package/src/components/series-nav/vox-series-nav.ts +94 -0
- package/src/components/sidenav/vox-sidenav.ts +192 -0
- package/src/components/sponsor/vox-sponsor.ts +187 -0
- package/src/components/stat/vox-stat.ts +55 -0
- package/src/components/step-indicator/vox-step-indicator.ts +152 -0
- package/src/components/subnav/vox-subnav.ts +64 -0
- package/src/components/switch/vox-switch.ts +90 -0
- package/src/components/tabs/vox-tabs.ts +201 -0
- package/src/components/textarea/vox-textarea.ts +75 -0
- package/src/components/theme-toggle/vox-theme-toggle.ts +164 -0
- package/src/components/timeline/vox-timeline.ts +100 -0
- package/src/index.ts +65 -0
- package/src/internal/field.ts +165 -0
- package/src/styles/utilities.css +409 -0
- package/src/tokens/tokens.css +161 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A horizontal secondary navigation bar. Children are plain links;
|
|
6
|
+
* mark the active one with `aria-current="page"`.
|
|
7
|
+
*
|
|
8
|
+
* @slot - `<a>` elements.
|
|
9
|
+
*/
|
|
10
|
+
@customElement('vox-subnav')
|
|
11
|
+
export class VoxSubnav extends LitElement {
|
|
12
|
+
/** Accessible name distinguishing this nav from others on the page. */
|
|
13
|
+
@property() label = 'Secondary';
|
|
14
|
+
|
|
15
|
+
static styles = css`
|
|
16
|
+
:host {
|
|
17
|
+
display: block;
|
|
18
|
+
font-family: var(--vox-font-family-base);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
nav {
|
|
22
|
+
display: flex;
|
|
23
|
+
gap: var(--vox-space-1);
|
|
24
|
+
overflow-x: auto;
|
|
25
|
+
border-bottom: 1px solid var(--vox-color-divider);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
::slotted(a) {
|
|
29
|
+
padding: var(--vox-space-2) var(--vox-space-4);
|
|
30
|
+
margin-bottom: -1px;
|
|
31
|
+
border-bottom: 2px solid transparent;
|
|
32
|
+
color: var(--vox-color-text-2);
|
|
33
|
+
font-size: 14px;
|
|
34
|
+
font-weight: 500;
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
transition: color var(--vox-transition-fast);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
::slotted(a:hover) {
|
|
41
|
+
color: var(--vox-color-text-1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
::slotted(a[aria-current='page']) {
|
|
45
|
+
color: var(--vox-color-brand-1);
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
border-bottom-color: var(--vox-color-brand-1);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
return html`
|
|
53
|
+
<nav aria-label=${this.label}>
|
|
54
|
+
<slot></slot>
|
|
55
|
+
</nav>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare global {
|
|
61
|
+
interface HTMLElementTagNameMap {
|
|
62
|
+
'vox-subnav': VoxSubnav;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 toggle switch for on/off settings.
|
|
7
|
+
*
|
|
8
|
+
* @slot - Switch label.
|
|
9
|
+
*/
|
|
10
|
+
@customElement('vox-switch')
|
|
11
|
+
export class VoxSwitch extends VoxFieldElement {
|
|
12
|
+
@property({ type: Boolean, reflect: true }) checked = false;
|
|
13
|
+
@property() value = 'on';
|
|
14
|
+
|
|
15
|
+
static styles = [
|
|
16
|
+
checkableStyles,
|
|
17
|
+
css`
|
|
18
|
+
.track {
|
|
19
|
+
flex: none;
|
|
20
|
+
position: relative;
|
|
21
|
+
width: 36px;
|
|
22
|
+
height: 20px;
|
|
23
|
+
margin-top: 1px;
|
|
24
|
+
border-radius: var(--vox-radius-full);
|
|
25
|
+
background-color: var(--vox-color-border);
|
|
26
|
+
transition: background-color var(--vox-transition-fast);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.track::after {
|
|
30
|
+
content: '';
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 2px;
|
|
33
|
+
left: 2px;
|
|
34
|
+
width: 16px;
|
|
35
|
+
height: 16px;
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
background-color: var(--vox-color-bg);
|
|
38
|
+
transition: transform var(--vox-transition-fast);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input:checked + .track {
|
|
42
|
+
background-color: var(--vox-color-brand-3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input:checked + .track::after {
|
|
46
|
+
transform: translateX(16px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
input:focus-visible + .track {
|
|
50
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
51
|
+
outline-offset: 2px;
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
formResetCallback() {
|
|
57
|
+
this.checked = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
updated() {
|
|
61
|
+
this.internals.setFormValue(this.checked ? this.value : null);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private handleChange(event: Event) {
|
|
65
|
+
this.checked = (event.target as HTMLInputElement).checked;
|
|
66
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
render() {
|
|
70
|
+
return html`
|
|
71
|
+
<label class="check">
|
|
72
|
+
<input
|
|
73
|
+
type="checkbox"
|
|
74
|
+
role="switch"
|
|
75
|
+
.checked=${this.checked}
|
|
76
|
+
?disabled=${this.disabled}
|
|
77
|
+
@change=${this.handleChange}
|
|
78
|
+
/>
|
|
79
|
+
<span class="track" aria-hidden="true"></span>
|
|
80
|
+
<span class="text"><slot></slot></span>
|
|
81
|
+
</label>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare global {
|
|
87
|
+
interface HTMLElementTagNameMap {
|
|
88
|
+
'vox-switch': VoxSwitch;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tabbed content:
|
|
6
|
+
*
|
|
7
|
+
* ```html
|
|
8
|
+
* <vox-tabs>
|
|
9
|
+
* <vox-tab slot="tab" panel="deb" selected>Debian</vox-tab>
|
|
10
|
+
* <vox-tab slot="tab" panel="rpm">RHEL</vox-tab>
|
|
11
|
+
* <vox-tab-panel name="deb">...</vox-tab-panel>
|
|
12
|
+
* <vox-tab-panel name="rpm">...</vox-tab-panel>
|
|
13
|
+
* </vox-tabs>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @slot tab - `<vox-tab>` elements.
|
|
17
|
+
* @slot - `<vox-tab-panel>` elements.
|
|
18
|
+
* @fires vox-tab-change - When the selected tab changes; `detail.panel` names it.
|
|
19
|
+
*/
|
|
20
|
+
@customElement('vox-tabs')
|
|
21
|
+
export class VoxTabs extends LitElement {
|
|
22
|
+
static styles = css`
|
|
23
|
+
:host {
|
|
24
|
+
display: block;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.tablist {
|
|
28
|
+
display: flex;
|
|
29
|
+
gap: var(--vox-space-1);
|
|
30
|
+
border-bottom: 1px solid var(--vox-color-divider);
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
private get tabs(): VoxTab[] {
|
|
35
|
+
return [...this.querySelectorAll<VoxTab>('vox-tab')];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private get panels(): VoxTabPanel[] {
|
|
39
|
+
return [...this.querySelectorAll<VoxTabPanel>('vox-tab-panel')];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private sync() {
|
|
43
|
+
const tabs = this.tabs;
|
|
44
|
+
if (tabs.length > 0 && !tabs.some((t) => t.selected)) {
|
|
45
|
+
tabs[0].selected = true;
|
|
46
|
+
}
|
|
47
|
+
const selected = tabs.find((t) => t.selected);
|
|
48
|
+
this.panels.forEach((panel) => {
|
|
49
|
+
panel.active = panel.name === selected?.panel;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private handleSelect(event: Event) {
|
|
54
|
+
const tab = event.target as VoxTab;
|
|
55
|
+
if (tab.tagName !== 'VOX-TAB') return;
|
|
56
|
+
this.tabs.forEach((t) => (t.selected = t === tab));
|
|
57
|
+
this.sync();
|
|
58
|
+
this.dispatchEvent(
|
|
59
|
+
new CustomEvent('vox-tab-change', {
|
|
60
|
+
detail: { panel: tab.panel },
|
|
61
|
+
bubbles: true,
|
|
62
|
+
composed: true,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private handleKeydown(event: KeyboardEvent) {
|
|
68
|
+
const deltas: Record<string, number> = { ArrowRight: 1, ArrowLeft: -1 };
|
|
69
|
+
const delta = deltas[event.key];
|
|
70
|
+
if (!delta) return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
const tabs = this.tabs;
|
|
73
|
+
const index = tabs.findIndex((t) => t.selected);
|
|
74
|
+
const next = tabs[(index + delta + tabs.length) % tabs.length];
|
|
75
|
+
next.select();
|
|
76
|
+
next.focusTab();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
return html`
|
|
81
|
+
<div
|
|
82
|
+
class="tablist"
|
|
83
|
+
role="tablist"
|
|
84
|
+
@vox-tab-select=${this.handleSelect}
|
|
85
|
+
@keydown=${this.handleKeydown}
|
|
86
|
+
>
|
|
87
|
+
<slot name="tab" @slotchange=${this.sync}></slot>
|
|
88
|
+
</div>
|
|
89
|
+
<slot @slotchange=${this.sync}></slot>
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A tab button; `panel` names the `<vox-tab-panel>` it controls.
|
|
96
|
+
*
|
|
97
|
+
* @slot - Tab label.
|
|
98
|
+
*/
|
|
99
|
+
@customElement('vox-tab')
|
|
100
|
+
export class VoxTab extends LitElement {
|
|
101
|
+
@property() panel = '';
|
|
102
|
+
@property({ type: Boolean, reflect: true }) selected = false;
|
|
103
|
+
|
|
104
|
+
static styles = css`
|
|
105
|
+
:host {
|
|
106
|
+
display: block;
|
|
107
|
+
font-family: var(--vox-font-family-base);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.tab {
|
|
111
|
+
padding: var(--vox-space-2) var(--vox-space-4);
|
|
112
|
+
margin-bottom: -1px;
|
|
113
|
+
background: none;
|
|
114
|
+
border: none;
|
|
115
|
+
border-bottom: 2px solid transparent;
|
|
116
|
+
color: var(--vox-color-text-2);
|
|
117
|
+
font-family: inherit;
|
|
118
|
+
font-size: 14px;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition: color var(--vox-transition-fast);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.tab:hover {
|
|
125
|
+
color: var(--vox-color-text-1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tab:focus-visible {
|
|
129
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
130
|
+
outline-offset: -2px;
|
|
131
|
+
border-radius: var(--vox-radius-sm);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:host([selected]) .tab {
|
|
135
|
+
color: var(--vox-color-brand-1);
|
|
136
|
+
border-bottom-color: var(--vox-color-brand-1);
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
select() {
|
|
141
|
+
this.dispatchEvent(
|
|
142
|
+
new CustomEvent('vox-tab-select', { bubbles: true, composed: true }),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
focusTab() {
|
|
147
|
+
this.renderRoot.querySelector<HTMLElement>('.tab')?.focus();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
render() {
|
|
151
|
+
return html`
|
|
152
|
+
<button
|
|
153
|
+
class="tab"
|
|
154
|
+
role="tab"
|
|
155
|
+
aria-selected=${this.selected ? 'true' : 'false'}
|
|
156
|
+
tabindex=${this.selected ? '0' : '-1'}
|
|
157
|
+
@click=${this.select}
|
|
158
|
+
>
|
|
159
|
+
<slot></slot>
|
|
160
|
+
</button>
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Content shown when the matching `<vox-tab>` is selected.
|
|
167
|
+
*
|
|
168
|
+
* @slot - Panel content.
|
|
169
|
+
*/
|
|
170
|
+
@customElement('vox-tab-panel')
|
|
171
|
+
export class VoxTabPanel extends LitElement {
|
|
172
|
+
@property() name = '';
|
|
173
|
+
@property({ type: Boolean, reflect: true }) active = false;
|
|
174
|
+
|
|
175
|
+
static styles = css`
|
|
176
|
+
:host {
|
|
177
|
+
display: none;
|
|
178
|
+
font-family: var(--vox-font-family-base);
|
|
179
|
+
font-size: 14px;
|
|
180
|
+
line-height: 1.7;
|
|
181
|
+
color: var(--vox-color-text-2);
|
|
182
|
+
padding: var(--vox-space-4) 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
:host([active]) {
|
|
186
|
+
display: block;
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
render() {
|
|
191
|
+
return html`<div role="tabpanel"><slot></slot></div>`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
declare global {
|
|
196
|
+
interface HTMLElementTagNameMap {
|
|
197
|
+
'vox-tabs': VoxTabs;
|
|
198
|
+
'vox-tab': VoxTab;
|
|
199
|
+
'vox-tab-panel': VoxTabPanel;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { html, css } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { live } from 'lit/directives/live.js';
|
|
4
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
5
|
+
import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A multi-line text input with label and help note.
|
|
9
|
+
*/
|
|
10
|
+
@customElement('vox-textarea')
|
|
11
|
+
export class VoxTextarea extends VoxFieldElement {
|
|
12
|
+
@property() value = '';
|
|
13
|
+
@property() placeholder?: string;
|
|
14
|
+
@property({ type: Number }) rows = 4;
|
|
15
|
+
@property({ type: Boolean, reflect: true }) readonly = false;
|
|
16
|
+
|
|
17
|
+
static styles = [
|
|
18
|
+
fieldStyles,
|
|
19
|
+
css`
|
|
20
|
+
textarea.control {
|
|
21
|
+
resize: vertical;
|
|
22
|
+
min-height: 4em;
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
formResetCallback() {
|
|
28
|
+
this.value = '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
updated() {
|
|
32
|
+
this.internals.setFormValue(this.value);
|
|
33
|
+
const inner = this.renderRoot.querySelector('textarea');
|
|
34
|
+
if (inner) this.syncValidity(inner);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
focus(options?: FocusOptions) {
|
|
38
|
+
this.renderRoot.querySelector('textarea')?.focus(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private handleInput(event: Event) {
|
|
42
|
+
this.value = (event.target as HTMLTextAreaElement).value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private handleChange() {
|
|
46
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render() {
|
|
50
|
+
return html`
|
|
51
|
+
<div class="field">
|
|
52
|
+
${this.renderLabel('textarea')}
|
|
53
|
+
<textarea
|
|
54
|
+
id="textarea"
|
|
55
|
+
class="control"
|
|
56
|
+
rows=${this.rows}
|
|
57
|
+
.value=${live(this.value)}
|
|
58
|
+
placeholder=${ifDefined(this.placeholder)}
|
|
59
|
+
?required=${this.required}
|
|
60
|
+
?readonly=${this.readonly}
|
|
61
|
+
?disabled=${this.disabled}
|
|
62
|
+
@input=${this.handleInput}
|
|
63
|
+
@change=${this.handleChange}
|
|
64
|
+
></textarea>
|
|
65
|
+
${this.renderNote()}
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare global {
|
|
72
|
+
interface HTMLElementTagNameMap {
|
|
73
|
+
'vox-textarea': VoxTextarea;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'vox-theme';
|
|
5
|
+
const THEME_ATTR = 'data-vox-theme';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A self-contained light/dark theme switch. Reads and writes
|
|
9
|
+
* `data-vox-theme` on `<html>` and persists the choice to
|
|
10
|
+
* `localStorage['vox-theme']` — the same convention VoxBlocks' own dark
|
|
11
|
+
* mode support already uses, so this is a drop-in control for it.
|
|
12
|
+
*
|
|
13
|
+
* A page still needs its own blocking inline script in `<head>` to apply
|
|
14
|
+
* the stored (or system) theme before first paint — a custom element
|
|
15
|
+
* can't upgrade in time to prevent that flash. See the docs page for the
|
|
16
|
+
* exact snippet.
|
|
17
|
+
*
|
|
18
|
+
* Fires `vox-theme-change` with `detail: { theme: 'light' | 'dark' }`.
|
|
19
|
+
*/
|
|
20
|
+
@customElement('vox-theme-toggle')
|
|
21
|
+
export class VoxThemeToggle extends LitElement {
|
|
22
|
+
/** Accessible label while the page is in light mode. */
|
|
23
|
+
@property({ attribute: 'light-label' }) lightLabel = 'Switch to dark theme';
|
|
24
|
+
|
|
25
|
+
/** Accessible label while the page is in dark mode. */
|
|
26
|
+
@property({ attribute: 'dark-label' }) darkLabel = 'Switch to light theme';
|
|
27
|
+
|
|
28
|
+
@state() private dark = false;
|
|
29
|
+
|
|
30
|
+
private media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
31
|
+
private observer?: MutationObserver;
|
|
32
|
+
|
|
33
|
+
static styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
button {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
border: none;
|
|
42
|
+
background: none;
|
|
43
|
+
padding: 0;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
-webkit-tap-highlight-color: transparent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
button:focus-visible {
|
|
49
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
50
|
+
outline-offset: 2px;
|
|
51
|
+
border-radius: var(--vox-radius-full);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.track {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: space-between;
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
width: 44px;
|
|
61
|
+
height: 24px;
|
|
62
|
+
padding: 0 5px;
|
|
63
|
+
border-radius: var(--vox-radius-full);
|
|
64
|
+
background-color: var(--vox-color-bg-soft);
|
|
65
|
+
border: 1px solid var(--vox-color-divider);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.track svg {
|
|
69
|
+
position: relative;
|
|
70
|
+
width: 13px;
|
|
71
|
+
height: 13px;
|
|
72
|
+
flex: none;
|
|
73
|
+
color: var(--vox-color-text-3);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.thumb {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 1px;
|
|
79
|
+
left: 1px;
|
|
80
|
+
width: 20px;
|
|
81
|
+
height: 20px;
|
|
82
|
+
border-radius: 50%;
|
|
83
|
+
background-color: var(--vox-color-bg-elv);
|
|
84
|
+
box-shadow: var(--vox-shadow-1);
|
|
85
|
+
transition: transform var(--vox-transition-base);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
:host([dark]) .thumb {
|
|
89
|
+
transform: translateX(20px);
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
connectedCallback() {
|
|
94
|
+
super.connectedCallback();
|
|
95
|
+
this.syncFromDocument();
|
|
96
|
+
this.observer = new MutationObserver(() => this.syncFromDocument());
|
|
97
|
+
this.observer.observe(document.documentElement, {
|
|
98
|
+
attributes: true,
|
|
99
|
+
attributeFilter: [THEME_ATTR],
|
|
100
|
+
});
|
|
101
|
+
this.media.addEventListener('change', this.handleSystemChange);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
disconnectedCallback() {
|
|
105
|
+
super.disconnectedCallback();
|
|
106
|
+
this.observer?.disconnect();
|
|
107
|
+
this.media.removeEventListener('change', this.handleSystemChange);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private handleSystemChange = (event: MediaQueryListEvent) => {
|
|
111
|
+
if (localStorage.getItem(STORAGE_KEY)) return;
|
|
112
|
+
document.documentElement.setAttribute(THEME_ATTR, event.matches ? 'dark' : 'light');
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
private syncFromDocument() {
|
|
116
|
+
const dark = document.documentElement.getAttribute(THEME_ATTR) === 'dark';
|
|
117
|
+
this.dark = dark;
|
|
118
|
+
this.toggleAttribute('dark', dark);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private handleClick() {
|
|
122
|
+
const next = this.dark ? 'light' : 'dark';
|
|
123
|
+
document.documentElement.setAttribute(THEME_ATTR, next);
|
|
124
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
125
|
+
this.dispatchEvent(
|
|
126
|
+
new CustomEvent('vox-theme-change', {
|
|
127
|
+
detail: { theme: next },
|
|
128
|
+
bubbles: true,
|
|
129
|
+
composed: true,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render() {
|
|
135
|
+
const label = this.dark ? this.darkLabel : this.lightLabel;
|
|
136
|
+
return html`
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
role="switch"
|
|
140
|
+
aria-checked=${this.dark}
|
|
141
|
+
aria-label=${label}
|
|
142
|
+
title=${label}
|
|
143
|
+
@click=${this.handleClick}
|
|
144
|
+
>
|
|
145
|
+
<span class="track">
|
|
146
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
147
|
+
<circle cx="12" cy="12" r="4" />
|
|
148
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
149
|
+
</svg>
|
|
150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
151
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" />
|
|
152
|
+
</svg>
|
|
153
|
+
<span class="thumb"></span>
|
|
154
|
+
</span>
|
|
155
|
+
</button>
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
declare global {
|
|
161
|
+
interface HTMLElementTagNameMap {
|
|
162
|
+
'vox-theme-toggle': VoxThemeToggle;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A vertical timeline of dated events.
|
|
6
|
+
*
|
|
7
|
+
* @slot - `<vox-timeline-item>` elements.
|
|
8
|
+
*/
|
|
9
|
+
@customElement('vox-timeline')
|
|
10
|
+
export class VoxTimeline extends LitElement {
|
|
11
|
+
static styles = css`
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
render() {
|
|
18
|
+
return html`<slot></slot>`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One event on a `<vox-timeline>`.
|
|
24
|
+
*
|
|
25
|
+
* @slot - Event description.
|
|
26
|
+
*/
|
|
27
|
+
@customElement('vox-timeline-item')
|
|
28
|
+
export class VoxTimelineItem extends LitElement {
|
|
29
|
+
@property() heading = '';
|
|
30
|
+
@property() date = '';
|
|
31
|
+
|
|
32
|
+
static styles = css`
|
|
33
|
+
:host {
|
|
34
|
+
display: block;
|
|
35
|
+
font-family: var(--vox-font-family-base);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.item {
|
|
39
|
+
position: relative;
|
|
40
|
+
padding: 0 0 var(--vox-space-6) var(--vox-space-6);
|
|
41
|
+
border-left: 2px solid var(--vox-color-divider);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:host(:last-child) .item {
|
|
45
|
+
border-left-color: transparent;
|
|
46
|
+
padding-bottom: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.dot {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 4px;
|
|
52
|
+
left: -7px;
|
|
53
|
+
width: 12px;
|
|
54
|
+
height: 12px;
|
|
55
|
+
border-radius: 50%;
|
|
56
|
+
background-color: var(--vox-color-brand-3);
|
|
57
|
+
border: 2px solid var(--vox-color-bg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.date {
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
letter-spacing: 0.05em;
|
|
64
|
+
text-transform: uppercase;
|
|
65
|
+
color: var(--vox-color-text-3);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.heading {
|
|
69
|
+
margin: var(--vox-space-1) 0 0;
|
|
70
|
+
font-size: 16px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
color: var(--vox-color-text-1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.body {
|
|
76
|
+
margin-top: var(--vox-space-1);
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
line-height: 1.6;
|
|
79
|
+
color: var(--vox-color-text-2);
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
render() {
|
|
84
|
+
return html`
|
|
85
|
+
<div class="item">
|
|
86
|
+
<span class="dot" aria-hidden="true"></span>
|
|
87
|
+
${this.date ? html`<div class="date">${this.date}</div>` : nothing}
|
|
88
|
+
<h3 class="heading">${this.heading}</h3>
|
|
89
|
+
<div class="body"><slot></slot></div>
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare global {
|
|
96
|
+
interface HTMLElementTagNameMap {
|
|
97
|
+
'vox-timeline': VoxTimeline;
|
|
98
|
+
'vox-timeline-item': VoxTimelineItem;
|
|
99
|
+
}
|
|
100
|
+
}
|