1import { Controller } from "@hotwired/stimulus" 2 3// Slides a single accent indicator along an expanded nav group's sub-item rail. 4// It rests on the active sub-item and follows the pointer to whichever sub-item 5// is hovered, sliding back to the active one when the pointer leaves the group. 6export default class extends Controller { 7 static targets = ["indicator", "item"] 8 9 // Vertical padding so the bar sits shorter than the item (matches the old 10 // top-1.5 / bottom-1.5 inset). 11 INSET = 6 12 13 connect() { 14 this.currentItem = this.activeItem 15 this.place(this.currentItem, { animate: false }) 16 17 this.itemTargets.forEach((item) => item.addEventListener("mouseenter", this.onEnter)) 18 this.element.addEventListener("mouseleave", this.onLeave) 19 20 this.resizeObserver = new ResizeObserver(() => this.place(this.currentItem, { animate: false })) 21 this.resizeObserver.observe(this.element) 22 } 23 24 disconnect() { 25 this.itemTargets.forEach((item) => item.removeEventListener("mouseenter", this.onEnter)) 26 this.element.removeEventListener("mouseleave", this.onLeave) 27 this.resizeObserver?.disconnect() 28 } 29 30 onEnter = (event) => { 31 this.currentItem = event.currentTarget 32 this.place(this.currentItem, { animate: true }) 33 } 34 35 onLeave = () => { 36 this.currentItem = this.activeItem 37 this.place(this.currentItem, { animate: true }) 38 } 39 40 place(item, { animate }) { 41 if (!this.hasIndicatorTarget) return 42 const el = this.indicatorTarget 43 44 if (!item) { 45 el.style.opacity = "0" 46 return 47 } 48 49 const top = item.offsetTop + this.INSET 50 const height = Math.max(0, item.offsetHeight - this.INSET * 2) 51 const active = item.dataset.navRailActive === "true" 52 53 if (!animate) el.style.transition = "none" 54 el.style.transform = `translateY(${top}px)` 55 el.style.height = `${height}px` 56 el.style.backgroundColor = active ? "var(--zw-blue-700)" : "var(--zw-blue-500)" 57 el.style.opacity = "1" 58 if (!animate) { 59 el.offsetHeight // force reflow so the resumed transition doesn't animate the initial placement 60 el.style.transition = "" 61 } 62 } 63 64 get activeItem() { 65 return this.itemTargets.find((item) => item.dataset.navRailActive === "true") || null 66 } 67}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.