@jungherz-de/glasskit-elements 1.13.0 → 1.15.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 (45) hide show
  1. package/README.md +43 -6
  2. package/dist/components/base.js +253 -0
  3. package/dist/components/glk-accordion-item.js +2 -1
  4. package/dist/components/glk-accordion.js +2 -1
  5. package/dist/components/glk-avatar.js +2 -1
  6. package/dist/components/glk-badge.js +2 -1
  7. package/dist/components/glk-button.js +2 -1
  8. package/dist/components/glk-card.js +2 -1
  9. package/dist/components/glk-checkbox.js +2 -1
  10. package/dist/components/glk-divider.js +2 -1
  11. package/dist/components/glk-empty.js +53 -0
  12. package/dist/components/glk-input.js +2 -1
  13. package/dist/components/glk-list-item.js +2 -1
  14. package/dist/components/glk-list.js +2 -1
  15. package/dist/components/glk-modal.js +2 -1
  16. package/dist/components/glk-nav.js +2 -1
  17. package/dist/components/glk-pill.js +2 -1
  18. package/dist/components/glk-popover.js +2 -1
  19. package/dist/components/glk-progress.js +2 -1
  20. package/dist/components/glk-radio.js +2 -1
  21. package/dist/components/glk-range.js +2 -1
  22. package/dist/components/glk-search.js +2 -1
  23. package/dist/components/glk-segmented.js +124 -0
  24. package/dist/components/glk-select.js +2 -1
  25. package/dist/components/glk-sheet.js +141 -0
  26. package/dist/components/glk-status.js +2 -1
  27. package/dist/components/glk-steps.js +75 -0
  28. package/dist/components/glk-tab-accessory.js +2 -1
  29. package/dist/components/glk-tab-bar.js +2 -1
  30. package/dist/components/glk-tab-dock.js +2 -1
  31. package/dist/components/glk-tab-item.js +2 -1
  32. package/dist/components/glk-textarea.js +2 -1
  33. package/dist/components/glk-title.js +2 -1
  34. package/dist/components/glk-toast.js +2 -1
  35. package/dist/components/glk-toggle.js +2 -1
  36. package/dist/glasskit-elements.esm.js +380 -3
  37. package/dist/glasskit-elements.js +385 -2
  38. package/dist/glasskit-elements.min.js +1 -1
  39. package/package.json +4 -3
  40. package/src/components/content/glk-empty.js +51 -0
  41. package/src/components/feedback/glk-sheet.js +139 -0
  42. package/src/components/forms/glk-segmented.js +122 -0
  43. package/src/components/navigation/glk-steps.js +73 -0
  44. package/src/index.js +10 -0
  45. package/dist/components/shared/base-ClsXgLON.js +0 -273
