bareframe 0.1.0 → 0.1.1

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 (154) hide show
  1. package/README.md +179 -0
  2. package/dist/bareframe.min.js +119 -0
  3. package/dist/components/accordion.js +66 -0
  4. package/dist/components/autocomplete.css +78 -15
  5. package/dist/components/autocomplete.js +220 -10
  6. package/dist/components/avatar.css +129 -17
  7. package/dist/components/avatar.js +47 -10
  8. package/dist/components/breadcrumb.css +63 -17
  9. package/dist/components/breadcrumb.js +140 -5
  10. package/dist/components/button.css +4 -0
  11. package/dist/components/button.js +95 -15
  12. package/dist/components/chart.css +163 -14
  13. package/dist/components/chart.js +59 -4
  14. package/dist/components/checkbox.css +43 -1
  15. package/dist/components/checkbox.js +98 -5
  16. package/dist/components/dialog.css +95 -0
  17. package/dist/components/dialog.js +172 -4
  18. package/dist/components/divider.css +18 -22
  19. package/dist/components/divider.js +31 -3
  20. package/dist/components/drawer.css +68 -18
  21. package/dist/components/drawer.js +84 -4
  22. package/dist/components/edge.css +54 -0
  23. package/dist/components/edge.js +55 -0
  24. package/dist/components/file-upload.css +72 -3
  25. package/dist/components/file-upload.js +186 -4
  26. package/dist/components/input.css +59 -0
  27. package/dist/components/input.js +369 -4
  28. package/dist/components/list.css +11 -0
  29. package/dist/components/list.js +45 -0
  30. package/dist/components/menu.css +20 -0
  31. package/dist/components/menu.js +144 -0
  32. package/dist/components/modal.css +30 -17
  33. package/dist/components/modal.js +68 -4
  34. package/dist/components/nav.css +39 -0
  35. package/dist/components/progress.css +196 -0
  36. package/dist/components/progress.js +304 -0
  37. package/dist/components/radio.css +35 -1
  38. package/dist/components/radio.js +86 -5
  39. package/dist/components/range.css +91 -0
  40. package/dist/components/range.js +250 -0
  41. package/dist/components/select.css +35 -1
  42. package/dist/components/select.js +255 -4
  43. package/dist/components/skeleton.css +108 -21
  44. package/dist/components/skeleton.js +57 -4
  45. package/dist/components/tab.css +9 -1
  46. package/dist/components/tab.js +66 -1
  47. package/dist/components/tag.css +36 -3
  48. package/dist/components/tag.js +32 -0
  49. package/dist/components/toast.css +113 -0
  50. package/dist/components/toast.js +265 -4
  51. package/dist/components/toggle.css +53 -0
  52. package/dist/components/toggle.js +73 -5
  53. package/dist/components/wizard.css +79 -14
  54. package/dist/components/wizard.js +141 -4
  55. package/dist/index.js +5147 -110
  56. package/dist/manifest.json +5 -42
  57. package/dist/themes/aurora.css +47 -0
  58. package/dist/themes/dark.css +12 -2
  59. package/dist/themes/desert.css +37 -0
  60. package/dist/themes/future.css +47 -0
  61. package/dist/themes/layout.css +191 -0
  62. package/dist/themes/light.css +12 -0
  63. package/dist/themes/matrix.css +37 -0
  64. package/dist/themes/modern.css +64 -0
  65. package/dist/themes/nature.css +47 -0
  66. package/dist/themes/nebula.css +37 -0
  67. package/dist/themes/noir.css +37 -0
  68. package/dist/themes/oceanic.css +37 -0
  69. package/dist/themes/retro.css +47 -0
  70. package/dist/themes/simple.css +47 -0
  71. package/dist/themes/sprint.css +12 -0
  72. package/dist/themes/sunrise.css +37 -0
  73. package/dist/themes/system.css +13 -0
  74. package/package.json +9 -2
  75. package/dist/components/alert.css +0 -30
  76. package/dist/components/alert.js +0 -31
  77. package/dist/components/badge.css +0 -30
  78. package/dist/components/badge.js +0 -31
  79. package/dist/components/banner.css +0 -30
  80. package/dist/components/banner.js +0 -31
  81. package/dist/components/bar-chart.css +0 -30
  82. package/dist/components/bar-chart.js +0 -31
  83. package/dist/components/bottom-sheet.css +0 -30
  84. package/dist/components/bottom-sheet.js +0 -31
  85. package/dist/components/button-group.css +0 -30
  86. package/dist/components/button-group.js +0 -31
  87. package/dist/components/chip.css +0 -30
  88. package/dist/components/chip.js +0 -31
  89. package/dist/components/color-picker.css +0 -30
  90. package/dist/components/color-picker.js +0 -31
  91. package/dist/components/context-menu.css +0 -30
  92. package/dist/components/context-menu.js +0 -31
  93. package/dist/components/donut-chart.css +0 -30
  94. package/dist/components/donut-chart.js +0 -31
  95. package/dist/components/expanded-panel.css +0 -30
  96. package/dist/components/expanded-panel.js +0 -31
  97. package/dist/components/footer.css +0 -30
  98. package/dist/components/footer.js +0 -31
  99. package/dist/components/gantt-chart.css +0 -30
  100. package/dist/components/gantt-chart.js +0 -31
  101. package/dist/components/gauge.css +0 -30
  102. package/dist/components/gauge.js +0 -31
  103. package/dist/components/graph.css +0 -30
  104. package/dist/components/graph.js +0 -31
  105. package/dist/components/header.css +0 -30
  106. package/dist/components/header.js +0 -31
  107. package/dist/components/heatmap.css +0 -30
  108. package/dist/components/heatmap.js +0 -31
  109. package/dist/components/line-chart.css +0 -30
  110. package/dist/components/line-chart.js +0 -31
  111. package/dist/components/list-item.css +0 -30
  112. package/dist/components/list-item.js +0 -31
  113. package/dist/components/menu-item.css +0 -30
  114. package/dist/components/menu-item.js +0 -31
  115. package/dist/components/multi-select.css +0 -30
  116. package/dist/components/multi-select.js +0 -31
  117. package/dist/components/notification.css +0 -30
  118. package/dist/components/notification.js +0 -31
  119. package/dist/components/pie-chart.css +0 -30
  120. package/dist/components/pie-chart.js +0 -31
  121. package/dist/components/popover.css +0 -30
  122. package/dist/components/popover.js +0 -31
  123. package/dist/components/progress-bar.css +0 -30
  124. package/dist/components/progress-bar.js +0 -31
  125. package/dist/components/progress-circle.css +0 -30
  126. package/dist/components/progress-circle.js +0 -31
  127. package/dist/components/radio-group.css +0 -30
  128. package/dist/components/radio-group.js +0 -31
  129. package/dist/components/range-slider.css +0 -30
  130. package/dist/components/range-slider.js +0 -31
  131. package/dist/components/rating.css +0 -30
  132. package/dist/components/rating.js +0 -31
  133. package/dist/components/sheet.css +0 -30
  134. package/dist/components/sheet.js +0 -31
  135. package/dist/components/slider.css +0 -30
  136. package/dist/components/slider.js +0 -31
  137. package/dist/components/snackbar.css +0 -30
  138. package/dist/components/snackbar.js +0 -31
  139. package/dist/components/sparkline.css +0 -30
  140. package/dist/components/sparkline.js +0 -31
  141. package/dist/components/stepper.css +0 -30
  142. package/dist/components/stepper.js +0 -31
  143. package/dist/components/switch.css +0 -30
  144. package/dist/components/switch.js +0 -31
  145. package/dist/components/tab-group.css +0 -30
  146. package/dist/components/tab-group.js +0 -31
  147. package/dist/components/textfield.css +0 -30
  148. package/dist/components/textfield.js +0 -31
  149. package/dist/components/tooltip.css +0 -30
  150. package/dist/components/tooltip.js +0 -31
  151. package/dist/components/treemap.css +0 -30
  152. package/dist/components/treemap.js +0 -31
  153. package/dist/components/upload-dropzone.css +0 -30
  154. package/dist/components/upload-dropzone.js +0 -31
