@timmo001/groundwork-ui 0.9.12 → 0.9.14

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.
@@ -1540,7 +1540,7 @@
1540
1540
  "text": "readonly CSSStyleSheet[]"
1541
1541
  },
1542
1542
  "static": true,
1543
- "default": "[ base, css` :host { position: sticky; inset-block-start: 0; z-index: 50; display: block; } :host([overlay]) { position: fixed; inset-inline: 0; } .bar { position: relative; display: flex; gap: var(--gw-space-3); align-items: center; min-height: 3.5rem; padding: var(--gw-space-1) max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2); border-block-end: max(var(--gw-border-width), 1px) solid transparent; transition: background-color var(--gw-motion-normal) var(--gw-motion-easing), border-color var(--gw-motion-normal) var(--gw-motion-easing), backdrop-filter var(--gw-motion-normal) var(--gw-motion-easing); } :host([floating]) .bar, :host([menu-open]) .bar { background: color-mix( in oklab, var(--gw-color-surface-default) 82%, transparent ); border-color: var(--gw-color-border-quiet); backdrop-filter: blur(12px) saturate(1.4); } /* A one-column grid, so a hidden brand can fold its width to nothing and the nav slides over as it opens, rather than leaving a gap */ .brand { display: grid; flex: none; grid-template-columns: 1fr; align-items: center; transition: grid-template-columns var(--gw-motion-slow) var(--gw-motion-easing), margin var(--gw-motion-slow) var(--gw-motion-easing), opacity var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .brand > slot { display: flex; align-items: center; min-width: 0; overflow: clip; overflow-clip-margin: calc( var(--gw-focus-width) + var(--gw-focus-offset) ); } :host([reveal-after]:not([floating])) .brand { grid-template-columns: 0fr; margin-inline-end: calc(var(--gw-space-3) * -1); pointer-events: none; visibility: hidden; opacity: 0; translate: 0 -0.25rem; transition: grid-template-columns var(--gw-motion-slow) var(--gw-motion-easing), margin var(--gw-motion-slow) var(--gw-motion-easing), opacity var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), visibility var(--gw-motion-slow) allow-discrete; } .nav { display: flex; flex: 1; gap: var(--gw-space-1); align-items: center; min-width: 0; } .actions { display: flex; flex: none; gap: var(--gw-space-1); align-items: center; margin-inline-start: auto; } .menu { display: none; place-items: center; width: 2.5rem; height: 2.5rem; padding: 0; color: var(--gw-color-text-strong); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); } .menu:hover { background: var(--gw-color-fill-neutral-hovered); } /* Below menu-below, which sets collapsed, the nav folds into a menu */ :host([collapsed]) .menu { display: inline-grid; } :host([collapsed]) .nav { position: absolute; inset-block-start: 100%; inset-inline: 0; flex-direction: column; gap: var(--gw-space-0-5); align-items: stretch; padding: var(--gw-space-2) max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2); visibility: hidden; background: var(--gw-color-surface-default); border-block-end: max(var(--gw-border-width), 1px) solid var(--gw-color-border-quiet); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.5rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), visibility var(--gw-motion-normal) allow-discrete; } :host([collapsed][menu-open]) .nav { visibility: visible; opacity: 1; translate: 0 0; } `, ]",
1543
+ "default": "[ base, css` :host { position: sticky; inset-block-start: 0; z-index: 50; display: block; } :host([overlay]) { position: fixed; inset-inline: 0; } .bar { position: relative; display: flex; gap: var(--gw-space-3); align-items: center; min-height: 3.5rem; padding: var(--gw-space-1) max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2); border-block-end: max(var(--gw-border-width), 1px) solid transparent; transition: background-color var(--gw-motion-normal) var(--gw-motion-easing), border-color var(--gw-motion-normal) var(--gw-motion-easing), backdrop-filter var(--gw-motion-normal) var(--gw-motion-easing); } :host([floating]) .bar, :host([menu-open]) .bar { background: color-mix( in oklab, var(--gw-color-surface-default) 82%, transparent ); border-color: var(--gw-color-border-quiet); backdrop-filter: blur(12px) saturate(1.4); } /* A one-column grid, so a hidden brand can fold its width to nothing and the nav slides over as it opens, rather than leaving a gap */ .brand { display: grid; flex: none; grid-template-columns: 1fr; align-items: center; transition: grid-template-columns var(--gw-motion-slow) var(--gw-motion-easing), margin var(--gw-motion-slow) var(--gw-motion-easing), opacity var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .brand > slot { display: flex; align-items: center; min-width: 0; overflow: clip; overflow-clip-margin: calc( var(--gw-focus-width) + var(--gw-focus-offset) ); } :host([reveal-after]:not([floating])) .brand { grid-template-columns: 0fr; margin-inline-end: calc(var(--gw-space-3) * -1); pointer-events: none; visibility: hidden; opacity: 0; translate: 0 -0.25rem; transition: grid-template-columns var(--gw-motion-slow) var(--gw-motion-easing), margin var(--gw-motion-slow) var(--gw-motion-easing), opacity var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), visibility var(--gw-motion-slow) allow-discrete; } /* The nav keeps its room; the actions take the rest, which a theme picker fills or gives up by moving its controls into its popover */ .nav { display: flex; flex: none; gap: var(--gw-space-1); align-items: center; min-width: 0; } .actions { display: flex; flex: 1 1 auto; gap: var(--gw-space-1); align-items: center; justify-content: flex-end; } .menu { display: none; place-items: center; width: 2.5rem; height: 2.5rem; padding: 0; color: var(--gw-color-text-strong); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); } .menu:hover { background: var(--gw-color-fill-neutral-hovered); } /* Below menu-below, which sets collapsed, the nav folds into a menu */ :host([collapsed]) .menu { display: inline-grid; } :host([collapsed]) .nav { position: absolute; inset-block-start: 100%; inset-inline: 0; flex-direction: column; gap: var(--gw-space-0-5); align-items: stretch; padding: var(--gw-space-2) max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2); visibility: hidden; background: var(--gw-color-surface-default); border-block-end: max(var(--gw-border-width), 1px) solid var(--gw-color-border-quiet); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.5rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), visibility var(--gw-motion-normal) allow-discrete; } :host([collapsed][menu-open]) .nav { visibility: visible; opacity: 1; translate: 0 0; } `, ]",
1544
1544
  "inheritedFrom": {
1545
1545
  "name": "GwElement",
1546
1546
  "module": "src/element.ts"
@@ -6414,7 +6414,7 @@
6414
6414
  "text": "readonly CSSStyleSheet[]"
6415
6415
  },
6416
6416
  "static": true,