@@ -0,0 +1,124 @@
1
+ import { GlkFormElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
3
+
4
+ // One button per option, the chosen one marked aria-pressed="true" — GlassKit
5
+ // styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
6
+ //
7
+ // Attributes: options (JSON [{value,label,tone?,disabled?}]), value, full,
8
+ // label (aria-label of the group), name (form field name)
9
+ // Properties: value, options (array or JSON text), full, label
10
+ // Event: glk-change { value } — only on a change made by the user
11
+ // Part: group
12
+ //
13
+ // Form-associated: a surrounding <form> receives name=value, reset restores
14
+ // the initial value. A value change only re-sets aria-pressed, never rebuilds,
15
+ // so focus stays on the button that was pressed.
16
+
17
+ const TONES = ['success', 'warning', 'error'];
18
+
19
+ function parseList(json) {
20
+ try {
21
+ const list = JSON.parse(json || '[]');
22
+ return Array.isArray(list) ? list : [];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ class GlkSegmented extends GlkFormElement {
29
+ static get observedAttributes() { return ['options', 'value', 'full', 'label']; }
30
+
31
+ render() {
32
+ this._group = this.createElement('div', ['glass-segmented'], { role: 'group', part: 'group' });
33
+ this._wrapper.appendChild(this._group);
34
+ this._initialValue = this.value;
35
+ this._build();
36
+ this._applyFull();
37
+ this._applyLabel();
38
+ }
39
+
40
+ _build() {
41
+ this._group.replaceChildren();
42
+ for (const option of parseList(this.getAttribute('options'))) {
43
+ const value = String(option.value ?? '');
44
+ const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
45
+ if (TONES.includes(option.tone)) {
46
+ button.classList.add(`glass-segmented__item--${option.tone}`);
47
+ button.appendChild(this.createElement('span', ['glass-segmented__dot']));
48
+ }
49
+ if (option.disabled) button.disabled = true;
50
+ button.appendChild(document.createTextNode(option.label ?? value));
51
+ this._group.appendChild(button);
52
+ }
53
+ this._syncValue();
54
+ }
55
+
56
+ _syncValue() {
57
+ const value = this.value;
58
+ for (const button of this._group.querySelectorAll('button[data-value]')) {
59
+ button.setAttribute('aria-pressed', String(button.dataset.value === value));
60
+ }
61
+ this.setFormValue(value);
62
+ }
63
+
64
+ _applyFull() {
65
+ this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
66
+ }
67
+
68
+ _applyLabel() {
69
+ const label = this.getAttribute('label');
70
+ if (label) this._group.setAttribute('aria-label', label);
71
+ else this._group.removeAttribute('aria-label');
72
+ }
73
+
74
+ setupEvents() {
75
+ this._onClick = (event) => {
76
+ const button = event.target.closest('button[data-value]');
77
+ if (!button || button.disabled) return;
78
+ const { value } = button.dataset;
79
+ if (value === this.value) return;
80
+ this.value = value;
81
+ this.emit('glk-change', { value });
82
+ };
83
+ this._group.addEventListener('click', this._onClick);
84
+ }
85
+
86
+ teardownEvents() {
87
+ this._group?.removeEventListener('click', this._onClick);
88
+ }
89
+
90
+ onAttributeChanged(name) {
91
+ if (!this._group) return;
92
+ switch (name) {
93
+ case 'value': this._syncValue(); break;
94
+ case 'options': this._build(); break;
95
+ case 'full': this._applyFull(); break;
96
+ case 'label': this._applyLabel(); break;
97
+ }
98
+ }
99
+
100
+ resetValue() { this.value = this._initialValue; }
101
+ restoreValue(state) { if (typeof state === 'string') this.value = state; }
102
+
103
+ get value() { return this.getAttribute('value') ?? ''; }
104
+ set value(v) {
105
+ if (v == null) this.removeAttribute('value');
106
+ else this.setAttribute('value', String(v));
107
+ }
108
+
109
+ get options() { return parseList(this.getAttribute('options')); }
110
+ set options(v) { this.setAttribute('options', typeof v === 'string' ? v : JSON.stringify(v ?? [])); }
111
+
112
+ get full() { return this.getBoolAttr('full'); }
113
+ set full(v) { this.setBoolAttr('full', v); }
114
+
115
+ get label() { return this.getAttribute('label') || ''; }
116
+ set label(v) {
117
+ if (v) this.setAttribute('label', v);
118
+ else this.removeAttribute('label');
119
+ }
120
+ }
121
+
122
+ customElements.define('glk-segmented', GlkSegmented);
123
+
124
+ export { GlkSegmented };
@@ -1,4 +1,5 @@
1
- import { a as GlkFormElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkFormElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkSelect extends GlkFormElement {
4
5
  static get observedAttributes() {
@@ -0,0 +1,141 @@
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
3
+
4
+ // Bottom sheet — the mobile sibling of <glk-modal>. Came back from
5
+ // EhrenPfoten in 1.15.0.
6
+ //
7
+ // Attributes: open, inline (no overlay, always visible), title
8
+ // Properties: open, inline · Methods: show(), close()
9
+ // Slots: default (content), actions
10
+ // Event: glk-close — only when the user closes it (scrim click, Escape),
11
+ // like <glk-modal>
12
+ // Parts: overlay, sheet, title, body, actions
13
+ //
14
+ // Opening and closing are animated by the CSS block: the overlay gets
15
+ // .is-active one frame after it is unhidden, and on closing it is hidden
16
+ // only after transitionend (safety net 400 ms), so the blurred overlay
17
+ // leaves the layout instead of idling at opacity 0. Under
18
+ // prefers-reduced-motion it switches at once.
19
+
20
+ const HIDE_FALLBACK_MS = 400;
21
+
22
+ function reducedMotion() {
23
+ return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
24
+ }
25
+
26
+ class GlkSheet extends GlkElement {
27
+ static get observedAttributes() { return ['open', 'inline', 'title']; }
28
+
29
+ render() {
30
+ this._overlay = this.createElement('div', [], { part: 'overlay' });
31
+ this._panel = this.createElement('div', ['glass-sheet'], { role: 'dialog', part: 'sheet' });
32
+ this._panel.appendChild(this.createElement('div', ['glass-sheet__grip'], { 'aria-hidden': 'true' }));
33
+ this._titleEl = this.createElement('h2', ['glass-sheet__title'], { part: 'title' });
34
+ const body = this.createElement('div', ['glass-sheet__body'], { part: 'body' });
35
+ body.appendChild(document.createElement('slot'));
36
+ const actions = this.createElement('div', ['glass-sheet__actions'], { part: 'actions' });
37
+ actions.appendChild(this.createElement('slot', [], { name: 'actions' }));
38
+ this._panel.append(this._titleEl, body, actions);
39
+ this._overlay.appendChild(this._panel);
40
+ this._wrapper.appendChild(this._overlay);
41
+ this._applyTitle();
42
+ this._applyMode();
43
+ }
44
+
45
+ _applyTitle() {
46
+ const title = this.getAttribute('title') || '';
47
+ this._titleEl.textContent = title;
48
+ this._titleEl.hidden = !title;
49
+ if (title) this._panel.setAttribute('aria-label', title);
50
+ else this._panel.removeAttribute('aria-label');
51
+ }
52
+
53
+ _applyMode() {
54
+ const inline = this.inline;
55
+ this._overlay.classList.toggle('glass-sheet-overlay', !inline);
56
+ this._panel.classList.toggle('glass-sheet--inline', inline);
57
+ this._panel.setAttribute('aria-modal', String(!inline));
58
+ clearTimeout(this._hideTimer);
59
+ if (inline) {
60
+ this._overlay.classList.remove('is-active');
61
+ this._overlay.hidden = false;
62
+ } else if (this.open) {
63
+ this._show();
64
+ } else {
65
+ this._overlay.classList.remove('is-active');
66
+ this._overlay.hidden = true;
67
+ }
68
+ }
69
+
70
+ _show() {
71
+ clearTimeout(this._hideTimer);
72
+ this._panel.removeEventListener('transitionend', this._onHidden);
73
+ this._overlay.hidden = false;
74
+ void this._overlay.offsetHeight; // reflow, so the transition starts from the hidden state
75
+ requestAnimationFrame(() => {
76
+ if (this.open && !this.inline) this._overlay.classList.add('is-active');
77
+ });
78
+ }
79
+
80
+ _hide() {
81
+ this._overlay.classList.remove('is-active');
82
+ this._onHidden = () => {
83
+ clearTimeout(this._hideTimer);
84
+ this._panel.removeEventListener('transitionend', this._onHidden);
85
+ if (!this.open && !this.inline) this._overlay.hidden = true;
86
+ };
87
+ if (reducedMotion()) {
88
+ this._onHidden();
89
+ return;
90
+ }
91
+ this._panel.addEventListener('transitionend', this._onHidden);
92
+ this._hideTimer = setTimeout(this._onHidden, HIDE_FALLBACK_MS);
93
+ }
94
+
95
+ _dismiss() {
96
+ this.removeAttribute('open');
97
+ this.emit('glk-close');
98
+ }
99
+
100
+ setupEvents() {
101
+ this._onOverlayClick = (event) => {
102
+ if (!this.inline && event.target === this._overlay) this._dismiss();
103
+ };
104
+ this._onKeydown = (event) => {
105
+ if (event.key === 'Escape' && this.open && !this.inline) this._dismiss();
106
+ };
107
+ this._overlay.addEventListener('click', this._onOverlayClick);
108
+ document.addEventListener('keydown', this._onKeydown);
109
+ }
110
+
111
+ teardownEvents() {
112
+ this._overlay?.removeEventListener('click', this._onOverlayClick);
113
+ document.removeEventListener('keydown', this._onKeydown);
114
+ }
115
+
116
+ onAttributeChanged(name) {
117
+ if (!this._overlay) return;
118
+ switch (name) {
119
+ case 'open':
120
+ if (this.inline) return;
121
+ if (this.open) this._show();
122
+ else this._hide();
123
+ break;
124
+ case 'inline': this._applyMode(); break;
125
+ case 'title': this._applyTitle(); break;
126
+ }
127
+ }
128
+
129
+ show() { this.setAttribute('open', ''); }
130
+ close() { this.removeAttribute('open'); }
131
+
132
+ get open() { return this.getBoolAttr('open'); }
133
+ set open(v) { this.setBoolAttr('open', v); }
134
+
135
+ get inline() { return this.getBoolAttr('inline'); }
136
+ set inline(v) { this.setBoolAttr('inline', v); }
137
+ }
138
+
139
+ customElements.define('glk-sheet', GlkSheet);
140
+
141
+ export { GlkSheet };
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkStatus extends GlkElement {
4
5
  static get observedAttributes() {
@@ -0,0 +1,75 @@
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
3
+
4
+ // Progress through a short flow — purely presentational. Came back from
5
+ // EhrenPfoten in 1.15.0.
6
+ //
7
+ // Attributes: steps (comma-separated labels), current (0-based), label
8
+ // (aria-label of the list, optional)
9
+ // Property: current, steps (array or comma-separated text)
10
+ // Part: steps
11
+ //
12
+ // The narrowing in tight frames is the CSS block's container query; the
13
+ // element knows nothing about it. Done steps show an SVG check, not a text
14
+ // glyph, so the mark does not depend on the font.
15
+
16
+ const CHECK = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>';
17
+
18
+ class GlkSteps extends GlkElement {
19
+ static get observedAttributes() { return ['steps', 'current', 'label']; }
20
+
21
+ render() {
22
+ this._list = this.createElement('ol', ['glass-steps'], { part: 'steps' });
23
+ this._wrapper.appendChild(this._list);
24
+ this._build();
25
+ this._applyLabel();
26
+ }
27
+
28
+ _build() {
29
+ const items = this.steps;
30
+ const current = this.current;
31
+ this._list.replaceChildren();
32
+ items.forEach((label, i) => {
33
+ const item = this.createElement('li', ['glass-steps__item']);
34
+ if (i < current) item.classList.add('glass-steps__item--done');
35
+ if (i === current) {
36
+ item.classList.add('glass-steps__item--current');
37
+ item.setAttribute('aria-current', 'step');
38
+ }
39
+ const num = this.createElement('span', ['glass-steps__num']);
40
+ if (i < current) num.innerHTML = CHECK;
41
+ else num.textContent = String(i + 1);
42
+ const text = this.createElement('span', ['glass-steps__label']);
43
+ text.textContent = label;
44
+ item.append(num, text);
45
+ this._list.appendChild(item);
46
+ if (i < items.length - 1) {
47
+ this._list.appendChild(this.createElement('li', ['glass-steps__line'], { 'aria-hidden': 'true' }));
48
+ }
49
+ });
50
+ }
51
+
52
+ _applyLabel() {
53
+ const label = this.getAttribute('label');
54
+ if (label) this._list.setAttribute('aria-label', label);
55
+ else this._list.removeAttribute('aria-label');
56
+ }
57
+
58
+ onAttributeChanged(name) {
59
+ if (!this._list) return;
60
+ if (name === 'label') this._applyLabel();
61
+ else this._build();
62
+ }
63
+
64
+ get current() { return Number(this.getAttribute('current')) || 0; }
65
+ set current(v) { this.setAttribute('current', String(Number(v) || 0)); }
66
+
67
+ get steps() {
68
+ return (this.getAttribute('steps') || '').split(',').map(s => s.trim()).filter(Boolean);
69
+ }
70
+ set steps(v) { this.setAttribute('steps', Array.isArray(v) ? v.join(',') : String(v ?? '')); }
71
+ }
72
+
73
+ customElements.define('glk-steps', GlkSteps);
74
+
75
+ export { GlkSteps };
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  const VARIANTS = ['accent', 'success', 'error'];
4
5
 
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkTabBar extends GlkElement {
4
5
  static get observedAttributes() {
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkTabDock extends GlkElement {
4
5
  static get observedAttributes() {
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkTabItem extends GlkElement {
4
5
  static get observedAttributes() {
@@ -1,4 +1,5 @@
1
- import { a as GlkFormElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkFormElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkTextarea extends GlkFormElement {
4
5
  static get observedAttributes() {
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkTitle extends GlkElement {
4
5
  static get observedAttributes() {
@@ -1,4 +1,5 @@
1
- import { G as GlkElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  const VARIANTS = ['success', 'error', 'warning'];
4
5
 
@@ -1,4 +1,5 @@
1
- import { a as GlkFormElement } from './shared/base-ClsXgLON.js';
1
+ import { GlkFormElement } from './base.js';
2
+ import '@jungherz-de/glasskit/glasskit-styles.js';
2
3
 
3
4
  class GlkToggle extends GlkFormElement {
4
5
  static get observedAttributes() {