V13-FE-006: consolidate approved UI and contract hardening
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s

This commit is contained in:
2026-08-13 02:41:00 +09:00
parent d79edae546
commit 3f293d8aa8
1278 changed files with 14384 additions and 1664 deletions
@@ -0,0 +1,52 @@
import type { KbxShortcutDefinition, KbxShortcutScope } from '@kbx/contracts'
const scopeRank:Record<KbxShortcutScope,number>={application:1,page:2,grid:3,dialog:4,editor:5}
const protectedBrowserShortcuts=new Set(['F5','CTRL+L','CTRL+T','CTRL+W','CTRL+R'])
let nextId=1
const registrations=new Map<number,KbxShortcutDefinition[]>()
let listening=false
function normalizeConfiguredKey(value:string){return value.trim().replace(/\s+/g,'').toUpperCase()}
function eventKey(event:KeyboardEvent){
const parts:string[]=[]
if(event.ctrlKey||event.metaKey)parts.push('CTRL')
if(event.altKey)parts.push('ALT')
if(event.shiftKey)parts.push('SHIFT')
const key=event.key===' '?'SPACE':event.key.toUpperCase()
parts.push(key==='ESC'?'ESCAPE':key)
return parts.join('+')
}
function isTextEditingTarget(target:EventTarget|null){
const el=target instanceof HTMLElement?target:null
if(!el)return false
const tag=el.tagName.toLowerCase()
return tag==='input'||tag==='textarea'||tag==='select'||el.isContentEditable
}
function candidates(event:KeyboardEvent){
const key=eventKey(event)
return [...registrations.values()].flat().filter(x=>normalizeConfiguredKey(x.key)===key&&x.enabled?.()!==false)
.sort((a,b)=>(scopeRank[b.scope]-scopeRank[a.scope])||((b.priority??0)-(a.priority??0)))
}
function handle(event:KeyboardEvent){
const key=eventKey(event)
if(protectedBrowserShortcuts.has(key))return
const matches=candidates(event)
if(!matches.length)return
const editing=isTextEditingTarget(event.target)
const match=matches.find(x=>!editing||x.scope==='editor'||!['ENTER','SPACE','ARROWUP','ARROWDOWN','ARROWLEFT','ARROWRIGHT'].includes(key))
if(!match)return
event.preventDefault();void match.execute()
}
function ensureListener(){if(listening||typeof window==='undefined')return;window.addEventListener('keydown',handle);listening=true}
function releaseListener(){if(!listening||registrations.size||typeof window==='undefined')return;window.removeEventListener('keydown',handle);listening=false}
export function registerKbxShortcuts(shortcuts:KbxShortcutDefinition[]){
for(const shortcut of shortcuts){
const key=normalizeConfiguredKey(shortcut.key)
if(protectedBrowserShortcuts.has(key))throw new Error(`KBX browser shortcut is protected: ${shortcut.key}`)
}
const id=nextId++;registrations.set(id,shortcuts);ensureListener()
return ()=>{registrations.delete(id);releaseListener()}
}
export function getKbxShortcutDebugSnapshot(){return [...registrations.values()].flat().map(x=>({key:x.key,scope:x.scope,priority:x.priority??0}))}