refactor(fe): replace utility classes with semantic CSS across components
- add ~400 lines of clean semantic CSS classes (msg-*, panel-*, search-*, filter-*, feed-*, auth-*, sidebar-*, etc.) - convert message_card.rs: .message-row -> .msg-row, .message-card -> .msg-card, remove all tailwind utility classes - convert messages/mod.rs: .card -> .panel-card, .search-row -> .search-bar, .filter-chip -> .filter-chip-v2, .message-stats -> .stats-bar - convert message_feed.rs: .space-y-4 -> .feed-wrap, .empty-state -> .feed-empty - convert auth.rs: inline styles -> .auth-* semantic classes - convert sidebar: .btn.btn-ghost -> .sidebar-btn.is-active - keep utility classes in CSS for backward compatibility with non-refactored components (dashboard, live)
This commit is contained in:
+1200
-1008
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,10 @@ pub fn App() -> impl IntoView {
|
||||
async move {
|
||||
match config_api::get_config().await {
|
||||
Ok(cfg) => {
|
||||
web_sys::console::log_2(
|
||||
&"[config] fetched OK".into(),
|
||||
&format!("monitorGuildId={:?}", cfg.monitor_guild_id).into(),
|
||||
);
|
||||
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -166,14 +170,9 @@ fn TabButton(tab: Tab, ui: UiContext, label: &'static str) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<button
|
||||
class:btn=true
|
||||
class:btn-ghost=true
|
||||
class:btn-active=move || active_tab.get() == tab1
|
||||
class="sidebar-btn"
|
||||
class:is-active=move || active_tab.get() == tab1
|
||||
on:click=move |_| active_tab.set(tab4.clone())
|
||||
style:background=move || if active_tab.get() == tab2 { "var(--surface-overlay)" } else { "" }
|
||||
style:color=move || if active_tab.get() == tab3 { "var(--color-primary)" } else { "" }
|
||||
style:width="100%"
|
||||
style:justify-content="flex-start"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
@@ -52,21 +52,17 @@ pub fn AuthOverlay() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content" style="width: 380px;">
|
||||
<div class="modal-body" style="text-align: center;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">
|
||||
<div class="modal-content auth-box">
|
||||
<div class="modal-body">
|
||||
<div class="auth-lock">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">
|
||||
"Akses Dashboard"
|
||||
</h2>
|
||||
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
|
||||
"Masukkan password admin untuk melanjutkan"
|
||||
</p>
|
||||
<form on:submit=handle_submit style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<h2 class="auth-title">"Akses Dashboard"</h2>
|
||||
<p class="auth-desc">"Masukkan password admin untuk melanjutkan"</p>
|
||||
<form class="auth-form" on:submit=handle_submit>
|
||||
<input
|
||||
type="password"
|
||||
class="input"
|
||||
@@ -75,11 +71,11 @@ pub fn AuthOverlay() -> impl IntoView {
|
||||
on:input=move |ev| set_password.set(event_target_value(&ev))
|
||||
/>
|
||||
{move || error.get().map(|e| view! {
|
||||
<p style="color: var(--color-error); font-size: 0.75rem;">{e}</p>
|
||||
<p class="auth-error">{e}</p>
|
||||
})}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-full btn-lg"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled=move || loading.get()
|
||||
>
|
||||
{move || if loading.get() { "Memproses..." } else { "Masuk" }}
|
||||
|
||||
@@ -189,23 +189,23 @@ pub fn MessageRow(
|
||||
let on_click_re = move |_| on_reanalyze(reanalyze_id.clone());
|
||||
|
||||
view! {
|
||||
<div class="message-row">
|
||||
<div class="msg-row">
|
||||
{/* Header */}
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span class="message-timestamp" title=time_ago(message.created_at)>
|
||||
<div class="msg-row-bar">
|
||||
<span class="msg-row-time" title=time_ago(message.created_at)>
|
||||
{fmt_time(message.created_at)}
|
||||
</span>
|
||||
{message.edited_at.is_some().then(|| view! {
|
||||
<span class="flex items-center gap-0.5 text-xs text-secondary">
|
||||
<span class="msg-row-badge edited">
|
||||
"✎ edited"
|
||||
</span>
|
||||
})}
|
||||
{message.deleted_at.is_some().then(|| view! {
|
||||
<span class="flex items-center gap-0.5 text-xs text-destructive">
|
||||
<span class="msg-row-badge deleted">
|
||||
"🗑 deleted"
|
||||
</span>
|
||||
})}
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<div class="msg-row-status">
|
||||
<StatusBadgeInline status=ai_st.clone() />
|
||||
{message.ai_severity.as_ref().filter(|s| **s != AiSeverity::None).map(|sev| view! {
|
||||
<span class=format!("badge text-xs {}", severity_class(sev))>{format!("{:?}", sev)}</span>
|
||||
@@ -213,19 +213,11 @@ pub fn MessageRow(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply - field removed from MessageRecord */}
|
||||
|
||||
{/* Forward - field removed from MessageRecord */}
|
||||
|
||||
{/* Crosspost - field removed from MessageRecord */}
|
||||
|
||||
{/* Content */}
|
||||
{show.then(|| {
|
||||
let rendered = render_emojis(display);
|
||||
let cls_str = if message.deleted_at.is_some() { "text-secondary/60" } else { "" };
|
||||
let class_str = format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls_str);
|
||||
view! {
|
||||
<p class=class_str>
|
||||
<p class="msg-row-body" class:is-deleted=message.deleted_at.is_some()>
|
||||
{rendered.into_iter().collect::<Vec<_>>()}
|
||||
</p>
|
||||
}
|
||||
@@ -233,7 +225,7 @@ pub fn MessageRow(
|
||||
|
||||
{/* Stickers */}
|
||||
{(!stickers.is_empty()).then(|| view! {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="msg-media-row">
|
||||
{stickers.iter().map(|s| {
|
||||
let url_owned = s.url.clone().unwrap_or_default();
|
||||
let name_owned = s.name.clone().unwrap_or_default();
|
||||
@@ -242,11 +234,11 @@ pub fn MessageRow(
|
||||
<div>
|
||||
{if has_url {
|
||||
view! {
|
||||
<img src=url_owned alt=name_owned class="h-12 w-12 rounded-lg border border-border object-contain bg-surface/50" loading="lazy" />
|
||||
<img src=url_owned alt=name_owned class="msg-sticker" loading="lazy" />
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-surface/50">
|
||||
<div class="msg-sticker-placeholder">
|
||||
"😊"
|
||||
</div>
|
||||
}.into_any()
|
||||
@@ -265,15 +257,15 @@ pub fn MessageRow(
|
||||
let url2 = a.url.clone();
|
||||
let name1 = a.name.clone();
|
||||
view! {
|
||||
<a href=url1 target="_blank" class="shrink-0 overflow-hidden rounded-lg border border-border">
|
||||
<img src=url2 alt=name1 class="h-16 w-16 object-cover hover:scale-105 transition-transform" loading="lazy" />
|
||||
<a href=url1 target="_blank" class="msg-thumb-link">
|
||||
<img src=url2 alt=name1 class="msg-thumb" loading="lazy" />
|
||||
</a>
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
let overflow = if imgs.len() > 4 {
|
||||
let extra = imgs.len() - 4;
|
||||
view! {
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
||||
<div class="msg-media-overflow">
|
||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"🖼"</span>
|
||||
</div>
|
||||
}.into_any()
|
||||
@@ -281,7 +273,7 @@ pub fn MessageRow(
|
||||
().into_any()
|
||||
};
|
||||
view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
<div class="msg-media-row is-scroll">
|
||||
{images_view}
|
||||
{overflow}
|
||||
</div>
|
||||
@@ -296,13 +288,13 @@ pub fn MessageRow(
|
||||
let videos_view = vids_local.iter().take(4).map(|a| {
|
||||
let url = a.url.clone();
|
||||
view! {
|
||||
<video src=url controls class="h-28 w-48 shrink-0 rounded-lg border border-border object-cover bg-black" preload="metadata"></video>
|
||||
<video src=url controls class="msg-video" preload="metadata"></video>
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
let overflow = if vids.len() > 4 {
|
||||
let extra = vids.len() - 4;
|
||||
view! {
|
||||
<div class="flex h-28 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
||||
<div class="msg-media-overflow tall">
|
||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"▶"</span>
|
||||
</div>
|
||||
}.into_any()
|
||||
@@ -310,7 +302,7 @@ pub fn MessageRow(
|
||||
().into_any()
|
||||
};
|
||||
view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
<div class="msg-media-row is-scroll">
|
||||
{videos_view}
|
||||
{overflow}
|
||||
</div>
|
||||
@@ -323,7 +315,7 @@ pub fn MessageRow(
|
||||
{if !cats.is_empty() {
|
||||
let cats_local = cats.clone();
|
||||
view! {
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="msg-cats">
|
||||
{cats_local.iter().map(|c| view! {
|
||||
<span class="badge badge-secondary text-xs">{c.clone()}</span>
|
||||
}).collect::<Vec<_>>()}
|
||||
@@ -335,17 +327,17 @@ pub fn MessageRow(
|
||||
|
||||
{/* AI Analysis */}
|
||||
{message.ai_analysis.as_ref().map(|analysis| {
|
||||
let border_str = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" };
|
||||
let f_cls = if ai_st == AiStatus::Flagged { "flagged" } else { "clean" };
|
||||
let icon = if ai_st == AiStatus::Flagged { "🚨" } else { "ℹ️" };
|
||||
let analysis_summary_str = analysis_summary.clone();
|
||||
let analysis_str = analysis.clone();
|
||||
view! {
|
||||
<div class=format!("rounded-lg px-3 py-2 {}", border_str)>
|
||||
<div class="flex items-start gap-2 text-xs">
|
||||
<span class="mt-0.5 shrink-0">{icon}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="block font-medium mb-1">{analysis_summary_str}</span>
|
||||
<div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis_str}</div>
|
||||
<div class=format!("msg-analysis {}", f_cls)>
|
||||
<div class="msg-analysis-row">
|
||||
<span class="msg-analysis-icon">{icon}</span>
|
||||
<div class="msg-analysis-body">
|
||||
<span class="msg-analysis-summary">{analysis_summary_str}</span>
|
||||
<div class="msg-analysis-text">{analysis_str}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,14 +348,14 @@ pub fn MessageRow(
|
||||
{message.ai_error.as_ref().map(|e| {
|
||||
let error_str = e.clone();
|
||||
view! {
|
||||
<div class="rounded-lg bg-warning/5 px-3 py-2 text-xs text-warning">
|
||||
<div class="msg-error">
|
||||
<span>"AI error: "{error_str}</span>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
{/* Re-analyze */}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="msg-actions">
|
||||
<button
|
||||
class=format!("btn btn-sm {}", if ai_st == AiStatus::Error { "btn-destructive" } else { "btn-outline" })
|
||||
on:click=on_click_re
|
||||
@@ -373,7 +365,7 @@ pub fn MessageRow(
|
||||
" Re-analyze"
|
||||
</button>
|
||||
{(ai_st == AiStatus::Error).then(|| view! {
|
||||
<span class="text-xs text-secondary/70">"Click to retry"</span>
|
||||
<span class="msg-retry-hint">"Click to retry"</span>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -404,39 +396,30 @@ pub fn MessageCard(
|
||||
format!("# {}", c.channel_name.as_deref().unwrap_or("?"))
|
||||
}
|
||||
});
|
||||
let card_cls = if deleted {
|
||||
"border-destructive/20 opacity-60"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let card_cls = if deleted { " is-deleted" } else { "" };
|
||||
|
||||
view! {
|
||||
<article class=format!("message-card shadow-sm transition-all {}", card_cls)>
|
||||
<div class="flex gap-3 p-4">
|
||||
<img src=avatar alt="" class="h-10 w-10 shrink-0 rounded-full object-cover ring-2 ring-primary/30" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-baseline gap-2 mb-2">
|
||||
<span class="font-semibold text-sm">{first.username.clone()}</span>
|
||||
<article class=format!("msg-card{}", card_cls)>
|
||||
<div class="msg-card-inner">
|
||||
<img src=avatar alt="" class="msg-card-avatar" />
|
||||
<div class="msg-card-body">
|
||||
<div class="msg-card-head">
|
||||
<span class="msg-card-name">{first.username.clone()}</span>
|
||||
{loc_label.as_ref().map(|l| {
|
||||
let label_str = l.clone();
|
||||
view! {
|
||||
<span class="flex items-center gap-1 text-xs text-secondary/50 bg-surface/50 px-1.5 py-0.5 rounded-full">
|
||||
"#" " " {label_str}
|
||||
</span>
|
||||
<span class="msg-card-channel">{label_str}</span>
|
||||
}
|
||||
})}
|
||||
<span class="text-xs text-secondary/60">
|
||||
<span class="msg-card-time">
|
||||
{time_ago(first.created_at)}
|
||||
{has_multi.then(|| format!(" · {} msgs", messages.len()))}
|
||||
</span>
|
||||
</div>
|
||||
<div class=if has_multi { "space-y-2.5" } else { "" }>
|
||||
<div class="msg-card-messages" class:separated=has_multi>
|
||||
{messages.into_iter().enumerate().map(|(i, msg)| {
|
||||
let sep = has_multi && i > 0;
|
||||
view! {
|
||||
<div class=if sep { "pt-2.5 border-t border-border/30" } else { "" }>
|
||||
<MessageRow message=msg on_reanalyze=on_reanalyze.clone() />
|
||||
</div>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</div>
|
||||
@@ -450,16 +433,16 @@ pub fn MessageCard(
|
||||
#[component]
|
||||
pub fn MessageCardSkeleton() -> impl IntoView {
|
||||
view! {
|
||||
<article class="message-card">
|
||||
<div class="flex gap-3 p-4">
|
||||
<div class="skeleton skeleton-circular" style="width:40px;height:40px"></div>
|
||||
<div class="min-w-0 flex-1 space-y-3">
|
||||
<div class="skeleton" style="height:20px;width:192px"></div>
|
||||
<div class="skeleton" style="height:16px;width:100%"></div>
|
||||
<div class="skeleton" style="height:16px;width:75%"></div>
|
||||
<div class="flex gap-2">
|
||||
<div class="skeleton" style="height:24px;width:64px;border-radius:9999px"></div>
|
||||
<div class="skeleton" style="height:24px;width:80px;border-radius:9999px"></div>
|
||||
<article class="msg-card">
|
||||
<div class="msg-skel">
|
||||
<div class="msg-skel-avatar"></div>
|
||||
<div class="msg-skel-lines">
|
||||
<div class="msg-skel-line" style="width:192px"></div>
|
||||
<div class="msg-skel-line" style="width:100%"></div>
|
||||
<div class="msg-skel-line" style="width:75%"></div>
|
||||
<div class="msg-skel-badges">
|
||||
<div class="msg-skel-badge" style="width:64px"></div>
|
||||
<div class="msg-skel-badge" style="width:80px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn MessageFeed(
|
||||
// Loading state
|
||||
if loading {
|
||||
return view! {
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
{std::iter::repeat_with(|| {
|
||||
use super::message_card::MessageCardSkeleton;
|
||||
view! { <MessageCardSkeleton /> }
|
||||
@@ -105,8 +105,8 @@ pub fn MessageFeed(
|
||||
|
||||
if messages.is_empty() {
|
||||
return view! {
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-title">
|
||||
<div class="feed-empty">
|
||||
<div class="feed-empty-title">
|
||||
{if empty_text.is_empty() { "No messages" } else { empty_text }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ pub fn MessageFeed(
|
||||
let loading_more_val = loading_more;
|
||||
|
||||
view! {
|
||||
<div class="space-y-4">
|
||||
<div class="feed-wrap">
|
||||
{groups.into_iter().map(|group| {
|
||||
let cb = on_reanalyze.clone();
|
||||
view! {
|
||||
@@ -131,7 +131,7 @@ pub fn MessageFeed(
|
||||
{has_more_val.then(|| {
|
||||
view! {
|
||||
<>
|
||||
<div node_ref=sentinel_ref class="h-4">
|
||||
<div node_ref=sentinel_ref class="feed-sentinel">
|
||||
{loading_more_val.then(|| {
|
||||
use super::message_card::MessageCardSkeleton;
|
||||
view! { <MessageCardSkeleton /> }
|
||||
@@ -141,7 +141,7 @@ pub fn MessageFeed(
|
||||
{(!loading_more_val).then(|| {
|
||||
let load_more_cb = on_load_more.clone();
|
||||
view! {
|
||||
<div class="mt-4 text-center">
|
||||
<div class="feed-loader">
|
||||
<button
|
||||
class="btn btn-outline btn-sm"
|
||||
on:click=move |_| {
|
||||
|
||||
@@ -80,12 +80,21 @@ pub fn use_messages() -> MessagesState {
|
||||
|
||||
match get_messages(&guild_id, Some(30), None, None).await {
|
||||
Ok(PageResult { data, next_cursor }) => {
|
||||
web_sys::console::log_3(
|
||||
&"[messages] fetch OK".into(),
|
||||
&format!("count={}", data.len()).into(),
|
||||
&format!("cursor={:?}", next_cursor).into(),
|
||||
);
|
||||
messages_signal.set(data);
|
||||
cursor_signal.set(next_cursor);
|
||||
current_guild_signal.set(Some(guild_id));
|
||||
set_loading.set(false);
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::log_2(
|
||||
&"[messages] fetch ERROR".into(),
|
||||
&format!("{}", e).into(),
|
||||
);
|
||||
error_signal.set(Some(format!("Failed to fetch messages: {}", e)));
|
||||
set_loading.set(false);
|
||||
}
|
||||
|
||||
@@ -186,10 +186,10 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
view! {
|
||||
<div class="messages-panel">
|
||||
{/* Header card */}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">"Messages"</div>
|
||||
<p class="card-description">
|
||||
<div class="panel-card">
|
||||
<div class="panel-card-head">
|
||||
<div class="panel-card-title">"Messages"</div>
|
||||
<p class="panel-card-desc">
|
||||
"Messages are automatically captured from all text channels. Real-time updates arrive via WebSocket."
|
||||
</p>
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
|
||||
{/* Stats badges */}
|
||||
{move || (total() > 0).then(|| view! {
|
||||
<div class="message-stats">
|
||||
<div class="stats-bar">
|
||||
<span class="badge badge-outline text-xs">{total()} " total" {state.has_more.get().then_some("+")}</span>
|
||||
<span class="badge badge-success text-xs">{clean()} " clean"</span>
|
||||
<span class="badge badge-primary text-xs">{flagged()} " flagged"</span>
|
||||
@@ -213,13 +213,11 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
})}
|
||||
|
||||
{/* Search + filters row */}
|
||||
<div class="search-row">
|
||||
<div class="relative flex-1" style="min-width:200px">
|
||||
{/* Search icon as SVG */}
|
||||
<svg width="16" height="16" style="position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:var(--color-primary)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
|
||||
<div class="search-bar">
|
||||
<div class="search-wrap">
|
||||
<svg width="16" height="16" class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
|
||||
<input
|
||||
class="input"
|
||||
style="padding-left:2.25rem;border-radius:9999px"
|
||||
class="search-input"
|
||||
placeholder="Search message content..."
|
||||
prop:value=search_query
|
||||
on:input=move |ev| set_search_query.set(event_target_value(&ev))
|
||||
@@ -258,20 +256,18 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
}
|
||||
disabled=move || retrying_all.get()
|
||||
>
|
||||
{/* Rotate CCW icon as SVN */}
|
||||
<svg class=format!("mr-1.5 h-3.5 w-3.5{}", if retrying_all.get() { " animate-spin" } else { "" }) xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"></path><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path></svg>
|
||||
<svg class=format!("mr-1.5 h-3.5 w-3.5{}", if retrying_all.get() { " icon-spin" } else { "" }) xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"></path><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path></svg>
|
||||
{move || if retrying_all.get() { "Retrying...".to_string() } else { format!("Retry All Errors ({})", err_count) }}
|
||||
</button>
|
||||
}
|
||||
})
|
||||
}}
|
||||
<div class="ml-auto flex items-center" style="gap:0.375rem">
|
||||
{/* Filter icon as SVG since lucide-leptos Filter unavailable */}
|
||||
<div class="filter-group">
|
||||
<svg width="16" height="16" style="color:var(--color-primary)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
|
||||
{FILTERS.iter().map(|f| {
|
||||
let f_ptr: &'static str = f;
|
||||
view! {
|
||||
<button class="filter-chip" class:active=move || ai_filter.get() == f_ptr on:click=move |_| set_filter(f_ptr) >{*f}</button>
|
||||
<button class="filter-chip-v2" class:is-active=move || ai_filter.get() == f_ptr on:click=move |_| set_filter(f_ptr) >{*f}</button>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</div>
|
||||
@@ -281,7 +277,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
{move || show_search.get().then(|| {
|
||||
let n = search_results.get().len();
|
||||
view! {
|
||||
<div class="text-sm text-secondary">
|
||||
<div class="search-count">
|
||||
"Found " {n} " result" {if n != 1 { "s" } else { "" }}
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user