diff --git a/CHANGELOG.md b/CHANGELOG.md index e75e737..0f7df77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ GlassKit uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [Unreleased] + +### Added + +- **Density tokens for the control sizes, and a compact preset: `data-density="compact"`.** The heights of fields, buttons, toggles, checkboxes, radios, list rows and modal actions were px literals in the component rules, so a denser layout — an admin screen, a desktop form, a narrow column — had to override a dozen selectors and keep the toggle's thumb, its travel and the first-line alignment of the labels in step by hand. They are tokens now, declared next to the spacing tokens with the values GlassKit has always drawn and read with that value as fallback, as `--gl-modal-max-width` is; `data-density="compact"` on ``, next to `data-theme`, swaps in a denser set, at load or at runtime: + + | Token | Default | Compact | + |---|---|---| + | `--gl-control-height` (input, select, search) | `52px` | `40px` | + | `--gl-control-padding-x` (input, select, textarea) | unset → `var(--gl-space-md)` | `var(--gl-space-sm)` | + | `--gl-btn-height` / `-sm` / `-lg` | `56px` / `44px` / `64px` | `40px` / `32px` / `48px` | + | `--gl-textarea-min-height` | `120px` | `88px` | + | `--gl-list-item-min-height` | `56px` | `48px` | + | `--gl-list-item-padding-y` | unset → `var(--gl-space-md)` | `10px` | + | `--gl-toggle-width` × `--gl-toggle-height` | `52px` × `30px` | `44px` × `26px` | + | `--gl-toggle-thumb-size` | `22px` | `18px` | + | `--gl-check-size` (checkbox, radio) | `24px` | `20px` | + | `--gl-modal-action-height` | `52px` | `44px` | + + Only these tokens change; colours, type and spacing stay, so nothing outside the controls moves. The toggle's thumb sits `(height − thumb) / 2 − 1px` inside the track — 3 px at 52 × 30, where it has always sat — and travels `width − height`, from the right and to the left under `dir="rtl"`; the invisible inputs that lie on top of the controls since 1.20.0 take the same sizes, and the first-line offsets of toggle, checkbox and radio are computed from them. Any value a project sets therefore stays aligned. Two paddings join the heights because the heights alone would not have shown: a list row's 16 px top and bottom padding already makes a plain row 50 px tall, so a 48 px minimum needs `--gl-list-item-padding-y`; and a 40 px field reads better with 12 px inside (`--gl-control-padding-x`, which the textarea follows too, so stacked fields keep one text edge). Both are left unset by default, like `--gl-toast-top`: the rules fall back to `--gl-space-md` as before, so a spacing override still reaches them wherever it is declared. The select keeps its padding pair under `:dir(rtl)`: the token takes the text side, left to right on the left and right to left on the right, and the chevron's room stays with the chevron. The chevron, the search icon and the check and radio glyphs keep their sizes. + + The preset is a bare attribute selector, like `[data-theme="light"]`, placed after the theme blocks, and the build puts it into `tokensCss` / `tokensSheet` with them. GlassKit Elements adopts only `componentsSheet` in its shadow roots and places the tokens on the document, so the compact values reach every element by inheritance — and so does a project's own `[data-density="compact"] { --gl-btn-height: 36px; }`, which a preset inside `componentsSheet` would have overridden in every shadow root. A `glassSheet` consumer whose shadow root holds an element with `data-theme` has to mirror `data-density` onto it as well, as GlassKit Elements does with its theme wrapper. + + Measured in Chromium and WebKit (Playwright 1.63) against 1.20.0: without the attribute every one of these controls has the same size, padding, thumb position, input box, label offset and first-line centre as before, left to right and right to left, and full-page screenshots — a test page in both themes, this repository's index, showcase and docs in English and German — are pixel-identical, apart from regions that also differ between two renders of 1.20.0. With `data-density="compact"` on ``, at load and switched at runtime: input, select and search 40 px with 12 px inner padding, buttons 40 / 32 / 48 px, the textarea 88 px, list rows 48 px with or without a 28 px icon (about 56 px with title and subtitle), the toggle 44 × 26 px with an 18 px thumb and the same gap at either end in both directions, checkbox and radio 20 px with their inputs on top, modal actions 44 px, every label still centred on its first line; under `dir="rtl"` the select's 12 px move to the right with the chevron on the left. Removing the attribute restores every value. + +### Changed + +- `build-styles-js.mjs` splits three token blocks off instead of two — `:root, [data-theme="dark"]`, `[data-theme="light"]` and `[data-density="compact"]` — and fails if `tokensCss` lacks the preset or `componentsCss` declares a density token. + +### Compatibility + +- Without `data-density` nothing changes, and a stylesheet that lacks the new tokens — an older `tokensCss` next to this `componentsSheet` — gets the old sizes from the fallbacks. +- Like every token of the dark block, the size defaults are declared again by an element below `` that carries `data-theme="dark"`; keep `data-density` on ``, next to `data-theme`. + +--- + ## [1.20.0] – 2026-10-05 The first batch of findings filed as GitHub issues, from a product that is adopting GlassKit at full depth; GlassKit Elements 1.20.0 ships the matching element changes. diff --git a/README.md b/README.md index de9bfa8..4478919 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,44 @@ The `theme-override.css` template comes with **4 example themes**: - 🌹 Rose - 🎨 Custom (empty, ready to fill) +### Density + +Admin screens, desktop layouts and narrow forms want denser controls. Set `data-density="compact"` on ``, next to `data-theme`: + +```html + +``` + +```js +// Switch at runtime +document.documentElement.setAttribute('data-density', 'compact'); +document.documentElement.removeAttribute('data-density'); // back to the default +``` + +Since **1.21.0** the control sizes are tokens, and the attribute swaps in a denser set. Without it every control keeps the size it has always had. Colours, type and spacing stay as they are, so nothing outside the controls moves: + +| Token | Default | Compact | +|---|---|---| +| `--gl-control-height` (input, select, search) | `52px` | `40px` | +| `--gl-control-padding-x` (input, select, textarea) | unset → `var(--gl-space-md)` | `var(--gl-space-sm)` | +| `--gl-btn-height` / `-sm` / `-lg` | `56px` / `44px` / `64px` | `40px` / `32px` / `48px` | +| `--gl-textarea-min-height` | `120px` | `88px` | +| `--gl-list-item-min-height` | `56px` | `48px` | +| `--gl-list-item-padding-y` | unset → `var(--gl-space-md)` | `10px` | +| `--gl-toggle-width` × `--gl-toggle-height` | `52px` × `30px` | `44px` × `26px` | +| `--gl-toggle-thumb-size` | `22px` | `18px` | +| `--gl-check-size` (checkbox, radio) | `24px` | `20px` | +| `--gl-modal-action-height` | `52px` | `44px` | + +Every token can also be set on its own, for one density or both — the toggle's thumb and the first-line alignment of toggle, checkbox and radio follow whatever size you give: + +```css +:root { --gl-btn-height: 48px; } /* default density */ +[data-density='compact'] { --gl-btn-height: 36px; } /* compact only */ +``` + +Like `data-theme`, keep `data-density` on ``: an element further down that carries `data-theme="dark"` re-declares the dark block, the default sizes included. + --- ## 🎛️ Design Tokens @@ -308,6 +346,12 @@ All visual values are controlled via CSS Custom Properties: /* Spacing */ --gl-space-xs … --gl-space-4xl +/* Density – control sizes, denser with data-density="compact" */ +--gl-control-height /* 52px – input, select */ +--gl-btn-height /* 56px; -sm 44px, -lg 64px */ +--gl-check-size /* 24px – checkbox, radio */ +--gl-toggle-width … --gl-modal-action-height + /* Shadows & Insets */ --gl-shadow-card --gl-inset-strong @@ -437,6 +481,8 @@ Since CSS Custom Properties penetrate the shadow boundary, **theme switching wor document.documentElement.setAttribute('data-theme', 'light'); ``` +`data-density="compact"` reaches the shadow roots the same way: the preset only sets tokens, and tokens cross the shadow boundary by inheritance. + > **Tip:** For maximum performance with many component instances, import `glassSheet` once and share it across all components. The browser keeps the stylesheet in memory only once. --- @@ -547,6 +593,10 @@ A few refinements arrive later and fall back quietly where they are missing: ## 📋 States & Modifiers – Cheat Sheet ``` +Page Attributes (on ): + data-theme="dark" / "light" + data-density="compact" (denser controls) + Interactive States: .is-active → Tab Bar item, Modal Overlay .is-open → Accordion item, Popover diff --git a/SKILL.md b/SKILL.md index bfb83a0..93fa478 100644 --- a/SKILL.md +++ b/SKILL.md @@ -89,6 +89,16 @@ and prose mirror. Pairs that must move together — the select's chevron and its toggle's thumb and its travel, the progress fill's gradient — are under `:dir(rtl)` (Chrome 120+); an older browser keeps them left to right rather than splitting them. +### Density + +Denser controls for admin screens, desktop layouts and narrow forms: `data-density="compact"` on ``, next to `data-theme` (since 1.21.0). + +```html + +``` + +Fields and buttons are 40px high instead of 52 and 56px; toggles, checkboxes, radios, list rows and modal actions shrink with them. Colours, type and spacing stay. Without the attribute nothing changes; `removeAttribute('data-density')` switches back at runtime. The values are the density tokens in section 2 — never shrink controls by overriding their `height`. It combines with `dir="rtl"`: the toggle's thumb and the select's padding mirror at either density. + --- ## 2. Design Tokens @@ -215,6 +225,28 @@ fully-translucent pre-1.7.0 chip. | `--gl-space-3xl` | `40px` | | `--gl-space-4xl` | `56px` | +### Density (since 1.21.0) + +The control sizes. `data-density="compact"` on `` switches to the second value column. + +| Token | Default | Compact | Used by | +|---|---|---|---| +| `--gl-control-height` | `52px` | `40px` | `.glass-input`, `.glass-select` (and the search field) | +| `--gl-control-padding-x` | unset → `var(--gl-space-md)` | `var(--gl-space-sm)` | Inner left/right padding of input, select, textarea | +| `--gl-btn-height` | `56px` | `40px` | `.glass-btn` | +| `--gl-btn-height-sm` | `44px` | `32px` | `.glass-btn--sm` | +| `--gl-btn-height-lg` | `64px` | `48px` | `.glass-btn--lg` | +| `--gl-textarea-min-height` | `120px` | `88px` | `.glass-textarea` | +| `--gl-list-item-min-height` | `56px` | `48px` | `.glass-list__item` | +| `--gl-list-item-padding-y` | unset → `var(--gl-space-md)` | `10px` | `.glass-list__item`, top and bottom | +| `--gl-toggle-width` | `52px` | `44px` | `.glass-toggle__track` | +| `--gl-toggle-height` | `30px` | `26px` | `.glass-toggle__track` | +| `--gl-toggle-thumb-size` | `22px` | `18px` | `.glass-toggle__thumb` | +| `--gl-check-size` | `24px` | `20px` | `.glass-checkbox__box`, `.glass-radio__circle` | +| `--gl-modal-action-height` | `52px` | `44px` | `.glass-modal__action` | + +The rules read each size with its default as fallback, like `--gl-modal-max-width`. The toggle's thumb inset and travel (in both directions) and the first-line alignment of toggle, checkbox and radio are derived from these, so any value stays aligned. The two paddings are unset by default and fall back to `--gl-space-md`, so a spacing override still reaches them. Set any token on `:root` for both densities, or on `[data-density="compact"]` for compact only. + ### Shadows | Token | Usage | @@ -478,6 +510,8 @@ Full-width buttons (56px height) with three variants and size modifiers. **Important:** Buttons default to `width: 100%`. Use `--auto` for inline/auto-width buttons. +**Heights** are density tokens: `--gl-btn-height`, `-sm`, `-lg` — 56 / 44 / 64px, or 40 / 32 / 48px with `data-density="compact"` on `` (since 1.21.0). + **Icons:** Button SVGs need **no inline `fill`/`stroke` attributes** – GlassKit styles them automatically: outline style (`fill: none; stroke: currentColor; stroke-width: 2`, round caps/joins) as the default, `--secondary`/`--tertiary` use their icon tokens, `--primary` renders icons filled. For deliberately filled icons (e.g. brand logos like the GitHub mark) add `glass-icon--fill` to the ``. **Links as buttons:** `.glass-btn` also works on `` elements – anchors render as `inline-flex` with `text-decoration: none`, so `--auto` shrink-wraps exactly like on a real `` or deeper elements. 8. **List dividers are automatic** – Never add a `
` or divider element between `.glass-list__item`s. The divider is rendered via `::after` and respects `:has(.glass-list__leading)` for the inset. 9. **Popover toggle naming** – Never name your popover toggle JS function `togglePopover`. It collides with the native `HTMLElement.togglePopover()` method. Use a prefix like `gkTogglePopover`. +10. **`data-density`** – Like `data-theme`, on ``: `compact` or absent. For a denser UI set the attribute or a density token, never `height` on the components. ### ❌ Common Mistakes @@ -1900,6 +1935,7 @@ Key points in this composition: | Putting `glass-list` inside `glass-popover` without `--bare` | Add `.glass-list--bare` to strip the double glass surface | | JS function named `togglePopover()` | Rename to `gkTogglePopover()` or similar to avoid native API clash | | Putting `.is-open` on the trigger button instead of `.glass-popover` | The state class belongs on the popover element | +| Overriding `height` of `.glass-btn`, `.glass-input` etc. for a denser UI | `data-density="compact"` on ``, or set a density token — toggle thumb and label alignment follow the tokens, not a hand-set `height` | --- @@ -1977,7 +2013,7 @@ customElements.define('my-card', MyCard); | `css` | `string` | The same, as a string | | `componentsSheet` | `CSSStyleSheet` | Component rules only, no token declarations (since 1.9.0) | | `componentsCss` | `string` | The same, as a string | -| `tokensSheet` | `CSSStyleSheet` | Only the two `[data-theme]` blocks that declare `--gl-*` (since 1.9.0) | +| `tokensSheet` | `CSSStyleSheet` | Only the blocks that declare `--gl-*`: the two `[data-theme]` blocks (since 1.9.0) and the `[data-density="compact"]` preset (since 1.21.0) | | `tokensCss` | `string` | The same, as a string | CSS Custom Properties (`--gl-*`) cross the shadow boundary by inheritance, so the example @@ -1988,6 +2024,7 @@ above picks up whatever the document defines and theme switching works globally. > element carrying `data-theme`, that selector matches it and re-declares every token > locally — and a matching rule always beats an inherited value. A project's > `:root { --gl-color-primary: … }` then silently stops arriving inside your component. +> The dark block declares the default control sizes too, so `` stops arriving as well — unless that element carries `data-density` mirrored from ``, which lets the compact preset (also in the full sheet) match it. > > If you need a themed element inside the shadow root, adopt `componentsSheet` instead > and make sure the tokens are present on the document: @@ -2060,6 +2097,13 @@ Overriding `--gl-color-success` / `--gl-color-error` is enough on its own: fill, text and glow of every state component are derived from those tokens. Only if you want a different ink than the derived one do you need to touch `--gl-color-{state}-on-surface`. +The density tokens (section 2) are overridden the same way, for both densities or for compact only: + +```css +:root { --gl-btn-height: 48px; } +[data-density='compact'] { --gl-btn-height: 36px; } +``` + Included theme templates in `theme-override.css`: - 🔵 Ocean Blue - 🟢 Emerald Green diff --git a/build-styles-js.mjs b/build-styles-js.mjs index 333a6ab..034b58c 100644 --- a/build-styles-js.mjs +++ b/build-styles-js.mjs @@ -7,7 +7,8 @@ // // The CSS is additionally emitted split in two: // -// tokensCss – the two [data-theme] blocks that declare --gl-* +// tokensCss – the blocks that declare --gl-*: the two [data-theme] +// blocks and the [data-density=compact] preset // componentsCss – everything else // // Why: a shadow root that adopts the *whole* sheet re-declares every token @@ -30,10 +31,13 @@ const pkg = JSON.parse(readFileSync('package.json', 'utf-8')); const css = readFileSync('glasskit.min.css', 'utf-8').replace(/\/\*# sourceMappingURL=.*?\*\/\s*$/, ''); // ---- Split off the token blocks ------------------------------------------- -// Both selectors appear exactly once in the minified output. The light block +// Each selector appears exactly once in the minified output. The light block // is matched with its brace attached so the `[data-theme=light] .glass-*` -// component rules are not mistaken for it. -const TOKEN_BLOCK_STARTS = [':root,[data-theme=dark]{', '[data-theme=light]{']; +// component rules are not mistaken for it; the density preset the same way. +// The preset belongs with the tokens: in componentsCss it would re-declare +// the density tokens inside every shadow root that carries the attribute, +// and a project's own density values would stop arriving there. +const TOKEN_BLOCK_STARTS = [':root,[data-theme=dark]{', '[data-theme=light]{', '[data-density=compact]{']; /** Returns [start, end) of the rule block beginning at `open`. */ function blockRange(source, open) { @@ -71,7 +75,9 @@ componentsCss += css.slice(cursor); const checks = [ [tokensCss.includes('--gl-color-primary:'), 'tokensCss is missing --gl-color-primary'], [tokensCss.includes('--gl-state-scrim:'), 'tokensCss is missing --gl-state-scrim'], + [tokensCss.includes('--gl-control-height:40px'), 'tokensCss is missing the compact density preset'], [!componentsCss.includes('--gl-color-primary:'), 'componentsCss still declares tokens'], + [!componentsCss.includes('--gl-control-height:'), 'componentsCss still declares density tokens'], [!css.includes('sourceMappingURL'), 'the embedded CSS still names a source map'], [componentsCss.includes('.glass-btn--primary'), 'componentsCss is missing component rules'], [componentsCss.includes('.glass-theme-toggle'), 'componentsCss lost the theme-toggle rules'], @@ -81,7 +87,7 @@ for (const [ok, message] of checks) { if (!ok) throw new Error(`Split check failed: ${message}`); } -// The two token blocks sit next to each other, so the full sheet is exactly +// The token blocks sit next to each other, so the full sheet is exactly // head + tokens + tail. Emitting the parts and re-joining them at runtime keeps // the module from carrying the CSS twice. const head = css.slice(0, ranges[0][0]); @@ -94,7 +100,7 @@ const output = [ `// GlassKit v${pkg.version} – Constructable Stylesheet`, '// Auto-generated by build-styles-js.mjs – do not edit manually.', '', - '// The [data-theme] blocks that declare --gl-* …', + '// The [data-theme] and [data-density] blocks that declare --gl-* …', `const tokensCss = ${JSON.stringify(tokensCss)};`, '', '// … and everything else, in two pieces around where they were.', diff --git a/de/docs.html b/de/docs.html index 7ee55d5..f46ad8a 100644 --- a/de/docs.html +++ b/de/docs.html @@ -387,6 +387,7 @@ Installation Design Tokens Theming (Light/Dark) + Dichte (Kompakt) Hintergrund @@ -530,6 +531,25 @@

