fix: 比较选项联动可视化——禁用开关灰显样式、已包含徽标与点击解释、字符级降级标记(0.5.4)
This commit is contained in:
@@ -100,7 +100,7 @@ src/
|
||||
- 作者:thzxx
|
||||
- 组织:MetonaTeam
|
||||
- 许可证:MIT License(见 [LICENSE](./LICENSE))
|
||||
- 版本:0.5.3
|
||||
- 版本:0.5.4
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-2
@@ -20,7 +20,7 @@ z = 补丁版本号(Patch)
|
||||
> **`x` 永远是 `0`,永远不要提升到 `1.0.0`。**
|
||||
> 版本迭代**只允许修改 `y` 和 `z`**,`x` 保持 `0` 不变。
|
||||
|
||||
当前基线版本:**`0.5.3`**
|
||||
当前基线版本:**`0.5.4`**
|
||||
|
||||
---
|
||||
|
||||
@@ -73,7 +73,8 @@ z = 补丁版本号(Patch)
|
||||
0.5.0 ← 字符级对比模式(已发布):新增“字符级对比”开关(空白与换行结构全部不参与判等,两侧归一化为字符流 diff,跨行重组也能判等) · 字符差异回映射到行(跨界行配对、单侧行回拉、锚行 zip 配对,输出仍为 DiffRow,视图/报告/导航零改动) · 归一化流超 20 万字符自动降级行级对比并提示(报告头部按实际语义描述) · 字符级开启时三个空白选项置灰(语义已包含)
|
||||
0.5.1 ← 字符级同行多块变更重复输出修复(已发布):字符换位专项测试暴露并修复两处回映射缺陷——同行内多处变更(ne-eq-ne 同行,如换位、同行两处替换)不再重复输出多个 modified · 回拉跨界判定与对齐行判定锚定同一 eq 块,多行换位不再把全 common 行误判为 added · 新增字符换位专项测试组(对齐任意性下的不变量固化)
|
||||
0.5.2 ← 稳健性收尾与偏好记忆(已发布):diff worker 运行崩溃 onerror 兜底(终止实例回退主线程同步计算,effect 取消/jobId 已推进的迟到错误不回填) · 主进程 will-navigate 导航白名单加固(仅放行回到应用首页) · nativeTheme 暗色主题(Windows 标题栏等系统控件跟随应用风格) · 比较选项与仅看差异开关持久化(localStorage 记忆用户偏好,损坏数据防御性回退默认) · 字符级降级报告选项构造与引擎降级分支对齐(补 trimWhitespace 置否)
|
||||
0.5.3 ← 稳健性补漏与偏好记忆收尾(当前):字符级对比少行大字符量输入改走 worker 后台计算(重输入判定补入字符量维度,行数防不住 diffChars O(ND) 耗时失控) · toast 队列化(最多 3 条堆叠、各自独立计时,二进制预警不再被行数预警顶掉) · 清空按钮二次确认(toast 提示 + 3 秒确认窗口,超时自动复位) · 窗口尺寸/位置记忆(关闭保存 bounds,启动恢复并钳制回工作区,损坏数据回退默认) · 文件对话框记忆上次打开目录 · 状态栏显示两侧行数 · 偏好读取挂载期单次化 · 主进程测试基建建立(windowState 纯逻辑模块 + vitest include 扩展至 src/main)
|
||||
0.5.3 ← 稳健性补漏与偏好记忆收尾(已发布):字符级对比少行大字符量输入改走 worker 后台计算(重输入判定补入字符量维度,行数防不住 diffChars O(ND) 耗时失控) · toast 队列化(最多 3 条堆叠、各自独立计时,二进制预警不再被行数预警顶掉) · 清空按钮二次确认(toast 提示 + 3 秒确认窗口,超时自动复位) · 窗口尺寸/位置记忆(关闭保存 bounds,启动恢复并钳制回工作区,损坏数据回退默认) · 文件对话框记忆上次打开目录 · 状态栏显示两侧行数 · 偏好读取挂载期单次化 · 主进程测试基建建立(windowState 纯逻辑模块 + vitest include 扩展至 src/main)
|
||||
0.5.4 ← 比较选项联动可视化与状态透明(当前):修复禁用开关零视觉反馈的样式缺陷(原 CSS 仅有 button:disabled,checkbox 禁用后外观不变,用户不知哪些可选) · 字符级开启时三个空白选项灰显勾选(循系统惯例表达“语义已包含且生效”,仅为视觉呈现不改写用户原状态,关闭字符级即复原) · 常显“已包含”徽标(原仅 hover title 可知禁用原因) · 点击被包含选项弹 toast 解释而非无反应 · 字符级因内容超限自动降级时开关旁常显“已降级”标记(状态透明)
|
||||
0.6.0 ← 新增功能(候选:主进程解码 worker 化 · Playwright E2E · 文件夹对比)
|
||||
...
|
||||
0.y.z ← 长期停留,永不进入 1.x
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ src/renderer/src/
|
||||
│ ├── ContextMenu.test.tsx # 右键菜单(含边缘防溢出)
|
||||
│ ├── DiffView.test.tsx # 对比视图(渲染/滚动同步/右键/拖放)
|
||||
│ ├── TextInputModal.test.tsx # 粘贴文本弹窗
|
||||
│ └── Toolbar.test.tsx # 工具栏(选项/导航/导出菜单向上弹出/计算中禁用/字符级开关与空白选项联动置灰)
|
||||
│ └── Toolbar.test.tsx # 工具栏(选项/导航/导出菜单向上弹出/计算中禁用/字符级开关与空白选项联动灰显勾选+已包含徽标+点击解释+已降级标记)
|
||||
└── __tests__/
|
||||
├── App.test.tsx # 应用集成级测试(加载/拖拽/粘贴/导出/容错/toast 队列/清空确认/状态栏行数/大输入回退)
|
||||
└── main.test.ts # 入口挂载冒烟测试
|
||||
@@ -117,7 +117,7 @@ src/renderer/src/
|
||||
- 清空二次确认:有内容时第一次点击仅提示不清空 / 确认窗口内再点执行清空 / 超时自动复位(再点仍先提示)/ 空态点击不弹确认
|
||||
- 状态栏行数:单侧/双侧加载显示对应行数(切分与引擎一致)/ 清空后随空态消失
|
||||
- 偏好持久化(比较选项 + 仅看差异开关):启动时恢复上次保存的状态(选项勾选与视图折叠生效)/ 损坏 JSON 回退默认且不崩溃 / 非布尔字段防御性忽略 / 切换选项后写回存储(用例间 beforeEach 清理 localStorage 防串扰)
|
||||
- 字符级对比:跨行重组开启后判为完全一致;开启时三个空白选项禁用;内容超限自动降级并 toast 提示;导出报告携带字符级说明;降级后报告按实际生效的行级语义描述选项
|
||||
- 字符级对比:跨行重组开启后判为完全一致;开启时三个空白选项灰显勾选并带"已包含"徽标,点击弹出解释提示,关闭后恢复用户原勾选状态;内容超限自动降级并 toast 提示(开关旁常显"已降级"标记);导出报告携带字符级说明;降级后报告按实际生效的行级语义描述选项
|
||||
- 忽略所有空白:仅行内空白不同的两侧文本开启后判为完全一致;导出报告携带比较选项说明
|
||||
- 大输入(超快路径阈值)在无 Worker 环境去抖到期后回退同步计算,完成后无计算中遮罩
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "DiffLens",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.4",
|
||||
"description": "DiffLens — 精美酷炫的文本对比桌面应用",
|
||||
"author": "thzxx",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -571,6 +571,10 @@ export default function App(): ReactElement {
|
||||
onlyDiff={onlyDiff}
|
||||
onOnlyDiffChange={setOnlyDiff}
|
||||
onSwap={swapPanes}
|
||||
onOptionBlocked={(label) =>
|
||||
showToast(`「${label}」已包含在字符级对比中,关闭字符级后可单独设置`)
|
||||
}
|
||||
charModeDowngraded={diff?.charModeDowngraded ?? false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -713,12 +713,44 @@ describe('App - 字符级对比', () => {
|
||||
await screen.findByText('导出报告')
|
||||
expect(screen.getByLabelText('忽略行首尾空白')).toBeEnabled()
|
||||
fireEvent.click(screen.getByLabelText('字符级对比'))
|
||||
expect(screen.getByLabelText('忽略行首尾空白')).toBeDisabled()
|
||||
expect(screen.getByLabelText('忽略空行')).toBeDisabled()
|
||||
expect(screen.getByLabelText('忽略所有空白')).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略行首尾空白/)).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略空行/)).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略所有空白/)).toBeDisabled()
|
||||
expect(screen.getByLabelText('忽略大小写')).toBeEnabled()
|
||||
})
|
||||
|
||||
// 字符级开启时空白选项 label 带“已包含”徽标,getByLabelText 需正则前缀匹配
|
||||
it('先勾选空白选项再开启字符级:灰显勾选+已包含徽标,关闭后恢复原状态', async () => {
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByText('打开左侧'))
|
||||
await screen.findByText('导出报告')
|
||||
// 用户先勾选“忽略所有空白”与“忽略空行”
|
||||
fireEvent.click(screen.getByLabelText('忽略所有空白'))
|
||||
fireEvent.click(screen.getByLabelText('忽略空行'))
|
||||
expect(screen.getByLabelText('忽略所有空白')).toBeChecked()
|
||||
// 开启字符级:三个空白选项全部灰显勾选(语义已包含)并带徽标
|
||||
fireEvent.click(screen.getByLabelText('字符级对比'))
|
||||
expect(screen.getByLabelText(/^忽略行首尾空白/)).toBeChecked()
|
||||
expect(screen.getByLabelText(/^忽略空行/)).toBeChecked()
|
||||
expect(screen.getByLabelText(/^忽略所有空白/)).toBeChecked()
|
||||
expect(screen.getAllByText('已包含')).toHaveLength(3)
|
||||
// 关闭字符级:回到用户自己的勾选状态(忽略所有空白与忽略空行仍勾选,行首尾空白未勾选)
|
||||
fireEvent.click(screen.getByLabelText('字符级对比'))
|
||||
expect(screen.getByLabelText('忽略行首尾空白')).not.toBeChecked()
|
||||
expect(screen.getByLabelText('忽略空行')).toBeChecked()
|
||||
expect(screen.getByLabelText('忽略所有空白')).toBeChecked()
|
||||
expect(screen.queryByText('已包含')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('点击被字符级包含的空白选项弹出解释提示', async () => {
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByText('打开左侧'))
|
||||
await screen.findByText('导出报告')
|
||||
fireEvent.click(screen.getByLabelText('字符级对比'))
|
||||
fireEvent.click(screen.getByLabelText(/^忽略所有空白/))
|
||||
expect(await screen.findByText(/「忽略所有空白」已包含在字符级对比中/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('内容超限时自动降级行级并给出提示', async () => {
|
||||
// 1900 行 × 106 字符 ≈ 201,400 归一化字符(超过 20 万阈值);
|
||||
// 总行数 1901 未超过快路径阈值,同步计算即时降级
|
||||
@@ -737,6 +769,8 @@ describe('App - 字符级对比', () => {
|
||||
await screen.findByText('导出报告')
|
||||
fireEvent.click(screen.getByLabelText('字符级对比'))
|
||||
expect(await screen.findByText(/已自动降级为行级对比/)).toBeInTheDocument()
|
||||
// 降级状态在工具栏可见:字符级开关旁出现“已降级”标记
|
||||
expect(await screen.findByText('已降级')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('导出报告携带字符级对比说明', async () => {
|
||||
|
||||
@@ -5,7 +5,13 @@ import { DiffSummary } from '../diff/diffEngine'
|
||||
|
||||
const summary: DiffSummary = { changedLines: 3, inserted: 1, deleted: 1, modified: 1 }
|
||||
|
||||
function setup(extra?: { computing?: boolean; charMode?: boolean }) {
|
||||
function setup(extra?: {
|
||||
computing?: boolean
|
||||
charMode?: boolean
|
||||
charModeDowngraded?: boolean
|
||||
baseOptions?: Partial<import('../diff/diffEngine').DiffOptions>
|
||||
onOptionBlocked?: (label: string) => void
|
||||
}) {
|
||||
const onOptionsChange = vi.fn()
|
||||
const onNav = vi.fn()
|
||||
const onExport = vi.fn()
|
||||
@@ -13,7 +19,14 @@ function setup(extra?: { computing?: boolean; charMode?: boolean }) {
|
||||
const onSwap = vi.fn()
|
||||
const utils = render(
|
||||
<Toolbar
|
||||
options={{ trimWhitespace: false, ignoreCase: false, ignoreBlankLines: false, ignoreAllWhitespace: false, charMode: extra?.charMode ?? false }}
|
||||
options={{
|
||||
trimWhitespace: false,
|
||||
ignoreCase: false,
|
||||
ignoreBlankLines: false,
|
||||
ignoreAllWhitespace: false,
|
||||
...extra?.baseOptions,
|
||||
charMode: extra?.charMode ?? false
|
||||
}}
|
||||
onOptionsChange={onOptionsChange}
|
||||
summary={summary}
|
||||
navIndex={0}
|
||||
@@ -24,6 +37,8 @@ function setup(extra?: { computing?: boolean; charMode?: boolean }) {
|
||||
onlyDiff={false}
|
||||
onOnlyDiffChange={onOnlyDiffChange}
|
||||
onSwap={onSwap}
|
||||
onOptionBlocked={extra?.onOptionBlocked}
|
||||
charModeDowngraded={extra?.charModeDowngraded ?? false}
|
||||
/>
|
||||
)
|
||||
return { onOptionsChange, onNav, onExport, onOnlyDiffChange, onSwap, ...utils }
|
||||
@@ -67,11 +82,62 @@ describe('Toolbar', () => {
|
||||
expect(onOptionsChange).toHaveBeenCalledWith({ trimWhitespace: false, ignoreCase: false, ignoreBlankLines: false, ignoreAllWhitespace: false, charMode: true })
|
||||
})
|
||||
|
||||
// 字符级开启时空白选项 label 带有“已包含”徽标,getByLabelText 需正则前缀匹配
|
||||
it('字符级对比开启时三个空白选项被禁用(语义已被包含)', () => {
|
||||
setup({ charMode: true })
|
||||
expect(screen.getByLabelText('忽略行首尾空白')).toBeDisabled()
|
||||
expect(screen.getByLabelText('忽略空行')).toBeDisabled()
|
||||
expect(screen.getByLabelText('忽略所有空白')).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略行首尾空白/)).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略空行/)).toBeDisabled()
|
||||
expect(screen.getByLabelText(/^忽略所有空白/)).toBeDisabled()
|
||||
})
|
||||
|
||||
it('字符级对比开启时空白选项灰显勾选(语义已生效)并渲染“已包含”徽标', () => {
|
||||
const { container } = setup({ charMode: true })
|
||||
// 灰显勾选:checked 仅为视觉呈现,表达语义已随字符级生效
|
||||
expect(screen.getByLabelText(/^忽略行首尾空白/)).toBeChecked()
|
||||
expect(screen.getByLabelText(/^忽略空行/)).toBeChecked()
|
||||
expect(screen.getByLabelText(/^忽略所有空白/)).toBeChecked()
|
||||
// 徽标常显(无需悬停 title)
|
||||
expect(screen.getAllByText('已包含')).toHaveLength(3)
|
||||
expect(container.querySelectorAll('.switch.disabled')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('字符级对比关闭时空白选项恢复用户原状态且不渲染徽标', () => {
|
||||
const { container } = setup({ baseOptions: { ignoreBlankLines: true } })
|
||||
// 用户原状态:仅忽略空行勾选
|
||||
expect(screen.getByLabelText('忽略行首尾空白')).not.toBeChecked()
|
||||
expect(screen.getByLabelText('忽略空行')).toBeChecked()
|
||||
expect(screen.getByLabelText('忽略所有空白')).not.toBeChecked()
|
||||
expect(screen.queryByText('已包含')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('.switch.disabled')).toBeNull()
|
||||
})
|
||||
|
||||
it('点击被包含的空白选项触发 onOptionBlocked 且不改写选项状态', () => {
|
||||
const onOptionBlocked = vi.fn()
|
||||
const { onOptionsChange } = setup({ charMode: true, onOptionBlocked })
|
||||
// 点击 label 本身(贴近真实用户点文字的行为;onClick preventDefault 阻止激活转发的干扰)
|
||||
const label = screen.getByLabelText(/^忽略所有空白/).closest('label') as HTMLElement
|
||||
fireEvent.click(label)
|
||||
expect(onOptionBlocked).toHaveBeenCalledWith('忽略所有空白')
|
||||
expect(onOptionsChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('字符级对比关闭时点击空白选项不触发 onOptionBlocked', () => {
|
||||
const onOptionBlocked = vi.fn()
|
||||
setup({ onOptionBlocked })
|
||||
fireEvent.click(screen.getByLabelText('忽略所有空白'))
|
||||
expect(onOptionBlocked).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('字符级因内容超限降级时开关旁显示“已降级”标记', () => {
|
||||
setup({ charMode: true, charModeDowngraded: true })
|
||||
expect(screen.getByText('已降级')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('未降级或字符级关闭时不显示“已降级”标记', () => {
|
||||
setup({ charMode: true })
|
||||
expect(screen.queryByText('已降级')).not.toBeInTheDocument()
|
||||
setup({ charModeDowngraded: true })
|
||||
expect(screen.queryByText('已降级')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('字符级对比开启时忽略大小写仍可用', () => {
|
||||
|
||||
@@ -24,6 +24,10 @@ interface ToolbarProps {
|
||||
onOnlyDiffChange: (v: boolean) => void
|
||||
/** 交换左右两侧 */
|
||||
onSwap: () => void
|
||||
/** 点击被字符级对比包含(禁用中)的空白选项时通知(App 弹 toast 解释) */
|
||||
onOptionBlocked?: (label: string) => void
|
||||
/** 字符级对比因内容超限自动降级为行级(开关旁显示“已降级”标记) */
|
||||
charModeDowngraded?: boolean
|
||||
}
|
||||
|
||||
export default function Toolbar({
|
||||
@@ -37,7 +41,9 @@ export default function Toolbar({
|
||||
computing,
|
||||
onlyDiff,
|
||||
onOnlyDiffChange,
|
||||
onSwap
|
||||
onSwap,
|
||||
onOptionBlocked,
|
||||
charModeDowngraded = false
|
||||
}: ToolbarProps): ReactElement {
|
||||
const set = (patch: Partial<DiffOptions>): void => onOptionsChange({ ...options, ...patch })
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
@@ -48,25 +54,37 @@ export default function Toolbar({
|
||||
|
||||
const canNav = navCount > 0
|
||||
|
||||
// 被包含选项的 label:灰显 + 点击解释(checkbox 本身 disabled 不响应,label 捕获点击给反馈)
|
||||
const blockedLabel = (label: string): ((e: { preventDefault(): void }) => void) | undefined =>
|
||||
options.charMode
|
||||
? (e) => {
|
||||
// 阻止 label 默认激活行为,统一走解释反馈
|
||||
e.preventDefault()
|
||||
onOptionBlocked?.(label)
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="tool-group">
|
||||
<span className="tool-label">比较选项</span>
|
||||
<label
|
||||
className="switch"
|
||||
className={'switch' + (options.charMode ? ' disabled' : '')}
|
||||
title={
|
||||
options.charMode
|
||||
? '已被“字符级对比”包含(字符级忽略全部空白与换行)'
|
||||
: '忽略行首尾空白差异'
|
||||
}
|
||||
onClick={blockedLabel('忽略行首尾空白')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.trimWhitespace}
|
||||
checked={options.charMode ? true : options.trimWhitespace}
|
||||
disabled={options.charMode}
|
||||
onChange={(e) => set({ trimWhitespace: e.target.checked })}
|
||||
/>
|
||||
忽略行首尾空白
|
||||
{options.charMode && <span className="opt-badge">已包含</span>}
|
||||
</label>
|
||||
<label className="switch">
|
||||
<input
|
||||
@@ -77,36 +95,40 @@ export default function Toolbar({
|
||||
忽略大小写
|
||||
</label>
|
||||
<label
|
||||
className="switch"
|
||||
className={'switch' + (options.charMode ? ' disabled' : '')}
|
||||
title={
|
||||
options.charMode
|
||||
? '已被“字符级对比”包含(字符级忽略全部空白与换行)'
|
||||
: '忽略空行差异(两侧空白行互相判等)'
|
||||
}
|
||||
onClick={blockedLabel('忽略空行')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.ignoreBlankLines ?? false}
|
||||
checked={options.charMode ? true : (options.ignoreBlankLines ?? false)}
|
||||
disabled={options.charMode}
|
||||
onChange={(e) => set({ ignoreBlankLines: e.target.checked })}
|
||||
/>
|
||||
忽略空行
|
||||
{options.charMode && <span className="opt-badge">已包含</span>}
|
||||
</label>
|
||||
<label
|
||||
className="switch"
|
||||
className={'switch' + (options.charMode ? ' disabled' : '')}
|
||||
title={
|
||||
options.charMode
|
||||
? '已被“字符级对比”包含(字符级忽略全部空白与换行)'
|
||||
: '忽略行内全部空白(空格/制表符/全角空格),仅比较实际文字;换行结构仍参与对比'
|
||||
}
|
||||
onClick={blockedLabel('忽略所有空白')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.ignoreAllWhitespace ?? false}
|
||||
checked={options.charMode ? true : (options.ignoreAllWhitespace ?? false)}
|
||||
disabled={options.charMode}
|
||||
onChange={(e) => set({ ignoreAllWhitespace: e.target.checked })}
|
||||
/>
|
||||
忽略所有空白
|
||||
{options.charMode && <span className="opt-badge">已包含</span>}
|
||||
</label>
|
||||
<label
|
||||
className="switch"
|
||||
@@ -118,6 +140,11 @@ export default function Toolbar({
|
||||
onChange={(e) => set({ charMode: e.target.checked })}
|
||||
/>
|
||||
字符级对比
|
||||
{options.charMode && charModeDowngraded && (
|
||||
<span className="opt-badge warn" title="内容较大,本次对比已自动降级为行级(忽略所有空白 + 忽略空行语义)">
|
||||
已降级
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -465,6 +465,35 @@ button:disabled {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* 被字符级对比包含的选项:整行灰显 + 不可点光标(勾选仅为视觉呈现,语义已随字符级生效) */
|
||||
.switch.disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.switch.disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 选项状态徽标:已包含 / 已降级 */
|
||||
.opt-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
letter-spacing: 0.5px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opt-badge.warn {
|
||||
color: var(--mod-fg);
|
||||
background: var(--mod-bg);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
/* 导航按钮 */
|
||||
.nav-btn {
|
||||
width: 28px;
|
||||
|
||||
Reference in New Issue
Block a user