@timmo001/groundwork-ui 0.9.11 → 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"
@@ -13353,7 +13353,7 @@
13353
13353
  "declarations": [
13354
13354
  {
13355
13355
  "kind": "class",
13356
- "description": "A page's title, with an optional icon tile and description, and its\ncontrols at the top right in the `actions` slot.",
13356
+ "description": "A page's title, with an optional icon tile and description, a back button\nor similar in the `start` slot, and its controls at the top right in the\n`actions` slot.",
13357
13357
  "name": "GwPageHeader",
13358
13358
  "cssParts": [
13359
13359
  {
@@ -13370,6 +13370,10 @@
13370
13370
  }
13371
13371
  ],
13372
13372
  "slots": [
13373
+ {
13374
+ "description": "Before the icon tile, such as a back button.",
13375
+ "name": "start"
13376
+ },
13373
13377
  {
13374
13378
  "description": "The page's controls.",
13375
13379
  "name": "actions"
@@ -13396,7 +13400,7 @@
13396
13400
  "text": "readonly CSSStyleSheet[]"
13397
13401
  },
13398
13402
  "static": true,
13399
- "default": "[ base, css` :host { display: flex; gap: var(--gw-space-3); align-items: flex-start; } .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; } `, ]",
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; } `, ]",
13400
13404
  "inheritedFrom": {
13401
13405
  "name": "GwElement",
13402
13406
  "module": "src/element.ts"
@@ -13432,6 +13436,18 @@
13432
13436
  "privacy": "private",
13433
13437
  "readonly": true
13434
13438
  },
13439
+ {
13440
+ "kind": "field",
13441
+ "name": "#startSlot",
13442
+ "privacy": "private",
13443
+ "readonly": true
13444
+ },
13445
+ {
13446
+ "kind": "field",
13447
+ "name": "#start",
13448
+ "privacy": "private",
13449
+ "readonly": true
13450
+ },
13435
13451
  {
13436
13452
  "kind": "field",
13437
13453
  "name": "#tile",
@@ -19599,7 +19615,7 @@
19599
19615
  "declarations": [
19600
19616
  {
19601
19617
  "kind": "class",
19602
- "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.",
19603
19619
  "name": "GwThemePicker",
19604
19620
  "cssParts": [
19605
19621
  {
@@ -19636,7 +19652,7 @@
19636
19652
  "text": "readonly CSSStyleSheet[]"
19637
19653
  },
19638
19654
  "static": true,
19639
- "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; } `, ]",
19640
19656
  "inheritedFrom": {
19641
19657
  "name": "GwElement",
19642
19658
  "module": "src/element.ts"
@@ -19694,10 +19710,10 @@
19694
19710
  "kind": "field",
19695
19711
  "name": "layout",
19696
19712
  "type": {
19697
- "text": "\"popover\" | \"cycle\" | \"inline\""
19713
+ "text": "\"auto\" | \"popover\" | \"cycle\" | \"inline\""
19698
19714
  },
19699
- "description": "How it shows: a button and popover, a button cycling the modes, or the controls in place.",
19700
- "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\""
19701
19717
  },
19702
19718
  {
19703
19719
  "kind": "field",
@@ -19799,6 +19815,60 @@
19799
19815
  ],
19800
19816
  "description": "Puts the controls and their buttons in the popover, or in place for `inline`."
19801
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
+ },
19802
19872
  {
19803
19873
  "kind": "method",
19804
19874
  "name": "#detail",
@@ -20163,9 +20233,9 @@
20163
20233
  "attributes": [
20164
20234
  {
20165
20235
  "type": {
20166
- "text": "\"popover\"|\"cycle\"|\"inline\""
20236
+ "text": "\"auto\"|\"popover\"|\"cycle\"|\"inline\""
20167
20237
  },
20168
- "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`.",
20169
20239
  "name": "layout"
20170
20240
  },
20171
20241
  {
@@ -20224,7 +20294,7 @@
20224
20294
  },
20225
20295
  "tagName": "gw-theme-picker",
20226
20296
  "customElement": true,
20227
- "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."
20228
20298
  }
20229
20299
  ],
