poetry-core 0.0.2

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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/DESIGN.md +145 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +30 -0
  6. data/THIRD_PARTY_NOTICES.md +507 -0
  7. data/app/components/poetry/core/box/component.rb +82 -0
  8. data/app/components/poetry/core/component.rb +390 -0
  9. data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
  10. data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
  11. data/app/components/poetry/core/concerns/introspection.rb +533 -0
  12. data/app/components/poetry/core/concerns/options.rb +262 -0
  13. data/app/components/poetry/core/concerns/parts.rb +148 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +363 -0
  15. data/app/components/poetry/core/concerns/styles.rb +323 -0
  16. data/app/components/poetry/core/style.rb +150 -0
  17. data/app/components/poetry/core/wrapper/component.rb +90 -0
  18. data/app/javascript/poetry/core/accordion_controller.js +165 -0
  19. data/app/javascript/poetry/core/action_bar_controller.js +109 -0
  20. data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
  21. data/app/javascript/poetry/core/calendar_controller.js +406 -0
  22. data/app/javascript/poetry/core/carousel_controller.js +167 -0
  23. data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
  24. data/app/javascript/poetry/core/checked_controller.js +208 -0
  25. data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
  26. data/app/javascript/poetry/core/combobox_controller.js +1095 -0
  27. data/app/javascript/poetry/core/command_controller.js +510 -0
  28. data/app/javascript/poetry/core/context_menu_controller.js +204 -0
  29. data/app/javascript/poetry/core/date_field_controller.js +584 -0
  30. data/app/javascript/poetry/core/date_picker_controller.js +111 -0
  31. data/app/javascript/poetry/core/deferred_controller.js +111 -0
  32. data/app/javascript/poetry/core/dialog_controller.js +206 -0
  33. data/app/javascript/poetry/core/dismissable_controller.js +182 -0
  34. data/app/javascript/poetry/core/drawer_controller.js +365 -0
  35. data/app/javascript/poetry/core/file_input_controller.js +191 -0
  36. data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
  37. data/app/javascript/poetry/core/helpers/announce.js +228 -0
  38. data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
  39. data/app/javascript/poetry/core/helpers/collection.js +18 -0
  40. data/app/javascript/poetry/core/helpers/direction.js +15 -0
  41. data/app/javascript/poetry/core/helpers/escape.js +38 -0
  42. data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
  43. data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
  44. data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
  45. data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
  46. data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
  47. data/app/javascript/poetry/core/helpers/mask.js +283 -0
  48. data/app/javascript/poetry/core/helpers/portal.js +217 -0
  49. data/app/javascript/poetry/core/helpers/presence.js +210 -0
  50. data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
  51. data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
  52. data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
  53. data/app/javascript/poetry/core/helpers/state.js +80 -0
  54. data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
  55. data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
  56. data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
  57. data/app/javascript/poetry/core/hotkey_controller.js +55 -0
  58. data/app/javascript/poetry/core/hover_card_controller.js +431 -0
  59. data/app/javascript/poetry/core/index.js +218 -0
  60. data/app/javascript/poetry/core/mask_controller.js +500 -0
  61. data/app/javascript/poetry/core/menu_controller.js +1050 -0
  62. data/app/javascript/poetry/core/menubar_controller.js +329 -0
  63. data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
  64. data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
  65. data/app/javascript/poetry/core/number_field_controller.js +404 -0
  66. data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
  67. data/app/javascript/poetry/core/otp_controller.js +219 -0
  68. data/app/javascript/poetry/core/popover_controller.js +300 -0
  69. data/app/javascript/poetry/core/popper_controller.js +439 -0
  70. data/app/javascript/poetry/core/pressed_controller.js +74 -0
  71. data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
  72. data/app/javascript/poetry/core/radio_group_controller.js +197 -0
  73. data/app/javascript/poetry/core/resizable_controller.js +202 -0
  74. data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
  75. data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
  76. data/app/javascript/poetry/core/search_field_controller.js +95 -0
  77. data/app/javascript/poetry/core/select_controller.js +924 -0
  78. data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
  79. data/app/javascript/poetry/core/sheet_controller.js +24 -0
  80. data/app/javascript/poetry/core/sidebar_controller.js +272 -0
  81. data/app/javascript/poetry/core/slider_controller.js +419 -0
  82. data/app/javascript/poetry/core/state_controller.js +82 -0
  83. data/app/javascript/poetry/core/table_selection_controller.js +153 -0
  84. data/app/javascript/poetry/core/tabs_controller.js +151 -0
  85. data/app/javascript/poetry/core/tag_group_controller.js +134 -0
  86. data/app/javascript/poetry/core/toast_controller.js +228 -0
  87. data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
  88. data/app/javascript/poetry/core/toaster_controller.js +197 -0
  89. data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
  90. data/app/javascript/poetry/core/tooltip_controller.js +541 -0
  91. data/app/javascript/poetry/core/tree_controller.js +265 -0
  92. data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
  93. data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
  94. data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
  95. data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
  96. data/app/views/poetry/core/preview.html.erb +13 -0
  97. data/config/component_registry.yml +20 -0
  98. data/config/controllers_manifest.json +1528 -0
  99. data/config/importmap.rb +11 -0
  100. data/config/state_vocabulary.json +31 -0
  101. data/lib/active_model/type/list.rb +33 -0
  102. data/lib/active_model/type/symbol.rb +36 -0
  103. data/lib/poetry/core/check/stable_identity.rb +104 -0
  104. data/lib/poetry/core/check.rb +1445 -0
  105. data/lib/poetry/core/config.rb +286 -0
  106. data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
  107. data/lib/poetry/core/css/bem_merger.rb +45 -0
  108. data/lib/poetry/core/css/bem_reference.rb +87 -0
  109. data/lib/poetry/core/css/override_scan.rb +110 -0
  110. data/lib/poetry/core/css/resolver.rb +161 -0
  111. data/lib/poetry/core/css/safelist.rb +41 -0
  112. data/lib/poetry/core/css/tailwind_merger.rb +102 -0
  113. data/lib/poetry/core/css/template_classes.rb +102 -0
  114. data/lib/poetry/core/css/theme_coverage.rb +72 -0
  115. data/lib/poetry/core/css/var_coverage.rb +75 -0
  116. data/lib/poetry/core/css/verifier.rb +106 -0
  117. data/lib/poetry/core/design_lint.rb +720 -0
  118. data/lib/poetry/core/design_md/import.rb +250 -0
  119. data/lib/poetry/core/design_md.rb +461 -0
  120. data/lib/poetry/core/engine.rb +71 -0
  121. data/lib/poetry/core/errors.rb +53 -0
  122. data/lib/poetry/core/html/attributes.rb +667 -0
  123. data/lib/poetry/core/icons.rb +157 -0
  124. data/lib/poetry/core/llms_text.rb +354 -0
  125. data/lib/poetry/core/page_architectures.rb +304 -0
  126. data/lib/poetry/core/part_contract.rb +285 -0
  127. data/lib/poetry/core/preview/abstract.rb +121 -0
  128. data/lib/poetry/core/preview/base.rb +227 -0
  129. data/lib/poetry/core/preview/sidecarable.rb +136 -0
  130. data/lib/poetry/core/preview/template.rb +186 -0
  131. data/lib/poetry/core/recipe_items.rb +82 -0
  132. data/lib/poetry/core/registry.rb +363 -0
  133. data/lib/poetry/core/registry_address.rb +115 -0
  134. data/lib/poetry/core/registry_client.rb +217 -0
  135. data/lib/poetry/core/registry_installer.rb +234 -0
  136. data/lib/poetry/core/registry_items.rb +184 -0
  137. data/lib/poetry/core/skill_text.rb +288 -0
  138. data/lib/poetry/core/stable_id.rb +110 -0
  139. data/lib/poetry/core/stimulus/builder.rb +395 -0
  140. data/lib/poetry/core/stimulus/declarations.rb +444 -0
  141. data/lib/poetry/core/stimulus/manifest.rb +68 -0
  142. data/lib/poetry/core/stimulus/merger.rb +131 -0
  143. data/lib/poetry/core/stimulus_contract.rb +263 -0
  144. data/lib/poetry/core/tag_helper.rb +10 -0
  145. data/lib/poetry/core/template_compile.rb +74 -0
  146. data/lib/poetry/core/token_import.rb +317 -0
  147. data/lib/poetry/core/tokens/color.rb +215 -0
  148. data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
  149. data/lib/poetry/core/tokens/generator.rb +211 -0
  150. data/lib/poetry/core/tokens.rb +117 -0
  151. data/lib/poetry/core/version.rb +9 -0
  152. data/lib/poetry/core.rb +52 -0
  153. data/lib/poetry-core.rb +5 -0
  154. data/tokens/aliases.css +145 -0
  155. data/tokens/tailwind-theme.css +47 -0
  156. data/tokens/tokens.css +78 -0
  157. data/tokens/tokens.dtcg.json +779 -0
  158. data/vendor/floating-ui/LICENSE +20 -0
  159. data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
  160. data/vendor/shadcn-tailwind/LICENSE +21 -0
  161. data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
  162. data/vendor/shadcn-tailwind/tailwind.css +629 -0
  163. data/vendor/tw-animate-css/LICENSE +21 -0
  164. data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
  165. data/vendor/tw-animate-css/tw-animate.css +1 -0
  166. metadata +267 -0