Spacing & Radii

--gl-space-2xl32px--gl-radius-pill50% + +

Dichte

+ + + + + + + + + + + + + + + + +
TokenStandardKompaktVerwendung
--gl-control-height52px40pxInput, Select, Suchfeld
--gl-control-padding-xnicht gesetzt → --gl-space-md--gl-space-smInnenabstand links und rechts von Input, Select, Textarea
--gl-btn-height56px40pxButton
--gl-btn-height-sm44px32pxButton --sm
--gl-btn-height-lg64px48pxButton --lg
--gl-textarea-min-height120px88pxTextarea
--gl-list-item-min-height56px48pxListenzeile
--gl-list-item-padding-ynicht gesetzt → --gl-space-md10pxListenzeile, oben und unten
--gl-toggle-width × -height52 × 30px44 × 26pxToggle-Spur
--gl-toggle-thumb-size22px18pxToggle-Knopf
--gl-check-size24px20pxCheckbox, Radio
--gl-modal-action-height52px44pxModal-Aktionen
@@ -550,6 +570,43 @@

Rechts nach links

Seit 1.20.0 folgen die Komponenten der Schreibrichtung. dir="rtl" auf <html> oder einem beliebigen Container genügt: Abstände, Positionen und Rahmen sind logische Eigenschaften, also spiegeln sich Labels und Hinweise, das Such-Icon, die Trennlinien der Liste, die Popover-Anker, die Tab-Badges, die Modal-Aktionen, das × des Toasts und die Ausrichtung von Akkordeon, Liste, Tabelle und Prose. Wo zwei Werte gemeinsam wandern müssen — der Chevron des Selects und der Platz für ihn, der Knopf des Schalters und sein Weg, der Verlauf der Fortschrittsfüllung — steht die Regel unter :dir(rtl): Chrome und Edge ab 120, Safari ab 16.4, Firefox ab 49. Ein älterer Browser lässt diese Teile links nach rechts, statt sie auseinanderzureißen.

