Files
T
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

219 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { KbxHomeAttentionItem, KbxHomeLaunchItem, KbxNavigationResolvedEntry, KbxOperationRun, KbxRecentNavigation, KbxUserNotification, KbxWorkspaceTab } from '@kbx/contracts'
import { buildKbxHomeAttentionQueue, buildKbxHomeWorkbench } from './homeNavigation'
const props=withDefaults(defineProps<{
entries:KbxNavigationResolvedEntry[]
favorites?:KbxNavigationResolvedEntry[]
recents?:KbxRecentNavigation[]
tabs?:KbxWorkspaceTab[]
operationCount?:number
operationFailureCount?:number
notificationCount?:number
urgentNotificationCount?:number
operations?:KbxOperationRun[]
notifications?:KbxUserNotification[]
}>(),{favorites:()=>[],recents:()=>[],tabs:()=>[],operationCount:0,operationFailureCount:0,notificationCount:0,urgentNotificationCount:0,operations:()=>[],notifications:()=>[]})
const emit=defineEmits<{
open:[entry:KbxNavigationResolvedEntry]
launch:[item:KbxHomeLaunchItem]
attention:[item:KbxHomeAttentionItem]
menuSearch:[]
operations:[]
notifications:[]
toggleFavorite:[screenId:string]
}>()
const moduleOrder=['OMS','ERP','WMS','COMMON'] as const
const moduleLabel:Record<string,string>={OMS:'주문·채널',ERP:'기준정보·구매·재고',WMS:'물류 작업',COMMON:'공통 운영'}
const selectedModule=ref<string>('ALL')
const launchableEntries=computed(()=>props.entries.filter(entry=>entry.menuVisible!==false))
const launchableScreenCount=computed(()=>launchableEntries.value.length)
const attentionExpanded=ref(false)
const attentionCollapsedLimit=8
const attentionExpandedLimit=24
const attentionToggleLabel=computed(()=>attentionExpanded.value?'상위 8개만 보기':exactAttentionCount.value<=attentionExpandedLimit?`전체 ${exactAttentionCount.value}개 보기`:`우선 ${attentionExpandedLimit}개 보기`)
const attentionOperationCount=computed(()=>(attentionQueue.value.sourceCounts['operation-failed']??0)+(attentionQueue.value.sourceCounts['operation-running']??0))
const attentionNotificationCount=computed(()=>(attentionQueue.value.sourceCounts['notification-urgent']??0)+(attentionQueue.value.sourceCounts.notification??0))
const modules=computed(()=>moduleOrder.map(module=>({module,label:moduleLabel[module],groups:[...new Set(props.entries.filter(x=>x.module===module&&x.menuVisible!==false).map(x=>x.section))].map(section=>({section,entries:props.entries.filter(x=>x.module===module&&x.section===section&&x.menuVisible!==false).sort((a,b)=>(a.order??0)-(b.order??0))}))})).filter(x=>x.groups.some(group=>group.entries.length)))
const visibleModules=computed(()=>selectedModule.value==='ALL'?modules.value:modules.value.filter(x=>x.module===selectedModule.value))
const allAttentionQueue=computed(()=>buildKbxHomeAttentionQueue(props.entries,props.tabs,props.operations,props.notifications,9999))
const moduleSummaries=computed(()=>modules.value.map(item=>{
const screenIds=new Set(item.groups.flatMap(group=>group.entries.map(entry=>entry.screenId)))
return {
module:item.module,
label:item.label,
screenCount:screenIds.size,
favoriteCount:props.favorites.filter(entry=>entry.module===item.module&&screenIds.has(entry.screenId)).length,
openCount:props.tabs.filter(tab=>screenIds.has(tab.screenId)).length,
attentionCount:allAttentionQueue.value.items.filter(attention=>attention.screenId&&screenIds.has(attention.screenId)).length,
}
}))
const workbench=computed(()=>buildKbxHomeWorkbench(props.entries,props.favorites,props.recents,props.tabs,10))
const attentionQueue=computed(()=>buildKbxHomeAttentionQueue(props.entries,props.tabs,props.operations,props.notifications,attentionExpanded.value?attentionExpandedLimit:attentionCollapsedLimit))
const attentionItems=computed(()=>attentionQueue.value.items)
const exactAttentionCount=computed(()=>attentionQueue.value.totalCount)
const attentionOverflowCount=computed(()=>attentionQueue.value.overflowCount)
const attentionSourceSummary=computed(()=>[
{key:'dirty',label:'미저장',count:attentionQueue.value.sourceCounts.dirty??0},
{key:'operation-failed',label:'실패',count:attentionQueue.value.sourceCounts['operation-failed']??0},
{key:'notification-urgent',label:'중요 알림',count:attentionQueue.value.sourceCounts['notification-urgent']??0},
{key:'operation-running',label:'진행',count:attentionQueue.value.sourceCounts['operation-running']??0},
{key:'notification',label:'알림',count:attentionQueue.value.sourceCounts.notification??0},
].filter(item=>item.count>0))
const attentionTriageGroups=computed(()=>{
const perGroup=attentionExpanded.value?6:2
const items=allAttentionQueue.value.items
const sourceCounts=allAttentionQueue.value.sourceCounts
return [
{key:'urgent',label:'긴급·예외',description:'실패와 중요 알림부터 처리',tone:'danger',count:(sourceCounts['operation-failed']??0)+(sourceCounts['notification-urgent']??0),items:items.filter(item=>item.source==='operation-failed'||item.source==='notification-urgent').slice(0,perGroup)},
{key:'draft',label:'미저장',description:'중단한 편집을 이어서 처리',tone:'warning',count:sourceCounts.dirty??0,items:items.filter(item=>item.source==='dirty').slice(0,perGroup)},
{key:'running',label:'진행 작업',description:'대량·연계 작업 상태 확인',tone:'info',count:sourceCounts['operation-running']??0,items:items.filter(item=>item.source==='operation-running').slice(0,perGroup)},
{key:'notice',label:'알림',description:'업무 후속 확인 항목',tone:'default',count:sourceCounts.notification??0,items:items.filter(item=>item.source==='notification').slice(0,perGroup)},
].filter(group=>group.count>0)
})
const attentionDisplayedCount=computed(()=>attentionTriageGroups.value.reduce((sum,group)=>sum+group.items.length,0))
const dirtyCount=computed(()=>props.tabs.filter(tab=>tab.dirty).length)
const pinnedCount=computed(()=>props.tabs.filter(tab=>tab.pinned).length)
const regularOpenCount=computed(()=>props.tabs.filter(tab=>!tab.pinned).length)
const regularNotificationCount=computed(()=>Math.max(props.notificationCount-props.urgentNotificationCount,0))
const attentionCount=computed(()=>props.operationCount+props.operationFailureCount+props.notificationCount+dirtyCount.value)
const sourceSummary=computed(()=>({favorites:props.favorites.length,open:regularOpenCount.value,pinned:pinnedCount.value,recent:props.recents.length}))
const firstDirty=computed(()=>workbench.value.find(item=>item.source==='dirty'))
const favoriteIds=computed(()=>new Set(props.favorites.map(entry=>entry.screenId)))
const openCounts=computed(()=>props.tabs.reduce((map,tab)=>map.set(tab.screenId,(map.get(tab.screenId)??0)+1),new Map<string,number>()))
function isFavorite(screenId:string){return favoriteIds.value.has(screenId)}
const visibleScreenCount=computed(()=>visibleModules.value.reduce((sum,module)=>sum+module.groups.reduce((groupSum,group)=>groupSum+group.entries.length,0),0))
watch(modules,value=>{if(selectedModule.value!=='ALL'&&!value.some(item=>item.module===selectedModule.value))selectedModule.value='ALL'})
function openCount(screenId:string){return openCounts.value.get(screenId)??0}
const moduleRailOrder=computed(()=>['ALL',...moduleSummaries.value.map(item=>item.module)])
function focusModuleRail(module:string){
if(typeof document==='undefined')return
document.querySelector<HTMLElement>(`[data-home-module="${module}"]`)?.focus()
}
function onModuleRailKeydown(event:KeyboardEvent){
if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return
const order=moduleRailOrder.value
const current=(event.target as HTMLElement | null)?.dataset.homeModule ?? selectedModule.value
let index=Math.max(order.indexOf(current),0)
if(event.key==='ArrowRight')index=(index+1)%order.length
else if(event.key==='ArrowLeft')index=(index-1+order.length)%order.length
else if(event.key==='Home')index=0
else if(event.key==='End')index=order.length-1
event.preventDefault()
focusModuleRail(order[index])
}
function selectModule(module:string){
selectedModule.value=module
if(typeof document==='undefined')return
requestAnimationFrame(()=>{
const explorer=document.getElementById('kbx-home-explorer')
explorer?.scrollIntoView({block:'start',behavior:'smooth'})
explorer?.focus({preventScroll:true})
})
}
</script>
<template>
<section class="kbx-home" aria-labelledby="kbx-home-title">
<header class="kbx-home__header">
<div><p>KBX 업무 시작</p><h1 id="kbx-home-title"></h1><span>검색하고, 이어서 처리하고, 예외만 확인하는 업무 시작점입니다.</span></div>
<button type="button" class="kbx-home__search" aria-keyshortcuts="Control+K" @click="emit('menuSearch')"><span>메뉴명 · 화면코드 · 업무명 검색</span><kbd>Ctrl K</kbd></button>
</header>
<nav class="kbx-home__pulse" aria-label="업무 시작 상태">
<button type="button" @click="emit('menuSearch')"><span>사용 가능 화면</span><b>{{launchableScreenCount}}</b><small>검색</small></button>
<button type="button" :disabled="!dirtyCount" @click="firstDirty && emit('launch',firstDirty)"><span>미저장 업무</span><b>{{dirtyCount}}</b><small>{{dirtyCount?'이어서':'없음'}}</small></button>
<button type="button" @click="emit('operations')"><span>진행 작업</span><b>{{props.operationCount}}</b><small>작업 센터</small></button>
<button type="button" @click="emit('notifications')"><span> 알림</span><b>{{props.notificationCount}}</b><small>알림 센터</small></button>
</nav>
<nav class="kbx-home__module-rail" aria-label="업무 영역 바로가기" @keydown="onModuleRailKeydown">
<span class="kbx-home__module-rail-label">업무영역</span>
<button type="button" data-home-module="ALL" :aria-pressed="selectedModule==='ALL'" @click="selectModule('ALL')"><b>전체</b><small>{{launchableScreenCount}} 화면 · 확인 {{exactAttentionCount}}</small></button>
<button v-for="item in moduleSummaries" :key="item.module" type="button" :data-kbx-module="item.module" :data-home-module="item.module" :aria-pressed="selectedModule===item.module" @click="selectModule(item.module)">
<b>{{item.module==='COMMON'?'공통':item.module}}</b><small>{{item.screenCount}} · 확인 {{item.attentionCount}} · {{item.favoriteCount}} · 열림 {{item.openCount}}</small>
</button>
</nav>
<section class="kbx-home__attention" aria-labelledby="kbx-home-attention-title">
<div class="kbx-home__attention-head"><strong id="kbx-home-attention-title">확인 필요</strong><span v-if="!exactAttentionCount && !attentionCount" class="quiet">현재 확인할 작업이나 알림이 없습니다.</span><div v-else class="kbx-home__attention-summary" aria-label="확인 필요 항목 구성"><span v-for="item in attentionSourceSummary" :key="item.key">{{item.label}} <b>{{item.count}}</b></span></div><small v-if="attentionOverflowCount">전체 {{exactAttentionCount}}개 · 우선순위 상위 {{attentionItems.length}}개 표시</small><small v-else-if="exactAttentionCount || attentionCount">{{exactAttentionCount || attentionCount}}개 우선 항목</small><button v-if="exactAttentionCount > attentionCollapsedLimit" type="button" class="kbx-home__attention-toggle" aria-controls="kbx-home-attention-list" :aria-expanded="attentionExpanded" @click="attentionExpanded=!attentionExpanded">{{attentionToggleLabel}}</button><div v-if="attentionExpanded && attentionOverflowCount" class="kbx-home__attention-centers" aria-label="나머지 확인 항목"><button v-if="attentionOperationCount" type="button" @click="emit('operations')">작업 센터 전체</button><button v-if="attentionNotificationCount" type="button" @click="emit('notifications')">알림 센터 전체</button></div></div>
<div v-if="attentionTriageGroups.length" id="kbx-home-attention-list" class="kbx-home__attention-list" role="list" :aria-label="`확인 필요 ${attentionDisplayedCount}개 표시`">
<section v-for="group in attentionTriageGroups" :key="group.key" class="kbx-home__triage-group" :data-tone="group.tone" role="listitem">
<header><div><strong>{{group.label}}</strong><small>{{group.description}}</small></div><b>{{group.count}}</b></header>
<div class="kbx-home__triage-items">
<button v-for="item in group.items" :key="item.key" type="button" :class="{critical:item.source==='operation-failed'||item.source==='notification-urgent'}" @click="emit('attention',item)">
<span class="attention-source">{{ item.source==='dirty'?'미저장':item.source==='operation-failed'?'실패':item.source==='notification-urgent'?'중요':item.source==='operation-running'?'진행':'알림' }}</span>
<span class="attention-copy"><b>{{item.title}}</b><small v-if="item.detail">{{item.detail}}</small></span>
<span class="attention-action">{{item.actionLabel}}</span><span aria-hidden="true"></span>
</button>
</div>
<button v-if="group.count>group.items.length" type="button" class="kbx-home__triage-more" @click="group.key==='running'?emit('operations'):group.key==='notice'||group.key==='urgent'?emit('notifications'):attentionExpanded=true">나머지 {{group.count-group.items.length}} 보기</button>
</section>
</div>
<div v-else-if="attentionCount" class="kbx-home__attention-legacy">
<button v-if="dirtyCount" type="button" @click="firstDirty && emit('launch',firstDirty)">미저장 업무 <b>{{dirtyCount}}</b></button>
<button v-if="operationFailureCount" type="button" class="critical" @click="emit('operations')">실패·부분완료 작업 <b>{{operationFailureCount}}</b></button>
<button v-if="urgentNotificationCount" type="button" class="critical" @click="emit('notifications')">중요 알림 <b>{{urgentNotificationCount}}</b></button>
<button v-if="operationCount" type="button" @click="emit('operations')">진행 작업 <b>{{operationCount}}</b></button>
<button v-if="regularNotificationCount" type="button" @click="emit('notifications')"> 알림 <b>{{regularNotificationCount}}</b></button>
</div>
</section>
<section class="kbx-home__section kbx-home__workbench" aria-labelledby="kbx-home-workbench-title">
<header>
<div><h2 id="kbx-home-workbench-title">바로 시작</h2><span>미저장·고정·열린 업무를 먼저 보여주고, 즐겨찾기·최근·권장 업무를 중복 없이 이어 붙입니다.</span></div>
<small>열림 {{sourceSummary.open}} · 고정 {{sourceSummary.pinned}} · 즐겨찾기 {{sourceSummary.favorites}} · 최근 {{sourceSummary.recent}}</small>
</header>
<div v-if="workbench.length" class="kbx-home__workbench-list" role="list">
<button v-for="item in workbench" :key="item.launchKey" type="button" role="listitem" @click="emit('launch',item)">
<span class="source" :data-source="item.source">{{item.sourceLabel}}</span>
<span class="main"><b>{{item.title}}</b><small>{{item.module}} · {{item.homeGroup ?? item.section}}<template v-if="item.instanceLabel"> · {{item.instanceLabel}}</template></small></span>
<span class="resume">{{item.tabKey ? '이어서' : '열기'}}</span><span aria-hidden="true"></span>
</button>
</div>
<p v-else class="kbx-home__empty">사용 가능한 주요 업무가 없습니다. 메뉴 검색에서 권한이 허용된 화면을 찾아보세요.</p>
</section>
<section id="kbx-home-explorer" class="kbx-home__all" aria-label="전체 업무" tabindex="-1">
<header class="kbx-home__all-header">
<div><h2>{{selectedModule==='ALL'?'전체 업무':`${selectedModule==='COMMON'?'공통':selectedModule} 업무`}}</h2><span>모듈별 업무 탐색기 · 화면을 다시 설계하지 않고 검증된 업무 문법에서 바로 시작합니다. 자주 쓰는 화면은 즐겨찾기에 고정하세요.</span></div>
<button v-if="selectedModule!=='ALL'" type="button" class="kbx-home__show-all" @click="selectModule('ALL')">전체 업무 보기</button>
<small class="kbx-home__filter-status" aria-live="polite">{{visibleScreenCount}} 화면</small>
</header>
<div class="kbx-home__modules">
<section v-for="item in visibleModules" :key="item.module" class="kbx-home__module" :data-kbx-module="item.module">
<header><strong>{{item.module}}</strong><span>{{item.label}}</span><small>{{item.groups.reduce((sum,group)=>sum+group.entries.length,0)}} 화면</small></header>
<div v-for="group in item.groups" :key="group.section" class="kbx-home__group">
<h3>{{group.section}}</h3>
<div class="kbx-home__group-links">
<div v-for="entry in group.entries" :key="entry.screenId" class="kbx-home__group-row">
<button class="launch" type="button" @click="emit('open',entry)">
<span>{{entry.title}}</span>
<small>{{entry.homeGroup ?? entry.section}}<template v-if="openCount(entry.screenId)"> · 열림 {{openCount(entry.screenId)}}</template></small>
</button>
<button v-if="entry.favoriteAllowed!==false" class="favorite" type="button" :aria-label="isFavorite(entry.screenId)?`${entry.title} 즐겨찾기 해제`:`${entry.title} 즐겨찾기 추가`" :aria-pressed="isFavorite(entry.screenId)" @click="emit('toggleFavorite',entry.screenId)">{{isFavorite(entry.screenId)?'★':'☆'}}</button>
</div>
</div>
</div>
</section>
</div>
</section>
</section>
</template>
<style scoped>
.kbx-home__module-rail{min-height:var(--kbx-control-lg);display:grid;grid-template-columns:auto repeat(5,minmax(0,1fr));align-items:stretch;border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-shell-chrome)}.kbx-home__module-rail-label{display:flex;align-items:center;padding:0 var(--kbx-space-3);border-right:var(--kbx-border-width) solid var(--kbx-color-border);color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);font-weight:600}.kbx-home__module-rail button{min-height:var(--kbx-control-lg);display:grid;align-content:center;gap:var(--kbx-space-1);padding:var(--kbx-space-1) var(--kbx-space-3);border:0;border-right:var(--kbx-border-width) solid var(--kbx-color-border);background:transparent;color:var(--kbx-color-text);text-align:left}.kbx-home__module-rail button:last-child{border-right:0}.kbx-home__module-rail button:hover{background:var(--kbx-color-surface-hover)}.kbx-home__module-rail button:focus-visible{outline:calc(var(--kbx-border-width) * 2) solid var(--kbx-color-focus);outline-offset:calc(var(--kbx-border-width) * -2);position:relative;z-index:1}.kbx-home__module-rail button[aria-pressed="true"]{box-shadow:inset 0 calc(var(--kbx-border-width) * -2) 0 var(--kbx-color-module-accent);background:var(--kbx-color-module-accent-surface)}.kbx-home__module-rail button b{font-size:var(--kbx-font-sm);font-weight:600}.kbx-home__module-rail button small{color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);font-variant-numeric:tabular-nums}.kbx-home__show-all{height:var(--kbx-control-xs);margin-left:auto;padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text);font-size:var(--kbx-font-xs)}.kbx-home__show-all:hover{background:var(--kbx-color-surface-hover)}
.kbx-home__pulse{min-height:var(--kbx-control-lg);display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface)}.kbx-home__pulse button{min-height:var(--kbx-control-lg);display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:0 var(--kbx-space-2);padding:var(--kbx-space-1) var(--kbx-space-3);border:0;border-right:var(--kbx-border-width) solid var(--kbx-color-border);background:transparent;text-align:left}.kbx-home__pulse button:last-child{border-right:0}.kbx-home__pulse button:hover:not(:disabled){background:var(--kbx-color-surface-hover)}.kbx-home__pulse button:disabled{cursor:default}.kbx-home__pulse span{font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted)}.kbx-home__pulse b{grid-row:1 / span 2;grid-column:2;font-size:var(--kbx-font-lg);font-variant-numeric:tabular-nums}.kbx-home__pulse small{font-size:var(--kbx-font-xs);color:var(--kbx-color-module-accent)}
.kbx-home{width:min(100%,var(--kbx-home-max-width));max-width:var(--kbx-home-max-width);margin:0 auto;padding:var(--kbx-space-5) var(--kbx-space-5) var(--kbx-space-8);display:flex;flex-direction:column;gap:var(--kbx-space-4)}.kbx-home__header{display:flex;align-items:center;justify-content:space-between;gap:var(--kbx-space-6);min-height:var(--kbx-home-header-min-height);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__header p,.kbx-home__header span{margin:0;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}.kbx-home__header h1{margin:0;font-size:var(--kbx-font-2xl)}.kbx-home__search{width:min(var(--kbx-home-search-max-width),45vw);height:var(--kbx-control-lg);display:flex;justify-content:space-between;align-items:center;padding:0 var(--kbx-space-3);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text-muted);text-align:left}.kbx-home__search kbd{font-size:var(--kbx-font-xs);border:var(--kbx-border-width) solid var(--kbx-color-border);padding:var(--kbx-space-1);background:var(--kbx-color-surface-muted)}.kbx-home__attention{display:grid;gap:var(--kbx-space-2);min-height:var(--kbx-home-attention-height);padding:var(--kbx-space-2) var(--kbx-space-3);border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-warning-surface)}.kbx-home__attention-head{display:flex;align-items:center;gap:var(--kbx-space-2);flex-wrap:wrap}.kbx-home__attention-head small{margin-left:auto;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-home__attention-summary{display:flex;align-items:center;gap:var(--kbx-space-1);flex-wrap:wrap}.kbx-home__attention-summary span{display:inline-flex;align-items:center;gap:var(--kbx-space-1);min-height:var(--kbx-control-xs);padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-pill);background:var(--kbx-color-surface);color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-home__attention-summary b{color:var(--kbx-color-text);font-variant-numeric:tabular-nums}.kbx-home__attention-toggle{min-height:var(--kbx-control-xs);padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-warning-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text);font-size:var(--kbx-font-xs);font-weight:600}.kbx-home__attention-toggle:hover{background:var(--kbx-color-surface-hover)}.kbx-home__attention-centers{display:flex;align-items:center;gap:var(--kbx-space-1)}.kbx-home__attention-centers button{min-height:var(--kbx-control-xs);padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text);font-size:var(--kbx-font-xs);font-weight:600}.kbx-home__attention-centers button:hover{background:var(--kbx-color-surface-hover)}.kbx-home__attention .quiet{color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}.kbx-home__attention-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--kbx-space-2)}.kbx-home__triage-group{min-width:0;border:var(--kbx-border-width) solid var(--kbx-color-border);border-top:calc(var(--kbx-border-width) * 2) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);overflow:hidden}.kbx-home__triage-group[data-tone="danger"]{border-top-color:var(--kbx-color-danger)}.kbx-home__triage-group[data-tone="warning"]{border-top-color:var(--kbx-color-warning)}.kbx-home__triage-group[data-tone="info"]{border-top-color:var(--kbx-color-primary)}.kbx-home__triage-group>header{min-height:var(--kbx-control-lg);display:flex;align-items:center;justify-content:space-between;gap:var(--kbx-space-2);padding:var(--kbx-space-1) var(--kbx-space-2);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-section-heading)}.kbx-home__triage-group>header>div{display:grid;min-width:0}.kbx-home__triage-group>header strong{font-size:var(--kbx-font-sm)}.kbx-home__triage-group>header small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-home__triage-group>header>b{font-size:var(--kbx-font-lg)}.kbx-home__triage-items{display:flex;flex-direction:column}.kbx-home__triage-items button{min-height:var(--kbx-control-lg);display:grid;grid-template-columns:auto minmax(0,1fr) auto var(--kbx-checkbox-track);align-items:center;gap:var(--kbx-space-2);border:0;border-left:var(--kbx-accent-border-width) solid transparent;border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:transparent;padding:var(--kbx-space-1) var(--kbx-space-2);text-align:left}.kbx-home__triage-items button:hover{background:var(--kbx-color-surface-hover)}.kbx-home__triage-items button.critical{border-left-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}.kbx-home__triage-more{width:100%;min-height:var(--kbx-control-xs);border:0;background:var(--kbx-color-surface-muted);color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);text-align:center}.kbx-home__triage-more:hover{color:var(--kbx-color-primary);background:var(--kbx-color-surface-hover)}.attention-source,.attention-action{font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted);white-space:nowrap}.critical .attention-source{color:var(--kbx-color-danger)}.attention-copy{display:grid;min-width:0}.attention-copy b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--kbx-color-text);font-size:var(--kbx-font-sm)}.attention-copy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-home__triage-items button:focus-visible,.kbx-home__triage-more:focus-visible{outline:calc(var(--kbx-border-width) * 2) solid var(--kbx-color-focus);outline-offset:calc(var(--kbx-border-width) * -2)}.kbx-home__attention-legacy{display:flex;align-items:center;gap:var(--kbx-space-2);flex-wrap:wrap}.kbx-home__attention-legacy button{height:var(--kbx-control-xs);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);padding:0 var(--kbx-space-3)}.kbx-home__attention-legacy button.critical{border-color:var(--kbx-color-danger-border);background:var(--kbx-color-danger-surface)}.kbx-home__section,.kbx-home__all{border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface)}.kbx-home__section>header,.kbx-home__all-header{min-height:var(--kbx-control-lg);display:flex;align-items:center;justify-content:space-between;gap:var(--kbx-space-3);padding:var(--kbx-space-2) var(--kbx-space-3);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface-muted)}.kbx-home__section>header>div,.kbx-home__all-header>div{display:grid;gap:var(--kbx-space-1)}.kbx-home__section h2,.kbx-home__all h2{margin:0;font-size:var(--kbx-font-lg);font-weight:600}.kbx-home__section>header span,.kbx-home__section>header small,.kbx-home__all-header span{font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted)}.kbx-home__workbench-list{display:flex;flex-direction:column}.kbx-home__workbench-list button{min-height:var(--kbx-home-workbench-row-height);display:grid;grid-template-columns:var(--kbx-home-source-label-width) minmax(0,1fr) auto var(--kbx-checkbox-track);align-items:center;gap:var(--kbx-space-2);padding:0 var(--kbx-space-3);border:0;border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:transparent;text-align:left}.kbx-home__workbench-list button:last-child{border-bottom:0}.kbx-home__workbench-list button:hover{background:var(--kbx-color-surface-hover)}.kbx-home__workbench-list .source{font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.kbx-home__workbench-list .source[data-source="dirty"]{color:var(--kbx-color-warning-text)}.kbx-home__workbench-list .source[data-source="pinned"],.kbx-home__workbench-list .source[data-source="favorite"]{color:var(--kbx-color-primary)}.kbx-home__workbench-list .main{display:grid;min-width:0}.kbx-home__workbench-list .main b{font-size:var(--kbx-font-md);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kbx-home__workbench-list .main small,.kbx-home__workbench-list .resume{font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted)}.kbx-home__workbench-list .resume{white-space:nowrap}.kbx-home__empty{margin:0;padding:var(--kbx-space-5) var(--kbx-space-3);color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}.kbx-home__all-header nav{display:flex;gap:var(--kbx-space-1);margin-left:auto}.kbx-home__filter-status{color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);white-space:nowrap}.kbx-home__all-header nav button{height:var(--kbx-control-xs);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);padding:0 var(--kbx-space-2)}.kbx-home__all-header nav button[aria-pressed="true"]{border-color:var(--kbx-color-primary);color:var(--kbx-color-primary);font-weight:600}.kbx-home__modules{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.kbx-home__module{min-width:0;border-right:var(--kbx-border-width) solid var(--kbx-color-border);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__module:nth-child(2n){border-right:0}.kbx-home__module>header{height:var(--kbx-home-module-header-height);display:flex;box-shadow:inset var(--kbx-accent-border-width) 0 0 var(--kbx-color-module-accent);align-items:center;gap:var(--kbx-space-2);padding:0 var(--kbx-space-3);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__module>header strong{font-size:var(--kbx-font-lg);color:var(--kbx-color-module-accent)}.kbx-home__module>header span,.kbx-home__module>header small{font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted)}.kbx-home__module>header small{margin-left:auto}.kbx-home__group{padding:var(--kbx-space-2) var(--kbx-space-2) var(--kbx-space-3)}.kbx-home__group h3{margin:0 var(--kbx-space-1) var(--kbx-space-1);font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.kbx-home__group-links{display:flex;flex-direction:column}.kbx-home__group-row{min-height:var(--kbx-control-height);display:flex;align-items:center;border-radius:var(--kbx-radius-sm)}.kbx-home__group-row:hover{background:var(--kbx-color-surface-hover)}.kbx-home__group-row .launch{min-height:var(--kbx-control-height);min-width:0;flex:1;display:flex;align-items:center;justify-content:space-between;gap:var(--kbx-space-2);padding:0 var(--kbx-space-2);border:0;background:transparent;text-align:left}.kbx-home__group-row .launch span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kbx-home__group-row .launch small{color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs);white-space:nowrap}.kbx-home__group-row .favorite{width:var(--kbx-control-xs);height:var(--kbx-control-xs);border:0;border-radius:var(--kbx-radius-sm);background:transparent;color:var(--kbx-color-text-muted)}.kbx-home__group-row .favorite[aria-pressed="true"]{color:var(--kbx-color-primary)}.kbx-home__group-row .favorite:hover{background:var(--kbx-color-surface)}@media(max-width:56rem){.kbx-home__module-rail{grid-template-columns:repeat(2,minmax(0,1fr))}.kbx-home__module-rail-label{grid-column:1/-1;min-height:var(--kbx-control-xs);border-right:0;border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__module-rail button{border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__pulse{grid-template-columns:repeat(2,minmax(0,1fr))}.kbx-home__pulse button:nth-child(2){border-right:0}.kbx-home__pulse button:nth-child(-n+2){border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-home__attention-list{grid-template-columns:repeat(2,minmax(0,1fr))}.kbx-home__modules{grid-template-columns:1fr}.kbx-home__module{border-right:0}.kbx-home__header,.kbx-home__all-header{align-items:flex-start;flex-direction:column;padding-bottom:var(--kbx-space-3)}.kbx-home__search{width:100%}}
@media(max-width:40rem){.kbx-home__attention-list{grid-template-columns:1fr}}
/* v43 theme hierarchy: business emphasis without dashboard-card inflation */
.kbx-home__header p{color:var(--kbx-color-primary);font-weight:600;letter-spacing:.01em}.kbx-home__header h1{letter-spacing:-.02em}.kbx-home__search:hover{border-color:var(--kbx-color-primary);background:var(--kbx-color-surface-hover)}
.kbx-home__attention{border-color:var(--kbx-color-warning-border)}.kbx-home__triage-items button{box-shadow:inset var(--kbx-border-width) 0 0 var(--kbx-color-warning-border)}.kbx-home__triage-items button.critical{box-shadow:inset var(--kbx-accent-border-width) 0 0 var(--kbx-color-danger)}
.kbx-home__workbench-list .source{justify-self:start;padding:var(--kbx-space-1) var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-pill);background:var(--kbx-color-surface-muted)}.kbx-home__workbench-list .source[data-source="dirty"]{border-color:var(--kbx-color-warning-border);background:var(--kbx-color-warning-surface)}.kbx-home__workbench-list .source[data-source="pinned"],.kbx-home__workbench-list .source[data-source="favorite"]{border-color:var(--kbx-color-info-border);background:var(--kbx-color-info-surface)}
.kbx-home__module>header{background:var(--kbx-color-surface-subtle)}.kbx-home__module>header strong{color:var(--kbx-color-module-accent)}.kbx-home__all-header nav button[aria-pressed="true"]{background:var(--kbx-color-info-surface);border-color:var(--kbx-color-info-border)}
@media(forced-colors:active){.kbx-home__module-rail button[aria-pressed="true"]{outline:var(--kbx-border-width) solid Highlight;outline-offset:calc(var(--kbx-border-width) * -1);box-shadow:none}.kbx-home__module-rail button:focus-visible{outline-color:Highlight}}
</style>