@jungherz-de/glasskit-elements 1.16.1 → 1.18.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.
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-33-7ec8e3?style=flat-square" alt="33 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.16.1-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.18.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
 
@@ -66,7 +66,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
66
66
 
67
67
  ```html
68
68
  <!-- 1. GlassKit CSS -->
69
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.16/glasskit.min.css">
69
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.18/glasskit.min.css">
70
70
 
71
71
  <!-- 2. GlassKit Elements -->
72
72
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
@@ -104,7 +104,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
104
104
  <!DOCTYPE html>
105
105
  <html data-theme="dark">
106
106
  <head>
107
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.16/glasskit.min.css">
107
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.18/glasskit.min.css">
108
108
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
109
109
  </head>
110
110
  <body>
@@ -137,7 +137,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
137
137
  | Component | Description | Key Attributes |
138
138
  |---|---|---|
139
139
  | `<glk-card>` | Glass-effect content card | `glow`, `fill` |
140
- | `<glk-badge>` | Inline status badge, or a filter chip with `interactive` | `variant` (primary, success, error), `interactive`, `selected` |
140
+ | `<glk-badge>` | Inline status badge, or a filter chip with `interactive` | `variant` (primary, success, warning, error), `interactive`, `selected` |
141
141
  | `<glk-avatar>` | Circular avatar | `size` (sm, lg), `src` |
142
142
  | `<glk-title>` | Styled heading | — |
143
143
  | `<glk-divider>` | Horizontal divider | — |
@@ -156,7 +156,7 @@ All form components support `name`, `value`, `disabled` and participate in nativ
156
156
 
157
157
  | Component | Description | Key Attributes |
158
158
  |---|---|---|
159
- | `<glk-input>` | Text input with label & hint | `label`, `type`, `placeholder`, `hint`, `error`, `required` |
159
+ | `<glk-input>` | Text input with label & hint | `label`, `type`, `placeholder`, `hint`, `error`, `required`, `min`, `max`, `step`, `maxlength`, `pattern`, `autocomplete`, `inputmode` |
160
160
  | `<glk-textarea>` | Multi-line text input | `label`, `rows`, `placeholder` |
161
161
  | `<glk-select>` | Dropdown select | `label` (children: `<option>`) |
162
162
  | `<glk-search>` | Search input with icon | `placeholder` |
@@ -164,7 +164,7 @@ All form components support `name`, `value`, `disabled` and participate in nativ
164
164
  | `<glk-checkbox>` | Checkbox | `label`, `checked`, `disabled` |
165
165
  | `<glk-radio>` | Radio button | `label`, `name`, `value`, `checked` |
166
166
  | `<glk-range>` | Range slider | `label`, `min`, `max`, `value`, `step` |
167
- | `<glk-segmented>` | Small exclusive choice as one control; form-associated | `options` (JSON), `value`, `full`, `label`, `name` |
167
+ | `<glk-segmented>` | Small exclusive choice as one control; form-associated | `options` (JSON), `value`, `full`, `overflow` (scroll, wrap), `label`, `name` |
168
168
  | `<glk-calendar>` | One month with a day to pick; keyboard navigation, `Intl` names; form-associated | `month`, `value`, `today`, `min`, `max`, `marks`, `locale`, `week-start`, `label`, `name` |
169
169
  | `<glk-image-picker>` | One image with preview, resized on the client, EXIF rotation applied; not form-associated | `src`, `label`, `hint`, `round`, `max`, `type`, `quality`, `accept` |
170
170
 
@@ -1,7 +1,9 @@
1
1
  import { GlkElement } from './base.js';
2
2
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
- const VARIANTS = ['primary', 'success', 'error'];
4
+ // warning since 1.18.0 (needs GlassKit 1.18.0). Any other value falls back to
5
+ // the neutral badge, as an unknown value of an enumerated attribute does.
6
+ const VARIANTS = ['primary', 'success', 'warning', 'error'];
5
7
 
6
8
  class GlkBadge extends GlkElement {
7
9
  static get displayInline() { return true; }
@@ -1,5 +1,6 @@
1
1
  import { GlkElement } from './base.js';
2
2
  import { f as formatters, r as resolveLocale, b as parseJsonObject, p as parseIso, i as isoDate, c as addDays, w as weekdayShort, d as isTone } from './shared/dates-CNWaJf09.js';
3
+ import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
3
4
  import '@jungherz-de/glasskit/glasskit-styles.js';
4
5
 
5
6
  // A row of day chips that scrolls sideways — a booking horizon, the days
@@ -85,24 +86,15 @@ class GlkDateStrip extends GlkElement {
85
86
  for (const chip of this._chips()) chip.tabIndex = chip === target ? 0 : -1;
86
87
  }
87
88
 
88
- // Scroll the chosen chip to the middle of the strip. Computed from client
89
- // rects and applied to the strip's own scrollLeft, so the page never moves —
90
- // scrollIntoView would scroll every ancestor. Before the first layout the
91
- // strip has no width; the ResizeObserver in setupEvents centres then, and
92
- // again whenever the strip's width changes — a parent that upgrades later
93
- // and adds its padding, a window resize, a strip that starts out hidden. A
94
- // value change glides unless the user asked for reduced motion; a size
95
- // change snaps, so a settling layout never animates.
89
+ // Centre the chosen chip in the strip (see reveal.js). Before the first
90
+ // layout the strip has no width; the ResizeObserver in setupEvents centres
91
+ // then, and again whenever the strip's width changes — a parent that
92
+ // upgrades later and adds its padding, a window resize, a strip that
93
+ // starts out hidden. A value change glides; a size change snaps, so a
94
+ // settling layout never animates.
96
95
  _reveal(behavior = 'instant') {
97
96
  const chip = this._strip.querySelector('[aria-pressed="true"]');
98
- const width = this._strip.clientWidth;
99
- if (!chip || !width) return;
100
- const strip = this._strip.getBoundingClientRect();
101
- const box = chip.getBoundingClientRect();
102
- const left = this._strip.scrollLeft + (box.left - strip.left) - (strip.width - box.width) / 2;
103
- const smooth = behavior === 'smooth' && !matchMedia('(prefers-reduced-motion: reduce)').matches;
104
- this._strip.scrollTo({ left: Math.max(0, left), behavior: smooth ? 'smooth' : 'instant' });
105
- this._revealedAt = width;
97
+ if (revealCentered(this._strip, chip, behavior === 'smooth')) this._revealedAt = this._strip.clientWidth;
106
98
  }
107
99
 
108
100
  _applyLabel() {
@@ -1,9 +1,15 @@
1
1
  import { GlkFormElement } from './base.js';
2
2
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
+ // Attributes that belong to the inner <input> and are handed down unchanged:
5
+ // the picker's range and step, the typing limits, the pattern, autofill and
6
+ // the on-screen keyboard. Absent on the host means absent inside (since
7
+ // 1.17.0 — before, a date field with min still offered every past day).
8
+ const FORWARDED = ['min', 'max', 'step', 'minlength', 'maxlength', 'pattern', 'autocomplete', 'inputmode'];
9
+
4
10
  class GlkInput extends GlkFormElement {
5
11
  static get observedAttributes() {
6
- return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required'];
12
+ return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required', ...FORWARDED];
7
13
  }
8
14
 
9
15
  render() {
@@ -29,6 +35,7 @@ class GlkInput extends GlkFormElement {
29
35
 
30
36
  if (this.getBoolAttr('disabled')) this._input.disabled = true;
31
37
  if (this.getBoolAttr('required')) this._input.required = true;
38
+ for (const attr of FORWARDED) this._forward(attr);
32
39
 
33
40
  // Hint
34
41
  this._hintEl = this.createElement('span', this._computeHintClasses());
@@ -98,9 +105,17 @@ class GlkInput extends GlkFormElement {
98
105
  case 'required':
99
106
  this._input.required = this.getBoolAttr('required');
100
107
  break;
108
+ default:
109
+ if (FORWARDED.includes(name)) this._forward(name);
101
110
  }
102
111
  }
103
112
 
113
+ _forward(attr) {
114
+ const value = this.getAttribute(attr);
115
+ if (value === null) this._input.removeAttribute(attr);
116
+ else this._input.setAttribute(attr, value);
117
+ }
118
+
104
119
  _computeInputClasses() {
105
120
  const classes = ['glass-input'];
106
121
  if (this.getBoolAttr('error')) classes.push('glass-input--error');
@@ -1,20 +1,32 @@
1
1
  import { GlkFormElement } from './base.js';
2
+ import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
2
3
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
4
 
4
5
  // One button per option, the chosen one marked aria-pressed="true" — GlassKit
5
6
  // styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
6
7
  //
7
8
  // 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
9
+ // overflow ("scroll" | "wrap": what happens when the options
10
+ // do not fit — without it the row stays one line and runs past
11
+ // the edge), label (aria-label of the group), name (form field)
12
+ // Properties: value, options (array or JSON text), full, overflow, label
10
13
  // Event: glk-change { value } — only on a change made by the user
11
14
  // Part: group
12
15
  //
13
16
  // Form-associated: a surrounding <form> receives name=value, reset restores
14
17
  // the initial value. A value change only re-sets aria-pressed, never rebuilds,
15
18
  // so focus stays on the button that was pressed.
19
+ //
20
+ // overflow="scroll" keeps the chosen option in view: it is centred in the
21
+ // row after the first layout, on every value change and when the row's width
22
+ // changes, through the row's own scrolling, so the page never moves (since
23
+ // 1.17.0). The attribute is not called "scroll": that is a method of every
24
+ // element, and a framework that sets a property whenever the element has one
25
+ // (hybrids, Vue, React 19) would overwrite the method instead of setting the
26
+ // attribute.
16
27
 
17
28
  const TONES = ['success', 'warning', 'error'];
29
+ const OVERFLOWS = ['scroll', 'wrap'];
18
30
 
19
31
  function parseList(json) {
20
32
  try {
@@ -26,14 +38,14 @@ function parseList(json) {
26
38
  }
27
39
 
28
40
  class GlkSegmented extends GlkFormElement {
29
- static get observedAttributes() { return ['options', 'value', 'full', 'label']; }
41
+ static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label']; }
30
42
 
31
43
  render() {
32
44
  this._group = this.createElement('div', ['glass-segmented'], { role: 'group', part: 'group' });
33
45
  this._wrapper.appendChild(this._group);
34
46
  this._initialValue = this.value;
35
47
  this._build();
36
- this._applyFull();
48
+ this._applyLayout();
37
49
  this._applyLabel();
38
50
  }
39
51
 
@@ -59,10 +71,20 @@ class GlkSegmented extends GlkFormElement {
59
71
  button.setAttribute('aria-pressed', String(button.dataset.value === value));
60
72
  }
61
73
  this.setFormValue(value);
74
+ this._reveal(this._revealedAt ? 'smooth' : 'instant');
62
75
  }
63
76
 
64
- _applyFull() {
77
+ _applyLayout() {
78
+ const overflow = this.overflow;
65
79
  this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
80
+ for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
81
+ this._reveal('instant');
82
+ }
83
+
84
+ _reveal(behavior) {
85
+ if (this.overflow !== 'scroll') return;
86
+ const pressed = this._group.querySelector('[aria-pressed="true"]');
87
+ if (revealCentered(this._group, pressed, behavior === 'smooth')) this._revealedAt = this._group.clientWidth;
66
88
  }
67
89
 
68
90
  _applyLabel() {
@@ -80,11 +102,16 @@ class GlkSegmented extends GlkFormElement {
80
102
  this.value = value;
81
103
  this.emit('glk-change', { value });
82
104
  };
105
+ this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
83
106
  this._group.addEventListener('click', this._onClick);
107
+ this._resizeObserver = new ResizeObserver(this._onResize);
108
+ this._resizeObserver.observe(this._group);
84
109
  }
85
110
 
86
111
  teardownEvents() {
87
112
  this._group?.removeEventListener('click', this._onClick);
113
+ this._resizeObserver?.disconnect();
114
+ this._resizeObserver = null;
88
115
  }
89
116
 
90
117
  onAttributeChanged(name) {
@@ -92,7 +119,8 @@ class GlkSegmented extends GlkFormElement {
92
119
  switch (name) {
93
120
  case 'value': this._syncValue(); break;
94
121
  case 'options': this._build(); break;
95
- case 'full': this._applyFull(); break;
122
+ case 'full':
123
+ case 'overflow': this._applyLayout(); break;
96
124
  case 'label': this._applyLabel(); break;
97
125
  }
98
126
  }
@@ -112,6 +140,15 @@ class GlkSegmented extends GlkFormElement {
112
140
  get full() { return this.getBoolAttr('full'); }
113
141
  set full(v) { this.setBoolAttr('full', v); }
114
142
 
143
+ get overflow() {
144
+ const v = this.getAttribute('overflow');
145
+ return OVERFLOWS.includes(v) ? v : '';
146
+ }
147
+ set overflow(v) {
148
+ if (v) this.setAttribute('overflow', String(v));
149
+ else this.removeAttribute('overflow');
150
+ }
151
+
115
152
  get label() { return this.getAttribute('label') || ''; }
116
153
  set label(v) {
117
154
  if (v) this.setAttribute('label', v);
@@ -0,0 +1,21 @@
1
+ // Scroll an item to the middle of its scrolling row — shared by
2
+ // <glk-date-strip> and <glk-segmented overflow="scroll">. Computed from
3
+ // client rects and applied to the row alone, where scrollIntoView would move
4
+ // every scrolling ancestor, the page included. A relative scrollBy works in
5
+ // both writing directions: scrollLeft runs negative in RTL, the distance on
6
+ // screen does not. Returns false while the row has no width — before the
7
+ // first layout, or hidden — so the caller can try again from a
8
+ // ResizeObserver. Pass smooth for a reveal that follows a value change, not
9
+ // for one caused by layout; reduced motion wins over it.
10
+
11
+ function revealCentered(scroller, item, smooth) {
12
+ if (!item || !scroller.clientWidth) return false;
13
+ const port = scroller.getBoundingClientRect();
14
+ const box = item.getBoundingClientRect();
15
+ const delta = box.left + box.width / 2 - (port.left + scroller.clientLeft + scroller.clientWidth / 2);
16
+ const glide = smooth && !matchMedia('(prefers-reduced-motion: reduce)').matches;
17
+ scroller.scrollBy({ left: delta, behavior: glide ? 'smooth' : 'instant' });
18
+ return true;
19
+ }
20
+
21
+ export { revealCentered as r };