20230
20300
  "exports": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.11",
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 {
@@ -8,8 +8,9 @@ import { GwElement, css, h } from "./element.js";
8
8
  import { base, createIcon, setIconPath } from "./styles.js";
9
9
 
10
10
  /**
11
- * A page's title, with an optional icon tile and description, and its
12
- * controls at the top right in the `actions` slot.
11
+ * A page's title, with an optional icon tile and description, a back button
12
+ * or similar in the `start` slot, and its controls at the top right in the
13
+ * `actions` slot.
13
14
  *
14
15
  * @element gw-page-header
15
16
  * @summary A page's title, icon, description and actions.
@@ -18,6 +19,7 @@ import { base, createIcon, setIconPath } from "./styles.js";
18
19
  * @attr {string} description - A line under the title.
19
20
  * @attr {string} icon - SVG path data for the icon tile.
20
21
  *
22
+ * @slot start - Before the icon tile, such as a back button.
21
23
  * @slot actions - The page's controls.
22
24
  *
23
25
  * @csspart tile - The icon tile.
@@ -40,6 +42,17 @@ export class GwPageHeader extends GwElement {
40
42
  align-items: flex-start;
41
43
  }
42
44
 
45
+ .start {
46
+ display: flex;
47
+ flex: none;
48
+ align-items: center;
49
+ min-height: 3rem;
50
+ }
51
+
52
+ .start[hidden] {
53
+ display: none;
54
+ }
55
+
43
56
  .tile {
44
57
  display: grid;
45
58
  flex: none;
@@ -54,7 +67,7 @@ export class GwPageHeader extends GwElement {
54
67
 
55
68
  .titles {
56
69
  display: flex;
57
- flex: 1;
70
+ flex: 0 1 auto;
58
71
  flex-direction: column;
59
72
  gap: var(--gw-space-0-5);
60
73
  min-width: 0;
@@ -74,12 +87,19 @@ export class GwPageHeader extends GwElement {
74
87
  color: var(--gw-color-text-secondary);
75
88
  }
76
89
 
90
+ /* Takes the room the titles leave, and gives it up before them, down
91
+ to its items' smallest */
77
92
  .actions {
78
93
  display: flex;
79
- flex: none;
94
+ flex: 1 1000 auto;
80
95
  gap: var(--gw-space-1);
81
96
  align-items: center;
82
- margin-inline-start: auto;
97
+ justify-content: flex-end;
98
+ }
99
+
100
+ /* Centered on the tile when there is one */
101
+ .tile:not([hidden]) ~ .actions {
102
+ min-height: 3rem;
83
103
  }
84
104
  `,
85
105
  ];
@@ -90,6 +110,11 @@ export class GwPageHeader extends GwElement {
90
110
 
91
111
  readonly #icon = createIcon("");
92
112
 
113
+ readonly #startSlot = h("slot", { name: "start" });
114
+
115
+ // Centered on the tile's height, and out of the row while it's empty
116
+ readonly #start = h("div", { class: "start", hidden: true }, this.#startSlot);
117
+
93
118
  readonly #tile = h("span", { class: "tile", part: "tile" }, this.#icon);
94
119
 
95
120
  readonly #heading = h("h1", { part: "heading" });
@@ -104,7 +129,11 @@ export class GwPageHeader extends GwElement {
104
129
  }
105
130
 
106
131
  protected override create(root: ShadowRoot) {
132
+ this.#startSlot.addEventListener("slotchange", () => {
133
+ this.#start.hidden = this.#startSlot.assignedElements().length === 0;
134
+ });
107
135
  root.append(
136
+ this.#start,
108
137
  this.#tile,
109
138
  h("div", { class: "titles" }, this.#heading, this.#description),
110
139
  h("div", { class: "actions" }, h("slot", { name: "actions" })),
@@ -12,6 +12,8 @@ export interface Segment {
12
12
  readonly label: string;
13
13
  /** SVG path data for an icon beside, or instead of, the label. */
14
14
  readonly icon?: string;
15
+ /** A longer description, shown as the option's tooltip. */
16
+ readonly hint?: string;
15
17
  }
16
18
 
17
19
  /**
@@ -297,7 +299,8 @@ export class GwSegmented extends GwElement {
297
299
  setAttribute(
298
300
  label,
299
301
  "title",
300
- option.icon && this.labels !== "show" ? option.label : null,
302
+ option.hint ??
303
+ (option.icon && this.labels !== "show" ? option.label : null),
301
304
  );
302
305
  }
303
306
 
@@ -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