Compare commits
	
		
			2 commits
		
	
	
		
			
				8b2bc9bb1c
			
			...
			
				fc85729374
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fc85729374 | |||
| 0450ec9e1c | 
					 3 changed files with 98 additions and 37 deletions
				
			
		
							
								
								
									
										22
									
								
								index.html
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								index.html
									
										
									
									
									
								
							|  | @ -40,6 +40,28 @@ | |||
|     <template id="tpl-search"> | ||||
|       <div class="ux-view fade"> | ||||
|         <div class="box"> | ||||
|           <header class="level mb-4"> | ||||
|             <div class="level-left"> | ||||
|               <div class="level-item"> | ||||
|                 <h2 class="title is-4 mb-0">Search</h2> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="level-right"> | ||||
|               <div class="level-item"> | ||||
|                 <button class="button is-text" type="button" data-action="back">Back</button> | ||||
|               </div> | ||||
|               <div class="level-item"> | ||||
|                 <div class="field is-grouped is-grouped-right is-align-items-center"> | ||||
|                   <div class="control"> | ||||
|                     <span class="is-size-7 has-text-grey mr-2">Sort</span> | ||||
|                   </div> | ||||
|                   <div class="control"> | ||||
|                     <div class="buttons has-addons is-small" role="group" aria-label="Sort results" data-ref="sort-buttons"></div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </header> | ||||
|           <form class="mb-4" role="search" aria-label="Track search" data-ref="form"> | ||||
|             <div class="field"> | ||||
|               <label class="label" for="q">Search</label> | ||||
|  |  | |||
							
								
								
									
										112
									
								
								script.js
									
										
									
									
									
								
							
							
						
						
									
										112
									
								
								script.js
									
										
									
									
									
								
							|  | @ -59,15 +59,68 @@ | |||
|   // UX Manager
 | ||||
