@timmo001/groundwork-ui 0.9.12 → 0.9.13

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"
@@ -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"
@@ -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, or from a more button once the mode and theme are out.\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",
@@ -19815,6 +19815,60 @@
19815
19815
  ],
19816
19816
  "description": "Puts the controls and their buttons in the popover, or in place for `inline`."
19817
19817
  },
19818
+ {
19819
+ "kind": "method",
19820
+ "name": "#arrange",
19821
+ "privacy": "private",
19822
+ "parameters": [
19823
+ {
19824
+ "name": "root",
19825
+ "type": {
19826
+ "text": "ShadowRoot"
19827
+ }
19828
+ }
19829
+ ],
19830
+ "description": "With `auto`, moves the mode and theme in place or back into the popover, as `#fits` says."
19831
+ },
19832
+ {
19833
+ "kind": "method",
19834
+ "name": "#fit",
19835
+ "privacy": "private",
19836
+ "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."
19837
+ },
19838
+ {
19839
+ "kind": "field",
19840
+ "name": "#fits",
19841
+ "privacy": "private",
19842
+ "type": {
19843
+ "text": "object"
19844
+ },
19845
+ "default": "{ mode: false, theme: false }"
19846
+ },
19847
+ {
19848
+ "kind": "field",
19849
+ "name": "#fitting",
19850
+ "privacy": "private",
19851
+ "type": {
19852
+ "text": "number"
19853
+ },
19854
+ "default": "0"
19855
+ },
19856
+ {
19857
+ "kind": "field",
19858
+ "name": "#measuring",
19859
+ "privacy": "private",
19860
+ "type": {
19861
+ "text": "boolean"
19862
+ },
19863
+ "default": "false"
19864
+ },
19865
+ {
19866
+ "kind": "field",
19867
+ "name": "#resize",
19868
+ "privacy": "private",
19869
+ "readonly": true,
19870
+ "default": "new ResizeObserver(() => { void this.#fit(); })"
19871
+ },
19818
19872
  {
19819
19873
  "kind": "method",
19820
19874
  "name": "#detail",
@@ -20179,9 +20233,9 @@
20179
20233
  "attributes": [
20180
20234
  {
20181
20235
  "type": {
20182
- "text": "\"popover\"|\"cycle\"|\"inline\""
20236
+ "text": "\"auto\"|\"popover\"|\"cycle\"|\"inline\""
20183
20237
  },
20184
- "description": "How it shows: a button and popover, a button cycling the modes, or the controls in place. Defaults to `popover`.",
20238
+ "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
20239
  "name": "layout"
20186
20240
  },
20187
20241
  {
@@ -20240,7 +20294,7 @@
20240
20294
  },
20241
20295
  "tagName": "gw-theme-picker",
20242
20296
  "customElement": true,
20243
- "summary": "A button opening the page's mode, theme and import."
20297
+ "summary": "The page's mode, theme and import, in place or in a popover."
20244
20298
  }
20245
20299
  ],
