@timmo001/groundwork-ui 0.5.0 → 0.7.0

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.
package/src/gw-card.ts CHANGED
@@ -15,6 +15,12 @@ import { base } from "./styles.js";
15
15
  * stand in for the content, and for the heading until it's known. The card
16
16
  * glides to its new height and the content fades in once it's loaded.
17
17
  *
18
+ * For a link the page's HTML has to carry, such as one with a `target` or
19
+ * `rel`, put an `<a>` in the `heading` slot instead of `href`. A click
20
+ * anywhere on the card that isn't on another control follows it, the card
21
+ * shows the link's hover and focus, and the link stays a real one in the
22
+ * page, so the rest of the card's content stays plain text.
23
+ *
18
24
  * @element gw-card
19
25
  * @summary A surface for one group of content, or a link to more.
20
26
  *
@@ -25,9 +31,13 @@ import { base } from "./styles.js";
25
31
  * @attr {number} lines - How many skeleton lines while loading. Defaults to 3.
26
32
  *
27
33
  * @slot - The content.
34
+ * @slot heading - The heading, in place of `heading`. An `<a>` here makes the whole card follow it.
28
35
  * @slot actions - Buttons beside the heading.
29
36
  *
30
37
  * @csspart card - The card's surface.
38
+ * @csspart header - The row with the heading and actions.
39
+ * @csspart heading - The heading.
40
+ * @csspart body - The content.
31
41
  */
