@jungherz-de/glasskit-elements 1.18.0 → 1.19.1

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/SKILL.md ADDED
@@ -0,0 +1,1555 @@
1
+ ---
2
+ name: glasskit-elements
3
+ description: GlassKit Elements is a vanilla-JS Web Components library (v1.19.1) wrapping GlassKit CSS v1.19.1. 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
+ ---
5
+
6
+ # GlassKit Elements – AI Component Reference
7
+
8
+ > **Purpose:** AI-optimized reference for generating correct `<glk-*>` markup.
9
+ > Companion to the class-based `SKILL.md` in [`@jungherz-de/glasskit`](https://github.com/JUNGHERZ/GlassKit) — the element wrappers delegate visuals and tokens to GlassKit CSS, so both references are best used together.
10
+
11
+ ---
12
+
13
+ ## 1. Setup & Boilerplate
14
+
15
+ ### Installation
16
+
17
+ ```bash
18
+ npm install @jungherz-de/glasskit-elements @jungherz-de/glasskit
19
+ ```
20
+
21
+ Peer dependency `@jungherz-de/glasskit >=1.19.1` is required — 1.9.0 is the release that made the stylesheet splittable, which is what lets document-level branding reach the elements at all.
22
+
23
+ ### Import (ES modules)
24
+
25
+ ```js
26
+ // Full bundle — registers all 36 elements
27
+ import '@jungherz-de/glasskit-elements';
28
+
29
+ // Named imports (for direct references to constructor classes)
30
+ import { GlkButton, GlkModal } from '@jungherz-de/glasskit-elements';
31
+
32
+ // Per-component import (since 1.7.0) — registers just this element
33
+ import '@jungherz-de/glasskit-elements/components/glk-button.js';
34
+
35
+ // …or with the constructor class
36
+ import { GlkButton } from '@jungherz-de/glasskit-elements/components/glk-button.js';
37
+ ```
38
+
39
+ Every element has its own entry at `components/glk-{name}.js`; they import `./base.js` and leave `@jungherz-de/glasskit/glasskit-styles.js` external (bundler or import map resolves it). Importing one pulls that
40
+ file plus a single shared chunk (`base.js` + the GlassKit stylesheet, ~48 KB) rather than
41
+ the 112 KB full bundle; several imports share that chunk. Each module registers its
42
+ custom element as a side effect, so a bare `import '…'` is enough.
43
+
44
+ ### CDN
45
+
46
+ ```html
47
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
48
+ ```
49
+
50
+ Note: the elements bundle the GlassKit component rules via `adoptedStyleSheets`, and put the token defaults on the document themselves. You do **not** need to load `glasskit.css` separately — but do load it if you also use plain `.glass-*` markup outside the elements.
51
+
52
+ ### Minimal template
53
+
54
+ ```html
55
+ <!DOCTYPE html>
56
+ <html lang="en" data-theme="dark">
57
+ <head>
58
+ <meta charset="UTF-8">
59
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
61
+ </head>
62
+ <body>
63
+ <div class="glass-bg">
64
+ <glk-title>Hello GlassKit</glk-title>
65
+ <glk-card>
66
+ <p>Welcome!</p>
67
+ </glk-card>
68
+ </div>
69
+ </body>
70
+ </html>
71
+ ```
72
+
73
+ The outer `.glass-bg` class (from GlassKit CSS) still provides the aurora background. Inside the shadow roots, each element carries its own glass styling.
74
+
75
+ ### Theme
76
+
77
+ ```html
78
+ <html data-theme="dark"> <!-- or "light" -->
79
+ ```
80
+
81
+ ```js
82
+ function toggleTheme() {
83
+ const html = document.documentElement;
84
+ const current = html.getAttribute('data-theme') || 'dark';
85
+ html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
86
+ }
87
+ ```
88
+
89
+ A single module-level `MutationObserver` watches `data-theme` on `<html>` and syncs every live GlassKit Element instance — you never set `data-theme` on individual elements.
90
+
91
+ ---
92
+
93
+ ## 2. Core Concepts
94
+
95
+ | Concept | Summary |
96
+ |---|---|
97
+ | Tag prefix | `glk-*` (analogous to the `glass-*` CSS prefix) |
98
+ | Rendering | Shadow DOM (`mode: 'open'`) with `adoptedStyleSheets` |
99
+ | Stylesheet sharing | GlassKit's `componentsSheet` is the same `CSSStyleSheet` object in every element — no CSS duplication. Token declarations live on the document, not in the shadow roots |
100
+ | Theme sync | Global MutationObserver on `<html data-theme>` |
101
+ | Events | Custom `glk-*` events, all `bubbles: true, composed: true` |
102
+ | Form participation | `GlkFormElement` uses `ElementInternals` (`static formAssociated = true`) |
103
+ | API style | Declarative HTML attributes + reflected JS properties |
104
+
105
+ Custom properties (`--gl-*`) defined on `:root` or `<html>` pass through shadow boundaries by inheritance, so custom theming works with a single global stylesheet.
106
+
107
+ ### Building your own element (since 1.14.0)
108
+
109
+ `GlkElement` and `GlkFormElement` are exported — from the bundle (`import { GlkElement } from '@jungherz-de/glasskit-elements'`), from the stable subpath `@jungherz-de/glasskit-elements/base.js` (the very module the components import, so `instanceof GlkElement` holds across both), and as `GlassKitElements.GlkElement` from the CDN `<script>` bundle. A subclass inherits the whole setup: open shadow root with GlassKit's stylesheet adopted (`.glass-*` classes work inside), the theme wrapper following `data-theme`, listeners re-armed when the element moves, `emit()` for bubbling, composed events.
110
+
111
+ ```js
112
+ class DemoCounter extends GlkElement {
113
+ static get observedAttributes() { return ['count']; }
114
+ render() { // build into this._wrapper
115
+ this._btn = this.createElement('button', ['glass-btn', 'glass-btn--secondary', 'glass-btn--sm', 'glass-btn--auto']);
116
+ this._wrapper.appendChild(this._btn);
117
+ }
118
+ setupEvents() { // runs on every connect
119
+ this._onClick = () => { this.count += 1; this.emit('demo-count', { count: this.count }); };
120
+ this._btn.addEventListener('click', this._onClick);
121
+ }
122
+ teardownEvents() { this._btn?.removeEventListener('click', this._onClick); } // every disconnect
123
+ onAttributeChanged() { this._btn.textContent = `Clicked ${this.count}×`; } // after first render
124
+ get count() { return Number(this.getAttribute('count')) || 0; }
125
+ set count(v) { this.setAttribute('count', v); }
126
+ }
127
+ customElements.define('demo-counter', DemoCounter);
128
+ ```
129
+
130
+ Which import to take depends on how the elements are loaded, and mixing them is the one mistake to avoid: the `<script>` / ESM **bundle** carries its own copy of `GlkElement`, so a subclass built on `base.js` next to it is a different class — `instanceof` fails across the two, and the GlassKit stylesheet lives twice. Bundle loaded → take `GlassKitElements.GlkElement` or `import { GlkElement } from '@jungherz-de/glasskit-elements'`. Per-component files loaded → `base.js`, which the components import themselves. The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` external, so a bundler resolves it from `node_modules` and an import-map project maps it to the one copy it already loads for its own elements.
131
+
132
+ Rules for a subclass: build only inside `this._wrapper` (it carries `data-theme`); keep listeners in `setupEvents()` / `teardownEvents()`, never in `render()`, or a moved element loses them; use `emit()` instead of `dispatchEvent()` so the event bubbles and crosses the shadow boundary — `emit(name, detail, { cancelable: true })` lets a listener call `preventDefault()`, and `emit()` returns `false` then (since 1.19.0); return `true` from `static get displayInline()` for inline elements; set `static get observesLightDom()` to `true` and implement `projectLightDom()` when copying light-DOM children into the shadow tree. `GlkFormElement` adds `setFormValue()`, `setValidity()`, `resetValue()` / `restoreValue()` and `static formAssociated = true`.
133
+
134
+ ### Light-DOM children (since 1.12.0)
135
+
136
+ Most elements project children through a real `<slot>`, which is live by definition.
137
+ Three copy them into the shadow tree instead, because GlassKit's CSS has to reach
138
+ them: `<glk-select>` (its `<option>`s), `<glk-tab-item>` (its `<svg>`) and
139
+ `<glk-modal>` (its `[slot="actions"]` buttons).
140
+
141
+ Those copies are now kept in step by a `MutationObserver`, so swapping the children
142
+ — by hand or through a framework — updates the rendered element. Before 1.12.0 the
143
+ copy was made once on the first frame and then silently went stale.
144
+
145
+ If you ever need to force it, `element.refresh()` re-runs the copy immediately.
146
+ There should be no reason to reach into `element.shadowRoot`.
147
+
148
+ ### Moving an element (since 1.12.0)
149
+
150
+ Moving a `<glk-*>` element in the DOM disconnects and reconnects it. Its event
151
+ listeners and its theme-sync registration are re-armed on every connect, so it keeps
152
+ working. Before 1.12.0 they were torn down on disconnect and never restored: the
153
+ element still looked right but no longer fired events.
154
+
155
+ ### Branding (since 1.9.0)
156
+
157
+ Override `--gl-*` tokens anywhere in the document — a brand stylesheet, a `<style>` block,
158
+ inline on `<html>`. Every `<glk-*>` element inherits them:
159
+
160
+ ```css
161
+ /* brand.css, loaded after glasskit.css */
162
+ :root, [data-theme='dark'] { --gl-color-primary: #2e9e8f; }
163
+ [data-theme='light'] { --gl-color-primary: #21786d; }
164
+ ```
165
+
166
+ No JavaScript, no `::part`, no per-element setup. Both `.glass-btn--primary` in the
167
+ document and `<glk-button variant="primary">` end up the same color.
168
+
169
+ **This did not work before 1.9.0.** Up to and including 1.8.0 every element adopted the
170
+ full GlassKit sheet, whose `[data-theme]` blocks matched the element's own theme wrapper
171
+ and re-declared all tokens inside the shadow root. A matching rule beats an inherited
172
+ value, so document-level overrides were silently ignored inside every component — while
173
+ the same override *did* apply to plain `.glass-*` markup, which made projects look
174
+ half-branded. Since 1.9.0 the elements adopt only the component rules.
175
+
176
+ **How the defaults get there.** Because the tokens no longer live in the shadow roots,
177
+ this module puts them on the document once, wrapped in `@layer glasskit-defaults`:
178
+
179
+ - Pages that never load `glasskit.css` still get styled elements, as advertised above.
180
+ - The layer loses to any ordinary stylesheet, so your brand file wins regardless of
181
+ load order — and so does a linked `glasskit.css`, at identical values.
182
+ - It is appended to `document.adoptedStyleSheets`, never assigned, and only once even if
183
+ several copies of the bundle are loaded.
184
+
185
+ If your app assigns `document.adoptedStyleSheets = [...]` wholesale at some later point,
186
+ re-append the existing entries rather than replacing them, or the defaults are lost.
187
+
188
+ ### Passing icons (fixed in 1.10.0)
189
+
190
+ Pass an icon as a **direct child** — the `<svg>` itself, not wrapped:
191
+
192
+ ```html
193
+ <glk-button><svg viewBox="0 0 24 24">…</svg>Save</glk-button>
194
+ <glk-list-item title="Download"><svg slot="leading" viewBox="0 0 24 24">…</svg></glk-list-item>
195
+ ```
196
+
197
+ GlassKit sizes and strokes it. Requires GlassKit CSS >= 1.10.0: the icon rules are
198
+ descendant selectors (`.glass-btn svg`), and a slotted icon stays in the light DOM, so it
199
+ is no descendant of the shadow tree. 1.10.0 added a `::slotted()` twin to every such rule.
200
+ Before that, icons came out at their intrinsic size — `glk-button` icons filled the whole
201
+ container, list and status icons collapsed to 0×0.
202
+
203
+ **Wrapping the icon does not work and cannot be made to work:**
204
+
205
+ ```html
206
+ <!-- the assigned node is the <span>; the <svg> inside is unreachable from the shadow root -->
207
+ <glk-list-item title="…"><span slot="leading"><svg …></svg></span></glk-list-item>
208
+ ```
209
+
210
+ `::slotted()` only ever matches the assigned node, never inside it. If you must wrap,
211
+ size the icon yourself in your own stylesheet.
212
+
213
+ Your own rules keep precedence: for slotted content the outer tree wins over `::slotted()`
214
+ from the shadow tree, so `glk-list-item svg { width: 32px }` in your CSS still applies.
215
+ Note that `.glass-list__leading` is a fixed 28×28 flex container — enlarging only the icon
216
+ shrinks it back on the main axis; size the container too.
217
+
218
+ ### CSS Parts (since 1.8.0)
219
+
220
+ Custom properties cover colors, but not layout. For that, the structural elements of the
221
+ container components are exposed as parts:
222
+
223
+ | Element | Parts |
224
+ |---|---|
225
+ | `<glk-card>` | `card` |
226
+ | `<glk-list>` | `header`, `list` |
227
+ | `<glk-modal>` | `overlay`, `modal`, `header`, `body`, `footer` |
228
+
229
+ ```css
230
+ glk-card::part(card) { border-radius: 8px; }
231
+ glk-modal::part(footer) { justify-content: flex-start; }
232
+ ```
233
+
234
+ Note that `height: 100%` on a part only resolves if the host gives it something definite
235
+ to resolve against. A host stretched by a grid keeps `height: auto`, which is why
236
+ `<glk-card fill>` makes the host a grid instead.
237
+
238
+ ---
239
+
240
+ ## 3. Element Catalog (36 elements)
241
+
242
+ ### 3.1 `<glk-nav>`
243
+
244
+ Horizontal navigation bar — flex container. Typically holds `<glk-pill>` buttons.
245
+
246
+ ```html
247
+ <glk-nav>
248
+ <glk-pill label="Back" onclick="history.back()">
249
+ <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
250
+ </glk-pill>
251
+ <glk-pill label="Settings">
252
+ <svg viewBox="0 0 24 24">...</svg>
253
+ </glk-pill>
254
+ </glk-nav>
255
+ ```
256
+
257
+ No attributes. Default slot for children.
258
+
259
+ ---
260
+
261
+ ### 3.2 `<glk-pill>`
262
+
263
+ Circular icon button (46×46 px).
264
+
265
+ ```html
266
+ <glk-pill label="Back">
267
+ <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
268
+ </glk-pill>
269
+ ```
270
+
271
+ | Attribute | Type | Description |
272
+ |---|---|---|
273
+ | `label` | String | `aria-label` for accessibility |
274
+ | `disabled` | Boolean | Disabled state |
275
+
276
+ Default slot: icon SVG (24×24).
277
+
278
+ Events: `glk-click` when clicked.
279
+
280
+ ---
281
+
282
+ ### 3.3 `<glk-tab-bar>` + `<glk-tab-item>`
283
+
284
+ Fixed bottom navigation. Requires `.glass-bg--has-tab-bar` on the outer background to reserve padding.
285
+
286
+ ```html
287
+ <div class="glass-bg glass-bg--has-tab-bar">
288
+ <!-- page content -->
289
+ <glk-tab-bar>
290
+ <glk-tab-item label="Home" active>
291
+ <svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
292
+ </glk-tab-item>
293
+ <glk-tab-item label="Search">
294
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/></svg>
295
+ </glk-tab-item>
296
+ <glk-tab-item label="Profile" badge="3">
297
+ <svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>
298
+ </glk-tab-item>
299
+ </glk-tab-bar>
300
+ </div>
301
+ ```
302
+
303
+ **`<glk-tab-bar>`** — container.
304
+
305
+ | Attribute | Type | Description |
306
+ |---|---|---|
307
+ | `static` | Boolean | Switches from `position: fixed` to `position: relative` (useful for docs / previews) |
308
+ | `floating` | Boolean | Pill-shaped Liquid Glass variant — adds `.glass-tab-bar--floating`. Use inside `<glk-tab-dock>` |
309
+
310
+ **`<glk-tab-item>`**
311
+
312
+ | Attribute | Type | Description |
313
+ |---|---|---|
314
+ | `label` | String | Tab label |
315
+ | `active` | Boolean | Active state — gets a soft radial Spotlight halo when inside a `floating` bar |
316
+ | `badge` | String / Number | Numeric badge on the icon |
317
+
318
+ Default slot: icon SVG. Events: `glk-tab-select` on click.
319
+
320
+ #### Floating variant + Accessory (`<glk-tab-dock>` + `<glk-tab-accessory>`)
321
+
322
+ iOS 26 Liquid Glass-style floating tab bar. Wrap a `<glk-tab-bar floating>` in a `<glk-tab-dock>`, optionally with a `<glk-tab-accessory>` (search, compose…) capsule next to it. Use `.glass-bg--has-tab-bar-floating` on the outer background to reserve padding.
323
+
324
+ ```html
325
+ <div class="glass-bg glass-bg--has-tab-bar-floating">
326
+ <!-- page content -->
327
+ <glk-tab-dock>
328
+ <glk-tab-bar floating>
329
+ <glk-tab-item label="Home" active>
330
+ <svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
331
+ </glk-tab-item>
332
+ <glk-tab-item label="Docs" badge="3">
333
+ <svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/></svg>
334
+ </glk-tab-item>
335
+ <glk-tab-item label="Upload">
336
+ <svg viewBox="0 0 24 24"><path d="M12 16V4"/><polyline points="8 8 12 4 16 8"/></svg>
337
+ </glk-tab-item>
338
+ </glk-tab-bar>
339
+ <glk-tab-accessory variant="accent" label="Compose">
340
+ <svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
341
+ </glk-tab-accessory>
342
+ </glk-tab-dock>
343
+ </div>
344
+ ```
345
+
346
+ **`<glk-tab-dock>`** — wrapper that holds the floating bar + optional accessory.
347
+
348
+ | Attribute | Type | Description |
349
+ |---|---|---|
350
+ | `accessory-left` | Boolean | Place the accessory on the left of the bar (default is right) |
351
+
352
+ **`<glk-tab-accessory>`** — standalone glass capsule next to the bar (e.g. search, compose). 56×56 px.
353
+
354
+ | Attribute | Type | Description |
355
+ |---|---|---|
356
+ | `label` | String | `aria-label` for accessibility |
357
+ | `variant` | String | `accent`, `success`, `error` — filled colored capsule with white icon |
358
+ | `disabled` | Boolean | Disabled state |
359
+
360
+ Default slot: icon SVG (22×22). Events: `glk-click` (suppressed when disabled).
361
+
362
+ ---
363
+
364
+ ### 3.4 `<glk-avatar>`
365
+
366
+ Inline avatar circle.
367
+
368
+ ```html
369
+ <glk-avatar size="lg" src="/user.jpg"></glk-avatar>
370
+ <glk-avatar>MJ</glk-avatar>
371
+ ```
372
+
373
+ | Attribute | Type | Description |
374
+ |---|---|---|
375
+ | `size` | String | `sm`, `md` (default), `lg` |
376
+ | `src` | String | Image URL; if omitted, text content is shown as initials |
377
+
378
+ ---
379
+
380
+ ### 3.5 `<glk-badge>`
381
+
382
+ Inline status badge — and, with `interactive`, a filter chip.
383
+
384
+ ```html
385
+ <glk-badge>Default</glk-badge>
386
+ <glk-badge variant="primary">Active</glk-badge>
387
+ <glk-badge variant="success">Done</glk-badge>
388
+ <glk-badge variant="warning">Pending</glk-badge>
389
+ <glk-badge variant="error">Failed</glk-badge>
390
+ ```
391
+
392
+ | Attribute | Type | Description |
393
+ |---|---|---|
394
+ | `variant` | String | `primary`, `success`, `warning` (since 1.18.0), `error`; any other value gives the neutral badge |
395
+ | `interactive` | Boolean | Renders the badge as a real `<button>`: pointer cursor, hover tint, focus ring, keyboard operable. Enables `glk-click`. |
396
+ | `selected` | Boolean | Marks the chip that is on. Mirrored to `aria-pressed` on the button. |
397
+
398
+ Default slot: label text. Events: `glk-click` (only when `interactive`).
399
+
400
+ **Filter chips (since 1.13.0, needs GlassKit 1.12.0).** A row of chips where one is picked: listen for `glk-click` on the row and move `selected`. The element keeps `aria-pressed` in step, so the row is announced as a group of toggles.
401
+
402
+ ```html
403
+ <div id="filter">
404
+ <glk-badge interactive selected>Active</glk-badge>
405
+ <glk-badge interactive>Applied</glk-badge>
406
+ <glk-badge interactive>Paused</glk-badge>
407
+ </div>
408
+ <script>
409
+ filter.addEventListener('glk-click', e => {
410
+ for (const chip of filter.querySelectorAll('glk-badge')) chip.selected = chip === e.target;
411
+ });
412
+ </script>
413
+ ```
414
+
415
+ `selected` deepens the colour the badge already has: a plain chip turns primary, `<glk-badge variant="success" interactive selected>` stays green. `interactive` can be toggled at runtime — the element swaps between `<span>` and `<button>` underneath, the slotted label stays put. Without `interactive`, clicks do not emit `glk-click`, exactly like `<glk-list-item>`.
416
+
417
+ ---
418
+
419
+ ### 3.6 `<glk-card>`
420
+
421
+ Glass container for content.
422
+
423
+ ```html
424
+ <glk-card>
425
+ <p>Standard card content.</p>
426
+ </glk-card>
427
+
428
+ <glk-card glow>
429
+ <p>Glowing card variant with light streak.</p>
430
+ </glk-card>
431
+ ```
432
+
433
+ | Attribute | Type | Description |
434
+ |---|---|---|
435
+ | `glow` | Boolean | Adds frosted glass gradient + light streak |
436
+ | `fill` | Boolean | Stretches the card to the full height of its grid or flex cell (since 1.8.0); the card yields to its cell, so nowrap content is truncated instead of widening it (since 1.15.2) |
437
+
438
+ Default slot: content. CSS part: `card`.
439
+
440
+ **Equal-height tiles.** In a grid the host stretches but the visible card keeps its
441
+ natural height, so tiles end up ragged and footer buttons sit at different heights.
442
+ Add `fill`:
443
+
444
+ ```html
445
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px">
446
+ <glk-card fill>
447
+ <div style="display:flex;flex-direction:column;height:100%">
448
+ <h3>Title</h3>
449
+ <p>Short.</p>
450
+ <div style="margin-top:auto"><glk-button variant="primary">Action</glk-button></div>
451
+ </div>
452
+ </glk-card>
453
+ <glk-card fill>…</glk-card>
454
+ </div>
455
+ ```
456
+
457
+ `fill` makes the card a flex column, so `margin-top: auto` on the last child pushes a
458
+ footer down and buttons line up across tiles. Give your own wrapper `height: 100%` as
459
+ shown — otherwise it sizes to its content inside the stretched card.
460
+
461
+ **The card yields to its cell (since 1.15.2).** `fill` makes the card a grid item of the host, and a grid item's `min-width: auto` is its min-content width — a nowrap subtitle in a `<glk-list-item>` used to widen the card past its cell instead of getting its ellipsis. The card and the host now carry `min-width: 0`, and the card `min-height: 0`, so long nowrap content truncates and a scrollable child (`overflow: auto`) inside a cell of fixed height shrinks and scrolls. Content that can neither wrap nor shrink overflows the card's edge rather than pushing the card out of the cell. A plain `<glk-card>` is unchanged: as a block in a `1fr` track it keeps `min-width: auto` like any block, so give it `min-width: 0` yourself when it holds nowrap content.
462
+
463
+ ---
464
+
465
+ ### 3.7 `<glk-divider>`
466
+
467
+ Horizontal separator with gradient fade.
468
+
469
+ ```html
470
+ <glk-divider></glk-divider>
471
+ ```
472
+
473
+ No attributes, no content.
474
+
475
+ ---
476
+
477
+ ### 3.8 `<glk-status>`
478
+
479
+ Inline status notice (muted surface).
480
+
481
+ ```html
482
+ <glk-status message="No results found"></glk-status>
483
+ ```
484
+
485
+ | Attribute | Type | Description |
486
+ |---|---|---|
487
+ | `message` | String | Status text |
488
+
489
+ ---
490
+
491
+ ### 3.9 `<glk-title>`
492
+
493
+ Page title with text shadow.
494
+
495
+ ```html
496
+ <glk-title>Settings</glk-title>
497
+ ```
498
+
499
+ No attributes. Default slot: heading text.
500
+
501
+ ---
502
+
503
+ ### 3.10 `<glk-button>`
504
+
505
+ Full-width glass button (56 px). Three variants, four sizes.
506
+
507
+ ```html
508
+ <glk-button variant="primary">Save</glk-button>
509
+ <glk-button variant="secondary">Cancel</glk-button>
510
+ <glk-button variant="tertiary">Learn more</glk-button>
511
+
512
+ <glk-button variant="primary" size="sm">Small</glk-button>
513
+ <glk-button variant="primary" size="lg">Large</glk-button>
514
+ <glk-button variant="primary" size="auto">Auto width</glk-button>
515
+
516
+ <glk-button variant="primary" disabled>Disabled</glk-button>
517
+ ```
518
+
519
+ | Attribute | Type | Default | Description |
520
+ |---|---|---|---|
521
+ | `variant` | String | — | `primary`, `secondary`, `tertiary` |
522
+ | `size` | String | `md` | `sm`, `md`, `lg`, `auto` |
523
+ | `disabled` | Boolean | `false` | Disabled state |
524
+ | `type` | String | `button` | Native button type |
525
+
526
+ Events: `glk-click` (suppressed when disabled).
527
+
528
+ Default slot: button label (text or SVG + text).
529
+
530
+ ---
531
+
532
+ ### 3.11 `<glk-checkbox>`
533
+
534
+ Form-associated checkbox with glass styling.
535
+
536
+ ```html
537
+ <glk-checkbox label="I agree to the terms" checked></glk-checkbox>
538
+ <glk-checkbox label="Subscribe to newsletter"></glk-checkbox>
539
+ ```
540
+
541
+ | Attribute | Type | Description |
542
+ |---|---|---|
543
+ | `label` | String | Label text |
544
+ | `checked` | Boolean | Checked state |
545
+ | `disabled` | Boolean | Disabled state |
546
+ | `name` | String | Form field name |
547
+ | `value` | String | Value submitted when checked (default `"on"`) |
548
+
549
+ Events: `glk-change` → `{ checked }`; also dispatches native `change`. Property: `.checked`.
550
+
551
+ ---
552
+
553
+ ### 3.12 `<glk-input>`
554
+
555
+ Text input with label + hint.
556
+
557
+ ```html
558
+ <glk-input label="Email" type="email" placeholder="you@example.com" hint="We won't share."></glk-input>
559
+ <glk-input label="Password" type="password" required></glk-input>
560
+ <glk-input label="Broken field" error hint="This field is required."></glk-input>
561
+ ```
562
+
563
+ | Attribute | Type | Description |
564
+ |---|---|---|
565
+ | `label` | String | Label above the input |
566
+ | `type` | String | Native input type (`text`, `email`, `password`, `number`, …) |
567
+ | `placeholder` | String | Placeholder text |
568
+ | `hint` | String | Helper text below the input |
569
+ | `error` | Boolean | Error styling (red border + red hint) |
570
+ | `disabled` | Boolean | Disabled state |
571
+ | `required` | Boolean | Required field |
572
+ | `name` | String | Form field name |
573
+ | `value` | String | Current value |
574
+ | `min`, `max`, `step` | String | Passed to the inner field — range and step of the picker for dates, times and numbers (since 1.17.0) |
575
+ | `minlength`, `maxlength`, `pattern` | String | Passed to the inner field (since 1.17.0) |
576
+ | `autocomplete`, `inputmode` | String | Autofill and on-screen keyboard, passed to the inner field (since 1.17.0) |
577
+
578
+ Events: `glk-input` → `{ value }`, `glk-change` → `{ value }`. Native `input` / `change` also dispatched. Property: `.value`.
579
+
580
+ The inner field's validity is not reported to the surrounding form: `required`, `pattern` and `min` constrain the field and its picker, but they do not block a submit — validate before sending.
581
+
582
+ ---
583
+
584
+ ### 3.13 `<glk-radio>`
585
+
586
+ Radio button. Group multiple with the same `name`.
587
+
588
+ ```html
589
+ <glk-radio name="plan" label="Free Plan" value="free" checked></glk-radio>
590
+ <glk-radio name="plan" label="Pro Plan" value="pro"></glk-radio>
591
+ <glk-radio name="plan" label="Enterprise" value="enterprise"></glk-radio>
592
+ ```
593
+
594
+ | Attribute | Type | Description |
595
+ |---|---|---|
596
+ | `label` | String | Label text |
597
+ | `name` | String | Group name |
598
+ | `value` | String | Value submitted when checked |
599
+ | `checked` | Boolean | Checked state |
600
+ | `disabled` | Boolean | Disabled state |
601
+
602
+ Events: `glk-change` → `{ checked, value }`.
603
+
604
+ **Grouping (since 1.11.0).** Each `<glk-radio>` keeps its `<input type="radio">` in
605
+ its own shadow root, and native radio grouping works per tree — it does not cross
606
+ shadow boundaries. The group is therefore kept by the component, following the HTML
607
+ definition: **same `name`, same containing tree, same form owner**. Selecting one
608
+ clears the others, whether by click, by arrow key, by the `checked` property or by
609
+ the `checked` attribute, and only the selected value is submitted. Two `<glk-radio>`
610
+ in two different `<form>`s are two groups, and a radio without `name` is not grouped.
611
+ If several carry `checked` in the markup, the last one wins — as with native radios.
612
+
613
+ Keyboard: the group is a single tab stop. <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd>
614
+ <kbd>→</kbd> move through it, wrap around, skip disabled entries and select as they
615
+ go, firing `glk-change` and `change` exactly as a click does.
616
+
617
+ Not covered: a screen reader still announces the entries as separate radios, not as
618
+ "1 of 3" — that needs a `role="radiogroup"` container, which no `<glk-*>` element
619
+ provides. Wrap them yourself if you need it:
620
+
621
+ ```html
622
+ <div role="radiogroup" aria-label="Plan">
623
+ <glk-radio name="plan" label="Free Plan" value="free" checked></glk-radio>
624
+ <glk-radio name="plan" label="Pro Plan" value="pro"></glk-radio>
625
+ </div>
626
+ ```
627
+
628
+ ---
629
+
630
+ ### 3.14 `<glk-range>`
631
+
632
+ Slider input.
633
+
634
+ ```html
635
+ <glk-range label="Volume" min="0" max="100" value="65" step="5"></glk-range>
636
+ ```
637
+
638
+ | Attribute | Type | Description |
639
+ |---|---|---|
640
+ | `label` | String | Label text |
641
+ | `min` | Number | Minimum value |
642
+ | `max` | Number | Maximum value |
643
+ | `value` | Number | Current value |
644
+ | `step` | Number | Step size |
645
+ | `name` | String | Form field name |
646
+ | `disabled` | Boolean | Disabled state |
647
+
648
+ Events: `glk-input`, `glk-change`. Property: `.value`.
649
+
650
+ ---
651
+
652
+ ### 3.15 `<glk-search>`
653
+
654
+ Search input with leading icon.
655
+
656
+ ```html
657
+ <glk-search placeholder="Search components..."></glk-search>
658
+ ```
659
+
660
+ | Attribute | Type | Description |
661
+ |---|---|---|
662
+ | `placeholder` | String | Placeholder text |
663
+ | `value` | String | Current value |
664
+ | `name` | String | Form field name |
665
+ | `disabled` | Boolean | Disabled state |
666
+
667
+ Events: `glk-input`, `glk-change`.
668
+
669
+ ---
670
+
671
+ ### 3.16 `<glk-select>`
672
+
673
+ Dropdown select. Pass native `<option>` elements as children.
674
+
675
+ ```html
676
+ <glk-select label="Category" name="category">
677
+ <option value="">Please select…</option>
678
+ <option value="1">Insurance</option>
679
+ <option value="2">Rental</option>
680
+ <option value="3">Employment</option>
681
+ </glk-select>
682
+ ```
683
+
684
+ | Attribute | Type | Description |
685
+ |---|---|---|
686
+ | `label` | String | Label text |
687
+ | `name` | String | Form field name |
688
+ | `value` | String | Selected value |
689
+ | `disabled` | Boolean | Disabled state |
690
+
691
+ Children: native `<option>` elements, read from the light DOM.
692
+
693
+ Events: `glk-change` → `{ value }`.
694
+
695
+ **Changing the options later (since 1.12.0).** The options are copied into the
696
+ shadow tree, and the copy is kept in step: replace, add or remove `<option>`
697
+ children at any time — by hand or through a framework — and the rendered select
698
+ follows. Before 1.12.0 the copy was made once and then silently went stale.
699
+
700
+ The selection survives a rebuild: if the selected value is still in the new list
701
+ it stays selected, otherwise the `value` attribute decides, otherwise the browser
702
+ falls back to the first option. The **empty string is a real value** throughout —
703
+ `value=""` selects `<option value="">` wherever it sits in the list, not just when
704
+ it happens to be first.
705
+
706
+ ```html
707
+ <!-- "" is a genuine choice here, and it is honoured -->
708
+ <glk-select label="Language" value="">
709
+ <option value="de">Deutsch</option>
710
+ <option value="">Detect automatically</option>
711
+ </glk-select>
712
+ ```
713
+
714
+ A `value` naming no existing option leaves the current selection alone rather than
715
+ clearing it — when the matching option arrives later, it is selected then.
716
+
717
+ ---
718
+
719
+ ### 3.17 `<glk-textarea>`
720
+
721
+ Multi-line text input.
722
+
723
+ ```html
724
+ <glk-textarea label="Message" rows="4" placeholder="Type your message..."></glk-textarea>
725
+ ```
726
+
727
+ | Attribute | Type | Description |
728
+ |---|---|---|
729
+ | `label` | String | Label text |
730
+ | `rows` | Number | Visible rows |
731
+ | `placeholder` | String | Placeholder text |
732
+ | `name` | String | Form field name |
733
+ | `value` | String | Current value |
734
+ | `disabled` | Boolean | Disabled state |
735
+ | `required` | Boolean | Required field |
736
+
737
+ Events: `glk-input`, `glk-change`.
738
+
739
+ ---
740
+
741
+ ### 3.18 `<glk-toggle>`
742
+
743
+ Switch-style toggle (form-associated).
744
+
745
+ ```html
746
+ <glk-toggle label="Enable notifications" checked></glk-toggle>
747
+ <glk-toggle label="Dark mode"></glk-toggle>
748
+ <glk-toggle label="Disabled toggle" disabled></glk-toggle>
749
+ ```
750
+
751
+ | Attribute | Type | Description |
752
+ |---|---|---|
753
+ | `label` | String | Label text |
754
+ | `checked` | Boolean | Checked state |
755
+ | `disabled` | Boolean | Disabled state |
756
+ | `name` | String | Form field name |
757
+ | `value` | String | Submitted value when checked (default `"on"`) |
758
+
759
+ Events: `glk-change` → `{ checked }`. Property: `.checked`. Has `role="switch"` and syncs `aria-checked`.
760
+
761
+ ---
762
+
763
+ ### 3.19 `<glk-modal>`
764
+
765
+ Modal dialog with overlay, title, body, and footer actions.
766
+
767
+ ```html
768
+ <glk-button variant="secondary" size="auto"
769
+ onclick="document.getElementById('confirm').show()">
770
+ Open
771
+ </glk-button>
772
+
773
+ <glk-modal id="confirm" title="Delete contract?">
774
+ <p>This action cannot be undone.</p>
775
+ <div slot="actions">
776
+ <button class="glass-modal__action" onclick="document.getElementById('confirm').close()">Cancel</button>
777
+ <button class="glass-modal__action glass-modal__action--danger"
778
+ onclick="deleteContract(); document.getElementById('confirm').close()">Delete</button>
779
+ </div>
780
+ </glk-modal>
781
+ ```
782
+
783
+ | Attribute | Type | Description |
784
+ |---|---|---|
785
+ | `open` | Boolean | Visibility state (reflected as `.is-active` on the overlay) |
786
+ | `title` | String | Header title |
787
+
788
+ Slots:
789
+ - default — modal body content
790
+ - `actions` — container for footer buttons (GlassKit expects `<button class="glass-modal__action">`)
791
+
792
+ Methods: `.show()`, `.close()`. Property: `.open`. Events: `glk-close`.
793
+
794
+ Closes automatically on overlay click and <kbd>Escape</kbd>.
795
+
796
+ ---
797
+
798
+ ### 3.20 `<glk-popover>`
799
+
800
+ Anchored dropdown / menu container. Wraps a trigger element (`slot="trigger"`) and floating content. The element handles `.is-open` toggling, outside-click dismiss, and <kbd>Escape</kbd>-key close internally.
801
+
802
+ ```html
803
+ <glk-popover placement="end">
804
+ <glk-button slot="trigger" variant="secondary" size="auto">Open menu</glk-button>
805
+ <glk-list bare>
806
+ <glk-list-item title="Share" interactive></glk-list-item>
807
+ <glk-list-item title="Duplicate" interactive></glk-list-item>
808
+ <glk-list-item title="Delete" variant="danger" interactive></glk-list-item>
809
+ </glk-list>
810
+ </glk-popover>
811
+ ```
812
+
813
+ | Attribute | Type | Default | Description |
814
+ |---|---|---|---|
815
+ | `open` | Boolean | `false` | Visibility state |
816
+ | `placement` | String | `bottom` | `top`, `bottom`, `start`, `end` |
817
+
818
+ Slots:
819
+ - `trigger` — element that toggles the popover on click
820
+ - default — floating content shown while open
821
+
822
+ Methods: `.show()`, `.close()`, `.toggle()`. Property: `.open`.
823
+ Events: `glk-open`, `glk-close`.
824
+
825
+ **Naming warning:** the toggle method is deliberately called `.toggle()`, **not** `.togglePopover()` — the latter collides with the native `HTMLElement.togglePopover()` API from the HTML Popover specification.
826
+
827
+ ---
828
+
829
+ ### 3.21 `<glk-progress>`
830
+
831
+ Progress bar.
832
+
833
+ ```html
834
+ <glk-progress label="Upload" value="75" variant="success"></glk-progress>
835
+ <glk-progress label="Storage" value="90" variant="error" size="lg"></glk-progress>
836
+ <glk-progress label="CPU" value="45" size="sm"></glk-progress>
837
+ ```
838
+
839
+ | Attribute | Type | Description |
840
+ |---|---|---|
841
+ | `label` | String | Label text shown left of the bar |
842
+ | `value` | Number | 0–100 percentage |
843
+ | `variant` | String | `success`, `error` |
844
+ | `size` | String | `sm`, `md` (default), `lg` |
845
+
846
+ ---
847
+
848
+ ### 3.22 `<glk-toast>`
849
+
850
+ Notification at the top of the screen that hides itself (shown imperatively) — and, since 1.19.0, one that offers something: "A new version · Reload".
851
+
852
+ ```html
853
+ <glk-toast id="toast"></glk-toast>
854
+
855
+ <script>
856
+ toast.show('Saved successfully!', 'success', 3000);
857
+
858
+ // An offer: a button and an ×, stays until one of them is used
859
+ toast.show('A new version is ready', { action: { label: 'Reload', value: 'reload' } });
860
+ toast.addEventListener('glk-action', (e) => { if (e.detail.action === 'reload') location.reload(); });
861
+ toast.addEventListener('glk-close', () => { /* dismissed with × or Escape */ });
862
+ </script>
863
+ ```
864
+
865
+ | Attribute | Type | Description |
866
+ |---|---|---|
867
+ | `message` | String | The text |
868
+ | `variant` | String | `success`, `error`, `warning` — colours the icon and the action |
869
+ | `duration` | Number | Auto-dismiss time in ms (default `3000`); `0` keeps it up. With an action there is no timeout unless `duration` is set |
870
+ | `visible` | Boolean | Shown |
871
+ | `action-label`, `action-value` | String | The action button, and the value `glk-action` reports (the label when missing) (since 1.19.0) |
872
+ | `dismissible` | Boolean | An × without an action; an action always brings one (since 1.19.0) |
873
+ | `close-label` | String | Name of the ×, `"Close"` by default (since 1.19.0) |
874
+
875
+ Methods: `.show(message, variant, duration)`, `.show(message, { variant, duration, action: { label, value }, dismissible })`, `.dismiss()`. In the options form, left-out keys keep their attribute, except `action`: a message without one shows no button, and one with an action but no `duration` stays. Calling `show()` again while the toast is up restarts its timer.
876
+
877
+ Slot: `icon` — replaces the built-in icon; leave its stroke unset and it takes the variant colour.
878
+
879
+ Events: `glk-action { action, label }` — the action was used; the toast closes unless a listener calls `preventDefault()` or shows the next message (`show('Restored')`) from the handler. `glk-close` — closed with the × or Escape. `glk-dismiss` — timed out.
880
+
881
+ While the pointer or the focus is on a toast with buttons, it does not time out. The toast is a polite live region (`role="status"`), so the message is announced; hidden, its buttons are inert, and closing it from the keyboard hands the focus back. Where it sits comes from GlassKit's `--gl-toast-top` — set it on an ancestor to place the toast below your header (with `viewport-fit=cover`, add `env(safe-area-inset-top)`).
882
+
883
+ ---
884
+
885
+ ### 3.23 `<glk-accordion>` + `<glk-accordion-item>`
886
+
887
+ Collapsible sections.
888
+
889
+ ```html
890
+ <glk-accordion>
891
+ <glk-accordion-item title="What is GlassKit?" open>
892
+ A glassmorphism CSS component library.
893
+ </glk-accordion-item>
894
+ <glk-accordion-item title="Do I need a framework?">
895
+ No — vanilla HTML works.
896
+ </glk-accordion-item>
897
+ </glk-accordion>
898
+ ```
899
+
900
+ **`<glk-accordion>`** — container, no attributes.
901
+
902
+ **`<glk-accordion-item>`**
903
+
904
+ | Attribute | Type | Description |
905
+ |---|---|---|
906
+ | `title` | String | Trigger label |
907
+ | `open` | Boolean | Expanded state |
908
+
909
+ Default slot: body content. Events: `glk-toggle` → `{ open }`. Property: `.open`.
910
+
911
+ ---
912
+
913
+ ### 3.24 `<glk-list>` + `<glk-list-item>`
914
+
915
+ iOS-style grouped settings list. Items carry a leading icon, title + optional subtitle, and a trailing element. Dividers between items are drawn automatically by GlassKit CSS — **never** add `<hr>` or divider markup manually. Supports section headers, large icons (40×40), multi-line subtitles, trailing value text, and semantic `danger`/`accent` variants.
916
+
917
+ #### Settings-style list with section header and large icons
918
+
919
+ ```html
920
+ <glk-list header="Recommendations">
921
+ <glk-list-item title="Review downloaded media" subtitle="Save up to 1.38 GB. Review files and remove as needed." leading-lg wrap interactive>
922
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
923
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
924
+ <polyline points="7 10 12 15 17 10"/>
925
+ <line x1="12" y1="15" x2="12" y2="3"/>
926
+ </svg>
927
+ <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
928
+ <polyline points="9 18 15 12 9 6"/>
929
+ </svg>
930
+ </glk-list-item>
931
+ <glk-list-item title="AusweisApp Bund" subtitle="Version 2.5.1" leading-lg detail="24 MB" interactive>
932
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
933
+ <rect x="2" y="3" width="20" height="18" rx="2"/>
934
+ <path d="M9 3v18"/><path d="M15 3v18"/>
935
+ </svg>
936
+ <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
937
+ <polyline points="9 18 15 12 9 6"/>
938
+ </svg>
939
+ </glk-list-item>
940
+ <glk-list-item title="View all subscriptions" variant="accent" center interactive></glk-list-item>
941
+ </glk-list>
942
+ ```
943
+
944
+ #### Compact menu with danger variant
945
+
946
+ ```html
947
+ <glk-list>
948
+ <glk-list-item title="Share" center interactive></glk-list-item>
949
+ <glk-list-item title="Duplicate" center interactive></glk-list-item>
950
+ <glk-list-item title="Delete" variant="danger" center interactive></glk-list-item>
951
+ </glk-list>
952
+ ```
953
+
954
+ #### Attributes
955
+
956
+ **`<glk-list>`**
957
+
958
+ | Attribute | Type | Description |
959
+ |---|---|---|
960
+ | `header` | String | Section header text — renders as uppercase label above the list |
961
+ | `flush` | Boolean | Edge-to-edge variant — removes side margin and radius |
962
+ | `bare` | Boolean | Strips background, border, shadow — use inside `<glk-popover>` or `<glk-card>` to avoid double glass surfaces |
963
+
964
+ **`<glk-list-item>`**
965
+
966
+ | Attribute | Type | Description |
967
+ |---|---|---|
968
+ | `title` | String | Primary text (medium weight, truncated with ellipsis) |
969
+ | `subtitle` | String | Secondary text (muted, small, truncated) |
970
+ | `interactive` | Boolean | Enables hover / focus / active states and `glk-click` event |
971
+ | `center` | Boolean | Centered single-text variant (for action rows like "Reset to defaults") |
972
+ | `leading-lg` | Boolean | Large 40×40 leading icon with rounded corners (for app icons; use 32×32 SVG or `<img>`) |
973
+ | `wrap` | Boolean | Multi-line subtitle — allows up to 3 lines with ellipsis (requires `subtitle`) |
974
+ | `detail` | String | Muted trailing value text (e.g. file size "24 MB", version number) — rendered before `slot="trailing"` |
975
+ | `variant` | String | Semantic color: `"danger"` (red destructive) or `"accent"` (primary color). Inherits to title and leading icon |
976
+
977
+ #### Slots (on `<glk-list-item>`)
978
+
979
+ | Slot | Purpose |
980
+ |---|---|
981
+ | `leading` | Icon slot (24×24 SVG recommended, or 32×32 with `leading-lg`; use `stroke: currentColor`) |
982
+ | `trailing` | Chevron, badge, or button — rendered after `detail` value |
983
+
984
+ #### Events
985
+
986
+ `<glk-list-item>` emits `glk-click` when clicked, but only if `interactive` is set.
987
+
988
+ #### Internals (for framework authors)
989
+
990
+ Both elements use pure Shadow DOM with slot projection — no child node cloning — so they compose cleanly inside lit-html, HybridsJS, React, Vue, and Svelte templates. `<glk-list>` observes its direct children via `MutationObserver` and sets a `data-last` attribute on the final `<glk-list-item>`; each item's shadow carries a one-line override sheet (`:host([data-last]) .glass-list__item::after { content: none; }`) to suppress the auto-divider on the last row. Divider inset automatically adjusts for `leading-lg` via CSS `:has()`. Zero CSS duplication.
991
+
992
+ ---
993
+
994
+ ### 3.25 `<glk-segmented>`
995
+
996
+ A small, exclusive choice as one control (since 1.15.0) — traffic light, morning / afternoon, mode. One `<button>` per option, the chosen one marked `aria-pressed="true"` (GlassKit styles exactly that attribute); a value change only re-sets the attribute, never rebuilds, so focus stays on the pressed button. Form-associated (`GlkFormElement`): a surrounding `<form>` receives `name=value`, reset restores the initial value.
997
+
998
+ ```html
999
+ <glk-segmented full name="status" label="Status" value="y"
1000
+ options='[{"value":"g","label":"Green","tone":"success"},{"value":"y","label":"Yellow","tone":"warning"},{"value":"r","label":"Red","tone":"error"},{"value":"x","label":"Locked","disabled":true}]'>
1001
+ </glk-segmented>
1002
+ ```
1003
+
1004
+ | Attribute | Type | Description |
1005
+ |---|---|---|
1006
+ | `options` | JSON | `[{ value, label, tone?, disabled? }]`; `tone` is `success` \| `warning` \| `error` and puts a dot in that colour before the label |
1007
+ | `value` | String | Value of the chosen option (reflected property) |
1008
+ | `full` | Boolean | Buttons share the width |
1009
+ | `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) |
1010
+ | `label` | String | `aria-label` of the group (no default — set it) |
1011
+ | `name` | String | Form field name |
1012
+
1013
+ Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`. Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.
1014
+
1015
+ 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:
1016
+
1017
+ ```html
1018
+ <glk-segmented full overflow="scroll" label="Area" value="g"
1019
+ options='[{"value":"a","label":"General"},{"value":"b","label":"Hours"},…,{"value":"g","label":"Privacy"}]'></glk-segmented>
1020
+ ```
1021
+
1022
+ The attribute is `overflow`, not `scroll`: `scroll` is a method of every element, and a framework that sets a property whenever the element has one (hybrids, Vue, React 19) would overwrite that method instead of setting the attribute.
1023
+
1024
+ ---
1025
+
1026
+ ### 3.26 `<glk-steps>`
1027
+
1028
+ 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.
1029
+
1030
+ ```html
1031
+ <glk-steps steps="Day,Slot,Dog,Confirm" current="2" label="Progress"></glk-steps>
1032
+ ```
1033
+
1034
+ | Attribute | Type | Description |
1035
+ |---|---|---|
1036
+ | `steps` | String | Comma-separated labels (property also accepts an array) |
1037
+ | `current` | Number | 0-based index of the current step (reflected property) |
1038
+ | `label` | String | `aria-label` of the list, optional |
1039
+
1040
+ Part: `steps`.
1041
+
1042
+ ---
1043
+
1044
+ ### 3.27 `<glk-sheet>`
1045
+
1046
+ Bottom sheet (since 1.15.0) — the mobile sibling of `<glk-modal>`, same API shape. Opening and closing are animated by GlassKit's `.glass-sheet` block: `.is-active` is added one frame after the overlay is unhidden, and on closing the overlay is hidden only after `transitionend` (safety net 400 ms), so no blurred layer idles behind the page; under `prefers-reduced-motion` it switches at once. The Escape listener sits on `document` and is removed in `teardownEvents()`.
1047
+
1048
+ ```html
1049
+ <glk-sheet id="sheet" title="Rebook to …">
1050
+ <p>Content: a list, text, a form.</p>
1051
+ <glk-button slot="actions" variant="secondary" onclick="sheet.close()">Close</glk-button>
1052
+ </glk-sheet>
1053
+ <script>sheet.show();</script>
1054
+ ```
1055
+
1056
+ | Attribute | Type | Description |
1057
+ |---|---|---|
1058
+ | `open` | Boolean | Shown as an overlay (reflected property) |
1059
+ | `inline` | Boolean | In the flow, no overlay, always visible — e.g. inside a phone frame or a card |
1060
+ | `title` | String | Heading and `aria-label` of the dialog |
1061
+
1062
+ Methods: `show()`, `close()`. Slots: default (content), `actions`. Events: `glk-close` — only when the user closes it (scrim click, Escape), like `<glk-modal>`; `close()` does not emit. Parts: `overlay`, `sheet`, `title`, `body`, `actions`.
1063
+
1064
+ ---
1065
+
1066
+ ### 3.28 `<glk-empty>`
1067
+
1068
+ Empty state for lists and result pages (since 1.15.0): icon plate, title, short muted text and room for one action. An empty `title` or `text` hides its element instead of leaving an empty paragraph.
1069
+
1070
+ ```html
1071
+ <glk-empty title="No bookings yet" text="Pick a day and a dog — the team is looking forward to it.">
1072
+ <svg viewBox="0 0 24 24">…</svg>
1073
+ <glk-button slot="action" variant="primary" size="sm">Book</glk-button>
1074
+ </glk-empty>
1075
+ ```
1076
+
1077
+ | Attribute | Type | Description |
1078
+ |---|---|---|
1079
+ | `title` | String | Heading; hidden when empty |
1080
+ | `text` | String | Muted text; hidden when empty |
1081
+
1082
+ Slots: default (an `<svg>` icon, 24 px stroked; a plain circle as fallback), `action`. Parts: `empty`, `icon`, `title`, `text`, `action`.
1083
+
1084
+ ### 3.29 `<glk-date-strip>`
1085
+
1086
+ A row of day chips that scrolls sideways (since 1.16.0) — a booking horizon, the days around today. Chips are buttons in a `role="group"`, the chosen one `aria-pressed="true"`, each named with its full date from `Intl` for the `locale`. One tab stop: arrow keys move between chips, Home/End jump to the ends, Enter or Space picks. The chosen chip is centred in the strip after the first layout and on every value change; only the strip scrolls, never the page. A value change re-sets state; `start`, `days`, `today`, `marks` and `locale` rebuild the chips.
1087
+
1088
+ ```html
1089
+ <glk-date-strip start="2026-09-22" days="29" value="2026-10-11" label="Pick a day"
1090
+ marks='{"2026-09-22":"success","2026-09-25":{"tone":"error","disabled":true}}'></glk-date-strip>
1091
+ ```
1092
+
1093
+ | Attribute | Type | Description |
1094
+ |---|---|---|
1095
+ | `start` | String | First day `YYYY-MM-DD`; today when missing |
1096
+ | `days` | Number | Number of chips (default 7) |
1097
+ | `value` | String | Chosen day `YYYY-MM-DD` |
1098
+ | `today` | String | The underlined day; the browser's day when missing |
1099
+ | `marks` | JSON | `{ date: tone \| { tone, disabled } }` — tone `primary` / `success` / `warning` / `error`; `disabled` makes the chip unpickable |
1100
+ | `locale` | String | BCP 47 tag for the names; the browser language when missing |
1101
+ | `label` | String | `aria-label` of the group |
1102
+
1103
+ Event: `glk-change { value }` — only on a change made by the user. Properties: `value`, `marks` (object or JSON text), `locale`, `label`. Part: `strip`.
1104
+
1105
+ ---
1106
+
1107
+ ### 3.30 `<glk-calendar>`
1108
+
1109
+ One month with a day to pick (since 1.16.0). The days are buttons in a `role="group"`, the chosen one `aria-pressed="true"`, each named with its full date; month title, weekday names and the first day of the week come from `Intl` for the `locale` (`week-start` overrides; Monday where a browser has no week info). One tab stop: arrows move by a day or a week, Home/End to the ends of the week, PageUp/PageDown by a month — past the month's edge the calendar shows that month and emits `glk-month`. Arrows only move focus; Enter or Space picks. Days outside `min`/`max` carry `aria-disabled="true"`: in the arrow path, announced, never picked. A value set to another month flips the calendar there. Form-associated (`GlkFormElement`): a surrounding `<form>` receives `name=value`, reset restores the initial value.
1110
+
1111
+ ```html
1112
+ <glk-calendar name="day" value="2026-09-22" min="2026-09-22" label="Pick a day"
1113
+ marks='{"2026-09-24":["error"],"2026-09-27":["success","warning"]}'></glk-calendar>
1114
+ ```
1115
+
1116
+ | Attribute | Type | Description |
1117
+ |---|---|---|
1118
+ | `month` | String | Shown month `YYYY-MM`; without it the month of the value, else of today; reflected when the user navigates |
1119
+ | `value` | String | Chosen day `YYYY-MM-DD` |
1120
+ | `today` | String | The day with the warm border; the browser's day when missing |
1121
+ | `min`, `max` | String | Days outside cannot be picked |
1122
+ | `marks` | JSON | `{ date: tone \| [tone, …] }` — up to three tones per day |
1123
+ | `locale` | String | BCP 47 tag for names and week start; the browser language when missing |
1124
+ | `week-start` | Number | 0 = Sunday … 6 = Saturday; from the locale when missing |
1125
+ | `label` | String | `aria-label` of the day group |
1126
+ | `prev-label`, `next-label` | String | Names of the nav buttons ("Previous month" / "Next month") |
1127
+ | `name` | String | Form field name |
1128
+
1129
+ Events: `glk-change { value }` on a pick by the user; `glk-month { month }` when the user moves to another month. Properties: `month`, `value`, `marks`, `locale`, `label`. Parts: `calendar`, `head`, `title`, `grid`.
1130
+
1131
+ ---
1132
+
1133
+ ### 3.31 `<glk-image-picker>`
1134
+
1135
+ One image with a preview, resized on the client before it goes anywhere (since 1.16.0). Choose opens the file dialog from a real button; the file is decoded with `createImageBitmap` (`imageOrientation: 'from-image'`, so a phone photo comes out upright), drawn onto a canvas no larger than `max` and handed out as a data URL in `glk-change`. The `src` property holds it and is not reflected, so a data URL of megabytes never lands in the DOM. A file the browser cannot decode — Chrome and HEIC, a corrupt file — emits `glk-error`. Not form-associated: listen to `glk-change` and upload, or copy the data URL into a hidden field.
1136
+
1137
+ ```html
1138
+ <glk-image-picker id="photo" round max="512" label="Profile photo" hint="JPG or PNG"></glk-image-picker>
1139
+ <script>photo.addEventListener('glk-change', e => upload(e.detail.dataUrl));</script>
1140
+ ```
1141
+
1142
+ | Attribute | Type | Description |
1143
+ |---|---|---|
1144
+ | `src` | String | Starting image: URL or data URL |
1145
+ | `label` | String | Visible label; names the group |
1146
+ | `hint` | String | Small muted line; none by default |
1147
+ | `round` | Boolean | Circular preview, for avatars |
1148
+ | `max` | Number | Longest edge in px after resizing (default 1024) |
1149
+ | `type`, `quality` | String, Number | Output MIME (default `image/jpeg`; `image/webp` where the browser encodes it, PNG otherwise), quality 0–1 (default 0.82) |
1150
+ | `accept` | String | File dialog filter (default `image/*`) |
1151
+ | `choose-label`, `change-label`, `remove-label` | String | Button texts ("Choose", "Change", "Remove") |
1152
+
1153
+ Events: `glk-change { dataUrl, width, height, size }` after a pick, on remove with an empty `dataUrl` and zeros; `glk-error { message, name }` when the file cannot be decoded. Property: `src` (not reflected). Parts: `picker`, `preview`, `meta`, `label`, `hint`, `actions`.
1154
+
1155
+ ---
1156
+
1157
+ ## 4. Composition Patterns
1158
+
1159
+ ### Login Screen
1160
+
1161
+ ```html
1162
+ <div class="glass-bg">
1163
+ <glk-nav>
1164
+ <glk-pill label="Back" onclick="history.back()">
1165
+ <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
1166
+ </glk-pill>
1167
+ <glk-pill label="Toggle theme" onclick="toggleTheme()">
1168
+ <svg class="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
1169
+ </glk-pill>
1170
+ </glk-nav>
1171
+
1172
+ <glk-title>Sign In</glk-title>
1173
+
1174
+ <form>
1175
+ <glk-input label="Email" type="email" name="email" placeholder="you@example.com" required></glk-input>
1176
+ <glk-input label="Password" type="password" name="password" required></glk-input>
1177
+
1178
+ <glk-button variant="primary" type="submit">Log In</glk-button>
1179
+ <glk-button variant="tertiary">Forgot password?</glk-button>
1180
+ </form>
1181
+ </div>
1182
+ ```
1183
+
1184
+ ### Dashboard with Cards + Tab Bar
1185
+
1186
+ ```html
1187
+ <div class="glass-bg glass-bg--has-tab-bar">
1188
+ <glk-nav>
1189
+ <glk-title>Dashboard</glk-title>
1190
+ </glk-nav>
1191
+
1192
+ <glk-card glow>
1193
+ <p>Capture and upload documents.</p>
1194
+ </glk-card>
1195
+ <glk-card glow>
1196
+ <p>Manage and archive contracts.</p>
1197
+ </glk-card>
1198
+
1199
+ <glk-tab-bar>
1200
+ <glk-tab-item label="Home" active>
1201
+ <svg viewBox="0 0 24 24">...</svg>
1202
+ </glk-tab-item>
1203
+ <glk-tab-item label="Contracts" badge="3">
1204
+ <svg viewBox="0 0 24 24">...</svg>
1205
+ </glk-tab-item>
1206
+ <glk-tab-item label="Profile">
1207
+ <svg viewBox="0 0 24 24">...</svg>
1208
+ </glk-tab-item>
1209
+ </glk-tab-bar>
1210
+ </div>
1211
+ ```
1212
+
1213
+ ### Form Page with Validation
1214
+
1215
+ ```html
1216
+ <form id="contract-form">
1217
+ <glk-title>New Contract</glk-title>
1218
+
1219
+ <glk-input label="Contract Name" name="name" required></glk-input>
1220
+ <glk-select label="Category" name="category">
1221
+ <option value="">Please select…</option>
1222
+ <option value="insurance">Insurance</option>
1223
+ <option value="rental">Rental</option>
1224
+ </glk-select>
1225
+ <glk-textarea label="Notes" name="notes" rows="4"></glk-textarea>
1226
+ <glk-toggle label="Enable reminder" name="reminder"></glk-toggle>
1227
+
1228
+ <glk-button variant="primary" type="submit">Save</glk-button>
1229
+ </form>
1230
+
1231
+ <script>
1232
+ document.getElementById('contract-form').addEventListener('submit', (e) => {
1233
+ e.preventDefault();
1234
+ const data = new FormData(e.target);
1235
+ console.log(Object.fromEntries(data)); // { name, category, notes, reminder? }
1236
+ });
1237
+ </script>
1238
+ ```
1239
+
1240
+ ### Delete Confirmation Modal
1241
+
1242
+ ```html
1243
+ <glk-button variant="secondary" size="auto"
1244
+ onclick="document.getElementById('confirm').show()">
1245
+ Delete contract
1246
+ </glk-button>
1247
+
1248
+ <glk-modal id="confirm" title="Delete contract?">
1249
+ <p>This action cannot be undone.</p>
1250
+ <div slot="actions">
1251
+ <button class="glass-modal__action" onclick="document.getElementById('confirm').close()">Cancel</button>
1252
+ <button class="glass-modal__action glass-modal__action--danger"
1253
+ onclick="handleDelete(); document.getElementById('confirm').close()">Delete</button>
1254
+ </div>
1255
+ </glk-modal>
1256
+ ```
1257
+
1258
+ ### iOS-style Settings Screen (List + Popover)
1259
+
1260
+ Full example reproducing the native iOS Settings layout with two grouped lists and an inline action menu.
1261
+
1262
+ ```html
1263
+ <div class="glass-bg">
1264
+ <glk-nav>
1265
+ <glk-pill label="Back" onclick="history.back()">
1266
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
1267
+ </glk-pill>
1268
+ <glk-title>Settings</glk-title>
1269
+
1270
+ <glk-popover placement="end">
1271
+ <glk-pill slot="trigger" label="More">
1272
+ <svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
1273
+ </glk-pill>
1274
+ <glk-list bare>
1275
+ <glk-list-item title="Edit profile" interactive></glk-list-item>
1276
+ <glk-list-item title="Sign out" interactive></glk-list-item>
1277
+ </glk-list>
1278
+ </glk-popover>
1279
+ </glk-nav>
1280
+
1281
+ <glk-list header="Account">
1282
+ <glk-list-item title="Marcel Jungherz" subtitle="marcel@jungherz.com" leading-lg interactive>
1283
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
1284
+ <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
1285
+ </glk-list-item>
1286
+ <glk-list-item title="Subscriptions" interactive detail="$34.94">
1287
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>
1288
+ <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
1289
+ </glk-list-item>
1290
+ </glk-list>
1291
+
1292
+ <glk-list header="Notifications">
1293
+ <glk-list-item title="Push notifications" interactive>
1294
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
1295
+ <span slot="trailing">On</span>
1296
+ </glk-list-item>
1297
+ <glk-list-item title="Email digest" interactive>
1298
+ <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22 6 12 13 2 6"/></svg>
1299
+ <span slot="trailing">Weekly</span>
1300
+ </glk-list-item>
1301
+ <glk-list-item title="Reset to defaults" variant="danger" center interactive></glk-list-item>
1302
+ </glk-list>
1303
+ </div>
1304
+ ```
1305
+
1306
+ Key points:
1307
+ - Section headers via `header` attribute — no need for external `<p>` labels
1308
+ - Large leading icon via `leading-lg` for profile-style rows
1309
+ - Trailing value via `detail` attribute — muted metadata text before the chevron
1310
+ - Auto-dividers — no manual `<hr>` between rows; inset adapts for large icons automatically
1311
+ - `variant="danger"` on destructive "Reset" row — red text signals caution
1312
+ - Popover anchored in nav — `placement="end"` aligns the menu to the right edge of the trigger
1313
+ - Bare list inside popover — `<glk-list bare>` strips the inner glass surface so only the popover's glass shows
1314
+
1315
+ ---
1316
+
1317
+ ## 5. State & Event Overview
1318
+
1319
+ | Element | Key Property | HTML Attribute | Event(s) | Detail |
1320
+ |---|---|---|---|---|
1321
+ | `<glk-button>` | — | `disabled` | `glk-click` | — |
1322
+ | `<glk-pill>` | — | `disabled` | `glk-click` | — |
1323
+ | `<glk-tab-item>` | — | `active` | `glk-tab-select` | — |
1324
+ | `<glk-tab-accessory>` | — | `disabled` | `glk-click` | — |
1325
+ | `<glk-input>` | `.value` | `value` | `glk-input`, `glk-change` (+ native `input`, `change`) | `{ value }` |
1326
+ | `<glk-textarea>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
1327
+ | `<glk-select>` | `.value` | `value` | `glk-change` | `{ value }` |
1328
+ | `<glk-search>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
1329
+ | `<glk-toggle>` | `.checked` | `checked` | `glk-change` (+ native `change`) | `{ checked }` |
1330
+ | `<glk-checkbox>` | `.checked` | `checked` | `glk-change` | `{ checked }` |
1331
+ | `<glk-radio>` | `.checked` | `checked` | `glk-change` | `{ checked, value }` |
1332
+ | `<glk-range>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
1333
+ | `<glk-modal>` | `.open` | `open` | `glk-close` | — |
1334
+ | `<glk-popover>` | `.open` | `open` | `glk-open`, `glk-close` | — |
1335
+ | `<glk-accordion-item>` | `.open` | `open` | `glk-toggle` | `{ open }` |
1336
+ | `<glk-list-item>` | — | `interactive` | `glk-click` (only when interactive) | — |
1337
+ | `<glk-segmented>` | `.value` | `value` | `glk-change` | `{ value }` |
1338
+ | `<glk-sheet>` | `.open` | `open` | `glk-close` (user close only) | — |
1339
+ | `<glk-date-strip>` | `.value` | `value` | `glk-change` (user only) | `{ value }` |
1340
+ | `<glk-calendar>` | `.value`, `.month` | `value`, `month` | `glk-change` (user only), `glk-month` (user only) | `{ value }`, `{ month }` |
1341
+ | `<glk-image-picker>` | `.src` (not reflected) | `src` | `glk-change`, `glk-error` | `{ dataUrl, width, height, size }`, `{ message, name }` |
1342
+ | `<glk-badge>` | `.selected` | `interactive`, `selected` | `glk-click` (only when interactive) | — |
1343
+ | `<glk-toast>` | `.show()` | `visible` | `glk-action`, `glk-close`, `glk-dismiss` | `{ action, label }`, —, — |
1344
+
1345
+ All `glk-*` events bubble and are `composed: true`, so they pierce shadow boundaries naturally.
1346
+
1347
+ ---
1348
+
1349
+ ## 6. Rules & Common Mistakes
1350
+
1351
+ ### Always follow
1352
+
1353
+ 1. **`data-theme` on `<html>`** — never on `<body>` or individual elements; the global MutationObserver only watches `<html>`.
1354
+ 2. **Form elements belong inside a `<form>`** — otherwise `ElementInternals.setFormValue()` has no effect and `new FormData()` will be empty.
1355
+ 3. **`<glk-list-item>` must be a direct child of `<glk-list>`** — nested deeper, it won't be observed for the auto-divider last-item detection.
1356
+ 4. **Popover trigger via `slot="trigger"`** — don't reference external elements by ID. The popover's built-in click handler listens on the trigger slot.
1357
+ 5. **Toggle popovers through the API** — `popover.open = true` or `popover.toggle()`, never `classList.add('is-open')` on internal nodes.
1358
+ 6. **Use `<glk-list bare>` inside `<glk-popover>`** — otherwise you get a double glass surface.
1359
+ 7. **Import the library once** — `import '@jungherz-de/glasskit-elements'` registers all elements via `customElements.define`. Multiple imports are idempotent but unnecessary.
1360
+ 8. **SVG icons: `stroke: currentColor, stroke-width: 2`** — matches GlassKit's icon convention and inherits the token-driven text color.
1361
+ 9. **`title` on `<glk-sheet>`, `<glk-modal>`, `<glk-list-item>`, `<glk-accordion-item>` and `<glk-empty>` is the heading, not a tooltip** (since 1.16.1) — the element reads it and takes the attribute off the host, so `el.getAttribute('title')` is `null` after upgrade; read and write `el.title` instead. Every other element keeps the native `title` tooltip.
1362
+
1363
+ ### Common mistakes
1364
+
1365
+ | Mistake | Correction |
1366
+ |---|---|
1367
+ | `onclick="popover.togglePopover()"` | Collides with the native `HTMLElement.togglePopover()` method (HTML Popover API). Use `.toggle()` on the `<glk-popover>` element, or set the `open` attribute. |
1368
+ | `<hr>` or `<glk-divider>` manually between `<glk-list-item>`s | Remove it — dividers render automatically via `::after`. |
1369
+ | `<glk-list>` inside `<glk-popover>` without `bare` | Add `bare` to the inner list so only the popover surface has glass. |
1370
+ | `document.querySelector('.glass-modal').classList.add('is-active')` | Set `<glk-modal>.open = true` instead — the element manages `.is-active` internally. |
1371
+ | `data-theme` directly on a `<glk-*>` element | Set it on `<html>`; theme propagates automatically through the MutationObserver. |
1372
+ | Form element outside `<form>` | Wrap in a `<form>`; `ElementInternals` needs a form owner. |
1373
+ | Overriding shadow styles with external CSS | Use CSS custom properties (`--gl-color-primary`, etc.) — they pierce shadow roots. Hard-coded selectors inside shadow roots cannot be targeted externally. |
1374
+ | Forgetting `slot="trigger"` on the popover trigger element | Without it, the popover's internal click handler can't identify the trigger and never toggles. |
1375
+ | Using `open` as a method name (`mypopover.open()`) | `open` is a reflected property. Use `.show()` / `.close()` / `.toggle()` for imperative control. |
1376
+ | `<glk-tab-bar>` without `.glass-bg--has-tab-bar` on the outer background | Tab bar covers bottom content. Add the modifier to the outer `.glass-bg` container. |
1377
+ | `<glk-tab-bar floating>` without `<glk-tab-dock>` wrapper | Floating bar relies on the dock for fixed centered positioning. Always wrap it in `<glk-tab-dock>`. |
1378
+ | `<glk-tab-dock>` without `.glass-bg--has-tab-bar-floating` on the outer background | Use `--has-tab-bar-floating` (not `--has-tab-bar`) for the floating variant — the padding budget differs. |
1379
+
1380
+ ---
1381
+
1382
+ ## 7. Quick Reference
1383
+
1384
+ | Tag | Category | Key attributes | Key slots | Key events |
1385
+ |---|---|---|---|---|
1386
+ | `<glk-nav>` | Navigation | — | default | — |
1387
+ | `<glk-pill>` | Navigation | `label`, `disabled` | default (icon) | `glk-click` |
1388
+ | `<glk-tab-bar>` | Navigation | `static`, `floating` | default | — |
1389
+ | `<glk-tab-item>` | Navigation | `label`, `active`, `badge` | default (icon) | `glk-tab-select` |
1390
+ | `<glk-tab-dock>` | Navigation | `accessory-left` | default | — |
1391
+ | `<glk-tab-accessory>` | Navigation | `label`, `variant`, `disabled` | default (icon) | `glk-click` |
1392
+ | `<glk-avatar>` | Content | `size`, `src` | default (initials) | — |
1393
+ | `<glk-badge>` | Content | `variant`, `interactive`, `selected` | default | `glk-click` (only when interactive) |
1394
+ | `<glk-empty>` | Content | `title`, `text` | default (icon), `action` | — |
1395
+ | `<glk-segmented>` | Forms | `options`, `value`, `full`, `overflow`, `label`, `name` | — | `glk-change` |
1396
+ | `<glk-steps>` | Navigation | `steps`, `current`, `label` | — | — |
1397
+ | `<glk-sheet>` | Feedback | `open`, `inline`, `title` | default, `actions` | `glk-close` |
1398
+ | `<glk-date-strip>` | Navigation | `start`, `days`, `value`, `today`, `marks`, `locale`, `label` | — | `glk-change` |
1399
+ | `<glk-calendar>` | Forms | `month`, `value`, `today`, `min`, `max`, `marks`, `locale`, `week-start`, `label`, `name` | — | `glk-change`, `glk-month` |
1400
+ | `<glk-image-picker>` | Forms | `src`, `label`, `hint`, `round`, `max`, `type`, `quality`, `accept` | — | `glk-change`, `glk-error` |
1401
+ | `<glk-card>` | Content | `glow` | default | — |
1402
+ | `<glk-divider>` | Content | — | — | — |
1403
+ | `<glk-status>` | Content | `message` | — | — |
1404
+ | `<glk-title>` | Content | — | default | — |
1405
+ | `<glk-button>` | Buttons | `variant`, `size`, `disabled`, `type` | default | `glk-click` |
1406
+ | `<glk-checkbox>` | Forms | `label`, `checked`, `disabled`, `name`, `value` | — | `glk-change` |
1407
+ | `<glk-input>` | Forms | `label`, `type`, `placeholder`, `hint`, `error`, `disabled`, `required`, `name`, `value`, `min`, `max`, `step`, `minlength`, `maxlength`, `pattern`, `autocomplete`, `inputmode` | — | `glk-input`, `glk-change` |
1408
+ | `<glk-radio>` | Forms | `label`, `name`, `value`, `checked`, `disabled` | — | `glk-change` |
1409
+ | `<glk-range>` | Forms | `label`, `min`, `max`, `value`, `step`, `name`, `disabled` | — | `glk-input`, `glk-change` |
1410
+ | `<glk-search>` | Forms | `placeholder`, `value`, `name`, `disabled` | — | `glk-input`, `glk-change` |
1411
+ | `<glk-select>` | Forms | `label`, `name`, `value`, `disabled` | default (`<option>`) | `glk-change` |
1412
+ | `<glk-textarea>` | Forms | `label`, `rows`, `placeholder`, `name`, `value`, `disabled`, `required` | — | `glk-input`, `glk-change` |
1413
+ | `<glk-toggle>` | Forms | `label`, `checked`, `disabled`, `name`, `value` | — | `glk-change` |
1414
+ | `<glk-modal>` | Feedback | `open`, `title` | default, `actions` | `glk-close` |
1415
+ | `<glk-popover>` | Feedback | `open`, `placement` | `trigger`, default | `glk-open`, `glk-close` |
1416
+ | `<glk-progress>` | Feedback | `value`, `label`, `variant`, `size` | — | — |
1417
+ | `<glk-toast>` | Feedback | `message`, `variant`, `duration`, `visible`, `action-label`, `action-value`, `dismissible`, `close-label` | `icon` | `glk-action`, `glk-close`, `glk-dismiss` |
1418
+ | `<glk-accordion>` | Containers | — | default | — |
1419
+ | `<glk-accordion-item>` | Containers | `title`, `open` | default | `glk-toggle` |
1420
+ | `<glk-list>` | Containers | `header`, `flush`, `bare` | default | — |
1421
+ | `<glk-list-item>` | Containers | `title`, `subtitle`, `interactive`, `center`, `leading-lg`, `wrap`, `detail`, `variant` | `leading`, `trailing` | `glk-click` (when interactive) |
1422
+
1423
+ ---
1424
+
1425
+ ## 8. Framework Integration
1426
+
1427
+ GlassKit Elements are standards-compliant Custom Elements. They work in every major framework:
1428
+
1429
+ ### Vanilla HTML / JS
1430
+
1431
+ ```html
1432
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.esm.js"></script>
1433
+ <glk-button variant="primary" onclick="alert('clicked')">Click</glk-button>
1434
+ ```
1435
+
1436
+ ### React
1437
+
1438
+ ```jsx
1439
+ import '@jungherz-de/glasskit-elements';
1440
+
1441
+ function App() {
1442
+ return (
1443
+ <glk-input
1444
+ label="Name"
1445
+ value={name}
1446
+ onGlkInput={(e) => setName(e.detail.value)}
1447
+ />
1448
+ );
1449
+ }
1450
+ ```
1451
+
1452
+ For React 18, custom-event listeners use the lowercase `onglkchange` form, or attach manually via `ref` + `addEventListener`. React 19 supports `onGlkChange`-style camelCase directly.
1453
+
1454
+ ### Vue 3
1455
+
1456
+ ```vue
1457
+ <template>
1458
+ <glk-input
1459
+ label="Name"
1460
+ :value="name"
1461
+ @glk-input="e => name = e.detail.value"
1462
+ />
1463
+ </template>
1464
+
1465
+ <script setup>
1466
+ import '@jungherz-de/glasskit-elements';
1467
+ import { ref } from 'vue';
1468
+ const name = ref('');
1469
+ </script>
1470
+ ```
1471
+
1472
+ ### Svelte
1473
+
1474
+ ```svelte
1475
+ <script>
1476
+ import '@jungherz-de/glasskit-elements';
1477
+ let name = '';
1478
+ </script>
1479
+
1480
+ <glk-input
1481
+ label="Name"
1482
+ value={name}
1483
+ on:glk-input={e => name = e.detail.value}
1484
+ />
1485
+ ```
1486
+
1487
+ ### lit-html / Hybrids
1488
+
1489
+ ```js
1490
+ import { html } from 'lit-html';
1491
+ import '@jungherz-de/glasskit-elements';
1492
+
1493
+ const template = (items) => html`
1494
+ <glk-list>
1495
+ ${items.map(item => html`
1496
+ <glk-list-item title=${item.title} subtitle=${item.subtitle} ?interactive=${true}>
1497
+ ${item.icon ? html`<svg slot="leading">...</svg>` : ''}
1498
+ </glk-list-item>
1499
+ `)}
1500
+ </glk-list>
1501
+ `;
1502
+ ```
1503
+
1504
+ Framework-safe note: `<glk-list>` and `<glk-list-item>` use pure slot projection — no child cloning — so lit-html parts, Hybrids templates, and React virtual-DOM bindings stay intact across re-renders.
1505
+
1506
+ ### SSR
1507
+
1508
+ GlassKit Elements are currently **client-side only** — they rely on `document.documentElement`, `adoptedStyleSheets`, and `ElementInternals` which are unavailable during server-side rendering. In SSR frameworks (Next.js, Nuxt, SvelteKit, Astro), use client-only directives or dynamic imports to defer hydration.
1509
+
1510
+ ---
1511
+
1512
+ ## 9. Custom Theming
1513
+
1514
+ Theming works identically to GlassKit CSS — define custom-property overrides in your global stylesheet; they pass through shadow boundaries automatically:
1515
+
1516
+ ```css
1517
+ :root {
1518
+ --gl-color-primary: #007AFF;
1519
+ --gl-color-primary-dark: #0055CC;
1520
+ --gl-color-primary-mid: #0066E0;
1521
+
1522
+ --gl-border-warm: rgba(0, 122, 255, 0.35);
1523
+ --gl-border-focus: rgba(0, 122, 255, 0.60);
1524
+
1525
+ --gl-shadow-btn-primary: 0 6px 24px rgba(0, 100, 220, 0.35),
1526
+ 0 2px 8px rgba(0, 0, 0, 0.15);
1527
+ --gl-shadow-focus: 0 0 0 3px rgba(0, 122, 255, 0.3);
1528
+ }
1529
+ ```
1530
+
1531
+ See the class-based [GlassKit CSS `SKILL.md`](https://github.com/JUNGHERZ/GlassKit/blob/main/SKILL.md) Section 2 for the complete list of design tokens (colors, surfaces, borders, blur, radii, spacing, shadows, typography).
1532
+
1533
+ ---
1534
+
1535
+ ## 10. Architecture Notes
1536
+
1537
+ | Concept | Location |
1538
+ |---|---|
1539
+ | Base class | `src/base.js` → `GlkElement`, `GlkFormElement` |
1540
+ | Theme sync | Single module-level `MutationObserver` in `base.js` |
1541
+ | Adopted stylesheets | `glassSheet` (from `@jungherz-de/glasskit/glasskit-styles.js`) + module-level `hostSheet` / `inlineHostSheet` |
1542
+ | Per-component structure | One `.js` file per element in `src/components/{category}/glk-{name}.js` |
1543
+ | Barrel | `src/index.js` — exports and registers all 36 elements, and exports `GlkElement` / `GlkFormElement` (since 1.14.0) |
1544
+ | Build | Rollup → IIFE (`glasskit-elements.js`), minified IIFE, ESM (`glasskit-elements.esm.js`), and per-element ESM in `dist/components/` with `base.js` as a stable entry |
1545
+
1546
+ Each element is a subclass of `GlkElement` (or `GlkFormElement` for form controls) and follows a consistent lifecycle:
1547
+
1548
+ 1. `constructor()` — attach open shadow, adopt GlassKit stylesheet + host display sheet
1549
+ 2. `connectedCallback()` — create theme wrapper, call `render()`, call `setupEvents()`, register for global theme sync
1550
+ 3. `render()` — build shadow DOM tree (override per component)
1551
+ 4. `setupEvents()` — attach DOM listeners (override per component)
1552
+ 5. `attributeChangedCallback()` — call `onAttributeChanged()` (override per component)
1553
+ 6. `disconnectedCallback()` — unregister, call `teardownEvents()`
1554
+
1555
+ The `GlkFormElement` subclass adds `ElementInternals` for native form participation via `this.setFormValue(value)`, plus `formResetCallback` → `resetValue()` and `formStateRestoreCallback` → `restoreValue()`.