@jungherz-de/glasskit-elements 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/components/glk-accordion-item.js +1 -1
  3. package/dist/components/glk-accordion.js +1 -1
  4. package/dist/components/glk-avatar.js +1 -1
  5. package/dist/components/glk-badge.js +1 -1
  6. package/dist/components/glk-button.js +1 -1
  7. package/dist/components/glk-card.js +1 -1
  8. package/dist/components/glk-checkbox.js +1 -1
  9. package/dist/components/glk-divider.js +1 -1
  10. package/dist/components/glk-input.js +1 -1
  11. package/dist/components/glk-list-item.js +1 -1
  12. package/dist/components/glk-list.js +1 -1
  13. package/dist/components/glk-modal.js +19 -12
  14. package/dist/components/glk-nav.js +1 -1
  15. package/dist/components/glk-pill.js +1 -1
  16. package/dist/components/glk-popover.js +1 -1
  17. package/dist/components/glk-progress.js +1 -1
  18. package/dist/components/glk-radio.js +1 -1
  19. package/dist/components/glk-range.js +1 -1
  20. package/dist/components/glk-search.js +1 -1
  21. package/dist/components/glk-select.js +39 -8
  22. package/dist/components/glk-status.js +1 -1
  23. package/dist/components/glk-tab-accessory.js +1 -1
  24. package/dist/components/glk-tab-bar.js +1 -1
  25. package/dist/components/glk-tab-dock.js +1 -1
  26. package/dist/components/glk-tab-item.js +26 -15
  27. package/dist/components/glk-textarea.js +1 -1
  28. package/dist/components/glk-title.js +1 -1
  29. package/dist/components/glk-toast.js +1 -1
  30. package/dist/components/glk-toggle.js +1 -1
  31. package/dist/components/shared/{base-C0B1hgOt.js → base-BoN33KPe.js} +36 -3
  32. package/dist/glasskit-elements.esm.js +117 -35
  33. package/dist/glasskit-elements.js +117 -35
  34. package/dist/glasskit-elements.min.js +1 -1
  35. package/package.json +1 -1
  36. package/src/base.js +36 -3
  37. package/src/components/feedback/glk-modal.js +18 -11
  38. package/src/components/forms/glk-select.js +38 -7
  39. package/src/components/navigation/glk-tab-item.js +25 -14
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-44cc11?style=flat-square" alt="Vanilla JS"></a>
6
6
  <a href="#"><img src="https://img.shields.io/badge/components-29-7ec8e3?style=flat-square" alt="29 Components"></a>
7
7
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License"></a>
8
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.11.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.12.0-lightgrey?style=flat-square" alt="Changelog"></a>
9
9
  <a href="https://www.npmjs.com/package/@jungherz-de/glasskit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fglasskit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
10
10
  </p>