6417
- "default": "[ base, dismissalStyles, css` dialog { width: min(32rem, calc(100vw - 2 * var(--gw-space-3))); padding: 0; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-l); } dialog::backdrop { background: rgb(0 0 0 / 50%); } /* Fades and lifts in and out, the backdrop with it */ dialog, dialog::backdrop { transition: opacity var(--gw-motion-normal) var(--gw-motion-easing), scale var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } dialog, dialog::backdrop { opacity: 0; } dialog { scale: 0.96; } dialog[open], dialog[open]::backdrop { opacity: 1; } dialog[open] { scale: 1; } @starting-style { dialog[open], dialog[open]::backdrop { opacity: 0; } dialog[open] { scale: 0.96; } } header { display: flex; gap: var(--gw-space-2); align-items: center; padding: var(--gw-space-3) var(--gw-space-3) 0 var(--gw-space-4); } h2 { flex: 1; margin: 0; font-size: var(--gw-font-size-l); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body { padding: var(--gw-space-3) var(--gw-space-4); font-size: var(--gw-font-size-s); } footer { display: flex; flex-wrap: wrap; gap: var(--gw-space-1); justify-content: flex-end; padding: 0 var(--gw-space-4) var(--gw-space-4); } dialog > .discard { padding: var(--gw-space-4); } `, ]",
6417
+ "default": "[ base, dismissalStyles, css` dialog { width: min(32rem, calc(100vw - 2 * var(--gw-space-3))); max-height: calc(100dvh - 2 * var(--gw-space-3)); padding: 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-l); } dialog::backdrop { background: rgb(0 0 0 / 50%); } /* Fades and lifts in and out, the backdrop with it */ dialog, dialog::backdrop { transition: opacity var(--gw-motion-normal) var(--gw-motion-easing), scale var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } dialog, dialog::backdrop { opacity: 0; } dialog { scale: 0.96; } dialog[open], dialog[open]::backdrop { opacity: 1; } dialog[open] { /* The heading and actions stay put while a long body scrolls */ display: flex; flex-direction: column; scale: 1; } @starting-style { dialog[open], dialog[open]::backdrop { opacity: 0; } dialog[open] { scale: 0.96; } } header { display: flex; gap: var(--gw-space-2); align-items: center; padding: var(--gw-space-3) var(--gw-space-3) 0 var(--gw-space-4); } h2 { flex: 1; margin: 0; font-size: var(--gw-font-size-l); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body { flex: 1 1 auto; min-height: 0; padding: var(--gw-space-3) var(--gw-space-4); overflow-y: auto; overscroll-behavior: contain; font-size: var(--gw-font-size-s); } footer { display: flex; flex-wrap: wrap; gap: var(--gw-space-1); justify-content: flex-end; padding: 0 var(--gw-space-4) var(--gw-space-4); } dialog > .discard { padding: var(--gw-space-4); } `, ]",
6418
6418
  "inheritedFrom": {
6419
6419
  "name": "GwElement",
6420
6420
  "module": "src/element.ts"
@@ -10441,7 +10441,7 @@
10441
10441
  "text": "readonly CSSStyleSheet[]"
10442
10442
  },
10443
10443
  "static": true,
10444
- "default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-block, flip-inline; inset: auto; top: anchor(bottom); left: anchor(left); box-sizing: border-box; min-width: 15rem; max-height: min(24rem, 60vh); padding: 0; margin: var(--gw-space-1) 0 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
10444
+ "default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square like an icon-only gw-button, such as a ⋮ for more actions, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { justify-content: center; min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .chevron { display: none; } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } /* Below the button down to the window's edge, or above it when there's more room there, growing to its content and scrolling past that */ [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-inline; /* The area is the room on that side, so 100% is all of it */ position-area: block-end span-inline-end; inset: auto; box-sizing: border-box; min-width: 15rem; max-width: calc(100vw - 2 * var(--gw-space-2)); max-height: min(24rem, calc(100% - 2 * var(--gw-space-2))); padding: 0; margin: var(--gw-space-1) 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
10445
10445
  "inheritedFrom": {
10446
10446
  "name": "GwElement",
10447
10447
  "module": "src/element.ts"
@@ -11026,7 +11026,7 @@
11026
11026
  "type": {
11027
11027
  "text": "\"show\"|\"hide\"|\"auto\""
11028
11028
  },
11029
- "description": "Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. Defaults to `show`.",
11029
+ "description": "Whether the trigger's text shows beside its icon: always, never (a square icon button without the chevron, such as ⋮ for more actions), or not on narrow screens. Defaults to `show`.",
11030
11030
  "name": "labels"
11031
11031
  }
11032
11032
  ],
@@ -13400,7 +13400,7 @@
13400
13400
  "text": "readonly CSSStyleSheet[]"
13401
13401
  },
13402
13402
  "static": true,
13403
- "default": "[ base, css` :host { display: flex; gap: var(--gw-space-3); align-items: flex-start; } .start { display: flex; flex: none; align-items: center; min-height: 3rem; } .start[hidden] { display: none; } .tile { display: grid; flex: none; place-items: center; width: 3rem; height: 3rem; color: var(--gw-color-accent); background: var(--gw-color-fill-accent-quiet); border-radius: var(--gw-radius-l); --gw-icon-size: 1.625rem; } .titles { display: flex; flex: 1; flex-direction: column; gap: var(--gw-space-0-5); min-width: 0; } h1 { margin: 0; font-size: var(--gw-font-size-2xl); font-weight: var(--gw-font-weight-strong); line-height: 1.2; color: var(--gw-color-text-strong); } p { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .actions { display: flex; flex: none; gap: var(--gw-space-1); align-items: center; margin-inline-start: auto; } /* Centered on the tile when there is one */ .tile:not([hidden]) ~ .actions { min-height: 3rem; } `, ]",
13403
+ "default": "[ base, css` :host { display: flex; gap: var(--gw-space-3); align-items: flex-start; } .start { display: flex; flex: none; align-items: center; min-height: 3rem; } .start[hidden] { display: none; } .tile { display: grid; flex: none; place-items: center; width: 3rem; height: 3rem; color: var(--gw-color-accent); background: var(--gw-color-fill-accent-quiet); border-radius: var(--gw-radius-l); --gw-icon-size: 1.625rem; } .titles { display: flex; flex: 0 1 auto; flex-direction: column; gap: var(--gw-space-0-5); min-width: 0; } h1 { margin: 0; font-size: var(--gw-font-size-2xl); font-weight: var(--gw-font-weight-strong); line-height: 1.2; color: var(--gw-color-text-strong); } p { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* Takes the room the titles leave, and gives it up before them, down to its items' smallest */ .actions { display: flex; flex: 1 1000 auto; gap: var(--gw-space-1); align-items: center; justify-content: flex-end; } /* Centered on the tile when there is one */ .tile:not([hidden]) ~ .actions { min-height: 3rem; } `, ]",
13404
13404
  "inheritedFrom": {
13405
13405
  "name": "GwElement",
13406
13406
  "module": "src/element.ts"
@@ -13884,7 +13884,7 @@
13884
13884
  "text": "readonly CSSStyleSheet[]"
13885
13885
  },
13886
13886
  "static": true,
