Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [Unreleased]

### Added

- **`data-density` on `<html>` reaches the elements, like `data-theme`.** GlassKit 1.21.0 turns the sizes of fields, buttons, toggles, checkboxes, radios, list rows and modal actions into density tokens, with `data-density="compact"` on `<html>` as a denser preset for admin screens, desktop layouts and narrow forms. The values arrive in every shadow root by inheritance — GlassKit puts the preset into `tokensCss`, which the elements place on the document in `@layer glasskit-defaults` — so a project's own density values (`[data-density='compact'] { --gl-btn-height: 36px; }`) reach the elements too; with `data-glk-defaults="off"` the preset comes from `glasskit.css` on the page, like every other token. The global `MutationObserver` now watches `data-density` next to `data-theme` and `data-glk-defaults` and mirrors it onto every element's `.glk-wrapper`: set when the wrapper is created, updated when `<html>` changes, removed when `<html>` has none. A rule keyed on the attribute — a GlassKit component rule, the `hostStyles` of a project's own `GlkElement` — therefore matches inside the shadow root, as it does for the theme. `GlkElement` gains `_syncDensity(density)` next to `_syncTheme(theme)`; the observer calls each only for the attribute that changed. The compact sizes need GlassKit 1.21.0: the bundles carry the GlassKit sheet they were built against, which in `dist/` is still the lockfile's 1.20.0 — rebuilt against 1.21.0, they bring the tokens along; the per-component files use the GlassKit the project installs.

### Fixed

- **An element that was out of the document while the theme changed came back with the old theme on its wrapper.** The observer only reaches connected elements, and the wrapper took `data-theme` once, when it was created; an element removed and re-inserted after a theme switch — a cached view, a list a framework re-sorts — kept the old value. Since 1.20.0 that value also sets the wrapper's `color-scheme`, so such an element drew its native parts — a select's list, a date picker, scrollbars — in the other scheme. It now takes `data-theme` and `data-density` from `<html>` again whenever it is connected anew.

Verified in Chromium and WebKit (Playwright 1.63), the elements built against the GlassKit branch with the density tokens: without `data-density` every element measures as with 1.20.0, and the elements' index, showcase and docs (EN/DE) render pixel-identical, apart from regions that also differ between two renders of 1.20.0; with `data-density="compact"` on `<html>`, at load and switched at runtime, `<glk-input>`, `<glk-select>` and `<glk-search>` measure 40 px with 12 px inner padding, `<glk-button>` 40 / 32 / 48 px, `<glk-textarea>` 88 px, `<glk-list-item>` 48 px, `<glk-toggle>` 44 × 26 px with an 18 px thumb, `<glk-checkbox>` and `<glk-radio>` 20 px, the `<glk-modal>` actions 44 px — with and without `glasskit.css` on the page, left to right and right to left. Every wrapper carries `data-density="compact"` and loses it with `<html>`; an element created after the switch starts with it, one detached during the switch catches up when it is inserted again, its `color-scheme` included.

---

## [1.20.0] – 2026-10-05

Versions realign with GlassKit at 1.20.0, which this release needs (peer dependency `>=1.20.0`). The first batch of findings filed as GitHub issues, from a product that is adopting GlassKit at full depth — mostly accessibility and forms. Everything below was measured in Chromium and WebKit.
Expand Down Expand Up @@ -735,6 +749,7 @@ Starting with this release, GlassKit Elements version numbers are aligned with G

---

