hee_!J commited on
Commit
970697d
·
1 Parent(s): 61f3789

fix: 사이드바 UI 정리 (로고 이동·전체 카드 클릭·간격 보정)

Browse files
Files changed (4) hide show
  1. app.py +22 -0
  2. components/alarm_inbox.py +42 -33
  3. components/header.py +1 -12
  4. 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
- st.markdown(
56
- f"""
57
- <div class="{classes}">
58
- <div class="alarm-head">
59
- <span class="chip chip-{alarm['status']}">
60
- <span class="dot"></span>{STATUS_LABELS[alarm['status']]}
61
- </span>
62
- {selected_tag}
63
- </div>
64
- <div class="alarm-title">{alarm['title']}</div>
65
- <div class="alarm-meta">
66
- <span style="font-family: var(--mono); font-size: 11px;">{alarm['lot_id']}</span>
67
- {feat_html}
68
- </div>
69
- <div class="alarm-time">{alarm['time']}</div>
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
- /* 사이드바 알람 카드 아래의 Streamlit 버튼은 카드와 덩어리보이게 */
35
- section[data-testid="stSidebar"] .stButton button {
36
- width: 100%;
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
- section[data-testid="stSidebar"] .stButton button:hover {
46
- background: var(--bg-subtle);
47
- color: var(--brand);
 
 
 
 
 
 
 
 
 
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: var(--radius);
268
  padding: 14px 14px 14px 16px;
269
  cursor: pointer;
270
- transition: background 0.15s, border-color 0.15s, transform 0.1s;
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);