13887
- "default": "[ base, dismissalStyles, css` [popover] { position: fixed; inset: unset; box-sizing: border-box; width: min(var(--gw-popover-width, 20rem), calc(100vw - 2 * ${gap}px)); padding: var(--gw-space-4); margin: 0; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-l); opacity: 0; scale: 0.96; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { opacity: 1; scale: 1; } @starting-style { [popover]:popover-open { opacity: 0; scale: 0.96; } } /* Grows from the corner nearest the button */ [data-placement=\"below\"] { transform-origin: top right; } [data-placement=\"above\"] { transform-origin: bottom right; } [data-placement=\"below\"][data-side=\"start\"] { transform-origin: top left; } [data-placement=\"above\"][data-side=\"start\"] { transform-origin: bottom left; } [data-placement=\"center\"] { inset: 0; margin: auto; height: fit-content; } [data-placement=\"center\"]::backdrop { background: rgb(0 0 0 / 40%); } /* A bottom sheet on narrow screens, sliding up from the edge */ [data-placement=\"sheet\"] { inset: auto 0 0; width: 100%; max-width: none; padding-block-end: calc( var(--gw-space-4) + env(safe-area-inset-bottom) ); border-end-start-radius: 0; border-end-end-radius: 0; scale: 1; translate: 0 100%; } [data-placement=\"sheet\"]:popover-open { translate: 0 0; } @starting-style { [data-placement=\"sheet\"]:popover-open { translate: 0 100%; } } [data-placement=\"sheet\"]::backdrop { background: rgb(0 0 0 / 40%); } header { display: flex; gap: var(--gw-space-2); align-items: flex-start; margin: calc(var(--gw-space-2) * -1) calc(var(--gw-space-2) * -1) var(--gw-space-2) 0; } header .content { flex: 1; min-width: 0; padding-block-start: var(--gw-space-2); } h2 { margin: 0 0 var(--gw-space-2); font-size: var(--gw-font-size-m); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body { font-size: var(--gw-font-size-s); } ::slotted(p) { margin: 0; } footer { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); justify-content: flex-end; margin-block-start: var(--gw-space-4); } `, ]",
13887
+ "default": "[ base, dismissalStyles, css` [popover] { position: fixed; inset: unset; box-sizing: border-box; width: min(var(--gw-popover-width, 20rem), calc(100vw - 2 * ${gap}px)); /* Never past the window, scrolling within it; placed popovers get the room on their side of the button */ max-height: calc(100dvh - 2 * ${gap}px); overflow-y: auto; overscroll-behavior: contain; padding: var(--gw-space-4); margin: 0; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-l); opacity: 0; scale: 0.96; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { opacity: 1; scale: 1; } @starting-style { [popover]:popover-open { opacity: 0; scale: 0.96; } } /* Grows from the corner nearest the button */ [data-placement=\"below\"] { transform-origin: top right; } [data-placement=\"above\"] { transform-origin: bottom right; } [data-placement=\"below\"][data-side=\"start\"] { transform-origin: top left; } [data-placement=\"above\"][data-side=\"start\"] { transform-origin: bottom left; } [data-placement=\"center\"] { inset: 0; margin: auto; height: fit-content; } [data-placement=\"center\"]::backdrop { background: rgb(0 0 0 / 40%); } /* A bottom sheet on narrow screens, sliding up from the edge */ [data-placement=\"sheet\"] { inset: auto 0 0; width: 100%; max-width: none; padding-block-end: calc( var(--gw-space-4) + env(safe-area-inset-bottom) ); border-end-start-radius: 0; border-end-end-radius: 0; scale: 1; translate: 0 100%; } [data-placement=\"sheet\"]:popover-open { translate: 0 0; } @starting-style { [data-placement=\"sheet\"]:popover-open { translate: 0 100%; } } [data-placement=\"sheet\"]::backdrop { background: rgb(0 0 0 / 40%); } header { display: flex; gap: var(--gw-space-2); align-items: flex-start; margin: calc(var(--gw-space-2) * -1) calc(var(--gw-space-2) * -1) var(--gw-space-2) 0; } header .content { flex: 1; min-width: 0; padding-block-start: var(--gw-space-2); } h2 { margin: 0 0 var(--gw-space-2); font-size: var(--gw-font-size-m); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body { font-size: var(--gw-font-size-s); } ::slotted(p) { margin: 0; } footer { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); justify-content: flex-end; margin-block-start: var(--gw-space-4); } `, ]",
13888
13888
  "inheritedFrom": {
13889
13889
  "name": "GwElement",
13890
13890
  "module": "src/element.ts"
@@ -19615,7 +19615,7 @@
19615
19615
  "declarations": [
19616
19616
  {
19617
19617
  "kind": "class",
19618
- "description": "A palette button for a page's look. It opens a popover with the mode\n(auto, light, dark or Darker), the theme, and buttons to import a theme\nfile (a Groundwork theme's `.json` or an Omarchy `colors.toml`) or reset.\n\nIt only asks: it fires `mode`, `theme`, `import` and `reset`, and the page\napplies them and sets `mode`, `theme` and `themes` back. `pageTheme` from\n`@timmo001/groundwork-tokens/page` does all of that. Show an import's\nresult, such as contrast misses, in `status`.\n\n`gallery` swaps the theme select for `gw-theme-gallery`'s cards, which\npreview each theme in the chosen mode.\n\n`layout` sets how it shows. `popover`, the default, is the palette button\nand its popover. `cycle` is one button showing the mode in use, which steps\nto the next mode on each press, for an app bar. `inline` lays the controls\nout in place, in one wrapping row with Reset and Import, for a footer or a\nsettings page; set `justify-content` on it to align the row. `modes` picks which modes it offers, such as `light dark` without\nAuto.",
19618
+ "description": "The page's mode, theme and import, in place where there's room and in a\npopover where there isn't. Offers the mode (auto, light, dark or Darker),\nthe theme, and buttons to import a theme file (a Groundwork theme's `.json`\nor an Omarchy `colors.toml`) or reset.\n\nIt only asks: it fires `mode`, `theme`, `import` and `reset`, and the page\napplies them and sets `mode`, `theme` and `themes` back. `pageTheme` from\n`@timmo001/groundwork-tokens/page` does all of that. Show an import's\nresult, such as contrast misses, in `status`.\n\n`gallery` swaps the theme select for `gw-theme-gallery`'s cards, which\npreview each theme in the chosen mode.\n\n`layout` sets how it shows. `auto`, the default, lays the mode out as icons\nand the theme as a select in place while they fit the room it's given,\nthen moves them into the popover, the theme first, as the room runs out.\nReset, Import, the gallery and `status` stay in the popover, opened from a\npalette button. Once the mode and theme are in place and only Reset and\nImport are left, they're a ⋮ menu instead.\nIn a flex row, such as `gw-page-header`'s actions, it gives up room before\nthe title does. `popover` keeps everything behind the palette button. `cycle` is one button\nshowing the mode in use, which steps to the next mode on each press, for an\napp bar. `inline` lays every control out in place, in one wrapping row with\nReset and Import, for a footer or a settings page; set `justify-content` on\nit to align the row. `modes` picks which modes it offers, such as\n`light dark` without Auto.",
19619
19619
  "name": "GwThemePicker",
19620
19620
  "cssParts": [
19621
19621
  {
@@ -19652,7 +19652,7 @@
19652
19652
  "text": "readonly CSSStyleSheet[]"
19653
19653
  },
19654
19654
  "static": true,
19655
- "default": "[ base, css` :host { display: inline-block; } :host([gallery]) gw-popover { --gw-popover-width: 34rem; } .gallery { max-height: min(24rem, 50vh); overflow: auto; /* Room for the cards' focus rings */ padding: var(--gw-space-1); margin: calc(var(--gw-space-1) * -1); } .controls { display: flex; flex-direction: column; gap: var(--gw-space-4); margin-block-start: var(--gw-space-2); } .status { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* One wrapping row: the controls, then Reset and Import. The host is the flex container, so a page can set its justify-content */ :host([layout=\"inline\"]) { display: flex; flex-wrap: wrap; gap: var(--gw-space-4); align-items: center; } :host([layout=\"inline\"]) .controls { flex-flow: row wrap; align-items: center; margin-block-start: 0; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); align-items: center; } `, ]",
19655
+ "default": "[ base, css` :host { display: inline-block; } :host([gallery]) gw-popover { --gw-popover-width: 34rem; } .gallery { max-height: min(24rem, 50vh); overflow: auto; /* Room for the cards' focus rings */ padding: var(--gw-space-1); margin: calc(var(--gw-space-1) * -1); } .controls { display: flex; flex-direction: column; gap: var(--gw-space-4); margin-block-start: var(--gw-space-2); } .status { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* One wrapping row: the controls, then Reset and Import. The host is the flex container, so a page can set its justify-content */ :host([layout=\"inline\"]) { display: flex; flex-wrap: wrap; gap: var(--gw-space-4); align-items: center; } :host([layout=\"inline\"]) .controls { flex-flow: row wrap; align-items: center; margin-block-start: 0; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); align-items: center; } /* Asks for the room to have all of it in place, but gives it up down to the button before a flex row's other items shrink */ :host([layout=\"auto\"]) { display: flex; flex: 0 1000 var(--gw-theme-picker-fit, auto); gap: var(--gw-space-1); align-items: center; min-width: 2.5rem; contain: inline-size; } :host([layout=\"auto\"]) > * { flex: none; } `, ]",
19656
19656
  "inheritedFrom": {
19657
19657
  "name": "GwElement",
19658
19658
  "module": "src/element.ts"
@@ -19710,10 +19710,10 @@
19710
19710
  "kind": "field",
19711
19711
  "name": "layout",
19712
19712
  "type": {
19713
- "text": "\"popover\" | \"cycle\" | \"inline\""
19713
+ "text": "\"auto\" | \"popover\" | \"cycle\" | \"inline\""
19714
19714
  },
19715
- "description": "How it shows: a button and popover, a button cycling the modes, or the controls in place.",
19716
- "default": "\"popover\""
19715
+ "description": "How it shows: in place while it fits then in a popover, a button and popover, a button cycling the modes, or every control in place.",
19716
+ "default": "\"auto\""
19717
19717
  },
19718
19718
  {
19719
19719
  "kind": "field",
@@ -19784,6 +19784,12 @@
19784
19784
  "privacy": "private",
19785
19785
  "readonly": true
19786
19786
  },
19787
+ {
19788
+ "kind": "field",
19789
+ "name": "#menu",
19790
+ "privacy": "private",
19791
+ "readonly": true
19792
+ },
19787
19793
  {
19788
19794
  "kind": "field",
19789
19795
  "name": "#actions",
@@ -19815,6 +19821,60 @@
19815
19821
  ],
19816
19822
  "description": "Puts the controls and their buttons in the popover, or in place for `inline`."
19817
19823
  },
19824
+ {
19825
+ "kind": "method",
19826
+ "name": "#arrange",
19827
+ "privacy": "private",
19828
+ "parameters": [
19829
+ {
19830
+ "name": "root",
19831
+ "type": {
19832
+ "text": "ShadowRoot"
19833
+ }
19834
+ }
19835
+ ],
19836
+ "description": "With `auto`, moves the mode and theme in place or back into the popover, as `#fits` says."
19837
+ },
19838
+ {
19839
+ "kind": "method",
19840
+ "name": "#fit",
19841
+ "privacy": "private",
19842
+ "description": "With `auto`, measures the mode and theme in place, sizes the host to fit\nthem all, then keeps in place what the room it gets allows, the mode\nfirst. The rest go into the popover."
19843
+ },
19844
+ {
19845
+ "kind": "field",
19846
+ "name": "#fits",
19847
+ "privacy": "private",
19848
+ "type": {
19849
+ "text": "object"
19850
+ },
19851
+ "default": "{ mode: false, theme: false }"
19852
+ },
19853
+ {
19854
+ "kind": "field",
19855
+ "name": "#fitting",
19856
+ "privacy": "private",
19857
+ "type": {
19858
+ "text": "number"
19859
+ },
19860
+ "default": "0"
19861
+ },
19862
+ {
19863
+ "kind": "field",
19864
+ "name": "#measuring",
19865
+ "privacy": "private",
19866
+ "type": {
19867
+ "text": "boolean"
19868
+ },
19869
+ "default": "false"
19870
+ },
19871
+ {
19872
+ "kind": "field",
19873
+ "name": "#resize",
19874
+ "privacy": "private",
19875
+ "readonly": true,
19876
+ "default": "new ResizeObserver(() => { void this.#fit(); })"
19877
+ },
19818
19878
  {
19819
19879
  "kind": "method",
19820
19880
  "name": "#detail",
@@ -20179,9 +20239,9 @@
20179
20239
  "attributes": [
20180
20240
  {
20181
20241
  "type": {
20182
- "text": "\"popover\"|\"cycle\"|\"inline\""
20242
+ "text": "\"auto\"|\"popover\"|\"cycle\"|\"inline\""
20183
20243
  },
20184
- "description": "How it shows: a button and popover, a button cycling the modes, or the controls in place. Defaults to `popover`.",
20244
+ "description": "How it shows: in place while it fits then in a popover, a button and popover, a button cycling the modes, or every control in place. Defaults to `auto`.",
20185
20245
  "name": "layout"
20186
20246
  },
20187
20247
  {
@@ -20240,7 +20300,7 @@
20240
20300
  },
20241
20301
  "tagName": "gw-theme-picker",
20242
20302
  "customElement": true,
20243
- "summary": "A button opening the page's mode, theme and import."
20303
+ "summary": "The page's mode, theme and import, in place or in a popover."
20244
20304
  }
20245
20305
  ],
20246
20306
  "exports": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "Groundwork's framework-free web components, styled by the --gw-* tokens",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/gw-app-bar.ts CHANGED
@@ -141,9 +141,11 @@ export class GwAppBar extends GwElement {
141
141
  visibility var(--gw-motion-slow) allow-discrete;
142
142
  }
143
143
 
144
+ /* The nav keeps its room; the actions take the rest, which a theme
145
+ picker fills or gives up by moving its controls into its popover */
144
146
  .nav {
145
147
  display: flex;
146
- flex: 1;
148
+ flex: none;
147
149
  gap: var(--gw-space-1);
148
150
  align-items: center;
149
151
  min-width: 0;
@@ -151,10 +153,10 @@ export class GwAppBar extends GwElement {
151
153
 
152
154
  .actions {
153
155
  display: flex;
154
- flex: none;
156
+ flex: 1 1 auto;
155
157
  gap: var(--gw-space-1);
156
158
  align-items: center;
157
- margin-inline-start: auto;
159
+ justify-content: flex-end;
158
160
  }
159
161
 
160
162
  .menu {
package/src/gw-dialog.ts CHANGED
@@ -50,7 +50,9 @@ export class GwDialog extends GwElement {
50
50
  css`
51
51
  dialog {
52
52
  width: min(32rem, calc(100vw - 2 * var(--gw-space-3)));
53
+ max-height: calc(100dvh - 2 * var(--gw-space-3));
53
54
  padding: 0;
55
+ overflow: hidden;
54
56
  color: var(--gw-color-text-primary);
55
57
  background: var(--gw-color-surface-overlay);
56
58
  border: var(--gw-border-width) solid var(--gw-color-border-normal);
@@ -87,6 +89,9 @@ export class GwDialog extends GwElement {
87
89
  }
88
90
 
89
91
  dialog[open] {
92
+ /* The heading and actions stay put while a long body scrolls */
93
+ display: flex;
94
+ flex-direction: column;
90
95
  scale: 1;
91
96
  }
92
97
 
@@ -117,7 +122,11 @@ export class GwDialog extends GwElement {
117
122
  }
118
123
 
119
124
  .body {
125
+ flex: 1 1 auto;
126
+ min-height: 0;
120
127
  padding: var(--gw-space-3) var(--gw-space-4);
128
+ overflow-y: auto;
129
+ overscroll-behavior: contain;
121
130
  font-size: var(--gw-font-size-s);
122
131
  }
123
132
 
@@ -55,7 +55,7 @@ const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
55
55
  * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The trigger's size, 24 to 56px tall. Defaults to `medium`.
56
56
  * @attr {"accent"|"filled"|"outlined"|"plain"} appearance - How the trigger is drawn: `filled` is a neutral fill with a border, and the others match a neutral `gw-button` of that appearance, so it sits in a row of them. Defaults to `filled`.
57
57
  * @attr {boolean} active - Marks the trigger as set, such as a grouping that's on, with an accent border and text.
58
- * @attr {"show"|"hide"|"auto"} labels - Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. Defaults to `show`.
58
+ * @attr {"show"|"hide"|"auto"} labels - Whether the trigger's text shows beside its icon: always, never (a square icon button without the chevron, such as ⋮ for more actions), or not on narrow screens. Defaults to `show`.
59
59
  *
60
60
  * @csspart trigger - The button that opens the menu.
61
61
  *
@@ -163,9 +163,16 @@ export class GwMenuButton extends GwElement {
163
163
  border-color: var(--gw-color-accent);
164
164
  }
165
165
 
166
- /* Icon only, square, with the text kept for assistive technology */
166
+ /* Icon only, square like an icon-only gw-button, such as a ⋮ for more
167
+ actions, with the text kept for assistive technology */
167
168
  :host([labels="hide"]) .trigger.has-icon {
168
- padding: 0 calc(var(--gw-space-2) * var(--gw-size));
169
+ justify-content: center;
170
+ min-width: calc(2.5rem * var(--gw-scale));
171
+ padding: 0;
172
+ }
173
+
174
+ :host([labels="hide"]) .has-icon .chevron {
175
+ display: none;
169
176
  }
170
177
 
171
178
  :host([labels="hide"]) .has-icon .label {
@@ -210,17 +217,20 @@ export class GwMenuButton extends GwElement {
210
217
  translate: 0 0.125rem;
211
218
  }
212
219
 
220
+ /* Below the button down to the window's edge, or above it when there's
221
+ more room there, growing to its content and scrolling past that */
213
222
  [popover] {
214
223
  position-anchor: --gw-menu;
215
- position-try-fallbacks: flip-block, flip-inline;
224
+ position-try-fallbacks: flip-inline;
225
+ /* The area is the room on that side, so 100% is all of it */
226
+ position-area: block-end span-inline-end;
216
227
  inset: auto;
217
- top: anchor(bottom);
218
- left: anchor(left);
219
228
  box-sizing: border-box;
220
229
  min-width: 15rem;
221
- max-height: min(24rem, 60vh);
230
+ max-width: calc(100vw - 2 * var(--gw-space-2));
231
+ max-height: min(24rem, calc(100% - 2 * var(--gw-space-2)));
222
232
  padding: 0;
223
- margin: var(--gw-space-1) 0 0;
233
+ margin: var(--gw-space-1) 0;
224
234
  overflow: hidden;
225
235
  color: var(--gw-color-text-primary);
226
236
  background: var(--gw-color-surface-overlay);
@@ -429,7 +439,25 @@ export class GwMenuButton extends GwElement {
429
439
 
430
440
  this.#trigger.setAttribute("aria-expanded", String(expanded));
431
441
 
432
- if (expanded) this.#buttons[0]?.focus();
442
+ if (!expanded) return;
443
+
444
+ // Below the button while the menu fits there or there's more room than
445
+ // above it; the area's height then caps the menu, which scrolls
446
+ const popover = this.#popover;
447
+ const rect = this.#trigger.getBoundingClientRect();
448
+ const below = window.innerHeight - rect.bottom;
449
+
450
+ popover.style.positionArea = "";
451
+
452
+ const items = this.#items;
453
+
454
+ const height =
455
+ popover.offsetHeight - items.clientHeight + items.scrollHeight;
456
+
457
+ const fitsBelow = below >= height + 16 || below >= rect.top;
458
+
459
+ popover.style.positionArea = fitsBelow ? "" : "block-start span-inline-end";
460
+ this.#buttons[0]?.focus({ preventScroll: true });
433
461
  };
434
462
 
435
463
  readonly #onKeydown = (event: KeyboardEvent) => {
@@ -67,7 +67,7 @@ export class GwPageHeader extends GwElement {
67
67
 
68
68
  .titles {
69
69
  display: flex;
70
- flex: 1;
70
+ flex: 0 1 auto;
71
71
  flex-direction: column;
72
72
  gap: var(--gw-space-0-5);
73
73
  min-width: 0;
@@ -87,12 +87,14 @@ export class GwPageHeader extends GwElement {
87
87
  color: var(--gw-color-text-secondary);
88
88
  }
89
89
 
90
+ /* Takes the room the titles leave, and gives it up before them, down
91
+ to its items' smallest */
90
92
  .actions {
91
93
  display: flex;
92
- flex: none;
94
+ flex: 1 1000 auto;
93
95
  gap: var(--gw-space-1);
94
96
  align-items: center;
95
- margin-inline-start: auto;
97
+ justify-content: flex-end;
96
98
  }
97
99
 
98
100
  /* Centered on the tile when there is one */
package/src/gw-popover.ts CHANGED
@@ -64,6 +64,11 @@ export class GwPopover extends GwElement {
64
64
  inset: unset;
65
65
  box-sizing: border-box;
66
66
  width: min(var(--gw-popover-width, 20rem), calc(100vw - 2 * ${gap}px));
67
+ /* Never past the window, scrolling within it; placed popovers get
68
+ the room on their side of the button */
69
+ max-height: calc(100dvh - 2 * ${gap}px);
70
+ overflow-y: auto;
71
+ overscroll-behavior: contain;
67
72
  padding: var(--gw-space-4);
68
73
  margin: 0;
69
74
  color: var(--gw-color-text-primary);
@@ -304,14 +309,20 @@ export class GwPopover extends GwElement {
304
309
  if (anchor === undefined || placement === "sheet") {
305
310
  popover.style.top = "";
306
311
  popover.style.left = "";
312
+ popover.style.maxHeight = "";
307
313
  popover.dataset.placement = placement;
308
314
 
309
315
  return;
310
316
  }
311
317
 
318
+ popover.style.maxHeight = "";
319
+
312
320
  const rect = anchor.getBoundingClientRect();
313
321
  const below = window.innerHeight - rect.bottom - gap;
314
322
  const fitsBelow = below >= popover.offsetHeight || below >= rect.top;
323
+ const room = fitsBelow ? below - gap : rect.top - 2 * gap;
324
+
325
+ if (popover.offsetHeight > room) popover.style.maxHeight = `${room}px`;
315
326
 
316
327
  // Lined up with the button's edge on its side of the screen, so the
317
328
  // popover opens toward the space rather than across the content
package/src/gw-select.ts CHANGED
@@ -252,9 +252,26 @@ export class GwSelect extends LightElement {
252
252
  const width = Math.max(rect.width, 224);
253
253
 
254
254
  popup.style.width = `${Math.min(width, window.innerWidth - 2 * gap)}px`;
255
+ this.#list.style.removeProperty("--gw-option-list-height");
255
256
 
256
257
  const below = window.innerHeight - rect.bottom - gap;
257
258
  const fitsBelow = below >= popup.offsetHeight || below >= rect.top;
259
+ const room = fitsBelow ? below - gap : rect.top - 2 * gap;
260
+
261
+ // A window that can't grow past the page, such as an extension's popup,
262
+ // would crop the list, so it scrolls within the room there is
263
+ if (popup.offsetHeight > room) {
264
+ const listbox = this.#list.shadowRoot?.querySelector('[role="listbox"]');
265
+
266
+ const rest =
267
+ popup.offsetHeight -
268
+ (listbox instanceof HTMLElement ? listbox.offsetHeight : 0);
269
+
270
+ this.#list.style.setProperty(
271
+ "--gw-option-list-height",
272
+ `${Math.max(room - rest, 96)}px`,
273
+ );
274
+ }
258
275
 
259
276
  popup.dataset.placement = fitsBelow ? "below" : "above";
260
277
  popup.style.top = `${fitsBelow ? rect.bottom + gap : Math.max(gap, rect.top - gap - popup.offsetHeight)}px`;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * `gw-theme-picker`: a button opening the page's mode, theme and import.
2
+ * `gw-theme-picker`: the page's mode, theme and import, in place or in a popover.
3
3
  * Docs: https://groundwork.timmo.dev/components/actions/#theme-picker
4
4
  *
5
5
  * @module
6
6
  */
7
7
  import { GwElement, css, h } from "./element.js";
8
8
  import "./gw-button.js";
9
+ import "./gw-menu-button.js";
9
10
  import type { OptionItem } from "./gw-option-list.js";
10
11
  import "./gw-popover.js";
11
12
  import "./gw-segmented.js";
@@ -17,10 +18,10 @@ import { base } from "./styles.js";
17
18
  const paletteIcon =
18
19
  "M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z";
19
20
 
20
- // MDI's sunny, moon and night (a moon with stars), for cycle's button. Auto
21
- // keeps the palette
21
+ // MDI's theme-light-dark, sunny, moon and night (a moon with stars). Cycle's
22
+ // button keeps the palette for auto
22
23
  const modeIcons = {
23
- auto: paletteIcon,
24
+ auto: "M7.5,2C5.71,3.15 4.5,5.18 4.5,7.5C4.5,9.82 5.71,11.85 7.53,13C4.46,13 2,10.54 2,7.5A5.5,5.5 0 0,1 7.5,2M19.07,3.5L20.5,4.93L4.93,20.5L3.5,19.07L19.07,3.5M12.89,5.93L11.41,5L9.97,6L10.39,4.3L9,3.24L10.75,3.12L11.33,1.47L12,3.1L13.73,3.13L12.38,4.26L12.89,5.93M9.59,9.54L8.43,8.81L7.31,9.59L7.65,8.27L6.56,7.44L7.92,7.35L8.37,6.06L8.88,7.33L10.24,7.36L9.19,8.23L9.59,9.54M19,13.5A5.5,5.5 0 0,1 13.5,19C12.28,19 11.15,18.6 10.24,17.93L17.93,10.24C18.6,11.15 19,12.28 19,13.5M14.6,20.08L17.37,18.93L17.13,22.28L14.6,20.08M18.93,17.38L20.08,14.61L22.28,17.15L18.93,17.38M20.08,12.42L18.94,9.64L22.28,9.88L20.08,12.42M9.63,18.93L12.4,20.08L9.87,22.27L9.63,18.93Z",
24
25
  light:
25
26
  "M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z",
26
27
  dark: "M2 12A10 10 0 0 0 15 21.54A10 10 0 0 1 15 2.46A10 10 0 0 0 2 12Z",
@@ -35,10 +36,15 @@ const allModes = [
35
36
  { value: "darker", label: "Darker", icon: modeIcons.darker },
36
37
  ] as const;
37
38
 
39
+ // MDI's dots-vertical, for the overflow once the mode and theme are in place
40
+ const moreIcon =
41
+ "M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z";
42
+
38
43
  /**
39
- * A palette button for a page's look. It opens a popover with the mode
40
- * (auto, light, dark or Darker), the theme, and buttons to import a theme
41
- * file (a Groundwork theme's `.json` or an Omarchy `colors.toml`) or reset.
44
+ * The page's mode, theme and import, in place where there's room and in a
45
+ * popover where there isn't. Offers the mode (auto, light, dark or Darker),
46
+ * the theme, and buttons to import a theme file (a Groundwork theme's `.json`
47
+ * or an Omarchy `colors.toml`) or reset.
42
48
  *
43
49
  * It only asks: it fires `mode`, `theme`, `import` and `reset`, and the page
44
50
  * applies them and sets `mode`, `theme` and `themes` back. `pageTheme` from
@@ -48,17 +54,24 @@ const allModes = [
48
54
  * `gallery` swaps the theme select for `gw-theme-gallery`'s cards, which
49
55
  * preview each theme in the chosen mode.
50
56
  *
51
- * `layout` sets how it shows. `popover`, the default, is the palette button
52
- * and its popover. `cycle` is one button showing the mode in use, which steps
53
- * to the next mode on each press, for an app bar. `inline` lays the controls
54
- * out in place, in one wrapping row with Reset and Import, for a footer or a
55
- * settings page; set `justify-content` on it to align the row. `modes` picks which modes it offers, such as `light dark` without
56
- * Auto.
57
+ * `layout` sets how it shows. `auto`, the default, lays the mode out as icons
58
+ * and the theme as a select in place while they fit the room it's given,
59
+ * then moves them into the popover, the theme first, as the room runs out.
60
+ * Reset, Import, the gallery and `status` stay in the popover, opened from a
61
+ * palette button. Once the mode and theme are in place and only Reset and
62
+ * Import are left, they're a ⋮ menu instead.
63
+ * In a flex row, such as `gw-page-header`'s actions, it gives up room before
64
+ * the title does. `popover` keeps everything behind the palette button. `cycle` is one button
65
+ * showing the mode in use, which steps to the next mode on each press, for an
66
+ * app bar. `inline` lays every control out in place, in one wrapping row with
67
+ * Reset and Import, for a footer or a settings page; set `justify-content` on
68
+ * it to align the row. `modes` picks which modes it offers, such as
69
+ * `light dark` without Auto.
57
70
  *
58
71
  * @element gw-theme-picker
59
- * @summary A button opening the page's mode, theme and import.
72
+ * @summary The page's mode, theme and import, in place or in a popover.
60
73
  *
61
- * @attr {"popover"|"cycle"|"inline"} layout - How it shows: a button and popover, a button cycling the modes, or the controls in place. Defaults to `popover`.
74
+ * @attr {"auto"|"popover"|"cycle"|"inline"} layout - How it shows: in place while it fits then in a popover, a button and popover, a button cycling the modes, or every control in place. Defaults to `auto`.
62
75
  * @attr {string} modes - The modes to offer, in order and separated by spaces. Defaults to `auto light dark darker`.
63
76
  * @attr {"auto"|"light"|"dark"|"darker"} mode - The mode in use. Defaults to `auto`.
64
77
  * @attr {string} theme - The theme in use, by id.
@@ -141,6 +154,21 @@ export class GwThemePicker extends GwElement {
141
154
  gap: var(--gw-space-2);
142
155
  align-items: center;
143
156
  }
157
+
158
+ /* Asks for the room to have all of it in place, but gives it up down
159
+ to the button before a flex row's other items shrink */
160
+ :host([layout="auto"]) {
161
+ display: flex;
162
+ flex: 0 1000 var(--gw-theme-picker-fit, auto);
163
+ gap: var(--gw-space-1);
164
+ align-items: center;
165
+ min-width: 2.5rem;
166
+ contain: inline-size;
167
+ }
168
+
169
+ :host([layout="auto"]) > * {
170
+ flex: none;
171
+ }
144
172
  `,
145
173
  ];
146
174
 
@@ -150,8 +178,8 @@ export class GwThemePicker extends GwElement {
150
178
  declare gallery: boolean;
151
179
  declare status: string;
152
180
  declare label: string;
153
- /** How it shows: a button and popover, a button cycling the modes, or the controls in place. */
154
- declare layout: "popover" | "cycle" | "inline";
181
+ /** How it shows: in place while it fits then in a popover, a button and popover, a button cycling the modes, or every control in place. */
182
+ declare layout: "auto" | "popover" | "cycle" | "inline";
155
183
  /** The modes to offer, in order and separated by spaces. */
156
184
  declare modes: string;
157
185
 
@@ -198,6 +226,18 @@ export class GwThemePicker extends GwElement {
198
226
 
199
227
  readonly #popover = h("gw-popover");
200
228
 
229
+ // Reset and Import on their own, once nothing else is left in the popover
230
+ readonly #menu = h("gw-menu-button", {
231
+ appearance: "plain",
232
+ icon: moreIcon,
233
+ labels: "hide",
234
+ hidden: true,
235
+ items: JSON.stringify([
236
+ { value: "reset", label: "Reset" },
237
+ { value: "import", label: "Import a theme file" },
238
+ ]),
239
+ });
240
+
201
241
  readonly #actions = h("div", { class: "actions", part: "actions" });
202
242
 
203
243
  constructor() {
@@ -208,7 +248,7 @@ export class GwThemePicker extends GwElement {
208
248
  this.gallery = false;
209
249
  this.status = "";
210
250
  this.label = "Theme";
211
- this.layout = "popover";
251
+ this.layout = "auto";
212
252
  this.modes = "auto light dark darker";
213
253
  }
214
254
 
@@ -251,15 +291,159 @@ export class GwThemePicker extends GwElement {
251
291
  this.#import.slot = inline ? "" : "actions";
252
292
 
253
293
  if (inline) {
294
+ this.#controls.replaceChildren(
295
+ this.#mode,
296
+ this.#theme,
297
+ this.#gallery,
298
+ this.#status,
299
+ );
254
300
  this.#actions.append(this.#reset, this.#import);
255
301
  root.append(this.#controls, this.#actions);
256
302
  this.#button.remove();
303
+ this.#menu.remove();
257
304
  this.#popover.remove();
258
305
  } else {
259
- this.#popover.append(this.#controls, this.#reset, this.#import);
260
- root.prepend(this.#button, this.#popover);
306
+ if (this.#controls.parentNode !== this.#popover) {
307
+ this.#popover.append(this.#controls, this.#reset, this.#import);
308
+ }
309
+
310
+ if (this.#button.parentNode !== root) {
311
+ root.prepend(this.#button, this.#menu, this.#popover);
312
+ }
313
+
261
314
  this.#actions.remove();
262
315
  }
316
+
317
+ this.#arrange(root);
318
+ }
319
+
320
+ /** With `auto`, moves the mode and theme in place or back into the popover, as `#fits` says. */
321
+ #arrange(root: ShadowRoot) {
322
+ // #fit has them in place to measure
323
+ if (this.#measuring) return;
324
+
325
+ const auto = this.layout === "auto";
326
+ const mode = auto && this.#fits.mode;
327
+ const theme = mode && this.#fits.theme;
328
+
329
+ if (mode && this.#mode.parentNode !== root) {
330
+ root.insertBefore(this.#mode, this.#button);
331
+ } else if (!mode && this.#mode.parentNode !== this.#controls) {
332
+ this.#controls.prepend(this.#mode);
333
+ }
334
+
335
+ if (theme && this.#theme.parentNode !== root) {
336
+ root.insertBefore(this.#theme, this.#button);
337
+ } else if (!theme && this.#theme.parentNode !== this.#controls) {
338
+ this.#controls.insertBefore(this.#theme, this.#gallery);
339
+ }
340
+
341
+ this.#mode.labels = mode ? "hide" : "show";
342
+ this.#theme.inlineLabel = theme;
343
+ // The popover's heading names it once the mode is out of it
344
+ this.#theme.labelHidden = mode && !theme;
345
+ // The palette while the popover holds the theme; the overflow dots once
346
+ // the theme is in place, as a menu of Reset and Import when they're all
347
+ // that's left
348
+ const more = theme || (mode && this.#theme.hidden && !this.gallery);
349
+ const menu = more && !this.status;
350
+
351
+ this.#button.icon =
352
+ this.layout === "cycle"
353
+ ? this.#button.icon
354
+ : more
355
+ ? moreIcon
356
+ : paletteIcon;
357
+
358
+ if (menu && this.#menu.hidden) this.#popover.close();
359
+
360
+ this.#button.hidden = menu;
361
+ this.#menu.hidden = !menu;
362
+ }
363
+
364
+ /**
365
+ * With `auto`, measures the mode and theme in place, sizes the host to fit
366
+ * them all, then keeps in place what the room it gets allows, the mode
367
+ * first. The rest go into the popover.
368
+ */
369
+ async #fit() {
370
+ const root = this.shadowRoot;
371
+
372
+ if (this.layout !== "auto" || root === null || !this.isConnected) return;
373
+
374
+ const run = ++this.#fitting;
375
+ const theme = !this.#theme.hidden;
376
+
377
+ this.#measuring = true;
378
+
379
+ if (this.#mode.parentNode !== root) {
380
+ this.#mode.labels = "hide";
381
+ root.insertBefore(this.#mode, this.#button);
382
+ }
383
+
384
+ if (theme && this.#theme.parentNode !== root) {
385
+ this.#theme.inlineLabel = true;
386
+ this.#theme.labelHidden = false;
387
+ root.insertBefore(this.#theme, this.#button);
388
+ }
389
+
390
+ // Drawn in place before measuring, still before the next paint
391
+ await Promise.all([this.#mode.updateComplete, this.#theme.updateComplete]);
392
+
393
+ if (run !== this.#fitting) return;
394
+
395
+ this.#measuring = false;
396
+
397
+ if (this.layout !== "auto") {
398
+ this.#arrange(root);
399
+
400
+ return;
401
+ }
402
+
403
+ const gap = Number.parseFloat(getComputedStyle(this).columnGap) || 0;
404
+
405
+ // Whichever of the button and the menu shows; both are square
406
+ const button = Math.max(
407
+ this.#button.getBoundingClientRect().width,
408
+ this.#menu.getBoundingClientRect().width,
409
+ );
410
+
411
+ const mode = this.#mode.getBoundingClientRect().width + gap;
412
+ const select = theme ? this.#theme.getBoundingClientRect().width + gap : 0;
413
+
414
+ this.style.setProperty(
415
+ "--gw-theme-picker-fit",
416
+ `${Math.ceil(button + mode + select)}px`,
417
+ );
418
+
419
+ const room = this.getBoundingClientRect().width + 0.5;
420
+
421
+ this.#fits = {
422
+ mode: room >= button + mode,
423
+ theme: theme && room >= button + mode + select,
424
+ };
425
+ this.#arrange(root);
426
+ }
427
+
428
+ #fits = { mode: false, theme: false };
429
+
430
+ #fitting = 0;
431
+
432
+ #measuring = false;
433
+
434
+ readonly #resize = new ResizeObserver(() => {
435
+ void this.#fit();
436
+ });
437
+
438
+ override connectedCallback() {
439
+ super.connectedCallback();
440
+ this.#resize.observe(this);
441
+ void document.fonts.ready.then(() => this.#fit());
442
+ }
443
+
444
+ override disconnectedCallback() {
445
+ super.disconnectedCallback();
446
+ this.#resize.disconnect();
263
447
  }
264
448
 
265
449
  #detail<Detail>(type: string, detail: Detail) {
@@ -291,6 +475,17 @@ export class GwThemePicker extends GwElement {
291
475
  this.#reset.addEventListener("click", () => {
292
476
  this.dispatchEvent(new Event("reset", { bubbles: true, composed: true }));
293
477
  });
478
+ this.#menu.addEventListener("select", (event) => {
479
+ event.stopPropagation();
480
+
481
+ if (event instanceof CustomEvent && event.detail.value === "import") {
482
+ this.#file.click();
483
+ } else {
484
+ this.dispatchEvent(
485
+ new Event("reset", { bubbles: true, composed: true }),
486
+ );
487
+ }
488
+ });
294
489
  this.#file.addEventListener("change", async () => {
295
490
  const file = this.#file.files?.[0];
296
491
 
@@ -320,16 +515,23 @@ export class GwThemePicker extends GwElement {
320
515
 
321
516
  this.#button.label = name;
322
517
  this.#button.textContent = name;
323
- this.#button.icon = current?.icon ?? paletteIcon;
518
+ this.#button.icon =
519
+ current === undefined || current.value === "auto"
520
+ ? paletteIcon
521
+ : current.icon;
324
522
  } else {
325
523
  this.#button.label = this.label;
326
524
  this.#button.textContent = this.label;
327
- this.#button.icon = paletteIcon;
328
525
  }
329
526
 
330
527
  this.#popover.heading = this.label;
528
+ this.#menu.label = this.label;
331
529
  this.#mode.label = "Mode";
332
- this.#mode.options = offered.map(({ value, label }) => ({ value, label }));
530
+ this.#mode.options = offered.map(({ value, label, icon }) => ({
531
+ value,
532
+ label,
533
+ icon,
534
+ }));
333
535
  this.#mode.value = this.mode;
334
536
  this.#theme.options = Array.isArray(this.themes) ? this.themes : [];
335
537
  this.#theme.value = this.theme;
@@ -341,6 +543,19 @@ export class GwThemePicker extends GwElement {
341
543
  this.#gallery.mode = this.mode === "auto" ? "" : this.mode;
342
544
  this.#status.textContent = this.status;
343
545
  this.#status.hidden = !this.status;
546
+
547
+ if (root === null) return;
548
+
549
+ this.#arrange(root);
550
+
551
+ // Measures again for the select's new value or the new set of controls
552
+ if (
553
+ ["layout", "modes", "theme", "themes", "gallery"].some((name) =>
554
+ changed.has(name),
555
+ )
556
+ ) {
557
+ void this.#fit();
558
+ }
344
559
  }
345
560
  }
346
561