20246
20300
  "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.13",
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 {
@@ -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 */
@@ -1,5 +1,5 @@
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
@@ -17,10 +17,10 @@ import { base } from "./styles.js";
17
17
  const paletteIcon =
18
18
  "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
19
 
20
- // MDI's sunny, moon and night (a moon with stars), for cycle's button. Auto
21
- // keeps the palette
20
+ // MDI's theme-light-dark, sunny, moon and night (a moon with stars). Cycle's
21
+ // button keeps the palette for auto
22
22
  const modeIcons = {
23
- auto: paletteIcon,
23
+ 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
24
  light:
25
25
  "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
26
  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 +35,15 @@ const allModes = [
35
35
  { value: "darker", label: "Darker", icon: modeIcons.darker },
36
36
  ] as const;
37
37
 
38
+ // MDI's dots-vertical, for the overflow once the mode and theme are in place
39
+ const moreIcon =
40
+ "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";
41
+
38
42
  /**
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.
43
+ * The page's mode, theme and import, in place where there's room and in a
44
+ * popover where there isn't. Offers the mode (auto, light, dark or Darker),
45
+ * the theme, and buttons to import a theme file (a Groundwork theme's `.json`
46
+ * or an Omarchy `colors.toml`) or reset.
42
47
  *
43
48
  * It only asks: it fires `mode`, `theme`, `import` and `reset`, and the page
44
49
  * applies them and sets `mode`, `theme` and `themes` back. `pageTheme` from
@@ -48,17 +53,23 @@ const allModes = [
48
53
  * `gallery` swaps the theme select for `gw-theme-gallery`'s cards, which
49
54
  * preview each theme in the chosen mode.
50
55
  *
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.
56
+ * `layout` sets how it shows. `auto`, the default, lays the mode out as icons
57
+ * and the theme as a select in place while they fit the room it's given,
58
+ * then moves them into the popover, the theme first, as the room runs out.
59
+ * Reset, Import, the gallery and `status` stay in the popover, opened from a
60
+ * palette button, or from a more button once the mode and theme are out.
61
+ * In a flex row, such as `gw-page-header`'s actions, it gives up room before
62
+ * the title does. `popover` keeps everything behind the palette button. `cycle` is one button
63
+ * showing the mode in use, which steps to the next mode on each press, for an
64
+ * app bar. `inline` lays every control out in place, in one wrapping row with
65
+ * Reset and Import, for a footer or a settings page; set `justify-content` on
66
+ * it to align the row. `modes` picks which modes it offers, such as
67
+ * `light dark` without Auto.
57
68
  *
58
69
  * @element gw-theme-picker
59
- * @summary A button opening the page's mode, theme and import.
70
+ * @summary The page's mode, theme and import, in place or in a popover.
60
71
  *
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`.
72
+ * @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
73
  * @attr {string} modes - The modes to offer, in order and separated by spaces. Defaults to `auto light dark darker`.
63
74
  * @attr {"auto"|"light"|"dark"|"darker"} mode - The mode in use. Defaults to `auto`.
64
75
  * @attr {string} theme - The theme in use, by id.
@@ -141,6 +152,21 @@ export class GwThemePicker extends GwElement {
141
152
  gap: var(--gw-space-2);
142
153
  align-items: center;
143
154
  }
155
+
156
+ /* Asks for the room to have all of it in place, but gives it up down
157
+ to the button before a flex row's other items shrink */
158
+ :host([layout="auto"]) {
159
+ display: flex;
160
+ flex: 0 1000 var(--gw-theme-picker-fit, auto);
161
+ gap: var(--gw-space-1);
162
+ align-items: center;
163
+ min-width: 2.5rem;
164
+ contain: inline-size;
165
+ }
166
+
167
+ :host([layout="auto"]) > * {
168
+ flex: none;
169
+ }
144
170
  `,
145
171
  ];
146
172
 
@@ -150,8 +176,8 @@ export class GwThemePicker extends GwElement {
150
176
  declare gallery: boolean;
151
177
  declare status: string;
152
178
  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";
179
+ /** 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. */
180
+ declare layout: "auto" | "popover" | "cycle" | "inline";
155
181
  /** The modes to offer, in order and separated by spaces. */
156
182
  declare modes: string;
157
183
 
@@ -208,7 +234,7 @@ export class GwThemePicker extends GwElement {
208
234
  this.gallery = false;
209
235
  this.status = "";
210
236
  this.label = "Theme";
211
- this.layout = "popover";
237
+ this.layout = "auto";
212
238
  this.modes = "auto light dark darker";
213
239
  }
214
240
 
@@ -251,15 +277,143 @@ export class GwThemePicker extends GwElement {
251
277
  this.#import.slot = inline ? "" : "actions";
252
278
 
253
279
  if (inline) {
280
+ this.#controls.replaceChildren(
281
+ this.#mode,
282
+ this.#theme,
283
+ this.#gallery,
284
+ this.#status,
285
+ );
254
286
  this.#actions.append(this.#reset, this.#import);
255
287
  root.append(this.#controls, this.#actions);
256
288
  this.#button.remove();
257
289
  this.#popover.remove();
258
290
  } else {
259
- this.#popover.append(this.#controls, this.#reset, this.#import);
260
- root.prepend(this.#button, this.#popover);
291
+ if (this.#controls.parentNode !== this.#popover) {
292
+ this.#popover.append(this.#controls, this.#reset, this.#import);
293
+ }
294
+
295
+ if (this.#button.parentNode !== root) {
296
+ root.prepend(this.#button, this.#popover);
297
+ }
298
+
261
299
  this.#actions.remove();
262
300
  }
301
+
302
+ this.#arrange(root);
303
+ }
304
+
305
+ /** With `auto`, moves the mode and theme in place or back into the popover, as `#fits` says. */
306
+ #arrange(root: ShadowRoot) {
307
+ // #fit has them in place to measure
308
+ if (this.#measuring) return;
309
+
310
+ const auto = this.layout === "auto";
311
+ const mode = auto && this.#fits.mode;
312
+ const theme = mode && this.#fits.theme;
313
+
314
+ if (mode && this.#mode.parentNode !== root) {
315
+ root.insertBefore(this.#mode, this.#button);
316
+ } else if (!mode && this.#mode.parentNode !== this.#controls) {
317
+ this.#controls.prepend(this.#mode);
318
+ }
319
+
320
+ if (theme && this.#theme.parentNode !== root) {
321
+ root.insertBefore(this.#theme, this.#button);
322
+ } else if (!theme && this.#theme.parentNode !== this.#controls) {
323
+ this.#controls.insertBefore(this.#theme, this.#gallery);
324
+ }
325
+
326
+ this.#mode.labels = mode ? "hide" : "show";
327
+ this.#theme.inlineLabel = theme;
328
+ // The popover's heading names it once the mode is out of it
329
+ this.#theme.labelHidden = mode && !theme;
330
+ // The palette while the popover holds the theme; the overflow dots
331
+ // once only Reset and Import are left in it
332
+ this.#button.icon =
333
+ this.layout === "cycle"
334
+ ? this.#button.icon
335
+ : theme || (mode && this.#theme.hidden && !this.gallery)
336
+ ? moreIcon
337
+ : paletteIcon;
338
+ }
339
+
340
+ /**
341
+ * With `auto`, measures the mode and theme in place, sizes the host to fit
342
+ * them all, then keeps in place what the room it gets allows, the mode
343
+ * first. The rest go into the popover.
344
+ */
345
+ async #fit() {
346
+ const root = this.shadowRoot;
347
+
348
+ if (this.layout !== "auto" || root === null || !this.isConnected) return;
349
+
350
+ const run = ++this.#fitting;
351
+ const theme = !this.#theme.hidden;
352
+
353
+ this.#measuring = true;
354
+
355
+ if (this.#mode.parentNode !== root) {
356
+ this.#mode.labels = "hide";
357
+ root.insertBefore(this.#mode, this.#button);
358
+ }
359
+
360
+ if (theme && this.#theme.parentNode !== root) {
361
+ this.#theme.inlineLabel = true;
362
+ this.#theme.labelHidden = false;
363
+ root.insertBefore(this.#theme, this.#button);
364
+ }
365
+
366
+ // Drawn in place before measuring, still before the next paint
367
+ await Promise.all([this.#mode.updateComplete, this.#theme.updateComplete]);
368
+
369
+ if (run !== this.#fitting) return;
370
+
371
+ this.#measuring = false;
372
+
373
+ if (this.layout !== "auto") {
374
+ this.#arrange(root);
375
+
376
+ return;
377
+ }
378
+
379
+ const gap = Number.parseFloat(getComputedStyle(this).columnGap) || 0;
380
+ const button = this.#button.getBoundingClientRect().width;
381
+ const mode = this.#mode.getBoundingClientRect().width + gap;
382
+ const select = theme ? this.#theme.getBoundingClientRect().width + gap : 0;
383
+
384
+ this.style.setProperty(
385
+ "--gw-theme-picker-fit",
386
+ `${Math.ceil(button + mode + select)}px`,
387
+ );
388
+
389
+ const room = this.getBoundingClientRect().width + 0.5;
390
+
391
+ this.#fits = {
392
+ mode: room >= button + mode,
393
+ theme: theme && room >= button + mode + select,
394
+ };
395
+ this.#arrange(root);
396
+ }
397
+
398
+ #fits = { mode: false, theme: false };
399
+
400
+ #fitting = 0;
401
+
402
+ #measuring = false;
403
+
404
+ readonly #resize = new ResizeObserver(() => {
405
+ void this.#fit();
406
+ });
407
+
408
+ override connectedCallback() {
409
+ super.connectedCallback();
410
+ this.#resize.observe(this);
411
+ void document.fonts.ready.then(() => this.#fit());
412
+ }
413
+
414
+ override disconnectedCallback() {
415
+ super.disconnectedCallback();
416
+ this.#resize.disconnect();
263
417
  }
264
418
 
265
419
  #detail<Detail>(type: string, detail: Detail) {
@@ -320,16 +474,22 @@ export class GwThemePicker extends GwElement {
320
474
 
321
475
  this.#button.label = name;
322
476
  this.#button.textContent = name;
323
- this.#button.icon = current?.icon ?? paletteIcon;
477
+ this.#button.icon =
478
+ current === undefined || current.value === "auto"
479
+ ? paletteIcon
480
+ : current.icon;
324
481
  } else {
325
482
  this.#button.label = this.label;
326
483
  this.#button.textContent = this.label;
327
- this.#button.icon = paletteIcon;
328
484
  }
329
485
 
330
486
  this.#popover.heading = this.label;
331
487
  this.#mode.label = "Mode";
332
- this.#mode.options = offered.map(({ value, label }) => ({ value, label }));
488
+ this.#mode.options = offered.map(({ value, label, icon }) => ({
489
+ value,
490
+ label,
491
+ icon,
492
+ }));
333
493
  this.#mode.value = this.mode;
334
494
  this.#theme.options = Array.isArray(this.themes) ? this.themes : [];
335
495
  this.#theme.value = this.theme;
@@ -341,6 +501,19 @@ export class GwThemePicker extends GwElement {
341
501
  this.#gallery.mode = this.mode === "auto" ? "" : this.mode;
342
502
  this.#status.textContent = this.status;
343
503
  this.#status.hidden = !this.status;
504
+
505
+ if (root === null) return;
506
+
507
+ this.#arrange(root);
508
+
509
+ // Measures again for the select's new value or the new set of controls
510
+ if (
511
+ ["layout", "modes", "theme", "themes", "gallery"].some((name) =>
512
+ changed.has(name),
513
+ )
514
+ ) {
515
+ void this.#fit();
516
+ }
344
517
  }
345
518
  }
346
519