@@ -1,11 +1,25 @@
1
1
  class BfSelect extends HTMLElement {
2
+ static observedAttributes = [
3
+ 'value',
4
+ 'multi',
5
+ 'disabled',
6
+ 'required',
7
+ 'placeholder',
8
+ 'label',
9
+ 'name',
10
+ ];
11
+
2
12
  constructor() {
3
13
  super();
4
14
  this.attachShadow({ mode: 'open' });
15
+ this._onChange = this._onChange.bind(this);
16
+ this._onLightDomMutate = this._onLightDomMutate.bind(this);
5
17
  }
6
18
 
7
19
  connectedCallback() {
8
20
  if (this._initialized) {
21
+ this._syncOptions();
22
+ this._syncState();
9
23
  return;
10
24
  }
11
25
  this._initialized = true;
@@ -18,13 +32,250 @@ class BfSelect extends HTMLElement {
18
32
  const root = document.createElement('div');
19
33
  root.className = 'root';
20
34
  root.setAttribute('part', 'root');
21
- root.innerHTML = '<slot></slot>';
35
+ root.innerHTML = `
36
+ <select part="select"></select>
37
+ <span class="chevron" part="chevron" aria-hidden="true"></span>
38
+ `;
39
+
40
+ this.shadowRoot.replaceChildren(link, root);
41
+ this._root = root;
42
+ this._select = root.querySelector('select');
43
+ this._select.addEventListener('change', this._onChange);
44
+
45
+ this._observer = new MutationObserver(this._onLightDomMutate);
46
+ this._observer.observe(this, {
47
+ childList: true,
48
+ subtree: true,
49
+ characterData: true,
50
+ attributes: true,
51
+ attributeFilter: ['value', 'selected', 'disabled', 'label'],
52
+ });
53
+
54
+ this._syncOptions();
55
+ this._syncState();
56
+ }
22
57
 
23
- if (!this.innerHTML.trim()) {
24
- root.textContent = 'select';
58
+ disconnectedCallback() {
59
+ if (this._observer) {
60
+ this._observer.disconnect();
25
61
  }
62
+ }
26
63
 
27
- this.shadowRoot.replaceChildren(link, root);
64
+ attributeChangedCallback(name) {
65
+ if (name === 'value' && this._reflectingValue) {
66
+ return;
67
+ }
68
+ this._syncState();
69
+ }
70
+
71
+ get value() {
72
+ if (this._isMulti()) {
73
+ return this.values;
74
+ }
75
+ return this._select?.value || '';
76
+ }
77
+
78
+ set value(nextValue) {
79
+ if (Array.isArray(nextValue)) {
80
+ this.setAttribute('value', nextValue.join(','));
81
+ return;
82
+ }
83
+ this.setAttribute('value', `${nextValue ?? ''}`);
84
+ }
85
+
86
+ get values() {
87
+ if (!this._select) {
88
+ return [];
89
+ }
90
+ return [...this._select.selectedOptions].map((option) => option.value);
91
+ }
92
+
93
+ _onLightDomMutate() {
94
+ this._syncOptions();
95
+ this._syncState();
96
+ }
97
+
98
+ _onChange() {
99
+ this._enforceMultiLimit();
100
+ this._reflectValueAttribute();
101
+ this.dispatchEvent(
102
+ new CustomEvent('bf-change', {
103
+ bubbles: true,
104
+ composed: true,
105
+ detail: {
106
+ value: this._isMulti() ? this.values : this._select.value,
107
+ values: this.values,
108
+ multiple: this._isMulti(),
109
+ multi: this._multiLimit(),
110
+ },
111
+ }),
112
+ );
113
+ }
114
+
115
+ _isMulti() {
116
+ return this.hasAttribute('multi');
117
+ }
118
+
119
+ _multiLimit() {
120
+ if (!this._isMulti()) {
121
+ return 1;
122
+ }
123
+ const raw = this.getAttribute('multi');
124
+ if (raw === '' || raw === null) {
125
+ return Infinity;
126
+ }
127
+ const parsed = Number.parseInt(raw, 10);
128
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : Infinity;
129
+ }
130
+
131
+ _parsedValueAttribute() {
132
+ const raw = this.getAttribute('value');
133
+ if (raw === null) {
134
+ return [];
135
+ }
136
+ if (this._isMulti()) {
137
+ return raw
138
+ .split(',')
139
+ .map((value) => value.trim())
140
+ .filter(Boolean);
141
+ }
142
+ return [raw];
143
+ }
144
+
145
+ cloneLightDomOptions() {
146
+ const fragment = document.createDocumentFragment();
147
+ let count = 0;
148
+
149
+ for (const child of this.children) {
150
+ const tag = child.tagName.toLowerCase();
151
+ if (tag === 'option') {
152
+ fragment.append(this._cloneOption(child));
153
+ count += 1;
154
+ continue;
155
+ }
156
+ if (tag === 'optgroup') {
157
+ const group = document.createElement('optgroup');
158
+ const label = child.getAttribute('label');
159
+ if (label) {
160
+ group.label = label;
161
+ }
162
+ for (const option of child.querySelectorAll('option')) {
163
+ group.append(this._cloneOption(option));
164
+ count += 1;
165
+ }
166
+ fragment.append(group);
167
+ }
168
+ }
169
+
170
+ return { fragment, count };
171
+ }
172
+
173
+ _cloneOption(option) {
174
+ const clone = document.createElement('option');
175
+ clone.value = option.getAttribute('value') ?? option.textContent?.trim() ?? '';
176
+ clone.textContent = option.textContent?.trim() ?? '';
177
+ clone.disabled = option.hasAttribute('disabled');
178
+ clone.defaultSelected = option.hasAttribute('selected');
179
+ clone.selected = option.hasAttribute('selected');
180
+ return clone;
181
+ }
182
+
183
+ _syncOptions() {
184
+ if (!this._select) {
185
+ return;
186
+ }
187
+
188
+ const { fragment, count } = this.cloneLightDomOptions();
189
+ this._select.replaceChildren();
190
+
191
+ const placeholder = this.getAttribute('placeholder');
192
+ const hasPlaceholder = Boolean(placeholder && !this._isMulti());
193
+ if (hasPlaceholder) {
194
+ const placeholderOption = document.createElement('option');
195
+ placeholderOption.value = '';
196
+ placeholderOption.textContent = placeholder;
197
+ this._select.append(placeholderOption);
198
+ }
199
+
200
+ if (count > 0) {
201
+ this._select.append(fragment);
202
+ } else {
203
+ const fallback = this.textContent?.trim();
204
+ if (fallback) {
205
+ const option = document.createElement('option');
206
+ option.value = fallback;
207
+ option.textContent = fallback;
208
+ this._select.append(option);
209
+ }
210
+ }
211
+ }
212
+
213
+ _applyValueAttribute() {
214
+ if (!this.hasAttribute('value')) {
215
+ return;
216
+ }
217
+
218
+ const values = new Set(this._parsedValueAttribute());
219
+ if (this._isMulti()) {
220
+ for (const option of this._select.options) {
221
+ option.selected = values.has(option.value);
222
+ }
223
+ return;
224
+ }
225
+
226
+ const first = this._parsedValueAttribute()[0] ?? '';
227
+ this._select.value = first;
228
+ }
229
+
230
+ _enforceMultiLimit() {
231
+ if (!this._isMulti()) {
232
+ return;
233
+ }
234
+ const limit = this._multiLimit();
235
+ if (limit === Infinity) {
236
+ return;
237
+ }
238
+ const selected = [...this._select.options].filter((option) => option.selected);
239
+ if (selected.length <= limit) {
240
+ return;
241
+ }
242
+ for (let index = limit; index < selected.length; index += 1) {
243
+ selected[index].selected = false;
244
+ }
245
+ }
246
+
247
+ _reflectValueAttribute() {
248
+ if (!this._select) {
249
+ return;
250
+ }
251
+ const nextValue = this._isMulti() ? this.values.join(',') : this._select.value;
252
+ if (this.getAttribute('value') === nextValue) {
253
+ return;
254
+ }
255
+ this._reflectingValue = true;
256
+ this.setAttribute('value', nextValue);
257
+ this._reflectingValue = false;
258
+ }
259
+
260
+ _syncState() {
261
+ if (!this._select || !this._root) {
262
+ return;
263
+ }
264
+
265
+ this._select.multiple = this._isMulti();
266
+ this._root.classList.toggle('is-multi', this._isMulti());
267
+ this._select.disabled = this.hasAttribute('disabled');
268
+ this._select.required = this.hasAttribute('required');
269
+ this._select.name = this.getAttribute('name') || '';
270
+ if (this.getAttribute('label')) {
271
+ this._select.setAttribute('aria-label', this.getAttribute('label'));
272
+ } else {
273
+ this._select.removeAttribute('aria-label');
274
+ }
275
+
276
+ this._applyValueAttribute();
277
+ this._enforceMultiLimit();
278
+ this._reflectValueAttribute();
28
279
  }
29
280
  }
30
281
 
@@ -1,30 +1,117 @@
1
1
  :host {
2
- --bf-skeleton-font: var(--bf-theme-font-family, inherit);
3
2
  --bf-skeleton-radius: var(--bf-theme-radius-md, 8px);
4
- --bf-skeleton-border-width: var(--bf-theme-border-width, 1px);
5
- --bf-skeleton-border-style: var(--bf-theme-border-style, solid);
6
- --bf-skeleton-border-color: var(--bf-theme-skeleton-border-color, var(--bf-theme-border-1, #cbd5e1));
7
- --bf-skeleton-bg: var(--bf-theme-skeleton-bg, var(--bf-theme-surface-1, #ffffff));
8
- --bf-skeleton-color: var(--bf-theme-skeleton-color, var(--bf-theme-text-1, #0f172a));
9
- --bf-skeleton-padding-y: var(--bf-theme-space-2, 0.6rem);
10
- --bf-skeleton-padding-x: var(--bf-theme-space-3, 0.9rem);
11
- --bf-skeleton-transition:
12
- var(--bf-theme-transition-bg, background-color 120ms ease),
13
- var(--bf-theme-transition-color, color 120ms ease),
14
- var(--bf-theme-transition-border, border-color 120ms ease);
3
+ --bf-skeleton-width: 100%;
4
+ --bf-skeleton-height: 0.9rem;
5
+ --bf-skeleton-base: color-mix(in srgb, var(--bf-theme-border-1, #cbd5e1) 55%, var(--bf-theme-surface-1, #ffffff));
6
+ --bf-skeleton-highlight: color-mix(in srgb, var(--bf-theme-surface-1, #ffffff) 86%, transparent);
7
+ --bf-skeleton-speed: 1.15s;
15
8
 
16
9
  display: block;
17
- font: var(--bf-skeleton-font);
18
- color: var(--bf-skeleton-color);
19
10
  }
20
11
 
21
12
  .root {
22
- background: var(--bf-skeleton-bg);
23
- color: var(--bf-skeleton-color);
24
- border-width: var(--bf-skeleton-border-width);
25
- border-style: var(--bf-skeleton-border-style);
26
- border-color: var(--bf-skeleton-border-color);
13
+ position: relative;
14
+ overflow: hidden;
15
+ width: var(--bf-skeleton-width);
16
+ height: var(--bf-skeleton-height);
17
+ background: var(--bf-skeleton-base);
27
18
  border-radius: var(--bf-skeleton-radius);
28
- padding: var(--bf-skeleton-padding-y) var(--bf-skeleton-padding-x);
29
- transition: var(--bf-skeleton-transition);
19
+ color: transparent;
20
+ user-select: none;
21
+ }
22
+
23
+ .root[data-animate='on']::after {
24
+ content: '';
25
+ position: absolute;
26
+ inset: 0;
27
+ background: linear-gradient(
28
+ 100deg,
29
+ transparent 0%,
30
+ var(--bf-skeleton-highlight) 45%,
31
+ transparent 80%
32
+ );
33
+ transform: translateX(-100%);
34
+ animation: bf-skeleton-shimmer var(--bf-skeleton-speed) ease-in-out infinite;
35
+ }
36
+
37
+ .root[data-variant='title'] {
38
+ --bf-skeleton-height: 1.05rem;
39
+ --bf-skeleton-width: 58%;
40
+ }
41
+
42
+ .root[data-variant='avatar'] {
43
+ --bf-skeleton-width: 2.5rem;
44
+ --bf-skeleton-height: 2.5rem;
45
+ --bf-skeleton-radius: 999px;
46
+ }
47
+
48
+ .root[data-variant='button'] {
49
+ --bf-skeleton-width: 8rem;
50
+ --bf-skeleton-height: 2.1rem;
51
+ --bf-skeleton-radius: 10px;
52
+ }
53
+
54
+ .root[data-variant='image'] {
55
+ --bf-skeleton-height: auto;
56
+ --bf-skeleton-width: 100%;
57
+ aspect-ratio: 16 / 9;
58
+ border-radius: 12px;
59
+ }
60
+
61
+ .root[data-variant='paragraph'] {
62
+ --bf-skeleton-height: auto;
63
+ background: transparent;
64
+ display: grid;
65
+ gap: 0.45rem;
66
+ }
67
+
68
+ .root[data-variant='paragraph']::after {
69
+ display: none;
70
+ }
71
+
72
+ .lines {
73
+ display: none;
74
+ }
75
+
76
+ .root[data-variant='paragraph'] .lines {
77
+ display: grid;
78
+ gap: 0.45rem;
79
+ }
80
+
81
+ .line {
82
+ position: relative;
83
+ display: block;
84
+ height: 0.78rem;
85
+ border-radius: 999px;
86
+ background: var(--bf-skeleton-base);
87
+ overflow: hidden;
88
+ }
89
+
90
+ .root[data-variant='paragraph'][data-animate='on'] .line::after {
91
+ content: '';
92
+ position: absolute;
93
+ inset: 0;
94
+ background: linear-gradient(
95
+ 100deg,
96
+ transparent 0%,
97
+ var(--bf-skeleton-highlight) 45%,
98
+ transparent 80%
99
+ );
100
+ transform: translateX(-100%);
101
+ animation: bf-skeleton-shimmer var(--bf-skeleton-speed) ease-in-out infinite;
102
+ }
103
+
104
+ slot {
105
+ display: none;
106
+ }
107
+
108
+ @keyframes bf-skeleton-shimmer {
109
+ 100% { transform: translateX(120%); }
110
+ }
111
+
112
+ @media (prefers-reduced-motion: reduce) {
113
+ .root::after,
114
+ .line::after {
115
+ animation: none !important;
116
+ }
30
117
  }
@@ -1,4 +1,6 @@
1
1
  class BfSkeleton extends HTMLElement {
2
+ static observedAttributes = ['variant', 'lines', 'width', 'height', 'radius', 'static'];
3
+
2
4
  constructor() {
3
5
  super();
4
6
  this.attachShadow({ mode: 'open' });
@@ -6,6 +8,7 @@ class BfSkeleton extends HTMLElement {
6
8
 
7
9
  connectedCallback() {
8
10
  if (this._initialized) {
11
+ this._sync();
9
12
  return;
10
13
  }
11
14
  this._initialized = true;
@@ -18,13 +21,63 @@ class BfSkeleton extends HTMLElement {
18
21
  const root = document.createElement('div');
19
22
  root.className = 'root';
20
23
  root.setAttribute('part', 'root');
21
- root.innerHTML = '<slot></slot>';
24
+ root.innerHTML = '<div class="lines" part="lines"></div><slot></slot>';
25
+
26
+ this.shadowRoot.replaceChildren(link, root);
27
+ this._root = root;
28
+ this._lines = root.querySelector('.lines');
29
+ this._sync();
30
+ }
31
+
32
+ attributeChangedCallback() {
33
+ this._sync();
34
+ }
22
35
 
23
- if (!this.innerHTML.trim()) {
24
- root.textContent = 'skeleton';
36
+ _sync() {
37
+ if (!this._root || !this._lines) {
38
+ return;
25
39
  }
26
40
 
27
- this.shadowRoot.replaceChildren(link, root);
41
+ const rawVariant = (this.getAttribute('variant') || 'text').toLowerCase();
42
+ const lines = Math.max(1, Number.parseInt(this.getAttribute('lines') || '1', 10) || 1);
43
+ const variant = rawVariant === 'text' && lines > 1 ? 'paragraph' : rawVariant;
44
+ this._root.dataset.variant = variant;
45
+ this._root.dataset.animate = this.hasAttribute('static') ? 'off' : 'on';
46
+
47
+ this._setSizeVar('--bf-skeleton-width', this.getAttribute('width'));
48
+ this._setSizeVar('--bf-skeleton-height', this.getAttribute('height'));
49
+ this._setSizeVar('--bf-skeleton-radius', this.getAttribute('radius'));
50
+
51
+ if (variant !== 'paragraph') {
52
+ this._lines.replaceChildren();
53
+ return;
54
+ }
55
+
56
+ const widths = ['96%', '90%', '94%', '86%', '92%', '84%'];
57
+ const nodes = [];
58
+ for (let i = 0; i < lines; i += 1) {
59
+ const line = document.createElement('span');
60
+ line.className = 'line';
61
+ const width = i === lines - 1 ? '68%' : widths[i % widths.length];
62
+ line.style.width = width;
63
+ nodes.push(line);
64
+ }
65
+ this._lines.replaceChildren(...nodes);
66
+ }
67
+
68
+ _setSizeVar(name, value) {
69
+ if (!value) {
70
+ this.style.removeProperty(name);
71
+ return;
72
+ }
73
+
74
+ const trimmed = value.trim();
75
+ if (/^-?\d*\.?\d+$/.test(trimmed)) {
76
+ this.style.setProperty(name, `${trimmed}px`);
77
+ return;
78
+ }
79
+
80
+ this.style.setProperty(name, trimmed);
28
81
  }
29
82
  }
30
83
 
@@ -13,12 +13,14 @@
13
13
  var(--bf-theme-transition-color, color 120ms ease),
14
14
  var(--bf-theme-transition-border, border-color 120ms ease);
15
15
 
16
- display: block;
16
+ display: inline-block;
17
17
  font: var(--bf-tab-font);
18
18
  color: var(--bf-tab-color);
19
19
  }
20
20
 
21
21
  .root {
22
+ font: inherit;
23
+ cursor: pointer;
22
24
  background: var(--bf-tab-bg);
23
25
  color: var(--bf-tab-color);
24
26
  border-width: var(--bf-tab-border-width);
@@ -28,3 +30,9 @@
28
30
  padding: var(--bf-tab-padding-y) var(--bf-tab-padding-x);
29
31
  transition: var(--bf-tab-transition);
30
32
  }
33
+
34
+ .root.is-selected {
35
+ background: var(--bf-theme-button-primary-bg, #2563eb);
36
+ color: var(--bf-theme-button-primary-color, #ffffff);
37
+ border-color: var(--bf-theme-button-primary-border-color, #1d4ed8);
38
+ }
@@ -1,11 +1,15 @@
1
1
  class BfTab extends HTMLElement {
2
+ static observedAttributes = ['selected', 'disabled', 'group', 'label', 'value'];
3
+
2
4
  constructor() {
3
5
  super();
4
6
  this.attachShadow({ mode: 'open' });
7
+ this._onClick = this._onClick.bind(this);
5
8
  }
6
9
 
7
10
  connectedCallback() {
8
11
  if (this._initialized) {
12
+ this._syncState();
9
13
  return;
10
14
  }
11
15
  this._initialized = true;
@@ -15,9 +19,10 @@ class BfTab extends HTMLElement {
15
19
  link.rel = 'stylesheet';
16
20
  link.href = cssUrl.href;
17
21
 
18
- const root = document.createElement('div');
22
+ const root = document.createElement('button');
19
23
  root.className = 'root';
20
24
  root.setAttribute('part', 'root');
25
+ root.type = 'button';
21
26
  root.innerHTML = '<slot></slot>';
22
27
 
23
28
  if (!this.innerHTML.trim()) {
@@ -25,6 +30,66 @@ class BfTab extends HTMLElement {
25
30
  }
26
31
 
27
32
  this.shadowRoot.replaceChildren(link, root);
33
+ this._button = root;
34
+ root.addEventListener('click', this._onClick);
35
+ this._syncState();
36
+ }
37
+
38
+ attributeChangedCallback() {
39
+ this._syncState();
40
+ }
41
+
42
+ get selected() {
43
+ return this.hasAttribute('selected');
44
+ }
45
+
46
+ set selected(value) {
47
+ if (value) {
48
+ this.setAttribute('selected', '');
49
+ return;
50
+ }
51
+ this.removeAttribute('selected');
52
+ }
53
+
54
+ get value() {
55
+ return this.getAttribute('value') || this.textContent?.trim() || '';
56
+ }
57
+
58
+ _onClick() {
59
+ const group = this.getAttribute('group');
60
+ if (group) {
61
+ const peers = document.querySelectorAll(`bf-tab[group="${CSS.escape(group)}"]`);
62
+ for (const peer of peers) {
63
+ peer.selected = peer === this;
64
+ }
65
+ } else {
66
+ this.selected = true;
67
+ }
68
+
69
+ this.dispatchEvent(
70
+ new CustomEvent('bf-change', {
71
+ bubbles: true,
72
+ composed: true,
73
+ detail: {
74
+ group: group || '',
75
+ value: this.value,
76
+ selected: this.selected,
77
+ },
78
+ }),
79
+ );
80
+ }
81
+
82
+ _syncState() {
83
+ if (!this._button) {
84
+ return;
85
+ }
86
+ if (this.getAttribute('label')) {
87
+ this._button.setAttribute('aria-label', this.getAttribute('label'));
88
+ }
89
+ this._button.disabled = this.hasAttribute('disabled');
90
+ this._button.setAttribute('aria-selected', String(this.selected));
91
+ this._button.setAttribute('role', 'tab');
92
+ this._button.classList.toggle('is-selected', this.selected);
28
93
  }
29
94
  }
30
95
 
@@ -6,19 +6,26 @@
6
6
  --bf-tag-border-color: var(--bf-theme-tag-border-color, var(--bf-theme-border-1, #cbd5e1));
7
7
  --bf-tag-bg: var(--bf-theme-tag-bg, var(--bf-theme-surface-1, #ffffff));
8
8
  --bf-tag-color: var(--bf-theme-tag-color, var(--bf-theme-text-1, #0f172a));
9
- --bf-tag-padding-y: var(--bf-theme-space-2, 0.6rem);
10
- --bf-tag-padding-x: var(--bf-theme-space-3, 0.9rem);
9
+ --bf-tag-padding-y: 0.35rem;
10
+ --bf-tag-padding-x: 0.55rem;
11
11
  --bf-tag-transition:
12
12
  var(--bf-theme-transition-bg, background-color 120ms ease),
13
13
  var(--bf-theme-transition-color, color 120ms ease),
14
14
  var(--bf-theme-transition-border, border-color 120ms ease);
15
15
 
16
- display: block;
16
+ display: inline-block;
17
17
  font: var(--bf-tag-font);
18
18
  color: var(--bf-tag-color);
19
19
  }
20
20
 
21
21
  .root {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ gap: 0.3rem;
26
+ line-height: 1;
27
+ font-size: 0.82rem;
28
+ font-weight: 600;
22
29
  background: var(--bf-tag-bg);
23
30
  color: var(--bf-tag-color);
24
31
  border-width: var(--bf-tag-border-width);
@@ -28,3 +35,29 @@
28
35
  padding: var(--bf-tag-padding-y) var(--bf-tag-padding-x);
29
36
  transition: var(--bf-tag-transition);
30
37
  }
38
+
39
+ .root[data-size='sm'] {
40
+ font-size: 0.74rem;
41
+ padding: 0.22rem 0.42rem;
42
+ }
43
+
44
+ .root[data-size='lg'] {
45
+ font-size: 0.9rem;
46
+ padding: 0.45rem 0.68rem;
47
+ }
48
+
49
+ .root[data-variant='badge'] {
50
+ font-size: 0.72rem;
51
+ font-weight: 700;
52
+ min-width: 1.35rem;
53
+ padding: 0.14rem 0.42rem;
54
+ border-radius: 999px;
55
+ }
56
+
57
+ .root[data-variant='pill'] {
58
+ border-radius: 999px;
59
+ }
60
+
61
+ .root[data-variant='chip'] {
62
+ padding-right: 0.75rem;
63
+ }