+ +
+

Dichte (Kompakt)

+

Admin-Oberflächen, Desktop-Layouts und schmale Formulare brauchen dichtere Bedienelemente. Seit 1.21.0 sind ihre Größen Dichte-Tokens, und data-density="compact" am <html>-Element setzt einen kleineren Satz: Felder und Buttons 40px hoch statt 52 und 56, kleinere Toggles, Checkboxen und Radios, dichtere Listenzeilen und Modal-Aktionen. Farben, Schrift und Abstände bleiben, wie sie sind. Ohne das Attribut behält jedes Element die Größe, die es immer hatte.

+
+
+
+ Standard + + + + +
+
+ Kompakt + + + + +
+
+
+

Die Vorschau setzt das Attribut auf ihre rechte Spalte. Auf einer Seite gehört es an <html>, neben data-theme: Ein Element weiter unten mit data-theme="dark" setzt die Standardgrößen wieder.

+
+
<html data-theme="dark" data-density="compact"> + +// Zur Laufzeit umschalten: +document.documentElement.setAttribute('data-density', 'compact'); +document.documentElement.removeAttribute('data-density'); + +/* Ein Token für beide Dichten – oder nur für kompakt: */ +:root { --gl-btn-height: 48px; } +[data-density='compact'] { --gl-btn-height: 36px; }
+
+

