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,40 @@
1
1
  class BfInput extends HTMLElement {
2
+ static observedAttributes = [
3
+ 'type',
4
+ 'value',
5
+ 'placeholder',
6
+ 'disabled',
7
+ 'required',
8
+ 'readonly',
9
+ 'name',
10
+ 'label',
11
+ 'min',
12
+ 'max',
13
+ 'step',
14
+ 'swatch',
15
+ 'swatch-hidden',
16
+ 'left',
17
+ 'right',
18
+ 'format',
19
+ 'hex',
20
+ 'rgba',
21
+ ];
22
+
2
23
  constructor() {
3
24
  super();
4
25
  this.attachShadow({ mode: 'open' });
26
+ this._onInput = this._onInput.bind(this);
27
+ this._onChange = this._onChange.bind(this);
28
+ this._onFormatChange = this._onFormatChange.bind(this);
29
+ this._onPickerInput = this._onPickerInput.bind(this);
30
+ this._onSwatchClick = this._onSwatchClick.bind(this);
31
+ this._color = { r: 0, g: 0, b: 0, a: 1 };
32
+ this._colorFormat = 'hex';
5
33
  }
6
34
 
7
35
  connectedCallback() {
8
36
  if (this._initialized) {
37
+ this._syncState();
9
38
  return;
10
39
  }
11
40
  this._initialized = true;
@@ -18,13 +47,349 @@ class BfInput extends HTMLElement {
18
47
  const root = document.createElement('div');
19
48
  root.className = 'root';
20
49
  root.setAttribute('part', 'root');
21
- root.innerHTML = '<slot></slot>';
50
+ root.innerHTML = `
51
+ <button class="swatch" part="swatch" type="button" aria-label="Pick color"></button>
52
+ <input class="field" part="input" />
53
+ <select class="format" part="format" aria-label="Color format">
54
+ <option value="hex">HEX</option>
55
+ <option value="rgba">RGBA</option>
56
+ </select>
57
+ <input class="picker" part="picker" type="color" tabindex="-1" aria-hidden="true" />
58
+ `;
59
+
60
+ this.shadowRoot.replaceChildren(link, root);
61
+ this._root = root;
62
+ this._field = root.querySelector('.field');
63
+ this._swatch = root.querySelector('.swatch');
64
+ this._format = root.querySelector('.format');
65
+ this._picker = root.querySelector('.picker');
22
66
 
23
- if (!this.innerHTML.trim()) {
24
- root.textContent = 'input';
67
+ this._field.addEventListener('input', this._onInput);
68
+ this._field.addEventListener('change', this._onChange);
69
+ this._format.addEventListener('change', this._onFormatChange);
70
+ this._picker.addEventListener('input', this._onPickerInput);
71
+ this._swatch.addEventListener('click', this._onSwatchClick);
72
+
73
+ if (!this.hasAttribute('value')) {
74
+ const fallbackValue = this.textContent?.trim();
75
+ if (fallbackValue) {
76
+ this.setAttribute('value', fallbackValue);
77
+ }
25
78
  }
26
79
 
27
- this.shadowRoot.replaceChildren(link, root);
80
+ this._syncState();
81
+ }
82
+
83
+ attributeChangedCallback(name) {
84
+ if (name === 'value' && this._reflectingValue) {
85
+ return;
86
+ }
87
+ this._syncState();
88
+ }
89
+
90
+ get value() {
91
+ return this._field?.value || '';
92
+ }
93
+
94
+ set value(nextValue) {
95
+ this.setAttribute('value', `${nextValue ?? ''}`);
96
+ }
97
+
98
+ _reflectValueAttribute() {
99
+ const next = this._field.value;
100
+ if (this.getAttribute('value') === next) {
101
+ return;
102
+ }
103
+ this._reflectingValue = true;
104
+ this.setAttribute('value', next);
105
+ this._reflectingValue = false;
106
+ }
107
+
108
+ _onInput() {
109
+ if (this._isColorType()) {
110
+ this._tryConsumeColorString(this._field.value);
111
+ }
112
+ this._reflectValueAttribute();
113
+ this.dispatchEvent(
114
+ new CustomEvent('bf-input', {
115
+ bubbles: true,
116
+ composed: true,
117
+ detail: {
118
+ value: this._field.value,
119
+ },
120
+ }),
121
+ );
122
+ }
123
+
124
+ _onChange() {
125
+ if (this._isColorType()) {
126
+ if (!this._tryConsumeColorString(this._field.value)) {
127
+ this._field.value = this._serializeColor(this._colorFormat, this._color);
128
+ }
129
+ this._renderColorUi();
130
+ }
131
+ this._reflectValueAttribute();
132
+ this.dispatchEvent(
133
+ new CustomEvent('bf-change', {
134
+ bubbles: true,
135
+ composed: true,
136
+ detail: {
137
+ value: this._field.value,
138
+ },
139
+ }),
140
+ );
141
+ }
142
+
143
+ _onFormatChange() {
144
+ if (!this._isColorType()) {
145
+ return;
146
+ }
147
+ this._colorFormat = this._format.value;
148
+ this._field.value = this._serializeColor(this._colorFormat, this._color);
149
+ this._reflectValueAttribute();
150
+ }
151
+
152
+ _onPickerInput() {
153
+ if (!this._isColorType()) {
154
+ return;
155
+ }
156
+ const parsed = this._parseHex(this._picker.value);
157
+ if (!parsed) {
158
+ return;
159
+ }
160
+ this._color = { ...parsed, a: this._color.a };
161
+ this._field.value = this._serializeColor(this._colorFormat, this._color);
162
+ this._renderColorUi();
163
+ this._reflectValueAttribute();
164
+ this.dispatchEvent(
165
+ new CustomEvent('bf-input', {
166
+ bubbles: true,
167
+ composed: true,
168
+ detail: {
169
+ value: this._field.value,
170
+ },
171
+ }),
172
+ );
173
+ }
174
+
175
+ _onSwatchClick() {
176
+ if (this._isColorType() && !this.hasAttribute('disabled')) {
177
+ this._picker.click();
178
+ }
179
+ }
180
+
181
+ _isColorType() {
182
+ return (this.getAttribute('type') || 'text').toLowerCase() === 'color';
183
+ }
184
+
185
+ _allowedColorFormats() {
186
+ const hasHex = this.hasAttribute('hex');
187
+ const hasRgba = this.hasAttribute('rgba');
188
+ if (hasHex && hasRgba) {
189
+ return ['hex', 'rgba'];
190
+ }
191
+ if (hasHex) {
192
+ return ['hex'];
193
+ }
194
+ if (hasRgba) {
195
+ return ['rgba'];
196
+ }
197
+ const explicit = (this.getAttribute('format') || '').toLowerCase();
198
+ if (explicit === 'hex') {
199
+ return ['hex'];
200
+ }
201
+ if (explicit === 'rgba') {
202
+ return ['rgba'];
203
+ }
204
+ return ['hex', 'rgba'];
205
+ }
206
+
207
+ _resolveColorFormat(allowed) {
208
+ const explicit = (this.getAttribute('format') || '').toLowerCase();
209
+ if (allowed.includes(explicit)) {
210
+ return explicit;
211
+ }
212
+ if (allowed.includes(this._colorFormat)) {
213
+ return this._colorFormat;
214
+ }
215
+ return allowed[0] || 'hex';
216
+ }
217
+
218
+ _resolveSwatchPosition() {
219
+ const explicit = (this.getAttribute('swatch') || '').toLowerCase();
220
+ if (explicit === 'left' || explicit === 'right' || explicit === 'hidden') {
221
+ return explicit;
222
+ }
223
+ if (this.hasAttribute('swatch-hidden')) {
224
+ return 'hidden';
225
+ }
226
+ if (this.hasAttribute('right')) {
227
+ return 'right';
228
+ }
229
+ if (this.hasAttribute('left')) {
230
+ return 'left';
231
+ }
232
+ return 'left';
233
+ }
234
+
235
+ _normalizeHex(hexValue) {
236
+ return this._serializeColor('hex', this._parseHex(hexValue) || this._color);
237
+ }
238
+
239
+ _renderColorUi() {
240
+ const swatchPosition = this._resolveSwatchPosition();
241
+ const allowedFormats = this._allowedColorFormats();
242
+ this._colorFormat = this._resolveColorFormat(allowedFormats);
243
+
244
+ this._root.classList.add('is-color');
245
+ this._root.classList.toggle('swatch-right', swatchPosition === 'right');
246
+ this._root.classList.toggle('swatch-hidden', swatchPosition === 'hidden');
247
+ this._format.innerHTML = allowedFormats
248
+ .map((value) => `<option value="${value}">${value.toUpperCase()}</option>`)
249
+ .join('');
250
+ this._format.value = this._colorFormat;
251
+ this._format.hidden = allowedFormats.length <= 1;
252
+ this._picker.value = this._normalizeHex(this._serializeColor('hex', this._color));
253
+ this._swatch.style.background = this._serializeColor('rgba', this._color);
254
+ }
255
+
256
+ _renderDefaultUi() {
257
+ this._root.classList.remove('is-color', 'swatch-right', 'swatch-hidden');
258
+ }
259
+
260
+ _clampChannel(value) {
261
+ return Math.min(255, Math.max(0, value));
262
+ }
263
+
264
+ _clampAlpha(value) {
265
+ return Math.min(1, Math.max(0, value));
266
+ }
267
+
268
+ _parseHex(value) {
269
+ const raw = `${value || ''}`.trim().replace(/^#/, '');
270
+ if (!raw) {
271
+ return null;
272
+ }
273
+ if (raw.length === 3 || raw.length === 4) {
274
+ const r = Number.parseInt(raw[0] + raw[0], 16);
275
+ const g = Number.parseInt(raw[1] + raw[1], 16);
276
+ const b = Number.parseInt(raw[2] + raw[2], 16);
277
+ const a = raw.length === 4 ? Number.parseInt(raw[3] + raw[3], 16) / 255 : 1;
278
+ if ([r, g, b, a].some((v) => Number.isNaN(v))) {
279
+ return null;
280
+ }
281
+ return { r, g, b, a };
282
+ }
283
+ if (raw.length === 6 || raw.length === 8) {
284
+ const r = Number.parseInt(raw.slice(0, 2), 16);
285
+ const g = Number.parseInt(raw.slice(2, 4), 16);
286
+ const b = Number.parseInt(raw.slice(4, 6), 16);
287
+ const a = raw.length === 8 ? Number.parseInt(raw.slice(6, 8), 16) / 255 : 1;
288
+ if ([r, g, b, a].some((v) => Number.isNaN(v))) {
289
+ return null;
290
+ }
291
+ return { r, g, b, a };
292
+ }
293
+ return null;
294
+ }
295
+
296
+ _parseRgba(value) {
297
+ const match = `${value || ''}`
298
+ .trim()
299
+ .match(/^rgba?\(\s*([^\)]+)\s*\)$/i);
300
+ if (!match) {
301
+ return null;
302
+ }
303
+ const parts = match[1].split(',').map((part) => part.trim());
304
+ if (parts.length !== 3 && parts.length !== 4) {
305
+ return null;
306
+ }
307
+ const r = Number.parseFloat(parts[0]);
308
+ const g = Number.parseFloat(parts[1]);
309
+ const b = Number.parseFloat(parts[2]);
310
+ const a = parts.length === 4 ? Number.parseFloat(parts[3]) : 1;
311
+ if ([r, g, b, a].some((v) => Number.isNaN(v))) {
312
+ return null;
313
+ }
314
+ return {
315
+ r: this._clampChannel(r),
316
+ g: this._clampChannel(g),
317
+ b: this._clampChannel(b),
318
+ a: this._clampAlpha(a),
319
+ };
320
+ }
321
+
322
+ _tryConsumeColorString(value) {
323
+ const parsedHex = this._parseHex(value);
324
+ if (parsedHex) {
325
+ this._color = parsedHex;
326
+ this._renderColorUi();
327
+ return true;
328
+ }
329
+ const parsedRgba = this._parseRgba(value);
330
+ if (parsedRgba) {
331
+ this._color = parsedRgba;
332
+ this._renderColorUi();
333
+ return true;
334
+ }
335
+ return false;
336
+ }
337
+
338
+ _toHexChannel(value) {
339
+ return Math.round(this._clampChannel(value))
340
+ .toString(16)
341
+ .padStart(2, '0');
342
+ }
343
+
344
+ _toAlphaString(value) {
345
+ const rounded = Math.round(this._clampAlpha(value) * 1000) / 1000;
346
+ return `${rounded}`;
347
+ }
348
+
349
+ _serializeColor(format, color) {
350
+ if (format === 'rgba') {
351
+ return `rgba(${Math.round(color.r)}, ${Math.round(color.g)}, ${Math.round(
352
+ color.b,
353
+ )}, ${this._toAlphaString(color.a)})`;
354
+ }
355
+ return `#${this._toHexChannel(color.r)}${this._toHexChannel(color.g)}${this._toHexChannel(color.b)}`;
356
+ }
357
+
358
+ _syncState() {
359
+ if (!this._field || !this._root) {
360
+ return;
361
+ }
362
+
363
+ const type = (this.getAttribute('type') || 'text').toLowerCase();
364
+ const value = this.getAttribute('value') || '';
365
+ this._field.type = type === 'color' ? 'text' : type;
366
+ this._field.value = value;
367
+ this._field.placeholder = this.getAttribute('placeholder') || '';
368
+ this._field.disabled = this.hasAttribute('disabled');
369
+ this._field.required = this.hasAttribute('required');
370
+ this._field.readOnly = this.hasAttribute('readonly');
371
+ this._field.name = this.getAttribute('name') || '';
372
+ this._field.min = this.getAttribute('min') || '';
373
+ this._field.max = this.getAttribute('max') || '';
374
+ this._field.step = this.getAttribute('step') || '';
375
+ if (this.getAttribute('label')) {
376
+ this._field.setAttribute('aria-label', this.getAttribute('label'));
377
+ } else {
378
+ this._field.removeAttribute('aria-label');
379
+ }
380
+
381
+ if (type === 'color') {
382
+ if (!this._tryConsumeColorString(value)) {
383
+ if (!value) {
384
+ this._color = { r: 0, g: 0, b: 0, a: 1 };
385
+ }
386
+ }
387
+ this._renderColorUi();
388
+ this._field.value = this._serializeColor(this._colorFormat, this._color);
389
+ this._reflectValueAttribute();
390
+ } else {
391
+ this._renderDefaultUi();
392
+ }
28
393
  }
29
394
  }
30
395
 
@@ -28,3 +28,14 @@
28
28
  padding: var(--bf-list-padding-y) var(--bf-list-padding-x);
29
29
  transition: var(--bf-list-transition);
30
30
  }
31
+
32
+ .root ::slotted([item]) {
33
+ display: block;
34
+ padding: 0.45rem 0.6rem;
35
+ border-radius: 0.4rem;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .root ::slotted([item]:hover) {
40
+ background: var(--bf-theme-surface-2, #f8fafc);
41
+ }
@@ -2,6 +2,7 @@ class BfList extends HTMLElement {
2
2
  constructor() {
3
3
  super();
4
4
  this.attachShadow({ mode: 'open' });
5
+ this._onSlotChange = this._onSlotChange.bind(this);
5
6
  }
6
7
 
7
8
  connectedCallback() {
@@ -25,6 +26,50 @@ class BfList extends HTMLElement {
25
26
  }
26
27
 
27
28
  this.shadowRoot.replaceChildren(link, root);
29
+ this._slot = root.querySelector('slot');
30
+ this._slot.addEventListener('slotchange', this._onSlotChange);
31
+ this._onSlotChange();
32
+ }
33
+
34
+ _onSlotChange() {
35
+ if (!this._slot) {
36
+ return;
37
+ }
38
+ const items = this._slot
39
+ .assignedElements({ flatten: true })
40
+ .filter((element) => element.hasAttribute('item'));
41
+
42
+ for (const [index, item] of items.entries()) {
43
+ item.setAttribute('role', item.getAttribute('role') || 'listitem');
44
+ if (!item.hasAttribute('tabindex')) {
45
+ item.setAttribute('tabindex', '0');
46
+ }
47
+ if (item.dataset.bfListBound === 'true') {
48
+ continue;
49
+ }
50
+ item.dataset.bfListBound = 'true';
51
+ item.addEventListener('click', () => this._emitSelect(item, index));
52
+ item.addEventListener('keydown', (event) => {
53
+ if (event.key === 'Enter' || event.key === ' ') {
54
+ event.preventDefault();
55
+ this._emitSelect(item, index);
56
+ }
57
+ });
58
+ }
59
+ }
60
+
61
+ _emitSelect(item, index) {
62
+ this.dispatchEvent(
63
+ new CustomEvent('bf-select', {
64
+ bubbles: true,
65
+ composed: true,
66
+ detail: {
67
+ index,
68
+ id: item.id || '',
69
+ value: item.getAttribute('value') || item.textContent?.trim() || '',
70
+ },
71
+ }),
72
+ );
28
73
  }
29
74
  }
30
75
 
@@ -28,3 +28,23 @@
28
28
  padding: var(--bf-menu-padding-y) var(--bf-menu-padding-x);
29
29
  transition: var(--bf-menu-transition);
30
30
  }
31
+
32
+ .root[data-variant='context'][data-open='false'] {
33
+ display: none;
34
+ }
35
+
36
+ .root[data-variant='context'] {
37
+ min-width: 11rem;
38
+ box-shadow: 0 14px 36px rgba(2, 6, 23, 0.2);
39
+ }
40
+
41
+ .root ::slotted([item]) {
42
+ display: block;
43
+ padding: 0.45rem 0.6rem;
44
+ border-radius: 0.4rem;
45
+ cursor: pointer;
46
+ }
47
+
48
+ .root ::slotted([item]:hover) {
49
+ background: var(--bf-theme-surface-2, #f8fafc);
50
+ }
@@ -1,11 +1,17 @@
1
1
  class BfMenu extends HTMLElement {
2
+ static observedAttributes = ['variant', 'context', 'open', 'x', 'y', 'for'];
3
+
2
4
  constructor() {
3
5
  super();
4
6
  this.attachShadow({ mode: 'open' });
7
+ this._onSlotChange = this._onSlotChange.bind(this);
8
+ this._onDocumentClick = this._onDocumentClick.bind(this);
5
9
  }
6
10
 
7
11
  connectedCallback() {
8
12
  if (this._initialized) {
13
+ this._sync();
14
+ this._onSlotChange();
9
15
  return;
10
16
  }
11
17
  this._initialized = true;
@@ -25,6 +31,144 @@ class BfMenu extends HTMLElement {
25
31
  }
26
32
 
27
33
  this.shadowRoot.replaceChildren(link, root);
34
+ this._root = root;
35
+ this._slot = root.querySelector('slot');
36
+ this._slot.addEventListener('slotchange', this._onSlotChange);
37
+ this._bindContextTarget();
38
+ document.addEventListener('click', this._onDocumentClick);
39
+ this._onSlotChange();
40
+ this._sync();
41
+ }
42
+
43
+ disconnectedCallback() {
44
+ document.removeEventListener('click', this._onDocumentClick);
45
+ if (this._contextTarget) {
46
+ this._contextTarget.removeEventListener('contextmenu', this._boundContextMenuHandler);
47
+ }
48
+ }
49
+
50
+ attributeChangedCallback(name) {
51
+ if (name === 'for') {
52
+ this._bindContextTarget();
53
+ }
54
+ this._sync();
55
+ }
56
+
57
+ open() {
58
+ this.setAttribute('open', '');
59
+ }
60
+
61
+ close() {
62
+ this.removeAttribute('open');
63
+ }
64
+
65
+ toggle() {
66
+ if (this.hasAttribute('open')) {
67
+ this.close();
68
+ return;
69
+ }
70
+ this.open();
71
+ }
72
+
73
+ _isContextMode() {
74
+ const variant = (this.getAttribute('variant') || '').toLowerCase();
75
+ return variant === 'context' || this.hasAttribute('context');
76
+ }
77
+
78
+ _bindContextTarget() {
79
+ if (!this._isContextMode()) {
80
+ return;
81
+ }
82
+ if (this._contextTarget && this._boundContextMenuHandler) {
83
+ this._contextTarget.removeEventListener('contextmenu', this._boundContextMenuHandler);
84
+ }
85
+ const targetId = this.getAttribute('for');
86
+ this._contextTarget = targetId ? document.getElementById(targetId) : this.parentElement;
87
+ if (!this._contextTarget) {
88
+ return;
89
+ }
90
+ this._boundContextMenuHandler = (event) => {
91
+ event.preventDefault();
92
+ this.setAttribute('x', `${event.clientX}`);
93
+ this.setAttribute('y', `${event.clientY}`);
94
+ this.open();
95
+ };
96
+ this._contextTarget.addEventListener('contextmenu', this._boundContextMenuHandler);
97
+ }
98
+
99
+ _onDocumentClick(event) {
100
+ if (!this._isContextMode() || !this.hasAttribute('open')) {
101
+ return;
102
+ }
103
+ const path = event.composedPath();
104
+ if (path.includes(this)) {
105
+ return;
106
+ }
107
+ this.close();
108
+ }
109
+
110
+ _onSlotChange() {
111
+ if (!this._slot) {
112
+ return;
113
+ }
114
+ const items = this._slot
115
+ .assignedElements({ flatten: true })
116
+ .filter((element) => element.hasAttribute('item'));
117
+
118
+ for (const [index, item] of items.entries()) {
119
+ item.setAttribute('role', item.getAttribute('role') || 'menuitem');
120
+ if (!item.hasAttribute('tabindex')) {
121
+ item.setAttribute('tabindex', '0');
122
+ }
123
+ if (item.dataset.bfMenuBound === 'true') {
124
+ continue;
125
+ }
126
+ item.dataset.bfMenuBound = 'true';
127
+ item.addEventListener('click', () => this._emitSelect(item, index));
128
+ item.addEventListener('keydown', (event) => {
129
+ if (event.key === 'Enter' || event.key === ' ') {
130
+ event.preventDefault();
131
+ this._emitSelect(item, index);
132
+ }
133
+ });
134
+ }
135
+ }
136
+
137
+ _emitSelect(item, index) {
138
+ this.dispatchEvent(
139
+ new CustomEvent('bf-select', {
140
+ bubbles: true,
141
+ composed: true,
142
+ detail: {
143
+ index,
144
+ id: item.id || '',
145
+ value: item.getAttribute('value') || item.textContent?.trim() || '',
146
+ },
147
+ }),
148
+ );
149
+ if (this._isContextMode()) {
150
+ this.close();
151
+ }
152
+ }
153
+
154
+ _sync() {
155
+ if (!this._root) {
156
+ return;
157
+ }
158
+ const context = this._isContextMode();
159
+ this._root.setAttribute('data-variant', context ? 'context' : 'menu');
160
+ this._root.setAttribute('data-open', this.hasAttribute('open') ? 'true' : 'false');
161
+ if (context) {
162
+ this.style.position = 'fixed';
163
+ this.style.left = `${Number.parseFloat(this.getAttribute('x') || '0') || 0}px`;
164
+ this.style.top = `${Number.parseFloat(this.getAttribute('y') || '0') || 0}px`;
165
+ this.style.zIndex = '1300';
166
+ } else {
167
+ this.style.position = '';
168
+ this.style.left = '';
169
+ this.style.top = '';
170
+ this.style.zIndex = '';
171
+ }
28
172
  }
29
173
  }
30
174
 
@@ -1,30 +1,43 @@
1
1
  :host {
2
2
  --bf-modal-font: var(--bf-theme-font-family, inherit);
3
- --bf-modal-radius: var(--bf-theme-radius-md, 8px);
4
- --bf-modal-border-width: var(--bf-theme-border-width, 1px);
5
- --bf-modal-border-style: var(--bf-theme-border-style, solid);
6
- --bf-modal-border-color: var(--bf-theme-modal-border-color, var(--bf-theme-border-1, #cbd5e1));
7
3
  --bf-modal-bg: var(--bf-theme-modal-bg, var(--bf-theme-surface-1, #ffffff));
8
4
  --bf-modal-color: var(--bf-theme-modal-color, var(--bf-theme-text-1, #0f172a));
9
- --bf-modal-padding-y: var(--bf-theme-space-2, 0.6rem);
10
- --bf-modal-padding-x: var(--bf-theme-space-3, 0.9rem);
11
- --bf-modal-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);
15
-
5
+ --bf-modal-border-color: var(--bf-theme-modal-border-color, var(--bf-theme-border-1, #cbd5e1));
6
+ --bf-modal-radius: var(--bf-theme-radius-md, 10px);
7
+ --bf-modal-backdrop: rgba(2, 6, 23, 0.48);
8
+ --bf-modal-max-w: 34rem;
9
+ --bf-modal-shadow: 0 18px 44px rgba(2, 6, 23, 0.28);
16
10
  display: block;
17
11
  font: var(--bf-modal-font);
18
- color: var(--bf-modal-color);
12
+ z-index: 1200;
19
13
  }
20
14
 
21
15
  .root {
16
+ position: fixed;
17
+ inset: 0;
18
+ display: grid;
19
+ align-items: center;
20
+ justify-items: center;
21
+ padding: 1rem;
22
+ }
23
+
24
+ .root[data-open='false'] {
25
+ display: none;
26
+ }
27
+
28
+ .backdrop {
29
+ position: absolute;
30
+ inset: 0;
31
+ background: var(--bf-modal-backdrop);
32
+ }
33
+
34
+ .panel {
35
+ position: relative;
36
+ width: min(100%, var(--bf-modal-max-w));
22
37
  background: var(--bf-modal-bg);
23
38
  color: var(--bf-modal-color);
24
- border-width: var(--bf-modal-border-width);
25
- border-style: var(--bf-modal-border-style);
26
- border-color: var(--bf-modal-border-color);
39
+ border: 1px solid var(--bf-modal-border-color);
27
40
  border-radius: var(--bf-modal-radius);
28
- padding: var(--bf-modal-padding-y) var(--bf-modal-padding-x);
29
- transition: var(--bf-modal-transition);
41
+ padding: 1rem;
42
+ box-shadow: var(--bf-modal-shadow);
30
43
  }