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,13 @@
import { computed, ref } from 'vue'
export function useKbxDirtyState() {
const revision = ref(0)
const savedRevision = ref(0)
const dirty = computed(() => revision.value !== savedRevision.value)
function touch() { revision.value++ }
function markSaved() { savedRevision.value = revision.value }
function reset() { revision.value = 0; savedRevision.value = 0 }
return { dirty, touch, markSaved, reset }
}
@@ -0,0 +1,55 @@
import { onBeforeUnmount, ref } from 'vue'
import type { KbxImportProgressEvent } from '@kbx/contracts'
export interface KbxImportProgressConnection {
start(): Promise<void>
stop(): Promise<void>
subscribe(sessionId: string): Promise<void>
unsubscribe(sessionId: string): Promise<void>
onProgress(callback: (event: KbxImportProgressEvent) => void): void
}
/**
* SignalR is intentionally hidden behind this adapter.
* The app shell can provide a @microsoft/signalr implementation without
* coupling KbxExcelImport or a vertical slice to HubConnection directly.
*/
export function useKbxImportProgress(connection: KbxImportProgressConnection | null) {
const connected = ref(false)
const lastEvent = ref<KbxImportProgressEvent | null>(null)
let activeSessionId: string | null = null
if (connection) {
connection.onProgress(event => {
if (!activeSessionId || event.sessionId === activeSessionId)
lastEvent.value = event
})
}
async function connect() {
if (!connection || connected.value) return
await connection.start()
connected.value = true
}
async function watch(sessionId: string) {
if (!connection) return
await connect()
if (activeSessionId && activeSessionId !== sessionId)
await connection.unsubscribe(activeSessionId)
activeSessionId = sessionId
await connection.subscribe(sessionId)
}
async function dispose() {
if (!connection) return
if (activeSessionId) await connection.unsubscribe(activeSessionId)
if (connected.value) await connection.stop()
connected.value = false
activeSessionId = null
}
onBeforeUnmount(() => { void dispose() })
return { connected, lastEvent, watch, dispose }
}
@@ -0,0 +1,19 @@
import { onBeforeUnmount, onMounted } from 'vue'
import type { KbxShortcutDefinition, KbxShortcutScope } from '@kbx/contracts'
import { registerKbxShortcuts } from '../keyboard/manager'
export interface KbxPageShortcut {
key:string
enabled?:()=>boolean
execute:()=>void|Promise<void>
}
export function useKbxShortcuts(shortcuts:KbxShortcutDefinition[]){
let unregister:(()=>void)|undefined
onMounted(()=>{unregister=registerKbxShortcuts(shortcuts)})
onBeforeUnmount(()=>unregister?.())
}
export function useKbxPageShortcuts(shortcuts:KbxPageShortcut[]){
useKbxShortcuts(shortcuts.map(x=>({...x,scope:'page' as KbxShortcutScope})))
}
@@ -0,0 +1,37 @@
import { computed, ref } from 'vue'
import type { KbxProblem, KbxValidationError } from '@kbx/contracts'
export function useKbxValidation() {
const errors = ref<KbxValidationError[]>([])
const fieldErrors = computed(() => {
const result = new Map<string, string>()
for (const error of errors.value) {
if (error.field && !error.rowKey && !result.has(error.field)) result.set(error.field, error.message)
}
return result
})
function fieldError(field: string) {
return fieldErrors.value.get(field)
}
function setErrors(next: KbxValidationError[]) {
errors.value = next
}
function setRowFieldError(rowKey: string, field: string, error?: Omit<KbxValidationError, 'rowKey' | 'field'>) {
errors.value = errors.value.filter(x => !(x.rowKey === rowKey && x.field === field))
if (error) errors.value.push({ rowKey, field, ...error })
}
function applyProblem(problem: KbxProblem) {
errors.value = problem.type === 'validation' ? problem.errors : []
}
function clear() {
errors.value = []
}
return { errors, fieldError, setErrors, setRowFieldError, applyProblem, clear }
}