219 lines
34 KiB
Vue
219 lines
34 KiB
Vue
<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>
|