Einzug und Weg des Toggle-Knopfs — in beiden Schreibrichtungen — und die Ausrichtung von Toggle, Checkbox und Radio an der ersten Zeile leiten sich aus den Tokens ab. Jede Größe, die ein Projekt setzt, bleibt so ausgerichtet. Die Regeln lesen jede Größe mit dem bisherigen Wert als Fallback, wie --gl-modal-max-width: Eine Seite, deren Token-Sheet älter ist, behält die alten Größen.

+
+

Hintergrund

diff --git a/de/showcase.html b/de/showcase.html index 5e3641c..656c8ed 100644 --- a/de/showcase.html +++ b/de/showcase.html @@ -290,6 +290,13 @@

Komponenten
Showcase v1.20.0

+ +

Dichte

+
+ + +
+

Card (Glow)

@@ -1007,6 +1014,14 @@

Vertrag löschen?

html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark'); } + // Density: data-density="compact" on , or none + function setDensity(button, density) { + const html = document.documentElement; + if (density) html.setAttribute('data-density', density); + else html.removeAttribute('data-density'); + for (const b of button.parentElement.children) b.setAttribute('aria-pressed', String(b === button)); + } + // Background Switcher function toggleBgPopover(e) { e.stopPropagation(); diff --git a/docs.html b/docs.html index d9895ab..ff12853 100644 --- a/docs.html +++ b/docs.html @@ -387,6 +387,7 @@ Installation Design Tokens Theming (Light/Dark) + Density (Compact) Background
@@ -530,6 +531,25 @@

