@substrate-system/actor-typeahead 0.0.3 → 0.0.4

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/README.md CHANGED
@@ -29,6 +29,7 @@ rewritten in TypeScript, and rendering in light DOM only. See
29
29
  * [ESM](#esm)
30
30
  * [Common JS](#common-js)
31
31
  * [Attributes](#attributes)
32
+ * [Properties](#properties)
32
33
  * [Events](#events)
33
34
  - [CSS](#css)
34
35
  * [Import CSS](#import-css)
@@ -106,6 +107,28 @@ it more than once is safe.
106
107
 
107
108
  Both are reflected as properties, so `el.rows = '3'` works too.
108
109
 
110
+ ### Properties
111
+
112
+ * `announce` -- A function `(count:number) => string` that returns the
113
+ text screen readers hear when a Typeahead request settles. `count` is
114
+ the number of Suggestions, and is 0 when nothing matched the Query.
115
+ The default is English, "No Suggestions", "1 Suggestion available" or
116
+ "N Suggestions available". Replace it to translate; your function
117
+ handles plurals and 0. It is read each time a request settles, so you
118
+ can set it at any time. There is no matching attribute.
119
+
120
+ ```ts
121
+ const el = document.querySelector('actor-typeahead')!
122
+ el.announce = count => {
123
+ if (count === 0) return 'Sin sugerencias'
124
+ return count === 1 ?
125
+ '1 sugerencia disponible' :
126
+ `${count} sugerencias disponibles`
127
+ }
128
+ ```
129
+
130
+ A request that fails is not announced.
131
+
109
132
  ### Events
110
133
 
111
134
  Events are namespaced by the tag name.
@@ -143,7 +166,7 @@ import '@substrate-system/actor-typeahead/min/css'
143
166
  ```css
144
167
  actor-typeahead {
145
168
  --color-background: #fff;
146
- --color-border: #0002;
169
+ --color-border: #0007;
147
170
  --color-shadow: #000;
148
171
  --color-hover: #0001;
149
172
  --color-avatar-fallback: #0002;
@@ -163,21 +186,27 @@ of it. The Suggestion list markup is:
163
186
  <input role="combobox" aria-autocomplete="list" aria-expanded="true"
164
187
  aria-controls="actor-typeahead-1-list"
165
188
  aria-activedescendant="actor-typeahead-1-option-0">
166
- <ul class="suggestion-list" id="actor-typeahead-1-list"
189
+ <div class="suggestion-list" id="actor-typeahead-1-list"
167
190
  role="listbox">
168
- <li class="suggestion" id="actor-typeahead-1-option-0"
169
- role="option" aria-selected="true">
191
+ <button type="button" class="suggestion"
192
+ id="actor-typeahead-1-option-0" role="option"
193
+ tabindex="-1" aria-selected="true">
170
194
  <div class="avatar"><img class="img" alt=""></div>
171
195
  <span class="handle"></span>
172
- </li>
173
- </ul>
196
+ </button>
197
+ </div>
198
+ <!-- visually hidden, announces how many Suggestions arrived -->
199
+ <div class="suggestion-count" role="status"></div>
174
200
  </actor-typeahead>
175
201
  ```
176
202
 
177
203
  `aria-selected="true"` marks the Suggestion highlighted with the
178
204
  keyboard. The input also gets `autocomplete="off"` unless you set
179
- `autocomplete` yourself. If the input has a `<label>` with an `id`, the
180
- Suggestion list is labelled by it.
205
+ `autocomplete` yourself. The Suggestion list gets the same name as the
206
+ input, from the input's `aria-labelledby`, its `aria-label`, or its
207
+ `<label>`s, in that order. Your labels do not need an `id`: when any
208
+ label lacks one, the list gets the label text as its `aria-label`. The
209
+ component never writes attributes on your labels.
181
210
 
182
211
  ### Avoid a flash of undefined content
183
212
 
@@ -64,6 +64,15 @@
64
64
  "text": "string|null"
65
65
  }
66
66
  },
67
+ {
68
+ "kind": "field",
69
+ "name": "announce",
70
+ "type": {
71
+ "text": "(count:number) => string"
72
+ },
73
+ "default": "defaultAnnounce",
74
+ "description": "Returns the text announced when a Typeahead request settles with\n`count` Suggestions, including 0 when nothing matched the Query.\nDefaults to English. Replace it to translate; the replacement\nhandles plurals and 0. Read at announce time, so it can be set\nafter connect."
75
+ },
67
76
  {
68
77
  "kind": "field",
69
78
  "name": "#id",
@@ -90,22 +99,31 @@
90
99
  },
91
100
  {
92
101
  "kind": "field",
93
- "name": "#pressed",
102
+ "name": "#suggestionList",
94
103
  "privacy": "private",
95
104
  "type": {
96
- "text": "boolean"
105
+ "text": "HTMLDivElement|null"
97
106
  },
98
- "default": "false"
107
+ "default": "null"
99
108
  },
100
109
  {
101
110
  "kind": "field",
102
- "name": "#suggestionList",
111
+ "name": "#suggestionCount",
103
112
  "privacy": "private",
104
113
  "type": {
105
- "text": "HTMLUListElement|null"
114
+ "text": "HTMLDivElement|null"
106
115
  },
107
116
  "default": "null"
108
117
  },
118
+ {
119
+ "kind": "field",
120
+ "name": "#nbsp",
121
+ "privacy": "private",
122
+ "type": {
123
+ "text": "boolean"
124
+ },
125
+ "default": "false"
126
+ },
109
127
  {
110
128
  "kind": "field",
111
129
  "name": "#timer",
@@ -183,14 +201,37 @@
183
201
  }
184
202
  ]
185
203
  },
204
+ {
205
+ "kind": "method",
206
+ "name": "#announce",
207
+ "privacy": "private",
208
+ "parameters": [
209
+ {
210
+ "name": "count",
211
+ "type": {
212
+ "text": "number"
213
+ }
214
+ }
215
+ ],
216
+ "description": "Write the Suggestion count. A trailing no-break space alternates\nbetween writes, so screen readers re-read an identical count."
217
+ },
186
218
  {
187
219
  "kind": "method",
188
220
  "name": "#onfocusout",
189
- "privacy": "private"
221
+ "privacy": "private",
222
+ "parameters": [
223
+ {
224
+ "name": "evt",
225
+ "type": {
226
+ "text": "FocusEvent"
227
+ }
228
+ }
229
+ ],
230
+ "description": "Close only when focus leaves the host. Some assistive technology\nfocuses a Suggestion before it sends the click that selects it."
190
231
  },
191
232
  {
192
233
  "kind": "method",
193
- "name": "#onpointerup",
234
+ "name": "#onclick",
194
235
  "privacy": "private",
195
236
  "parameters": [
196
237
  {
@@ -228,7 +269,7 @@
228
269
  {
229
270
  "kind": "method",
230
271
  "name": "render",
231
- "description": "Render into light DOM, next to the authored `<input>`. Only the\nSuggestion list is rebuilt, so the input element, and anything the\nconsumer attached to it, is never replaced. On the input, only the\nARIA combobox attributes are written, plus `autocomplete=\"off\"`\nwhen the page did not set `autocomplete`."
272
+ "description": "Render into light DOM. Only the suggestion list is rebuilt,\nso the input element, and anything the consumer attached to it,\nis never replaced. On the input, only the\nARIA combobox attributes are written, plus `autocomplete=\"off\"`\nwhen the page did not set `autocomplete`."
232
273
  },
233
274
  {
234
275
  "kind": "method",
package/dist/index.cjs CHANGED
@@ -33,11 +33,20 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
33
33
  __name(this, "ActorTypeahead");
34
34
  }
35
35
  static reflectedStringAttributes = ["host", "rows"];
36
+ /**
37
+ * Returns the text announced when a Typeahead request settles with
38
+ * `count` Suggestions, including 0 when nothing matched the Query.
39
+ * Defaults to English. Replace it to translate; the replacement
40
+ * handles plurals and 0. Read at announce time, so it can be set
41
+ * after connect.
42
+ */
43
+ announce = defaultAnnounce;
36
44
  #id = `actor-typeahead-${++instances}`;
37
45
  #actors = [];
38
46
  #index = -1;
39
- #pressed = false;
40
47
  #suggestionList = null;
48
+ #suggestionCount = null;
49
+ #nbsp = false;
41
50
  #timer;
42
51
  #controller = null;
43
52
  constructor() {
@@ -66,18 +75,23 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
66
75
  if (evt instanceof KeyboardEvent) this.#onkeydown(evt);
67
76
  break;
68
77
  case "focusout":
69
- this.#onfocusout();
78
+ if (evt instanceof FocusEvent) this.#onfocusout(evt);
70
79
  break;
71
- case "pointerdown":
72
- this.#pressed = true;
80
+ case "mousedown":
81
+ evt.preventDefault();
73
82
  break;
74
- case "pointerup":
75
- this.#onpointerup(evt);
83
+ case "click":
84
+ this.#onclick(evt);
76
85
  break;
77
86
  }
78
87
  }
79
88
  #onkeydown(evt) {
80
89
  const last = this.#actors.length - 1;
90
+ const moves = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"];
91
+ if (last < 0 && moves.includes(evt.key)) {
92
+ evt.preventDefault();
93
+ return;
94
+ }
81
95
  switch (evt.key) {
82
96
  case "ArrowDown":
83
97
  evt.preventDefault();
@@ -91,7 +105,7 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
91
105
  break;
92
106
  case "ArrowUp":
93
107
  evt.preventDefault();
94
- this.#index = Math.max(this.#index - 1, 0);
108
+ this.#index = this.#index < 0 ? last : this.#index - 1;
95
109
  this.render();
96
110
  break;
97
111
  case "PageUp":
@@ -149,18 +163,29 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
149
163
  this.#actors = parseActors(json);
150
164
  this.#index = -1;
151
165
  this.render();
166
+ this.#announce(this.#actors.length);
152
167
  }
153
- #onfocusout() {
154
- if (this.#pressed) return;
168
+ /**
169
+ * Write the Suggestion count. A trailing no-break space alternates
170
+ * between writes, so screen readers re-read an identical count.
171
+ */
172
+ #announce(count) {
173
+ if (!this.#suggestionCount) return;
174
+ this.#nbsp = !this.#nbsp;
175
+ this.#suggestionCount.textContent = this.announce(count) + (this.#nbsp ? "\xA0" : "");
176
+ }
177
+ /**
178
+ * Close only when focus leaves the host. Some assistive technology
179
+ * focuses a Suggestion before it sends the click that selects it.
180
+ */
181
+ #onfocusout(evt) {
182
+ const next = evt.relatedTarget;
183
+ if (next instanceof Node && this.contains(next)) return;
155
184
  this.#clear();
156
185
  }
157
- #onpointerup(evt) {
158
- this.#pressed = false;
186
+ #onclick(evt) {
159
187
  const option = evt.target instanceof Element ? evt.target.closest('[role="option"]') : null;
160
- if (!option) {
161
- this.qs("input")?.focus();
162
- return;
163
- }
188
+ if (!option) return;
164
189
  this.#select(Number(option.dataset.index));
165
190
  }
166
191
  #select(index) {
@@ -189,24 +214,32 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
189
214
  this.render();
190
215
  }
191
216
  /**
192
- * Render into light DOM, next to the authored `<input>`. Only the
193
- * Suggestion list is rebuilt, so the input element, and anything the
194
- * consumer attached to it, is never replaced. On the input, only the
217
+ * Render into light DOM. Only the suggestion list is rebuilt,
218
+ * so the input element, and anything the consumer attached to it,
219
+ * is never replaced. On the input, only the
195
220
  * ARIA combobox attributes are written, plus `autocomplete="off"`
196
221
  * when the page did not set `autocomplete`.
197
222
  */
198
223
  render() {
199
224
  if (!this.#suggestionList) {
200
- this.#suggestionList = document.createElement("ul");
225
+ this.#suggestionList = document.createElement("div");
201
226
  this.#suggestionList.className = "suggestion-list";
202
227
  this.#suggestionList.id = `${this.#id}-list`;
203
228
  this.#suggestionList.setAttribute("role", "listbox");
204
229
  if (ANCHORED) this.#suggestionList.popover = "manual";
205
- this.#suggestionList.addEventListener("pointerdown", this);
206
- this.#suggestionList.addEventListener("pointerup", this);
230
+ this.#suggestionList.addEventListener("mousedown", this);
231
+ this.#suggestionList.addEventListener("click", this);
207
232
  }
208
233
  const list = this.#suggestionList;
209
234
  if (list.parentNode !== this) this.append(list);
235
+ if (!this.#suggestionCount) {
236
+ this.#suggestionCount = document.createElement("div");
237
+ this.#suggestionCount.className = "suggestion-count";
238
+ this.#suggestionCount.setAttribute("role", "status");
239
+ }
240
+ const count = this.#suggestionCount;
241
+ if (count.parentNode !== this) this.append(count);
242
+ if (!this.#actors.length) count.textContent = "";
210
243
  list.replaceChildren(...this.#actors.map((actor, i) => {
211
244
  const option = suggestion(actor, i, i === this.#index);
212
245
  option.id = this.#optionId(i);
@@ -222,11 +255,11 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
222
255
  }
223
256
  input.setAttribute("aria-controls", list.id);
224
257
  input.setAttribute("aria-expanded", String(open));
225
- const labels = [...input.labels ?? []].map((label) => label.id).filter(Boolean);
226
- if (labels.length) {
227
- list.setAttribute("aria-labelledby", labels.join(" "));
228
- } else {
229
- list.removeAttribute("aria-labelledby");
258
+ const name = nameSource(input, this);
259
+ for (const attr of ["aria-labelledby", "aria-label"]) {
260
+ const value = name[attr];
261
+ if (value) list.setAttribute(attr, value);
262
+ else list.removeAttribute(attr);
230
263
  }
231
264
  if (this.#actors[this.#index]) {
232
265
  input.setAttribute(
@@ -258,12 +291,14 @@ class ActorTypeahead extends import_web_component.WebComponent.create("actor-typ
258
291
  }
259
292
  ActorTypeahead.define();
260
293
  function suggestion(actor, index, active) {
261
- const li = document.createElement("li");
262
- li.className = "suggestion";
263
- li.setAttribute("role", "option");
264
- li.setAttribute("aria-selected", String(active));
265
- li.dataset.index = String(index);
266
- li.dataset.handle = actor.handle;
294
+ const button = document.createElement("button");
295
+ button.type = "button";
296
+ button.tabIndex = -1;
297
+ button.className = "suggestion";
298
+ button.setAttribute("role", "option");
299
+ button.setAttribute("aria-selected", String(active));
300
+ button.dataset.index = String(index);
301
+ button.dataset.handle = actor.handle;
267
302
  const avatar = document.createElement("div");
268
303
  avatar.className = "avatar";
269
304
  const img = document.createElement("img");
@@ -274,10 +309,40 @@ function suggestion(actor, index, active) {
274
309
  handle.className = "handle";
275
310
  handle.textContent = actor.handle;
276
311
  avatar.append(img);
277
- li.append(avatar, handle);
278
- return li;
312
+ button.append(avatar, handle);
313
+ return button;
279
314
  }
280
315
  __name(suggestion, "suggestion");
316
+ function nameSource(input, host) {
317
+ const labelledby = input.getAttribute("aria-labelledby");
318
+ if (labelledby) return { "aria-labelledby": labelledby };
319
+ const label = input.getAttribute("aria-label");
320
+ if (label) return { "aria-label": label };
321
+ const labels = [...input.labels ?? []];
322
+ if (!labels.length) return {};
323
+ if (labels.every((label2) => label2.id)) {
324
+ return {
325
+ "aria-labelledby": labels.map((label2) => label2.id).join(" ")
326
+ };
327
+ }
328
+ const text = labels.map((label2) => labelText(label2, host).trim()).filter(Boolean).join(" ");
329
+ return text ? { "aria-label": text } : {};
330
+ }
331
+ __name(nameSource, "nameSource");
332
+ function labelText(label, host) {
333
+ const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {
334
+ acceptNode: /* @__PURE__ */ __name((node) => host.contains(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, "acceptNode")
335
+ });
336
+ let text = "";
337
+ while (walker.nextNode()) text += walker.currentNode.nodeValue;
338
+ return text;
339
+ }
340
+ __name(labelText, "labelText");
341
+ function defaultAnnounce(count) {
342
+ if (count === 0) return "No Suggestions";
343
+ return count === 1 ? "1 Suggestion available" : `${count} Suggestions available`;
344
+ }
345
+ __name(defaultAnnounce, "defaultAnnounce");
281
346
  function parseActors(json) {
282
347
  if (!isRecord(json) || !Array.isArray(json.actors)) return [];
283
348
  return json.actors.flatMap((actor) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// how long the Query must stay unchanged before a Typeahead request\nconst DEBOUNCE_MS = 150\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #pressed = false\n #suggestionList:HTMLUListElement|null = null\n #timer:ReturnType<typeof setTimeout>|undefined\n #controller:AbortController|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n /**\n * Stop any pending Typeahead request. The Suggestion list is left as\n * is; the next Query rebuilds it.\n */\n disconnectedCallback () {\n this.#cancel()\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n this.#onfocusout()\n break\n\n case 'pointerdown':\n this.#pressed = true\n break\n\n case 'pointerup':\n this.#onpointerup(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n this.#index = Math.max(this.#index - 1, 0)\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n // with the list closed, let a dialog close; still cancel\n // any pending Typeahead request\n if (this.#actors.length) evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n // with nothing Highlighted, let the form submit, and\n // drop any pending Typeahead request\n if (!this.#actors[this.#index]) {\n this.#cancel()\n break\n }\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n // keep the current Suggestions on screen until the new response\n this.#cancel()\n this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS)\n }\n\n async #request (query:string) {\n this.#timer = undefined\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const controller = new AbortController()\n this.#controller = controller\n const { signal } = controller\n\n let json:unknown\n try {\n const res = await fetch(url, { signal })\n if (signal.aborted) return\n if (!res.ok) throw new Error(`status ${res.status}`)\n json = await res.json()\n } catch (err) {\n // a newer Query, or a close, cancelled this request\n if (isAbortError(err)) return\n // any other failure closes the Suggestion list, quietly\n this.#clear()\n return\n }\n if (signal.aborted) return\n\n this.#actors = parseActors(json)\n this.#index = -1\n this.render()\n }\n\n #onfocusout () {\n if (this.#pressed) return\n this.#clear()\n }\n\n #onpointerup (evt:Event) {\n this.#pressed = false\n\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n\n if (!option) {\n this.qs('input')?.focus()\n return\n }\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n /**\n * Stop any pending Typeahead request: the debounce timer, and the\n * request in flight, whose response is then dropped.\n */\n #cancel () {\n clearTimeout(this.#timer)\n this.#timer = undefined\n this.#controller?.abort()\n this.#controller = null\n }\n\n #clear () {\n this.#cancel()\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM, next to the authored `<input>`. Only the\n * Suggestion list is rebuilt, so the input element, and anything the\n * consumer attached to it, is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('ul')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('pointerdown', this)\n this.#suggestionList.addEventListener('pointerup', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n const labels = [...(input.labels ?? [])]\n .map(label => label.id)\n .filter(Boolean)\n if (labels.length) {\n list.setAttribute('aria-labelledby', labels.join(' '))\n } else {\n list.removeAttribute('aria-labelledby')\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLLIElement {\n const li = document.createElement('li')\n li.className = 'suggestion'\n li.setAttribute('role', 'option')\n li.setAttribute('aria-selected', String(active))\n li.dataset.index = String(index)\n li.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n li.append(avatar, handle)\n\n return li\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isAbortError (err:unknown):boolean {\n return err instanceof DOMException && err.name === 'AbortError'\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright (c) 2026 Jake Lazaroff\n *\n * Repository: https://tangled.org/jakelazaroff.com/actor-typeahead\n */\n\nimport { WebComponent } from '@substrate-system/web-component'\n\n// for document.querySelector\ndeclare global {\n interface HTMLElementTagNameMap {\n 'actor-typeahead':ActorTypeahead\n }\n}\n\nexport interface Actor {\n handle:string;\n avatar?:string;\n}\n\nconst DEFAULT_HOST = 'https://public.api.bsky.app'\nconst DEFAULT_ROWS = 5\n\n// how long the Query must stay unchanged before a Typeahead request\nconst DEBOUNCE_MS = 150\n\n// The Suggestion list is a top-layer popover, positioned against the host\n// with CSS anchor positioning. `anchor-scope` shipped last of that set, so\n// it gates the group. Without it, the old absolute layout is used.\nconst ANCHORED = typeof CSS !== 'undefined' &&\n CSS.supports('anchor-scope: --a')\n\n// counts instances, so each one gets unique element ids\nlet instances = 0\n\n/**\n * @attribute {string} [host] - The host to which to make the typeahead\n * API call.\n * @attribute {number} [rows] - The maximum number of Suggestions to\n * display in the Suggestion list.\n *\n * @fires actor-typeahead:select - A Suggestion was chosen from the\n * Suggestion list. The event `detail` is `{ handle, avatar }`.\n *\n * @cssprop --color-background - Controls the color of the Suggestion list\n * background.\n * @cssprop --color-border - Controls the color of the Suggestion list\n * border.\n * @cssprop --color-shadow - Controls the color of the Suggestion list\n * shadow.\n * @cssprop --color-hover - Controls the background color of each\n * Suggestion on hover.\n * @cssprop --color-avatar-fallback - Controls the background color of an\n * avatar circle if the image fails to load.\n * @cssprop --radius - Controls the corner radius of the Suggestion list.\n * @cssprop --padding-suggestion-list - Controls the padding of the\n * Suggestion list.\n *\n * @summary A small web component that progressively enhances an <input>\n * element into an autocomplete for ATProto handles.\n *\n * @tag actor-typeahead\n */\nexport class ActorTypeahead extends WebComponent.create('actor-typeahead') {\n static reflectedStringAttributes = ['host', 'rows']\n\n declare host:string|null\n declare rows:string|null\n\n /**\n * Returns the text announced when a Typeahead request settles with\n * `count` Suggestions, including 0 when nothing matched the Query.\n * Defaults to English. Replace it to translate; the replacement\n * handles plurals and 0. Read at announce time, so it can be set\n * after connect.\n */\n announce:(count:number) => string = defaultAnnounce\n\n #id = `actor-typeahead-${++instances}`\n #actors:Actor[] = []\n #index = -1\n #suggestionList:HTMLDivElement|null = null\n #suggestionCount:HTMLDivElement|null = null\n #nbsp = false\n #timer:ReturnType<typeof setTimeout>|undefined\n #controller:AbortController|null = null\n\n constructor () {\n super()\n this.addEventListener('input', this)\n this.addEventListener('focusout', this)\n this.addEventListener('keydown', this)\n }\n\n /**\n * Stop any pending Typeahead request. The Suggestion list is left as\n * is; the next Query rebuilds it.\n */\n disconnectedCallback () {\n this.#cancel()\n }\n\n get #limit ():number {\n const rows = Number.parseInt(this.rows ?? '')\n return Number.isNaN(rows) ? DEFAULT_ROWS : rows\n }\n\n handleEvent (evt:Event) {\n switch (evt.type) {\n case 'input':\n this.#oninput(evt)\n break\n\n case 'keydown':\n if (evt instanceof KeyboardEvent) this.#onkeydown(evt)\n break\n\n case 'focusout':\n if (evt instanceof FocusEvent) this.#onfocusout(evt)\n break\n\n case 'mousedown':\n // a press or a tap keeps focus on the Authored input\n evt.preventDefault()\n break\n\n case 'click':\n this.#onclick(evt)\n break\n }\n }\n\n #onkeydown (evt:KeyboardEvent) {\n const last = this.#actors.length - 1\n const moves = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp']\n // with no Suggestions there is nothing to move to, and a render\n // would empty a \"No Suggestions\" announcement\n if (last < 0 && moves.includes(evt.key)) {\n evt.preventDefault()\n return\n }\n\n switch (evt.key) {\n case 'ArrowDown':\n evt.preventDefault()\n this.#index = Math.min(this.#index + 1, last)\n this.render()\n break\n\n case 'PageDown':\n evt.preventDefault()\n this.#index = last\n this.render()\n break\n\n case 'ArrowUp':\n evt.preventDefault()\n // wrap from nothing Highlighted to the last Suggestion,\n // and from the first back to nothing (the APG pattern)\n this.#index = this.#index < 0 ? last : this.#index - 1\n this.render()\n break\n\n case 'PageUp':\n evt.preventDefault()\n this.#index = 0\n this.render()\n break\n\n case 'Escape':\n // with the list closed, let a dialog close; still cancel\n // any pending Typeahead request\n if (this.#actors.length) evt.preventDefault()\n this.#clear()\n break\n\n case 'Enter':\n // with nothing Highlighted, let the form submit, and\n // drop any pending Typeahead request\n if (!this.#actors[this.#index]) {\n this.#cancel()\n break\n }\n evt.preventDefault()\n this.#select(this.#index)\n break\n }\n }\n\n #oninput (evt:Event) {\n if (!(evt.target instanceof HTMLInputElement)) return\n\n const query = evt.target.value\n if (!query) {\n this.#clear()\n return\n }\n\n // keep the current Suggestions on screen until the new response\n this.#cancel()\n this.#timer = setTimeout(() => this.#request(query), DEBOUNCE_MS)\n }\n\n async #request (query:string) {\n this.#timer = undefined\n const url = new URL(\n 'xrpc/app.bsky.actor.searchActorsTypeahead',\n this.host ?? DEFAULT_HOST\n )\n url.searchParams.set('q', query)\n url.searchParams.set('limit', String(this.#limit))\n\n const controller = new AbortController()\n this.#controller = controller\n const { signal } = controller\n\n let json:unknown\n try {\n const res = await fetch(url, { signal })\n if (signal.aborted) return\n if (!res.ok) throw new Error(`status ${res.status}`)\n json = await res.json()\n } catch (err) {\n // a newer Query, or a close, cancelled this request\n if (isAbortError(err)) return\n // any other failure closes the Suggestion list, quietly\n this.#clear()\n return\n }\n if (signal.aborted) return\n\n this.#actors = parseActors(json)\n this.#index = -1\n // after render(), which empties the count when there are none\n this.render()\n this.#announce(this.#actors.length)\n }\n\n /**\n * Write the Suggestion count. A trailing no-break space alternates\n * between writes, so screen readers re-read an identical count.\n */\n #announce (count:number) {\n if (!this.#suggestionCount) return\n this.#nbsp = !this.#nbsp\n this.#suggestionCount.textContent = this.announce(count) +\n (this.#nbsp ? '\\u00a0' : '')\n }\n\n /**\n * Close only when focus leaves the host. Some assistive technology\n * focuses a Suggestion before it sends the click that selects it.\n */\n #onfocusout (evt:FocusEvent) {\n const next = evt.relatedTarget\n if (next instanceof Node && this.contains(next)) return\n this.#clear()\n }\n\n #onclick (evt:Event) {\n const option = evt.target instanceof Element ?\n evt.target.closest<HTMLElement>('[role=\"option\"]') :\n null\n if (!option) return\n\n this.#select(Number(option.dataset.index))\n }\n\n #select (index:number) {\n const actor = this.#actors[index]\n const input = this.qs('input')\n if (!actor || !input) return\n\n input.value = actor.handle\n input.focus()\n this.#clear()\n this.emit<Actor>('select', { detail: actor })\n }\n\n /**\n * Stop any pending Typeahead request: the debounce timer, and the\n * request in flight, whose response is then dropped.\n */\n #cancel () {\n clearTimeout(this.#timer)\n this.#timer = undefined\n this.#controller?.abort()\n this.#controller = null\n }\n\n #clear () {\n this.#cancel()\n this.#actors = []\n this.#index = -1\n this.render()\n }\n\n /**\n * Render into light DOM. Only the suggestion list is rebuilt,\n * so the input element, and anything the consumer attached to it,\n * is never replaced. On the input, only the\n * ARIA combobox attributes are written, plus `autocomplete=\"off\"`\n * when the page did not set `autocomplete`.\n */\n render () {\n if (!this.#suggestionList) {\n this.#suggestionList = document.createElement('div')\n this.#suggestionList.className = 'suggestion-list'\n this.#suggestionList.id = `${this.#id}-list`\n this.#suggestionList.setAttribute('role', 'listbox')\n if (ANCHORED) this.#suggestionList.popover = 'manual'\n this.#suggestionList.addEventListener('mousedown', this)\n this.#suggestionList.addEventListener('click', this)\n }\n\n const list = this.#suggestionList\n if (list.parentNode !== this) this.append(list)\n\n // the live region exists empty before its first write, because\n // screen readers often skip a region inserted already filled\n if (!this.#suggestionCount) {\n this.#suggestionCount = document.createElement('div')\n this.#suggestionCount.className = 'suggestion-count'\n this.#suggestionCount.setAttribute('role', 'status')\n }\n const count = this.#suggestionCount\n if (count.parentNode !== this) this.append(count)\n // only a response writes the count; any close empties it, and a\n // response with no Suggestions writes it after this render\n if (!this.#actors.length) count.textContent = ''\n\n list.replaceChildren(...this.#actors.map((actor, i) => {\n const option = suggestion(actor, i, i === this.#index)\n option.id = this.#optionId(i)\n return option\n }))\n\n const open = this.#toggle(list, this.#actors.length > 0)\n\n const input = this.qs('input')\n if (input) {\n input.setAttribute('role', 'combobox')\n input.setAttribute('aria-autocomplete', 'list')\n if (!input.hasAttribute('autocomplete')) {\n input.setAttribute('autocomplete', 'off')\n }\n input.setAttribute('aria-controls', list.id)\n input.setAttribute('aria-expanded', String(open))\n\n // the Suggestion list carries at most one of the two\n const name = nameSource(input, this)\n for (const attr of ['aria-labelledby', 'aria-label'] as const) {\n const value = name[attr]\n if (value) list.setAttribute(attr, value)\n else list.removeAttribute(attr)\n }\n\n if (this.#actors[this.#index]) {\n input.setAttribute(\n 'aria-activedescendant',\n this.#optionId(this.#index)\n )\n } else {\n input.removeAttribute('aria-activedescendant')\n }\n }\n }\n\n /**\n * Show or hide the Suggestion list popover, guarded on its current\n * state so repeated renders neither throw nor toggle. A disconnected\n * popover cannot be shown, e.g. when a response arrives after the host\n * was removed. Returns whether the Suggestion list is open.\n */\n #toggle (list:HTMLElement, open:boolean):boolean {\n if (!ANCHORED) return open\n if (!list.isConnected) return false\n\n const isOpen = list.matches(':popover-open')\n if (open && !isOpen) list.showPopover()\n if (!open && isOpen) list.hidePopover()\n return list.matches(':popover-open')\n }\n\n #optionId (index:number):string {\n return `${this.#id}-option-${index}`\n }\n}\n\nActorTypeahead.define()\n\n/**\n * Build one Suggestion. Values from the API go through `textContent` and\n * properties, never through `innerHTML`.\n */\nfunction suggestion (\n actor:Actor,\n index:number,\n active:boolean\n):HTMLButtonElement {\n const button = document.createElement('button')\n // a button, so every input method can activate it, but not a\n // submit button, and out of the tab order, because focus stays on\n // the Authored input\n button.type = 'button'\n button.tabIndex = -1\n button.className = 'suggestion'\n button.setAttribute('role', 'option')\n button.setAttribute('aria-selected', String(active))\n button.dataset.index = String(index)\n button.dataset.handle = actor.handle\n\n const avatar = document.createElement('div')\n avatar.className = 'avatar'\n\n const img = document.createElement('img')\n img.className = 'img'\n img.alt = ''\n if (actor.avatar) img.src = actor.avatar\n\n const handle = document.createElement('span')\n handle.className = 'handle'\n handle.textContent = actor.handle\n\n avatar.append(img)\n button.append(avatar, handle)\n\n return button\n}\n\n/**\n * What names the Authored input, in the order of accessible name\n * computation, as the attribute the Suggestion list copies it to.\n */\nfunction nameSource (input:HTMLInputElement, host:HTMLElement):{\n 'aria-labelledby'?:string;\n 'aria-label'?:string;\n} {\n const labelledby = input.getAttribute('aria-labelledby')\n if (labelledby) return { 'aria-labelledby': labelledby }\n\n const label = input.getAttribute('aria-label')\n if (label) return { 'aria-label': label }\n\n // reference the labels when they all have ids, otherwise copy their\n // text, because the component never writes ids onto the page\n const labels = [...(input.labels ?? [])]\n if (!labels.length) return {}\n if (labels.every(label => label.id)) {\n return {\n 'aria-labelledby': labels.map(label => label.id).join(' ')\n }\n }\n\n const text = labels\n .map(label => labelText(label, host).trim())\n .filter(Boolean)\n .join(' ')\n return text ? { 'aria-label': text } : {}\n}\n\n/**\n * The text of a label, without the Host element's content, which a\n * label that wraps the Host element would otherwise include.\n */\nfunction labelText (label:HTMLLabelElement, host:HTMLElement):string {\n const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, {\n acceptNode: node => host.contains(node) ?\n NodeFilter.FILTER_REJECT :\n NodeFilter.FILTER_ACCEPT\n })\n\n let text = ''\n while (walker.nextNode()) text += walker.currentNode.nodeValue\n return text\n}\n\n/**\n * The default English Suggestion count announcement.\n */\nfunction defaultAnnounce (count:number):string {\n if (count === 0) return 'No Suggestions'\n return count === 1 ?\n '1 Suggestion available' :\n `${count} Suggestions available`\n}\n\nfunction parseActors (json:unknown):Actor[] {\n if (!isRecord(json) || !Array.isArray(json.actors)) return []\n\n return json.actors.flatMap((actor:unknown) => {\n if (!isRecord(actor) || typeof actor.handle !== 'string') return []\n\n return [typeof actor.avatar === 'string' ?\n { handle: actor.handle, avatar: actor.avatar } :\n { handle: actor.handle }]\n })\n}\n\nfunction isAbortError (err:unknown):boolean {\n return err instanceof DOMException && err.name === 'AbortError'\n}\n\nfunction isRecord (value:unknown):value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["label"]
7
7
  }
package/dist/index.css CHANGED
@@ -1,6 +1,6 @@
1
1
  actor-typeahead {
2
2
  --color-background-inherited: var(--color-background, #fff);
3
- --color-border-inherited: var(--color-border, #0002);
3
+ --color-border-inherited: var(--color-border, #0007);
4
4
  --color-shadow-inherited: var(--color-shadow, #000);
5
5
  --color-hover-inherited: var(--color-hover, #0001);
6
6
  --color-avatar-fallback-inherited: var(--color-avatar-fallback, #0002);
@@ -29,7 +29,6 @@ actor-typeahead {
29
29
  background-clip: padding-box;
30
30
  flex-direction: column;
31
31
  margin-top: 4px;
32
- list-style: none;
33
32
  display: flex;
34
33
  position: absolute;
35
34
  left: 0;
@@ -57,6 +56,15 @@ actor-typeahead {
57
56
  }
58
57
  }
59
58
 
59
+ & .suggestion-count {
60
+ clip-path: inset(50%);
61
+ white-space: nowrap;
62
+ width: 1px;
63
+ height: 1px;
64
+ position: absolute;
65
+ overflow: hidden;
66
+ }
67
+
60
68
  & .suggestion {
61
69
  all: unset;
62
70
  box-sizing: border-box;
@@ -73,6 +81,19 @@ actor-typeahead {
73
81
  &:hover, &[aria-selected="true"] {
74
82
  background-color: var(--color-hover-inherited);
75
83
  }
84
+
85
+ &[aria-selected="true"] {
86
+ outline-offset: -2px;
87
+ outline: 2px solid;
88
+ }
89
+
90
+ @media (forced-colors: active) {
91
+ &[aria-selected="true"] {
92
+ forced-color-adjust: none;
93
+ color: highlighttext;
94
+ background-color: highlight;
95
+ }
96
+ }
76
97
  }
77
98
 
78
99
  & .avatar {
package/dist/index.d.ts CHANGED
@@ -49,6 +49,14 @@ export declare class ActorTypeahead extends ActorTypeahead_base {
49
49
  static reflectedStringAttributes: string[];
50
50
  host: string | null;
51
51
  rows: string | null;
52
+ /**
53
+ * Returns the text announced when a Typeahead request settles with
54
+ * `count` Suggestions, including 0 when nothing matched the Query.
55
+ * Defaults to English. Replace it to translate; the replacement
56
+ * handles plurals and 0. Read at announce time, so it can be set
57
+ * after connect.
58
+ */
59
+ announce: (count: number) => string;
52
60
  constructor();
53
61
  /**
54
62
  * Stop any pending Typeahead request. The Suggestion list is left as
@@ -57,9 +65,9 @@ export declare class ActorTypeahead extends ActorTypeahead_base {
57
65
  disconnectedCallback(): void;
58
66
  handleEvent(evt: Event): void;
59
67
  /**
60
- * Render into light DOM, next to the authored `<input>`. Only the
61
- * Suggestion list is rebuilt, so the input element, and anything the
62
- * consumer attached to it, is never replaced. On the input, only the
68
+ * Render into light DOM. Only the suggestion list is rebuilt,
69
+ * so the input element, and anything the consumer attached to it,
70
+ * is never replaced. On the input, only the
63
71
  * ARIA combobox attributes are written, plus `autocomplete="off"`
64
72
  * when the page did not set `autocomplete`.
65
73
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAG9D,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAC,cAAc,CAAA;KACnC;CACJ;AAED,MAAM,WAAW,KAAK;IAClB,MAAM,EAAC,MAAM,CAAC;IACd,MAAM,CAAC,EAAC,MAAM,CAAC;CAClB;;;;;;;;;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,cAAe,SAAQ,mBAAsC;;IACtE,MAAM,CAAC,yBAAyB,WAAmB;IAE3C,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAChB,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;;IAiBxB;;;OAGG;IACH,oBAAoB;IASpB,WAAW,CAAE,GAAG,EAAC,KAAK;IAwKtB;;;;;;OAMG;IACH,MAAM;CAuET"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAG9D,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,iBAAiB,EAAC,cAAc,CAAA;KACnC;CACJ;AAED,MAAM,WAAW,KAAK;IAClB,MAAM,EAAC,MAAM,CAAC;IACd,MAAM,CAAC,EAAC,MAAM,CAAC;CAClB;;;;;;;;;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,cAAe,SAAQ,mBAAsC;;IACtE,MAAM,CAAC,yBAAyB,WAAmB;IAE3C,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAChB,IAAI,EAAC,MAAM,GAAC,IAAI,CAAA;IAExB;;;;;;OAMG;IACH,QAAQ,EAAC,CAAC,KAAK,EAAC,MAAM,KAAK,MAAM,CAAkB;;IAkBnD;;;OAGG;IACH,oBAAoB;IASpB,WAAW,CAAE,GAAG,EAAC,KAAK;IA8LtB;;;;;;OAMG;IACH,MAAM;CAmFT"}