@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,127 @@
1
+ import { html, css } from 'lit';
2
+ import { customElement, property, state, query } from 'lit/decorators.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
4
+ import { VoxFieldElement, fieldStyles } from '../../internal/field.js';
5
+
6
+ /**
7
+ * A file picker: a themed button backed by a native file input,
8
+ * showing the selected file name(s).
9
+ */
10
+ @customElement('vox-file-input')
11
+ export class VoxFileInput extends VoxFieldElement {
12
+ @property() accept?: string;
13
+ @property({ type: Boolean }) multiple = false;
14
+
15
+ /** Label on the picker button. */
16
+ @property({ attribute: 'button-label' }) buttonLabel = 'Choose a file';
17
+
18
+ @state() private fileNames: string[] = [];
19
+
20
+ @query('input') private inputEl!: HTMLInputElement;
21
+
22
+ static styles = [
23
+ fieldStyles,
24
+ css`
25
+ input {
26
+ position: absolute;
27
+ width: 1px;
28
+ height: 1px;
29
+ opacity: 0;
30
+ }
31
+
32
+ .picker {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: var(--vox-space-3);
36
+ flex-wrap: wrap;
37
+ }
38
+
39
+ .button {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ padding: 0 var(--vox-space-4);
43
+ height: 34px;
44
+ background-color: var(--vox-color-bg-soft);
45
+ border: 1px dashed var(--vox-color-border);
46
+ border-radius: var(--vox-radius-md);
47
+ color: var(--vox-color-text-1);
48
+ font-size: 14px;
49
+ font-weight: 600;
50
+ cursor: pointer;
51
+ transition:
52
+ border-color var(--vox-transition-fast),
53
+ color var(--vox-transition-fast);
54
+ }
55
+
56
+ .button:hover {
57
+ border-color: var(--vox-color-brand-1);
58
+ color: var(--vox-color-brand-1);
59
+ }
60
+
61
+ input:focus-visible ~ .picker .button {
62
+ outline: 2px solid var(--vox-color-brand-1);
63
+ outline-offset: 2px;
64
+ }
65
+
66
+ .names {
67
+ font-size: 13px;
68
+ color: var(--vox-color-text-2);
69
+ }
70
+ `,
71
+ ];
72
+
73
+ formResetCallback() {
74
+ this.fileNames = [];
75
+ if (this.inputEl) this.inputEl.value = '';
76
+ this.internals.setFormValue(null);
77
+ }
78
+
79
+ private handleChange() {
80
+ const files = [...(this.inputEl.files ?? [])];
81
+ this.fileNames = files.map((f) => f.name);
82
+
83
+ const data = new FormData();
84
+ for (const file of files) data.append(this.name, file);
85
+ this.internals.setFormValue(files.length > 0 ? data : null);
86
+
87
+ this.internals.setValidity(
88
+ this.required && files.length === 0 ? { valueMissing: true } : {},
89
+ 'Please select a file.',
90
+ this.inputEl,
91
+ );
92
+ this.dispatchEvent(new Event('change', { bubbles: true }));
93
+ }
94
+
95
+ render() {
96
+ return html`
97
+ <div class="field">
98
+ ${this.renderLabel('file')}
99
+ <label>
100
+ <input
101
+ id="file"
102
+ type="file"
103
+ accept=${ifDefined(this.accept)}
104
+ ?multiple=${this.multiple}
105
+ ?disabled=${this.disabled}
106
+ @change=${this.handleChange}
107
+ />
108
+ <span class="picker">
109
+ <span class="button">${this.buttonLabel}</span>
110
+ <span class="names">
111
+ ${this.fileNames.length > 0
112
+ ? this.fileNames.join(', ')
113
+ : 'No file selected'}
114
+ </span>
115
+ </span>
116
+ </label>
117
+ ${this.renderNote()}
118
+ </div>
119
+ `;
120
+ }
121
+ }
122
+
123
+ declare global {
124
+ interface HTMLElementTagNameMap {
125
+ 'vox-file-input': VoxFileInput;
126
+ }
127
+ }
@@ -0,0 +1,109 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A site footer. The default slot holds link columns
6
+ * (`<vox-footer-column>`), the `bottom` slot holds copyright text.
7
+ *
8
+ * @slot - `<vox-footer-column>` elements.
9
+ * @slot bottom - Copyright / legal line.
10
+ */
11
+ @customElement('vox-footer')
12
+ export class VoxFooter extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: block;
16
+ font-family: var(--vox-font-family-base);
17
+ background-color: var(--vox-color-bg-alt);
18
+ border-top: 1px solid var(--vox-color-divider);
19
+ }
20
+
21
+ .inner {
22
+ max-width: 1280px;
23
+ margin: 0 auto;
24
+ padding: var(--vox-space-8) var(--vox-space-6);
25
+ }
26
+
27
+ .columns {
28
+ display: grid;
29
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
30
+ gap: var(--vox-space-6);
31
+ }
32
+
33
+ .bottom {
34
+ margin-top: var(--vox-space-8);
35
+ padding-top: var(--vox-space-4);
36
+ border-top: 1px solid var(--vox-color-divider);
37
+ font-size: 13px;
38
+ color: var(--vox-color-text-3);
39
+ }
40
+ `;
41
+
42
+ render() {
43
+ return html`
44
+ <footer>
45
+ <div class="inner">
46
+ <div class="columns"><slot></slot></div>
47
+ <div class="bottom"><slot name="bottom"></slot></div>
48
+ </div>
49
+ </footer>
50
+ `;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * One column of footer links.
56
+ *
57
+ * @slot - `<a>` elements.
58
+ */
59
+ @customElement('vox-footer-column')
60
+ export class VoxFooterColumn extends LitElement {
61
+ @property() heading = '';
62
+
63
+ static styles = css`
64
+ :host {
65
+ display: block;
66
+ font-family: var(--vox-font-family-base);
67
+ }
68
+
69
+ .heading {
70
+ margin: 0 0 var(--vox-space-3);
71
+ font-size: 13px;
72
+ font-weight: 600;
73
+ letter-spacing: 0.05em;
74
+ text-transform: uppercase;
75
+ color: var(--vox-color-text-1);
76
+ }
77
+
78
+ .links {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: var(--vox-space-2);
82
+ }
83
+
84
+ ::slotted(a) {
85
+ color: var(--vox-color-text-2);
86
+ font-size: 14px;
87
+ text-decoration: none;
88
+ }
89
+
90
+ ::slotted(a:hover) {
91
+ color: var(--vox-color-brand-1);
92
+ text-decoration: underline;
93
+ }
94
+ `;
95
+
96
+ render() {
97
+ return html`
98
+ <h3 class="heading">${this.heading}</h3>
99
+ <div class="links"><slot></slot></div>
100
+ `;
101
+ }
102
+ }
103
+
104
+ declare global {
105
+ interface HTMLElementTagNameMap {
106
+ 'vox-footer': VoxFooter;
107
+ 'vox-footer-column': VoxFooterColumn;
108
+ }
109
+ }
@@ -0,0 +1,54 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A responsive CSS grid container. By default columns auto-fit to the
6
+ * `min` track width; set `cols` for a fixed column count.
7
+ *
8
+ * @slot - Grid items.
9
+ */
10
+ @customElement('vox-grid')
11
+ export class VoxGrid extends LitElement {
12
+ /** Fixed number of columns; 0 means auto-fit. */
13
+ @property({ type: Number }) cols = 0;
14
+
15
+ /** Minimum column width used when auto-fitting. */
16
+ @property() min = '240px';
17
+
18
+ @property({ reflect: true }) gap: 'sm' | 'md' | 'lg' = 'md';
19
+
20
+ static styles = css`
21
+ :host {
22
+ display: grid;
23
+ }
24
+
25
+ :host([gap='sm']) {
26
+ gap: var(--vox-space-2);
27
+ }
28
+
29
+ :host([gap='md']) {
30
+ gap: var(--vox-space-4);
31
+ }
32
+
33
+ :host([gap='lg']) {
34
+ gap: var(--vox-space-6);
35
+ }
36
+ `;
37
+
38
+ updated() {
39
+ this.style.gridTemplateColumns =
40
+ this.cols > 0
41
+ ? `repeat(${this.cols}, minmax(0, 1fr))`
42
+ : `repeat(auto-fit, minmax(min(${this.min}, 100%), 1fr))`;
43
+ }
44
+
45
+ render() {
46
+ return html`<slot></slot>`;
47
+ }
48
+ }
49
+
50
+ declare global {
51
+ interface HTMLElementTagNameMap {
52
+ 'vox-grid': VoxGrid;
53
+ }
54
+ }
@@ -0,0 +1,109 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A site header with brand, primary navigation, and an actions area.
6
+ * Nav entries are plain links; mark the current page with
7
+ * `aria-current="page"`.
8
+ *
9
+ * @slot logo - Brand mark shown before the site title.
10
+ * @slot - Navigation links.
11
+ * @slot actions - Right-aligned extras (buttons, theme toggle, ...).
12
+ */
13
+ @customElement('vox-header')
14
+ export class VoxHeader extends LitElement {
15
+ /** Site title shown next to the logo. */
16
+ @property({ attribute: 'site-title' }) siteTitle = '';
17
+
18
+ /** Where the brand links to. */
19
+ @property() href = '/';
20
+
21
+ static styles = css`
22
+ :host {
23
+ display: block;
24
+ font-family: var(--vox-font-family-base);
25
+ background-color: var(--vox-color-bg);
26
+ border-bottom: 1px solid var(--vox-color-divider);
27
+ }
28
+
29
+ .header {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: var(--vox-space-6);
33
+ flex-wrap: wrap;
34
+ max-width: 1280px;
35
+ margin: 0 auto;
36
+ padding: var(--vox-space-3) var(--vox-space-6);
37
+ }
38
+
39
+ .brand {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ gap: var(--vox-space-2);
43
+ color: var(--vox-color-text-1);
44
+ font-size: 16px;
45
+ font-weight: 700;
46
+ text-decoration: none;
47
+ }
48
+
49
+ .brand ::slotted(img),
50
+ .brand ::slotted(svg) {
51
+ height: 28px;
52
+ width: auto;
53
+ }
54
+
55
+ nav {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--vox-space-4);
59
+ flex-wrap: wrap;
60
+ flex: 1 1 auto;
61
+ }
62
+
63
+ nav ::slotted(a) {
64
+ color: var(--vox-color-text-2);
65
+ font-size: 14px;
66
+ font-weight: 500;
67
+ text-decoration: none;
68
+ transition: color var(--vox-transition-fast);
69
+ }
70
+
71
+ nav ::slotted(a:hover) {
72
+ color: var(--vox-color-brand-1);
73
+ }
74
+
75
+ nav ::slotted(a[aria-current='page']) {
76
+ color: var(--vox-color-brand-1);
77
+ font-weight: 600;
78
+ }
79
+
80
+ .actions {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: var(--vox-space-3);
84
+ }
85
+ `;
86
+
87
+ render() {
88
+ return html`
89
+ <header class="header">
90
+ <a class="brand" href=${this.href}>
91
+ <slot name="logo"></slot>
92
+ ${this.siteTitle ? html`<span>${this.siteTitle}</span>` : nothing}
93
+ </a>
94
+ <nav aria-label="Main">
95
+ <slot></slot>
96
+ </nav>
97
+ <div class="actions">
98
+ <slot name="actions"></slot>
99
+ </div>
100
+ </header>
101
+ `;
102
+ }
103
+ }
104
+
105
+ declare global {
106
+ interface HTMLElementTagNameMap {
107
+ 'vox-header': VoxHeader;
108
+ }
109
+ }
@@ -0,0 +1,100 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * A page-title header section.
6
+ *
7
+ * @slot - Intro text under the heading.
8
+ * @slot actions - Buttons or CTA links.
9
+ */
10
+ @customElement('vox-hero')
11
+ export class VoxHero extends LitElement {
12
+ /** Small label displayed above the heading. */
13
+ @property() eyebrow = '';
14
+ @property() heading = '';
15
+
16
+ static styles = css`
17
+ :host {
18
+ display: block;
19
+ font-family: var(--vox-font-family-base);
20
+ }
21
+
22
+ .hero {
23
+ padding: var(--vox-space-8) 0;
24
+ border-bottom: 1px solid var(--vox-color-divider);
25
+ }
26
+
27
+ .eyebrow {
28
+ margin: 0 0 var(--vox-space-2);
29
+ font-size: 13px;
30
+ font-weight: 600;
31
+ letter-spacing: 0.05em;
32
+ text-transform: uppercase;
33
+ color: var(--vox-color-brand-1);
34
+ }
35
+
36
+ .heading {
37
+ margin: 0;
38
+ font-family: var(--vox-font-family-display);
39
+ font-size: 36px;
40
+ font-weight: 600;
41
+ line-height: 1.25;
42
+ color: var(--vox-color-text-1);
43
+ }
44
+
45
+ .body {
46
+ max-width: 44rem;
47
+ margin-top: var(--vox-space-3);
48
+ font-size: 17px;
49
+ line-height: 1.7;
50
+ color: var(--vox-color-text-2);
51
+ }
52
+
53
+ .body ::slotted(a) {
54
+ color: var(--vox-color-brand-1);
55
+ text-decoration: underline;
56
+ }
57
+
58
+ .body ::slotted(a:hover) {
59
+ color: var(--vox-color-brand-2);
60
+ }
61
+
62
+ .actions {
63
+ display: flex;
64
+ gap: var(--vox-space-3);
65
+ flex-wrap: wrap;
66
+ margin-top: var(--vox-space-6);
67
+ }
68
+
69
+ .actions:not(.has-content) {
70
+ display: none;
71
+ }
72
+ `;
73
+
74
+ private hasActions = false;
75
+
76
+ private handleActionsSlotChange(event: Event) {
77
+ const slot = event.target as HTMLSlotElement;
78
+ this.hasActions = slot.assignedNodes({ flatten: true }).length > 0;
79
+ this.requestUpdate();
80
+ }
81
+
82
+ render() {
83
+ return html`
84
+ <header class="hero">
85
+ ${this.eyebrow ? html`<p class="eyebrow">${this.eyebrow}</p>` : nothing}
86
+ <h1 class="heading">${this.heading}</h1>
87
+ <div class="body"><slot></slot></div>
88
+ <div class="actions ${this.hasActions ? 'has-content' : ''}">
89
+ <slot name="actions" @slotchange=${this.handleActionsSlotChange}></slot>
90
+ </div>
91
+ </header>
92
+ `;
93
+ }
94
+ }
95
+
96
+ declare global {
97
+ interface HTMLElementTagNameMap {
98
+ 'vox-hero': VoxHero;
99
+ }
100
+ }
@@ -0,0 +1,72 @@
1
+ import { html, nothing } 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 single-line text input with label and help note.
9
+ * Participates in native form submission via ElementInternals.
10
+ */
11
+ @customElement('vox-input')
12
+ export class VoxInput extends VoxFieldElement {
13
+ @property() type = 'text';
14
+ @property() value = '';
15
+ @property() placeholder?: string;
16
+ @property() autocomplete?: string;
17
+ @property({ type: Boolean, reflect: true }) readonly = false;
18
+
19
+ static styles = fieldStyles;
20
+
21
+ formResetCallback() {
22
+ this.value = '';
23
+ }
24
+
25
+ updated() {
26
+ this.internals.setFormValue(this.value);
27
+ const inner = this.renderRoot.querySelector('input');
28
+ if (inner) this.syncValidity(inner);
29
+ }
30
+
31
+ focus(options?: FocusOptions) {
32
+ this.renderRoot.querySelector('input')?.focus(options);
33
+ }
34
+
35
+ private handleInput(event: Event) {
36
+ this.value = (event.target as HTMLInputElement).value;
37
+ }
38
+
39
+ private handleChange() {
40
+ // Native change events don't cross shadow boundaries; re-dispatch.
41
+ this.dispatchEvent(new Event('change', { bubbles: true }));
42
+ }
43
+
44
+ render() {
45
+ return html`
46
+ <div class="field">
47
+ ${this.renderLabel('input')}
48
+ <input
49
+ id="input"
50
+ class="control"
51
+ type=${this.type}
52
+ .value=${live(this.value)}
53
+ placeholder=${ifDefined(this.placeholder)}
54
+ autocomplete=${ifDefined(this.autocomplete)}
55
+ ?required=${this.required}
56
+ ?readonly=${this.readonly}
57
+ ?disabled=${this.disabled}
58
+ aria-label=${this.label ? nothing : 'text input'}
59
+ @input=${this.handleInput}
60
+ @change=${this.handleChange}
61
+ />
62
+ ${this.renderNote()}
63
+ </div>
64
+ `;
65
+ }
66
+ }
67
+
68
+ declare global {
69
+ interface HTMLElementTagNameMap {
70
+ 'vox-input': VoxInput;
71
+ }
72
+ }
@@ -0,0 +1,85 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * Joins inputs, selects, buttons, and text segments into one attached row:
6
+ *
7
+ * ```html
8
+ * <vox-input-group>
9
+ * <vox-input aria-label="Search modules"></vox-input>
10
+ * <vox-button>Search</vox-button>
11
+ * </vox-input-group>
12
+ * ```
13
+ *
14
+ * The group marks each child with a `data-vox-group` position attribute
15
+ * (`start` / `middle` / `end`); VoxBlocks controls flatten their inner
16
+ * corners accordingly. Plain `<span>` children render as text addons.
17
+ */
18
+ @customElement('vox-input-group')
19
+ export class VoxInputGroup extends LitElement {
20
+ static styles = css`
21
+ :host {
22
+ display: block;
23
+ font-family: var(--vox-font-family-base);
24
+ }
25
+
26
+ .group {
27
+ display: flex;
28
+ align-items: stretch;
29
+ }
30
+
31
+ ::slotted(*) {
32
+ flex: none;
33
+ }
34
+
35
+ ::slotted(vox-input),
36
+ ::slotted(vox-select) {
37
+ flex: 1 1 auto;
38
+ min-width: 0;
39
+ }
40
+
41
+ ::slotted(span) {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ padding: 0 var(--vox-space-3);
45
+ background-color: var(--vox-color-bg-soft);
46
+ border: 1px solid var(--vox-color-border);
47
+ color: var(--vox-color-text-2);
48
+ font-size: 14px;
49
+ white-space: nowrap;
50
+ }
51
+
52
+ ::slotted(span[data-vox-group='start']) {
53
+ border-radius: var(--vox-radius-md) 0 0 var(--vox-radius-md);
54
+ border-right: none;
55
+ }
56
+
57
+ ::slotted(span[data-vox-group='end']) {
58
+ border-radius: 0 var(--vox-radius-md) var(--vox-radius-md) 0;
59
+ border-left: none;
60
+ }
61
+ `;
62
+
63
+ private handleSlotChange(event: Event) {
64
+ const children = (event.target as HTMLSlotElement).assignedElements();
65
+ children.forEach((child, index) => {
66
+ const position =
67
+ index === 0 ? 'start' : index === children.length - 1 ? 'end' : 'middle';
68
+ child.setAttribute('data-vox-group', position);
69
+ });
70
+ }
71
+
72
+ render() {
73
+ return html`
74
+ <div class="group" role="group">
75
+ <slot @slotchange=${this.handleSlotChange}></slot>
76
+ </div>
77
+ `;
78
+ }
79
+ }
80
+
81
+ declare global {
82
+ interface HTMLElementTagNameMap {
83
+ 'vox-input-group': VoxInputGroup;
84
+ }
85
+ }