@boostdev/design-system-components 1.1.1 → 1.1.2

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 (206) hide show
  1. package/dist/client.cjs +50 -50
  2. package/dist/client.css +456 -456
  3. package/dist/client.js +50 -50
  4. package/dist/index.cjs +50 -50
  5. package/dist/index.css +456 -456
  6. package/dist/index.js +50 -50
  7. package/dist/web-components/chunk-2QA62WCQ.js +48 -0
  8. package/dist/web-components/chunk-4BX6AWXX.js +146 -0
  9. package/dist/web-components/chunk-57TRNCWQ.js +93 -0
  10. package/dist/web-components/chunk-5IPHEONG.js +139 -0
  11. package/dist/web-components/chunk-5R6IFELV.js +63 -0
  12. package/dist/web-components/chunk-5TFWOPLH.js +49 -0
  13. package/dist/web-components/chunk-6I3CFC26.js +89 -0
  14. package/dist/web-components/chunk-6T3RKUKG.js +65 -0
  15. package/dist/web-components/chunk-7FMR53BW.js +127 -0
  16. package/dist/web-components/chunk-A67LCT7C.js +134 -0
  17. package/dist/web-components/chunk-AGKJTMJI.js +56 -0
  18. package/dist/web-components/chunk-BQLZ7I4G.js +56 -0
  19. package/dist/web-components/chunk-CAPI25CU.js +53 -0
  20. package/dist/web-components/chunk-CJ5ZOO42.js +229 -0
  21. package/dist/web-components/chunk-CR5PGUHV.js +109 -0
  22. package/dist/web-components/chunk-ECW3HHWA.js +185 -0
  23. package/dist/web-components/chunk-HGBNDR22.js +98 -0
  24. package/dist/web-components/chunk-IK3PTFZ4.js +133 -0
  25. package/dist/web-components/chunk-IQGLIQSO.js +188 -0
  26. package/dist/web-components/chunk-ITHLKR7Q.js +105 -0
  27. package/dist/web-components/chunk-K5CE6HSB.js +169 -0
  28. package/dist/web-components/chunk-L3XHOMN6.js +111 -0
  29. package/dist/{web-components.js → web-components/chunk-LGEZYFUU.js} +62 -844
  30. package/dist/web-components/chunk-LSC4N56Q.js +168 -0
  31. package/dist/web-components/chunk-LY62SAVN.js +64 -0
  32. package/dist/web-components/chunk-O4UFI2RX.js +244 -0
  33. package/dist/web-components/chunk-O7I63SQX.js +124 -0
  34. package/dist/web-components/chunk-OHUTLSIQ.js +69 -0
  35. package/dist/web-components/chunk-OQTRTMAB.js +27 -0
  36. package/dist/web-components/chunk-OTX4COSV.js +187 -0
  37. package/dist/web-components/chunk-PY62FTRW.js +179 -0
  38. package/dist/web-components/chunk-Q4T5BJT6.js +94 -0
  39. package/dist/web-components/chunk-QDRH46TF.js +281 -0
  40. package/dist/web-components/chunk-QDST57JC.js +119 -0
  41. package/dist/web-components/chunk-QPOKHLMQ.js +45 -0
  42. package/dist/web-components/chunk-SOQQ7RVJ.js +88 -0
  43. package/dist/web-components/chunk-UFFCKVZE.js +149 -0
  44. package/dist/web-components/chunk-VM3YHGQ2.js +152 -0
  45. package/dist/web-components/chunk-VMMVWLXL.js +53 -0
  46. package/dist/web-components/chunk-VSS5ITNE.js +109 -0
  47. package/dist/web-components/chunk-XIENXR7H.js +118 -0
  48. package/dist/web-components/chunk-Y4MAXTMA.js +80 -0
  49. package/dist/web-components/chunk-YLVTEY23.js +165 -0
  50. package/dist/web-components/globals.d.ts +2 -0
  51. package/dist/web-components/globals.js +1 -0
  52. package/dist/web-components/index.d.ts +41 -0
  53. package/dist/web-components/index.js +165 -0
  54. package/dist/web-components/interaction/bds-accordion-item.d.ts +54 -0
  55. package/dist/web-components/interaction/bds-accordion-item.js +7 -0
  56. package/dist/web-components/interaction/bds-accordion.d.ts +50 -0
  57. package/dist/web-components/interaction/bds-accordion.js +7 -0
  58. package/dist/web-components/interaction/bds-button.d.ts +100 -0
  59. package/dist/web-components/interaction/bds-button.js +7 -0
  60. package/dist/web-components/interaction/bds-collapsible.d.ts +52 -0
  61. package/dist/web-components/interaction/bds-collapsible.js +7 -0
  62. package/dist/web-components/interaction/bds-dialog.d.ts +56 -0
  63. package/dist/web-components/interaction/bds-dialog.js +7 -0
  64. package/dist/web-components/interaction/bds-drawer.d.ts +64 -0
  65. package/dist/web-components/interaction/bds-drawer.js +7 -0
  66. package/dist/web-components/interaction/bds-popover.d.ts +62 -0
  67. package/dist/web-components/interaction/bds-popover.js +7 -0
  68. package/dist/web-components/interaction/bds-skip-link.d.ts +36 -0
  69. package/dist/web-components/interaction/bds-skip-link.js +7 -0
  70. package/dist/web-components/interaction/bds-tab-panel.d.ts +26 -0
  71. package/dist/web-components/interaction/bds-tab-panel.js +7 -0
  72. package/dist/web-components/interaction/bds-tab.d.ts +45 -0
  73. package/dist/web-components/interaction/bds-tab.js +7 -0
  74. package/dist/web-components/interaction/bds-tabs.d.ts +46 -0
  75. package/dist/web-components/interaction/bds-tabs.js +7 -0
  76. package/dist/web-components/interaction/bds-toast-provider.d.ts +57 -0
  77. package/dist/web-components/interaction/bds-toast-provider.js +7 -0
  78. package/dist/web-components/interaction/bds-tooltip.d.ts +50 -0
  79. package/dist/web-components/interaction/bds-tooltip.js +7 -0
  80. package/dist/web-components/interaction/form/bds-checkbox.d.ts +70 -0
  81. package/dist/web-components/interaction/form/bds-checkbox.js +7 -0
  82. package/dist/web-components/interaction/form/bds-combobox.d.ts +84 -0
  83. package/dist/web-components/interaction/form/bds-combobox.js +7 -0
  84. package/dist/web-components/interaction/form/bds-file-input.d.ts +62 -0
  85. package/dist/web-components/interaction/form/bds-file-input.js +7 -0
  86. package/dist/web-components/interaction/form/bds-number-input.d.ts +68 -0
  87. package/dist/web-components/interaction/form/bds-number-input.js +7 -0
  88. package/dist/web-components/interaction/form/bds-radio.d.ts +68 -0
  89. package/dist/web-components/interaction/form/bds-radio.js +7 -0
  90. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +71 -0
  91. package/dist/web-components/interaction/form/bds-segmented-control.js +7 -0
  92. package/dist/web-components/interaction/form/bds-select.d.ts +79 -0
  93. package/dist/web-components/interaction/form/bds-select.js +7 -0
  94. package/dist/web-components/interaction/form/bds-slider.d.ts +70 -0
  95. package/dist/web-components/interaction/form/bds-slider.js +7 -0
  96. package/dist/web-components/interaction/form/bds-switch.d.ts +67 -0
  97. package/dist/web-components/interaction/form/bds-switch.js +7 -0
  98. package/dist/web-components/interaction/form/bds-textarea.d.ts +70 -0
  99. package/dist/web-components/interaction/form/bds-textarea.js +7 -0
  100. package/dist/web-components/ui/bds-alert.d.ts +66 -0
  101. package/dist/web-components/ui/bds-alert.js +7 -0
  102. package/dist/web-components/ui/bds-avatar.d.ts +50 -0
  103. package/dist/web-components/ui/bds-avatar.js +7 -0
  104. package/dist/web-components/ui/bds-badge.d.ts +35 -0
  105. package/dist/web-components/ui/bds-badge.js +7 -0
  106. package/dist/web-components/ui/bds-breadcrumb.d.ts +35 -0
  107. package/dist/web-components/ui/bds-breadcrumb.js +7 -0
  108. package/dist/web-components/ui/bds-card.d.ts +72 -0
  109. package/dist/web-components/ui/bds-card.js +7 -0
  110. package/dist/web-components/ui/bds-description-list.d.ts +42 -0
  111. package/dist/web-components/ui/bds-description-list.js +7 -0
  112. package/dist/web-components/ui/bds-icon-wrapper.d.ts +39 -0
  113. package/dist/web-components/ui/bds-icon-wrapper.js +7 -0
  114. package/dist/web-components/ui/bds-link.d.ts +59 -0
  115. package/dist/web-components/ui/bds-link.js +7 -0
  116. package/dist/web-components/ui/bds-loading.d.ts +32 -0
  117. package/dist/web-components/ui/bds-loading.js +7 -0
  118. package/dist/web-components/ui/bds-notification-banner.d.ts +61 -0
  119. package/dist/web-components/ui/bds-notification-banner.js +7 -0
  120. package/dist/web-components/ui/bds-progress-circle.d.ts +53 -0
  121. package/dist/web-components/ui/bds-progress-circle.js +7 -0
  122. package/dist/web-components/ui/bds-progress.d.ts +54 -0
  123. package/dist/web-components/ui/bds-progress.js +7 -0
  124. package/dist/web-components/ui/bds-rating.d.ts +36 -0
  125. package/dist/web-components/ui/bds-rating.js +7 -0
  126. package/dist/web-components/ui/bds-section-header.d.ts +53 -0
  127. package/dist/web-components/ui/bds-section-header.js +8 -0
  128. package/dist/web-components/ui/bds-separator.d.ts +32 -0
  129. package/dist/web-components/ui/bds-separator.js +7 -0
  130. package/dist/web-components/ui/bds-skeleton.d.ts +22 -0
  131. package/dist/web-components/ui/bds-skeleton.js +7 -0
  132. package/dist/web-components/ui/bds-typography.d.ts +44 -0
  133. package/dist/web-components/ui/bds-typography.js +8 -0
  134. package/package.json +169 -4
  135. package/src/web-components/index.ts +47 -0
  136. package/src/web-components/interaction/bds-accordion-item.spec.ts +75 -0
  137. package/src/web-components/interaction/bds-accordion-item.ts +181 -0
  138. package/src/web-components/interaction/bds-accordion.spec.ts +73 -0
  139. package/src/web-components/interaction/bds-accordion.ts +99 -0
  140. package/src/web-components/interaction/bds-collapsible.spec.ts +56 -0
  141. package/src/web-components/interaction/bds-collapsible.ts +170 -0
  142. package/src/web-components/interaction/bds-dialog.spec.ts +119 -0
  143. package/src/web-components/interaction/bds-dialog.ts +224 -0
  144. package/src/web-components/interaction/bds-drawer.spec.ts +113 -0
  145. package/src/web-components/interaction/bds-drawer.ts +266 -0
  146. package/src/web-components/interaction/bds-skip-link.spec.ts +42 -0
  147. package/src/web-components/interaction/bds-skip-link.ts +74 -0
  148. package/src/web-components/interaction/bds-tab-panel.spec.ts +32 -0
  149. package/src/web-components/interaction/bds-tab-panel.ts +38 -0
  150. package/src/web-components/interaction/bds-tab.spec.ts +71 -0
  151. package/src/web-components/interaction/bds-tab.ts +110 -0
  152. package/src/web-components/interaction/bds-tabs.spec.ts +120 -0
  153. package/src/web-components/interaction/bds-tabs.ts +152 -0
  154. package/src/web-components/interaction/bds-tooltip.spec.ts +49 -0
  155. package/src/web-components/interaction/bds-tooltip.ts +127 -0
  156. package/src/web-components/interaction/form/bds-checkbox.spec.ts +63 -0
  157. package/src/web-components/interaction/form/bds-checkbox.ts +168 -0
  158. package/src/web-components/interaction/form/bds-combobox.spec.ts +117 -0
  159. package/src/web-components/interaction/form/bds-combobox.ts +336 -0
  160. package/src/web-components/interaction/form/bds-file-input.spec.ts +58 -0
  161. package/src/web-components/interaction/form/bds-file-input.ts +209 -0
  162. package/src/web-components/interaction/form/bds-number-input.spec.ts +87 -0
  163. package/src/web-components/interaction/form/bds-number-input.ts +221 -0
  164. package/src/web-components/interaction/form/bds-radio.spec.ts +70 -0
  165. package/src/web-components/interaction/form/bds-radio.ts +157 -0
  166. package/src/web-components/interaction/form/bds-segmented-control.spec.ts +74 -0
  167. package/src/web-components/interaction/form/bds-segmented-control.ts +211 -0
  168. package/src/web-components/interaction/form/bds-select.spec.ts +74 -0
  169. package/src/web-components/interaction/form/bds-select.ts +190 -0
  170. package/src/web-components/interaction/form/bds-slider.spec.ts +41 -0
  171. package/src/web-components/interaction/form/bds-slider.ts +162 -0
  172. package/src/web-components/interaction/form/bds-switch.spec.ts +62 -0
  173. package/src/web-components/interaction/form/bds-switch.ts +166 -0
  174. package/src/web-components/interaction/form/bds-textarea.spec.ts +55 -0
  175. package/src/web-components/interaction/form/bds-textarea.ts +143 -0
  176. package/src/web-components/ui/bds-avatar.spec.ts +75 -0
  177. package/src/web-components/ui/bds-avatar.ts +122 -0
  178. package/src/web-components/ui/bds-breadcrumb.spec.ts +55 -0
  179. package/src/web-components/ui/bds-breadcrumb.ts +114 -0
  180. package/src/web-components/ui/bds-card.spec.ts +70 -0
  181. package/src/web-components/ui/bds-card.ts +156 -0
  182. package/src/web-components/ui/bds-description-list.spec.ts +36 -0
  183. package/src/web-components/ui/bds-description-list.ts +79 -0
  184. package/src/web-components/ui/bds-icon-wrapper.spec.ts +21 -0
  185. package/src/web-components/ui/bds-icon-wrapper.ts +68 -0
  186. package/src/web-components/ui/bds-link.spec.ts +72 -0
  187. package/src/web-components/ui/bds-link.ts +138 -0
  188. package/src/web-components/ui/bds-loading.spec.ts +38 -0
  189. package/src/web-components/ui/bds-loading.ts +76 -0
  190. package/src/web-components/ui/bds-notification-banner.spec.ts +82 -0
  191. package/src/web-components/ui/bds-notification-banner.ts +185 -0
  192. package/src/web-components/ui/bds-progress-circle.spec.ts +58 -0
  193. package/src/web-components/ui/bds-progress-circle.ts +142 -0
  194. package/src/web-components/ui/bds-progress.spec.ts +66 -0
  195. package/src/web-components/ui/bds-progress.ts +127 -0
  196. package/src/web-components/ui/bds-rating.spec.ts +70 -0
  197. package/src/web-components/ui/bds-rating.ts +80 -0
  198. package/src/web-components/ui/bds-section-header.spec.ts +57 -0
  199. package/src/web-components/ui/bds-section-header.ts +96 -0
  200. package/src/web-components/ui/bds-separator.spec.ts +50 -0
  201. package/src/web-components/ui/bds-separator.ts +66 -0
  202. package/src/web-components/ui/bds-skeleton.spec.ts +22 -0
  203. package/src/web-components/ui/bds-skeleton.ts +55 -0
  204. package/src/web-components/ui/bds-typography.spec.ts +76 -0
  205. package/src/web-components/ui/bds-typography.ts +112 -0
  206. package/dist/web-components.d.ts +0 -304