32
42
  export class GwCard extends GwElement {
33
43
  static override properties = {
@@ -68,10 +78,32 @@ export class GwCard extends GwElement {
68
78
  transition: border-color var(--gw-motion-fast) var(--gw-motion-easing);
69
79
  }
70
80
 
71
- a.card:hover {
81
+ a.card:hover,
82
+ .card.linked:hover {
72
83
  border-color: var(--gw-color-border-accent);
73
84
  }
74
85
 
86
+ /* A card following a link in its heading slot */
87
+ .card.linked {
88
+ cursor: pointer;
89
+ transition: border-color var(--gw-motion-fast) var(--gw-motion-easing);
90
+ }
91
+
92
+ .card.focused {
93
+ outline: var(--gw-focus-width) solid var(--gw-color-focus);
94
+ outline-offset: var(--gw-focus-offset);
95
+ }
96
+
97
+ ::slotted([slot="heading"]) {
98
+ font: inherit;
99
+ color: inherit;
100
+ }
101
+
102
+ ::slotted(a[slot="heading"]) {
103
+ text-decoration: none;
104
+ outline: none;
105
+ }
106
+
75
107
  header {
76
108
  display: flex;
77
109
  gap: var(--gw-space-2);
@@ -105,16 +137,24 @@ export class GwCard extends GwElement {
105
137
 
106
138
  readonly #titleSkeleton = h("gw-skeleton-text");
107
139
 
140
+ readonly #headingSlot = h("slot", { name: "heading" });
141
+
108
142
  readonly #header = h(
109
143
  "header",
110
- {},
111
- h("h2", {}, this.#title, this.#titleSkeleton),
144
+ { part: "header" },
145
+ h(
146
+ "h2",
147
+ { part: "heading" },
148
+ this.#title,
149
+ this.#headingSlot,
150
+ this.#titleSkeleton,
151
+ ),
112
152
  h("slot", { name: "actions" }),
113
153
  );
114
154
 
115
155
  readonly #skeleton = h("gw-skeleton-lines");
116
156
 
117
- readonly #body = h("div", { class: "body" }, h("slot"));
157
+ readonly #body = h("div", { class: "body", part: "body" }, h("slot"));
118
158
 
119
159
  #card: HTMLElement = h("section", { class: "card", part: "card" });
120
160
 
@@ -129,6 +169,57 @@ export class GwCard extends GwElement {
129
169
  this.flush = false;
130
170
  this.loading = false;
131
171
  this.lines = 3;
172
+
173
+ // A click on the card, but not on another control in it, follows the
174
+ // link in the heading slot
175
+ this.addEventListener("click", (event) => {
176
+ const link = this.#link();
177
+
178
+ if (link === null || this.loading) return;
179
+
180
+ const path = event.composedPath();
181
+ const own = path.slice(0, path.indexOf(this));
182
+
183
+ if (
184
+ own.some(
185
+ (node) =>
186
+ node instanceof HTMLElement &&
187
+ node.matches(
188
+ "a, button, input, select, textarea, label, [tabindex], gw-button",
189
+ ),
190
+ )
191
+ ) {
192
+ return;
193
+ }
194
+
195
+ // Selecting text in the card shouldn't open the link
196
+ if (getSelection()?.toString()) return;
197
+
198
+ link.click();
199
+ });
200
+
201
+ const focus = () => {
202
+ this.#card.classList.toggle(
203
+ "focused",
204
+ this.#link()?.matches(":focus-visible") === true,
205
+ );
206
+ };
207
+
208
+ this.addEventListener("focusin", focus);
209
+ this.addEventListener("focusout", focus);
210
+
211
+ this.#headingSlot.addEventListener("slotchange", () => {
212
+ this.requestUpdate("heading");
213
+ });
214
+ }
215
+
216
+ /** The `<a>` in the heading slot, or holding it, when there is one. */
217
+ #link(): HTMLAnchorElement | null {
218
+ const heading = this.querySelector(":scope > [slot='heading']");
219
+
220
+ if (heading instanceof HTMLAnchorElement) return heading;
221
+
222
+ return heading?.querySelector("a") ?? null;
132
223
  }
133
224
 
134
225
  protected override create(root: ShadowRoot) {
@@ -160,9 +251,14 @@ export class GwCard extends GwElement {
160
251
  if (this.#card instanceof HTMLAnchorElement) this.#card.href = this.href;
161
252
 
162
253
  setAttribute(this.#card, "aria-busy", this.loading ? "true" : null);
254
+
255
+ const slotted = this.querySelector(":scope > [slot='heading']") !== null;
256
+
257
+ this.#card.classList.toggle("linked", this.#link() !== null && !this.href);
163
258
  this.#title.textContent = this.heading;
164
- this.#titleSkeleton.hidden = this.heading !== "" || !this.loading;
165
- this.#header.hidden = !this.heading && !this.loading;
259
+ this.#titleSkeleton.hidden =
260
+ this.heading !== "" || slotted || !this.loading;
261
+ this.#header.hidden = !this.heading && !slotted && !this.loading;
166
262
  this.#skeleton.lines = this.lines;
167
263
  this.#skeleton.hidden = !this.loading;
168
264
  this.#body.hidden = this.loading;
package/src/gw-chip.ts CHANGED
@@ -7,7 +7,13 @@
7
7
  import { GwElement, css, h, setAttribute } from "./element.js";
8
8
  import "./gw-skeleton.js";
9
9
  import { fadeIn } from "./motion.js";
10
- import { base, createIcon, setIconPath, type Tone } from "./styles.js";
10
+ import {
11
+ base,
12
+ controlSizes,
13
+ createIcon,
14
+ setIconPath,
15
+ type Tone,
16
+ } from "./styles.js";
11
17
 
12
18
  /**
13
19
  * A short status or category, tinted by its `tone`. While `loading`, a
@@ -19,42 +25,56 @@ import { base, createIcon, setIconPath, type Tone } from "./styles.js";
19
25
  * @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The tint. Defaults to `neutral`.
20
26
  * @attr {string} icon - SVG path data for an icon before the text.
21
27
  * @attr {boolean} loading - Shows a skeleton for the text.
28
+ * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The size, around 32px tall at medium. Defaults to `medium`.
22
29
  *
23
30
  * @slot - The text.
24
31
  * @slot icon - An icon before the text, in place of `icon`.
25
32
  *
26
33
  * @csspart chip - The chip.
34
+ *
35
+ * @cssprop --gw-tone-fill - The tint, for a tone of your own. `tone` sets it.
36
+ * @cssprop --gw-tone-border - The border. `tone` sets it.
37
+ * @cssprop --gw-chip-scale - Scales the chip, around 1 for medium.
38
+ * @cssprop --gw-control-scale - Scales its height and icon with the theme's density. The theme sets it.
27
39
  */
28
40
  export class GwChip extends GwElement {
29
41
  static override properties = {
30
42
  tone: { reflect: true },
31
43
  icon: {},
32
44
  loading: { type: "boolean", reflect: true },
45
+ size: { reflect: true },
33
46
  } as const;
34
47
 
35
48
  static override styles = [
36
49
  base,
50
+ controlSizes("chip"),
37
51
  css`
38
52
  :host {
39
53
  display: inline-flex;
40
54
  }
41
55
 
42
- /* 32px, with 12px padding */
56
+ /* 32px at medium, with 12px padding */
43
57
  .chip {
58
+ --gw-size: var(--gw-chip-scale, 1);
59
+ --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1));
60
+ --gw-icon-size: calc(1rem * var(--gw-scale));
61
+
44
62
  display: inline-flex;
45
- gap: var(--gw-space-2);
63
+ gap: calc(var(--gw-space-2) * var(--gw-size));
46
64
  align-items: center;
47
65
  box-sizing: border-box;
48
- height: 2rem;
49
- padding: 0 var(--gw-space-3);
50
- font-size: var(--gw-font-size-xs);
66
+ height: calc(2rem * var(--gw-scale));
67
+ padding: 0 calc(var(--gw-space-3) * var(--gw-size));
68
+ font-size: max(
69
+ var(--gw-font-size-2xs),
70
+ calc(var(--gw-font-size-xs) * var(--gw-size))
71
+ );
51
72
  font-weight: var(--gw-font-weight-strong);
52
73
  color: var(--gw-color-text-strong);
53
74
  background: var(--gw-tone-fill, var(--gw-color-fill-neutral-resting));
54
75
  border: var(--gw-border-width) solid
55
76
  var(--gw-tone-border, var(--gw-color-border-quiet));
56
77
  border-radius: var(--gw-radius-chip);
57
- --gw-icon-size: 1rem;
58
78
  }
59
79
 
60
80
  .content {
@@ -69,6 +89,7 @@ export class GwChip extends GwElement {
69
89
  /** SVG path data for an icon before the text, instead of the `icon` slot. */
70
90
  declare icon: string;
71
91
  declare loading: boolean;
92
+ declare size: "xs" | "small" | "medium" | "large" | "xl";
72
93
 
73
94
  readonly #icon = createIcon("");
74
95
 
@@ -90,6 +111,7 @@ export class GwChip extends GwElement {
90
111
  this.tone = "neutral";
91
112
  this.icon = "";
92
113
  this.loading = false;
114
+ this.size = "medium";
93
115
  }
94
116
 
95
117
  protected override create(root: ShadowRoot) {
@@ -28,6 +28,10 @@ const sheetBelow = 600;
28
28
  * @attr {string} shortcut - The key that opens it with Cmd or Ctrl, such as `mod+k`, or `none`. Defaults to `mod+k`.
29
29
  * @attr {boolean} open - Whether it shows.
30
30
  *
31
+ * @csspart dialog - The menu's surface.
32
+ * @csspart list - The `gw-option-list` of commands.
33
+ * @csspart footer - The row of key hints.
34
+ *
31
35
  * @fires {CustomEvent<{ value: string }>} pick - When a command is picked, with its `value` in `detail`.
32
36
  */
33
37
  export class GwCommandMenu extends GwElement {
@@ -150,15 +154,15 @@ export class GwCommandMenu extends GwElement {
150
154
  declare shortcut: string;
151
155
  declare open: boolean;
152
156
 
153
- readonly #list = h("gw-option-list", { search: true });
157
+ readonly #list = h("gw-option-list", { search: true, part: "list" });
154
158
 
155
159
  readonly #dialog = h(
156
160
  "dialog",
157
- {},
161
+ { part: "dialog" },
158
162
  this.#list,
159
163
  h(
160
164
  "footer",
161
- { "aria-hidden": "true" },
165
+ { part: "footer", "aria-hidden": "true" },
162
166
  h("span", {}, h("kbd", {}, "↑"), " ", h("kbd", {}, "↓"), " to move"),
163
167
  h("span", {}, h("kbd", {}, "Enter"), " to pick"),
164
168
  h("span", {}, h("kbd", {}, "Esc"), " to close"),
package/src/gw-dialog.ts CHANGED
@@ -28,6 +28,12 @@ const closeIcon =
28
28
  * @slot - The content.
29
29
  * @slot actions - Buttons; one with a `value` closes it with that value.
30
30
  *
31
+ * @csspart dialog - The dialog's surface.
32
+ * @csspart header - The row with the heading and close button.
33
+ * @csspart heading - The heading.
34
+ * @csspart body - The content.
35
+ * @csspart footer - The actions row.
36
+ *
31
37
  * @fires {Event} close - When it closes; `returnValue` holds the button's value.
32
38
  */
33
39
  export class GwDialog extends GwElement {
@@ -139,9 +145,13 @@ export class GwDialog extends GwElement {
139
145
  this.open = false;
140
146
  }
141
147
 
142
- readonly #heading = h("h2", { id: "heading" });
148
+ readonly #heading = h("h2", { id: "heading", part: "heading" });
143
149
 
144
- readonly #footer = h("footer", {}, h("slot", { name: "actions" }));
150
+ readonly #footer = h(
151
+ "footer",
152
+ { part: "footer" },
153
+ h("slot", { name: "actions" }),
154
+ );
145
155
 
146
156
  readonly #closeButton = h(
147
157
  "gw-button",
@@ -158,9 +168,9 @@ export class GwDialog extends GwElement {
158
168
 
159
169
  readonly #dialog = h(
160
170
  "dialog",
161
- { "aria-labelledby": "heading" },
162
- h("header", {}, this.#heading, this.#closeButton),
163
- h("div", { class: "body" }, h("slot")),
171
+ { "aria-labelledby": "heading", part: "dialog" },
172
+ h("header", { part: "header" }, this.#heading, this.#closeButton),
173
+ h("div", { class: "body", part: "body" }, h("slot")),
164
174
  this.#footer,
165
175
  );
166
176
 
package/src/gw-dot.ts CHANGED
@@ -17,6 +17,8 @@ import { base, type Tone } from "./styles.js";
17
17
  * @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The color. Defaults to `neutral`.
18
18
  * @attr {string} label - What it means, for assistive technology.
19
19
  * @attr {boolean} pulse - Pulses, for something live.
20
+ *
21
+ * @cssprop --gw-tone - The color, for a tone of your own. `tone` sets it.
20
22
  */
21
23
  export class GwDot extends GwElement {
22
24
  static override properties = {
@@ -25,6 +25,8 @@ import { base } from "./styles.js";
25
25
  *
26
26
  * @csspart details - The native `<details>`.
27
27
  * @csspart summary - The summary row.
28
+ * @csspart heading - The heading.
29
+ * @csspart description - The line under the heading.
28
30
  * @csspart content - The content's wrapper.
29
31
  *
30
32
  * @fires {Event} toggle - When it opens or closes.
@@ -169,9 +171,12 @@ export class GwExpansion extends GwElement {
169
171
  declare description: string;
170
172
  declare open: boolean;
171
173
 
172
- readonly #heading = h("span", { class: "heading" });
174
+ readonly #heading = h("span", { class: "heading", part: "heading" });
173
175
 
174
- readonly #description = h("span", { class: "description" });
176
+ readonly #description = h("span", {
177
+ class: "description",
178
+ part: "description",
179
+ });
175
180
 
176
181
  readonly #details = h(
177
182
  "details",
package/src/gw-facts.ts CHANGED
@@ -28,6 +28,10 @@ const skeletonWidths = ["60%", "40%", "75%", "50%", "65%"];
28
28
  * @attr {Fact[]} items - The pairs, as JSON.
29
29
  * @attr {boolean} loading - Shows skeleton pairs, keeping any labels already known.
30
30
  * @attr {number} rows - How many skeleton pairs while loading. Defaults to 4.
31
+ *
32
+ * @csspart list - The description list.
33
+ * @csspart label - Each label.
34
+ * @csspart value - Each value.
31
35
  */
32
36
  export class GwFacts extends GwElement {
33
37
  static override properties = {
@@ -69,7 +73,7 @@ export class GwFacts extends GwElement {
69
73
  declare loading: boolean;
70
74
  declare rows: number;
71
75
 
72
- readonly #list = h("dl");
76
+ readonly #list = h("dl", { part: "list" });
73
77
 
74
78
  #rendered = false;
75
79
 
@@ -97,13 +101,13 @@ export class GwFacts extends GwElement {
97
101
  ...Array.from({ length: count }, (_, index) => [
98
102
  h(
99
103
  "dt",
100
- {},
104
+ { part: "label" },
101
105
  items[index]?.label ??
102
106
  h("gw-skeleton-text", { style: "--gw-skeleton-width: 5rem" }),
103
107
  ),
104
108
  h(
105
109
  "dd",
106
- {},
110
+ { part: "value" },
107
111
  h("gw-skeleton-text", {
108
112
  style: `--gw-skeleton-width: ${skeletonWidths[index % skeletonWidths.length]}`,
109
113
  }),
@@ -114,8 +118,12 @@ export class GwFacts extends GwElement {
114
118
  this.#list.removeAttribute("aria-busy");
115
119
  this.#list.replaceChildren(
116
120
  ...items.flatMap((item) => [
117
- h("dt", {}, item.label),
118
- h("dd", {}, loaded ? h("span", {}, item.value) : item.value),
121
+ h("dt", { part: "label" }, item.label),
122
+ h(
123
+ "dd",
124
+ { part: "value" },
125
+ loaded ? h("span", {}, item.value) : item.value,
126
+ ),
119
127
  ]),
120
128
  );
121
129
  }
package/src/gw-gauge.ts CHANGED
@@ -76,6 +76,12 @@ const circle = (className: string) => {
76
76
  * @attr {boolean} inverted - Lower is worse, such as for a battery.
77
77
  * @attr {boolean} loading - Pulses the track and shows a skeleton for the reading.
78
78
  *
79
+ * @csspart dial - The dial, with the arc and the reading.
80
+ * @csspart value - The reading's number.
81
+ * @csspart unit - The unit after it.
82
+ * @csspart label - What it measures.
83
+ * @csspart caption - The line under the label.
84
+ *
79
85
  * @cssprop --gw-gauge-size - The dial's width. Defaults to `8.5rem`.
80
86
  * @cssprop --gw-gauge-color - The arc's color, overriding the thresholds.
81
87
  */
@@ -208,11 +214,11 @@ export class GwGauge extends GwElement {
208
214
 
209
215
  readonly #fill = circle("fill");
210
216
 
211
- readonly #dial = h("div", { class: "dial", role: "meter" });
217
+ readonly #dial = h("div", { class: "dial", part: "dial", role: "meter" });
212
218
 
213
- readonly #value = h("span", { class: "value" });
219
+ readonly #value = h("span", { class: "value", part: "value" });
214
220
 
215
- readonly #unit = h("span", { class: "unit" });
221
+ readonly #unit = h("span", { class: "unit", part: "unit" });
216
222
 
217
223
  readonly #shown = h("span", { class: "shown" }, this.#value, this.#unit);
218
224
 
@@ -220,9 +226,9 @@ export class GwGauge extends GwElement {
220
226
  style: "--gw-skeleton-width: 2em",
221
227
  });
222
228
 
223
- readonly #label = h("span", { class: "label" });
229
+ readonly #label = h("span", { class: "label", part: "label" });
224
230
 
225
- readonly #caption = h("span", { class: "caption" });
231
+ readonly #caption = h("span", { class: "caption", part: "caption" });
226
232
 
227
233
  readonly #captionSkeleton = h("gw-skeleton-text", {
228
234
  style: "--gw-skeleton-width: 5rem",
package/src/gw-input.ts CHANGED
@@ -25,6 +25,10 @@ import { LightElement, fieldId } from "./fields.js";
25
25
  * @attr {string} error - An error under the field, which marks it invalid.
26
26
  * @attr {boolean} required - Requires a value.
27
27
  * @attr {boolean} disabled - Disables the field.
28
+ * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The field's size, 40px tall at medium. Defaults to `medium`.
29
+ *
30
+ * @cssprop --gw-field-scale - Scales the field, around 1 for medium.
31
+ * @cssprop --gw-control-scale - Scales its height with the theme's density. The theme sets it.
28
32
  */
29
33
  export class GwInput extends LightElement {
30
34
  static override properties = {
@@ -39,6 +43,7 @@ export class GwInput extends LightElement {
39
43
  error: {},
40
44
  required: { type: "boolean" },
41
45
  disabled: { type: "boolean" },
46
+ size: { reflect: true },
42
47
  } as const;
43
48
 
44
49
  declare label: string;
@@ -53,6 +58,7 @@ export class GwInput extends LightElement {
53
58
  declare error: string;
54
59
  declare required: boolean;
55
60
  declare disabled: boolean;
61
+ declare size: "xs" | "small" | "medium" | "large" | "xl";
56
62
 
57
63
  readonly #id = fieldId();
58
64
 
@@ -79,6 +85,7 @@ export class GwInput extends LightElement {
79
85
  this.error = "";
80
86
  this.required = false;
81
87
  this.disabled = false;
88
+ this.size = "medium";
82
89
  }
83
90
 
84
91
  protected override create(root: HTMLElement) {
@@ -5,7 +5,7 @@
5
5
  * @module
6
6
  */
7
7
  import { GwElement, css, h } from "./element.js";
8
- import { base, createIcon, setIconPath } from "./styles.js";
8
+ import { base, controlSizes, createIcon, setIconPath } from "./styles.js";
9
9
 
10
10
  export interface MenuItem {
11
11
  readonly value: string;
@@ -38,9 +38,13 @@ export interface MenuItem {
38
38
  * @attr {string} empty - What the menu says with no items. Defaults to `Nothing here yet`.
39
39
  * @attr {MenuItem[]} items - The items, as JSON.
40
40
  * @attr {MenuItem[]} actions - Actions pinned under the items, as JSON.
41
+ * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The trigger's size, 24 to 56px tall. Defaults to `medium`.
41
42
  *
42
43
  * @csspart trigger - The button that opens the menu.
43
44
  *
45
+ * @cssprop --gw-menu-button-scale - Scales the trigger, around 1 for medium.
46
+ * @cssprop --gw-control-scale - Scales its height and icon with the theme's density. The theme sets it.
47
+ *
44
48
  * @fires {CustomEvent<{ value: string }>} select - When an item or action is chosen, with its `value` in `detail`.
45
49
  */
46
50
  export class GwMenuButton extends GwElement {
@@ -50,10 +54,12 @@ export class GwMenuButton extends GwElement {
50
54
  empty: {},
51
55
  items: { type: "json" },
52
56
  actions: { type: "json" },
57
+ size: { reflect: true },
53
58
  } as const;
54
59
 
55
60
  static override styles = [
56
61
  base,
62
+ controlSizes("menu-button"),
57
63
  css`
58
64
  :host {
59
65
  position: relative;
@@ -62,14 +68,18 @@ export class GwMenuButton extends GwElement {
62
68
  }
63
69
 
64
70
  .trigger {
71
+ --gw-size: var(--gw-menu-button-scale, 1);
72
+ --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1));
73
+ --gw-icon-size: calc(1.25rem * var(--gw-scale));
74
+
65
75
  display: inline-flex;
66
- gap: var(--gw-space-2);
76
+ gap: calc(var(--gw-space-2) * var(--gw-size));
67
77
  align-items: center;
68
78
  box-sizing: border-box;
69
- height: 2.5rem;
70
- padding: 0 var(--gw-space-4);
79
+ height: calc(2.5rem * var(--gw-scale));
80
+ padding: 0 calc(var(--gw-space-4) * var(--gw-size));
71
81
  font: inherit;
72
- font-size: var(--gw-font-size-s);
82
+ font-size: calc(var(--gw-font-size-s) * var(--gw-size));
73
83
  font-weight: var(--gw-font-weight-strong);
74
84
  color: var(--gw-color-text-strong);
75
85
  cursor: pointer;
@@ -233,6 +243,7 @@ export class GwMenuButton extends GwElement {
233
243
  declare empty: string;
234
244
  declare items: readonly MenuItem[];
235
245
  declare actions: readonly MenuItem[];
246
+ declare size: "xs" | "small" | "medium" | "large" | "xl";
236
247
 
237
248
  readonly #trigger = h("button", {
238
249
  class: "trigger",
@@ -264,6 +275,7 @@ export class GwMenuButton extends GwElement {
264
275
  this.empty = "Nothing here yet";
265
276
  this.items = [];
266
277
  this.actions = [];
278
+ this.size = "medium";
267
279
  }
268
280
 
269
281
  /** The enabled items then actions, in the order arrow keys move through them. */
package/src/gw-meter.ts CHANGED
@@ -26,6 +26,11 @@ import { base, type Tone } from "./styles.js";
26
26
  * @attr {string} display - The value as shown, such as `42 GB`. Defaults to a percentage.
27
27
  * @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - A fixed color, overriding `high` and `critical`.
28
28
  * @attr {boolean} loading - Pulses the track and shows a skeleton for the value.
29
+ *
30
+ * @csspart top - The row with the label and value.
31
+ * @csspart value - The value as shown.
32
+ * @csspart track - The bar's track.
33
+ * @csspart fill - The filled part of the bar.
29
34
  */
30
35
  export class GwMeter extends GwElement {
31
36
  static override properties = {
@@ -93,7 +98,7 @@ export class GwMeter extends GwElement {
93
98
  declare tone: Tone | undefined;
94
99
  declare loading: boolean;
95
100
 
96
- readonly #top = h("div", { class: "top" });
101
+ readonly #top = h("div", { class: "top", part: "top" });
97
102
 
98
103
  readonly #label = h("span");
99
104
 
@@ -101,15 +106,23 @@ export class GwMeter extends GwElement {
101
106
  style: "--gw-skeleton-width: 6rem",
102
107
  });
103
108
 
104
- readonly #value = h("span", { class: "value", "aria-hidden": "true" });
109
+ readonly #value = h("span", {
110
+ class: "value",
111
+ part: "value",
112
+ "aria-hidden": "true",
113
+ });
105
114
 
106
115
  readonly #valueSkeleton = h("gw-skeleton-text", {
107
116
  style: "--gw-skeleton-width: 2.5rem",
108
117
  });
109
118
 
110
- readonly #fill = h("div", { class: "fill" });
119
+ readonly #fill = h("div", { class: "fill", part: "fill" });
111
120
 
112
- readonly #track = h("div", { class: "track", role: "meter" }, this.#fill);
121
+ readonly #track = h(
122
+ "div",
123
+ { class: "track", part: "track", role: "meter" },
124
+ this.#fill,
125
+ );
113
126
 
114
127
  readonly #bar = h("gw-skeleton", { class: "bar" });
115
128
 
@@ -29,6 +29,10 @@ const emptyIcon =
29
29
  *
30
30
  * @slot - The `gw-notification` cards.
31
31
  *
32
+ * @csspart header - The row with the heading and Clear all.
33
+ * @csspart heading - The heading.
34
+ * @csspart list - The list of cards.
35
+ *
32
36
  * @fires {Event} clear - When Clear all is pressed.
33
37
  */
34
38
  export class GwNotificationCenter extends GwElement {
@@ -75,7 +79,7 @@ export class GwNotificationCenter extends GwElement {
75
79
 
76
80
  declare heading: string;
77
81
 
78
- readonly #heading = h("h2");
82
+ readonly #heading = h("h2", { part: "heading" });
79
83
 
80
84
  readonly #clear = h(
81
85
  "gw-button",
@@ -83,7 +87,11 @@ export class GwNotificationCenter extends GwElement {
83
87
  "Clear all",
84
88
  );
85
89
 
86
- readonly #list = h("div", { class: "list", role: "list" }, h("slot"));
90
+ readonly #list = h(
91
+ "div",
92
+ { class: "list", part: "list", role: "list" },
93
+ h("slot"),
94
+ );
87
95
 
88
96
  readonly #empty = h(
89
97
  "gw-empty-state",
@@ -138,7 +146,10 @@ export class GwNotificationCenter extends GwElement {
138
146
  this.#clear.addEventListener("click", () => {
139
147
  this.clear();
140
148
  });
141
- root.append(h("header", {}, this.#heading, this.#clear), this.#body);
149
+ root.append(
150
+ h("header", { part: "header" }, this.#heading, this.#clear),
151
+ this.#body,
152
+ );
142
153
  }
143
154
 
144
155
  protected override update() {
@@ -34,6 +34,8 @@ const closeIcon =
34
34
  *
35
35
  * @csspart card - The card.
36
36
  *
37
+ * @cssprop --gw-tone - The icon's color, for a tone of your own. `tone` sets it.
38
+ *
37
39
  * @fires {Event} dismiss - When the close button is pressed. Cancel it to keep the card.
38
40
  */
39
41
  export class GwNotification extends GwElement {