Filter options
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 12m29s
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 12m29s
This commit is contained in:
Submodule src-tauri/easy-nostr updated: 1becf76264...9845dd025d
@@ -6,7 +6,13 @@ use tauri::{AppHandle, Manager, State};
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct RssConfig {
|
||||
pub urls: Vec<String>,
|
||||
pub feeds: Vec<RssFeed>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RssFeed {
|
||||
pub url: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -22,6 +28,7 @@ pub struct NewsArticle {
|
||||
pub content: String,
|
||||
pub author: String,
|
||||
pub created_at: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
fn get_config_path(app: &AppHandle) -> PathBuf {
|
||||
@@ -31,6 +38,81 @@ fn get_config_path(app: &AppHandle) -> PathBuf {
|
||||
.join("rss.ron")
|
||||
}
|
||||
|
||||
fn default_feeds() -> Vec<RssFeed> {
|
||||
vec![
|
||||
// 💻 Technik
|
||||
RssFeed {
|
||||
url: "https://www.heise.de/rss/heise-atom.xml".into(),
|
||||
category: "Technik".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://www.golem.de/rss.php?feed=ATOM1.0".into(),
|
||||
category: "Technik".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://www.theverge.com/rss/index.xml".into(),
|
||||
category: "Technik".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://arstechnica.com/feed/".into(),
|
||||
category: "Technik".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://www.phoronix.com/rss.php".into(),
|
||||
category: "Technik".into(),
|
||||
},
|
||||
// 📰 Nachrichten
|
||||
RssFeed {
|
||||
url: "https://www.tagesschau.de/xml/rss2".into(),
|
||||
category: "Nachrichten".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://www.spiegel.de/schlagzeilen/index.rss".into(),
|
||||
category: "Nachrichten".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://feeds.bbci.co.uk/news/technology/rss.xml".into(),
|
||||
category: "Nachrichten".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://feeds.reuters.com/reuters/technologyNews".into(),
|
||||
category: "Nachrichten".into(),
|
||||
},
|
||||
// 🔭 Wissenschaft
|
||||
RssFeed {
|
||||
url: "https://www.nasa.gov/news-release/feed/".into(),
|
||||
category: "Wissenschaft".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://www.sciencedaily.com/rss/computers_math/artificial_intelligence.xml"
|
||||
.into(),
|
||||
category: "Wissenschaft".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://deepmind.google/blog/rss.xml".into(),
|
||||
category: "Wissenschaft".into(),
|
||||
},
|
||||
// 🤖 KI
|
||||
RssFeed {
|
||||
url: "https://openai.com/blog/rss.xml".into(),
|
||||
category: "KI".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://venturebeat.com/feed/".into(),
|
||||
category: "KI".into(),
|
||||
},
|
||||
// 🐧 Open Source
|
||||
RssFeed {
|
||||
url: "https://lwn.net/headlines/rss".into(),
|
||||
category: "Open Source".into(),
|
||||
},
|
||||
RssFeed {
|
||||
url: "https://github.blog/feed/".into(),
|
||||
category: "Open Source".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_openrouter_key(key: String, state: State<'_, NewsState>) -> Result<(), String> {
|
||||
let mut lock = state.openrouter_key.lock().map_err(|_| "Lock failed")?;
|
||||
@@ -49,39 +131,45 @@ pub async fn save_groq_key(key: String, state: State<'_, NewsState>) -> Result<(
|
||||
pub async fn load_rss_config(
|
||||
app: AppHandle,
|
||||
state: State<'_, NewsState>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<RssFeed>, String> {
|
||||
let path = get_config_path(&app);
|
||||
if !path.exists() {
|
||||
let default_urls = vec![
|
||||
"https://www.nasa.gov/news-release/feed/".to_string(),
|
||||
"https://www.heise.de/rss/heise-atom.xml".to_string(),
|
||||
];
|
||||
let config = RssConfig {
|
||||
urls: default_urls.clone(),
|
||||
};
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
let defaults = default_feeds();
|
||||
|
||||
let existing_config: Option<RssConfig> = if path.exists() {
|
||||
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
ron::from_str(&content).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Datei fehlt ODER hat weniger als 3 Einträge → defaults schreiben
|
||||
let feeds = match existing_config {
|
||||
Some(config) if config.feeds.len() >= 3 => config.feeds,
|
||||
_ => {
|
||||
let config = RssConfig {
|
||||
feeds: defaults.clone(),
|
||||
};
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?;
|
||||
fs::write(&path, ron_str).map_err(|e| e.to_string())?;
|
||||
defaults
|
||||
}
|
||||
let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?;
|
||||
fs::write(&path, ron_str).map_err(|e| e.to_string())?;
|
||||
let mut lock = state.rss_config.lock().unwrap();
|
||||
lock.urls = default_urls.clone();
|
||||
return Ok(default_urls);
|
||||
}
|
||||
let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
let config: RssConfig = ron::from_str(&content).map_err(|e| e.to_string())?;
|
||||
};
|
||||
|
||||
let mut lock = state.rss_config.lock().unwrap();
|
||||
*lock = config.clone();
|
||||
Ok(config.urls)
|
||||
lock.feeds = feeds.clone();
|
||||
Ok(feeds)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_rss_urls(
|
||||
urls: Vec<String>,
|
||||
feeds: Vec<RssFeed>,
|
||||
app: AppHandle,
|
||||
state: State<'_, NewsState>,
|
||||
) -> Result<(), String> {
|
||||
let config = RssConfig { urls };
|
||||
let config = RssConfig { feeds };
|
||||
let path = get_config_path(&app);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
@@ -152,17 +240,18 @@ pub async fn fetch_ai_news(
|
||||
content,
|
||||
author: author.into(),
|
||||
created_at: "Gerade eben".into(),
|
||||
category: "KI".into(),
|
||||
}])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArticle>, String> {
|
||||
let urls = state.rss_config.lock().unwrap().urls.clone();
|
||||
let feeds = state.rss_config.lock().unwrap().feeds.clone();
|
||||
let mut all_articles = Vec::new();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
for url in urls {
|
||||
if let Ok(res) = client.get(&url).send().await {
|
||||
for feed_cfg in feeds {
|
||||
if let Ok(res) = client.get(&feed_cfg.url).send().await {
|
||||
if let Ok(bytes) = res.bytes().await {
|
||||
if let Ok(feed) = feed_rs::parser::parse(&bytes[..]) {
|
||||
let source = feed
|
||||
@@ -184,6 +273,7 @@ pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArtic
|
||||
),
|
||||
author: source.clone(),
|
||||
created_at: date,
|
||||
category: feed_cfg.category.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ pub struct NewsArticle {
|
||||
pub content: String,
|
||||
pub author: String,
|
||||
pub created_at: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct RssFeed {
|
||||
pub url: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -30,14 +37,23 @@ struct StringArgs {
|
||||
key: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct UrlsArgs {
|
||||
urls: Vec<String>,
|
||||
struct FeedsArgs {
|
||||
feeds: Vec<RssFeed>,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct AiArgs {
|
||||
provider: String,
|
||||
}
|
||||
|
||||
const CATEGORIES: &[&str] = &[
|
||||
"Alle",
|
||||
"Technik",
|
||||
"Nachrichten",
|
||||
"Wissenschaft",
|
||||
"KI",
|
||||
"Open Source",
|
||||
];
|
||||
|
||||
#[function_component(News)]
|
||||
pub fn news() -> Html {
|
||||
let articles = use_state(|| None::<Vec<NewsArticle>>);
|
||||
@@ -45,20 +61,22 @@ pub fn news() -> Html {
|
||||
let show_config = use_state(|| false);
|
||||
let current_mode = use_state(|| NewsMode::Ai);
|
||||
let ai_provider = use_state(|| "openrouter".to_string());
|
||||
let rss_urls = use_state(Vec::<String>::new);
|
||||
let rss_feeds = use_state(Vec::<RssFeed>::new);
|
||||
let selected_category = use_state(|| "Alle".to_string());
|
||||
|
||||
let or_key_ref = use_node_ref();
|
||||
let groq_key_ref = use_node_ref();
|
||||
let new_url_ref = use_node_ref();
|
||||
let new_cat_ref = use_node_ref();
|
||||
|
||||
// Init: RSS laden
|
||||
// Init: RSS Feeds laden
|
||||
{
|
||||
let rss_urls = rss_urls.clone();
|
||||
let rss_feeds = rss_feeds.clone();
|
||||
use_effect_with((), move |_| {
|
||||
spawn_local(async move {
|
||||
if let Ok(res) = invoke("load_rss_config", JsValue::NULL).await {
|
||||
if let Ok(urls) = serde_wasm_bindgen::from_value::<Vec<String>>(res) {
|
||||
rss_urls.set(urls);
|
||||
if let Ok(feeds) = serde_wasm_bindgen::from_value::<Vec<RssFeed>>(res) {
|
||||
rss_feeds.set(feeds);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,7 +116,7 @@ pub fn news() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// Auto-Reload bei Switch
|
||||
// Auto-Reload bei Mode/Provider-Switch
|
||||
{
|
||||
let load_news = load_news.clone();
|
||||
use_effect_with(
|
||||
@@ -111,7 +129,7 @@ pub fn news() -> Html {
|
||||
}
|
||||
|
||||
let save_settings = {
|
||||
let rss_urls = rss_urls.clone();
|
||||
let rss_feeds = rss_feeds.clone();
|
||||
let or_ref = or_key_ref.clone();
|
||||
let groq_ref = groq_key_ref.clone();
|
||||
let show_config = show_config.clone();
|
||||
@@ -125,7 +143,7 @@ pub fn news() -> Html {
|
||||
.cast::<HtmlInputElement>()
|
||||
.map(|i| i.value())
|
||||
.unwrap_or_default();
|
||||
let urls = (*rss_urls).clone();
|
||||
let feeds = (*rss_feeds).clone();
|
||||
let show_config = show_config.clone();
|
||||
let load_news = load_news.clone();
|
||||
spawn_local(async move {
|
||||
@@ -141,7 +159,7 @@ pub fn news() -> Html {
|
||||
.await;
|
||||
let _ = invoke(
|
||||
"save_rss_urls",
|
||||
serde_wasm_bindgen::to_value(&UrlsArgs { urls }).unwrap(),
|
||||
serde_wasm_bindgen::to_value(&FeedsArgs { feeds }).unwrap(),
|
||||
)
|
||||
.await;
|
||||
show_config.set(false);
|
||||
@@ -150,6 +168,14 @@ pub fn news() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// Gefilterte Artikel für die Anzeige
|
||||
let filtered_articles: Vec<NewsArticle> = (*articles)
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|a| *selected_category == "Alle" || a.category == *selected_category)
|
||||
.collect();
|
||||
|
||||
html! {
|
||||
<div class="feed-container">
|
||||
<div class="feed-header">
|
||||
@@ -178,6 +204,24 @@ pub fn news() -> Html {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ── Kategorie-Filterleiste ──────────────────────────────────────
|
||||
if *current_mode == NewsMode::Rss {
|
||||
<div class="category-bar">
|
||||
{ for CATEGORIES.iter().map(|&cat| {
|
||||
let selected_category = selected_category.clone();
|
||||
let is_active = *selected_category == cat;
|
||||
html! {
|
||||
<button
|
||||
class={if is_active { "cat-btn active" } else { "cat-btn" }}
|
||||
onclick={Callback::from(move |_| selected_category.set(cat.to_string()))}>
|
||||
{ cat }
|
||||
</button>
|
||||
}
|
||||
}) }
|
||||
</div>
|
||||
}
|
||||
|
||||
// ── Einstellungen ───────────────────────────────────────────────
|
||||
if *show_config {
|
||||
<div class="config-panel">
|
||||
<div class="config-section">
|
||||
@@ -190,19 +234,51 @@ pub fn news() -> Html {
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<label>{"RSS FEEDS"}</label>
|
||||
<div class="rss-input-row">
|
||||
<input ref={new_url_ref.clone()} type="text" class="config-input" placeholder="URL..." />
|
||||
|
||||
// Bestehende Feeds auflisten
|
||||
<div class="rss-list-scroll">
|
||||
{ for (*rss_feeds).iter().enumerate().map(|(i, feed)| {
|
||||
let rss_feeds = rss_feeds.clone();
|
||||
let url_display = feed.url.clone();
|
||||
let cat_display = feed.category.clone();
|
||||
let on_delete = Callback::from(move |_| {
|
||||
let mut list = (*rss_feeds).clone();
|
||||
list.remove(i);
|
||||
rss_feeds.set(list);
|
||||
});
|
||||
html! {
|
||||
<div class="rss-url-entry" key={url_display.clone()}>
|
||||
<span class="url-text">{ &url_display }</span>
|
||||
<span class="cat-badge">{ &cat_display }</span>
|
||||
<button class="delete-btn" onclick={on_delete}>{"🗑️"}</button>
|
||||
</div>
|
||||
}
|
||||
}) }
|
||||
</div>
|
||||
|
||||
// Neue URL + Kategorie hinzufügen
|
||||
<div class="rss-add-row">
|
||||
<input ref={new_url_ref.clone()} type="text" class="config-input" placeholder="https://..." />
|
||||
<select ref={new_cat_ref.clone()} class="cat-select">
|
||||
{ for CATEGORIES.iter().skip(1).map(|&cat| html! {
|
||||
<option value={cat}>{ cat }</option>
|
||||
}) }
|
||||
</select>
|
||||
<button class="add-btn" onclick={
|
||||
let rss_urls = rss_urls.clone();
|
||||
let new_url_ref = new_url_ref.clone();
|
||||
let rss_feeds = rss_feeds.clone();
|
||||
let url_ref = new_url_ref.clone();
|
||||
let cat_ref = new_cat_ref.clone();
|
||||
move |_| {
|
||||
if let Some(input) = new_url_ref.cast::<HtmlInputElement>() {
|
||||
let val = input.value();
|
||||
if !val.trim().is_empty() {
|
||||
let mut list = (*rss_urls).clone();
|
||||
list.push(val);
|
||||
rss_urls.set(list);
|
||||
input.set_value("");
|
||||
let url_input = url_ref.cast::<HtmlInputElement>();
|
||||
let cat_input = cat_ref.cast::<HtmlInputElement>();
|
||||
if let (Some(u), Some(c)) = (url_input, cat_input) {
|
||||
let url = u.value();
|
||||
let category = c.value();
|
||||
if !url.trim().is_empty() {
|
||||
let mut list = (*rss_feeds).clone();
|
||||
list.push(RssFeed { url, category });
|
||||
rss_feeds.set(list);
|
||||
u.set_value("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,13 +289,16 @@ pub fn news() -> Html {
|
||||
</div>
|
||||
}
|
||||
|
||||
// ── Artikel-Liste ───────────────────────────────────────────────
|
||||
<div class="posts-list">
|
||||
{ if let Some(msg) = (*error_msg).clone() {
|
||||
html! { <div class="error-box">{ format!("⚠️ Error: {}", msg) }</div> }
|
||||
} else if let Some(list) = (*articles).clone() {
|
||||
html! { for list.iter().map(|a| html! { <NewsCard article={a.clone()} /> }) }
|
||||
} else {
|
||||
} else if (*articles).is_none() {
|
||||
html! { <div class="loading-spinner">{"Lade Nachrichten..."}</div> }
|
||||
} else if filtered_articles.is_empty() {
|
||||
html! { <div class="empty-msg">{"Keine Artikel in dieser Kategorie."}</div> }
|
||||
} else {
|
||||
html! { for filtered_articles.iter().map(|a| html! { <NewsCard article={a.clone()} /> }) }
|
||||
} }
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,7 +325,10 @@ pub fn news_card(props: &NewsCardProps) -> Html {
|
||||
<div class="post-card">
|
||||
<div class="post-header">
|
||||
<span class="post-author">{ &a.author }</span>
|
||||
<span class="post-date">{ &a.created_at }</span>
|
||||
<div class="post-meta">
|
||||
<span class="post-category-badge">{ &a.category }</span>
|
||||
<span class="post-date">{ &a.created_at }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-content markdown-body">
|
||||
{ Html::from_html_unchecked(AttrValue::from(markdown_html)) }
|
||||
|
||||
110
styles.css
110
styles.css
@@ -51,7 +51,8 @@ html {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
/* Safe Areas: Top inset and mobile-friendly padding */
|
||||
padding: calc(20px + env(safe-area-inset-top)) 16px calc(120px + env(safe-area-inset-bottom)) 16px;
|
||||
padding: calc(20px + env(safe-area-inset-top)) 16px
|
||||
calc(120px + env(safe-area-inset-bottom)) 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -361,7 +362,6 @@ html {
|
||||
|
||||
/* --- Animations --- */
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
@@ -387,9 +387,11 @@ html {
|
||||
/* --- Utilities --- */
|
||||
.reload-btn-large {
|
||||
cursor: pointer;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(74, 144, 226, 0.15),
|
||||
rgba(74, 144, 226, 0.05));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.15),
|
||||
rgba(74, 144, 226, 0.05)
|
||||
);
|
||||
color: var(--accent-color);
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
@@ -589,4 +591,100 @@ html {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Category filter --- */
|
||||
/* ── Kategorie-Filterleiste ── */
|
||||
.category-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.category-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cat-btn {
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.cat-btn.active {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
.cat-btn:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Kategorie-Badge im Feed-Eintrag ── */
|
||||
.cat-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-color);
|
||||
background: rgba(74, 144, 226, 0.12);
|
||||
border: 1px solid rgba(74, 144, 226, 0.25);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
margin: 0 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Kategorie-Badge auf NewsCard ── */
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.post-category-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-color);
|
||||
background: rgba(74, 144, 226, 0.12);
|
||||
border: 1px solid rgba(74, 144, 226, 0.25);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ── Neue URL-Zeile mit Kategorie-Dropdown ── */
|
||||
.rss-add-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.cat-select {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 14px 10px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
width: 130px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.cat-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
.cat-select option {
|
||||
background: #1e1e2e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user