2026-10-26 12:15:48 +00:00
/* hal/ui/public/ops.css — 운영자 콘솔 스타일. 상태는 글자+기호로 구분(색은 보조). 760px 이하에서 메뉴가 접히고 표가 카드로 바뀐다(360px 조회·승인·중지·재시도 가능). */
: root { --bg : #f3f5f8 ; --fg : #1b2430 ; --mut : #5b6672 ; --line : #d9dee6 ; --pri : #1f4fb0 ; --ok : #1a7f4b ; --warn : #b26a00 ; --bad : #b3261e ; --card : #fff ; --rad : 10 px ; font-family : - apple-system , "Segoe UI" , "Noto Sans KR" , sans-serif }
* { box-sizing : border-box } body { margin : 0 ; background : var ( -- bg ); color : var ( -- fg ); font-size : 14 px ; line-height : 1.5 } a { color : var ( -- pri )}
. skip { position : absolute ; left : -999 px }. skip : focus { left : 8 px ; top : 8 px ; background : #fff ; padding : 6 px ; z-index : 99 }
. top { display : flex ; align-items : center ; gap : 12 px ; padding : 8 px 14 px ; background : #101826 ; color : #fff ; position : sticky ; top : 0 ; z-index : 20 }. top a { color : #fff }. brand { font-weight : 700 ; text-decoration : none }. ver { font-size : 12 px ; opacity : .9 }. menu-btn { display : none ; font-size : 20 px ; background : none ; border : 0 ; color : #fff }
. top-right { margin-left : auto ; display : flex ; gap : 12 px ; align-items : center }. top-right . muted { color : #c7d0dc }
. shell { display : flex ; min-height : calc ( 100 vh - 48 px )}. side { width : 210 px ; background : #fff ; border-right : 1 px solid var ( -- line ); padding : 10 px 8 px }. side . nav { display : block ; padding : 8 px 10 px ; border-radius : 8 px ; color : var ( -- fg ); text-decoration : none }. side . nav . on { background : #e6edfb ; color : var ( -- pri ); font-weight : 600 }
. main { flex : 1 ; padding : 14 px 18 px ; max-width : 1400 px ; min-width : 0 } h1 { font-size : 20 px ; margin : 4 px 0 10 px } h2 { font-size : 16 px ; margin : 18 px 0 8 px } h3 { font-size : 15 px ; margin : 14 px 0 6 px }
. alerts { display : flex ; flex-direction : column ; gap : 6 px ; margin-bottom : 10 px }. alert { display : block ; padding : 9 px 12 px ; border-radius : 8 px ; border : 1 px solid ; text-decoration : none ; color : var ( -- fg )}. alert . warning { background : #fff7e6 ; border-color : #f0c36d }. alert . critical { background : #fdecea ; border-color : #f3a09a }. alert . info { background : #e8effc ; border-color : #b9cdf2 }
. card { background : var ( -- card ); border : 1 px solid var ( -- line ); border-radius : var ( -- rad ); padding : 14 px ; margin-bottom : 12 px ; min-width : 0 }. cards { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 260 px , 1 fr )); gap : 12 px }. card h2 { margin-top : 0 }. comp { display : flex ; flex-direction : column ; gap : 4 px }
. kv { display : grid ; grid-template-columns : max-content 1 fr ; gap : 5 px 12 px ; margin : 8 px 0 }. kv dt { color : var ( -- mut )}. kv dd { margin : 0 ; word-break : break-word }
. btn { display : inline-block ; padding : 7 px 12 px ; border : 1 px solid var ( -- line ); background : #fff ; border-radius : 8 px ; cursor : pointer ; font-size : 13 px ; color : var ( -- fg ); text-decoration : none }. btn . primary { background : var ( -- pri ); color : #fff ; border-color : var ( -- pri )}. btn . danger { color : var ( -- bad ); border-color : var ( -- bad )}. btn . small { padding : 4 px 8 px ; font-size : 12 px }. btn . on { background : #e6edfb ; border-color : var ( -- pri )}. btn : disabled { opacity : .5 ; cursor : not-allowed }. actions { display : flex ; gap : 6 px ; flex-wrap : wrap ; margin : 8 px 0 }
. badge { display : inline-block ; padding : 1 px 8 px ; border-radius : 12 px ; font-size : 12 px ; border : 1 px solid var ( -- line ); background : #fff ; white-space : nowrap }. badge . ok , . badge . done { border-color : var ( -- ok ); color : var ( -- ok )}. badge . failed { border-color : var ( -- bad ); color : var ( -- bad )}. badge . warn , . badge . stale { border-color : var ( -- warn ); color : var ( -- warn )}. badge . running , . badge . waiting , . badge . info { border-color : var ( -- pri ); color : var ( -- pri )}
table { width : 100 % ; border-collapse : collapse ; background : #fff ; border : 1 px solid var ( -- line ); border-radius : var ( -- rad ); overflow : hidden ; margin-bottom : 10 px } th , td { text-align : left ; padding : 8 px 10 px ; border-bottom : 1 px solid var ( -- line ); vertical-align : top ; font-size : 13 px } th { background : #f7f9fc ; font-weight : 600 } tr : last-child td { border-bottom : 0 }
. aw-state { padding : 20 px ; text-align : center ; color : var ( -- mut ); background : #fff ; border : 1 px dashed var ( -- line ); border-radius : var ( -- rad ); margin : 8 px 0 }. aw-state . failed , . aw-state . forbidden { border-color : var ( -- bad ); color : var ( -- bad )}. aw-state . stale , . aw-state . partial { border-color : var ( -- warn ); color : var ( -- warn )}
. row { display : flex ; gap : 8 px ; align-items : center ; flex-wrap : wrap ; margin : 6 px 0 } input , select , textarea { font : inherit ; padding : 7 px 9 px ; border : 1 px solid var ( -- line ); border-radius : 8 px ; width : 100 % ; max-width : 420 px } label { display : block ; font-weight : 600 ; margin : 8 px 0 2 px }. row label { margin : 0 ; font-weight : 400 }
. steps { list-style : none ; padding : 0 ; margin : 6 px 0 }. steps li { display : flex ; gap : 8 px ; padding : 4 px 0 ; border-bottom : 1 px solid var ( -- line )}. steps . s { width : 20 px ; text-align : center }. steps . done . s { color : var ( -- ok )}. steps . running . s { color : var ( -- pri )}. steps . failed . s { color : var ( -- bad )}. steps . waiting . s , . steps . needs_user . s { color : var ( -- warn )}
. flow { display : flex ; flex-wrap : wrap ; gap : 4 px ; padding : 0 ; list-style : none ; counter-reset : f }. flow li { padding : 3 px 8 px ; border : 1 px solid var ( -- line ); border-radius : 8 px ; font-size : 12 px ; color : var ( -- mut )}. flow li . done { border-color : var ( -- ok ); color : var ( -- ok )}. flow li . now { border-color : var ( -- pri ); color : var ( -- pri ); font-weight : 600 }
. pre { background : #f7f9fc ; border : 1 px solid var ( -- line ); border-radius : 8 px ; padding : 8 px ; white-space : pre-wrap ; word-break : break-word ; font-size : 12 px ; max-height : 320 px ; overflow : auto }. muted { color : var ( -- mut )}. err { color : var ( -- bad )}. mono { font-family : ui-monospace , Menlo , monospace ; font-size : 12 px }. small { font-size : 12 px }
. chart { width : 100 % ; height : auto ; background : #fff ; border : 1 px solid var ( -- line ); border-radius : 8 px }. chart . line { stroke : var ( -- pri ); stroke-width : 1.5 }. chart . axis { stroke : var ( -- line )}. chart . lbl { font-size : 11 px ; fill : var ( -- mut )}
. toast { position : fixed ; bottom : 14 px ; left : 50 % ; transform : translateX ( -50 % ); background : #101826 ; color : #fff ; padding : 10 px 16 px ; border-radius : 8 px ; display : none ; z-index : 50 ; max-width : 92 vw }. toast . show { display : block }
. modal { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .5 ); display : flex ; align-items : center ; justify-content : center ; z-index : 40 ; padding : 14 px }. modal [ hidden ] { display : none }. modal . box { background : #fff ; border-radius : 12 px ; padding : 18 px ; max-width : 600 px ; width : 100 % ; max-height : 92 vh ; overflow : auto }. modal . box h2 { margin-top : 0 }. once { background : #fff7e6 ; border : 1 px dashed var ( -- warn ); padding : 10 px ; border-radius : 8 px ; word-break : break-all }
. tabs { display : flex ; gap : 4 px ; border-bottom : 1 px solid var ( -- line ); margin-bottom : 10 px }. tabs a { padding : 8 px 12 px ; text-decoration : none ; color : var ( -- fg ); border-bottom : 2 px solid transparent }. tabs a . on { border-bottom-color : var ( -- pri ); color : var ( -- pri ); font-weight : 600 }
. pending { list-style : none ; padding : 0 ; display : flex ; gap : 14 px ; flex-wrap : wrap }. pending li { background : #fff ; border : 1 px solid var ( -- line ); border-radius : 8 px ; padding : 8 px 12 px }
body . login { display : grid ; place-items : center ; min-height : 100 vh }. login-box { background : #fff ; border : 1 px solid var ( -- line ); border-radius : 12 px ; padding : 24 px ; width : min ( 420 px , 94 vw )}
@ media ( max-width : 760px ) {. menu-btn { display : block }. side { position : fixed ; left : -230 px ; top : 48 px ; bottom : 0 ; transition : left .2 s ; z-index : 30 ; overflow : auto }. side . open { left : 0 }. main { padding : 10 px 12 px }. top-right . muted { display : none }
table . r { display : block ; border : 0 ; background : none } table . r thead { display : none } table . r tr { display : block ; background : #fff ; border : 1 px solid var ( -- line ); border-radius : var ( -- rad ); margin-bottom : 8 px ; padding : 6 px 10 px } table . r td { display : flex ; justify-content : space-between ; gap : 8 px ; border : 0 ; padding : 5 px 0 ; flex-wrap : wrap } table . r td :: before { content : attr ( data - l ); color : var ( -- mut ); font-size : 12 px ; min-width : 70 px }. kv { grid-template-columns : 1 fr }}
HAL AI 채팅룸 도입(0.3.0): 운영자 콘솔 오른쪽 패널에 통합 방(첫 탭 고정) + 모델별 개별 방, 모델 연결(로그인·OAuth PKCE·API 키, 비밀은 Vault), 모델별 고유 정보(이름·역할·특성), HAL 제어·감시 스킬 문서·도구 29종(읽기 즉시 / 쓰기·고위험은 제안 → 운영자 승인(재인증·3단계 확인)), 통합 방 규칙(@이름 지정, 모델→모델 자동 1회 전달), MCP stdio 서버(읽기 전용), 개발 지시 v0.3.0(Oz), docs/HAL-AI.md, VERSION_HISTORY 0.3.0 — test/hal-ai.test.mjs RED 0/11 → GREEN 11/11, 전체 26파일 182건 통과
2026-10-28 17:41:50 +00:00
/* 0.3.0 AI 채팅룸 패널: 본문 오른쪽 고정, 너비 조절, 접기. 1100px 이하에서는 본문 아래 전체 너비. 상태는 글자+배지로 구분 */
. ai-panel { width : var ( -- ai - w , 400 px ); min-width : 280 px ; max-width : 60 vw ; flex : 0 0 auto ; border-left : 1 px solid var ( -- line ); background : #fff ; display : flex ; flex-direction : column ; position : sticky ; top : 48 px ; height : calc ( 100 vh - 48 px ); min-height : 0 }
. ai-panel . closed { display : none }. ai-resize { position : absolute ; left : 0 ; top : 0 ; bottom : 0 ; width : 6 px ; cursor : col-resize ; z-index : 2 }. ai-panel { position : sticky }. ai-panel > . ai-resize { position : absolute }. ai-panel { isolation : isolate }
. ai-head { border-bottom : 1 px solid var ( -- line ); padding : 6 px 6 px 0 10 px }. ai-tabs { display : flex ; flex-wrap : wrap ; gap : 2 px }
. ai-tab { border : 1 px solid transparent ; border-bottom : 0 ; background : none ; padding : 6 px 10 px ; border-radius : 8 px 8 px 0 0 ; cursor : pointer ; font-size : 13 px ; color : var ( -- fg ); text-decoration : none }. ai-tab . on { background : #e6edfb ; color : var ( -- pri ); font-weight : 600 ; border-color : var ( -- line )}. ai-tab . fixed { font-weight : 600 }. ai-tab . off { color : var ( -- mut )}. ai-tab . add { color : var ( -- mut )}. ai-dot { color : var ( -- warn )}
. ai-status { padding : 4 px 10 px ; border-bottom : 1 px solid var ( -- line ); font-size : 12 px ; min-height : 22 px }
. ai-log { flex : 1 ; overflow : auto ; padding : 8 px 10 px ; display : flex ; flex-direction : column ; gap : 8 px ; min-height : 0 }
. ai-msg { border : 1 px solid var ( -- line ); border-radius : 10 px ; padding : 8 px 10 px ; background : #fff ; font-size : 13 px }. ai-msg header { margin-bottom : 4 px }. ai-msg . user { background : #e8effc ; border-color : #b9cdf2 }. ai-msg . assistant { background : #fff }. ai-msg . tool { background : #f7f9fc }. ai-msg . system { background : #fff7e6 ; border-color : #f0c36d }. ai-msg . failed { border-color : var ( -- bad )}. ai-msg . relay { border-style : dashed }
. ai-text { white-space : pre-wrap ; word-break : break-word }. ai-tool { margin : 4 px 0 ; font-size : 12 px }. ai-result summary { cursor : pointer ; font-size : 12 px }. ai-result . pre { max-height : 200 px }
. ai-proposal { border : 1 px solid var ( -- warn ); background : #fff7e6 ; border-radius : 8 px ; padding : 8 px ; margin-top : 6 px }. ai-proposal . executed { border-color : var ( -- ok ); background : #eef8f1 }. ai-proposal . rejected , . ai-proposal . cancelled { border-color : var ( -- line ); background : #f7f9fc }. ai-proposal . failed { border-color : var ( -- bad ); background : #fdecea }
. ai-compose { border-top : 1 px solid var ( -- line ); padding : 8 px 10 px ; display : flex ; flex-direction : column ; gap : 6 px }. ai-compose textarea { max-width : none ; resize : vertical }. ai-compose-row { display : flex ; gap : 8 px ; align-items : center ; justify-content : flex-end ; flex-wrap : wrap }. ai-compose-row label { margin-right : auto ; font-weight : 400 }
. badge . neutral { border-color : var ( -- line ); color : var ( -- mut )}
@ media ( max-width : 1100px ) {. shell { flex-wrap : wrap }. ai-panel { position : static ; width : 100 % !important ; max-width : none ; height : 60 vh ; border-left : 0 ; border-top : 1 px solid var ( -- line )}. ai-resize { display : none }}