@openvoxproject/voxblocks 0.12.0 → 0.14.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/README.md +2 -2
- package/dist/cdn/voxblocks.css +1 -1
- package/dist/cdn/voxblocks.js +1417 -1610
- package/dist/types/components/datum/vox-datum.d.ts +25 -0
- package/dist/types/components/menu/vox-menu.d.ts +44 -0
- package/dist/types/components/record-list/vox-record-list.d.ts +53 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/voxblocks.css +1 -1
- package/dist/voxblocks.js +1389 -1582
- package/package.json +1 -1
- package/src/components/datum/vox-datum.ts +76 -0
- package/src/components/dropdown/vox-dropdown.ts +6 -0
- package/src/components/menu/vox-menu.ts +203 -0
- package/src/components/record-list/vox-record-list.ts +263 -0
- package/src/index.ts +5 -2
- package/src/tokens/tokens.css +0 -17
- package/dist/types/components/code-block/languages.d.ts +0 -28
- package/dist/types/components/code-block/vox-code-block.d.ts +0 -34
- package/src/components/code-block/languages.ts +0 -259
- package/src/components/code-block/vox-code-block.ts +0 -302
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A small labeled value with an optional icon — an owner, a date, a
|
|
6
|
+
* target host, anything that's "icon + value" — for use inside record
|
|
7
|
+
* rows, cards, or wherever a compact data point is needed.
|
|
8
|
+
*
|
|
9
|
+
* `name` isn't shown visually (the icon, if any, carries that meaning for
|
|
10
|
+
* sighted users); it's exposed to assistive tech as a spoken label, e.g.
|
|
11
|
+
* "User: J. Smith".
|
|
12
|
+
*
|
|
13
|
+
* @slot icon - Optional icon before the value, e.g. `<vox-icon size="sm">`.
|
|
14
|
+
* @slot - The value.
|
|
15
|
+
*/
|
|
16
|
+
@customElement('vox-datum')
|
|
17
|
+
export class VoxDatum extends LitElement {
|
|
18
|
+
@property() name = '';
|
|
19
|
+
|
|
20
|
+
static styles = css`
|
|
21
|
+
:host {
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: var(--vox-space-1);
|
|
25
|
+
font-family: var(--vox-font-family-base);
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
color: var(--vox-color-text-2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.icon {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex: none;
|
|
33
|
+
color: var(--vox-color-text-3);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.icon:not(.has-icon) {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sr-only {
|
|
41
|
+
position: absolute;
|
|
42
|
+
width: 1px;
|
|
43
|
+
height: 1px;
|
|
44
|
+
padding: 0;
|
|
45
|
+
margin: -1px;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
clip: rect(0, 0, 0, 0);
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
border: 0;
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
private hasIcon = false;
|
|
54
|
+
|
|
55
|
+
private handleIconSlotChange(event: Event) {
|
|
56
|
+
const slot = event.target as HTMLSlotElement;
|
|
57
|
+
this.hasIcon = slot.assignedNodes({ flatten: true }).length > 0;
|
|
58
|
+
this.requestUpdate();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render() {
|
|
62
|
+
return html`
|
|
63
|
+
<span class="icon ${this.hasIcon ? 'has-icon' : ''}">
|
|
64
|
+
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
|
|
65
|
+
</span>
|
|
66
|
+
${this.name ? html`<span class="sr-only">${this.name}: </span>` : nothing}
|
|
67
|
+
<slot></slot>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare global {
|
|
73
|
+
interface HTMLElementTagNameMap {
|
|
74
|
+
'vox-datum': VoxDatum;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -22,6 +22,12 @@ export class VoxDropdown extends LitElement {
|
|
|
22
22
|
:host {
|
|
23
23
|
position: relative;
|
|
24
24
|
display: inline-block;
|
|
25
|
+
/* Without this, a flex/grid container's default stretch alignment
|
|
26
|
+
grows the host to fill the cross axis while the trigger button
|
|
27
|
+
inside stays content-sized — and since the menu's "top: 100%" is
|
|
28
|
+
measured against the host's own box, it then opens far below the
|
|
29
|
+
trigger instead of right under it. */
|
|
30
|
+
align-self: flex-start;
|
|
25
31
|
font-family: var(--vox-font-family-base);
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export type MenuPlacement = 'bottom-start' | 'bottom-end';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An overlay menu anchored to an arbitrary trigger — an avatar, an icon
|
|
8
|
+
* button, anything — unlike `<vox-dropdown>`, which owns a fixed
|
|
9
|
+
* label+chevron trigger of its own.
|
|
10
|
+
*
|
|
11
|
+
* ```html
|
|
12
|
+
* <vox-menu label="Account menu">
|
|
13
|
+
* <vox-avatar slot="trigger" initials="UN"></vox-avatar>
|
|
14
|
+
* <a href="/profile">Profile</a>
|
|
15
|
+
* <a href="/settings">Settings</a>
|
|
16
|
+
* <hr />
|
|
17
|
+
* <button type="button">Log out</button>
|
|
18
|
+
* </vox-menu>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @slot trigger - Content shown inside the trigger button, e.g. a `<vox-avatar>`.
|
|
22
|
+
* @slot - Menu entries (`<a>`, `<button>`, `<hr>` for separators).
|
|
23
|
+
* @fires vox-close - When the menu closes (Escape, outside click, or an entry click).
|
|
24
|
+
*/
|
|
25
|
+
@customElement('vox-menu')
|
|
26
|
+
export class VoxMenu extends LitElement {
|
|
27
|
+
/**
|
|
28
|
+
* Accessible name for the trigger button. Set this when the `trigger`
|
|
29
|
+
* slot has no visible text of its own (e.g. an avatar-only trigger);
|
|
30
|
+
* leave unset when it does, so that text stays the accessible name.
|
|
31
|
+
*/
|
|
32
|
+
@property() label?: string;
|
|
33
|
+
@property({ reflect: true }) placement: MenuPlacement = 'bottom-end';
|
|
34
|
+
@property({ type: Boolean, reflect: true }) open = false;
|
|
35
|
+
|
|
36
|
+
static styles = css`
|
|
37
|
+
:host {
|
|
38
|
+
position: relative;
|
|
39
|
+
display: inline-block;
|
|
40
|
+
/* Without this, a flex/grid container's default stretch alignment
|
|
41
|
+
grows the host to fill the cross axis (e.g. a tall sibling, or a
|
|
42
|
+
container given a min-height for layout purposes) while the
|
|
43
|
+
trigger button inside stays content-sized — and since the menu's
|
|
44
|
+
"top: 100%" is measured against the host's own box, it then opens
|
|
45
|
+
far below the trigger instead of right under it. */
|
|
46
|
+
align-self: flex-start;
|
|
47
|
+
font-family: var(--vox-font-family-base);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.trigger {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
background: none;
|
|
54
|
+
border: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
border-radius: var(--vox-radius-md);
|
|
57
|
+
color: inherit;
|
|
58
|
+
font: inherit;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.trigger:focus-visible {
|
|
63
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.menu {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: calc(100% + 4px);
|
|
70
|
+
z-index: 10;
|
|
71
|
+
min-width: 180px;
|
|
72
|
+
display: none;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
padding: var(--vox-space-2);
|
|
75
|
+
background-color: var(--vox-color-bg-elv);
|
|
76
|
+
border: 1px solid var(--vox-color-divider);
|
|
77
|
+
border-radius: var(--vox-radius-md);
|
|
78
|
+
box-shadow: var(--vox-shadow-2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:host([placement='bottom-start']) .menu {
|
|
82
|
+
left: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:host([placement='bottom-end']) .menu {
|
|
86
|
+
right: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:host([open]) .menu {
|
|
90
|
+
display: flex;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
::slotted(a),
|
|
94
|
+
::slotted(button) {
|
|
95
|
+
display: block;
|
|
96
|
+
width: 100%;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
padding: var(--vox-space-2) var(--vox-space-3);
|
|
99
|
+
background: none;
|
|
100
|
+
border: none;
|
|
101
|
+
border-radius: var(--vox-radius-sm);
|
|
102
|
+
color: var(--vox-color-text-1);
|
|
103
|
+
font-family: inherit;
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
text-align: left;
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
::slotted(a:hover),
|
|
112
|
+
::slotted(button:hover),
|
|
113
|
+
::slotted(a:focus-visible),
|
|
114
|
+
::slotted(button:focus-visible) {
|
|
115
|
+
background-color: var(--vox-color-brand-soft);
|
|
116
|
+
color: var(--vox-color-brand-1);
|
|
117
|
+
outline: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
::slotted(hr) {
|
|
121
|
+
width: 100%;
|
|
122
|
+
margin: var(--vox-space-1) 0;
|
|
123
|
+
border: none;
|
|
124
|
+
border-top: 1px solid var(--vox-color-divider);
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
connectedCallback() {
|
|
129
|
+
super.connectedCallback();
|
|
130
|
+
document.addEventListener('click', this.handleOutsideClick);
|
|
131
|
+
this.addEventListener('keydown', this.handleKeydown);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
disconnectedCallback() {
|
|
135
|
+
super.disconnectedCallback();
|
|
136
|
+
document.removeEventListener('click', this.handleOutsideClick);
|
|
137
|
+
this.removeEventListener('keydown', this.handleKeydown);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private handleOutsideClick = (event: MouseEvent) => {
|
|
141
|
+
if (this.open && !event.composedPath().includes(this)) {
|
|
142
|
+
this.close();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
private handleKeydown = (event: KeyboardEvent) => {
|
|
147
|
+
if (event.key === 'Escape' && this.open) {
|
|
148
|
+
this.close();
|
|
149
|
+
this.renderRoot.querySelector<HTMLElement>('.trigger')?.focus();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && this.open) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
const items = [...this.querySelectorAll<HTMLElement>('a, button')].filter(
|
|
156
|
+
(el) => el.slot !== 'trigger',
|
|
157
|
+
);
|
|
158
|
+
if (items.length === 0) return;
|
|
159
|
+
const active = document.activeElement as HTMLElement;
|
|
160
|
+
const index = items.indexOf(active);
|
|
161
|
+
const delta = event.key === 'ArrowDown' ? 1 : -1;
|
|
162
|
+
const next = items[(Math.max(index, 0) + delta + items.length) % items.length];
|
|
163
|
+
next.focus();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
private toggle() {
|
|
168
|
+
if (this.open) {
|
|
169
|
+
this.close();
|
|
170
|
+
} else {
|
|
171
|
+
this.open = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private close() {
|
|
176
|
+
if (!this.open) return;
|
|
177
|
+
this.open = false;
|
|
178
|
+
this.dispatchEvent(new CustomEvent('vox-close', { bubbles: true, composed: true }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
render() {
|
|
182
|
+
return html`
|
|
183
|
+
<button
|
|
184
|
+
class="trigger"
|
|
185
|
+
aria-expanded=${this.open ? 'true' : 'false'}
|
|
186
|
+
aria-haspopup="true"
|
|
187
|
+
aria-label=${this.label ?? nothing}
|
|
188
|
+
@click=${this.toggle}
|
|
189
|
+
>
|
|
190
|
+
<slot name="trigger"></slot>
|
|
191
|
+
</button>
|
|
192
|
+
<div class="menu" role="menu">
|
|
193
|
+
<slot @click=${() => this.close()}></slot>
|
|
194
|
+
</div>
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare global {
|
|
200
|
+
interface HTMLElementTagNameMap {
|
|
201
|
+
'vox-menu': VoxMenu;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export type RecordListItemSize = 'md' | 'sm';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A list of linked records — search results, an admin index, anything
|
|
8
|
+
* that's a stack of "go to this thing" rows. A grid (heading, meta, end,
|
|
9
|
+
* plus a trailing filler column) is always defined here; regular
|
|
10
|
+
* `size="md"` items ignore the first three tracks and just render their own
|
|
11
|
+
* full-width flex row, while `size="sm"` items opt into them via `subgrid`
|
|
12
|
+
* so their columns line up into a real table across every row, sized to
|
|
13
|
+
* each column's widest cell like a native `<table>`. The filler column
|
|
14
|
+
* absorbs whatever width the three content columns don't use, so every
|
|
15
|
+
* item — table row or not — still spans the list's full width instead of
|
|
16
|
+
* shrinking to fit its content.
|
|
17
|
+
*
|
|
18
|
+
* @slot - `<vox-record-list-item>` elements.
|
|
19
|
+
*/
|
|
20
|
+
@customElement('vox-record-list')
|
|
21
|
+
export class VoxRecordList extends LitElement {
|
|
22
|
+
static styles = css`
|
|
23
|
+
:host {
|
|
24
|
+
display: grid;
|
|
25
|
+
grid-template-columns: repeat(3, max-content) 1fr;
|
|
26
|
+
column-gap: var(--vox-space-4);
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
render() {
|
|
31
|
+
return html`<slot></slot>`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One row in a `<vox-record-list>`: a heading link plus optional metadata
|
|
37
|
+
* — pass `<vox-datum>` elements (or anything else) into the default slot.
|
|
38
|
+
* Becomes fully navigable (heading and trailing arrow both link to `href`)
|
|
39
|
+
* when `href` is set; otherwise renders as a static row.
|
|
40
|
+
*
|
|
41
|
+
* At `size="sm"`, the arrow drops out and heading/meta/`end` become real
|
|
42
|
+
* table columns (via CSS `subgrid`, aligned against every other
|
|
43
|
+
* `size="sm"` row in the same `<vox-record-list>`) — suited to a dense
|
|
44
|
+
* activity/run log rather than an indexed record. `heading` and the meta
|
|
45
|
+
* column each link to `href` individually, since a single grid row can't
|
|
46
|
+
* itself be one native link the way a block row can.
|
|
47
|
+
*
|
|
48
|
+
* @slot - Metadata, e.g. `<vox-datum>` elements.
|
|
49
|
+
* @slot end - Trailing content after the meta, e.g. a `<vox-badge>` status.
|
|
50
|
+
*/
|
|
51
|
+
@customElement('vox-record-list-item')
|
|
52
|
+
export class VoxRecordListItem extends LitElement {
|
|
53
|
+
@property() heading = '';
|
|
54
|
+
@property() href?: string;
|
|
55
|
+
@property({ reflect: true }) size: RecordListItemSize = 'md';
|
|
56
|
+
|
|
57
|
+
static styles = css`
|
|
58
|
+
:host {
|
|
59
|
+
display: block;
|
|
60
|
+
grid-column: 1 / -1;
|
|
61
|
+
font-family: var(--vox-font-family-base);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.row {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
gap: var(--vox-space-4);
|
|
69
|
+
padding: var(--vox-space-4) 0;
|
|
70
|
+
border-bottom: 1px solid var(--vox-color-divider);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:host(:last-child) .row {
|
|
74
|
+
border-bottom: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.main {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--vox-space-1);
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.heading {
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
line-height: 1.4;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
a.heading {
|
|
91
|
+
color: var(--vox-color-brand-1);
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
a.heading:hover,
|
|
96
|
+
a.heading:focus-visible {
|
|
97
|
+
text-decoration: underline;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
a.heading:focus-visible {
|
|
101
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
102
|
+
outline-offset: 2px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
span.heading {
|
|
106
|
+
color: var(--vox-color-text-1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.meta {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: var(--vox-space-4);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.meta:not(.has-meta) {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.arrow {
|
|
121
|
+
display: flex;
|
|
122
|
+
flex: none;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
width: 36px;
|
|
126
|
+
height: 36px;
|
|
127
|
+
border-radius: var(--vox-radius-full);
|
|
128
|
+
background-color: var(--vox-color-bg-soft);
|
|
129
|
+
color: var(--vox-color-brand-1);
|
|
130
|
+
text-decoration: none;
|
|
131
|
+
transition: background-color var(--vox-transition-fast);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.arrow:hover {
|
|
135
|
+
background-color: var(--vox-color-brand-soft);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.arrow:focus-visible {
|
|
139
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
140
|
+
outline-offset: 2px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.arrow svg {
|
|
144
|
+
width: 16px;
|
|
145
|
+
height: 16px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.end:not(.has-end) {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Small size: heading, meta, and end each become their own subgrid
|
|
153
|
+
column, aligned against the same column in every other size="sm"
|
|
154
|
+
row in the parent <vox-record-list> — the arrow drops out since
|
|
155
|
+
there's no room for it at this density, and the whole "row" is
|
|
156
|
+
really 3 separate grid cells rather than one box, so heading and
|
|
157
|
+
the meta cell each link to href individually. */
|
|
158
|
+
:host([size='sm']) {
|
|
159
|
+
display: grid;
|
|
160
|
+
grid-template-columns: subgrid;
|
|
161
|
+
grid-column: 1 / -1;
|
|
162
|
+
align-items: baseline;
|
|
163
|
+
column-gap: var(--vox-space-4);
|
|
164
|
+
padding: var(--vox-space-2) 0;
|
|
165
|
+
border-bottom: 1px solid var(--vox-color-divider);
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
:host([size='sm']:last-child) {
|
|
170
|
+
border-bottom: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
:host([size='sm']) .meta {
|
|
174
|
+
flex-wrap: nowrap;
|
|
175
|
+
gap: var(--vox-space-2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.cell {
|
|
179
|
+
color: var(--vox-color-text-2);
|
|
180
|
+
text-decoration: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
a.cell:hover,
|
|
184
|
+
a.cell:focus-visible {
|
|
185
|
+
text-decoration: underline;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
a.cell:focus-visible {
|
|
189
|
+
outline: 2px solid var(--vox-color-brand-1);
|
|
190
|
+
outline-offset: 2px;
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
private hasMeta = false;
|
|
195
|
+
private hasEnd = false;
|
|
196
|
+
|
|
197
|
+
private handleMetaSlotChange(event: Event) {
|
|
198
|
+
const slot = event.target as HTMLSlotElement;
|
|
199
|
+
this.hasMeta = slot.assignedNodes({ flatten: true }).length > 0;
|
|
200
|
+
this.requestUpdate();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private handleEndSlotChange(event: Event) {
|
|
204
|
+
const slot = event.target as HTMLSlotElement;
|
|
205
|
+
this.hasEnd = slot.assignedNodes({ flatten: true }).length > 0;
|
|
206
|
+
this.requestUpdate();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
render() {
|
|
210
|
+
if (this.size === 'sm') {
|
|
211
|
+
const metaSlot = html`
|
|
212
|
+
<span class="meta ${this.hasMeta ? 'has-meta' : ''}">
|
|
213
|
+
<slot @slotchange=${this.handleMetaSlotChange}></slot>
|
|
214
|
+
</span>
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
return html`
|
|
218
|
+
${this.href
|
|
219
|
+
? html`<a class="cell" href=${this.href}>${this.heading}</a>`
|
|
220
|
+
: html`<span class="cell">${this.heading}</span>`}
|
|
221
|
+
${this.hasMeta && this.href
|
|
222
|
+
? html`<a class="cell" href=${this.href}>${metaSlot}</a>`
|
|
223
|
+
: html`<span class="cell">${metaSlot}</span>`}
|
|
224
|
+
<span class="cell">
|
|
225
|
+
<slot name="end" @slotchange=${this.handleEndSlotChange}></slot>
|
|
226
|
+
</span>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return html`
|
|
231
|
+
<div class="row">
|
|
232
|
+
<div class="main">
|
|
233
|
+
${this.href
|
|
234
|
+
? html`<a class="heading" href=${this.href}>${this.heading}</a>`
|
|
235
|
+
: html`<span class="heading">${this.heading}</span>`}
|
|
236
|
+
<div class="meta ${this.hasMeta ? 'has-meta' : ''}">
|
|
237
|
+
<slot @slotchange=${this.handleMetaSlotChange}></slot>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<span class="end ${this.hasEnd ? 'has-end' : ''}">
|
|
241
|
+
<slot name="end" @slotchange=${this.handleEndSlotChange}></slot>
|
|
242
|
+
</span>
|
|
243
|
+
${this.href
|
|
244
|
+
? html`
|
|
245
|
+
<a class="arrow" href=${this.href} aria-label="View ${this.heading}">
|
|
246
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
247
|
+
<path d="M5 12h14" />
|
|
248
|
+
<path d="m13 6 6 6-6 6" />
|
|
249
|
+
</svg>
|
|
250
|
+
</a>
|
|
251
|
+
`
|
|
252
|
+
: nothing}
|
|
253
|
+
</div>
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
declare global {
|
|
259
|
+
interface HTMLElementTagNameMap {
|
|
260
|
+
'vox-record-list': VoxRecordList;
|
|
261
|
+
'vox-record-list-item': VoxRecordListItem;
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { VoxToc, VoxTocItem } from './components/toc/vox-toc.js';
|
|
|
38
38
|
export { VoxDialog } from './components/dialog/vox-dialog.js';
|
|
39
39
|
export { VoxDisclosure } from './components/disclosure/vox-disclosure.js';
|
|
40
40
|
export { VoxDropdown } from './components/dropdown/vox-dropdown.js';
|
|
41
|
+
export { VoxMenu } from './components/menu/vox-menu.js';
|
|
41
42
|
|
|
42
43
|
// Page content
|
|
43
44
|
export { VoxAccordion, VoxAccordionItem } from './components/accordion/vox-accordion.js';
|
|
@@ -46,8 +47,8 @@ export { VoxBadge } from './components/badge/vox-badge.js';
|
|
|
46
47
|
export { VoxCalendarTile } from './components/calendar-tile/vox-calendar-tile.js';
|
|
47
48
|
export { VoxCallout } from './components/callout/vox-callout.js';
|
|
48
49
|
export { VoxCard } from './components/card/vox-card.js';
|
|
49
|
-
export { VoxCodeBlock } from './components/code-block/vox-code-block.js';
|
|
50
50
|
export { VoxCtaBand } from './components/cta-band/vox-cta-band.js';
|
|
51
|
+
export { VoxDatum } from './components/datum/vox-datum.js';
|
|
51
52
|
export { VoxEmptyState } from './components/empty-state/vox-empty-state.js';
|
|
52
53
|
export { VoxFooter, VoxFooterColumn } from './components/footer/vox-footer.js';
|
|
53
54
|
export { VoxGrid } from './components/grid/vox-grid.js';
|
|
@@ -56,6 +57,7 @@ export { VoxLinkHub, VoxLinkHubItem } from './components/link-hub/vox-link-hub.j
|
|
|
56
57
|
export { VoxLoader } from './components/loader/vox-loader.js';
|
|
57
58
|
export { VoxPagination } from './components/pagination/vox-pagination.js';
|
|
58
59
|
export { VoxQuote } from './components/quote/vox-quote.js';
|
|
60
|
+
export { VoxRecordList, VoxRecordListItem } from './components/record-list/vox-record-list.js';
|
|
59
61
|
export { VoxSponsor, VoxSponsorTier } from './components/sponsor/vox-sponsor.js';
|
|
60
62
|
export { VoxStat } from './components/stat/vox-stat.js';
|
|
61
63
|
export { VoxStepIndicator, VoxStep } from './components/step-indicator/vox-step-indicator.js';
|
|
@@ -65,9 +67,10 @@ export { VoxTimeline, VoxTimelineItem } from './components/timeline/vox-timeline
|
|
|
65
67
|
export type { IconName, IconSize } from './components/icon/vox-icon.js';
|
|
66
68
|
export type { ButtonVariant, ButtonSize } from './components/button/vox-button.js';
|
|
67
69
|
export type { CalloutVariant } from './components/callout/vox-callout.js';
|
|
68
|
-
export type { CodeBlockLanguage } from './components/code-block/vox-code-block.js';
|
|
69
70
|
export type { BadgeVariant } from './components/badge/vox-badge.js';
|
|
70
71
|
export type { AlertVariant } from './components/alert/vox-alert.js';
|
|
71
72
|
export type { AvatarSize } from './components/avatar/vox-avatar.js';
|
|
72
73
|
export type { LoaderSize } from './components/loader/vox-loader.js';
|
|
73
74
|
export type { StepState } from './components/step-indicator/vox-step-indicator.js';
|
|
75
|
+
export type { MenuPlacement } from './components/menu/vox-menu.js';
|
|
76
|
+
export type { RecordListItemSize } from './components/record-list/vox-record-list.js';
|
package/src/tokens/tokens.css
CHANGED
|
@@ -33,15 +33,6 @@
|
|
|
33
33
|
--vox-color-danger-3: #e0575b;
|
|
34
34
|
--vox-color-danger-soft: rgba(244, 63, 94, 0.14);
|
|
35
35
|
|
|
36
|
-
/* Code syntax highlighting — one palette hue per token role */
|
|
37
|
-
--vox-code-comment: var(--vox-palette-gray-700);
|
|
38
|
-
--vox-code-keyword: var(--vox-palette-purple-700);
|
|
39
|
-
--vox-code-string: var(--vox-palette-green-800);
|
|
40
|
-
--vox-code-number: var(--vox-palette-gold-800);
|
|
41
|
-
--vox-code-function: var(--vox-palette-blue-700);
|
|
42
|
-
--vox-code-property: var(--vox-palette-teal-800);
|
|
43
|
-
--vox-code-tag: var(--vox-palette-red-700);
|
|
44
|
-
|
|
45
36
|
/* Surfaces */
|
|
46
37
|
--vox-color-bg: #f5faf9;
|
|
47
38
|
--vox-color-bg-alt: #e8f1ef;
|
|
@@ -105,14 +96,6 @@
|
|
|
105
96
|
--vox-color-danger-3: #b62a3c;
|
|
106
97
|
--vox-color-danger-soft: rgba(244, 63, 94, 0.16);
|
|
107
98
|
|
|
108
|
-
--vox-code-comment: var(--vox-palette-gray-400);
|
|
109
|
-
--vox-code-keyword: var(--vox-palette-purple-300);
|
|
110
|
-
--vox-code-string: var(--vox-palette-green-400);
|
|
111
|
-
--vox-code-number: var(--vox-palette-gold-400);
|
|
112
|
-
--vox-code-function: var(--vox-palette-blue-300);
|
|
113
|
-
--vox-code-property: var(--vox-palette-teal-400);
|
|
114
|
-
--vox-code-tag: var(--vox-palette-red-300);
|
|
115
|
-
|
|
116
99
|
--vox-color-bg: #0d1417;
|
|
117
100
|
--vox-color-bg-alt: #131b1f;
|
|
118
101
|
--vox-color-bg-soft: #131b1f;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A small, dependency-free syntax tokenizer. It trades completeness for
|
|
3
|
-
* being self-contained (no Prism/highlight.js/Shiki) and covers the
|
|
4
|
-
* languages that actually show up in OpenVox docs: shell, Puppet
|
|
5
|
-
* manifests, Hiera/YAML data, JSON, Ruby, plus the web basics.
|
|
6
|
-
*
|
|
7
|
-
* Each language is an ordered list of rules; the first rule whose regex
|
|
8
|
-
* matches at the current position wins, so more specific rules (e.g. "a
|
|
9
|
-
* quoted key before a colon") must be listed before the general-purpose
|
|
10
|
-
* ones they'd otherwise be shadowed by.
|
|
11
|
-
*/
|
|
12
|
-
export type TokenType = 'plain' | 'comment' | 'keyword' | 'string' | 'number' | 'function' | 'property' | 'tag';
|
|
13
|
-
export interface Token {
|
|
14
|
-
type: TokenType;
|
|
15
|
-
text: string;
|
|
16
|
-
}
|
|
17
|
-
type TypeResolver = (matchText: string, code: string, matchEnd: number) => TokenType;
|
|
18
|
-
interface Rule {
|
|
19
|
-
re: RegExp;
|
|
20
|
-
type: TokenType | TypeResolver;
|
|
21
|
-
}
|
|
22
|
-
/** Runs `rules` over `code`, falling back to a single plain-text token for anything unmatched. */
|
|
23
|
-
export declare function tokenize(code: string, rules: Rule[]): Token[];
|
|
24
|
-
export declare const LANGUAGE_LABELS: Record<string, string>;
|
|
25
|
-
export type CodeBlockLanguage = 'plaintext' | 'bash' | 'sh' | 'shell' | 'zsh' | 'yaml' | 'yml' | 'json' | 'javascript' | 'js' | 'typescript' | 'ts' | 'css' | 'html' | 'xml' | 'ruby' | 'rb' | 'puppet' | 'pp';
|
|
26
|
-
/** Tokenizes `code` for `language`; unrecognized languages render as plain, unhighlighted text. */
|
|
27
|
-
export declare function highlight(code: string, language: string): Token[];
|
|
28
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { LitElement } from 'lit';
|
|
2
|
-
export type { CodeBlockLanguage } from './languages.js';
|
|
3
|
-
/**
|
|
4
|
-
* A syntax-highlighted code block with a copy button, no external
|
|
5
|
-
* highlighter required. Covers shell, Puppet, YAML, JSON, Ruby, JS/TS,
|
|
6
|
-
* CSS, and HTML; unrecognized languages render as plain text.
|
|
7
|
-
*
|
|
8
|
-
* @slot - The code, as plain text (escape `<` and `&` as you would in
|
|
9
|
-
* any HTML source, e.g. inside a `<code>` child).
|
|
10
|
-
*/
|
|
11
|
-
export declare class VoxCodeBlock extends LitElement {
|
|
12
|
-
language: string;
|
|
13
|
-
/** Shown in the header instead of/alongside the language label. */
|
|
14
|
-
filename: string;
|
|
15
|
-
lineNumbers: boolean;
|
|
16
|
-
noCopy: boolean;
|
|
17
|
-
/** Hides the whole header bar (filename, language label, and copy button) — for dense contexts like a table cell. */
|
|
18
|
-
noHeader: boolean;
|
|
19
|
-
/** Drops the outer border — for dropping into a surface (e.g. a table cell) that already has its own edge. */
|
|
20
|
-
noBorder: boolean;
|
|
21
|
-
private _code;
|
|
22
|
-
private _copied;
|
|
23
|
-
private _copyResetTimer?;
|
|
24
|
-
static styles: import("lit").CSSResult;
|
|
25
|
-
disconnectedCallback(): void;
|
|
26
|
-
private _handleSlotChange;
|
|
27
|
-
private _copy;
|
|
28
|
-
render(): import("lit-html").TemplateResult<1>;
|
|
29
|
-
}
|
|
30
|
-
declare global {
|
|
31
|
-
interface HTMLElementTagNameMap {
|
|
32
|
-
'vox-code-block': VoxCodeBlock;
|
|
33
|
-
}
|
|
34
|
-
}
|