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,458 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { isImeKeydown } from "@poetry/controllers/helpers/escape"
3
+ import { setState } from "@poetry/controllers/helpers/state"
4
+ import { enterPresence, exitPresence, flushPendingExits } from "@poetry/controllers/helpers/presence"
5
+ import { onBeforeCache } from "@poetry/controllers/helpers/turbo_cache"
6
+
7
+ // The NavigationMenu coordinator (the viewport=false mode): a
8
+ // DISCLOSURE BAR, not a menu - Tab moves through triggers and links
9
+ // normally, arrows are convenience navigation, nothing traps. Each item's
10
+ // panel is its own popup positioned under the item; this controller owns
11
+ // what per-item popovers can't: ONE panel open at a time, hover intent
12
+ // (open/close delays so diagonal travel into a panel doesn't flicker),
13
+ // Esc + focus-out + outside-press closing, and the vocabulary writes
14
+ // (data-popup-open on the trigger - the chevron's rotation hook - and the
15
+ // presence-driven open/closed pair on the panel).
16
+ //
17
+ // THE VIEWPORT MODE: when the markup ships the shared
18
+ // positioner > popup > viewport shell (viewport: true), panels are
19
+ // lazily ADOPTED into the viewport on first activation (the Rails
20
+ // stand-in for a portal - server-rendered content stays in
21
+ // place until JS activates) and the composite MORPHS: the popper
22
+ // re-anchors to the active trigger (full floating-ui) while CSS
23
+ // transitions the positioner's insets, the popup's --popup-width/height
24
+ // pin old -> new across two frames so width/height transition, panels
25
+ // slide by data-activation-direction (new trigger vs old - travel
26
+ // direction drives the slide), and data-instant suppresses transitions
27
+ // on cold opens. Vars reset to auto after the animations finish (the
28
+ // auto-size reset via getAnimations().finished).
29
+ const TRIGGER_SELECTOR = '[data-slot="navigation-menu-trigger"]'
30
+ const PANEL_SELECTOR = '[data-slot="navigation-menu-content"]'
31
+ const ITEM_SELECTOR = '[data-slot="navigation-menu-item"]'
32
+ const POSITIONER_SELECTOR = '[data-slot="navigation-menu-positioner"]'
33
+ const POPUP_SELECTOR = '[data-slot="navigation-menu-popup"]'
34
+ const VIEWPORT_SELECTOR = '[data-slot="navigation-menu-viewport"]'
35
+
36
+ export default class NavigationMenuController extends Controller {
37
+ static values = {
38
+ openDelay: { type: Number, default: 50 },
39
+ closeDelay: { type: Number, default: 150 }
40
+ }
41
+
42
+ #openValue = null
43
+ #timer = null
44
+ #cancelExit = new Map() // value -> abandon-this-panel's-exit (per panel, not global)
45
+ #onOutsidePress = null
46
+ #unsubscribeBeforeCache = null
47
+ #sizeGeneration = 0
48
+
49
+ /**
50
+ * Subscribes the before-cache close (the body comment holds the zombie
51
+ * rules).
52
+ */
53
+ connect() {
54
+ // Close before Turbo snapshots: an open panel serialized into the
55
+ // cache restores with data-open (viewport-mode panels also serialize
56
+ // adopted into the shared viewport). The explicit flush completes the
57
+ // exit THIS close just started, regardless of listener order (the
58
+ // dismissable layer's pattern).
59
+ this.#unsubscribeBeforeCache = onBeforeCache(() => {
60
+ if (this.#openValue === null) return
61
+ this.#clearTimer()
62
+ this.#close()
63
+ flushPendingExits()
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Clears the timer and unwires the outside-press and before-cache
69
+ * listeners.
70
+ */
71
+ disconnect() {
72
+ this.#clearTimer()
73
+ this.#unbindOutsidePress()
74
+ this.#unsubscribeBeforeCache?.()
75
+ this.#unsubscribeBeforeCache = null
76
+ }
77
+
78
+ /**
79
+ * Each trigger's click action - immediate, cancels hover intent.
80
+ *
81
+ * @param {MouseEvent} event
82
+ */
83
+ toggle(event) {
84
+ const value = this.#valueFrom(event)
85
+ if (value === null) return
86
+
87
+ this.#clearTimer()
88
+ if (this.#openValue === value) this.#close()
89
+ else this.#open(value)
90
+ }
91
+
92
+ /**
93
+ * Each ITEM's pointerenter action (the panel lives inside its item, so
94
+ * moving into the panel never schedules a close): arms hover intent -
95
+ * instant switch while the bar is already open, the open delay on cold
96
+ * entry. Touch is click's job.
97
+ *
98
+ * @param {PointerEvent} event
99
+ */
100
+ scheduleOpen(event) {
101
+ if (event.pointerType === "touch") return // touch is click's job
102
+
103
+ const value = this.#valueFrom(event)
104
+ if (value === null) return
105
+ if (value === this.#openValue) {
106
+ this.#clearTimer() // re-entering the open item cancels a pending close
107
+ return
108
+ }
109
+
110
+ // Instant switch while the bar is already open (the mega-menu feel);
111
+ // intent delay only on cold entry.
112
+ this.#schedule(() => this.#open(value), this.#openValue === null ? this.openDelayValue : 0)
113
+ }
114
+
115
+ /**
116
+ * Each ITEM's pointerleave action: arms the close grace.
117
+ *
118
+ * @param {PointerEvent} event
119
+ */
120
+ scheduleClose(event) {
121
+ if (event.pointerType === "touch") return
122
+ if (this.#openValue === null) return
123
+
124
+ this.#schedule(() => this.#close(), this.closeDelayValue)
125
+ }
126
+
127
+ /**
128
+ * The shared positioner's pointerenter action - in viewport mode the
129
+ * panel no longer lives inside its item, so entering the popup must
130
+ * cancel a pending close.
131
+ */
132
+ cancelClose() {
133
+ this.#clearTimer()
134
+ }
135
+
136
+ /**
137
+ * The root's keydown action: Escape closes and refocuses the trigger;
138
+ * ArrowLeft/Right move between the bar's stops (triggers and top-level
139
+ * links).
140
+ *
141
+ * @param {KeyboardEvent} event
142
+ */
143
+ keydown(event) {
144
+ if (event.key === "Escape") {
145
+ if (isImeKeydown(event)) return
146
+ if (this.#openValue === null) return
147
+
148
+ const trigger = this.#triggerFor(this.#openValue)
149
+ this.#close()
150
+ trigger?.focus()
151
+ event.preventDefault()
152
+ return
153
+ }
154
+
155
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
156
+
157
+ const stops = this.#arrowStops()
158
+ const index = stops.indexOf(event.target)
159
+ if (index === -1) return
160
+
161
+ const next = stops[index + (event.key === "ArrowRight" ? 1 : -1)]
162
+ if (!next) return
163
+
164
+ event.preventDefault()
165
+ next.focus()
166
+ }
167
+
168
+ /**
169
+ * The root's focusout action - a disclosure closes when focus leaves it
170
+ * entirely (never traps).
171
+ *
172
+ * @param {FocusEvent} event
173
+ */
174
+ focusLeft(event) {
175
+ const next = event.relatedTarget
176
+
177
+ if (next instanceof Element && this.element.contains(next)) return
178
+
179
+ this.#clearTimer()
180
+ this.#close()
181
+ }
182
+
183
+ #open(value) {
184
+ if (this.#openValue === value) return
185
+
186
+ const previous = this.#openValue
187
+ if (this.#viewport() && previous !== null) this.#stampDirection(previous, value)
188
+ if (previous !== null) this.#hidePanel(previous)
189
+
190
+ this.#openValue = value
191
+ const trigger = this.#triggerFor(value)
192
+ const panel = this.#panelFor(value)
193
+
194
+ if (trigger) {
195
+ trigger.setAttribute("aria-expanded", "true")
196
+ setState(trigger, "popup-open")
197
+ setState(trigger, "open")
198
+ }
199
+ if (panel) {
200
+ // Re-opening THIS panel mid-close: abandon only its own exit (a
201
+ // sibling's pending exit must still run onRemove and hide it).
202
+ this.#cancelExit.get(value)?.()
203
+ this.#cancelExit.delete(value)
204
+ if (this.#viewport()) {
205
+ this.#showInViewport(panel, trigger, previous)
206
+ } else {
207
+ panel.hidden = false
208
+ enterPresence(panel)
209
+ }
210
+ }
211
+ this.#bindOutsidePress()
212
+ }
213
+
214
+ #close() {
215
+ if (this.#openValue === null) return
216
+
217
+ this.#hidePanel(this.#openValue)
218
+ this.#openValue = null
219
+ this.#unbindOutsidePress()
220
+ this.#closeViewport()
221
+ }
222
+
223
+ // -- the morphing viewport -----------------------------------------------
224
+
225
+ #viewport() {
226
+ return this.element.querySelector(VIEWPORT_SELECTOR)
227
+ }
228
+
229
+ #showInViewport(panel, trigger, previous) {
230
+ const viewport = this.#viewport()
231
+ const positioner = this.element.querySelector(POSITIONER_SELECTOR)
232
+ const popup = this.element.querySelector(POPUP_SELECTOR)
233
+ const cold = previous === null
234
+
235
+ // Lazy adoption: the server-rendered panel moves from its item into
236
+ // the shared viewport on FIRST activation (the portal's Rails
237
+ // stand-in - no-JS content stays in place until JS activates).
238
+ if (panel.parentElement !== viewport) {
239
+ panel.setAttribute("data-viewport-panel", "")
240
+ viewport.appendChild(panel)
241
+ }
242
+
243
+ // The size choreography: pin the popup
244
+ // to the OLD size, reveal the new panel, measure its natural size
245
+ // (panels are absolutely stacked, so they size intrinsically), then
246
+ // pin the NEW size a frame later so width/height transition.
247
+ const oldWidth = popup.offsetWidth
248
+ const oldHeight = popup.offsetHeight
249
+
250
+ panel.hidden = false
251
+ const nextWidth = panel.offsetWidth
252
+ const nextHeight = panel.offsetHeight
253
+
254
+ if (cold) {
255
+ this.#stampInstant(positioner, popup) // no transition on a cold open
256
+ this.#pinSize(positioner, popup, nextWidth, nextHeight)
257
+ this.#scheduleSizeReset(positioner, popup)
258
+ } else {
259
+ // Pin old, pin new a frame later (the width/height transition), and
260
+ // only THEN arm the settle reset - armed alongside the OLD pin it
261
+ // samples getAnimations before the transition exists, resets to auto
262
+ // synchronously, and the late new-size pin sticks forever (the D3
263
+ // Chrome-proof catch: the morph never ran).
264
+ this.#pinSize(positioner, popup, oldWidth, oldHeight)
265
+ this.#nextFrame(() => {
266
+ this.#pinSize(positioner, popup, nextWidth, nextHeight)
267
+ this.#nextFrame(() => this.#scheduleSizeReset(positioner, popup))
268
+ })
269
+ }
270
+
271
+ positioner.hidden = false
272
+ setState(popup, "open")
273
+ enterPresence(panel)
274
+ this.#anchorPopper(trigger)
275
+ }
276
+
277
+ #closeViewport() {
278
+ const positioner = this.element.querySelector(POSITIONER_SELECTOR)
279
+ const popup = this.element.querySelector(POPUP_SELECTOR)
280
+ if (!positioner || positioner.hidden) return
281
+
282
+ setState(popup, "closed")
283
+ positioner.hidden = true
284
+ }
285
+
286
+ // The popper (full floating-ui) re-anchors to the active
287
+ // trigger; the positioner's inset transition gives the slide.
288
+ #anchorPopper(trigger) {
289
+ if (!trigger) return
290
+
291
+ const popper =
292
+ this.application.getControllerForElementAndIdentifier(this.element, "poetry--core--popper")
293
+ popper?.setAnchorElement(trigger)
294
+ }
295
+
296
+ // data-activation-direction: which way the activation traveled (the new
297
+ // trigger relative to the old); outgoing AND incoming panels wear it so
298
+ // the starting/ending-style slides read one direction.
299
+ #stampDirection(previousValue, nextValue) {
300
+ const previous = this.#triggerFor(previousValue)
301
+ const next = this.#triggerFor(nextValue)
302
+ if (!previous || !next) return
303
+
304
+ const delta = next.getBoundingClientRect().left - previous.getBoundingClientRect().left
305
+ if (delta === 0) return
306
+
307
+ const direction = delta > 0 ? "right" : "left"
308
+ for (const panel of [this.#panelFor(previousValue), this.#panelFor(nextValue)]) {
309
+ panel?.setAttribute("data-activation-direction", direction)
310
+ }
311
+ }
312
+
313
+ // Pin the shared size vars (popup + positioner move together).
314
+ #pinSize(positioner, popup, width, height) {
315
+ popup.style.setProperty("--popup-width", `${width}px`)
316
+ popup.style.setProperty("--popup-height", `${height}px`)
317
+ positioner.style.setProperty("--positioner-width", `${width}px`)
318
+ positioner.style.setProperty("--positioner-height", `${height}px`)
319
+ }
320
+
321
+ // The auto-size reset: once the morph settles the vars return
322
+ // to auto, so later content growth isn't clipped. A newer activation
323
+ // cancels a stale reset (the generation counter).
324
+ #scheduleSizeReset(positioner, popup) {
325
+ const generation = ++this.#sizeGeneration
326
+ const reset = () => {
327
+ if (generation !== this.#sizeGeneration) return
328
+
329
+ this.#pinAuto(positioner, popup)
330
+ }
331
+
332
+ if (typeof popup.getAnimations === "function") {
333
+ const animations = popup.getAnimations()
334
+ if (animations.length > 0) {
335
+ Promise.allSettled(animations.map((animation) => animation.finished)).then(reset)
336
+ return
337
+ }
338
+ }
339
+ reset()
340
+ }
341
+
342
+ #pinAuto(positioner, popup) {
343
+ popup.style.setProperty("--popup-width", "auto")
344
+ popup.style.setProperty("--popup-height", "auto")
345
+ positioner.style.setProperty("--positioner-width", "auto")
346
+ positioner.style.setProperty("--positioner-height", "auto")
347
+ }
348
+
349
+ // data-instant suppresses the positioner/popup transitions for one
350
+ // painted frame (cold opens must not animate from nothing).
351
+ #stampInstant(positioner, popup) {
352
+ positioner.setAttribute("data-instant", "")
353
+ popup.setAttribute("data-instant", "")
354
+ this.#nextFrame(() => this.#nextFrame(() => {
355
+ positioner.removeAttribute("data-instant")
356
+ popup.removeAttribute("data-instant")
357
+ }))
358
+ }
359
+
360
+ #nextFrame(callback) {
361
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(callback)
362
+ else callback()
363
+ }
364
+
365
+ #hidePanel(value) {
366
+ const trigger = this.#triggerFor(value)
367
+ const panel = this.#panelFor(value)
368
+
369
+ if (trigger) {
370
+ trigger.setAttribute("aria-expanded", "false")
371
+ setState(trigger, "popup-closed")
372
+ setState(trigger, "closed")
373
+ }
374
+ if (panel) {
375
+ this.#cancelExit.set(value, exitPresence(panel, {
376
+ onRemove: () => {
377
+ panel.hidden = true
378
+ this.#cancelExit.delete(value)
379
+ }
380
+ }))
381
+ }
382
+ }
383
+
384
+ // Outside press closes - bound only while open (no idle listener).
385
+ #bindOutsidePress() {
386
+ if (this.#onOutsidePress) return
387
+
388
+ this.#onOutsidePress = (event) => {
389
+ // The dismissable layer's press rules: a disconnected target says
390
+ // nothing about WHERE the press landed (the false-dismiss class);
391
+ // composedPath is fixed at dispatch, so a press on a node an inner
392
+ // handler removed still counts as inside; and the top layer
393
+ // (toasts) sits above every dismissal surface by fiat.
394
+ if (!(event.target instanceof Element) || !event.target.isConnected) return
395
+ if (event.composedPath().includes(this.element)) return
396
+ if (event.target.closest("[data-poetry-top-layer]")) return
397
+
398
+ this.#clearTimer()
399
+ this.#close()
400
+ }
401
+ document.addEventListener("pointerdown", this.#onOutsidePress)
402
+ }
403
+
404
+ #unbindOutsidePress() {
405
+ if (!this.#onOutsidePress) return
406
+
407
+ document.removeEventListener("pointerdown", this.#onOutsidePress)
408
+ this.#onOutsidePress = null
409
+ }
410
+
411
+ #schedule(action, delay) {
412
+ this.#clearTimer()
413
+ this.#timer = window.setTimeout(() => {
414
+ this.#timer = null
415
+ action()
416
+ }, delay)
417
+ }
418
+
419
+ #clearTimer() {
420
+ if (this.#timer === null) return
421
+
422
+ window.clearTimeout(this.#timer)
423
+ this.#timer = null
424
+ }
425
+
426
+ #valueFrom(event) {
427
+ const origin = event.target instanceof Element ? event.target : null
428
+ const item = origin?.closest(ITEM_SELECTOR)
429
+
430
+ return item?.dataset.value ?? null
431
+ }
432
+
433
+ #triggerFor(value) {
434
+ return this.#itemFor(value)?.querySelector(TRIGGER_SELECTOR) ?? null
435
+ }
436
+
437
+ // An adopted panel (viewport mode) no longer lives inside its item -
438
+ // the trigger's aria-controls id finds it wherever it moved.
439
+ #panelFor(value) {
440
+ const inItem = this.#itemFor(value)?.querySelector(PANEL_SELECTOR)
441
+ if (inItem) return inItem
442
+
443
+ const id = this.#triggerFor(value)?.getAttribute("aria-controls")
444
+ return id ? document.getElementById(id) : null
445
+ }
446
+
447
+ #itemFor(value) {
448
+ return [...this.element.querySelectorAll(ITEM_SELECTOR)]
449
+ .find((item) => item.dataset.value === value) ?? null
450
+ }
451
+
452
+ // Arrow stops: every trigger and top-level link in the bar, DOM order.
453
+ #arrowStops() {
454
+ return [...this.element.querySelectorAll(
455
+ `${TRIGGER_SELECTOR}, [data-slot="navigation-menu-list"] > ${ITEM_SELECTOR} > a`
456
+ )]
457
+ }
458
+ }