Spacing & Radii

--gl-space-2xl32px--gl-radius-pill50% + +

Density

+ + + + + + + + + + + + + + + + +
TokenDefaultCompactUsage
--gl-control-height52px40pxInput, select, search
--gl-control-padding-xunset → --gl-space-md--gl-space-smInner left/right padding of input, select, textarea
--gl-btn-height56px40pxButton
--gl-btn-height-sm44px32pxButton --sm
--gl-btn-height-lg64px48pxButton --lg
--gl-textarea-min-height120px88pxTextarea
--gl-list-item-min-height56px48pxList row
--gl-list-item-padding-yunset → --gl-space-md10pxList row, top and bottom
--gl-toggle-width × -height52 × 30px44 × 26pxToggle track
--gl-toggle-thumb-size22px18pxToggle thumb
--gl-check-size24px20pxCheckbox, radio
--gl-modal-action-height52px44pxModal actions
@@ -550,6 +570,43 @@

Right to left

Since 1.20.0 the components follow the writing direction. Set dir="rtl" on <html> or on any container: paddings, insets and borders are logical properties, so labels and hints, the search icon, list dividers, popover anchors, tab badges, modal actions, the toast’s close button and the alignment of accordion, list, table and prose all mirror. Where two values have to move together — the select’s chevron and the room kept for it, the toggle’s thumb and its travel, the progress fill’s gradient — the rule sits under :dir(rtl): Chrome and Edge from 120, Safari from 16.4, Firefox from 49. An older browser keeps those parts left to right instead of tearing them apart.

+ +
+

Density (Compact)

+

Admin screens, desktop layouts and narrow forms want denser controls. Since 1.21.0 the control sizes are density tokens, and data-density="compact" on the <html> element swaps in a smaller set: fields and buttons 40px high instead of 52 and 56, smaller toggles, checkboxes and radios, denser list rows and modal actions. Colours, type and spacing stay as they are. Without the attribute every control keeps the size it has always had.

+
+
+
+ Default + + + + +
+
+ Compact + + + + +
+
+
+

The preview sets the attribute on its right column. On a page it belongs on <html>, next to data-theme: an element further down that carries data-theme="dark" re-declares the default sizes.

+
+
<html data-theme="dark" data-density="compact"> + +// Switch at runtime: +document.documentElement.setAttribute('data-density', 'compact'); +document.documentElement.removeAttribute('data-density'); + +/* One token for both densities – or for compact only: */ +:root { --gl-btn-height: 48px; } +[data-density='compact'] { --gl-btn-height: 36px; }
+
+

The toggle's thumb inset and travel — in either writing direction — and the first-line alignment of toggle, checkbox and radio are derived from the tokens, so any size a project sets stays aligned. The rules read every size with today's value as fallback, like --gl-modal-max-width, so a page whose token sheet predates them keeps the old sizes.

+
+

Background

