poetry-core 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +0 -4
- data/app/components/poetry/core/component.rb +246 -7
- data/app/components/poetry/core/concerns/options.rb +1 -0
- data/app/components/poetry/core/concerns/styles.rb +46 -5
- data/app/components/poetry/core/style.rb +32 -1
- data/app/javascript/poetry/core/drawer_controller.js +28 -0
- data/app/javascript/poetry/core/hover_card_controller.js +26 -11
- data/app/javascript/poetry/core/message_scroller_controller.js +82 -4
- data/app/javascript/poetry/core/navigation_menu_controller.js +59 -4
- data/app/javascript/poetry/core/number_field_controller.js +24 -14
- data/config/component_registry.yml +2 -0
- data/config/controllers_manifest.json +2 -3
- data/lib/poetry/core/check/stable_identity.rb +14 -1
- data/lib/poetry/core/check.rb +182 -41
- data/lib/poetry/core/config.rb +15 -5
- data/lib/poetry/core/css/modes.rb +127 -0
- data/lib/poetry/core/css/override_scan.rb +17 -2
- data/lib/poetry/core/css/resolver.rb +21 -4
- data/lib/poetry/core/css/token_collisions.rb +184 -0
- data/lib/poetry/core/engine.rb +22 -0
- data/lib/poetry/core/host_components.rb +159 -0
- data/lib/poetry/core/host_helpers.rb +110 -0
- data/lib/poetry/core/html/attributes.rb +33 -2
- data/lib/poetry/core/llms_text.rb +39 -7
- data/lib/poetry/core/registry.rb +154 -5
- data/lib/poetry/core/skill_text.rb +27 -1
- data/lib/poetry/core/stimulus/declarations.rb +8 -3
- data/lib/poetry/core/stimulus/host_manifest.rb +556 -0
- data/lib/poetry/core/stimulus/manifest.rb +24 -0
- data/lib/poetry/core/tokens/generator.rb +10 -1
- data/lib/poetry/core/version.rb +1 -1
- data/tokens/tailwind-theme.css +1 -1
- metadata +6 -1
|
@@ -8,8 +8,9 @@ import { tabbableWithin } from "@poetry/controllers/helpers/tabbable"
|
|
|
8
8
|
// pointer-only enrichment behind a LINK. Two timers (open 600 / close
|
|
9
9
|
// 300, over the trigger+content
|
|
10
10
|
// pair, re-enter cancels - the grace window, no polygon), the touch double-guard
|
|
11
|
-
// (pointerType 'touch' no-ops AND
|
|
12
|
-
//
|
|
11
|
+
// (pointerType 'touch' no-ops AND the pointerdown latch swallows the focus a
|
|
12
|
+
// tap or click causes, so a tap keeps its click and just navigates the
|
|
13
|
+
// link - cancelling touchstart would cancel that click with it), the focus
|
|
13
14
|
// mirror (trigger focus opens immediately / blur closes - a keyboard user
|
|
14
15
|
// SEES the card), the per-open TABINDEX STRIP (every tabbable inside is
|
|
15
16
|
// forced tabindex=-1: keyboard and touch users never reach inside,
|
|
@@ -28,6 +29,11 @@ const CONTENT_SELECTOR = '[data-slot="hover-card-content"]'
|
|
|
28
29
|
const EVENT_PREFIX = "poetry:hover-card"
|
|
29
30
|
|
|
30
31
|
const DISMISSABLE = "poetry--core--dismissable"
|
|
32
|
+
// The pointerdown latch releases once the press has fully landed: the
|
|
33
|
+
// compatibility mouseup (a tap dispatches its mouse events, focus among
|
|
34
|
+
// them, AFTER its pointerup - so pointerup is too early), a cancelled
|
|
35
|
+
// touch, or any key (keyboard use is never mid-press).
|
|
36
|
+
const LATCH_RELEASE_EVENTS = ["mouseup", "pointercancel", "keydown"]
|
|
31
37
|
const POPPER_STRATEGY = "data-poetry--core--popper-strategy-value"
|
|
32
38
|
|
|
33
39
|
export default class HoverCardController extends Controller {
|
|
@@ -49,7 +55,12 @@ export default class HoverCardController extends Controller {
|
|
|
49
55
|
#containSelection = false
|
|
50
56
|
#hasSelection = false
|
|
51
57
|
#previousBodyUserSelect = null
|
|
58
|
+
#isPointerDown = false
|
|
52
59
|
#onPointerup = () => this.#handlePointerup()
|
|
60
|
+
#releaseLatch = () => {
|
|
61
|
+
this.#isPointerDown = false
|
|
62
|
+
for (const type of LATCH_RELEASE_EVENTS) document.removeEventListener(type, this.#releaseLatch)
|
|
63
|
+
}
|
|
53
64
|
|
|
54
65
|
/**
|
|
55
66
|
* Wires the content listeners (portal-safe) and reconciles a
|
|
@@ -93,6 +104,7 @@ export default class HoverCardController extends Controller {
|
|
|
93
104
|
|
|
94
105
|
this.#wired = []
|
|
95
106
|
document.removeEventListener("pointerup", this.#onPointerup)
|
|
107
|
+
this.#releaseLatch()
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
/**
|
|
@@ -152,9 +164,11 @@ export default class HoverCardController extends Controller {
|
|
|
152
164
|
/**
|
|
153
165
|
* The trigger's focus action: opens IMMEDIATELY, skipping the timers -
|
|
154
166
|
* a keyboard user sees the preview even though they
|
|
155
|
-
* cannot enter it.
|
|
167
|
+
* cannot enter it. Focus a pointer caused (the latch) does not open.
|
|
156
168
|
*/
|
|
157
169
|
focusOpen() {
|
|
170
|
+
if (this.#isPointerDown) return
|
|
171
|
+
|
|
158
172
|
this.#clearCloseTimer()
|
|
159
173
|
this.#show()
|
|
160
174
|
}
|
|
@@ -169,15 +183,16 @@ export default class HoverCardController extends Controller {
|
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
/**
|
|
172
|
-
* The trigger's
|
|
173
|
-
*
|
|
174
|
-
* touch would be unreachable)
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
186
|
+
* The trigger's pointerdown action - the latch: the focus a tap or a
|
|
187
|
+
* click causes must not open the card (a pointer user never gets a
|
|
188
|
+
* focus-opened card; on touch it would be unreachable), so focusOpen
|
|
189
|
+
* stays shut until the press has landed (LATCH_RELEASE_EVENTS). The
|
|
190
|
+
* event is never cancelled: cancelling touchstart would cancel the
|
|
191
|
+
* tap's click, and the tap must still navigate the link.
|
|
178
192
|
*/
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
pointerDown() {
|
|
194
|
+
this.#isPointerDown = true
|
|
195
|
+
for (const type of LATCH_RELEASE_EVENTS) document.addEventListener(type, this.#releaseLatch)
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
// --- open / close ---
|
|
@@ -27,6 +27,17 @@ import {
|
|
|
27
27
|
// auto-scroll animation cannot release itself.
|
|
28
28
|
const AUTOSCROLLING_CLEAR_DELAY = 180
|
|
29
29
|
|
|
30
|
+
// The opening-position hold: data-pending-scroll sits on the root and the
|
|
31
|
+
// viewport until defaultScrollPosition end / last-anchor is applied (or
|
|
32
|
+
// skipped, on an empty transcript), so the dictionary can hide the viewport
|
|
33
|
+
// instead of painting the top of the thread and then jumping. The server
|
|
34
|
+
// renders the attribute for those two positions (a scroll container always
|
|
35
|
+
// opens at the top, and the first paint must not wait for this controller);
|
|
36
|
+
// from connect on the controller owns it. Mount-only: a live value change
|
|
37
|
+
// re-applies the position without hiding the viewport again.
|
|
38
|
+
const PENDING_SCROLL_ATTRIBUTE = "data-pending-scroll"
|
|
39
|
+
const HELD_SCROLL_POSITIONS = new Set(["end", "last-anchor"])
|
|
40
|
+
|
|
30
41
|
// Viewport keys that count as deliberate scroll intent and release follow.
|
|
31
42
|
const USER_SCROLL_KEYS = new Set([
|
|
32
43
|
"ArrowDown",
|
|
@@ -56,6 +67,10 @@ const USER_SCROLL_KEYS = new Set([
|
|
|
56
67
|
// anchored-to-message a turn held at the reading line while a reply streams
|
|
57
68
|
// settling-jump a programmatic jump animating; intent suppressed
|
|
58
69
|
//
|
|
70
|
+
// Presence attributes mirrored on the root AND the viewport: data-scrollable
|
|
71
|
+
// (which edges have room), data-autoscrolling (a programmatic scroll is
|
|
72
|
+
// settling), data-pending-scroll (the opening-position hold, above).
|
|
73
|
+
//
|
|
59
74
|
// Viewport scroll/wheel/touchmove/keydown listeners are wired here (passive
|
|
60
75
|
// flags need addEventListener) - do NOT also declare them as data-actions.
|
|
61
76
|
// The jump button IS a data-action: click->...#jump.
|
|
@@ -101,6 +116,7 @@ export default class extends Controller {
|
|
|
101
116
|
this.prependRestore = null
|
|
102
117
|
this.pendingScrollToMessage = null
|
|
103
118
|
this.defaultScrollPositionApplied = false
|
|
119
|
+
this.pendingScroll = HELD_SCROLL_POSITIONS.has(this.defaultScrollPositionValue)
|
|
104
120
|
this.spacerHeight = 0
|
|
105
121
|
this.spacerGap = getFlexGap(this.#spacer()?.parentElement ?? null)
|
|
106
122
|
this.handledScrollAnchors = new WeakSet()
|
|
@@ -150,6 +166,16 @@ export default class extends Controller {
|
|
|
150
166
|
// defaultScrollPosition once, commits scrollable state.
|
|
151
167
|
this.#handleContentChange()
|
|
152
168
|
|
|
169
|
+
// The hold releases with the opening position (inside the mount pass),
|
|
170
|
+
// or right away when there is nothing to scroll. A Turbo morph that
|
|
171
|
+
// re-stamps the server's attribute afterwards is stripped by the
|
|
172
|
+
// observer; a cache snapshot re-arms it (see #rearmPendingScroll).
|
|
173
|
+
if (this.itemCount === 0) this.pendingScroll = false
|
|
174
|
+
this.#writePendingScroll()
|
|
175
|
+
this.#observePendingScroll(viewport)
|
|
176
|
+
this.onBeforeCache = () => this.#rearmPendingScroll()
|
|
177
|
+
document.addEventListener("turbo:before-cache", this.onBeforeCache)
|
|
178
|
+
|
|
153
179
|
if (this.trackVisibilityValue) this.#observeVisibility()
|
|
154
180
|
|
|
155
181
|
this.started = true
|
|
@@ -189,6 +215,9 @@ export default class extends Controller {
|
|
|
189
215
|
this.contentResizeObserver = null
|
|
190
216
|
this.visibilityObserver?.disconnect()
|
|
191
217
|
this.visibilityObserver = null
|
|
218
|
+
this.pendingScrollObserver?.disconnect()
|
|
219
|
+
this.pendingScrollObserver = null
|
|
220
|
+
document.removeEventListener("turbo:before-cache", this.onBeforeCache)
|
|
192
221
|
this.observedRows.clear()
|
|
193
222
|
this.visibleMessageIds.clear()
|
|
194
223
|
|
|
@@ -661,7 +690,7 @@ export default class extends Controller {
|
|
|
661
690
|
|
|
662
691
|
if (!handled) return false
|
|
663
692
|
|
|
664
|
-
this
|
|
693
|
+
this.#markDefaultScrollPositionApplied()
|
|
665
694
|
|
|
666
695
|
return true
|
|
667
696
|
}
|
|
@@ -708,6 +737,55 @@ export default class extends Controller {
|
|
|
708
737
|
return true
|
|
709
738
|
}
|
|
710
739
|
|
|
740
|
+
// --- the opening-position hold ---
|
|
741
|
+
|
|
742
|
+
#markDefaultScrollPositionApplied() {
|
|
743
|
+
this.defaultScrollPositionApplied = true
|
|
744
|
+
|
|
745
|
+
if (!this.pendingScroll) return
|
|
746
|
+
|
|
747
|
+
this.pendingScroll = false
|
|
748
|
+
this.#writePendingScroll()
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#writePendingScroll() {
|
|
752
|
+
for (const element of [this.element, this.#viewport()]) {
|
|
753
|
+
if (!element) continue
|
|
754
|
+
|
|
755
|
+
if (this.pendingScroll) element.setAttribute(PENDING_SCROLL_ATTRIBUTE, "")
|
|
756
|
+
else element.removeAttribute(PENDING_SCROLL_ATTRIBUTE)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A Turbo morph re-stamps the server-rendered attribute onto the live
|
|
761
|
+
// elements after the position already applied: strip it. Never re-add
|
|
762
|
+
// through this path - an inline script that scrolled and released the
|
|
763
|
+
// hold before connect must keep its release.
|
|
764
|
+
#observePendingScroll(viewport) {
|
|
765
|
+
if (typeof MutationObserver === "undefined") return
|
|
766
|
+
|
|
767
|
+
this.pendingScrollObserver = new MutationObserver(() => {
|
|
768
|
+
if (!this.pendingScroll) this.#writePendingScroll()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
for (const element of [this.element, viewport]) {
|
|
772
|
+
this.pendingScrollObserver.observe(element, {
|
|
773
|
+
attributes: true,
|
|
774
|
+
attributeFilter: [PENDING_SCROLL_ATTRIBUTE]
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// A cached snapshot clones the elements without their scroll position, so
|
|
780
|
+
// a restore would paint the top of the thread until reconnect re-applies
|
|
781
|
+
// the opening position: re-arm the hold before Turbo caches the page.
|
|
782
|
+
#rearmPendingScroll() {
|
|
783
|
+
if (!HELD_SCROLL_POSITIONS.has(this.defaultScrollPositionValue)) return
|
|
784
|
+
|
|
785
|
+
this.pendingScroll = true
|
|
786
|
+
this.#writePendingScroll()
|
|
787
|
+
}
|
|
788
|
+
|
|
711
789
|
// --- scroll commands (mechanics, split from the policy above as in source) ---
|
|
712
790
|
|
|
713
791
|
#setAutoScrolling(autoscrolling) {
|
|
@@ -878,7 +956,7 @@ export default class extends Controller {
|
|
|
878
956
|
if (!element) {
|
|
879
957
|
if (this.itemCount === 0) {
|
|
880
958
|
this.pendingScrollToMessage = { messageId, options }
|
|
881
|
-
this
|
|
959
|
+
this.#markDefaultScrollPositionApplied()
|
|
882
960
|
|
|
883
961
|
return true
|
|
884
962
|
}
|
|
@@ -886,7 +964,7 @@ export default class extends Controller {
|
|
|
886
964
|
return false
|
|
887
965
|
}
|
|
888
966
|
|
|
889
|
-
this
|
|
967
|
+
this.#markDefaultScrollPositionApplied()
|
|
890
968
|
|
|
891
969
|
if (this.#scrollToElement(element, options)) {
|
|
892
970
|
this.pendingScrollToMessage = null
|
|
@@ -910,7 +988,7 @@ export default class extends Controller {
|
|
|
910
988
|
if (!this.#scrollToElement(element, pending.options)) return false
|
|
911
989
|
|
|
912
990
|
this.pendingScrollToMessage = null
|
|
913
|
-
this
|
|
991
|
+
this.#markDefaultScrollPositionApplied()
|
|
914
992
|
|
|
915
993
|
return true
|
|
916
994
|
}
|
|
@@ -44,6 +44,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
44
44
|
#cancelExit = new Map() // value -> abandon-this-panel's-exit (per panel, not global)
|
|
45
45
|
#onOutsidePress = null
|
|
46
46
|
#unsubscribeBeforeCache = null
|
|
47
|
+
#itemsObserver = null
|
|
47
48
|
#sizeGeneration = 0
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -62,6 +63,16 @@ export default class NavigationMenuController extends Controller {
|
|
|
62
63
|
this.#close()
|
|
63
64
|
flushPendingExits()
|
|
64
65
|
})
|
|
66
|
+
|
|
67
|
+
// A morph or stream that removes the OPEN trigger's item takes the
|
|
68
|
+
// per-item panel with it but leaves an adopted viewport panel behind,
|
|
69
|
+
// and either way the bar still counts itself open: close as if the
|
|
70
|
+
// trigger had been dismissed, and drop any adopted panel no trigger
|
|
71
|
+
// owns any more.
|
|
72
|
+
if (typeof MutationObserver !== "undefined") {
|
|
73
|
+
this.#itemsObserver = new MutationObserver(() => this.#reconcileRemovedItems())
|
|
74
|
+
this.#itemsObserver.observe(this.element, { childList: true, subtree: true })
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
/**
|
|
@@ -73,6 +84,8 @@ export default class NavigationMenuController extends Controller {
|
|
|
73
84
|
this.#unbindOutsidePress()
|
|
74
85
|
this.#unsubscribeBeforeCache?.()
|
|
75
86
|
this.#unsubscribeBeforeCache = null
|
|
87
|
+
this.#itemsObserver?.disconnect()
|
|
88
|
+
this.#itemsObserver = null
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
/**
|
|
@@ -82,7 +95,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
82
95
|
*/
|
|
83
96
|
toggle(event) {
|
|
84
97
|
const value = this.#valueFrom(event)
|
|
85
|
-
if (value === null) return
|
|
98
|
+
if (value === null || this.#disabled(value)) return
|
|
86
99
|
|
|
87
100
|
this.#clearTimer()
|
|
88
101
|
if (this.#openValue === value) this.#close()
|
|
@@ -101,7 +114,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
101
114
|
if (event.pointerType === "touch") return // touch is click's job
|
|
102
115
|
|
|
103
116
|
const value = this.#valueFrom(event)
|
|
104
|
-
if (value === null) return
|
|
117
|
+
if (value === null || this.#disabled(value)) return
|
|
105
118
|
if (value === this.#openValue) {
|
|
106
119
|
this.#clearTimer() // re-entering the open item cancels a pending close
|
|
107
120
|
return
|
|
@@ -152,6 +165,20 @@ export default class NavigationMenuController extends Controller {
|
|
|
152
165
|
return
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
// ArrowDown on a trigger opens its panel and is consumed; focus stays
|
|
169
|
+
// on the trigger (Tab enters the panel). A top-level link is not a
|
|
170
|
+
// trigger and keeps the key.
|
|
171
|
+
if (event.key === "ArrowDown") {
|
|
172
|
+
const trigger = event.target instanceof Element ? event.target.closest(TRIGGER_SELECTOR) : null
|
|
173
|
+
const value = this.#valueFrom(event)
|
|
174
|
+
if (!trigger || value === null || this.#disabled(value)) return
|
|
175
|
+
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
this.#clearTimer()
|
|
178
|
+
if (this.#openValue !== value) this.#open(value)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
155
182
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
|
|
156
183
|
|
|
157
184
|
const stops = this.#arrowStops()
|
|
@@ -211,6 +238,28 @@ export default class NavigationMenuController extends Controller {
|
|
|
211
238
|
this.#bindOutsidePress()
|
|
212
239
|
}
|
|
213
240
|
|
|
241
|
+
#reconcileRemovedItems() {
|
|
242
|
+
const viewport = this.#viewport()
|
|
243
|
+
if (viewport) {
|
|
244
|
+
for (const panel of viewport.querySelectorAll("[data-viewport-panel]")) {
|
|
245
|
+
if (!this.#ownerOf(panel)) panel.remove()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (this.#openValue === null || this.#triggerFor(this.#openValue)) return
|
|
249
|
+
|
|
250
|
+
this.#clearTimer()
|
|
251
|
+
this.#close()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// The trigger whose aria-controls names this adopted panel, if it still
|
|
255
|
+
// exists.
|
|
256
|
+
#ownerOf(panel) {
|
|
257
|
+
if (!panel.id) return null
|
|
258
|
+
|
|
259
|
+
return [...this.element.querySelectorAll(TRIGGER_SELECTOR)]
|
|
260
|
+
.find((trigger) => trigger.getAttribute("aria-controls") === panel.id) ?? null
|
|
261
|
+
}
|
|
262
|
+
|
|
214
263
|
#close() {
|
|
215
264
|
if (this.#openValue === null) return
|
|
216
265
|
|
|
@@ -434,6 +483,12 @@ export default class NavigationMenuController extends Controller {
|
|
|
434
483
|
return this.#itemFor(value)?.querySelector(TRIGGER_SELECTOR) ?? null
|
|
435
484
|
}
|
|
436
485
|
|
|
486
|
+
// A disabled trigger (native disabled, data-disabled) never opens its
|
|
487
|
+
// panel - not by click, not by hover - and the arrows step over it.
|
|
488
|
+
#disabled(value) {
|
|
489
|
+
return this.#triggerFor(value)?.disabled === true
|
|
490
|
+
}
|
|
491
|
+
|
|
437
492
|
// An adopted panel (viewport mode) no longer lives inside its item -
|
|
438
493
|
// the trigger's aria-controls id finds it wherever it moved.
|
|
439
494
|
#panelFor(value) {
|
|
@@ -449,10 +504,10 @@ export default class NavigationMenuController extends Controller {
|
|
|
449
504
|
.find((item) => item.dataset.value === value) ?? null
|
|
450
505
|
}
|
|
451
506
|
|
|
452
|
-
// Arrow stops: every trigger and top-level link in the bar, DOM order.
|
|
507
|
+
// Arrow stops: every enabled trigger and top-level link in the bar, DOM order.
|
|
453
508
|
#arrowStops() {
|
|
454
509
|
return [...this.element.querySelectorAll(
|
|
455
510
|
`${TRIGGER_SELECTOR}, [data-slot="navigation-menu-list"] > ${ITEM_SELECTOR} > a`
|
|
456
|
-
)]
|
|
511
|
+
)].filter((stop) => !stop.disabled)
|
|
457
512
|
}
|
|
458
513
|
}
|
|
@@ -32,7 +32,6 @@ export default class extends Controller {
|
|
|
32
32
|
static events = ["poetry:number-field:change", "poetry:number-field:commit"]
|
|
33
33
|
|
|
34
34
|
#value = null
|
|
35
|
-
#focusedOnce = false
|
|
36
35
|
#holdTimer = null
|
|
37
36
|
#holdInterval = null
|
|
38
37
|
#holdChanged = false
|
|
@@ -119,15 +118,6 @@ export default class extends Controller {
|
|
|
119
118
|
if (parsed !== null) this.#apply(parsed, { display: false })
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/** The focus action: the first focus parks the caret at the end. */
|
|
123
|
-
focus() {
|
|
124
|
-
if (this.#focusedOnce) return
|
|
125
|
-
|
|
126
|
-
this.#focusedOnce = true
|
|
127
|
-
const end = this.inputTarget.value.length
|
|
128
|
-
this.inputTarget.setSelectionRange(end, end)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
/**
|
|
132
122
|
* The blur action - the text commit point: empty clears, unparseable
|
|
133
123
|
* text is left as typed with no commit, parseable text clamps and
|
|
@@ -161,7 +151,7 @@ export default class extends Controller {
|
|
|
161
151
|
if (event.button !== 0 || this.inputTarget.readOnly) return
|
|
162
152
|
|
|
163
153
|
// Mouse focuses the input (touch would pop the software keyboard).
|
|
164
|
-
if (event.pointerType !== "touch") this
|
|
154
|
+
if (event.pointerType !== "touch") this.#focusInput()
|
|
165
155
|
this.#pressTicked = true
|
|
166
156
|
this.#holdChanged = false
|
|
167
157
|
const direction = this.#directionFor(event.currentTarget)
|
|
@@ -224,6 +214,17 @@ export default class extends Controller {
|
|
|
224
214
|
return changed
|
|
225
215
|
}
|
|
226
216
|
|
|
217
|
+
// Programmatic focus (the steppers) parks the caret at the end BEFORE
|
|
218
|
+
// focusing: every engine restores the stored selection on focus, and a
|
|
219
|
+
// host's own focus handler can still choose differently. Keyboard and
|
|
220
|
+
// pointer focus keep the browser's native selection (Tab selects the
|
|
221
|
+
// value, a click places the caret).
|
|
222
|
+
#focusInput() {
|
|
223
|
+
const end = this.inputTarget.value.length
|
|
224
|
+
this.inputTarget.setSelectionRange(end, end)
|
|
225
|
+
this.inputTarget.focus()
|
|
226
|
+
}
|
|
227
|
+
|
|
227
228
|
#directionFor(button) {
|
|
228
229
|
return this.hasIncrementTarget && button === this.incrementTarget ? 1 : -1
|
|
229
230
|
}
|
|
@@ -240,13 +241,22 @@ export default class extends Controller {
|
|
|
240
241
|
this.#apply(candidate, { display: true })
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// The dominant axis decides (a precision touchpad emits sub-pixel
|
|
245
|
+
// cross-axis noise): a sideways gesture is page scroll and passes
|
|
246
|
+
// through untouched, except under Shift, which some browsers deliver
|
|
247
|
+
// on the horizontal axis - there the horizontal delta is the intended
|
|
248
|
+
// one. Positive is "down", stepping the value down.
|
|
243
249
|
#wheel(event) {
|
|
244
250
|
if (event.ctrlKey || document.activeElement !== this.inputTarget) return
|
|
245
251
|
|
|
246
|
-
event.
|
|
247
|
-
if (event.
|
|
252
|
+
const horizontal = Math.abs(event.deltaX) > Math.abs(event.deltaY)
|
|
253
|
+
if (horizontal && !event.shiftKey) return
|
|
254
|
+
|
|
255
|
+
const delta = horizontal ? event.deltaX : event.deltaY
|
|
256
|
+
if (delta === 0) return
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
event.preventDefault()
|
|
259
|
+
this.#step(delta > 0 ? -1 : 1, event)
|
|
250
260
|
this.#commit()
|
|
251
261
|
}
|
|
252
262
|
|
|
@@ -14,7 +14,9 @@ components:
|
|
|
14
14
|
description: The element to render ('div', 'section', 'span', ...); void elements
|
|
15
15
|
(br, hr, img, ...) self-close and take no content block.
|
|
16
16
|
slots: []
|
|
17
|
+
helper: poetry_box
|
|
17
18
|
identity: false
|
|
18
19
|
parts:
|
|
19
20
|
- name: box
|
|
20
21
|
description: The rendered element itself - the whole component is one part
|
|
22
|
+
internal: true
|
|
@@ -595,9 +595,9 @@
|
|
|
595
595
|
"disconnect",
|
|
596
596
|
"focusOpen",
|
|
597
597
|
"openValueChanged",
|
|
598
|
+
"pointerDown",
|
|
598
599
|
"pointerEnter",
|
|
599
|
-
"pointerLeave"
|
|
600
|
-
"touchGuard"
|
|
600
|
+
"pointerLeave"
|
|
601
601
|
],
|
|
602
602
|
"events": [
|
|
603
603
|
"poetry:hover-card:closed",
|
|
@@ -841,7 +841,6 @@
|
|
|
841
841
|
"blur",
|
|
842
842
|
"connect",
|
|
843
843
|
"disconnect",
|
|
844
|
-
"focus",
|
|
845
844
|
"hiddenChanged",
|
|
846
845
|
"input",
|
|
847
846
|
"keydown",
|
|
@@ -68,6 +68,19 @@ module Poetry
|
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
# The gem convention plus every helper the catalog knows by a name
|
|
72
|
+
# of its own (an app component's declared helper), so the
|
|
73
|
+
# heuristics cover app components too.
|
|
74
|
+
def helper_pattern
|
|
75
|
+
@helper_pattern ||= begin
|
|
76
|
+
declared = @catalog.helper_names.reject { |name| name.start_with?("poetry_") }
|
|
77
|
+
# A bare call: not after a dot or a colon (`u.badge`, `Foo::badge`).
|
|
78
|
+
Regexp.union(/(?<![\w.:])(poetry_[a-z0-9_]+)\b/, *declared.map do |name|
|
|
79
|
+
/(?<![\w.:])(#{Regexp.escape(name)})\b/
|
|
80
|
+
end)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
71
84
|
def frame_for(code)
|
|
72
85
|
return :cache if code.match?(CACHE_OPENER)
|
|
73
86
|
return :loop if code.match?(LOOP_OPENER)
|
|
@@ -76,7 +89,7 @@ module Poetry
|
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
def flag_helpers(code, line, stack, findings)
|
|
79
|
-
code.scan(
|
|
92
|
+
code.scan(helper_pattern) do |(helper)|
|
|
80
93
|
next unless @catalog.helper?(helper)
|
|
81
94
|
next if @catalog.identity_free?(helper)
|
|
82
95
|
next if code.match?(IDENTITY_ARG)
|