@jungherz-de/glasskit-elements 1.28.1 → 1.29.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/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.29.0] – 2026-10-21
11
+
12
+ Versions stay aligned with GlassKit at 1.29.0, which this release is built against (peer dependency `>=1.29.0`).
13
+
14
+ ### Added
15
+
16
+ - **A count per option in `<glk-segmented>`: `badge` and `badgeLabel`.** In an admin area a customer record has a row of tabs — payments, subscriptions, invoices, tickets — and a tab should say that something waits there, and how much. `tone` puts a dot before the label, which says that, not how much, so the count went into the label ("Current (2)"), where it read as part of it and could be neither styled nor announced apart. An option is `{ value, label, tone?, disabled?, badge?, badgeLabel? }` now. `badge`, a number or a short text, shows as GlassKit 1.29.0's `.glass-segmented__badge` after the label, neutral or in the tone's colour; missing, empty or `0` shows none. The button's name says it with the label, "Tickets, 1", and `badgeLabel` words it, "Tickets, 1 open". With `icon-only` the badge sits at the icon's corner, as on `<glk-tab-item>`, and the button's `title` carries it as well. Measured in Chromium 153, WebKit 26.6 and Firefox 151: the badges stand after their labels, or 4px above and 8px beyond the icon in an icon-only row; the names read "Payments, 2", "Invoices, 12 open", "Alerts, 1 new", and "Mails" for a `0`; the drop covers the chosen option with its badge, and in a scrolling row the option stays centred; a tap on a badge chooses its option. ([#19](https://github.com/JUNGHERZ/GlassKit-Elements/issues/19))
17
+ - **New options with the same values in the same order fill the buttons in place.** Setting `options` rebuilt every button, so a keyboard user on the row lost the focus whenever a count changed, and the drop was handed a new element. Now the buttons stay and take their new label, tone, icon box and count; other values or another order rebuild the row as before. Measured in all three engines: after three counts changed, the six buttons are the same nodes, the focus stays on the focused option, the drop stays where it was, and no `glk-change` fires; in 1.28.1 the buttons were new and the focus was gone. ([#19](https://github.com/JUNGHERZ/GlassKit-Elements/issues/19))
18
+
19
+ ---
20
+
10
21
  ## [1.28.1] – 2026-10-20
11
22
 
12
23
  GlassKit is unchanged; the peer dependency stays `>=1.28.0`.
@@ -935,7 +946,8 @@ Starting with this release, GlassKit Elements version numbers are aligned with G
935
946
 
936
947
  ---
937
948
 
938
- [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.28.1...HEAD
949
+ [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.29.0...HEAD
950
+ [1.29.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.29.0
939
951
  [1.28.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.28.1
940
952
  [1.28.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.28.0
941
953
  [1.27.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.27.0
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-36-7ec8e3?style=flat-square" alt="36 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.28.1-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.29.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
 
@@ -67,7 +67,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
67
67
 
68
68
  ```html
69
69
  <!-- 1. GlassKit CSS -->
70
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.28/glasskit.min.css">
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.29/glasskit.min.css">
71
71
 
72
72
  <!-- 2. GlassKit Elements -->
73
73
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
@@ -105,7 +105,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
105
105
  <!DOCTYPE html>
106
106
  <html data-theme="dark">
107
107
  <head>
108
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.28/glasskit.min.css">
108
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.29/glasskit.min.css">
109
109
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
110
110
  </head>
111
111
  <body>
@@ -165,7 +165,7 @@ All form components support `name`, `value`, `disabled` and participate in nativ
165
165
  | `<glk-checkbox>` | Checkbox | `label`, `checked`, `disabled`, `required` |
166
166
  | `<glk-radio>` | Radio button | `label`, `name`, `value`, `checked`, `required` (per group) |
167
167
  | `<glk-range>` | Range slider | `label`, `min`, `max`, `value`, `step` |
168
- | `<glk-segmented>` | Small exclusive choice as one control, icons per option in `slot="icon-<value>"`; form-associated | `options` (JSON), `value`, `full`, `overflow` (scroll, wrap), `droplet`, `icon-only`, `label`, `name` |
168
+ | `<glk-segmented>` | Small exclusive choice as one control, icons per option in `slot="icon-<value>"`, a count per option (`badge`); form-associated | `options` (JSON), `value`, `full`, `overflow` (scroll, wrap), `droplet`, `icon-only`, `label`, `name` |
169
169
  | `<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` |
170
170
  | `<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` |
171
171
 
@@ -257,7 +257,14 @@ Since 1.28.0 a tab item and each segmented option take icons that are not inline
257
257
 
258
258
  `glk-tab-select` is cancelable and comes before `active` changes: `event.preventDefault()` keeps the current tab — for an item that opens a drawer or a link — and the bar, its drop and `glk-tab-change` stay put. `droplet` and `floating` are properties of `<glk-tab-bar>` as well. The action buttons of `<glk-modal>` follow attribute changes of their originals, so a confirm button that starts `disabled` unlocks when the page or a framework enables it.
259
259
 
260
- > **Requires GlassKit CSS >= 1.28.0.** The icon colours come from GlassKit's icon boxes; its new tokens reach the elements by inheritance: `--gl-tab-bar-color`, `--gl-tab-bar-hover-color`, `--gl-tab-bar-label-size`, `--gl-tab-bar-z-index` for the tab bar, `--gl-segmented-active-bg`, `-shadow` and `-color` for the chosen option and its drop.
260
+ Since 1.29.0 a segmented option also takes a count, `badge`, shown as a pill after its label or at the icon's corner, with `badgeLabel` for the words a screen reader says — `"Tickets, 1 open"`. New options with the same values fill the buttons in place, so a live count keeps the focus and the drop:
261
+
262
+ ```html
263
+ <glk-segmented droplet overflow="scroll" label="Customer" value="invoices"
264
+ options='[{"value":"invoices","label":"Invoices","badge":2},{"value":"tickets","label":"Tickets","tone":"warning","badge":1,"badgeLabel":"1 open"}]'></glk-segmented>
265
+ ```
266
+
267
+ > **Requires GlassKit CSS >= 1.28.0** (1.29.0 for the counts). The icon colours come from GlassKit's icon boxes; its new tokens reach the elements by inheritance: `--gl-tab-bar-color`, `--gl-tab-bar-hover-color`, `--gl-tab-bar-label-size`, `--gl-tab-bar-z-index` for the tab bar, `--gl-segmented-active-bg`, `-shadow` and `-color` for the chosen option and its drop.
261
268
 
262
269
  ### Solid and tint surfaces
263
270
 
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: glasskit-elements
3
- description: GlassKit Elements is a vanilla-JS Web Components library (v1.28.1) wrapping GlassKit CSS v1.28.0. It provides 36 custom elements with the `glk-` prefix, Dark/Light mode with automatic theme sync, Shadow DOM encapsulation, and form-associated custom elements. Use this reference whenever generating HTML that uses `<glk-*>` tags to ensure correct attributes, slots, events, and composition.
3
+ description: GlassKit Elements is a vanilla-JS Web Components library (v1.29.0) wrapping GlassKit CSS v1.29.0. It provides 36 custom elements with the `glk-` prefix, Dark/Light mode with automatic theme sync, Shadow DOM encapsulation, and form-associated custom elements. Use this reference whenever generating HTML that uses `<glk-*>` tags to ensure correct attributes, slots, events, and composition.
4
4
  ---
5
5
 
6
6
  # GlassKit Elements – AI Component Reference
@@ -1153,7 +1153,7 @@ A small, exclusive choice as one control (since 1.15.0) — traffic light, morni
1153
1153
 
1154
1154
  | Attribute | Type | Description |
1155
1155
  |---|---|---|
1156
- | `options` | JSON | `[{ value, label, tone?, disabled? }]`; `tone` is `success` \| `warning` \| `error` and puts a dot in that colour before the label |
1156
+ | `options` | JSON | `[{ value, label, tone?, disabled?, badge?, badgeLabel? }]`; `tone` is `success` \| `warning` \| `error` and puts a dot in that colour before the label; `badge` is a count after the label (since 1.29.0), `badgeLabel` words it for the name |
1157
1157
  | `value` | String | Value of the chosen option (reflected property) |
1158
1158
  | `full` | Boolean | Buttons share the width |
1159
1159
  | `overflow` | String | What happens when the options do not fit: `scroll` — one row that scrolls sideways, the chosen option kept in view; `wrap` — breaks into lines. Without it the row stays one line (since 1.17.0) |
@@ -1178,6 +1178,14 @@ Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`
1178
1178
 
1179
1179
  GlassKit 1.28.0's tokens for the chosen option reach the element by inheritance: `--gl-segmented-active-bg`, `--gl-segmented-active-shadow` and `--gl-segmented-active-color` — for a dark brand, on which the light surface leaves a white label below 4.5:1. The drop takes the same surface.
1180
1180
 
1181
+ **Counts (since 1.29.0).** `badge` on an option shows how much waits behind it — open invoices, new tickets — as a pill after the label (GlassKit's `.glass-segmented__badge`, tabular figures), neutral without a tone and in the tone's colour with one. A number or a short text; missing, empty or `0` shows none. Use it instead of a count in the label: `"Tickets (1)"` reads as part of the label and can be neither styled nor announced apart. The button's name says both, `"Tickets, 1"`; `badgeLabel` words it, `"Tickets, 1 open"`. With `icon-only` the badge sits at the icon's corner, as on `<glk-tab-item>`, and the button's `title` carries it too. Setting `options` again with the same values in the same order fills the buttons in place, so a live count keeps the focus and the drop; other values rebuild the row.
1182
+
1183
+ ```html
1184
+ <glk-segmented droplet overflow="scroll" label="Customer" value="invoices"
1185
+ options='[{"value":"payments","label":"Payments"},{"value":"invoices","label":"Invoices","badge":2},{"value":"tickets","label":"Tickets","tone":"warning","badge":1,"badgeLabel":"1 open"}]'>
1186
+ </glk-segmented>
1187
+ ```
1188
+
1181
1189
  With `overflow="scroll"` the chosen option is centred in the row after the first layout, on every value change and when the row's width changes — the row scrolls, never the page. Seven areas on a phone:
1182
1190
 
1183
1191
  ```html
@@ -6,7 +6,11 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
6
6
  // One button per option, the chosen one marked aria-pressed="true" — GlassKit
7
7
  // styles exactly that attribute. Came back from EhrenPfoten in 1.15.0.
8
8
  //
9
- // Attributes: options (JSON [{value,label,tone?,disabled?}]), value, full,
9
+ // Attributes: options (JSON [{value,label,tone?,disabled?,badge?,badgeLabel?}]
10
+ // — badge since 1.29.0: a count after the label, GlassKit's
11
+ // .glass-segmented__badge, in the tone's colour where one is
12
+ // set; missing, empty or 0 shows none; badgeLabel words it for
13
+ // the name, "1 open"), value, full,
10
14
  // overflow ("scroll" | "wrap": what happens when the options
11
15
  // do not fit — without it the row stays one line and runs past
12
16
  // the edge), label (aria-label of the group), name (form field),
@@ -28,7 +32,9 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
28
32
  //
29
33
  // Form-associated: a surrounding <form> receives name=value, reset restores
30
34
  // the initial value. A value change only re-sets aria-pressed, never rebuilds,
31
- // so focus stays on the button that was pressed.
35
+ // so focus stays on the button that was pressed. New options with the same
36
+ // values in the same order — a count that changed — fill the buttons in
37
+ // place (since 1.29.0), so the focus and the drop stay as well.
32
38
  //
33
39
  // overflow="scroll" keeps the chosen option in view: it is centred in the
34
40
  // row after the first layout, on every value change and when the row's width
@@ -50,6 +56,13 @@ function parseList(json) {
50
56
  }
51
57
  }
52
58
 
59
+ /** The text of an option's count; empty when there is none to show. */
60
+ function badgeText(badge) {
61
+ if (badge == null || badge === false) return '';
62
+ const text = String(badge).trim();
63
+ return text === '0' ? '' : text;
64
+ }
65
+
53
66
  class GlkSegmented extends GlkFormElement {
54
67
  static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet', 'icon-only']; }
55
68
 
@@ -67,33 +80,54 @@ class GlkSegmented extends GlkFormElement {
67
80
  }
68
81
 
69
82
  _build() {
70
- this._group.replaceChildren();
83
+ const list = parseList(this.getAttribute('options'));
71
84
  const iconOnly = this.iconOnly;
72
- for (const option of parseList(this.getAttribute('options'))) {
73
- const value = String(option.value ?? '');
74
- const label = String(option.label ?? value);
75
- const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
76
- if (TONES.includes(option.tone)) {
77
- button.classList.add(`glass-segmented__item--${option.tone}`);
78
- button.appendChild(this.createElement('span', ['glass-segmented__dot']));
79
- }
80
- // The label names the button; an icon font's text must not join it.
81
- const icon = this.createElement('span', ['glass-segmented__icon'], { 'aria-hidden': 'true' });
82
- icon.appendChild(this.createElement('slot', [], { name: `icon-${value}` }));
83
- button.appendChild(icon);
84
- if (iconOnly) {
85
- button.setAttribute('aria-label', label);
86
- button.title = label;
87
- } else {
88
- button.appendChild(document.createTextNode(label));
89
- }
90
- this._group.appendChild(button);
91
- }
85
+ // The same values in the same order keep their buttons; anything else
86
+ // starts the row afresh.
87
+ const buttons = [...this._group.children];
88
+ const same = buttons.length === list.length
89
+ && list.every((option, i) => buttons[i].dataset.value === String(option.value ?? ''));
90
+ if (!same) this._group.replaceChildren(...list.map(() => this.createElement('button', [], { type: 'button' })));
91
+ [...this._group.children].forEach((button, i) => this._fill(button, list[i], iconOnly));
92
92
  this._syncIcons();
93
93
  this.applyDisabled(this._actuallyDisabled);
94
94
  this._syncValue();
95
95
  }
96
96
 
97
+ /** Gives an option's button its tone, icon box, label and count. */
98
+ _fill(button, option, iconOnly) {
99
+ const value = String(option.value ?? '');
100
+ const label = String(option.label ?? value);
101
+ const parts = [];
102
+ button.className = 'glass-segmented__item';
103
+ button.dataset.value = value;
104
+ if (TONES.includes(option.tone)) {
105
+ button.classList.add(`glass-segmented__item--${option.tone}`);
106
+ parts.push(this.createElement('span', ['glass-segmented__dot']));
107
+ }
108
+ // The label names the button; an icon font's text must not join it.
109
+ const icon = this.createElement('span', ['glass-segmented__icon'], { 'aria-hidden': 'true' });
110
+ icon.appendChild(this.createElement('slot', [], { name: `icon-${value}` }));
111
+ parts.push(icon);
112
+ if (!iconOnly) parts.push(document.createTextNode(label));
113
+ // The count after the label, or at the icon's corner in an icon-only row;
114
+ // the name says it in words — "Tickets, 1 open".
115
+ const badge = badgeText(option.badge);
116
+ let name = label;
117
+ if (badge) {
118
+ const pill = this.createElement('span', ['glass-segmented__badge']);
119
+ pill.textContent = badge;
120
+ if (iconOnly) icon.appendChild(pill);
121
+ else parts.push(pill);
122
+ name = `${label}, ${option.badgeLabel ? String(option.badgeLabel) : badge}`;
123
+ }
124
+ button.replaceChildren(...parts);
125
+ if (iconOnly || badge) button.setAttribute('aria-label', name);
126
+ else button.removeAttribute('aria-label');
127
+ if (iconOnly) button.title = name;
128
+ else button.removeAttribute('title');
129
+ }
130
+
97
131
  /** An option shows its icon box only while an icon is slotted into it. */
98
132
  _syncIcons() {
99
133
  for (const slot of this._group.querySelectorAll('.glass-segmented__icon > slot')) {