@@ -0,0 +1,62 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsSwitch } from './bds-switch';
3
+
4
+ describe('bds-switch', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-switch')).toBe(BdsSwitch);
7
+ });
8
+
9
+ it('renders a checkbox input with role="switch"', async () => {
10
+ const el = await fixture('<bds-switch name="notify">Notifications</bds-switch>');
11
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
12
+ expect(input.type).toBe('checkbox');
13
+ expect(input.getAttribute('role')).toBe('switch');
14
+ cleanup(el);
15
+ });
16
+
17
+ it('is off by default', async () => {
18
+ const el = await fixture('<bds-switch name="notify">Notifications</bds-switch>');
19
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
20
+ expect(input.checked).toBe(false);
21
+ cleanup(el);
22
+ });
23
+
24
+ it('is on when checked attribute is set', async () => {
25
+ const el = await fixture('<bds-switch name="notify" checked>Notifications</bds-switch>');
26
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
27
+ expect(input.checked).toBe(true);
28
+ cleanup(el);
29
+ });
30
+
31
+ it('input is disabled when disabled attribute is set', async () => {
32
+ const el = await fixture('<bds-switch name="notify" disabled>Notifications</bds-switch>');
33
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
34
+ expect(input.disabled).toBe(true);
35
+ cleanup(el);
36
+ });
37
+
38
+ it('renders a visual track element', async () => {
39
+ const el = await fixture('<bds-switch name="notify">Notifications</bds-switch>');
40
+ expect(el.shadowRoot!.querySelector('.track')).not.toBeNull();
41
+ cleanup(el);
42
+ });
43
+
44
+ it('renders slotted label text', async () => {
45
+ const el = await fixture('<bds-switch name="notify">Turn on alerts</bds-switch>');
46
+ expect(el.textContent?.trim()).toBe('Turn on alerts');
47
+ cleanup(el);
48
+ });
49
+
50
+ it('updates checked property on change', async () => {
51
+ const el = await fixture('<bds-switch name="notify">Notifications</bds-switch>') as BdsSwitch;
52
+ const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
53
+ input.checked = true;
54
+ input.dispatchEvent(new Event('change', { bubbles: true }));
55
+ expect(el.checked).toBe(true);
56
+ cleanup(el);
57
+ });
58
+
59
+ it('is form-associated', () => {
60
+ expect(BdsSwitch.formAssociated).toBe(true);
61
+ });
62
+ });
@@ -0,0 +1,166 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-switch>` — form-associated Toggle Switch custom element.
5
+ *
6
+ * Visually a toggle switch but semantically equivalent to a checkbox
7
+ * (`role="switch"`). Participates in HTML forms via the Form-Associated
8
+ * Custom Elements API.
9
+ *
10
+ * Attributes:
11
+ * name — form field name
12
+ * value — submitted value when on (default: "on")
13
+ * checked — boolean; whether the switch is on
14
+ * required — boolean
15
+ * disabled — boolean
16
+ *
17
+ * Slots:
18
+ * (default) — label text
19
+ *
20
+ * @example
21
+ * <bds-switch name="notifications" value="enabled">Enable notifications</bds-switch>
22
+ */
23
+ export class BdsSwitch extends LitElement {
24
+ static formAssociated = true;
25
+
26
+ static styles = css`
27
+ :host {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ gap: var(--bds-space_xs);
31
+ cursor: pointer;
32
+ font-size: var(--bds-font_size--body);
33
+ font-family: var(--bds-font_family--body);
34
+ color: var(--bds-color_on-bg);
35
+ }
36
+
37
+ :host([disabled]) {
38
+ opacity: 0.5;
39
+ cursor: not-allowed;
40
+ pointer-events: none;
41
+ }
42
+
43
+ .track {
44
+ position: relative;
45
+ inline-size: 2.75rem;
46
+ block-size: 1.5rem;
47
+ border-radius: 999px;
48
+ background-color: var(--switch_color_bg, var(--bds-color_bg--subtle));
49
+ --bdc_color: currentcolor;
50
+
51
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
52
+ outline-offset: var(--bdc-outline_offset);
53
+ transition: background-color 0.2s ease;
54
+ flex-shrink: 0;
55
+ }
56
+
57
+ .thumb {
58
+ position: absolute;
59
+ inset-block-start: 0.2rem;
60
+ inset-inline-start: 0.2rem;
61
+ inline-size: 1.1rem;
62
+ block-size: 1.1rem;
63
+ border-radius: 50%;
64
+ background-color: var(--switch_thumb_color, var(--bds-color_bg));
65
+ transition: inset-inline-start 0.2s ease;
66
+ }
67
+
68
+ .input {
69
+ position: absolute;
70
+ opacity: 0;
71
+ inline-size: 0;
72
+ block-size: 0;
73
+ margin: 0;
74
+ }
75
+
76
+ .input:checked + .track {
77
+ background-color: var(--switch_color-active, var(--bdc-color_active));
78
+ }
79
+
80
+ .input:checked + .track .thumb {
81
+ inset-inline-start: calc(100% - 1.3rem);
82
+ }
83
+
84
+ .input:focus-visible + .track {
85
+ outline: var(--bds-outline_default);
86
+ outline-offset: var(--bds-outline_offset);
87
+ }
88
+
89
+ label {
90
+ cursor: pointer;
91
+ }
92
+ `;
93
+
94
+ static properties = {
95
+ name: { type: String, reflect: true },
96
+ value: { type: String },
97
+ checked: { type: Boolean, reflect: true },
98
+ required: { type: Boolean, reflect: true },
99
+ disabled: { type: Boolean, reflect: true },
100
+ };
101
+
102
+ declare name: string;
103
+ declare value: string;
104
+ declare checked: boolean;
105
+ declare required: boolean;
106
+ declare disabled: boolean;
107
+
108
+ private _internals!: ElementInternals;
109
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
110
+
111
+ constructor() {
112
+ super();
113
+ this._internals = this.attachInternals();
114
+ this.name = '';
115
+ this.value = 'on';
116
+ this.checked = false;
117
+ this.required = false;
118
+ this.disabled = false;
119
+ }
120
+
121
+ updated(changed: Map<string, unknown>) {
122
+ super.updated(changed);
123
+ if (changed.has('checked') || changed.has('value')) {
124
+ this._internals.setFormValue?.(this.checked ? this.value : null);
125
+ }
126
+ }
127
+
128
+ formResetCallback() {
129
+ this.checked = false;
130
+ this._internals.setFormValue?.(null);
131
+ }
132
+
133
+ private _handleChange(e: Event) {
134
+ this.checked = (e.target as HTMLInputElement).checked;
135
+ }
136
+
137
+ render() {
138
+ const id = `bds-switch-${this._uid}`;
139
+ return html`
140
+ <input
141
+ id=${id}
142
+ type="checkbox"
143
+ role="switch"
144
+ class="input"
145
+ .checked=${this.checked}
146
+ ?required=${this.required}
147
+ ?disabled=${this.disabled}
148
+ @change=${this._handleChange}
149
+ />
150
+ <label for=${id}>
151
+ <span class="track">
152
+ <span class="thumb"></span>
153
+ </span>
154
+ <slot></slot>
155
+ </label>
156
+ `;
157
+ }
158
+ }
159
+
160
+ customElements.define('bds-switch', BdsSwitch);
161
+
162
+ declare global {
163
+ interface HTMLElementTagNameMap {
164
+ 'bds-switch': BdsSwitch;
165
+ }
166
+ }
@@ -0,0 +1,55 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsTextarea } from './bds-textarea';
3
+
4
+ describe('bds-textarea', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-textarea')).toBe(BdsTextarea);
7
+ });
8
+
9
+ it('renders a <textarea> element in shadow DOM', async () => {
10
+ const el = await fixture('<bds-textarea name="message"></bds-textarea>');
11
+ expect(el.shadowRoot!.querySelector('textarea')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('forwards placeholder attribute to inner textarea', async () => {
16
+ const el = await fixture('<bds-textarea name="message" placeholder="Type here…"></bds-textarea>');
17
+ const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea')!;
18
+ expect(textarea.placeholder).toBe('Type here…');
19
+ cleanup(el);
20
+ });
21
+
22
+ it('forwards rows attribute to inner textarea', async () => {
23
+ const el = await fixture('<bds-textarea name="message" rows="8"></bds-textarea>');
24
+ const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea')!;
25
+ expect(textarea.rows).toBe(8);
26
+ cleanup(el);
27
+ });
28
+
29
+ it('is disabled when disabled attribute is set', async () => {
30
+ const el = await fixture('<bds-textarea name="message" disabled></bds-textarea>');
31
+ const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea')!;
32
+ expect(textarea.disabled).toBe(true);
33
+ cleanup(el);
34
+ });
35
+
36
+ it('is required when required attribute is set', async () => {
37
+ const el = await fixture('<bds-textarea name="message" required></bds-textarea>');
38
+ const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea')!;
39
+ expect(textarea.required).toBe(true);
40
+ cleanup(el);
41
+ });
42
+
43
+ it('updates value property on input', async () => {
44
+ const el = await fixture('<bds-textarea name="message"></bds-textarea>') as BdsTextarea;
45
+ const textarea = el.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea')!;
46
+ textarea.value = 'Hello world';
47
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
48
+ expect(el.value).toBe('Hello world');
49
+ cleanup(el);
50
+ });
51
+
52
+ it('is form-associated', () => {
53
+ expect(BdsTextarea.formAssociated).toBe(true);
54
+ });
55
+ });
@@ -0,0 +1,143 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-textarea>` — form-associated Textarea custom element.
5
+ *
6
+ * Wraps a native `<textarea>` for consistent styling while participating
7
+ * in HTML forms via the Form-Associated Custom Elements API.
8
+ *
9
+ * Attributes:
10
+ * name — form field name
11
+ * value — text value
12
+ * placeholder — placeholder text
13
+ * rows — number of visible rows (default: 4)
14
+ * required — boolean
15
+ * disabled — boolean
16
+ *
17
+ * Events:
18
+ * input — native input event
19
+ * change — native change event
20
+ *
21
+ * @example
22
+ * <bds-textarea name="message" placeholder="Enter message…" rows="6"></bds-textarea>
23
+ */
24
+ export class BdsTextarea extends LitElement {
25
+ static formAssociated = true;
26
+
27
+ static styles = css`
28
+ :host {
29
+ display: block;
30
+ }
31
+
32
+ :host([disabled]) {
33
+ opacity: 0.5;
34
+ pointer-events: none;
35
+ }
36
+
37
+ .textarea {
38
+ inline-size: 100%;
39
+ font-family: var(--bds-font_family--body);
40
+ font-size: var(--bds-font_size--body);
41
+ line-height: var(--bds-font_line-height--body);
42
+ padding: var(--bds-space_s);
43
+ border: none;
44
+ --bdc_color: currentcolor;
45
+ --bdc-outline_radius: var(--bds-border_radius--xs);
46
+
47
+ outline: var(--bdc-outline_width) solid var(--bdc_color);
48
+ outline-offset: var(--bdc-outline_offset);
49
+ border-radius: var(--textarea_radius, var(--bdc-outline_radius));
50
+ box-shadow: var(--textarea_shadow, var(--bdc-outline_shadow));
51
+ background-color: var(--textarea_color_bg, var(--bds-color_bg));
52
+ color: var(--textarea_color, var(--bds-color_on-bg));
53
+ resize: vertical;
54
+ min-block-size: calc(var(--bds-space_m) * 5);
55
+ box-sizing: border-box;
56
+ transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing),
57
+ background-color var(--bds-animation_transition-duration) var(--bds-animation_easing);
58
+ }
59
+
60
+ .textarea:focus {
61
+ --bdc_color: var(--bdc_color--focus);
62
+ }
63
+
64
+ .textarea:focus-visible {
65
+ outline: var(--bds-outline_default);
66
+ outline-offset: var(--bds-outline_offset);
67
+ }
68
+
69
+ .textarea:disabled {
70
+ opacity: 0.5;
71
+ cursor: not-allowed;
72
+ resize: none;
73
+ }
74
+ `;
75
+
76
+ static properties = {
77
+ name: { type: String, reflect: true },
78
+ value: { type: String },
79
+ placeholder: { type: String },
80
+ rows: { type: Number },
81
+ required: { type: Boolean, reflect: true },
82
+ disabled: { type: Boolean, reflect: true },
83
+ };
84
+
85
+ declare name: string;
86
+ declare value: string;
87
+ declare placeholder: string;
88
+ declare rows: number;
89
+ declare required: boolean;
90
+ declare disabled: boolean;
91
+
92
+ private _internals!: ElementInternals;
93
+
94
+ constructor() {
95
+ super();
96
+ this._internals = this.attachInternals();
97
+ this.name = '';
98
+ this.value = '';
99
+ this.placeholder = '';
100
+ this.rows = 4;
101
+ this.required = false;
102
+ this.disabled = false;
103
+ }
104
+
105
+ updated(changed: Map<string, unknown>) {
106
+ super.updated(changed);
107
+ if (changed.has('value')) {
108
+ this._internals.setFormValue?.(this.value || null);
109
+ }
110
+ }
111
+
112
+ formResetCallback() {
113
+ this.value = '';
114
+ this._internals.setFormValue?.(null);
115
+ }
116
+
117
+ private _handleInput(e: Event) {
118
+ this.value = (e.target as HTMLTextAreaElement).value;
119
+ }
120
+
121
+ render() {
122
+ return html`
123
+ <textarea
124
+ class="textarea"
125
+ name=${this.name}
126
+ .value=${this.value}
127
+ placeholder=${this.placeholder}
128
+ rows=${this.rows}
129
+ ?required=${this.required}
130
+ ?disabled=${this.disabled}
131
+ @input=${this._handleInput}
132
+ ></textarea>
133
+ `;
134
+ }
135
+ }
136
+
137
+ customElements.define('bds-textarea', BdsTextarea);
138
+
139
+ declare global {
140
+ interface HTMLElementTagNameMap {
141
+ 'bds-textarea': BdsTextarea;
142
+ }
143
+ }
@@ -0,0 +1,75 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsAvatar } from './bds-avatar';
3
+
4
+ describe('bds-avatar', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-avatar')).toBe(BdsAvatar);
7
+ });
8
+
9
+ it('defaults to size="medium"', async () => {
10
+ const el = await fixture('<bds-avatar></bds-avatar>');
11
+ expect(el.getAttribute('size')).toBe('medium');
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders an <img> when src is set', async () => {
16
+ const el = await fixture('<bds-avatar src="/photo.jpg" name="Jane Doe"></bds-avatar>');
17
+ const img = el.shadowRoot!.querySelector('img');
18
+ expect(img).not.toBeNull();
19
+ expect(img!.getAttribute('src')).toBe('/photo.jpg');
20
+ cleanup(el);
21
+ });
22
+
23
+ it('img alt defaults to name when alt is not set', async () => {
24
+ const el = await fixture('<bds-avatar src="/photo.jpg" name="Jane Doe"></bds-avatar>');
25
+ const img = el.shadowRoot!.querySelector('img');
26
+ expect(img!.getAttribute('alt')).toBe('Jane Doe');
27
+ cleanup(el);
28
+ });
29
+
30
+ it('img uses explicit alt when set', async () => {
31
+ const el = await fixture('<bds-avatar src="/photo.jpg" alt="Custom alt" name="Jane Doe"></bds-avatar>');
32
+ const img = el.shadowRoot!.querySelector('img');
33
+ expect(img!.getAttribute('alt')).toBe('Custom alt');
34
+ cleanup(el);
35
+ });
36
+
37
+ it('renders fallback with initials when src is not set', async () => {
38
+ const el = await fixture('<bds-avatar name="Jane Doe"></bds-avatar>');
39
+ const fallback = el.shadowRoot!.querySelector('.fallback');
40
+ expect(fallback).not.toBeNull();
41
+ expect(fallback!.getAttribute('role')).toBe('img');
42
+ const initials = el.shadowRoot!.querySelector('.initials');
43
+ expect(initials!.textContent).toBe('JD');
44
+ cleanup(el);
45
+ });
46
+
47
+ it('computes initials from first two words', async () => {
48
+ const el = await fixture('<bds-avatar name="Alice Bob Charlie"></bds-avatar>');
49
+ const initials = el.shadowRoot!.querySelector('.initials');
50
+ expect(initials!.textContent).toBe('AB');
51
+ cleanup(el);
52
+ });
53
+
54
+ it('fallback aria-label is the name', async () => {
55
+ const el = await fixture('<bds-avatar name="Jane Doe"></bds-avatar>');
56
+ const fallback = el.shadowRoot!.querySelector('.fallback');
57
+ expect(fallback!.getAttribute('aria-label')).toBe('Jane Doe');
58
+ cleanup(el);
59
+ });
60
+
61
+ it('fallback aria-label defaults to "User avatar" when name is not set', async () => {
62
+ const el = await fixture('<bds-avatar></bds-avatar>');
63
+ const fallback = el.shadowRoot!.querySelector('.fallback');
64
+ expect(fallback!.getAttribute('aria-label')).toBe('User avatar');
65
+ cleanup(el);
66
+ });
67
+
68
+ it('reflects size property to attribute', async () => {
69
+ const el = await fixture('<bds-avatar></bds-avatar>') as BdsAvatar;
70
+ el.size = 'large';
71
+ await el.updateComplete;
72
+ expect(el.getAttribute('size')).toBe('large');
73
+ cleanup(el);
74
+ });
75
+ });
@@ -0,0 +1,122 @@
1
+ import { LitElement, css, html, nothing } from 'lit';
2
+
3
+ /**
4
+ * `<bds-avatar>` — framework-agnostic Avatar custom element.
5
+ *
6
+ * Shows an image when `src` is set; falls back to initials derived from `name`.
7
+ *
8
+ * Attributes:
9
+ * src — image URL
10
+ * alt — image alt text (defaults to `name` or "User avatar")
11
+ * name — full name; used to compute initials and as fallback aria-label
12
+ * size — "small" | "medium" (default) | "large"
13
+ *
14
+ * @example
15
+ * <bds-avatar src="/photo.jpg" name="Jane Doe"></bds-avatar>
16
+ * <bds-avatar name="Jane Doe" size="large"></bds-avatar>
17
+ */
18
+ export class BdsAvatar extends LitElement {
19
+ static styles = css`
20
+ :host {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ inline-size: var(--avatar_size, 3em);
25
+ block-size: var(--avatar_size, 3em);
26
+ border-radius: 50%;
27
+ overflow: hidden;
28
+ flex-shrink: 0;
29
+ }
30
+
31
+ :host([size='small']) { --avatar_size: 2em; }
32
+ :host([size='medium']) { --avatar_size: 3em; }
33
+ :host([size='large']) { --avatar_size: 4.5em; }
34
+
35
+ .fallback {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ inline-size: 100%;
40
+ block-size: 100%;
41
+ background-color: var(--avatar_bg, var(--bds-color_blue));
42
+ color: var(--avatar_text, var(--bds-color_on-blue));
43
+ font-weight: var(--bds-font_weight--semibold);
44
+ }
45
+
46
+ .initials {
47
+ line-height: 1;
48
+ user-select: none;
49
+ font-size: var(--bds-font_size--body);
50
+ }
51
+
52
+ :host([size='small']) .initials { font-size: var(--bds-font_size--body--s); }
53
+ :host([size='large']) .initials { font-size: var(--bds-font_size--heading-3); }
54
+
55
+ .image {
56
+ inline-size: 100%;
57
+ block-size: 100%;
58
+ object-fit: cover;
59
+ display: block;
60
+ }
61
+ `;
62
+
63
+ static properties = {
64
+ src: { type: String },
65
+ alt: { type: String },
66
+ name: { type: String },
67
+ size: { type: String, reflect: true },
68
+ };
69
+
70
+ declare src: string;
71
+ declare alt: string;
72
+ declare name: string;
73
+ declare size: 'small' | 'medium' | 'large';
74
+
75
+ constructor() {
76
+ super();
77
+ this.src = '';
78
+ this.alt = '';
79
+ this.name = '';
80
+ this.size = 'medium';
81
+ }
82
+
83
+ private get _initials(): string {
84
+ if (!this.name) return '';
85
+ return this.name
86
+ .split(' ')
87
+ .filter(Boolean)
88
+ .slice(0, 2)
89
+ .map(word => word[0].toUpperCase())
90
+ .join('');
91
+ }
92
+
93
+ render() {
94
+ if (this.src) {
95
+ return html`<img
96
+ class="image"
97
+ src=${this.src}
98
+ alt=${this.alt || this.name || 'User avatar'}
99
+ />`;
100
+ }
101
+
102
+ return html`
103
+ <span
104
+ class="fallback"
105
+ role="img"
106
+ aria-label=${this.name || 'User avatar'}
107
+ >
108
+ ${this._initials
109
+ ? html`<span class="initials" aria-hidden="true">${this._initials}</span>`
110
+ : nothing}
111
+ </span>
112
+ `;
113
+ }
114
+ }
115
+
116
+ customElements.define('bds-avatar', BdsAvatar);
117
+
118
+ declare global {
119
+ interface HTMLElementTagNameMap {
120
+ 'bds-avatar': BdsAvatar;
121
+ }
122
+ }
@@ -0,0 +1,55 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsBreadcrumb } from './bds-breadcrumb';
3
+
4
+ describe('bds-breadcrumb', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-breadcrumb')).toBe(BdsBreadcrumb);
7
+ });
8
+
9
+ it('renders a <nav> with aria-label="Breadcrumb"', async () => {
10
+ const el = await fixture('<bds-breadcrumb></bds-breadcrumb>');
11
+ const nav = el.shadowRoot!.querySelector('nav');
12
+ expect(nav).not.toBeNull();
13
+ expect(nav!.getAttribute('aria-label')).toBe('Breadcrumb');
14
+ cleanup(el);
15
+ });
16
+
17
+ it('renders items as list items', async () => {
18
+ const el = await fixture('<bds-breadcrumb></bds-breadcrumb>') as BdsBreadcrumb;
19
+ el.items = [{ label: 'Home', href: '/' }, { label: 'Products', href: '/products' }, { label: 'Item' }];
20
+ await el.updateComplete;
21
+ const items = el.shadowRoot!.querySelectorAll('.item');
22
+ expect(items.length).toBe(3);
23
+ cleanup(el);
24
+ });
25
+
26
+ it('renders links for non-last items', async () => {
27
+ const el = await fixture('<bds-breadcrumb></bds-breadcrumb>') as BdsBreadcrumb;
28
+ el.items = [{ label: 'Home', href: '/' }, { label: 'Current' }];
29
+ await el.updateComplete;
30
+ const link = el.shadowRoot!.querySelector('a.link');
31
+ expect(link).not.toBeNull();
32
+ expect(link!.getAttribute('href')).toBe('/');
33
+ cleanup(el);
34
+ });
35
+
36
+ it('renders current page as span with aria-current="page"', async () => {
37
+ const el = await fixture('<bds-breadcrumb></bds-breadcrumb>') as BdsBreadcrumb;
38
+ el.items = [{ label: 'Home', href: '/' }, { label: 'Current' }];
39
+ await el.updateComplete;
40
+ const current = el.shadowRoot!.querySelector('.current');
41
+ expect(current).not.toBeNull();
42
+ expect(current!.getAttribute('aria-current')).toBe('page');
43
+ expect(current!.textContent).toBe('Current');
44
+ cleanup(el);
45
+ });
46
+
47
+ it('renders separators between items', async () => {
48
+ const el = await fixture('<bds-breadcrumb></bds-breadcrumb>') as BdsBreadcrumb;
49
+ el.items = [{ label: 'Home', href: '/' }, { label: 'Products', href: '/p' }, { label: 'Item' }];
50
+ await el.updateComplete;
51
+ const separators = el.shadowRoot!.querySelectorAll('.separator');
52
+ expect(separators.length).toBe(2); // between each pair
53
+ cleanup(el);
54
+ });
55
+ });