11
11
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const CHEVRON_SVG = `<span class="glass-accordion__trigger-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="6 9 12 15 18 9"/></svg></span>`;
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkAccordion extends GlkElement {
4
4
  render() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const SIZES = ['sm', 'lg'];
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const VARIANTS = ['primary', 'success', 'error'];
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const VARIANTS = ['primary', 'secondary', 'tertiary'];
4
4
  const SIZES = ['sm', 'md', 'lg', 'auto'];
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  // <glk-card fill> stretches the visible card to the height of its grid or flex
4
4
  // cell. The host must become a grid for this: it is stretched by the outer
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const CHECKMARK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkDivider extends GlkElement {
4
4
  render() {
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkInput extends GlkFormElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  // Module-level override sheet — shared by all instances.
4
4
  // Suppresses the auto-divider when glk-list marks this item as the
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkList extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,10 +1,12 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkModal extends GlkElement {
4
4
  static get observedAttributes() {
5
5
  return ['open', 'title'];
6
6
  }
7
7
 
8
+ static get observesLightDom() { return true; }
9
+
8
10
  render() {
9
11
  this._overlay = this.createElement('div', ['glass-modal-overlay'], { part: 'overlay' });
10
12
 
@@ -28,7 +30,7 @@ class GlkModal extends GlkElement {
28
30
  modal.appendChild(this._footer);
29
31
 
30
32
  // Defer footer population — children may not be parsed yet
31
- requestAnimationFrame(() => this._populateFooter());
33
+ requestAnimationFrame(() => this.projectLightDom());
32
34
 
33
35
  this._overlay.appendChild(modal);
34
36
  this._wrapper.appendChild(this._overlay);
@@ -38,18 +40,23 @@ class GlkModal extends GlkElement {
38
40
  }
39
41
  }
40
42
 
41
- _populateFooter() {
43
+ projectLightDom() {
44
+ const buttons = [...this.querySelectorAll('[slot="actions"] button')];
45
+ const signature = buttons.map(b => b.outerHTML).join('');
46
+ if (signature === this._footerSignature) return;
47
+ this._footerSignature = signature;
48
+
42
49
  this._footer.innerHTML = '';
43
- const actionsSlot = this.querySelector('[slot="actions"]');
44
- if (actionsSlot) {
45
- const buttons = actionsSlot.querySelectorAll('button');
46
- buttons.forEach(btn => {
47
- const clone = btn.cloneNode(true);
48
- // Forward click events to the original button
49
- clone.addEventListener('click', () => btn.click());
50
- this._footer.appendChild(clone);
50
+ buttons.forEach((btn, i) => {
51
+ const clone = btn.cloneNode(true);
52
+ // Look the original up again at click time instead of closing over it: a
53
+ // framework that re-renders replaces the node, and a captured reference
54
+ // would then forward the click to a button no longer in the document.
55
+ clone.addEventListener('click', () => {
56
+ this.querySelectorAll('[slot="actions"] button')[i]?.click();
51
57
  });
52
- }
58
+ this._footer.appendChild(clone);
59
+ });
53
60
  }
54
61
 
55
62
  setupEvents() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkNav extends GlkElement {
4
4
  render() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkPill extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const PLACEMENTS = ['top', 'bottom', 'start', 'end'];
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const VARIANTS = ['success', 'error'];
4
4
  const SIZES = ['sm', 'lg'];
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  // ── Grouping ──
4
4
  // Every <glk-radio> keeps its <input type="radio"> in its own shadow root, and
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkRange extends GlkFormElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const SEARCH_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
4
4
 
@@ -1,10 +1,12 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkSelect extends GlkFormElement {
4
4
  static get observedAttributes() {
5
5
  return ['label', 'disabled', 'name', 'value', 'required'];
6
6
  }
7
7
 
8
+ static get observesLightDom() { return true; }
9
+
8
10
  render() {
9
11
  const group = this.createElement('div', ['glass-input-group']);
10
12
 
@@ -25,12 +27,29 @@ class GlkSelect extends GlkFormElement {
25
27
  this._wrapper.appendChild(group);
26
28
 
27
29
  // Defer option copying — children may not be parsed yet in connectedCallback
28
- requestAnimationFrame(() => {
29
- this._moveOptions();
30
- const value = this.getAttribute('value');
31
- if (value) this._select.value = value;
32
- this._syncFormValue();
33
- });
30
+ requestAnimationFrame(() => this.projectLightDom());
31
+ }
32
+
33
+ projectLightDom() {
34
+ // The options are pure data — the clones carry no listeners — so skipping an
35
+ // unchanged rebuild is safe, and it keeps an open dropdown from snapping shut
36
+ // on light-DOM churn elsewhere.
37
+ const signature = [...this.querySelectorAll('option')].map(o => o.outerHTML).join('');
38
+ if (signature === this._optionSignature) return;
39
+ this._optionSignature = signature;
40
+
41
+ // innerHTML = '' below drops the selection, so remember it first. A
42
+ // selectedIndex of -1 means "nothing is selected", which is not the same as
43
+ // an option whose value happens to be the empty string.
44
+ const previous = this._select.selectedIndex >= 0 ? this._select.value : null;
45
+
46
+ this._moveOptions();
47
+
48
+ // Keep the live selection when it survived the rebuild; otherwise fall back
49
+ // to the value attribute. Without this the selection jumps back to the first
50
+ // entry every time the list is updated.
51
+ if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
52
+ this._syncFormValue();
34
53
  }
35
54
 
36
55
  _moveOptions() {
@@ -41,6 +60,18 @@ class GlkSelect extends GlkFormElement {
41
60
  });
42
61
  }
43
62
 
63
+ /**
64
+ * Selects `value` if an option carries it, and reports whether it did. The
65
+ * empty string is a value like any other — "" is a real option in plenty of
66
+ * forms ("detect automatically", "enter your own below").
67
+ */
68
+ _applyValue(value) {
69
+ if (value === null) return false;
70
+ if (![...this._select.options].some(o => o.value === value)) return false;
71
+ this._select.value = value;
72
+ return true;
73
+ }
74
+
44
75
  setupEvents() {
45
76
  this._onChange = () => {
46
77
  this._syncFormValue();
@@ -67,7 +98,7 @@ class GlkSelect extends GlkFormElement {
67
98
  this._select.setAttribute('name', this.getAttribute('name') || '');
68
99
  break;
69
100
  case 'value':
70
- this._select.value = this.getAttribute('value') || '';
101
+ this._applyValue(this.getAttribute('value'));
71
102
  this._syncFormValue();
72
103
  break;
73
104
  }
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkStatus extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const VARIANTS = ['accent', 'success', 'error'];
4
4
 
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkTabBar extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkTabDock extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,10 +1,12 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkTabItem extends GlkElement {
4
4
  static get observedAttributes() {
5
5
  return ['label', 'active', 'badge'];
6
6
  }
7
7
 
8
+ static get observesLightDom() { return true; }
9
+
8
10
  render() {
9
11
  this._btn = this.createElement('button', ['glass-tab-bar__item']);
10
12
  if (this.getBoolAttr('active')) this._btn.classList.add('is-active');
@@ -30,23 +32,32 @@ class GlkTabItem extends GlkElement {
30
32
  this._wrapper.appendChild(this._btn);
31
33
 
32
34
  // Defer icon cloning — children may not be parsed yet
33
- requestAnimationFrame(() => this._cloneIcon());
35
+ requestAnimationFrame(() => this.projectLightDom());
34
36
  }
35
37
 
36
- _cloneIcon() {
38
+ projectLightDom() {
37
39
  const svg = this.querySelector('svg');
38
- if (svg) {
39
- const clone = svg.cloneNode(true);
40
- // Remove inline attributes that would override GlassKit styles
41
- clone.removeAttribute('width');
42
- clone.removeAttribute('height');
43
- clone.removeAttribute('stroke');
44
- clone.removeAttribute('stroke-width');
45
- clone.removeAttribute('stroke-linecap');
46
- clone.removeAttribute('stroke-linejoin');
47
- clone.removeAttribute('fill');
48
- this._iconEl.insertBefore(clone, this._iconEl.firstChild);
49
- }
40
+ const signature = svg ? svg.outerHTML : '';
41
+ if (signature === this._iconSignature) return;
42
+ this._iconSignature = signature;
43
+
44
+ // Drop the previous clone by hand: the icon container also holds the badge,
45
+ // and inserting without removing would stack a second icon on every update.
46
+ this._iconClone?.remove();
47
+ this._iconClone = null;
48
+ if (!svg) return;
49
+
50
+ const clone = svg.cloneNode(true);
51
+ // Remove inline attributes that would override GlassKit styles
52
+ clone.removeAttribute('width');
53
+ clone.removeAttribute('height');
54
+ clone.removeAttribute('stroke');
55
+ clone.removeAttribute('stroke-width');
56
+ clone.removeAttribute('stroke-linecap');
57
+ clone.removeAttribute('stroke-linejoin');
58
+ clone.removeAttribute('fill');
59
+ this._iconEl.insertBefore(clone, this._iconEl.firstChild);
60
+ this._iconClone = clone;
50
61
  }
51
62
 
52
63
  setupEvents() {
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkTextarea extends GlkFormElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkTitle extends GlkElement {
4
4
  static get observedAttributes() {
@@ -1,4 +1,4 @@
1
- import { G as GlkElement } from './shared/base-C0B1hgOt.js';
1
+ import { G as GlkElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  const VARIANTS = ['success', 'error', 'warning'];
4
4
 
@@ -1,4 +1,4 @@
1
- import { a as GlkFormElement } from './shared/base-C0B1hgOt.js';
1
+ import { a as GlkFormElement } from './shared/base-BoN33KPe.js';
2
2
 
3
3
  class GlkToggle extends GlkFormElement {
4
4
  static get observedAttributes() {
@@ -104,6 +104,14 @@ class GlkElement extends HTMLElement {
104
104
  */
105
105
  static get hostStyles() { return null; }
106
106
 
107
+ /**
108
+ * Opt in when the component copies light-DOM children into its shadow tree.
109
+ * A MutationObserver then calls projectLightDom() again whenever those
110
+ * children change, so a framework that swaps them keeps the rendered element
111
+ * in step. Without it the copy is made once and silently goes stale.
112
+ */
113
+ static get observesLightDom() { return false; }
114
+
107
115
  static get observedAttributes() {
108
116
  return [];
109
117
  }
@@ -130,15 +138,26 @@ class GlkElement extends HTMLElement {
130
138
  this._shadow.appendChild(this._wrapper);
131
139
 
132
140
  this.render();
133
- this.setupEvents();
141
+ }
134
142
 
135
- // Register for theme sync
136
- instances.add(this);
143
+ // Everything below runs on every connect, not just the first. Moving an
144
+ // element in the DOM disconnects and reconnects it, and disconnectedCallback
145
+ // tears all of this down — without re-arming it here a moved element would
146
+ // keep its markup but silently stop reacting.
147
+ this.setupEvents();
148
+ instances.add(this);
149
+
150
+ if (this.constructor.observesLightDom) {
151
+ this._lightDomObserver ??= new MutationObserver(records => this.projectLightDom(records));
152
+ this._lightDomObserver.observe(this, {
153
+ childList: true, subtree: true, characterData: true
154
+ });
137
155
  }
138
156
  }
139
157
 
140
158
  disconnectedCallback() {
141
159
  instances.delete(this);
160
+ this._lightDomObserver?.disconnect();
142
161
  this.teardownEvents();
143
162
  }
144
163
 
@@ -166,6 +185,20 @@ class GlkElement extends HTMLElement {
166
185
  /** Subclasses override to react to attribute changes. */
167
186
  onAttributeChanged(name, oldValue, newValue) {}
168
187
 
188
+ /**
189
+ * Subclasses that set observesLightDom override this to (re-)copy their
190
+ * light-DOM children into the shadow tree. Runs on every change to those
191
+ * children, so it has to be safe to call repeatedly.
192
+ */
193
+ projectLightDom() {}
194
+
195
+ /**
196
+ * Escape hatch: re-copy the light-DOM children now. The observer covers the
197
+ * ordinary cases; this is for the ones it cannot see, so nobody has to reach
198
+ * into element.shadowRoot.
199
+ */
200
+ refresh() { this.projectLightDom(); }
201
+
169
202
  // ── Utility Methods ──
170
203
 
171
204
  getBoolAttr(name) {