hee_!J commited on
Commit ·
970697d
1
Parent(s): 61f3789
fix: 사이드바 UI 정리 (로고 이동·전체 카드 클릭·간격 보정)
Browse files- app.py +22 -0
- components/alarm_inbox.py +42 -33
- components/header.py +1 -12
- styles/main.css +65 -15
app.py
CHANGED
|
@@ -34,6 +34,27 @@ def inject_css():
|
|
| 34 |
)
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def init_state():
|
| 38 |
ss = st.session_state
|
| 39 |
ss.setdefault("selected_alarm_id", "A1")
|
|
@@ -86,6 +107,7 @@ def render_main():
|
|
| 86 |
|
| 87 |
|
| 88 |
inject_css()
|
|
|
|
| 89 |
init_state()
|
| 90 |
render_alarm_inbox()
|
| 91 |
render_main()
|
|
|
|
| 34 |
)
|
| 35 |
|
| 36 |
|
| 37 |
+
def handle_query_params():
|
| 38 |
+
# 사이드바 FabAgent 로고 클릭 -> ?reset=1로 진입 -> 세션 초기화
|
| 39 |
+
if "reset" in st.query_params:
|
| 40 |
+
for key in list(st.session_state.keys()):
|
| 41 |
+
del st.session_state[key]
|
| 42 |
+
del st.query_params["reset"]
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
# 알람 카드 클릭 -> ?alarm=X로 진입 -> 선택 알람 변경
|
| 46 |
+
if "alarm" in st.query_params:
|
| 47 |
+
alarm_id = st.query_params["alarm"]
|
| 48 |
+
ss = st.session_state
|
| 49 |
+
if alarm_id != ss.get("selected_alarm_id"):
|
| 50 |
+
ss.selected_alarm_id = alarm_id
|
| 51 |
+
ss.stage = 0
|
| 52 |
+
ss.completed_tiers = set()
|
| 53 |
+
ss.approved = False
|
| 54 |
+
ss.animation_pending = True
|
| 55 |
+
del st.query_params["alarm"]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
def init_state():
|
| 59 |
ss = st.session_state
|
| 60 |
ss.setdefault("selected_alarm_id", "A1")
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
inject_css()
|
| 110 |
+
handle_query_params()
|
| 111 |
init_state()
|
| 112 |
render_alarm_inbox()
|
| 113 |
render_main()
|
components/alarm_inbox.py
CHANGED
|
@@ -12,6 +12,8 @@ from data.demo import STATUS_LABELS
|
|
| 12 |
def render_alarm_inbox():
|
| 13 |
ss = st.session_state
|
| 14 |
with st.sidebar:
|
|
|
|
|
|
|
| 15 |
critical_count = sum(1 for a in ss.alarms if a["status"] == "critical")
|
| 16 |
total = len(ss.alarms)
|
| 17 |
badge = f'<span class="badge">긴급 {critical_count}</span>' if critical_count else ""
|
|
@@ -32,6 +34,29 @@ def render_alarm_inbox():
|
|
| 32 |
_render_alarm_card(alarm)
|
| 33 |
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
def _render_alarm_card(alarm):
|
| 36 |
ss = st.session_state
|
| 37 |
is_selected = alarm["id"] == ss.selected_alarm_id
|
|
@@ -52,37 +77,21 @@ def _render_alarm_card(alarm):
|
|
| 52 |
f'<span class="alarm-feat-arrow">{arrow}</span></span>'
|
| 53 |
)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
unsafe_allow_html=True,
|
| 73 |
)
|
| 74 |
-
|
| 75 |
-
if st.button("이 알람 분석", key=f"sel-{alarm['id']}", use_container_width=True):
|
| 76 |
-
_on_alarm_click(alarm["id"])
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def _on_alarm_click(alarm_id: str):
|
| 80 |
-
ss = st.session_state
|
| 81 |
-
if alarm_id == ss.selected_alarm_id:
|
| 82 |
-
return
|
| 83 |
-
ss.selected_alarm_id = alarm_id
|
| 84 |
-
ss.stage = 0
|
| 85 |
-
ss.completed_tiers = set()
|
| 86 |
-
ss.approved = False
|
| 87 |
-
ss.animation_pending = True
|
| 88 |
-
st.rerun()
|
|
|
|
| 12 |
def render_alarm_inbox():
|
| 13 |
ss = st.session_state
|
| 14 |
with st.sidebar:
|
| 15 |
+
_render_logo()
|
| 16 |
+
|
| 17 |
critical_count = sum(1 for a in ss.alarms if a["status"] == "critical")
|
| 18 |
total = len(ss.alarms)
|
| 19 |
badge = f'<span class="badge">긴급 {critical_count}</span>' if critical_count else ""
|
|
|
|
| 34 |
_render_alarm_card(alarm)
|
| 35 |
|
| 36 |
|
| 37 |
+
def _render_logo():
|
| 38 |
+
# 클릭 시 ?reset=1 쿼리로 진입 -> app.maybe_handle_home이 세션 초기화
|
| 39 |
+
st.markdown(
|
| 40 |
+
"""
|
| 41 |
+
<a href="?reset=1" class="fab-sidebar-logo-link" target="_self">
|
| 42 |
+
<div class="fab-sidebar-logo">
|
| 43 |
+
<div class="fab-logo-mark">
|
| 44 |
+
<svg width="16" height="16" viewBox="0 0 160 160" fill="none">
|
| 45 |
+
<circle cx="80" cy="80" r="60" stroke="#fff" stroke-width="10"/>
|
| 46 |
+
<path d="M70 22 L90 22 L86 32 L74 32 Z" fill="#fff"/>
|
| 47 |
+
<rect x="46" y="58" width="68" height="8" rx="4" fill="#fff"/>
|
| 48 |
+
<rect x="46" y="76" width="52" height="8" rx="4" fill="#fff" opacity="0.7"/>
|
| 49 |
+
<rect x="46" y="94" width="36" height="8" rx="4" fill="#fff" opacity="0.4"/>
|
| 50 |
+
</svg>
|
| 51 |
+
</div>
|
| 52 |
+
<span>FabAgent</span>
|
| 53 |
+
</div>
|
| 54 |
+
</a>
|
| 55 |
+
""",
|
| 56 |
+
unsafe_allow_html=True,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
def _render_alarm_card(alarm):
|
| 61 |
ss = st.session_state
|
| 62 |
is_selected = alarm["id"] == ss.selected_alarm_id
|
|
|
|
| 77 |
f'<span class="alarm-feat-arrow">{arrow}</span></span>'
|
| 78 |
)
|
| 79 |
|
| 80 |
+
# 카드 전체를 클릭 영역으로, ?alarm=X 쿼리로 진입하면 app.handle_query_params가 처리
|
| 81 |
+
# markdown 파서가 <a> 안의 <div>를 reparse하는 문제를 피하려고 st.html 사용
|
| 82 |
+
st.html(
|
| 83 |
+
f'<a href="?alarm={alarm["id"]}" class="alarm-card-link" target="_self">'
|
| 84 |
+
f'<div class="{classes}">'
|
| 85 |
+
f'<div class="alarm-head">'
|
| 86 |
+
f'<span class="chip chip-{alarm["status"]}"><span class="dot"></span>{STATUS_LABELS[alarm["status"]]}</span>'
|
| 87 |
+
f'{selected_tag}'
|
| 88 |
+
f'</div>'
|
| 89 |
+
f'<div class="alarm-title">{alarm["title"]}</div>'
|
| 90 |
+
f'<div class="alarm-meta">'
|
| 91 |
+
f'<span style="font-family: var(--mono); font-size: 11px;">{alarm["lot_id"]}</span>'
|
| 92 |
+
f'{feat_html}'
|
| 93 |
+
f'</div>'
|
| 94 |
+
f'<div class="alarm-time">{alarm["time"]}</div>'
|
| 95 |
+
f'</div>'
|
| 96 |
+
f'</a>'
|
|
|
|
| 97 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/header.py
CHANGED
|
@@ -9,21 +9,10 @@ import streamlit as st
|
|
| 9 |
|
| 10 |
def render_header():
|
| 11 |
now = datetime.now().strftime("%H:%M:%S")
|
|
|
|
| 12 |
st.markdown(
|
| 13 |
f"""
|
| 14 |
<header class="fab-header">
|
| 15 |
-
<div class="fab-logo">
|
| 16 |
-
<div class="fab-logo-mark">
|
| 17 |
-
<svg width="16" height="16" viewBox="0 0 160 160" fill="none">
|
| 18 |
-
<circle cx="80" cy="80" r="60" stroke="#fff" stroke-width="10"/>
|
| 19 |
-
<path d="M70 22 L90 22 L86 32 L74 32 Z" fill="#fff"/>
|
| 20 |
-
<rect x="46" y="58" width="68" height="8" rx="4" fill="#fff"/>
|
| 21 |
-
<rect x="46" y="76" width="52" height="8" rx="4" fill="#fff" opacity="0.7"/>
|
| 22 |
-
<rect x="46" y="94" width="36" height="8" rx="4" fill="#fff" opacity="0.4"/>
|
| 23 |
-
</svg>
|
| 24 |
-
</div>
|
| 25 |
-
FabAgent
|
| 26 |
-
</div>
|
| 27 |
<div class="fab-context">
|
| 28 |
<span class="ctx-label">공정</span><span>Photo · Lithography</span>
|
| 29 |
<span class="ctx-sep"></span>
|
|
|
|
| 9 |
|
| 10 |
def render_header():
|
| 11 |
now = datetime.now().strftime("%H:%M:%S")
|
| 12 |
+
# 로고는 사이드바 최상단으로 옮김 (components/alarm_inbox.py)
|
| 13 |
st.markdown(
|
| 14 |
f"""
|
| 15 |
<header class="fab-header">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<div class="fab-context">
|
| 17 |
<span class="ctx-label">공정</span><span>Photo · Lithography</span>
|
| 18 |
<span class="ctx-sep"></span>
|
styles/main.css
CHANGED
|
@@ -18,8 +18,12 @@ header[data-testid="stHeader"] { display: none; }
|
|
| 18 |
section[data-testid="stSidebar"] {
|
| 19 |
width: 320px !important;
|
| 20 |
min-width: 320px !important;
|
|
|
|
| 21 |
background: var(--bg-card);
|
| 22 |
border-right: 1px solid var(--border);
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
section[data-testid="stSidebar"] > div {
|
| 25 |
padding-top: 0;
|
|
@@ -27,24 +31,33 @@ section[data-testid="stSidebar"] > div {
|
|
| 27 |
section[data-testid="stSidebar"] [data-testid="stSidebarContent"] {
|
| 28 |
padding: 0;
|
| 29 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
/* Streamlit이 위젯 사이에 자동으로 끼우는 간격 축소 */
|
| 32 |
[data-testid="stVerticalBlock"] { gap: 0.4rem; }
|
| 33 |
|
| 34 |
-
/*
|
| 35 |
-
section[data-testid="stSidebar"]
|
| 36 |
-
|
| 37 |
-
background: transparent;
|
| 38 |
-
border: none;
|
| 39 |
-
color: var(--text-secondary);
|
| 40 |
-
font-size: 11px;
|
| 41 |
-
padding: 6px 14px;
|
| 42 |
-
margin-top: -6px;
|
| 43 |
-
text-align: left;
|
| 44 |
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
:root {
|
|
@@ -227,6 +240,39 @@ html, body {
|
|
| 227 |
place-items: center;
|
| 228 |
}
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
/* ============== Sidebar — Alarm Inbox ============== */
|
| 231 |
|
| 232 |
.fab-sidebar-head {
|
|
@@ -264,13 +310,17 @@ html, body {
|
|
| 264 |
position: relative;
|
| 265 |
background: var(--bg-card);
|
| 266 |
border: 1px solid var(--border);
|
| 267 |
-
border-radius:
|
| 268 |
padding: 14px 14px 14px 16px;
|
| 269 |
cursor: pointer;
|
| 270 |
-
transition: background 0.15s, border-color 0.15s,
|
| 271 |
overflow: hidden;
|
| 272 |
margin: 0 12px;
|
| 273 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
.alarm-card:hover {
|
| 276 |
background: var(--bg-subtle);
|
|
|
|
| 18 |
section[data-testid="stSidebar"] {
|
| 19 |
width: 320px !important;
|
| 20 |
min-width: 320px !important;
|
| 21 |
+
max-width: 320px !important;
|
| 22 |
background: var(--bg-card);
|
| 23 |
border-right: 1px solid var(--border);
|
| 24 |
+
transform: translateX(0) !important;
|
| 25 |
+
visibility: visible !important;
|
| 26 |
+
display: block !important;
|
| 27 |
}
|
| 28 |
section[data-testid="stSidebar"] > div {
|
| 29 |
padding-top: 0;
|
|
|
|
| 31 |
section[data-testid="stSidebar"] [data-testid="stSidebarContent"] {
|
| 32 |
padding: 0;
|
| 33 |
}
|
| 34 |
+
/* 사이드바 상단 빈 헤더(collapse 버튼 자리) 자체를 제거해 로고가 최상단에 붙도록 */
|
| 35 |
+
section[data-testid="stSidebar"] [data-testid="stSidebarHeader"],
|
| 36 |
+
[data-testid="stSidebarCollapseButton"],
|
| 37 |
+
[data-testid="stSidebarCollapsedControl"],
|
| 38 |
+
[data-testid="collapsedControl"] {
|
| 39 |
+
display: none !important;
|
| 40 |
+
}
|
| 41 |
|
| 42 |
/* Streamlit이 위젯 사이에 자동으로 끼우는 간격 축소 */
|
| 43 |
[data-testid="stVerticalBlock"] { gap: 0.4rem; }
|
| 44 |
|
| 45 |
+
/* 알람 카드 전체를 클릭 영역으로, <a> 래퍼는 시각적으로 투명 */
|
| 46 |
+
section[data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
|
| 47 |
+
gap: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
+
a.alarm-card-link {
|
| 50 |
+
display: block;
|
| 51 |
+
text-decoration: none;
|
| 52 |
+
color: inherit;
|
| 53 |
+
margin-top: 12px;
|
| 54 |
+
}
|
| 55 |
+
a.alarm-card-link:last-of-type {
|
| 56 |
+
margin-bottom: 16px;
|
| 57 |
+
}
|
| 58 |
+
a.alarm-card-link:focus,
|
| 59 |
+
a.alarm-card-link:focus-visible {
|
| 60 |
+
outline: none;
|
| 61 |
}
|
| 62 |
|
| 63 |
:root {
|
|
|
|
| 240 |
place-items: center;
|
| 241 |
}
|
| 242 |
|
| 243 |
+
/* ============== Sidebar — Logo (home) ============== */
|
| 244 |
+
|
| 245 |
+
a.fab-sidebar-logo-link {
|
| 246 |
+
text-decoration: none;
|
| 247 |
+
color: inherit;
|
| 248 |
+
display: block;
|
| 249 |
+
}
|
| 250 |
+
.fab-sidebar-logo {
|
| 251 |
+
display: flex;
|
| 252 |
+
align-items: center;
|
| 253 |
+
gap: 12px;
|
| 254 |
+
padding: 22px 20px;
|
| 255 |
+
font-weight: 800;
|
| 256 |
+
font-size: 22px;
|
| 257 |
+
letter-spacing: -0.02em;
|
| 258 |
+
color: var(--brand);
|
| 259 |
+
cursor: pointer;
|
| 260 |
+
transition: background 0.15s;
|
| 261 |
+
border-bottom: 1px solid var(--border);
|
| 262 |
+
}
|
| 263 |
+
.fab-sidebar-logo .fab-logo-mark {
|
| 264 |
+
width: 38px;
|
| 265 |
+
height: 38px;
|
| 266 |
+
border-radius: 9px;
|
| 267 |
+
}
|
| 268 |
+
.fab-sidebar-logo .fab-logo-mark svg {
|
| 269 |
+
width: 22px;
|
| 270 |
+
height: 22px;
|
| 271 |
+
}
|
| 272 |
+
.fab-sidebar-logo:hover {
|
| 273 |
+
background: var(--bg-subtle);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
/* ============== Sidebar — Alarm Inbox ============== */
|
| 277 |
|
| 278 |
.fab-sidebar-head {
|
|
|
|
| 310 |
position: relative;
|
| 311 |
background: var(--bg-card);
|
| 312 |
border: 1px solid var(--border);
|
| 313 |
+
border-radius: 8px;
|
| 314 |
padding: 14px 14px 14px 16px;
|
| 315 |
cursor: pointer;
|
| 316 |
+
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
| 317 |
overflow: hidden;
|
| 318 |
margin: 0 12px;
|
| 319 |
}
|
| 320 |
+
a.alarm-card-link:hover .alarm-card {
|
| 321 |
+
border-color: var(--border-strong);
|
| 322 |
+
box-shadow: var(--shadow-card);
|
| 323 |
+
}
|
| 324 |
|
| 325 |
.alarm-card:hover {
|
| 326 |
background: var(--bg-subtle);
|