新增: 灵感软删除回收站+雷达图正名(父⑤)

- 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:
2026-06-26 20:42:48 +08:00
parent 308775cf4d
commit 005079f762
7 changed files with 230 additions and 37 deletions

View File

@@ -271,8 +271,10 @@ impl IdeaRepo {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let guard = conn.blocking_lock(); let guard = conn.blocking_lock();
// 动态拼 WHERE 子句 + 收集参数(按出现顺序绑定占位符) // 累积 WHERE 子句 + 收集参数(按出现顺序绑定占位符)
let mut where_clauses: Vec<String> = Vec::new(); // 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(); let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(s) = &status { if let Some(s) = &status {
@@ -288,11 +290,8 @@ impl IdeaRepo {
params_vec.push(Box::new(pat)); params_vec.push(Box::new(pat));
} }
let where_sql = if where_clauses.is_empty() { // where_clauses 恒含 deleted_at IS NULL(初始项),永非空,直接 join 拼 WHERE。
String::new() let where_sql = format!(" WHERE {}", where_clauses.join(" AND "));
} else {
format!(" WHERE {}", where_clauses.join(" AND "))
};
// 排序:score 走 COALESCE(NULL 当 0,对齐前端 computed 语义);其余直接列名。 // 排序:score 走 COALESCE(NULL 当 0,对齐前端 computed 语义);其余直接列名。
// order_col 来自白名单 &'static str,format! 出来是 String,生命周期随 sql 一起 OK。 // order_col 来自白名单 &'static str,format! 出来是 String,生命周期随 sql 一起 OK。
@@ -341,6 +340,70 @@ impl IdeaRepo {
.await .await
.map_err(storage_err)? .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 { impl KnowledgeRepo {
@@ -1202,4 +1265,101 @@ mod tests {
let rec = repo.get_by_id("k1").await.unwrap().unwrap(); let rec = repo.get_by_id("k1").await.unwrap().unwrap();
assert_eq!(rec.embedding_status.as_deref(), Some("done")); 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");
}
} }

View File

@@ -32,7 +32,8 @@ pub fn run(conn: &Connection) -> Result<()> {
// V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed, // V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed,
// 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 + // 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 +
// 治 AuditLog executed 记录显示错位蓝pending标签+raw"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), (1, migrate_v1),
(2, migrate_v2), (2, migrate_v2),
(3, migrate_v3), (3, migrate_v3),
@@ -60,6 +61,7 @@ pub fn run(conn: &Connection) -> Result<()> {
(25, migrate_v25), (25, migrate_v25),
(26, migrate_v26), (26, migrate_v26),
(27, migrate_v27), (27, migrate_v27),
(28, migrate_v28),
]; ];
for (version, migrate_fn) in steps { for (version, migrate_fn) in steps {
@@ -679,6 +681,23 @@ fn migrate_v27(conn: &Connection) -> Result<()> {
Ok(()) 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 表 /// V21 建表 SQL — 消息拆分存储 ai_messages 表
/// ///
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。 /// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。

View File

@@ -40,7 +40,7 @@ graph TD
P2["父② 知识图谱Phase1<br/>📋待办"]:::todo P2["父② 知识图谱Phase1<br/>📋待办"]:::todo
P3["父③ AI对话体验<br/>📋待办"]:::todo P3["父③ AI对话体验<br/>📋待办"]:::todo
P4["父④ F-09 per-conv<br/>📋待办"]:::todo P4["父④ F-09 per-conv<br/>📋待办"]:::todo
P5["父⑤ 灵感剩余<br/>📋待办"]:::todo P5["父⑤ 灵感模块<br/>🔨⑤.1/①.4✅·⑤.2待办"]:::doing
P6["父⑥ Phase2-5<br/>📋待办"]:::todo P6["父⑥ Phase2-5<br/>📋待办"]:::todo
P7["父⑦ 技术债<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(弱) | | **父②** 知识图谱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 审批状态机 | — | | **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — |
| **父④** F-09 per-conv | 📋 待办 | ④.1 streaming/currentText per-conv | — | | **父④** 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 | | **父⑥** Phase2-5 | 📋 待办 | ⑥.1事件流 / ⑥.2基础设施 / ⑥.3注入 / ⑥.4前端 | ⑥→父②, ⑥.3→父②+父④+G1 |
| **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) | | **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) |

View File

@@ -122,10 +122,22 @@ pub async fn update_idea(
.map_err(err_str) .map_err(err_str)
} }
/// 删除灵感 /// 删除灵感(软删 → 回收站,可恢复)。对标 delete_task(SET deleted_at=now)。
#[tauri::command] #[tauri::command]
pub async fn delete_idea(state: State<'_, AppState>, id: String) -> Result<bool, String> { 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 /// 将灵感晋升为项目 — 复用 df-project 领域逻辑创建项目,回写灵感 status=promoted/promoted_to

View File

@@ -260,6 +260,8 @@ pub fn run() {
commands::idea::create_idea, commands::idea::create_idea,
commands::idea::update_idea, commands::idea::update_idea,
commands::idea::delete_idea, commands::idea::delete_idea,
commands::idea::restore_idea,
commands::idea::list_deleted_ideas,
commands::idea::evaluate_idea, commands::idea::evaluate_idea,
commands::idea::list_idea_evaluations, commands::idea::list_idea_evaluations,
commands::idea::promote_idea, commands::idea::promote_idea,

View File

@@ -162,16 +162,16 @@
</template> </template>
</div> </div>
<!-- 传统评分雷达图 --> <!-- 传统评分(score-bar) -->
<div class="detail-section"> <div class="detail-section">
<h3>{{ $t('ideas.multiScoreTitle') }}</h3> <h3>{{ $t('ideas.multiScoreTitle') }}</h3>
<div class="radar-chart" v-if="parseScores(idea.scores).length > 0"> <div class="score-bar-chart" v-if="parseScores(idea.scores).length > 0">
<div class="radar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name"> <div class="score-bar-row" v-for="dim in parseScores(idea.scores)" :key="dim.name">
<span class="radar-label">{{ dim.name }}</span> <span class="score-bar-label">{{ dim.name }}</span>
<div class="radar-bar-track"> <div class="score-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-fill" :style="{ width: dim.score + '%' }" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"></div>
</div> </div>
<span class="radar-value">{{ dim.score }}</span> <span class="score-bar-value">{{ dim.score }}</span>
</div> </div>
</div> </div>
<div v-else class="eval-report" style="opacity:0.5">{{ $t('ideas.noEval') }}</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; margin: 6px 0 0;
} }
/* ===== 雷达图div 模拟) ===== */ /* ===== 多维评分条div 模拟) ===== */
.radar-chart { .score-bar-chart {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.radar-row { .score-bar-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.radar-label { .score-bar-label {
font-size: 12px; font-size: 12px;
color: var(--df-text-secondary); color: var(--df-text-secondary);
min-width: 72px; min-width: 72px;
text-align: right; text-align: right;
} }
.radar-bar-track { .score-bar-track {
flex: 1; flex: 1;
height: 8px; height: 8px;
background: var(--df-border); background: var(--df-border);
border-radius: var(--df-radius-xs); border-radius: var(--df-radius-xs);
overflow: hidden; overflow: hidden;
} }
.radar-bar-fill { .score-bar-fill {
height: 100%; height: 100%;
border-radius: var(--df-radius-xs); border-radius: var(--df-radius-xs);
transition: width 0.4s; transition: width 0.4s;
@@ -806,7 +806,7 @@ watch(() => props.idea.id, loadHistory)
.fill-high { background: var(--df-success); } .fill-high { background: var(--df-success); }
.fill-mid { background: var(--df-warning); } .fill-mid { background: var(--df-warning); }
.fill-low { background: var(--df-danger); } .fill-low { background: var(--df-danger); }
.radar-value { .score-bar-value {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
min-width: 28px; min-width: 28px;

View File

@@ -154,16 +154,16 @@
<p v-if="sourceAiAnalysis.summary" class="source-summary">{{ sourceAiAnalysis.summary }}</p> <p v-if="sourceAiAnalysis.summary" class="source-summary">{{ sourceAiAnalysis.summary }}</p>
<!-- 多维评分条(借鉴 IdeaDetail parseScores 渲染) --> <!-- 多维评分条(借鉴 IdeaDetail parseScores 渲染) -->
<div v-if="sourceScores.length" class="source-scores"> <div v-if="sourceScores.length" class="source-scores">
<div class="radar-row" v-for="dim in sourceScores" :key="dim.name"> <div class="score-bar-row" v-for="dim in sourceScores" :key="dim.name">
<span class="radar-label">{{ dim.name }}</span> <span class="score-bar-label">{{ dim.name }}</span>
<div class="radar-bar-track"> <div class="score-bar-track">
<div <div
class="radar-bar-fill" class="score-bar-fill"
:class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'" :class="dim.score >= 80 ? 'fill-high' : dim.score >= 60 ? 'fill-mid' : 'fill-low'"
:style="{ width: dim.score + '%' }" :style="{ width: dim.score + '%' }"
></div> ></div>
</div> </div>
<span class="radar-value">{{ dim.score }}</span> <span class="score-bar-value">{{ dim.score }}</span>
</div> </div>
</div> </div>
</template> </template>
@@ -634,24 +634,24 @@ onUnmounted(() => {
.assessment-badge.defer { background: var(--df-danger-bg); color: var(--df-danger); } .assessment-badge.defer { background: var(--df-danger-bg); color: var(--df-danger); }
.assessment-badge.cancel { 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 { .source-scores {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-top: 8px; margin-top: 8px;
} }
.radar-row { display: flex; align-items: center; gap: 10px; } .score-bar-row { display: flex; align-items: center; gap: 10px; }
.radar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; text-align: right; } .score-bar-label { font-size: 12px; color: var(--df-text-secondary); min-width: 64px; text-align: right; }
.radar-bar-track { .score-bar-track {
flex: 1; height: 8px; background: var(--df-border); flex: 1; height: 8px; background: var(--df-border);
border-radius: var(--df-radius-xs); overflow: hidden; 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-high { background: var(--df-success); }
.fill-mid { background: var(--df-warning); } .fill-mid { background: var(--df-warning); }
.fill-low { background: var(--df-danger); } .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> </style>