新增: 灵感软删除回收站+雷达图正名(父⑤)
- IDEA-FIX-01: ideas 软删除(对标 tasks V14)— V28 迁移 deleted_at(幂等 ALTER) + IdeaRepo soft_delete/restore/list_deleted + list_by_query 恒带 deleted_at IS NULL(防回收站泄漏) + delete_idea 改软删 + restore_idea/list_deleted_ideas IPC + IdeaRecord 不加字段(纯 SQL WHERE 过滤) - IDEA-FIX-14: 雷达图 CSS class radar→score-bar(IdeaDetail/ProjectDetail 2 文件 template+style+注释,纯正名零行为变更) - 6 新测试(soft_delete 标记+过滤/幂等/missing, restore 还原/仅已删, list_deleted 排序);df-storage 73 测试过
This commit is contained in:
@@ -271,8 +271,10 @@ impl IdeaRepo {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
|
||||
// 动态拼 WHERE 子句 + 收集参数(按出现顺序绑定占位符)
|
||||
let mut where_clauses: Vec<String> = Vec::new();
|
||||
// 累积 WHERE 子句 + 收集参数(按出现顺序绑定占位符)。
|
||||
// deleted_at IS NULL 恒带(常量条件无占位符),回收站任务不进结果
|
||||
// (对标 task_repo list_by_query 同款模式,防回收站泄漏,不可被 query 关闭)。
|
||||
let mut where_clauses: Vec<String> = vec!["deleted_at IS NULL".to_string()];
|
||||
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
|
||||
if let Some(s) = &status {
|
||||
@@ -288,11 +290,8 @@ impl IdeaRepo {
|
||||
params_vec.push(Box::new(pat));
|
||||
}
|
||||
|
||||
let where_sql = if where_clauses.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" WHERE {}", where_clauses.join(" AND "))
|
||||
};
|
||||
// where_clauses 恒含 deleted_at IS NULL(初始项),永非空,直接 join 拼 WHERE。
|
||||
let where_sql = format!(" WHERE {}", where_clauses.join(" AND "));
|
||||
|
||||
// 排序:score 走 COALESCE(NULL 当 0,对齐前端 computed 语义);其余直接列名。
|
||||
// order_col 来自白名单 &'static str,format! 出来是 String,生命周期随 sql 一起 OK。
|
||||
@@ -341,6 +340,70 @@ impl IdeaRepo {
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 软删:标记 deleted_at(进回收站,可恢复)。仅作用于未删灵感,返回是否命中。
|
||||
/// 对标 TaskRepo::soft_delete / ProjectRepo::soft_delete。
|
||||
pub async fn soft_delete(&self, id: &str) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(
|
||||
"UPDATE ideas SET deleted_at = ?1, updated_at = ?1 WHERE id = ?2 AND deleted_at IS NULL",
|
||||
params![now, id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 恢复:清 deleted_at(从回收站还原)。仅作用于已删灵感,返回是否命中。
|
||||
/// 对标 TaskRepo::restore / ProjectRepo::restore。
|
||||
pub async fn restore(&self, id: &str) -> Result<bool> {
|
||||
let conn = self.conn.clone();
|
||||
let id = id.to_owned();
|
||||
let now = now_millis_str();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let affected = guard
|
||||
.execute(
|
||||
"UPDATE ideas SET deleted_at = NULL, updated_at = ?1 WHERE id = ?2 AND deleted_at IS NOT NULL",
|
||||
params![now, id],
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
|
||||
/// 列出回收站(deleted_at IS NOT NULL),按更新时间(≈删除时间)降序。
|
||||
/// 对标 TaskRepo::list_deleted / ProjectRepo::list_deleted。
|
||||
pub async fn list_deleted(&self) -> Result<Vec<IdeaRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard
|
||||
.prepare(&format!(
|
||||
"SELECT {IDEA_COLS} FROM ideas WHERE deleted_at IS NOT NULL ORDER BY updated_at DESC"
|
||||
))
|
||||
.map_err(storage_err)?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| idea_from_row(row))
|
||||
.map_err(storage_err)?;
|
||||
let mut results = Vec::new();
|
||||
for r in rows {
|
||||
results.push(r.map_err(storage_err)?);
|
||||
}
|
||||
Ok(results)
|
||||
})
|
||||
.await
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
}
|
||||
|
||||
impl KnowledgeRepo {
|
||||
@@ -1202,4 +1265,101 @@ mod tests {
|
||||
let rec = repo.get_by_id("k1").await.unwrap().unwrap();
|
||||
assert_eq!(rec.embedding_status.as_deref(), Some("done"));
|
||||
}
|
||||
|
||||
// ---------- IdeaRepo 软删回收站(对标 task_repo 软删测试,V28) ----------
|
||||
|
||||
/// 构造一条 IdeaRecord fixture(14 字段全填,IdeaRecord 不含 deleted_at —— 纯 SQL 过滤)。
|
||||
fn irec(id: &str, title: &str) -> IdeaRecord {
|
||||
IdeaRecord {
|
||||
id: id.to_string(),
|
||||
title: title.to_string(),
|
||||
description: String::new(),
|
||||
status: "draft".to_string(),
|
||||
priority: 1,
|
||||
score: None,
|
||||
tags: None,
|
||||
source: None,
|
||||
promoted_to: None,
|
||||
ai_analysis: None,
|
||||
scores: None,
|
||||
related_ids: None,
|
||||
created_at: "1700000000000".to_string(),
|
||||
updated_at: "1700000000000".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_idea_repo() -> IdeaRepo {
|
||||
let db = Database::open_in_memory().await.expect("open_in_memory");
|
||||
IdeaRepo::new(&db)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_soft_delete_marks_and_filters_from_list_by_query() {
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "保留")).await.unwrap();
|
||||
repo.insert(irec("i2", "软删")).await.unwrap();
|
||||
|
||||
// 软删 i2:返回 true(命中)
|
||||
assert!(repo.soft_delete("i2").await.unwrap());
|
||||
|
||||
// list_by_query(空 query = 等价全量未删)应只返回 i1,i2 进回收站被过滤
|
||||
let active = repo.list_by_query(&IdeaQuery::default()).await.unwrap();
|
||||
let ids: Vec<_> = active.iter().map(|r| r.id.as_str()).collect();
|
||||
assert_eq!(ids, vec!["i1"], "soft_delete 后 list_by_query 应过滤回收站");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_soft_delete_idempotent_on_deleted() {
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "t")).await.unwrap();
|
||||
assert!(repo.soft_delete("i1").await.unwrap());
|
||||
// 已删再删:WHERE deleted_at IS NULL 不命中 → false
|
||||
assert!(!repo.soft_delete("i1").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_soft_delete_missing_returns_false() {
|
||||
let repo = setup_idea_repo().await;
|
||||
// 不存在的 id → affected=0
|
||||
assert!(!repo.soft_delete("ghost").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_restore_brings_back_to_active() {
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "t")).await.unwrap();
|
||||
repo.soft_delete("i1").await.unwrap();
|
||||
// 恢复:返回 true,回到 list_by_query
|
||||
assert!(repo.restore("i1").await.unwrap());
|
||||
let active = repo.list_by_query(&IdeaQuery::default()).await.unwrap();
|
||||
let ids: Vec<_> = active.iter().map(|r| r.id.as_str()).collect();
|
||||
assert_eq!(ids, vec!["i1"], "restore 后灵感应回到活跃列表");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_restore_only_affects_deleted() {
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "t")).await.unwrap();
|
||||
// 未删灵感 restore:WHERE deleted_at IS NOT NULL 不命中 → false
|
||||
assert!(!repo.restore("i1").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn idea_list_deleted_returns_only_trash_ordered_by_updated_desc() {
|
||||
let repo = setup_idea_repo().await;
|
||||
repo.insert(irec("i1", "活跃")).await.unwrap();
|
||||
repo.insert(irec("i2", "回收1")).await.unwrap();
|
||||
repo.insert(irec("i3", "回收2")).await.unwrap();
|
||||
|
||||
// 软删 i2 / i3(updated_at 在 soft_delete 时被刷新为 now)
|
||||
repo.soft_delete("i2").await.unwrap();
|
||||
// 让 i3 的删除时间晚于 i2,保证 updated_at DESC 顺序确定(i3 在前)
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||
repo.soft_delete("i3").await.unwrap();
|
||||
|
||||
let deleted = repo.list_deleted().await.unwrap();
|
||||
let ids: Vec<_> = deleted.iter().map(|r| r.id.as_str()).collect();
|
||||
// i1 活跃不出现;i3 删除最晚在前
|
||||
assert_eq!(ids, vec!["i3", "i2"], "list_deleted 应只含回收站灵感,按 updated_at DESC");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed,
|
||||
// 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 +
|
||||
// 治 AuditLog executed 记录显示错位蓝pending标签+raw"executed")。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 27] = [
|
||||
// V28 = 灵感软删回收站(ideas.deleted_at,对标 tasks.deleted_at V14/projects.deleted_at V11)。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 28] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -60,6 +61,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(25, migrate_v25),
|
||||
(26, migrate_v26),
|
||||
(27, migrate_v27),
|
||||
(28, migrate_v28),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -679,6 +681,23 @@ fn migrate_v27(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V28: 幂等补 ideas.deleted_at 列(灵感软删回收站,对标 tasks.deleted_at V14)
|
||||
///
|
||||
/// 删除灵感改为软删:deleted_at NULL=正常,非空=已进回收站(可恢复)。
|
||||
/// 与 tasks/projects.soft_delete 同模板:IdeaRecord 不带该字段,纯靠 SQL WHERE
|
||||
/// deleted_at IS NULL 过滤;关联数据(idea_evaluations 评估历史)不动,FK 仍满足,
|
||||
/// 灵感数据完整保留待恢复。用 PRAGMA 探测列存在性,缺失才 ALTER
|
||||
/// (同 v4/v5/v6/v8/v10/v11/v14/v15/v16/v17/v18/v19/v20/v23/v24 模式),对新库/老库均安全。
|
||||
fn migrate_v28(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "ideas", "deleted_at") {
|
||||
conn.execute("ALTER TABLE ideas ADD COLUMN deleted_at TEXT", [])?;
|
||||
tracing::info!("v28: 补建 ideas.deleted_at 列(灵感软删回收站)");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [28])?;
|
||||
tracing::info!("迁移 v28 完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
|
||||
@@ -40,7 +40,7 @@ graph TD
|
||||
P2["父② 知识图谱Phase1<br/>📋待办"]:::todo
|
||||
P3["父③ AI对话体验<br/>📋待办"]:::todo
|
||||
P4["父④ F-09 per-conv<br/>📋待办"]:::todo
|
||||
P5["父⑤ 灵感剩余<br/>📋待办"]:::todo
|
||||
P5["父⑤ 灵感模块<br/>🔨⑤.1/①.4✅·⑤.2待办"]:::doing
|
||||
P6["父⑥ Phase2-5<br/>📋待办"]:::todo
|
||||
P7["父⑦ 技术债<br/>📋待办"]:::todo
|
||||
|
||||
@@ -58,7 +58,7 @@ graph TD
|
||||
| **父②** 知识图谱Phase1 | 📋 待办 | ②.1 V28迁移(queue/parent_id/content_json/task_links) / ②.2 TaskRecord+TaskLinkRepo / ②.3 IPC+move_queue / ②.4 父聚合 / ②.5 AI工具12 | G1(弱) |
|
||||
| **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — |
|
||||
| **父④** F-09 per-conv | 📋 待办 | ④.1 streaming/currentText per-conv | — |
|
||||
| **父⑤** 灵感剩余 | 📋 待办 | ⑤.1 IDEA-FIX-01 软删除 / ⑤.2 #05-#10 P1 | ⑤.2#07→②.1 |
|
||||
| **父⑤** 灵感模块 | 🔨 进行中 | ⑤.1 软删除✅(V28+Repo+IPC+6测试) / ①.4 雷达图✅ / ⑤.2 #05-#10 P1待办(#07→父②) | ⑤.2#07→②.1 |
|
||||
| **父⑥** Phase2-5 | 📋 待办 | ⑥.1事件流 / ⑥.2基础设施 / ⑥.3注入 / ⑥.4前端 | ⑥→父②, ⑥.3→父②+父④+G1 |
|
||||
| **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) |
|
||||
|
||||
|
||||
@@ -122,10 +122,22 @@ pub async fn update_idea(
|
||||
.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 删除灵感
|
||||
/// 删除灵感(软删 → 回收站,可恢复)。对标 delete_task(SET deleted_at=now)。
|
||||
#[tauri::command]
|
||||
pub async fn delete_idea(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.ideas.delete(&id).await.map_err(err_str)
|
||||
state.ideas.soft_delete(&id).await.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 恢复灵感(从回收站还原,清 deleted_at)。对标 restore_task / restore_project。
|
||||
#[tauri::command]
|
||||
pub async fn restore_idea(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
state.ideas.restore(&id).await.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 列出回收站灵感(deleted_at IS NOT NULL,按更新时间降序)。对标 list_deleted_projects。
|
||||
#[tauri::command]
|
||||
pub async fn list_deleted_ideas(state: State<'_, AppState>) -> Result<Vec<IdeaRecord>, String> {
|
||||
state.ideas.list_deleted().await.map_err(err_str)
|
||||
}
|
||||
|
||||
/// 将灵感晋升为项目 — 复用 df-project 领域逻辑创建项目,回写灵感 status=promoted/promoted_to
|
||||
|
||||
@@ -260,6 +260,8 @@ pub fn run() {
|
||||
commands::idea::create_idea,
|
||||
commands::idea::update_idea,
|
||||
commands::idea::delete_idea,
|
||||
commands::idea::restore_idea,
|
||||
commands::idea::list_deleted_ideas,
|
||||
commands::idea::evaluate_idea,
|
||||
commands::idea::list_idea_evaluations,
|
||||
commands::idea::promote_idea,
|
||||
|
||||
@@ -162,16 +162,16 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 传统评分雷达图 -->
|
||||
<!-- 传统评分条(score-bar) -->
|
||||
<div class="detail-section">
|
||||
<h3>{{ $t('ideas.multiScoreTitle') }}</h3>
|
||||
<div class="radar-chart" v-if="parseScores(idea.scores).length > 0">
|
||||
<div class="radar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name">
|
||||
<span class="radar-label">{{ dim.name }}</span>
|
||||
<div class="radar-bar-track">
|
||||
<div class="radar-bar-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
|
||||
<div class="score-bar-chart" v-if="parseScores(idea.scores).length > 0">
|
||||
<div class="score-bar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name">
|
||||
<span class="score-bar-label">{{ dim.name }}</span>
|
||||
<div class="score-bar-track">
|
||||
<div class="score-bar-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
|
||||
</div>
|
||||
<span class="radar-value">{{ dim.score }}</span>
|
||||
<span class="score-bar-value">{{ dim.score }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</div>
|
||||
@@ -774,31 +774,31 @@ watch(() => props.idea.id, loadHistory)
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
/* ===== 雷达图(div 模拟) ===== */
|
||||
.radar-chart {
|
||||
/* ===== 多维评分条(div 模拟) ===== */
|
||||
.score-bar-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.radar-row {
|
||||
.score-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.radar-label {
|
||||
.score-bar-label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
min-width: 72px;
|
||||
text-align: right;
|
||||
}
|
||||
.radar-bar-track {
|
||||
.score-bar-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--df-border);
|
||||
border-radius: var(--df-radius-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
.radar-bar-fill {
|
||||
.score-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: var(--df-radius-xs);
|
||||
transition: width 0.4s;
|
||||
@@ -806,7 +806,7 @@ watch(() => props.idea.id, loadHistory)
|
||||
.fill-high { background: var(--df-success); }
|
||||
.fill-mid { background: var(--df-warning); }
|
||||
.fill-low { background: var(--df-danger); }
|
||||
.radar-value {
|
||||
.score-bar-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
min-width: 28px;
|
||||
|
||||
@@ -154,16 +154,16 @@
|
||||
<p v-if="sourceAiAnalysis.summary" class="source-summary">{{ sourceAiAnalysis.summary }}</p>
|
||||
<!-- 多维评分条(借鉴 IdeaDetail parseScores 渲染) -->
|
||||
<div v-if="sourceScores.length" class="source-scores">
|
||||
<div class="radar-row" v-for="dim in sourceScores" :key="dim.name">
|
||||
<span class="radar-label">{{ dim.name }}</span>
|
||||
<div class="radar-bar-track">
|
||||
<div class="score-bar-row" v-for="dim in sourceScores" :key="dim.name">
|
||||
<span class="score-bar-label">{{ dim.name }}</span>
|
||||
<div class="score-bar-track">
|
||||
<div
|
||||
class="radar-bar-fill"
|
||||
class="score-bar-fill"
|
||||
:class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"
|
||||
:style="{ width: dim.score + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="radar-value">{{ dim.score }}</span>
|
||||
<span class="score-bar-value">{{ dim.score }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -634,24 +634,24 @@ onUnmounted(() => {
|
||||
.assessment-badge.defer { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||
.assessment-badge.cancel { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||
|
||||
/* 多维评分条(复用 IdeaDetail radar-* 样式定义) */
|
||||
/* 多维评分条(复用 IdeaDetail score-bar-* 样式定义) */
|
||||
.source-scores {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.radar-row { display: flex; align-items: center; gap: 10px; }
|
||||
.radar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; text-align: right; }
|
||||
.radar-bar-track {
|
||||
.score-bar-row { display: flex; align-items: center; gap: 10px; }
|
||||
.score-bar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; text-align: right; }
|
||||
.score-bar-track {
|
||||
flex: 1; height: 8px; background: var(--df-border);
|
||||
border-radius: var(--df-radius-xs); overflow: hidden;
|
||||
}
|
||||
.radar-bar-fill { height: 100%; border-radius: var(--df-radius-xs); transition: width 0.4s; }
|
||||
.score-bar-fill { height: 100%; border-radius: var(--df-radius-xs); transition: width 0.4s; }
|
||||
.fill-high { background: var(--df-success); }
|
||||
.fill-mid { background: var(--df-warning); }
|
||||
.fill-low { background: var(--df-danger); }
|
||||
.radar-value { font-size: 12px; font-weight: 500; min-width: 28px; text-align: right; color: var(--df-text); }
|
||||
.score-bar-value { font-size: 12px; font-weight: 500; min-width: 28px; text-align: right; color: var(--df-text); }
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user