@@ -0,0 +1,510 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { collectionItems } from "@poetry/controllers/helpers/collection"
3
+ import { scoreItem } from "@poetry/controllers/helpers/filter_rank"
4
+
5
+ // The Command palette engine - the suite's
6
+ // first ACTIVEDESCENDANT component (the APG editable-combobox pattern, the
7
+ // deliberate delta vs the menus/Select family's roving focus): real DOM
8
+ // focus stays pinned to the input for the whole session; the highlighted
9
+ // option carries data-highlighted + its server-stable id in the input's
10
+ // aria-activedescendant (the twin-write, activedescendant-flavored).
11
+ // Options never get tabindex; roving-focus is NOT attached; aria-selected
12
+ // is NEVER written here (reserved for committed values - Combobox's
13
+ // twin-write; a bare palette has no committed value).
14
+ //
15
+ // THE FILTER is the deterministic helpers/filter_rank spec (prefix 4 >
16
+ // word-boundary 3 > substring 2 > keyword 1 > hidden 0, diacritic-folded)
17
+ // and it is HIDE-ONLY: score-0 items get hidden + data-hidden, matches get
18
+ // both removed, and children are NEVER reordered - DOM order is the
19
+ // ranking authority within a band; the score's only job is seating the
20
+ // auto-highlight. filter:false is the server-driven mode: steps 1-2
21
+ // (hiding) are skipped entirely and only
22
+ // highlight/activation/announcement run over whatever the server rendered
23
+ // - the Turbo-frame async seam Combobox's recipe plugs into.
24
+ //
25
+ // ACTIVATION IS AN EVENT, NOT AN ACTION: Enter/click dispatches cancelable
26
+ // poetry:command:select and this controller does nothing further - no
27
+ // navigation, no close, no value write. The listener (host data-action,
28
+ // Combobox's commit pipeline) owns the consequences - the engine-purity
29
+ // Combobox's composition depends on (no popper/commit/native-select code
30
+ // lives here, fenced by the conformance greps).
31
+ //
32
+ // The DOM is the store, filtering included: query in the input, visibility
33
+ // as hidden + data-hidden, highlight as data-highlighted +
34
+ // aria-activedescendant, group visibility derived - a Turbo Stream can
35
+ // append items mid-session and the next keystroke ranks them.
36
+ // Each part matches both families' vocabularies: Command's own names and the
37
+ // combobox's (two names for the same parts; the engine rides both).
38
+ const INPUT_SELECTOR = '[data-slot="command-input"], [data-slot="combobox-input"], [data-slot="combobox-chip-input"]'
39
+ const LIST_SELECTOR = '[data-slot="command-list"], [data-slot="combobox-list"]'
40
+ const ITEM_SELECTOR = '[data-slot="command-item"], [data-slot="combobox-item"]'
41
+ const ITEM_TEXT_SELECTOR = '[data-slot="command-item-text"], [data-slot="combobox-item-text"]'
42
+ const GROUP_SELECTOR = '[data-slot="command-group"], [data-slot="combobox-group"]'
43
+ const SEPARATOR_SELECTOR = '[data-slot="command-separator"], [data-slot="combobox-separator"]'
44
+ const EMPTY_SELECTOR = '[data-slot="command-empty"], [data-slot="combobox-empty"]'
45
+ const STATUS_SELECTOR = '[data-slot="command-status"], [data-slot="combobox-status"]'
46
+
47
+ const EVENT_PREFIX = "poetry:command"
48
+
49
+ // The status live region is ALWAYS debounced (keystroke bursts announce
50
+ // once) - activedescendant says WHERE you are, the count says HOW MANY
51
+ // remain (the count is the piece filter UIs usually leave silent).
52
+ const STATUS_DEBOUNCE = 100
53
+
54
+ export default class CommandController extends Controller {
55
+ // The events this controller dispatches (manifest surface;
56
+ // events_declaration.test.js enforces the list stays honest).
57
+ static events = ["poetry:command:filter", "poetry:command:highlight", "poetry:command:select"]
58
+
59
+ static values = {
60
+ // false = SERVER-DRIVEN mode: never hide -
61
+ // the host re-renders the list (Turbo frame) and Command only runs
62
+ // highlight/activation/announcement over what is rendered.
63
+ filter: { type: Boolean, default: true },
64
+ // Arrow-key wrap over visible enabled items.
65
+ loop: { type: Boolean, default: false },
66
+ // Filter-pass debounce in ms (0 = synchronous - client filtering is
67
+ // cheap string work; the status announcement is debounced separately).
68
+ debounce: { type: Number, default: 0 }
69
+ }
70
+
71
+ #connected = false
72
+ #passTimer = null
73
+ #statusTimer = null
74
+ #delegatedList = null
75
+
76
+ /**
77
+ * Reconciles (a non-empty input re-derives visibility + highlight
78
+ * silently; an empty one just seats the highlight) and wires the portal
79
+ * delegation on the list node (the body comments hold the rules).
80
+ */
81
+ connect() {
82
+ // Reconcile-on-connect (Turbo morph/stream safe): a non-empty input
83
+ // re-derives visibility + highlight silently (no events for state the
84
+ // server already declared); an empty input just seats the highlight
85
+ // (a server-rendered data-highlighted item wins, else first enabled).
86
+ if ((this.#input()?.value ?? "") !== "") {
87
+ this.#pass({ silent: true })
88
+ } else {
89
+ this.#seat({ silent: true })
90
+ }
91
+
92
+ // Portal delegation: Stimulus scopes
93
+ // data-actions to the controller's subtree, so the per-item
94
+ // activate/pointerHighlight actions go DEAD when the popup portals
95
+ // out of it (Combobox multiple mounts this engine on the ROOT while
96
+ // the listbox moves to body). The listeners ride the LIST node - they
97
+ // travel with the portal; in-scope items remain the actions' job
98
+ // (#delegatedItem guards the double-fire).
99
+ const list = this.#list()
100
+
101
+ if (list) {
102
+ list.addEventListener("click", this.#onListClick)
103
+ list.addEventListener("pointermove", this.#onListPointermove)
104
+ this.#delegatedList = list
105
+ }
106
+
107
+ this.#connected = true
108
+ }
109
+
110
+ /** Clears the timers and unwires the list delegation. */
111
+ disconnect() {
112
+ this.#connected = false
113
+
114
+ if (this.#passTimer !== null) window.clearTimeout(this.#passTimer)
115
+ if (this.#statusTimer !== null) window.clearTimeout(this.#statusTimer)
116
+
117
+ this.#passTimer = null
118
+ this.#statusTimer = null
119
+
120
+ if (this.#delegatedList) {
121
+ this.#delegatedList.removeEventListener("click", this.#onListClick)
122
+ this.#delegatedList.removeEventListener("pointermove", this.#onListPointermove)
123
+ this.#delegatedList = null
124
+ }
125
+ }
126
+
127
+ #onListClick = (event) => {
128
+ const item = this.#delegatedItem(event)
129
+
130
+ if (item && !this.#isDisabled(item)) this.#activate(item)
131
+ }
132
+
133
+ #onListPointermove = (event) => {
134
+ const item = this.#delegatedItem(event)
135
+
136
+ if (item && !this.#isDisabled(item) && !this.#isHidden(item)) this.#highlight(item, { scroll: false })
137
+ }
138
+
139
+ #delegatedItem(event) {
140
+ if (!(event.target instanceof Element)) return null
141
+ // In-scope items are the Stimulus actions' job - delegation exists
142
+ // only for items the portal moved OUT of this controller's subtree.
143
+ if (this.element.contains(event.target)) return null
144
+
145
+ return event.target.closest(ITEM_SELECTOR)
146
+ }
147
+
148
+ // --- the filter pass (input action) ---
149
+
150
+ /**
151
+ * The input's input action: runs the filter pass (debounced when
152
+ * configured).
153
+ */
154
+ filterInput() {
155
+ if (this.debounceValue <= 0) {
156
+ this.#pass()
157
+ return
158
+ }
159
+
160
+ if (this.#passTimer !== null) window.clearTimeout(this.#passTimer)
161
+
162
+ this.#passTimer = window.setTimeout(() => {
163
+ this.#passTimer = null
164
+ this.#pass()
165
+ }, this.debounceValue)
166
+ }
167
+
168
+ // --- the activedescendant keyboard map (input action) ---
169
+
170
+ /**
171
+ * The input's keydown action: ArrowUp/Down move the highlight
172
+ * (Meta/Ctrl jumps first/last), Enter activates it.
173
+ * Home/End/ArrowLeft/ArrowRight fall through to the input (CARET
174
+ * movement - APG-correct for an editable field; a Home/End list-jump
175
+ * is deliberately not offered). Space TYPES a space, never
176
+ * activates (a text field - the family delta vs Select/menus). Esc/Tab
177
+ * are NOT handled - the hosting layer owns them (Dialog dismiss /
178
+ * Combobox close / natural tab-out).
179
+ *
180
+ * @param {KeyboardEvent} event
181
+ */
182
+ keydown(event) {
183
+ switch (event.key) {
184
+ case "ArrowDown":
185
+ case "ArrowUp": {
186
+ event.preventDefault() // the caret never moves on vertical arrows
187
+
188
+ const items = this.#navigableItems()
189
+
190
+ if (items.length === 0) return
191
+
192
+ const down = event.key === "ArrowDown"
193
+
194
+ if (event.metaKey || event.ctrlKey) {
195
+ // Meta/Ctrl+ArrowDown/Up jump last / first.
196
+ this.#highlight(items[down ? items.length - 1 : 0])
197
+ return
198
+ }
199
+
200
+ const index = items.indexOf(this.#highlighted())
201
+ let next
202
+
203
+ if (index === -1) {
204
+ next = down ? 0 : items.length - 1
205
+ } else {
206
+ next = index + (down ? 1 : -1)
207
+
208
+ if (this.loopValue) next = (next + items.length) % items.length
209
+ else next = Math.min(items.length - 1, Math.max(0, next))
210
+ }
211
+
212
+ this.#highlight(items[next])
213
+ return
214
+ }
215
+ case "Enter": {
216
+ event.preventDefault() // never submit a form through the palette input
217
+
218
+ const item = this.#highlighted()
219
+
220
+ if (item && !this.#isDisabled(item) && !this.#isHidden(item)) this.#activate(item)
221
+
222
+ return
223
+ }
224
+ default:
225
+ // Everything else - printable keys, Space, Backspace, Home/End,
226
+ // Left/Right, Esc, Tab - belongs to the input or the host.
227
+ }
228
+ }
229
+
230
+ // --- activation (item click action) ---
231
+
232
+ /**
233
+ * Each in-scope item's click action: dispatches the cancelable select
234
+ * event - and nothing further (the engine-purity contract).
235
+ *
236
+ * @param {MouseEvent} event
237
+ */
238
+ activate(event) {
239
+ const origin = event.currentTarget instanceof Element ? event.currentTarget : event.target
240
+ const item = origin instanceof Element ? origin.closest(ITEM_SELECTOR) : null
241
+
242
+ if (!item || this.#isDisabled(item)) return
243
+
244
+ this.#activate(item)
245
+ }
246
+
247
+ // --- pointer highlight (item pointermove action) ---
248
+
249
+ /**
250
+ * Each in-scope item's pointermove action: hovering highlights (no
251
+ * scroll chasing); pointerleave does NOT clear - the keyboard position
252
+ * survives mouse exit (contractual).
253
+ *
254
+ * @param {PointerEvent} event
255
+ */
256
+ pointerHighlight(event) {
257
+ const origin = event.currentTarget instanceof Element ? event.currentTarget : event.target
258
+ const item = origin instanceof Element ? origin.closest(ITEM_SELECTOR) : null
259
+
260
+ if (!item || this.#isDisabled(item) || this.#isHidden(item)) return
261
+
262
+ this.#highlight(item, { scroll: false })
263
+ }
264
+
265
+ // --- the composition surface (engine-generic; Combobox calls these) ---
266
+
267
+ /**
268
+ * The composition surface (Combobox calls it): moves the highlight to a
269
+ * given option (visible + enabled required).
270
+ *
271
+ * @param {Element} item
272
+ * @param {Object} [options]
273
+ * @param {boolean} [options.scroll=true] - scroll the option into view
274
+ */
275
+ highlightItem(item, { scroll = true } = {}) {
276
+ if (!item || this.#isDisabled(item) || this.#isHidden(item)) return
277
+
278
+ this.#highlight(item, { scroll })
279
+ }
280
+
281
+ /**
282
+ * Clears the query and silently re-derives visibility + highlight (the
283
+ * clean-reopen reset an overlay host runs on close).
284
+ */
285
+ reset() {
286
+ const input = this.#input()
287
+
288
+ if (!input) return
289
+
290
+ input.value = ""
291
+ this.#pass({ silent: true })
292
+ }
293
+
294
+ // --- the pass ---
295
+
296
+ // Synchronous, on every input event (and silently on connect/reset):
297
+ // 1. score every collection item; client mode hides score-0 (unless
298
+ // data-always-render) via hidden + data-hidden - NEVER reorders.
299
+ // 2. groups hide when ALL their items hide; separators hide whenever
300
+ // the query is non-empty. Skipped in filter:false.
301
+ // 3. re-seat the highlight: highest score among visible ∩ enabled,
302
+ // first-in-DOM tiebreak; zero visible clears both twin-writes and
303
+ // unhides the empty part.
304
+ // 4. dispatch poetry:command:filter {query, visible}; debounce the
305
+ // status live-region count.
306
+ #pass({ silent = false } = {}) {
307
+ const input = this.#input()
308
+ const list = this.#list()
309
+
310
+ if (!input || !list) return
311
+
312
+ const query = input.value ?? ""
313
+ const items = this.#items(list)
314
+ const scores = new Map()
315
+
316
+ for (const item of items) {
317
+ const score = scoreItem(item, query)
318
+
319
+ scores.set(item, score)
320
+
321
+ if (this.filterValue) {
322
+ const hide = score === 0 && !item.hasAttribute("data-always-render")
323
+
324
+ item.toggleAttribute("hidden", hide)
325
+ item.toggleAttribute("data-hidden", hide)
326
+ }
327
+ }
328
+
329
+ if (this.filterValue) {
330
+ for (const group of list.querySelectorAll(GROUP_SELECTOR)) {
331
+ const members = this.#items(group)
332
+ const hide = !group.hasAttribute("data-always-render") &&
333
+ members.every((item) => item.hasAttribute("data-hidden"))
334
+
335
+ group.toggleAttribute("hidden", hide)
336
+ }
337
+
338
+ for (const separator of list.querySelectorAll(SEPARATOR_SELECTOR)) {
339
+ separator.toggleAttribute("hidden", query !== "")
340
+ }
341
+ }
342
+
343
+ const visibleItems = items.filter((item) => !this.#isHidden(item))
344
+ const visible = visibleItems.length
345
+ // List-scoped first: in Combobox's multiple mode this engine rides the
346
+ // ROOT while portal-on-open moves the popup (list + empty + status) to
347
+ // body - element scoping goes blind there; the list resolves by the
348
+ // input's aria-controls id, and the parts around it hang off it.
349
+ const empty = list.querySelector(EMPTY_SELECTOR) ?? this.element.querySelector(EMPTY_SELECTOR)
350
+
351
+ if (empty) empty.hidden = visible !== 0
352
+
353
+ // Re-seat: highest score among visible ∩ enabled, first-in-DOM
354
+ // tiebreak (strictly-greater replacement preserves DOM order as the
355
+ // within-band authority).
356
+ let best = null
357
+ let bestScore = -1
358
+
359
+ for (const item of visibleItems) {
360
+ if (this.#isDisabled(item)) continue
361
+
362
+ const score = scores.get(item) ?? 0
363
+
364
+ if (score > bestScore) {
365
+ best = item
366
+ bestScore = score
367
+ }
368
+ }
369
+
370
+ if (best) this.#highlight(best, { silent })
371
+ else this.#clearHighlight()
372
+
373
+ if (silent) return
374
+
375
+ this.dispatch("filter", { prefix: EVENT_PREFIX, detail: { query, visible } })
376
+ this.#announce(visible)
377
+ }
378
+
379
+ // Initial seat (empty query): a server-rendered data-highlighted item
380
+ // wins (the value: option), else the first visible enabled item; the
381
+ // activedescendant twin is written either way.
382
+ #seat({ silent = false } = {}) {
383
+ const seeded = this.#highlighted()
384
+
385
+ if (seeded && !this.#isDisabled(seeded) && !this.#isHidden(seeded)) {
386
+ this.#highlight(seeded, { silent, force: true })
387
+ return
388
+ }
389
+
390
+ const first = this.#navigableItems()[0]
391
+
392
+ if (first) this.#highlight(first, { silent })
393
+ else this.#clearHighlight()
394
+ }
395
+
396
+ // --- highlight (the twin-write: data-highlighted + activedescendant) ---
397
+
398
+ #highlight(item, { silent = false, scroll = true, force = false } = {}) {
399
+ const previous = this.#highlighted()
400
+
401
+ if (previous === item && !force) return
402
+
403
+ previous?.removeAttribute("data-highlighted")
404
+ item.setAttribute("data-highlighted", "")
405
+ this.#input()?.setAttribute("aria-activedescendant", item.id)
406
+ if (scroll) item.scrollIntoView?.({ block: "nearest" })
407
+
408
+ if (silent || previous === item) return
409
+
410
+ this.dispatch("highlight", {
411
+ prefix: EVENT_PREFIX,
412
+ target: item,
413
+ detail: { item, value: item.dataset.value ?? "" }
414
+ })
415
+ }
416
+
417
+ #clearHighlight() {
418
+ this.#highlighted()?.removeAttribute("data-highlighted")
419
+ this.#input()?.removeAttribute("aria-activedescendant")
420
+ }
421
+
422
+ // --- activation ---
423
+
424
+ // Cancelable select; NOT canceled -> Command does NOTHING FURTHER (no
425
+ // navigation, no close, no value write) - a palette engine, not an actor.
426
+ #activate(item) {
427
+ this.dispatch("select", {
428
+ prefix: EVENT_PREFIX,
429
+ target: item,
430
+ cancelable: true,
431
+ detail: { item, value: item.dataset.value ?? "", label: this.#labelOf(item) }
432
+ })
433
+ }
434
+
435
+ #labelOf(item) {
436
+ return (item.querySelector(ITEM_TEXT_SELECTOR)?.textContent ?? item.textContent ?? "").trim()
437
+ }
438
+
439
+ // --- the status live region ---
440
+
441
+ // Debounced result counts into the component-rendered polite region;
442
+ // the localized templates ride data attributes on the region (data-zero
443
+ // / data-one / data-other with a %{count} placeholder) so the engine
444
+ // stays i18n-free.
445
+ #announce(visible) {
446
+ // Element scope first (the bare palette), then beside the resolved
447
+ // list - Combobox multiple portals the popup (status included) out of
448
+ // this engine's root subtree.
449
+ const status = this.element.querySelector(STATUS_SELECTOR) ??
450
+ this.#list()?.parentElement?.querySelector(STATUS_SELECTOR)
451
+
452
+ if (!status) return
453
+
454
+ if (this.#statusTimer !== null) window.clearTimeout(this.#statusTimer)
455
+
456
+ this.#statusTimer = window.setTimeout(() => {
457
+ this.#statusTimer = null
458
+
459
+ const template = (visible === 0 && status.dataset.zero) ||
460
+ (visible === 1 && status.dataset.one) ||
461
+ status.dataset.other || "%{count} results"
462
+
463
+ status.textContent = template.replace("%{count}", String(visible))
464
+ }, STATUS_DEBOUNCE)
465
+ }
466
+
467
+ // --- structural resolution (data-slots, portal/stream-safe) ---
468
+
469
+ #input() {
470
+ return this.element.querySelector(INPUT_SELECTOR)
471
+ }
472
+
473
+ #list() {
474
+ const id = this.#input()?.getAttribute("aria-controls")
475
+
476
+ return (id && document.getElementById(id)) || this.element.querySelector(LIST_SELECTOR)
477
+ }
478
+
479
+ #items(root = this.#list()) {
480
+ return root ? collectionItems(root, ITEM_SELECTOR) : []
481
+ }
482
+
483
+ #navigableItems() {
484
+ return this.#items().filter((item) => !this.#isHidden(item) && !this.#isDisabled(item))
485
+ }
486
+
487
+ #highlighted() {
488
+ return this.#list()?.querySelector("[data-highlighted]") ?? null
489
+ }
490
+
491
+ #isDisabled(item) {
492
+ return item.hasAttribute("data-disabled") || item.getAttribute("aria-disabled") === "true"
493
+ }
494
+
495
+ // An item is hidden when itself or any ancestor UP TO THE LIST (its
496
+ // group, a server-hidden frame) carries the hidden attribute. The walk
497
+ // stops at the list ON PURPOSE: the close-time reset() runs after the
498
+ // host hid the whole popup, and counting that ancestor made every item
499
+ // "hidden" - zero visible - so the pass UNHID the empty part and every
500
+ // reopen flashed "No results found." until the first keystroke.
501
+ #isHidden(item) {
502
+ const list = this.#list()
503
+
504
+ for (let node = item; node && node !== list; node = node.parentElement) {
505
+ if (node.hidden) return true
506
+ }
507
+
508
+ return false
509
+ }
510
+ }