feat: strengthen backup and mobile workflows
ci / gitleaks (push) Successful in 1m19s
ci / docker (push) Successful in 5m48s

This commit is contained in:
2026-09-16 21:12:52 +08:00
parent 6f38190c92
commit 6c234d7d82
64 changed files with 5059 additions and 635 deletions
@@ -0,0 +1,82 @@
import { nextTick } from 'vue'
type OverlayEntry = {
id: symbol
close: () => void
busy: () => boolean
restoreFocus: HTMLElement | null
focusPanel: () => void
}
const stack: OverlayEntry[] = []
const background = new Map<HTMLElement, { inert: boolean; ariaHidden: string | null }>()
let listening = false
function root() {
let element = document.getElementById('overlay-root')
if (!element) {
element = document.createElement('div')
element.id = 'overlay-root'
document.body.appendChild(element)
}
return element
}
function syncBackground() {
const overlayRoot = root()
if (stack.length) {
for (const child of Array.from(document.body.children)) {
if (!(child instanceof HTMLElement) || child === overlayRoot || background.has(child)) continue
background.set(child, { inert: child.hasAttribute('inert'), ariaHidden: child.getAttribute('aria-hidden') })
child.setAttribute('inert', '')
child.setAttribute('aria-hidden', 'true')
}
return
}
for (const [element, state] of background) {
if (!state.inert) element.removeAttribute('inert')
if (state.ariaHidden === null) element.removeAttribute('aria-hidden')
else element.setAttribute('aria-hidden', state.ariaHidden)
}
background.clear()
}
function onKeydown(event: KeyboardEvent) {
if (event.key !== 'Escape' || event.defaultPrevented) return
const entry = stack.at(-1)
if (!entry || entry.busy()) return
event.preventDefault()
entry.close()
}
export function overlayRoot() { return root() }
export function pushOverlay(close: () => void, busy: () => boolean, focusPanel: () => void) {
const entry: OverlayEntry = {
id: Symbol('overlay'), close, busy, focusPanel,
restoreFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null,
}
stack.push(entry)
if (!listening) { document.addEventListener('keydown', onKeydown); listening = true }
syncBackground()
return entry.id
}
export function popOverlay(id: symbol) {
const index = stack.findIndex((entry) => entry.id === id)
if (index < 0) return
const wasTop = index === stack.length - 1
const [entry] = stack.splice(index, 1)
if (!stack.length && listening) { document.removeEventListener('keydown', onKeydown); listening = false }
syncBackground()
if (!wasTop) return
const newTop = stack.at(-1)
void nextTick(() => {
if (newTop) newTop.focusPanel()
else if (entry.restoreFocus?.isConnected) entry.restoreFocus.focus()
})
}
export function isTopOverlay(id: symbol | null) {
return Boolean(id && stack.at(-1)?.id === id)
}