1import { Controller } from '@hotwired/stimulus'; 2 3/** 4 * VA-drawer â the in-page "mini VA" (VA_MOUNT_SPEC D6). 5 * 6 * Lives once per page (templates/_partials/va_drawer.html.twig, included from 7 * base.html.twig only when the tenant has the va_mounts flag). On first open it 8 * creates a same-origin iframe of /workspace/embed, so the heavy va-chat 9 * controller only loads when someone actually asks for it. 10 * 11 * On wide screens the drawer DOCKS: the page is pushed left by the drawer's width 12 * (body padding) so it becomes a right-hand column of the page instead of covering 13 * it. Below the lg breakpoint there is no room to push, so it overlays. 14 * 15 * Exposes window.openVaDrawer(opts) â the contract va-launch already calls â so 16 * existing "Create with AI" / "Audit with AI" buttons open in-page instead of a 17 * new tab. opts: { mount?: string, entityId?: string|number }. 18 */ 19export default class extends Controller { 20 static targets = ['panel', 'frameHost']; 21 22 connect() { 23 this._open = (opts = {}) => this.open(opts); 24 window.openVaDrawer = this._open; 25 this._onKey = (e) => { if (e.key === 'Escape') this.close(); }; 26 this._onResize = () => { if (this._isOpen()) this._dock(); }; 27 document.addEventListener('keydown', this._onKey); 28 window.addEventListener('resize', this._onResize); 29 } 30 31 disconnect() { 32 if (window.openVaDrawer === this._open) delete window.openVaDrawer; 33 document.removeEventListener('keydown', this._onKey); 34 window.removeEventListener('resize', this._onResize); 35 this._undock(); 36 } 37 38 open(opts = {}) { 39 const params = new URLSearchParams(); 40 if (opts.mount) params.set('mount', String(opts.mount)); 41 if (opts.entityId !== undefined && opts.entityId !== null && opts.entityId !== '') { 42 params.set('entity_id', String(opts.entityId)); 43 } 44 const qs = params.toString(); 45 const src = '/workspace/embed' + (qs ? `?${qs}` : ''); 46 47 let frame = this.frameHostTarget.querySelector('iframe'); 48 if (!frame) { 49 frame = document.createElement('iframe'); 50 frame.className = 'w-full h-full border-0'; 51 frame.title = 'Workplace VA'; 52 this.frameHostTarget.appendChild(frame); 53 } 54 // Reload only when the thread changes; re-opening the same one keeps its state. 55 if (frame.dataset.src !== src) { 56 frame.dataset.src = src; 57 frame.src = src; 58 } else if (!opts.mount) { 59 // va-launch may have queued a new prefill prompt for the main thread. 60 frame.src = src; 61 } 62 63 this.panelTarget.classList.remove('hidden'); 64 this.panelTarget.setAttribute('aria-hidden', 'false'); 65 this._dock(); 66 } 67 68 close() { 69 this.panelTarget.classList.add('hidden'); 70 this.panelTarget.setAttribute('aria-hidden', 'true'); 71 this._undock(); 72 } 73 74 _isOpen() { 75 return !this.panelTarget.classList.contains('hidden'); 76 } 77 78 /** Push the page left by the drawer's width when there is room (lg+). */ 79 _dock() { 80 if (!window.matchMedia('(min-width: 1024px)').matches) { 81 this._undock(); 82 return; 83 } 84 document.body.style.transition = 'padding-right 150ms ease-out'; 85 document.body.style.paddingRight = `${this.panelTarget.offsetWidth}px`; 86 } 87 88 _undock() { 89 document.body.style.paddingRight = ''; 90 } 91}
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.