1import { Controller } from "@hotwired/stimulus" 2 3// Accordion controller for collapsible sections 4// Persists open/closed state to localStorage 5export default class extends Controller { 6 static targets = ["content", "icon"] 7 static values = { 8 id: String, 9 defaultOpen: { type: Boolean, default: true } 10 } 11 12 connect() { 13 // Restore state from localStorage or use default 14 const savedState = this.getSavedState() 15 if (savedState !== null) { 16 this.isOpen = savedState 17 } else { 18 this.isOpen = this.defaultOpenValue 19 } 20 21 // Apply initial state without animation 22 this.applyState(false) 23 } 24 25 toggle() { 26 this.isOpen = !this.isOpen 27 this.applyState(true) 28 this.saveState() 29 } 30 31 expand() { 32 if (!this.isOpen) { 33 this.isOpen = true 34 this.applyState(true) 35 this.saveState() 36 } 37 } 38 39 collapse() { 40 if (this.isOpen) { 41 this.isOpen = false 42 this.applyState(true) 43 this.saveState() 44 } 45 } 46 47 applyState(animate) { 48 if (!this.hasContentTarget) return 49 50 const content = this.contentTarget 51 52 if (animate) { 53 content.classList.add("transition-all", "duration-200", "ease-in-out") 54 } else { 55 content.classList.remove("transition-all", "duration-200", "ease-in-out") 56 } 57 58 if (this.isOpen) { 59 // Expand 60 content.style.maxHeight = content.scrollHeight + "px" 61 content.classList.remove("overflow-hidden") 62 63 // After transition, remove max-height to allow dynamic content 64 if (animate) { 65 setTimeout(() => { 66 content.style.maxHeight = "none" 67 }, 200) 68 } else { 69 content.style.maxHeight = "none" 70 } 71 } else { 72 // Collapse - first set explicit height, then animate to 0 73 if (animate) { 74 content.style.maxHeight = content.scrollHeight + "px" 75 content.classList.add("overflow-hidden") 76 // Force reflow 77 content.offsetHeight 78 content.style.maxHeight = "0px" 79 } else { 80 content.classList.add("overflow-hidden") 81 content.style.maxHeight = "0px" 82 } 83 } 84 85 // Rotate icon 86 if (this.hasIconTarget) { 87 if (this.isOpen) { 88 this.iconTarget.classList.remove("rotate-[-90deg]") 89 } else { 90 this.iconTarget.classList.add("rotate-[-90deg]") 91 } 92 } 93 } 94 95 getSavedState() { 96 if (!this.idValue) return null 97 const key = `accordion-${this.idValue}` 98 const saved = localStorage.getItem(key) 99 if (saved === null) return null 100 return saved === "true" 101 } 102 103 saveState() { 104 if (!this.idValue) return 105 const key = `accordion-${this.idValue}` 106 localStorage.setItem(key, this.isOpen.toString()) 107 } 108};
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.