diff --git a/glasskit.css b/glasskit.css index fe3b850..a8f1158 100644 --- a/glasskit.css +++ b/glasskit.css @@ -6,6 +6,7 @@ - Scoped Reset (box-sizing fix) - Dark Mode (default) - Light Mode via [data-theme="light"] + - Compact density via [data-density="compact"] - 34 components (incl. floating Tab-Bar variant + Accessory) ============================================================ */ @@ -200,6 +201,29 @@ --gl-space-3xl: 40px; --gl-space-4xl: 56px; + /* ---- Density: control sizes ---- + The sizes GlassKit has always drawn its controls at, as tokens; the + rules read them with that value as fallback, like --gl-modal-max-width. + [data-density="compact"] (1c below) swaps in a denser set. The toggle's + thumb inset and travel and the first-line alignment of toggle, checkbox + and radio are derived from these, so any value set here stays aligned. + Two more are left unset on purpose, like --gl-toast-top: + --gl-control-padding-x (inner left and right of input, select and + textarea) and --gl-list-item-padding-y. Unset, the rules fall back to + --gl-space-md as before, so a spacing override still reaches them + wherever it is declared. */ + --gl-control-height: 52px; /* input, select */ + --gl-btn-height: 56px; + --gl-btn-height-sm: 44px; + --gl-btn-height-lg: 64px; + --gl-textarea-min-height: 120px; + --gl-list-item-min-height: 56px; + --gl-toggle-width: 52px; + --gl-toggle-height: 30px; + --gl-toggle-thumb-size: 22px; + --gl-check-size: 24px; /* checkbox, radio */ + --gl-modal-action-height: 52px; + /* ---- Typography ---- */ --gl-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; @@ -352,6 +376,37 @@ } +/* ============================================================ + 1c. DESIGN TOKENS – COMPACT DENSITY + ============================================================ */ +/* Opt-in, next to the theme: . Fields and + buttons at 40px instead of 52 and 56, smaller toggles and boxes, denser + list rows — for admin screens, desktop layouts and narrow forms. Only + the density tokens change; colours, type and spacing stay as they are. + + A bare attribute selector, like [data-theme="light"]: it matches , + and a theme wrapper that carries the attribute in a shadow root that + adopts the full sheet. build-styles-js.mjs moves it into tokensCss with + the theme blocks. It stays after them: the dark one declares the + defaults with the same specificity, and where both match, the later + block wins. */ +[data-density="compact"] { + --gl-control-height: 40px; + --gl-control-padding-x: var(--gl-space-sm); + --gl-btn-height: 40px; + --gl-btn-height-sm: 32px; + --gl-btn-height-lg: 48px; + --gl-textarea-min-height: 88px; + --gl-list-item-min-height: 48px; + --gl-list-item-padding-y: 10px; + --gl-toggle-width: 44px; + --gl-toggle-height: 26px; + --gl-toggle-thumb-size: 18px; + --gl-check-size: 20px; + --gl-modal-action-height: 44px; +} + + /* ============================================================ 2. HINTERGRUND ============================================================ */ @@ -524,7 +579,7 @@ ============================================================ */ .glass-btn { display: flex; align-items: center; justify-content: center; - gap: 10px; width: 100%; height: 56px; + gap: 10px; width: 100%; height: var(--gl-btn-height, 56px); border: none; border-radius: var(--gl-radius-btn); font-family: var(--gl-font-family); font-size: var(--gl-font-size-btn); @@ -603,8 +658,8 @@ a.glass-btn { display: inline-flex; text-decoration: none; } .glass-btn--tertiary:active { transform: translateY(1px); } /* Sizes */ -.glass-btn--sm { height: 44px; font-size: var(--gl-font-size-sm); border-radius: var(--gl-radius-sm); } -.glass-btn--lg { height: 64px; font-size: var(--gl-font-size-lg); } +.glass-btn--sm { height: var(--gl-btn-height-sm, 44px); font-size: var(--gl-font-size-sm); border-radius: var(--gl-radius-sm); } +.glass-btn--lg { height: var(--gl-btn-height-lg, 64px); font-size: var(--gl-font-size-lg); } .glass-btn--auto { width: auto; padding: 0 var(--gl-space-xl); } @@ -652,8 +707,8 @@ a.glass-btn { display: inline-flex; text-decoration: none; } } .glass-input { - width: 100%; height: 52px; - padding: 0 var(--gl-space-md); + width: 100%; height: var(--gl-control-height, 52px); + padding: 0 var(--gl-control-padding-x, var(--gl-space-md)); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-light)); -webkit-backdrop-filter: blur(var(--gl-blur-light)); @@ -708,8 +763,8 @@ a.glass-btn { display: inline-flex; text-decoration: none; } /* Textarea */ .glass-textarea { - width: 100%; min-height: 120px; - padding: var(--gl-space-md); + width: 100%; min-height: var(--gl-textarea-min-height, 120px); + padding: var(--gl-space-md) var(--gl-control-padding-x, var(--gl-space-md)); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-light)); -webkit-backdrop-filter: blur(var(--gl-blur-light)); @@ -735,8 +790,8 @@ a.glass-btn { display: inline-flex; text-decoration: none; } 9. SELECT ============================================================ */ .glass-select { - width: 100%; height: 52px; - padding: 0 var(--gl-space-3xl) 0 var(--gl-space-md); + width: 100%; height: var(--gl-control-height, 52px); + padding: 0 var(--gl-space-3xl) 0 var(--gl-control-padding-x, var(--gl-space-md)); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-light)); -webkit-backdrop-filter: blur(var(--gl-blur-light)); @@ -757,7 +812,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } /* Right to left: the chevron and the room kept for it move to the left — together, under :dir(), so a browser without it keeps both on the right. */ .glass-select:dir(rtl) { - padding: 0 var(--gl-space-md) 0 var(--gl-space-3xl); + padding: 0 var(--gl-control-padding-x, var(--gl-space-md)) 0 var(--gl-space-3xl); background-position: left 16px center; } .glass-select:focus { @@ -795,7 +850,8 @@ a.glass-btn { display: inline-flex; text-decoration: none; } /* ============================================================ 11. TOGGLE SWITCH ============================================================ */ -/* First-line alignment — see the note on .glass-checkbox below. Track is 30px. */ +/* First-line alignment — see the note on .glass-checkbox below. The track + is --gl-toggle-height high (30px by default). */ .glass-toggle { display: inline-flex; align-items: flex-start; gap: var(--gl-space-sm); cursor: pointer; @@ -812,12 +868,12 @@ a.glass-btn { display: inline-flex; text-decoration: none; } the lh unit drops it and keeps margin: 0, as the track does. */ .glass-toggle__input { position: absolute; z-index: 1; opacity: 0; cursor: inherit; - width: 52px; height: 30px; margin: 0; font: inherit; - margin-top: max(0px, calc((1lh - 30px) / 2)); + width: var(--gl-toggle-width, 52px); height: var(--gl-toggle-height, 30px); margin: 0; font: inherit; + margin-top: max(0px, calc((1lh - var(--gl-toggle-height, 30px)) / 2)); } .glass-toggle__track { - position: relative; width: 52px; height: 30px; + position: relative; width: var(--gl-toggle-width, 52px); height: var(--gl-toggle-height, 30px); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-soft)); -webkit-backdrop-filter: blur(var(--gl-blur-soft)); @@ -826,12 +882,19 @@ a.glass-btn { display: inline-flex; text-decoration: none; } transition: var(--gl-transition); box-shadow: var(--gl-inset-subtle); flex-shrink: 0; - margin-top: max(0px, calc((1lh - 30px) / 2)); + margin-top: max(0px, calc((1lh - var(--gl-toggle-height, 30px)) / 2)); } +/* Inset and travel follow the track. Top and left (right, right to left) + are (height − thumb) / 2 − 1px inside the 1.5px border: 3px at 52 × 30, + where the thumb has always sat (taking off the whole border would move + it by half a pixel). It travels width − height, so the gap at the end + mirrors the one below. */ .glass-toggle__thumb { - position: absolute; top: 3px; left: 3px; - width: 22px; height: 22px; + position: absolute; + top: calc((var(--gl-toggle-height, 30px) - var(--gl-toggle-thumb-size, 22px)) / 2 - 1px); + left: calc((var(--gl-toggle-height, 30px) - var(--gl-toggle-thumb-size, 22px)) / 2 - 1px); + width: var(--gl-toggle-thumb-size, 22px); height: var(--gl-toggle-thumb-size, 22px); border-radius: var(--gl-radius-pill); background: var(--gl-toggle-thumb-bg); box-shadow: 0 2px 6px rgba(0,0,0,0.2), var(--gl-inset-milk); @@ -846,13 +909,15 @@ a.glass-btn { display: inline-flex; text-decoration: none; } } .glass-toggle__input:checked + .glass-toggle__track .glass-toggle__thumb { - transform: translateX(22px); + transform: translateX(calc(var(--gl-toggle-width, 52px) - var(--gl-toggle-height, 30px))); } /* Right to left the thumb starts on the right and travels left — both under :dir(), so a browser without it keeps the left-to-right switch. */ -.glass-toggle__thumb:dir(rtl) { left: auto; right: 3px; } +.glass-toggle__thumb:dir(rtl) { + left: auto; right: calc((var(--gl-toggle-height, 30px) - var(--gl-toggle-thumb-size, 22px)) / 2 - 1px); +} .glass-toggle__input:checked + .glass-toggle__track .glass-toggle__thumb:dir(rtl) { - transform: translateX(-22px); + transform: translateX(calc(var(--gl-toggle-height, 30px) - var(--gl-toggle-width, 52px))); } .glass-toggle__input:focus-visible + .glass-toggle__track { box-shadow: var(--gl-shadow-focus); } @@ -861,7 +926,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } font-size: var(--gl-font-size-base); color: var(--gl-color-text); user-select: none; - margin-top: max(0px, calc((30px - 1lh) / 2)); + margin-top: max(0px, calc((var(--gl-toggle-height, 30px) - 1lh) / 2)); } /* A control without a label keeps no gap for one. */ .glass-toggle__label:empty, @@ -874,11 +939,12 @@ a.glass-btn { display: inline-flex; text-decoration: none; } ============================================================ */ /* Control and label are aligned on the *first line*, not on the middle of the text block — a consent label runs over several lines and a centred box then - points at nothing. The two margins below centre the 24px box on that first - line; whichever is the shorter of the two gets the offset, so exactly one is - ever non-zero. For a single-line label this is pixel-identical to the - `align-items: center` of pre-1.11.0. Repeated on .glass-radio (24px) and - .glass-toggle (30px) — keep the three in step. */ + points at nothing. The two margins below centre the box (--gl-check-size, + 24px by default) on that first line; whichever is the shorter of the two + gets the offset, so exactly one is ever non-zero. For a single-line label + this is pixel-identical to the `align-items: center` of pre-1.11.0. + Repeated on .glass-radio (--gl-check-size) and .glass-toggle + (--gl-toggle-height) — keep the three in step. */ .glass-checkbox { display: inline-flex; align-items: flex-start; gap: var(--gl-space-sm); cursor: pointer; @@ -887,12 +953,12 @@ a.glass-btn { display: inline-flex; text-decoration: none; } /* On top of the box, covering it only — see .glass-toggle__input. */ .glass-checkbox__input { position: absolute; z-index: 1; opacity: 0; cursor: inherit; - width: 24px; height: 24px; margin: 0; font: inherit; - margin-top: max(0px, calc((1lh - 24px) / 2)); + width: var(--gl-check-size, 24px); height: var(--gl-check-size, 24px); margin: 0; font: inherit; + margin-top: max(0px, calc((1lh - var(--gl-check-size, 24px)) / 2)); } .glass-checkbox__box { - width: 24px; height: 24px; + width: var(--gl-check-size, 24px); height: var(--gl-check-size, 24px); border-radius: var(--gl-radius-xs); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-soft)); @@ -902,7 +968,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } transition: var(--gl-transition); box-shadow: var(--gl-inset-subtle); flex-shrink: 0; - margin-top: max(0px, calc((1lh - 24px) / 2)); + margin-top: max(0px, calc((1lh - var(--gl-check-size, 24px)) / 2)); } .glass-checkbox__box svg { @@ -925,7 +991,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } .glass-checkbox__label { font-size: var(--gl-font-size-base); color: var(--gl-color-text); user-select: none; - margin-top: max(0px, calc((24px - 1lh) / 2)); + margin-top: max(0px, calc((var(--gl-check-size, 24px) - 1lh) / 2)); } @@ -941,12 +1007,12 @@ a.glass-btn { display: inline-flex; text-decoration: none; } /* On top of the circle, covering it only — see .glass-toggle__input. */ .glass-radio__input { position: absolute; z-index: 1; opacity: 0; cursor: inherit; - width: 24px; height: 24px; margin: 0; font: inherit; - margin-top: max(0px, calc((1lh - 24px) / 2)); + width: var(--gl-check-size, 24px); height: var(--gl-check-size, 24px); margin: 0; font: inherit; + margin-top: max(0px, calc((1lh - var(--gl-check-size, 24px)) / 2)); } .glass-radio__circle { - width: 24px; height: 24px; + width: var(--gl-check-size, 24px); height: var(--gl-check-size, 24px); border-radius: var(--gl-radius-pill); background: var(--gl-surface-2); backdrop-filter: blur(var(--gl-blur-soft)); @@ -955,7 +1021,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } transition: var(--gl-transition); box-shadow: var(--gl-inset-subtle); flex-shrink: 0; - margin-top: max(0px, calc((1lh - 24px) / 2)); + margin-top: max(0px, calc((1lh - var(--gl-check-size, 24px)) / 2)); } .glass-radio__dot { @@ -978,7 +1044,7 @@ a.glass-btn { display: inline-flex; text-decoration: none; } .glass-radio__label { font-size: var(--gl-font-size-base); color: var(--gl-color-text); user-select: none; - margin-top: max(0px, calc((24px - 1lh) / 2)); + margin-top: max(0px, calc((var(--gl-check-size, 24px) - 1lh) / 2)); } @@ -1166,7 +1232,7 @@ dialog.glass-modal-overlay::backdrop { background: transparent; } .glass-modal__footer { display: flex; border-top: 1px solid var(--gl-border-subtle); } .glass-modal__action { - flex: 1; height: 52px; + flex: 1; height: var(--gl-modal-action-height, 52px); background: transparent; border: none; font-family: var(--gl-font-family); font-size: var(--gl-font-size-btn); @@ -1894,8 +1960,8 @@ dialog.glass-modal-overlay::backdrop { background: transparent; } display: flex; align-items: center; gap: var(--gl-space-sm); - padding: var(--gl-space-md) var(--gl-space-lg); - min-height: 56px; + padding: var(--gl-list-item-padding-y, var(--gl-space-md)) var(--gl-space-lg); + min-height: var(--gl-list-item-min-height, 56px); color: var(--gl-color-text); font-size: var(--gl-font-size-base); text-decoration: none; diff --git a/showcase.html b/showcase.html index 1598fdd..338ebae 100644 --- a/showcase.html +++ b/showcase.html @@ -290,6 +290,13 @@

Component
Showcase v1.20.0

+ +

Density

+
+ + +
+

Card (Glow)

@@ -1007,6 +1014,14 @@

Delete document?

html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark'); } + // Density: data-density="compact" on , or none + function setDensity(button, density) { + const html = document.documentElement; + if (density) html.setAttribute('data-density', density); + else html.removeAttribute('data-density'); + for (const b of button.parentElement.children) b.setAttribute('aria-pressed', String(b === button)); + } + // Background Switcher function toggleBgPopover(e) { e.stopPropagation(); diff --git a/theme-override.css b/theme-override.css index f4ee142..9c6bce2 100644 --- a/theme-override.css +++ b/theme-override.css @@ -126,6 +126,26 @@ */ +/* ============================================================ + BEISPIEL: Dichtere Bedienelemente (seit 1.21.0) + + Für die ganze Seite genügt . + Einzelne Größen lassen sich setzen wie jedes Token: auf :root + für beide Dichten, auf [data-density="compact"] nur für die + kompakte. Knopf und Zeilenausrichtung von Toggle, Checkbox + und Radio folgen der gesetzten Größe. + ============================================================ */ + +/* +:root { + --gl-btn-height: 48px; +} +[data-density="compact"] { + --gl-btn-height: 36px; +} +*/ + + /* ============================================================ DEIN CUSTOM THEME Entkommentiere und passe an: