@timmo001/groundwork-ui 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Groundwork's framework-free web components, styled by the --gw-* tokens",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,9 +10,11 @@
10
10
  },
11
11
  "files": [
12
12
  "src",
13
+ "custom-elements.json",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
17
+ "customElements": ".manifest-check/custom-elements.json",
16
18
  "sideEffects": true,
17
19
  "publishConfig": {
18
20
  "access": "public",
@@ -28,9 +30,12 @@
28
30
  "lint": "oxlint .",
29
31
  "typecheck": "tsc",
30
32
  "format": "prettier --write .",
31
- "format:check": "prettier --check ."
33
+ "format:check": "prettier --check .",
34
+ "gen": "bun run scripts/manifest.ts",
35
+ "gen:check": "bun run scripts/manifest.ts --check"
32
36
  },
33
37
  "devDependencies": {
38
+ "@custom-elements-manifest/analyzer": "0.11.0",
34
39
  "@oxlint/plugins": "1.90.0",
35
40
  "@timmo001/oxlint-rules": "0.6.3",
36
41
  "oxlint": "1.90.0",
package/src/events.ts ADDED
@@ -0,0 +1,44 @@
1
+ // Generated by `mise run gen` from the elements' `@fires` docstrings. Don't edit.
2
+
3
+ /** The events each `gw-` element fires, by tag. `on()` reads it. */
4
+ export interface GwEventMap {
5
+ "gw-app-bar": {
6
+ toggle: Event;
7
+ };
8
+ "gw-command-menu": {
9
+ pick: CustomEvent<{ value: string }>;
10
+ };
11
+ "gw-dialog": {
12
+ close: Event;
13
+ };
14
+ "gw-expansion": {
15
+ toggle: Event;
16
+ };
17
+ "gw-menu-button": {
18
+ select: CustomEvent<{ value: string }>;
19
+ };
20
+ "gw-notification": {
21
+ dismiss: Event;
22
+ };
23
+ "gw-notification-center": {
24
+ clear: Event;
25
+ };
26
+ "gw-option-list": {
27
+ pick: CustomEvent<{ value: string }>;
28
+ };
29
+ "gw-popover": {
30
+ close: Event;
31
+ };
32
+ "gw-segmented": {
33
+ change: Event;
34
+ };
35
+ "gw-select": {
36
+ change: Event;
37
+ };
38
+ "gw-theme-picker": {
39
+ reset: Event;
40
+ mode: CustomEvent<{ mode: string }>;
41
+ theme: CustomEvent<{ id: string }>;
42
+ import: CustomEvent<{ text: string, name: string }>;
43
+ };
44
+ }
package/src/fields.ts CHANGED
@@ -216,6 +216,40 @@ export const fieldStyles = css`
216
216
  background: rgb(0 0 0 / 40%);
217
217
  }
218
218
 
219
+ /* With multiple, a full-width Clear under the list empties the choice */
220
+ gw-select .gw-select-clear {
221
+ display: block;
222
+ width: 100%;
223
+ min-height: 2.5rem;
224
+ padding: 0 var(--gw-space-3);
225
+ font: inherit;
226
+ font-size: var(--gw-font-size-s);
227
+ font-weight: var(--gw-font-weight-strong);
228
+ color: var(--gw-color-text-secondary);
229
+ cursor: pointer;
230
+ background: none;
231
+ border: 0;
232
+ border-block-start: var(--gw-border-width) solid
233
+ var(--gw-color-border-quiet);
234
+ transition:
235
+ color var(--gw-motion-fast) var(--gw-motion-easing),
236
+ background-color var(--gw-motion-fast) var(--gw-motion-easing);
237
+ }
238
+
239
+ gw-select .gw-select-clear:hover {
240
+ color: var(--gw-color-text-strong);
241
+ background: var(--gw-color-fill-neutral-hovered);
242
+ }
243
+
244
+ gw-select .gw-select-clear:focus-visible {
245
+ outline: var(--gw-focus-width) solid var(--gw-color-focus);
246
+ outline-offset: calc(var(--gw-focus-width) * -1);
247
+ }
248
+
249
+ gw-select .gw-select-clear[hidden] {
250
+ display: none;
251
+ }
252
+
219
253
  :where(gw-input, gw-select) :where(input, .gw-select-trigger):hover {
220
254
  border-color: var(--gw-color-border-loud);
221
255
  }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * `gw-app-bar`: a header pinned to the top, floating once the page scrolls.
3
+ * Docs: https://groundwork.timmo.dev/components/layout/#app-bar
4
+ *
5
+ * @module
6
+ */
7
+ import { GwElement, css, h, setAttribute } from "./element.js";
8
+ import { base, createIcon, setIconPath } from "./styles.js";
9
+
10
+ // MDI's menu and close
11
+ const menuIcon = "M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z";
12
+
13
+ const closeIcon =
14
+ "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z";
15
+
16
+ /** Below this width the nav moves into a menu. */
17
+ const menuBelow = 600;
18
+
19
+ /**
20
+ * A site or app header pinned to the top of the page, with a brand, nav
21
+ * links and actions, full width with its content kept to the content width.
22
+ * It's transparent at first, then gets a blurred surface and a bottom border
23
+ * once the page scrolls, or once the element
24
+ * `reveal-after` names scrolls out of view, such as a hero. With
25
+ * `reveal-after` the brand stays hidden until then, so a large logo in the
26
+ * hero hands over to the small one in the bar.
27
+ *
28
+ * `overlay` lays it over the top of the page rather than taking room, for a
29
+ * page that opens with a hero. Below 600px wide the nav moves into a menu
30
+ * opened from a button, which closes on Esc, a click outside or a link.
31
+ *
32
+ * @element gw-app-bar
33
+ * @summary A header pinned to the top, floating once the page scrolls.
34
+ *
35
+ * @attr {string} reveal-after - A CSS selector for the element, such as a hero, to scroll past before the bar floats and the brand shows.
36
+ * @attr {boolean} overlay - Lays the bar over the top of the page instead of taking room.
37
+ * @attr {boolean} floating - Whether it floats. Set by the bar as the page scrolls.
38
+ * @attr {boolean} menu-open - Whether the nav menu is open on narrow screens.
39
+ * @attr {string} menu-label - The menu button's accessible name. Defaults to `Menu`.
40
+ *
41
+ * @slot brand - A logo or name linking home.
42
+ * @slot - The nav links.
43
+ * @slot actions - Icon links and buttons at the end.
44
+ *
45
+ * @csspart bar - The bar.
46
+ * @csspart nav - The nav links' container, the menu panel on narrow screens.
47
+ *
48
+ * @fires {Event} toggle - When the nav menu opens or closes.
49
+ */
50
+ export class GwAppBar extends GwElement {
51
+ static override properties = {
52
+ revealAfter: { attribute: "reveal-after" },
53
+ overlay: { type: "boolean", reflect: true },
54
+ floating: { type: "boolean", reflect: true },
55
+ menuOpen: { type: "boolean", attribute: "menu-open", reflect: true },
56
+ menuLabel: { attribute: "menu-label" },
57
+ } as const;
58
+
59
+ static override styles = [
60
+ base,
61
+ css`
62
+ :host {
63
+ position: sticky;
64
+ inset-block-start: 0;
65
+ z-index: 50;
66
+ display: block;
67
+ }
68
+
69
+ :host([overlay]) {
70
+ position: fixed;
71
+ inset-inline: 0;
72
+ }
73
+
74
+ .bar {
75
+ position: relative;
76
+ display: flex;
77
+ gap: var(--gw-space-3);
78
+ align-items: center;
79
+ min-height: 3.5rem;
80
+ padding: var(--gw-space-1)
81
+ max(var(--gw-space-3), (100% - var(--gw-content-width)) / 2);
82
+ border-block-end: max(var(--gw-border-width), 1px) solid transparent;
83
+ transition:
84
+ background-color var(--gw-motion-normal) var(--gw-motion-easing),
85
+ border-color var(--gw-motion-normal) var(--gw-motion-easing),
86
+ backdrop-filter var(--gw-motion-normal) var(--gw-motion-easing);
87
+ }
88
+
89
+ :host([floating]) .bar,
90
+ :host([menu-open]) .bar {
91
+ background: color-mix(
92
+ in oklab,
93
+ var(--gw-color-surface-default) 82%,
94
+ transparent
95
+ );
96
+ border-color: var(--gw-color-border-quiet);
97
+ backdrop-filter: blur(12px) saturate(1.4);
98
+ }
99
+
100
+ .brand {
101
+ display: flex;
102
+ flex: none;
103
+ align-items: center;
104
+ transition:
105
+ opacity var(--gw-motion-normal) var(--gw-motion-easing),
106
+ translate var(--gw-motion-normal) var(--gw-motion-easing);
107
+ }
108
+
109
+ :host([reveal-after]:not([floating])) .brand {
110
+ pointer-events: none;
111
+ visibility: hidden;
112
+ opacity: 0;
113
+ translate: 0 -0.25rem;
114
+ transition:
115
+ opacity var(--gw-motion-normal) var(--gw-motion-easing),
116
+ translate var(--gw-motion-normal) var(--gw-motion-easing),
117
+ visibility var(--gw-motion-normal) allow-discrete;
118
+ }
119
+
120
+ .nav {
121
+ display: flex;
122
+ flex: 1;
123
+ gap: var(--gw-space-1);
124
+ align-items: center;
125
+ min-width: 0;
126
+ }
127
+
128
+ .actions {
129
+ display: flex;
130
+ flex: none;
131
+ gap: var(--gw-space-1);
132
+ align-items: center;
133
+ margin-inline-start: auto;
134
+ }
135
+
136
+ .menu {
137
+ display: none;
138
+ place-items: center;
139
+ width: 2.5rem;
140
+ height: 2.5rem;
141
+ padding: 0;
142
+ color: var(--gw-color-text-strong);
143
+ cursor: pointer;
144
+ background: none;
145
+ border: 0;
146
+ border-radius: var(--gw-radius-pill);
147
+ transition: background-color var(--gw-motion-fast)
148
+ var(--gw-motion-easing);
149
+ }
150
+
151
+ .menu:hover {
152
+ background: var(--gw-color-fill-neutral-hovered);
153
+ }
154
+
155
+ @media (max-width: ${menuBelow - 0.02}px) {
156
+ .menu {
157
+ display: inline-grid;
158
+ }
159
+
160
+ .nav {
161
+ position: absolute;
162
+ inset-block-start: 100%;
163
+ inset-inline: 0;
164
+ flex-direction: column;
165
+ gap: var(--gw-space-0-5);
166
+ align-items: stretch;
167
+ padding: var(--gw-space-2) var(--gw-space-3);
168
+ visibility: hidden;
169
+ background: var(--gw-color-surface-default);
170
+ border-block-end: max(var(--gw-border-width), 1px) solid
171
+ var(--gw-color-border-quiet);
172
+ box-shadow: var(--gw-shadow-m);
173
+ opacity: 0;
174
+ translate: 0 -0.5rem;
175
+ transition:
176
+ opacity var(--gw-motion-fast) var(--gw-motion-easing),
177
+ translate var(--gw-motion-normal) var(--gw-motion-easing),
178
+ visibility var(--gw-motion-normal) allow-discrete;
179
+ }
180
+
181
+ :host([menu-open]) .nav {
182
+ visibility: visible;
183
+ opacity: 1;
184
+ translate: 0 0;
185
+ }
186
+ }
187
+ `,
188
+ ];
189
+
190
+ declare revealAfter: string;
191
+ declare overlay: boolean;
192
+ declare floating: boolean;
193
+ declare menuOpen: boolean;
194
+ declare menuLabel: string;
195
+
196
+ readonly #menuIcon = createIcon(menuIcon);
197
+
198
+ readonly #menu = h(
199
+ "button",
200
+ { type: "button", class: "menu", "aria-expanded": "false" },
201
+ this.#menuIcon,
202
+ );
203
+
204
+ readonly #nav = h("div", { class: "nav", part: "nav" }, h("slot"));
205
+
206
+ #observer: IntersectionObserver | undefined;
207
+
208
+ #frame = 0;
209
+
210
+ constructor() {
211
+ super();
212
+ this.revealAfter = "";
213
+ this.overlay = false;
214
+ this.floating = false;
215
+ this.menuOpen = false;
216
+ this.menuLabel = "Menu";
217
+ }
218
+
219
+ override connectedCallback() {
220
+ super.connectedCallback();
221
+ document.addEventListener("keydown", this.#onKeydown);
222
+ document.addEventListener("pointerdown", this.#onOutside, true);
223
+
224
+ if (document.readyState === "loading") {
225
+ document.addEventListener("DOMContentLoaded", () => this.#watch(), {
226
+ once: true,
227
+ });
228
+ } else {
229
+ this.#watch();
230
+ }
231
+ }
232
+
233
+ override disconnectedCallback() {
234
+ this.#unwatch();
235
+ document.removeEventListener("keydown", this.#onKeydown);
236
+ document.removeEventListener("pointerdown", this.#onOutside, true);
237
+ super.disconnectedCallback();
238
+ }
239
+
240
+ /** Opens or closes the nav menu on narrow screens. */
241
+ toggleMenu(open = !this.menuOpen) {
242
+ if (open === this.menuOpen) return;
243
+
244
+ this.menuOpen = open;
245
+ this.dispatchEvent(new Event("toggle"));
246
+ }
247
+
248
+ readonly #onScroll = () => {
249
+ if (this.#frame !== 0) return;
250
+
251
+ this.#frame = requestAnimationFrame(() => {
252
+ this.#frame = 0;
253
+ this.floating = window.scrollY > 0;
254
+ });
255
+ };
256
+
257
+ readonly #onKeydown = (event: KeyboardEvent) => {
258
+ if (event.key === "Escape" && this.menuOpen) {
259
+ this.toggleMenu(false);
260
+ this.#menu.focus();
261
+ }
262
+ };
263
+
264
+ readonly #onOutside = (event: PointerEvent) => {
265
+ if (this.menuOpen && !event.composedPath().includes(this)) {
266
+ this.toggleMenu(false);
267
+ }
268
+ };
269
+
270
+ #unwatch() {
271
+ this.#observer?.disconnect();
272
+ this.#observer = undefined;
273
+ window.removeEventListener("scroll", this.#onScroll);
274
+ cancelAnimationFrame(this.#frame);
275
+ this.#frame = 0;
276
+ }
277
+
278
+ /** Floats past the `reveal-after` element, or once the page scrolls without one. */
279
+ #watch() {
280
+ this.#unwatch();
281
+
282
+ const target = this.revealAfter
283
+ ? document.querySelector(this.revealAfter)
284
+ : null;
285
+
286
+ if (target === null) {
287
+ window.addEventListener("scroll", this.#onScroll, { passive: true });
288
+ this.floating = window.scrollY > 0;
289
+
290
+ return;
291
+ }
292
+
293
+ // The bar floats once the target's bottom passes under the bar
294
+ this.#observer = new IntersectionObserver(
295
+ ([entry]) => {
296
+ if (entry !== undefined) this.floating = !entry.isIntersecting;
297
+ },
298
+ { rootMargin: `-${this.offsetHeight}px 0px 0px 0px` },
299
+ );
300
+ this.#observer.observe(target);
301
+ }
302
+
303
+ protected override create(root: ShadowRoot) {
304
+ this.#menu.addEventListener("click", () => {
305
+ this.toggleMenu();
306
+ });
307
+ // A link picked from the menu closes it
308
+ this.#nav.addEventListener("click", (event) => {
309
+ if (
310
+ event.composedPath().some((node) => node instanceof HTMLAnchorElement)
311
+ ) {
312
+ this.toggleMenu(false);
313
+ }
314
+ });
315
+
316
+ root.append(
317
+ h(
318
+ "div",
319
+ { class: "bar", part: "bar" },
320
+ h("div", { class: "brand" }, h("slot", { name: "brand" })),
321
+ this.#menu,
322
+ this.#nav,
323
+ h("div", { class: "actions" }, h("slot", { name: "actions" })),
324
+ ),
325
+ );
326
+ }
327
+
328
+ protected override update(changed: ReadonlySet<string>) {
329
+ setAttribute(this.#menu, "aria-label", this.menuLabel);
330
+ setAttribute(this.#menu, "aria-expanded", this.menuOpen ? "true" : "false");
331
+ setIconPath(this.#menuIcon, this.menuOpen ? closeIcon : menuIcon);
332
+
333
+ if (changed.has("revealAfter") && this.isConnected) this.#watch();
334
+ }
335
+ }
336
+
337
+ GwAppBar.define("gw-app-bar");
338
+
339
+ declare global {
340
+ interface HTMLElementTagNameMap {
341
+ "gw-app-bar": GwAppBar;
342
+ }
343
+ }
@@ -28,7 +28,7 @@ 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
- * @fires pick - When a command is picked, with its `value` in `detail`.
31
+ * @fires {CustomEvent<{ value: string }>} pick - When a command is picked, with its `value` in `detail`.
32
32
  */
33
33
  export class GwCommandMenu extends GwElement {
34
34
  static override properties = {
package/src/gw-dialog.ts CHANGED
@@ -28,7 +28,7 @@ const closeIcon =
28
28
  * @slot - The content.
29
29
  * @slot actions - Buttons; one with a `value` closes it with that value.
30
30
  *
31
- * @fires close - When it closes; `returnValue` holds the button's value.
31
+ * @fires {Event} close - When it closes; `returnValue` holds the button's value.
32
32
  */
33
33
  export class GwDialog extends GwElement {
34
34
  static override properties = {
@@ -27,7 +27,7 @@ import { base } from "./styles.js";
27
27
  * @csspart summary - The summary row.
28
28
  * @csspart content - The content's wrapper.
29
29
  *
30
- * @fires toggle - When it opens or closes.
30
+ * @fires {Event} toggle - When it opens or closes.
31
31
  */
32
32
  export class GwExpansion extends GwElement {
33
33
  static override properties = {
@@ -41,7 +41,7 @@ export interface MenuItem {
41
41
  *
42
42
  * @csspart trigger - The button that opens the menu.
43
43
  *
44
- * @fires select - When an item or action is chosen, with its `value` in `detail`.
44
+ * @fires {CustomEvent<{ value: string }>} select - When an item or action is chosen, with its `value` in `detail`.
45
45
  */
46
46
  export class GwMenuButton extends GwElement {
47
47
  static override properties = {
@@ -29,7 +29,7 @@ const emptyIcon =
29
29
  *
30
30
  * @slot - The `gw-notification` cards.
31
31
  *
32
- * @fires clear - When Clear all is pressed.
32
+ * @fires {Event} clear - When Clear all is pressed.
33
33
  */
34
34
  export class GwNotificationCenter extends GwElement {
35
35
  static override properties = {
@@ -34,7 +34,7 @@ const closeIcon =
34
34
  *
35
35
  * @csspart card - The card.
36
36
  *
37
- * @fires dismiss - When the close button is pressed. Cancel it to keep the card.
37
+ * @fires {Event} dismiss - When the close button is pressed. Cancel it to keep the card.
38
38
  */
39
39
  export class GwNotification extends GwElement {
40
40
  static override properties = {
@@ -22,6 +22,8 @@ export interface OptionItem {
22
22
  readonly keywords?: readonly string[];
23
23
  /** Colors shown as small swatches, such as a theme's palette. */
24
24
  readonly swatches?: readonly string[];
25
+ /** One color shown as a dot before the label, such as a tag's color. */
26
+ readonly swatch?: string;
25
27
  }
26
28
 
27
29
  // MDI's magnify and check
@@ -65,23 +67,30 @@ let lastList = 0;
65
67
  * End and Page Up or Down, and pick with Enter or a click; it fires `pick`
66
68
  * with the option's value. Call `reset()` each time it's shown.
67
69
  *
70
+ * With `multiple`, every value in `values` is marked as chosen and the list
71
+ * is announced as multi-select; the owner toggles `values` on each `pick`.
72
+ *
68
73
  * @element gw-option-list
69
74
  * @summary A searchable, windowed list of options.
70
75
  *
71
76
  * @attr {OptionItem[]} options - The options, as JSON.
72
77
  * @attr {string} value - The selected option's value.
78
+ * @attr {boolean} multiple - Marks every value in `values` as chosen, for picking several.
79
+ * @attr {string[]} values - The chosen values with `multiple`, as JSON.
73
80
  * @attr {string} label - The list's accessible name.
74
81
  * @attr {boolean} search - Shows a search field above the list.
75
82
  * @attr {string} placeholder - The search placeholder. Defaults to `Search`.
76
83
  *
77
84
  * @cssprop --gw-option-list-height - The list's maximum height. Defaults to `20rem`.
78
85
  *
79
- * @fires pick - When an option is picked, with its `value` in `detail`.
86
+ * @fires {CustomEvent<{ value: string }>} pick - When an option is picked, with its `value` in `detail`.
80
87
  */
81
88
  export class GwOptionList extends GwElement {
82
89
  static override properties = {
83
90
  options: { type: "json" },
84
91
  value: {},
92
+ multiple: { type: "boolean" },
93
+ values: { type: "json" },
85
94
  label: {},
86
95
  search: { type: "boolean", reflect: true },
87
96
  placeholder: {},
@@ -211,7 +220,8 @@ export class GwOptionList extends GwElement {
211
220
  flex: none;
212
221
  }
213
222
 
214
- .swatches span {
223
+ .swatches span,
224
+ .swatch {
215
225
  width: 0.875rem;
216
226
  height: 0.875rem;
217
227
  margin-inline-start: -0.25rem;
@@ -220,6 +230,13 @@ export class GwOptionList extends GwElement {
220
230
  border-radius: 50%;
221
231
  }
222
232
 
233
+ .swatch {
234
+ flex: none;
235
+ width: 0.625rem;
236
+ height: 0.625rem;
237
+ margin-inline: 0.125rem;
238
+ }
239
+
223
240
  .none {
224
241
  padding: var(--gw-space-4);
225
242
  font-size: var(--gw-font-size-s);
@@ -235,6 +252,8 @@ export class GwOptionList extends GwElement {
235
252
 
236
253
  declare options: readonly OptionItem[];
237
254
  declare value: string;
255
+ declare multiple: boolean;
256
+ declare values: readonly string[];
238
257
  declare label: string;
239
258
  declare search: boolean;
240
259
  declare placeholder: string;
@@ -276,11 +295,19 @@ export class GwOptionList extends GwElement {
276
295
  super();
277
296
  this.options = [];
278
297
  this.value = "";
298
+ this.multiple = false;
299
+ this.values = [];
279
300
  this.label = "";
280
301
  this.search = false;
281
302
  this.placeholder = "Search";
282
303
  }
283
304
 
305
+ #isChosen(option: OptionItem) {
306
+ return this.multiple
307
+ ? Array.isArray(this.values) && this.values.includes(option.value)
308
+ : option.value === this.value;
309
+ }
310
+
284
311
  /** The options left after the search, in order. */
285
312
  get filtered() {
286
313
  return this.#filtered;
@@ -296,7 +323,7 @@ export class GwOptionList extends GwElement {
296
323
  this.#build();
297
324
  this.#active = Math.max(
298
325
  0,
299
- this.#filtered.findIndex((option) => option.value === this.value),
326
+ this.#filtered.findIndex((option) => this.#isChosen(option)),
300
327
  );
301
328
  (this.search ? this.#input : this.#listbox).focus({ preventScroll: true });
302
329
  this.#reveal(true);
@@ -420,8 +447,18 @@ export class GwOptionList extends GwElement {
420
447
  setAttribute(this.#input, "placeholder", this.placeholder);
421
448
  setAttribute(this.#input, "aria-label", this.label || this.placeholder);
422
449
  setAttribute(this.#listbox, "aria-label", this.label || null);
450
+ setAttribute(
451
+ this.#listbox,
452
+ "aria-multiselectable",
453
+ this.multiple ? "true" : null,
454
+ );
423
455
 
424
- if (changed.has("options") || changed.has("value")) {
456
+ if (
457
+ changed.has("options") ||
458
+ changed.has("value") ||
459
+ changed.has("values") ||
460
+ changed.has("multiple")
461
+ ) {
425
462
  this.#build();
426
463
  this.#render();
427
464
  }
@@ -559,7 +596,7 @@ export class GwOptionList extends GwElement {
559
596
  }
560
597
 
561
598
  const { option, index } = row;
562
- const selected = option.value === this.value;
599
+ const selected = this.#isChosen(option);
563
600
 
564
601
  const element = h(
565
602
  "div",
@@ -574,6 +611,7 @@ export class GwOptionList extends GwElement {
574
611
  "data-active": index === this.#active,
575
612
  },
576
613
  option.icon === undefined ? null : createIcon(option.icon),
614
+ option.swatch === undefined ? null : this.#swatch(option.swatch),
577
615
  h(
578
616
  "span",
579
617
  { class: "text" },
@@ -605,6 +643,14 @@ export class GwOptionList extends GwElement {
605
643
 
606
644
  return element;
607
645
  }
646
+
647
+ #swatch(color: string) {
648
+ const swatch = h("span", { class: "swatch", "aria-hidden": "true" });
649
+
650
+ swatch.style.background = color;
651
+
652
+ return swatch;
653
+ }
608
654
  }
609
655
 
610
656
  GwOptionList.define("gw-option-list");
package/src/gw-popover.ts CHANGED
@@ -38,7 +38,7 @@ const sheetBelow = 600;
38
38
  * @slot - The content.
39
39
  * @slot actions - Buttons; one with a `value` closes it with that value.
40
40
  *
41
- * @fires close - When it closes; `returnValue` holds the button's value.
41
+ * @fires {Event} close - When it closes; `returnValue` holds the button's value.
42
42
  */
43
43
  export class GwPopover extends GwElement {
44
44
  static override properties = {
@@ -34,7 +34,7 @@ export interface Segment {
34
34
  *
35
35
  * @csspart indicator - The pill that slides to the chosen option.
36
36
  *
37
- * @fires change - When the chosen option changes.
37
+ * @fires {Event} change - When the chosen option changes.
38
38
  */
39
39
  export class GwSegmented extends GwElement {
40
40
  static override properties = {