|   const UX = (() => { | ||||
|     let currentBase = null; // { view, el }
 | ||||
|     const overlayStack = []; // [{ view, el, lastFocus }]
 | ||||
|     const overlayStack = []; // [{ view, el, lastFocus, isHidden, isClosing }]
 | ||||
| 
 | ||||
|     function ensureViewEl(view) { | ||||
|       if (!view || !view.el) throw new Error('Invalid view'); | ||||
|       view.el.classList.add('ux-view'); | ||||
|       if (view.kind) view.el.dataset.uxKind = view.kind; | ||||
|       return view.el; | ||||
|     } | ||||
| 
 | ||||
|     function assertKind(view, expected) { | ||||
|       if (!view) throw new Error('Missing view'); | ||||
|       if (view.kind && view.kind !== expected) { | ||||
|         throw new Error(`Expected view kind "${expected}" but received "${view.kind}"`); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     async function closeAllOverlays({ restoreFocus = false } = {}) { | ||||
|       while (overlayStack.length) { | ||||
|         const shouldRestore = restoreFocus && overlayStack.length === 1; | ||||
|         await closeTop({ restoreFocus: shouldRestore }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     function syncOverlayVisibility() { | ||||
|       const topIndex = overlayStack.length - 1; | ||||
|       overlayStack.forEach((entry, idx) => { | ||||
|         const { el, view } = entry; | ||||
|         const shouldHide = idx !== topIndex; | ||||
|         if (shouldHide) { | ||||
|           if (!el.hasAttribute('hidden')) el.setAttribute('hidden', ''); | ||||
|           el.setAttribute('aria-hidden', 'true'); | ||||
|           el.setAttribute('inert', ''); | ||||
|           el.inert = true; | ||||
|           if (!entry.isHidden) { | ||||
|             entry.isHidden = true; | ||||
|             if (view && typeof view.onHide === 'function') view.onHide(); | ||||
|           } | ||||
|         } else { | ||||
|           if (el.hasAttribute('hidden')) el.removeAttribute('hidden'); | ||||
|           el.removeAttribute('aria-hidden'); | ||||
|           el.removeAttribute('inert'); | ||||
|           el.inert = false; | ||||
|           entry.isHidden = false; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if (currentBase && currentBase.el) { | ||||
|         if (overlayStack.length > 0) { | ||||
|           currentBase.el.setAttribute('aria-hidden', 'true'); | ||||
|           currentBase.el.setAttribute('inert', ''); | ||||
|           currentBase.el.inert = true; | ||||
|         } else { | ||||
|           currentBase.el.removeAttribute('aria-hidden'); | ||||
|           currentBase.el.removeAttribute('inert'); | ||||
|           currentBase.el.inert = false; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     async function replace(view) { | ||||
|       assertKind(view, 'base'); | ||||
|       await closeAllOverlays({ restoreFocus: false }); | ||||
|       const el = ensureViewEl(view); | ||||
|       const prev = currentBase; | ||||
|       if (prev) { | ||||
|  | @ -80,30 +133,40 @@ | |||
|       } | ||||
|       $uxRoot.appendChild(el); | ||||
|       currentBase = { view, el }; | ||||
|       syncOverlayVisibility(); | ||||
|       await fadeIn(el); | ||||
|       if (typeof view.onShow === 'function') view.onShow(); | ||||
|     } | ||||
| 
 | ||||
|     async function openOverlay(view) { | ||||
|       assertKind(view, 'overlay'); | ||||
|       const el = ensureViewEl(view); | ||||
|       el.classList.add('ux-view--overlay'); | ||||
|       const lastFocus = document.activeElement; | ||||
|       const prevEntry = overlayStack.length ? overlayStack[overlayStack.length - 1] : null; | ||||
|       const entry = { view, el, lastFocus, isHidden: false, isClosing: false, prev: prevEntry }; | ||||
|       overlayStack.push(entry); | ||||
|       $uxOverlays.appendChild(el); | ||||
|       syncOverlayVisibility(); | ||||
|       await fadeIn(el); | ||||
|       overlayStack.push({ view, el, lastFocus }); | ||||
|       $uxRoot.setAttribute('aria-hidden', 'true'); | ||||
|       if (typeof view.onShow === 'function') view.onShow(); | ||||
|     } | ||||
| 
 | ||||
|     async function closeTop() { | ||||
|       const top = overlayStack.pop(); | ||||
|     async function closeTop({ restoreFocus = true } = {}) { | ||||
|       const top = overlayStack[overlayStack.length - 1]; | ||||
|       if (!top) return; | ||||
|       if (top.isClosing) return; | ||||
|       top.isClosing = true; | ||||
|       const { view, el, lastFocus } = top; | ||||
|       if (view && typeof view.onHide === 'function') view.onHide(); | ||||
|       await fadeOut(el); | ||||
|       if (el.parentNode) el.parentNode.removeChild(el); | ||||
|       if (typeof view.destroy === 'function') view.destroy(); | ||||
|       if (overlayStack.length === 0) $uxRoot.removeAttribute('aria-hidden'); | ||||
|       if (lastFocus && typeof lastFocus.focus === 'function') lastFocus.focus(); | ||||
|       overlayStack.pop(); | ||||
|       syncOverlayVisibility(); | ||||
|       if (restoreFocus && lastFocus && typeof lastFocus.focus === 'function') { | ||||
|         lastFocus.focus(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     window.addEventListener('keydown', (e) => { | ||||
|  | @ -113,7 +176,7 @@ | |||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return { replace, openOverlay, closeTop }; | ||||
|     return { replace, openOverlay, closeTop, closeAllOverlays }; | ||||
|   })(); | ||||
| 
 | ||||
|   // --- IndexedDB helpers (minimal, no external deps) ---
 | ||||
|  | @ -679,35 +742,8 @@ | |||
|     const $form = el.querySelector('[data-ref="form"]'); | ||||
|     const $q = el.querySelector('[data-ref="q"]'); | ||||
|     const $results = el.querySelector('[data-ref="results"]'); | ||||
| 
 | ||||
|     // Back + sort toolbar keeps controls grouped for accessibility.
 | ||||
|     const $toolbar = document.createElement('div'); | ||||
|     $toolbar.className = 'level mb-4 is-mobile'; | ||||
|     const $toolbarLeft = document.createElement('div'); | ||||
|     $toolbarLeft.className = 'level-left'; | ||||
|     const $toolbarLeftItem = document.createElement('div'); | ||||
|     $toolbarLeftItem.className = 'level-item'; | ||||
|     const $backBtn = document.createElement('button'); | ||||
|     $backBtn.type = 'button'; | ||||
|     $backBtn.className = 'button is-small is-text'; | ||||
|     $backBtn.textContent = 'Back to menu'; | ||||
|     $toolbarLeftItem.appendChild($backBtn); | ||||
|     $toolbarLeft.appendChild($toolbarLeftItem); | ||||
|     const $toolbarRight = document.createElement('div'); | ||||
|     $toolbarRight.className = 'level-right'; | ||||
|     const $toolbarRightItem = document.createElement('div'); | ||||
|     $toolbarRightItem.className = 'level-item'; | ||||
|     const $sortLabel = document.createElement('span'); | ||||
|     $sortLabel.className = 'is-size-7 has-text-grey mr-2'; | ||||
|     $sortLabel.textContent = 'Sort'; | ||||
|     const $sortButtons = document.createElement('div'); | ||||
|     $sortButtons.className = 'buttons has-addons is-small'; | ||||
|     $toolbarRightItem.appendChild($sortLabel); | ||||
|     $toolbarRightItem.appendChild($sortButtons); | ||||
|     $toolbarRight.appendChild($toolbarRightItem); | ||||
|     $toolbar.appendChild($toolbarLeft); | ||||
|     $toolbar.appendChild($toolbarRight); | ||||
|     $form.insertAdjacentElement('afterend', $toolbar); | ||||
|     const $backBtn = el.querySelector('[data-action="back"]'); | ||||
|     const $sortButtons = el.querySelector('[data-ref="sort-buttons"]'); | ||||
| 
 | ||||
|     const $status = document.createElement('p'); | ||||
|     $status.className = 'has-text-grey'; | ||||
|  | @ -753,6 +789,8 @@ | |||
|       { key: 'alpha', label: 'A–Z' }, | ||||
|     ]; | ||||
| 
 | ||||
|     if (!$backBtn) throw new Error('Missing back button'); | ||||
|     if (!$sortButtons) throw new Error('Missing sort container'); | ||||
|     sortOptions.forEach((opt) => { | ||||
|       const btn = document.createElement('button'); | ||||
|       btn.type = 'button'; | ||||
|  |  | |||
							
								
								
									
										1
									
								
								site.css
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								site.css
									
										
									
									
									
								
							|  | @ -11,6 +11,7 @@ | |||
| .ux-overlays { position: absolute; inset: 0; pointer-events: none; } | ||||
| .ux-view { width: 100%; } | ||||
| .ux-view--overlay { position: absolute; inset: 0; z-index: 10; pointer-events: auto; } | ||||
| .ux-view[hidden] { display: none !important; } | ||||
| 
 | ||||
| .is-sr-only { | ||||
|   position: absolute; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue