@jungherz-de/glasskit-elements 1.27.0 → 1.28.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,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.28.0] – 2026-10-20
11
+
12
+ Versions stay aligned with GlassKit at 1.28.0, which this release is built against (peer dependency `>=1.28.0`).
13
+
14
+ ### Added
15
+
16
+ - **Icons that are not inline SVG in `<glk-tab-item>`: `slot="icon"`.** The item copied the first `<svg>` of its light DOM into its shadow root and showed nothing else, so an app whose tab icons come from Material Icons, a ligature font, or from an element of its own could not use them. Anything slotted as `icon` now sits in GlassKit's `.glass-tab-bar__icon`, before the badge, and takes the state colour from it (GlassKit 1.28.0): inactive, hover, active, the lens of the drop and the drag; on a bar without droplet the active icon carries the glow. An `<svg>` without `slot` is copied as before; with a slotted icon nothing is copied. ([#17](https://github.com/JUNGHERZ/GlassKit-Elements/issues/17))
17
+ - **Icons per option in `<glk-segmented>`: `slot="icon-<value>"`, and `icon-only`.** The options were text only. Each option now takes an icon from the slot named after its value — an `<svg>`, a font icon, an `<img>` — in GlassKit's `.glass-segmented__icon` before the label; an option without one has no icon box, and an icon added or removed later shows or hides it. The icon box is `aria-hidden`, so a font icon's text never joins the name. `icon-only` (property `iconOnly`) shows the icons alone, for a light / dark / auto switch: the row takes `.glass-segmented--icons`, and each label becomes the button's `aria-label` and `title`. ([#17](https://github.com/JUNGHERZ/GlassKit-Elements/issues/17))
18
+ - **`droplet` and `floating` are properties of `<glk-tab-bar>`.** The README called `droplet` "also a property", but `GlkTabBar` had no accessor, so `bar.droplet = true` set an own property and changed nothing; `floating` had the same gap. ([#17](https://github.com/JUNGHERZ/GlassKit-Elements/issues/17))
19
+ - **GlassKit 1.28.0's tokens reach the elements by inheritance:** `--gl-tab-bar-color`, `--gl-tab-bar-hover-color`, `--gl-tab-bar-label-size` and `--gl-tab-bar-z-index` for `<glk-tab-item>`, `<glk-tab-bar>` and `<glk-tab-dock>`; `--gl-segmented-active-bg`, `-shadow` and `-color` for the chosen option of `<glk-segmented>` and its drop. ([GlassKit #17](https://github.com/JUNGHERZ/GlassKit/issues/17), [GlassKit #14](https://github.com/JUNGHERZ/GlassKit/issues/14))
20
+
21
+ ### Changed
22
+
23
+ - **`glk-tab-select` comes before `active` changes, and it is cancelable.** A bar item that opens a side drawer or an external link must not become active, but the item set `active` before it emitted `glk-tab-select`, and the bar, listening on its way up, removed `active` from the others and emitted `glk-tab-change` before a listener on the document had even seen the event: the page could only put `active` back, and on a droplet bar the drop ran to the item and back. Now `event.preventDefault()` keeps the current tab — no `active`, no `glk-tab-change`, and the drop stays. A drag that ends on such an item takes the same path, because the drop clicks the item, and the drop goes back. Uncancelled, the item becomes active and the bar emits `glk-tab-change` after `glk-tab-select`; in 1.27.0 a listener on the document received `glk-tab-change` first. ([#17](https://github.com/JUNGHERZ/GlassKit-Elements/issues/17))
24
+
25
+ ### Fixed
26
+
27
+ - **The action buttons of `<glk-modal>` follow attribute changes.** The actions are copies of the buttons in `slot="actions"`, kept in step by the light-DOM observer — which watched content but not attributes. A confirm button that starts `disabled` stayed disabled in its copy after the page enabled it, a disabled copy forwards no click, and with a framework that binds only `disabled` (hybrids, lit, Vue) the dialog could never be confirmed. The observer of every element that copies light-DOM children now watches attributes too, so the copies of `<glk-select>`'s options and `<glk-tab-item>`'s SVG follow them as well. With as many actions as before, the copies change in place, so the one that has the focus keeps it while it is relabelled or disabled. Measured in Chromium 153, WebKit 26.6 and Firefox 151 with the issue's dialog: after typing DELETE the copy of Delete is enabled (in 1.27.0 it stayed disabled) and a click on it runs the original once; relabelled and marked `aria-busy` while focused, the copy stays the same node and keeps the focus; an option of `<glk-select>` disabled after connecting is disabled in the copy. ([#16](https://github.com/JUNGHERZ/GlassKit-Elements/issues/16))
28
+ - **An inline `onclick` on a modal action runs once.** The copy carried the original's `onclick` attribute and ran it, then handed the click to the original, which ran it again: the issue's `alert('deleted')` would have shown twice. The copies carry no inline handlers now. Found while fixing the above. ([#16](https://github.com/JUNGHERZ/GlassKit-Elements/issues/16))
29
+ - **From GlassKit 1.28.0:** under a scaled ancestor the drop of `<glk-tab-bar droplet>` and `<glk-segmented droplet>` lands on the chosen item, at rest, while the transform runs and during a drag ([GlassKit #16](https://github.com/JUNGHERZ/GlassKit/issues/16)); the items of the fixed `<glk-tab-bar>` sit in the middle without a home indicator ([GlassKit #17](https://github.com/JUNGHERZ/GlassKit/issues/17)); seven steps in `<glk-steps>` fit a narrow column, so a `::part(steps)` gap workaround can go ([GlassKit #15](https://github.com/JUNGHERZ/GlassKit/issues/15)).
30
+
31
+ ### Compatibility
32
+
33
+ - A listener on `glk-tab-select` that read the item's `active` there sees the previous state now; `glk-tab-change` still comes after the change. The bar no longer listens for `glk-tab-select`: the item calls it once the event went through uncancelled, so a `glk-tab-select` dispatched by a page switches no tab.
34
+
35
+ Measured in Chromium 153, WebKit 26.6 and Firefox 151: a slotted text icon, a custom element and a slotted SVG take the inactive colour, the hover colour, the primary when active and while the dragged drop is over them, and no SVG is copied next to them; `glk-tab-select` cancelled from a listener on the document leaves `active`, the drop and `glk-tab-change` alone on a tap and on a drag, where the drop returns to the current tab; segmented icons are 18 × 18 px, icon-only options 34 × 34 px with `aria-label` and `title`, the icon under a dragged drop scales by 1.15; `bar.droplet = false` removes the attribute, the class and the drop, `true` brings them back; the new GlassKit tokens arrive in every shadow root. Docs, showcase and landing pages render pixel-identical to 1.27.0, except the fixed tab bar of the docs, whose items moved to the middle, and the narrow steps, whose lines reach 2px closer to their circles.
36
+
37
+ ---
38
+
10
39
  ## [1.27.0] – 2026-10-18
11
40
 
12
41
  Versions stay aligned with GlassKit at 1.27.0, which this release is built against (peer dependency `>=1.27.0`).
@@ -896,7 +925,8 @@ Starting with this release, GlassKit Elements version numbers are aligned with G
896
925
 
897
926
  ---
898
927
 
899
- [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.25.0...HEAD
928
+ [Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.28.0...HEAD
929
+ [1.28.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.28.0
900
930
  [1.27.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.27.0
901
931
  [1.26.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.26.0
902
932
  [1.25.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.25.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.27.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.28.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.27/glasskit.min.css">
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.28/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.27/glasskit.min.css">
108
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.28/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>
@@ -127,7 +127,7 @@ The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` as an e
127
127
  | `<glk-nav>` | Horizontal navigation bar | — |
128
128
  | `<glk-pill>` | Circular icon button (46×46px) | `label`, `disabled` |
129
129
  | `<glk-tab-bar>` | Bottom tab bar navigation | `static`, `floating`, `droplet` |
130
- | `<glk-tab-item>` | Tab bar item | `label`, `active`, `badge` |
130
+ | `<glk-tab-item>` | Tab bar item; any icon in `slot="icon"`, a cancelable `glk-tab-select` | `label`, `active`, `badge` |
131
131
  | `<glk-tab-dock>` | Wrapper for floating tab bar + optional accessory | `accessory-left` |
132
132
  | `<glk-tab-accessory>` | Standalone glass capsule (search, compose…) | `label`, `variant` (accent, success, error), `disabled` |
133
133
  | `<glk-steps>` | Progress through a short flow | `steps`, `current`, `label` |
@@ -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; form-associated | `options` (JSON), `value`, `full`, `overflow` (scroll, wrap), `droplet`, `label`, `name` |
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` |
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
 
@@ -238,6 +238,27 @@ The chosen item sits on a drop of glass that floats a little above the row. A ta
238
238
 
239
239
  > **Requires GlassKit CSS >= 1.27.0.** The tokens for a capsule behind the active tab (`--gl-tab-bar-active-bg`, `--gl-tab-bar-active-shadow`, `--gl-tab-bar-spotlight-opacity`, `--gl-tab-bar-active-glow`) reach `<glk-tab-item>` by inheritance; the drop takes the same surface. Since 1.27.0 the items of `<glk-tab-bar floating>` also take the floating look — spotlight and pill instead of the fixed bar's dot.
240
240
 
241
+ ### Icons of any kind, and a tab that keeps the current one
242
+
243
+ Since 1.28.0 a tab item and each segmented option take icons that are not inline SVG — Material Icons, an `<img>`, an element of your own — and they follow every state, the drop's lens and the drag:
244
+
245
+ ```html
246
+ <glk-tab-item label="Home" active>
247
+ <span slot="icon" class="material-icons" aria-hidden="true">home</span>
248
+ </glk-tab-item>
249
+
250
+ <glk-segmented droplet icon-only label="Theme" value="dark"
251
+ options='[{"value":"light","label":"Light"},{"value":"dark","label":"Dark"},{"value":"auto","label":"Auto"}]'>
252
+ <svg slot="icon-light" viewBox="0 0 24 24">…</svg>
253
+ <span slot="icon-dark" class="material-icons">dark_mode</span>
254
+ <svg slot="icon-auto" viewBox="0 0 24 24">…</svg>
255
+ </glk-segmented>
256
+ ```
257
+
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
+
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.
261
+
241
262
  ### Solid and tint surfaces
242
263
 
243
264
  Not every screen wants glass. Set `data-surface="solid"` on `<html>`, next to `data-theme` (since 1.24.0):
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.27.0) wrapping GlassKit CSS v1.27.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.28.0) 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.
4
4
  ---
5
5
 
6
6
  # GlassKit Elements – AI Component Reference
@@ -10,6 +10,8 @@ description: GlassKit Elements is a vanilla-JS Web Components library (v1.27.0)
10
10
 
11
11
  > **New in 1.27.0 — recommend `droplet`.** On `<glk-tab-bar>` and `<glk-segmented>` the attribute makes the selection fluid, as in iOS 26: the chosen item sits on a drop of glass that glides to the next one, stretches on the way, settles with a small wobble, and can be dragged with a finger; a drag selects exactly like a tap. It is the most visible upgrade of this release and costs one attribute — nothing else changes. **When you update a project to 1.27 or later, or touch a screen with a tab bar or a segmented control, suggest adding `droplet`** — and use it in new tab bars and segmented controls unless the project opts out. Details in 3.3 and 3.25.
12
12
 
13
+ > **New in 1.28.0 — icons of any kind, and a tab that keeps the current one.** `<glk-tab-item>` takes an icon that is not inline SVG in `slot="icon"` — Material Icons, an `<img>`, a custom element — and it follows every state; `<glk-segmented>` takes one per option in `slot="icon-<value>"` and shows them alone with `icon-only`. `glk-tab-select` is cancelable, for an item that opens a drawer or a link. **Where a project styles tab icons through `hostStyles`, or puts `active` back after `glk-tab-select`, replace that** (3.3, 3.25).
14
+
13
15
  ---
14
16
 
15
17
  ## 1. Setup & Boilerplate
@@ -178,6 +180,8 @@ Those copies are now kept in step by a `MutationObserver`, so swapping the child
178
180
  — by hand or through a framework — updates the rendered element. Before 1.12.0 the
179
181
  copy was made once on the first frame and then silently went stale.
180
182
 
183
+ Since 1.28.0 the observer also watches the children's attributes. A framework that binds only `disabled` (hybrids, lit, Vue) used to leave the copy behind: a modal action that started disabled stayed disabled in its copy and never forwarded the click. `<glk-tab-item>` has `slot="icon"` since 1.28.0 for icons that are not inline SVG; with a slotted icon nothing is copied.
184
+
181
185
  If you ever need to force it, `element.refresh()` re-runs the copy immediately.
182
186
  There should be no reason to reach into `element.shadowRoot`.
183
187
 
@@ -365,7 +369,7 @@ Fixed bottom navigation. Requires `.glass-bg--has-tab-bar` on the outer backgrou
365
369
  |---|---|---|
366
370
  | `static` | Boolean | Switches from `position: fixed` to `position: relative` (useful for docs / previews) |
367
371
  | `floating` | Boolean | Pill-shaped Liquid Glass variant — adds `.glass-tab-bar--floating`. Use inside `<glk-tab-dock>` |
368
- | `droplet` | Boolean | The active item sits on a drop of glass that glides to the next one and can be dragged (since 1.27.0) — recommended; also a property |
372
+ | `droplet` | Boolean | The active item sits on a drop of glass that glides to the next one and can be dragged (since 1.27.0) — recommended; also a property, like `floating` (since 1.28.0: before, `bar.droplet = true` set an own property and changed nothing) |
369
373
 
370
374
  **`<glk-tab-item>`**
371
375
 
@@ -375,7 +379,24 @@ Fixed bottom navigation. Requires `.glass-bg--has-tab-bar` on the outer backgrou
375
379
  | `active` | Boolean | Active state — gets a soft radial Spotlight halo when inside a `floating` bar (since 1.27.0 really: before, the items of a floating bar kept the fixed bar's look), the drop when inside a `droplet` bar |
376
380
  | `badge` | String / Number | Numeric badge on the icon |
377
381
 
378
- Default slot: icon SVG. Events: `glk-tab-select` on click.
382
+ Icon: an `<svg>` child is copied into the shadow root. Anything else — a font icon such as Material Icons, an `<img>`, a custom element — goes into `slot="icon"` (since 1.28.0) and takes the state colour from GlassKit's icon box: inactive, hover, active, the lens of the drop and the drag. Give a font icon `aria-hidden="true"`, or its text becomes part of the tab's name. With a slotted icon no SVG is copied.
383
+
384
+ ```html
385
+ <glk-tab-item label="Home" active>
386
+ <span slot="icon" class="material-icons" aria-hidden="true">home</span>
387
+ </glk-tab-item>
388
+ ```
389
+
390
+ Events: `glk-tab-select` on the item, then `glk-tab-change` `{ tab }` on the bar. **`glk-tab-select` is cancelable (since 1.28.0)** and comes before `active` changes: `event.preventDefault()` keeps the current tab — for an item that opens a drawer or a link — so `active` stays, no `glk-tab-change` follows and the drop does not move. A drag of the drop that ends on such an item takes the same path, and the drop goes back. Before 1.28.0 the item was active already when the event arrived, and the bar had moved `active` and emitted `glk-tab-change` before a listener on the document could react.
391
+
392
+ ```js
393
+ menuItem.addEventListener('glk-tab-select', (event) => {
394
+ event.preventDefault(); // stays on the current tab
395
+ openDrawer();
396
+ });
397
+ ```
398
+
399
+ GlassKit 1.28.0's tab bar tokens reach the items by inheritance: `--gl-tab-bar-color` (inactive icon and label), `--gl-tab-bar-hover-color`, `--gl-tab-bar-label-size`, `--gl-tab-bar-z-index` (the fixed bar and `<glk-tab-dock>`).
379
400
 
380
401
  #### Floating variant + Accessory (`<glk-tab-dock>` + `<glk-tab-accessory>`)
381
402
 
@@ -417,7 +438,7 @@ iOS 26 Liquid Glass-style floating tab bar. Wrap a `<glk-tab-bar floating>` in a
417
438
  | `variant` | String | `accent`, `success`, `error` — filled colored capsule with white icon |
418
439
  | `disabled` | Boolean | Disabled state |
419
440
 
420
- Default slot: icon SVG (22×22). Events: `glk-click` (suppressed when disabled).
441
+ Default slot: icon SVG (22×22), or a font icon, which follows hover and the variants (GlassKit 1.28.0). Events: `glk-click` (suppressed when disabled).
421
442
 
422
443
  #### Droplet (since 1.27.0) — recommended
423
444
 
@@ -904,6 +925,8 @@ Closes automatically on overlay click and <kbd>Escape</kbd>, with its fade, and
904
925
 
905
926
  While it is open, **everything outside is inert** — a `<glk-toast>` shown then is neither clickable nor announced. Give feedback inside the modal, or close it first.
906
927
 
928
+ **The actions are copies** of the `<button>`s in `slot="actions"`; a click on one is handed to its original. Since 1.28.0 they follow every change of the originals, attributes included: a confirm button that starts `disabled` unlocks once the page or a framework enables it — no `refresh()` needed. With the same number of actions the copies change in place, so the focused one keeps the focus while it is relabelled or disabled. An inline `onclick` runs once per click, on the original; until 1.28.0 the copy carried it too and it ran twice.
929
+
907
930
  ---
908
931
 
909
932
  ### 3.20 `<glk-popover>`
@@ -1138,8 +1161,22 @@ A small, exclusive choice as one control (since 1.15.0) — traffic light, morni
1138
1161
  | `name` | String | Form field name |
1139
1162
  | `disabled` | Boolean | The whole row (since 1.22.3); a `<fieldset disabled>` around it disables it too |
1140
1163
  | `droplet` | Boolean | The chosen option sits on a drop of glass that glides to the next one and can be dragged (since 1.27.0) — recommended |
1164
+ | `icon-only` | Boolean | The options show their icons alone, square; each label becomes the button's `aria-label` and `title` (since 1.28.0) |
1165
+
1166
+ Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`, `disabled`, `droplet`, `iconOnly`. Slots: `icon-<value>` (since 1.28.0). Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.
1167
+
1168
+ **Icons (since 1.28.0).** Each option takes its icon from the slot `icon-<value>` — an `<svg>`, a font icon, an `<img>`. It sits in GlassKit's `.glass-segmented__icon` before the label, in the option's colour, and grows under the drop; an option without one has no icon box, and an icon added or removed later shows or hides it. The icon box is `aria-hidden`, so a font icon's text never joins the name. `icon-only` is for a row of icons alone — a light / dark / auto switch:
1169
+
1170
+ ```html
1171
+ <glk-segmented droplet icon-only label="Theme" value="dark"
1172
+ options='[{"value":"light","label":"Light"},{"value":"dark","label":"Dark"},{"value":"auto","label":"Auto"}]'>
1173
+ <svg slot="icon-light" viewBox="0 0 24 24"><!-- sun --></svg>
1174
+ <span slot="icon-dark" class="material-icons">dark_mode</span>
1175
+ <svg slot="icon-auto" viewBox="0 0 24 24"><!-- screen --></svg>
1176
+ </glk-segmented>
1177
+ ```
1141
1178
 
1142
- Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`, `disabled`, `droplet`. Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.
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.
1143
1180
 
1144
1181
  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:
1145
1182
 
@@ -1160,7 +1197,7 @@ The attribute is `overflow`, not `scroll`: `scroll` is a method of every element
1160
1197
 
1161
1198
  ### 3.26 `<glk-steps>`
1162
1199
 
1163
- Progress through a short flow (since 1.15.0), purely presentational: the current step carries `aria-current="step"`, done steps show an SVG check. The narrowing in tight frames is the CSS block's container query (below 360 px only the current label stays) — the element knows nothing about it, so in a flex row give it a width.
1200
+ Progress through a short flow (since 1.15.0), purely presentational: the current step carries `aria-current="step"`, done steps show an SVG check. The narrowing in tight frames is the CSS block's container query (below 360 px only the current label stays) — the element knows nothing about it, so in a flex row give it a width. Since GlassKit 1.28.0 the lines give way there first, the circles keep their size and the row never runs past its frame; a `::part(steps) { gap: … }` workaround can go.
1164
1201
 
1165
1202
  ```html
1166
1203
  <glk-steps steps="Day,Slot,Dog,Confirm" current="2" label="Progress"></glk-steps>
@@ -1457,7 +1494,7 @@ Key points:
1457
1494
  |---|---|---|---|---|
1458
1495
  | `<glk-button>` | — | `disabled` | `glk-click` | — |
1459
1496
  | `<glk-pill>` | — | `disabled` | `glk-click` | — |
1460
- | `<glk-tab-item>` | — | `active` | `glk-tab-select` | — |
1497
+ | `<glk-tab-item>` | — | `active` | `glk-tab-select` (cancelable since 1.28.0; `glk-tab-change` `{ tab }` follows on the bar) | — |
1461
1498
  | `<glk-tab-accessory>` | — | `disabled` | `glk-click` | — |
1462
1499
  | `<glk-input>` | `.value` | `value` | `glk-input`, `glk-change` (+ native `input`, `change`) | `{ value }` |
1463
1500
  | `<glk-textarea>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
@@ -1526,14 +1563,14 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
1526
1563
  |---|---|---|---|---|
1527
1564
  | `<glk-nav>` | Navigation | — | default | — |
1528
1565
  | `<glk-pill>` | Navigation | `label`, `disabled` | default (icon) | `glk-click` |
1529
- | `<glk-tab-bar>` | Navigation | `static`, `floating` | default | — |
1530
- | `<glk-tab-item>` | Navigation | `label`, `active`, `badge` | default (icon) | `glk-tab-select` |
1566
+ | `<glk-tab-bar>` | Navigation | `static`, `floating`, `droplet` | default | `glk-tab-change` |
1567
+ | `<glk-tab-item>` | Navigation | `label`, `active`, `badge` | `icon`; an unslotted `<svg>` is copied | `glk-tab-select` (cancelable) |
1531
1568
  | `<glk-tab-dock>` | Navigation | `accessory-left` | default | — |
1532
1569
  | `<glk-tab-accessory>` | Navigation | `label`, `variant`, `disabled` | default (icon) | `glk-click` |
1533
1570
  | `<glk-avatar>` | Content | `size`, `src` | default (initials) | — |
1534
1571
  | `<glk-badge>` | Content | `variant`, `interactive`, `selected` | default | `glk-click` (only when interactive) |
1535
1572
  | `<glk-empty>` | Content | `title`, `text` | default (icon), `action` | — |
1536
- | `<glk-segmented>` | Forms | `options`, `value`, `full`, `overflow`, `label`, `name` | — | `glk-change` |
1573
+ | `<glk-segmented>` | Forms | `options`, `value`, `full`, `overflow`, `droplet`, `icon-only`, `label`, `name` | `icon-<value>` | `glk-change` |
1537
1574
  | `<glk-steps>` | Navigation | `steps`, `current`, `label` | — | — |
1538
1575
  | `<glk-sheet>` | Feedback | `open`, `inline`, `title` | default, `actions` | `glk-close` |
1539
1576
  | `<glk-date-strip>` | Navigation | `start`, `days`, `value`, `today`, `marks`, `locale`, `label` | — | `glk-change` |
@@ -209,8 +209,9 @@ class GlkElement extends HTMLElement {
209
209
  /**
210
210
  * Opt in when the component copies light-DOM children into its shadow tree.
211
211
  * A MutationObserver then calls projectLightDom() again whenever those
212
- * children change, so a framework that swaps them keeps the rendered element
213
- * in step. Without it the copy is made once and silently goes stale.
212
+ * children change — their content and, since 1.28.0, their attributes — so
213
+ * a framework that swaps them or only binds `disabled` keeps the rendered
214
+ * element in step. Without it the copy is made once and silently goes stale.
214
215
  */
215
216
  static get observesLightDom() { return false; }
216
217
 
@@ -272,8 +273,13 @@ class GlkElement extends HTMLElement {
272
273
 
273
274
  if (this.constructor.observesLightDom) {
274
275
  this._lightDomObserver ??= new MutationObserver(records => this.projectLightDom(records));
276
+ // Attributes too: the copies show them. Until 1.27.0 a modal action
277
+ // that started disabled stayed disabled in its copy after the page
278
+ // enabled it, and the dialog could not be confirmed. The elements'
279
+ // own projectLightDom() compare a signature first, so a change
280
+ // elsewhere in the content costs a string comparison.
275
281
  this._lightDomObserver.observe(this, {
276
- childList: true, subtree: true, characterData: true
282
+ childList: true, subtree: true, characterData: true, attributes: true
277
283
  });
278
284
  }
279
285
  }
@@ -24,6 +24,33 @@ function reducedMotion() {
24
24
  return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
25
25
  }
26
26
 
27
+ // The copies carry no inline handlers (onclick="…"): a click on a copy is
28
+ // handed to its original, and until 1.28.0 such a handler ran twice.
29
+ const isHandler = name => name.startsWith('on');
30
+
31
+ function copyOf(node) {
32
+ const copy = node.cloneNode(true);
33
+ if (copy.nodeType === Node.ELEMENT_NODE) {
34
+ for (const el of [copy, ...copy.querySelectorAll('*')]) {
35
+ for (const { name } of [...el.attributes]) if (isHandler(name)) el.removeAttribute(name);
36
+ }
37
+ }
38
+ return copy;
39
+ }
40
+
41
+ /** Gives an action's copy the attributes and the content of its original. */
42
+ function mirror(copy, original) {
43
+ for (const { name } of [...copy.attributes]) {
44
+ if (!original.hasAttribute(name)) copy.removeAttribute(name);
45
+ }
46
+ for (const { name, value } of original.attributes) {
47
+ if (!isHandler(name) && copy.getAttribute(name) !== value) copy.setAttribute(name, value);
48
+ }
49
+ if (copy.innerHTML !== original.innerHTML) {
50
+ copy.replaceChildren(...[...original.childNodes].map(copyOf));
51
+ }
52
+ }
53
+
27
54
  class GlkModal extends GlkElement {
28
55
  static get observedAttributes() {
29
56
  return ['open', 'title'];
@@ -69,9 +96,18 @@ class GlkModal extends GlkElement {
69
96
  if (signature === this._footerSignature) return;
70
97
  this._footerSignature = signature;
71
98
 
99
+ // As many actions as copies: each copy takes its original's attributes
100
+ // and content in place, so the one that has the focus keeps it — a
101
+ // confirm button that the page enables, or relabels while it works.
102
+ const copies = [...this._footer.children];
103
+ if (copies.length === buttons.length) {
104
+ buttons.forEach((btn, i) => mirror(copies[i], btn));
105
+ return;
106
+ }
107
+
72
108
  this._footer.innerHTML = '';
73
109
  buttons.forEach((btn, i) => {
74
- const clone = btn.cloneNode(true);
110
+ const clone = copyOf(btn);
75
111
  // Look the original up again at click time instead of closing over it: a
76
112
  // framework that re-renders replaces the node, and a captured reference
77
113
  // would then forward the click to a button no longer in the document.
@@ -1,6 +1,6 @@
1
1
  import { GlkFormElement } from './base.js';
2
2
  import { r as revealCentered } from './shared/reveal-CjjzoxTa.js';
3
- import { a as attachDroplet } from './shared/glasskit-droplet-C3k1mxn4.js';
3
+ import { a as attachDroplet } from './shared/glasskit-droplet-3vJVXB3u.js';
4
4
  import '@jungherz-de/glasskit/glasskit-styles.js';
5
5
 
6
6
  // One button per option, the chosen one marked aria-pressed="true" — GlassKit
@@ -13,9 +13,16 @@ import '@jungherz-de/glasskit/glasskit-styles.js';
13
13
  // disabled (the whole row; since 1.22.3, as is a disabled
14
14
  // fieldset around it), droplet (since 1.27.0: the chosen option
15
15
  // sits on a drop of glass that glides to the next one and can be
16
- // dragged — GlassKit's .glass-segmented--droplet)
16
+ // dragged — GlassKit's .glass-segmented--droplet), icon-only
17
+ // (since 1.28.0: the options show their icons alone, each label
18
+ // becomes the button's aria-label and title — GlassKit's
19
+ // .glass-segmented--icons)
17
20
  // Properties: value, options (array or JSON text), full, overflow, label,
18
- // disabled, droplet
21
+ // disabled, droplet, iconOnly
22
+ // Slots: icon-<value> (since 1.28.0) — the icon of the option with that
23
+ // value: an <svg>, a font icon, an <img>. It sits in GlassKit's
24
+ // .glass-segmented__icon before the label and takes the option's
25
+ // colour; an option without one has no icon box.
19
26
  // Event: glk-change { value } — only on a change made by the user
20
27
  // Part: group
21
28
  //
@@ -44,7 +51,7 @@ function parseList(json) {
44
51
  }
45
52
 
46
53
  class GlkSegmented extends GlkFormElement {
47
- static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet']; }
54
+ static get observedAttributes() { return ['options', 'value', 'full', 'overflow', 'label', 'droplet', 'icon-only']; }
48
55
 
49
56
  // A row of buttons with one tab stop: delegated focus would land on the
50
57
  // first button, not the chosen one.
@@ -61,20 +68,39 @@ class GlkSegmented extends GlkFormElement {
61
68
 
62
69
  _build() {
63
70
  this._group.replaceChildren();
71
+ const iconOnly = this.iconOnly;
64
72
  for (const option of parseList(this.getAttribute('options'))) {
65
73
  const value = String(option.value ?? '');
74
+ const label = String(option.label ?? value);
66
75
  const button = this.createElement('button', ['glass-segmented__item'], { type: 'button', 'data-value': value });
67
76
  if (TONES.includes(option.tone)) {
68
77
  button.classList.add(`glass-segmented__item--${option.tone}`);
69
78
  button.appendChild(this.createElement('span', ['glass-segmented__dot']));
70
79
  }
71
- button.appendChild(document.createTextNode(option.label ?? value));
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
+ }
72
90
  this._group.appendChild(button);
73
91
  }
92
+ this._syncIcons();
74
93
  this.applyDisabled(this._actuallyDisabled);
75
94
  this._syncValue();
76
95
  }
77
96
 
97
+ /** An option shows its icon box only while an icon is slotted into it. */
98
+ _syncIcons() {
99
+ for (const slot of this._group.querySelectorAll('.glass-segmented__icon > slot')) {
100
+ slot.parentElement.hidden = slot.assignedElements().length === 0;
101
+ }
102
+ }
103
+
78
104
  /** The whole row is disabled, or an option by its own `disabled`. */
79
105
  applyDisabled(disabled) {
80
106
  const options = this.options;
@@ -96,6 +122,7 @@ class GlkSegmented extends GlkFormElement {
96
122
  const overflow = this.overflow;
97
123
  this._group.classList.toggle('glass-segmented--full', this.getBoolAttr('full'));
98
124
  this._group.classList.toggle('glass-segmented--droplet', this.getBoolAttr('droplet'));
125
+ this._group.classList.toggle('glass-segmented--icons', this.iconOnly);
99
126
  for (const mode of OVERFLOWS) this._group.classList.toggle(`glass-segmented--${mode}`, overflow === mode);
100
127
  this._reveal('instant');
101
128
  }
@@ -122,14 +149,18 @@ class GlkSegmented extends GlkFormElement {
122
149
  this.emit('glk-change', { value });
123
150
  };
124
151
  this._onResize = () => { if (this._group.clientWidth !== this._revealedAt) this._reveal('instant'); };
152
+ this._onSlotChange = () => this._syncIcons();
125
153
  this._group.addEventListener('click', this._onClick);
154
+ this._group.addEventListener('slotchange', this._onSlotChange);
126
155
  this._resizeObserver = new ResizeObserver(this._onResize);
127
156
  this._resizeObserver.observe(this._group);
157
+ this._syncIcons();
128
158
  this._applyDroplet();
129
159
  }
130
160
 
131
161
  teardownEvents() {
132
162
  this._group?.removeEventListener('click', this._onClick);
163
+ this._group?.removeEventListener('slotchange', this._onSlotChange);
133
164
  this._resizeObserver?.disconnect();
134
165
  this._resizeObserver = null;
135
166
  this._droplet?.destroy();
@@ -152,6 +183,7 @@ class GlkSegmented extends GlkFormElement {
152
183
  case 'full':
153
184
  case 'overflow': this._applyLayout(); break;
154
185
  case 'droplet': this._applyLayout(); this._applyDroplet(); break;
186
+ case 'icon-only': this._build(); this._applyLayout(); break;
155
187
  case 'label': this._applyLabel(); break;
156
188
  }
157
189
  }
@@ -174,6 +206,9 @@ class GlkSegmented extends GlkFormElement {
174
206
  get droplet() { return this.getBoolAttr('droplet'); }
175
207
  set droplet(v) { this.setBoolAttr('droplet', v); }
176
208
 
209
+ get iconOnly() { return this.getBoolAttr('icon-only'); }
210
+ set iconOnly(v) { this.setBoolAttr('icon-only', v); }
211
+
177
212
  get overflow() {
178
213
  const v = this.getAttribute('overflow');
179
214
  return OVERFLOWS.includes(v) ? v : '';
@@ -1,5 +1,5 @@
1
1
  import { GlkElement } from './base.js';
2
- import { a as attachDroplet } from './shared/glasskit-droplet-C3k1mxn4.js';
2
+ import { a as attachDroplet } from './shared/glasskit-droplet-3vJVXB3u.js';
3
3
  import '@jungherz-de/glasskit/glasskit-styles.js';
4
4
 
5
5
  // droplet (since 1.27.0): the active item sits on a drop of glass that glides
@@ -83,20 +83,20 @@ class GlkTabBar extends GlkElement {
83
83
  this._syncItems();
84
84
  }
85
85
 
86
- setupEvents() {
87
- // Listen for tab selection events from child glk-tab-item elements
88
- this._onTabSelect = (e) => {
89
- // Deactivate all other tabs
90
- const items = this.querySelectorAll('glk-tab-item');
91
- items.forEach(item => {
92
- if (item !== e.target) {
93
- item.removeAttribute('active');
94
- }
95
- });
96
- this.emit('glk-tab-change', { tab: e.target });
97
- };
98
- this.addEventListener('glk-tab-select', this._onTabSelect);
86
+ /**
87
+ * Called by an item the user selected once its glk-tab-select went through
88
+ * uncancelled; the item is active already. Until 1.27.0 the bar did this
89
+ * while the event was still on its way, so a listener further up could not
90
+ * have kept the current tab.
91
+ */
92
+ _selected(tab) {
93
+ for (const item of this.querySelectorAll('glk-tab-item')) {
94
+ if (item !== tab) item.removeAttribute('active');
95
+ }
96
+ this.emit('glk-tab-change', { tab });
97
+ }
99
98
 
99
+ setupEvents() {
100
100
  // The drop's state lives on the nav; the items carry it on their wrappers.
101
101
  this._navObserver = new MutationObserver(() => this._syncItems());
102
102
  this._navObserver.observe(this._nav, { attributes: true, attributeFilter: ['data-droplet', 'data-droplet-drag'] });
@@ -107,12 +107,19 @@ class GlkTabBar extends GlkElement {
107
107
  }
108
108
 
109
109
  teardownEvents() {
110
- this.removeEventListener('glk-tab-select', this._onTabSelect);
111
110
  this._navObserver?.disconnect();
112
111
  this._itemObserver?.disconnect();
113
112
  this._droplet?.destroy();
114
113
  this._droplet = null;
115
114
  }
115
+
116
+ // Properties as well (since 1.28.0): `bar.droplet = true` set an own
117
+ // property before and changed nothing.
118
+ get floating() { return this.getBoolAttr('floating'); }
119
+ set floating(v) { this.setBoolAttr('floating', v); }
120
+
121
+ get droplet() { return this.getBoolAttr('droplet'); }
122
+ set droplet(v) { this.setBoolAttr('droplet', v); }
116
123
  }
117
124
 
118
125
  customElements.define('glk-tab-bar', GlkTabBar);
@@ -1,6 +1,19 @@
1
1
  import { GlkElement } from './base.js';
2
2
  import '@jungherz-de/glasskit/glasskit-styles.js';
3
3
 
4
+ // The icon: an inline <svg> child is copied into the shadow root, where
5
+ // GlassKit's rules reach it. Anything else — a font icon such as Material
6
+ // Icons, an <img>, an element of the page's own — goes into slot="icon"
7
+ // (since 1.28.0) and takes the state colour from GlassKit's icon box: active,
8
+ // hover, the lens of the drop and the drag. With a slotted icon no SVG is
9
+ // copied.
10
+ //
11
+ // glk-tab-select (cancelable since 1.28.0) comes before `active` changes:
12
+ // preventDefault() keeps the current tab — for an item that opens a drawer
13
+ // or a link — and the bar then neither moves `active` nor its drop, nor
14
+ // emits glk-tab-change. A drag of the drop that ends on the item clicks it,
15
+ // so it takes the same path.
16
+
4
17
  class GlkTabItem extends GlkElement {
5
18
  static get observedAttributes() {
6
19
  return ['label', 'active', 'badge'];
@@ -12,8 +25,10 @@ class GlkTabItem extends GlkElement {
12
25
  this._btn = this.createElement('button', ['glass-tab-bar__item']);
13
26
  if (this.getBoolAttr('active')) this._btn.classList.add('is-active');
14
27
 
15
- // Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies
28
+ // Icon — clone SVG from light DOM into shadow DOM so GlassKit CSS applies,
29
+ // or show what is slotted as icon
16
30
  this._iconEl = this.createElement('span', ['glass-tab-bar__icon']);
31
+ this._iconEl.appendChild(this.createElement('slot', [], { name: 'icon' }));
17
32
 
18
33
  // Label
19
34
  this._labelEl = this.createElement('span', ['glass-tab-bar__label']);
@@ -37,7 +52,8 @@ class GlkTabItem extends GlkElement {
37
52
  }
38
53
 
39
54
  projectLightDom() {
40
- const svg = this.querySelector('svg');
55
+ // A slotted icon wins; only without one is an <svg> copied.
56
+ const svg = this.querySelector(':scope > [slot="icon"]') ? null : this.querySelector('svg');
41
57
  const signature = svg ? svg.outerHTML : '';
42
58
  if (signature === this._iconSignature) return;
43
59
  this._iconSignature = signature;
@@ -63,8 +79,9 @@ class GlkTabItem extends GlkElement {
63
79
 
64
80
  setupEvents() {
65
81
  this._onClick = () => {
82
+ if (!this.emit('glk-tab-select', null, { cancelable: true })) return;
66
83
  this.setAttribute('active', '');
67
- this.emit('glk-tab-select');
84
+ this.closest('glk-tab-bar')?._selected?.(this);
68
85
  };
69
86
  this._btn.addEventListener('click', this._onClick);
70
87
  this._tellBar();