[Unreleased]: https://github.com/JUNGHERZ/GlassKit-Elements/compare/v1.20.0...HEAD
[1.20.0]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.20.0
[1.19.2]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.19.2
[1.19.1]: https://github.com/JUNGHERZ/GlassKit-Elements/releases/tag/v1.19.1
Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ It is the **app layer** of the GlassKit family — three layers, one design lang
| 🪶 **Lightweight** | 156 KB minified / 28 KB gzipped (IIFE bundle, GlassKit's component rules included), no external dependencies |
| 📦 **Three bundle formats** | IIFE for `<script>`, minified IIFE for production, ESM for bundlers & tree-shaking |
| 🎛️ **Form Participation** | Input, toggle, checkbox, radio, select — all work natively with `<form>` via `ElementInternals`: value, reset and, since 1.20.0, validity; `<glk-button type="submit">` submits |
| 🌗 **Theme Sync** | Automatic dark/light mode sync via `data-theme` on `<html>` |
| 🌗 **Theme Sync** | Automatic dark/light mode sync via `data-theme` on `<html>`, compact controls via `data-density` |
| 📱 **Mobile-first** | Inherits GlassKit's mobile-optimized design with `safe-area-inset` support |

---
Expand Down Expand Up @@ -204,6 +204,18 @@ const current = html.getAttribute('data-theme');
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
```

### Density

Admin screens, desktop layouts and narrow forms want denser controls. Set `data-density="compact"` on `<html>`, next to `data-theme`:

```html
<html data-theme="dark" data-density="compact">
```

Fields and buttons become 40px high instead of 52 and 56, toggles, checkboxes and radios smaller, list rows and modal actions denser; `removeAttribute('data-density')` switches back at runtime. Without the attribute nothing changes.

> **Requires GlassKit CSS >= 1.21.0.** The sizes are GlassKit's density tokens, so they reach every shadow root by inheritance, like the brand colors below — and so do your own: `[data-density='compact'] { --gl-btn-height: 36px; }`. With `data-glk-defaults="off"` the preset comes from `glasskit.css` on the page, like every other token. The global observer mirrors `data-density` onto each element's theme wrapper as well, so a rule keyed on the attribute matches inside the shadow root too.

### Icons

Pass an icon as a **direct child**, not wrapped:
Expand Down Expand Up @@ -256,15 +268,15 @@ itself, or wants them on part of the page only, switches the defaults off:
## 🛠️ Architecture

- **Shadow DOM** with `adoptedStyleSheets` — GlassKit's `componentsSheet` is shared across all component instances; token declarations stay on the document so branding can be inherited
- **Theme wrapper** with `display: contents` — layout-transparent `<div>` for `data-theme` CSS selectors
- **Global `MutationObserver`** — single observer watches `data-theme` changes and syncs all instances
- **Theme wrapper** with `display: contents` — layout-transparent `<div>` for `data-theme` and `data-density` CSS selectors
- **Global `MutationObserver`** — single observer watches `data-theme` and `data-density` changes and syncs all instances
- **`GlkElement`** base class — handles Shadow DOM setup, theme sync, attribute reflection
- **`GlkFormElement`** extends `GlkElement` — adds `ElementInternals` for native form participation, delegates focus to its field and reports the field's validity
- **Native semantics** — labels tied to their fields, the toggle's `role="switch"` on its native checkbox, `<glk-modal>` a native `<dialog>`, `aria-expanded` on accordion and popover triggers

### Build your own element

Since 1.14.0 both base classes are exported, so a project element inherits the whole setup — open shadow root with GlassKit's stylesheet adopted (`.glass-*` classes work inside), theme wrapper following `data-theme`, listeners re-armed when the element moves, `emit()` for bubbling, composed events:
Since 1.14.0 both base classes are exported, so a project element inherits the whole setup — open shadow root with GlassKit's stylesheet adopted (`.glass-*` classes work inside), theme wrapper following `data-theme` and `data-density`, listeners re-armed when the element moves, `emit()` for bubbling, composed events:

```js
import { GlkElement } from '@jungherz-de/glasskit-elements'; // or '…/base.js', or GlassKitElements.GlkElement from the CDN bundle
Expand Down
19 changes: 15 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ function toggleTheme() {

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.

### Density

```html
<html data-theme="dark" data-density="compact">
```

Denser controls for admin screens, desktop layouts and narrow forms: fields and buttons 40px instead of 52 and 56, smaller toggles, checkboxes and radios, denser list rows and modal actions. Without the attribute nothing changes; `removeAttribute('data-density')` switches back at runtime. The sizes are GlassKit's density tokens (GlassKit 1.21.0 or later), so they reach every shadow root by inheritance, and a project's own values do too (`[data-density='compact'] { --gl-btn-height: 36px; }`); with `data-glk-defaults="off"` the preset comes from `glasskit.css` on the page, like every other token. The same observer mirrors `data-density` onto each element's theme wrapper — set it on `<html>` only, never on individual elements.

---

## 2. Core Concepts
Expand All @@ -98,7 +106,7 @@ A single module-level `MutationObserver` watches `data-theme` on `<html>` and sy
| Tag prefix | `glk-*` (analogous to the `glass-*` CSS prefix) |
| Rendering | Shadow DOM (`mode: 'open'`) with `adoptedStyleSheets` |
| 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 |
| Theme sync | Global MutationObserver on `<html data-theme>` |
| Theme sync | Global MutationObserver on `<html data-theme>` and `<html data-density>` |
| Events | Custom `glk-*` events, all `bubbles: true, composed: true` |
| Form participation | `GlkFormElement` uses `ElementInternals` (`static formAssociated = true`): value, reset and — since 1.20.0 — the inner field's validity |
| Focus | Form elements with one field delegate focus (since 1.20.0): `element.focus()`, a click on the label, a `<label for>` land in the field |
Expand All @@ -108,7 +116,7 @@ Custom properties (`--gl-*`) defined on `:root` or `<html>` pass through shadow

### Building your own element (since 1.14.0)

`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.
`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` and `data-density`, listeners re-armed when the element moves, `emit()` for bubbling, composed events.

```js
class DemoCounter extends GlkElement {
Expand All @@ -131,7 +139,7 @@ customElements.define('demo-counter', DemoCounter);

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.

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`.
Rules for a subclass: build only inside `this._wrapper` (it carries `data-theme`, and `data-density` while `<html>` has one); 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`.

### Light-DOM children (since 1.12.0)

Expand All @@ -154,6 +162,8 @@ listeners and its theme-sync registration are re-armed on every connect, so it k
working. Before 1.12.0 they were torn down on disconnect and never restored: the
element still looked right but no longer fired events.

An element that was out of the document while `data-theme` or `data-density` changed on `<html>` catches up when it is connected again (since 1.21.0); before, its wrapper kept the old value — and since 1.20.0 that value also sets the `color-scheme` of the native parts inside.

### Branding (since 1.9.0)

Override `--gl-*` tokens anywhere in the document — a brand stylesheet, a `<style>` block,
Expand Down Expand Up @@ -1433,7 +1443,7 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda

### Always follow

1. **`data-theme` on `<html>`** — never on `<body>` or individual elements; the global MutationObserver only watches `<html>`.
1. **`data-theme` and `data-density` on `<html>`** — never on `<body>` or individual elements; the global MutationObserver only watches `<html>`.
2. **Form elements belong inside a `<form>`** — otherwise `ElementInternals.setFormValue()` has no effect and `new FormData()` will be empty.
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.
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.
Expand All @@ -1452,6 +1462,7 @@ All `glk-*` events bubble and are `composed: true`, so they pierce shadow bounda
| `<glk-list>` inside `<glk-popover>` without `bare` | Add `bare` to the inner list so only the popover surface has glass. |
| `document.querySelector('.glass-modal').classList.add('is-active')` | Set `<glk-modal>.open = true` instead — the element manages `.is-active` internally. |
| `data-theme` directly on a `<glk-*>` element | Set it on `<html>`; theme propagates automatically through the MutationObserver. |
| Shrinking elements one by one (`::part`, host heights) for a denser UI | `data-density="compact"` on `<html>`, or a density token such as `--gl-btn-height` on `:root` — GlassKit 1.21.0 or later. |
| Form element outside `<form>` | Wrap in a `<form>`; `ElementInternals` needs a form owner. |
| 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. |
| Forgetting `slot="trigger"` on the popover trigger element | Without it, the popover's internal click handler can't identify the trigger and never toggles. |
Expand Down
Loading
Loading