dsh-vscode-mode 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/lib/client.js +885 -32
- package/lib/client.js.map +1 -1
- package/lib/index.js +562 -3
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- package/src/client/index.ts +9 -2
- package/src/client/paths.ts +2 -0
- package/src/client/rulesMdc.ts +141 -0
- package/src/client/sidebar/SidebarView.ts +83 -16
- package/src/client/sidebar/panels/RulesPanel.ts +445 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/sidebarMin.ts +48 -0
- package/src/client/styles/editor.css +53 -1
- package/src/client/ui/EditorView.ts +22 -4
- package/src/client/ui/McpSettings.ts +37 -2
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/fileOpenSettings.ts +2 -0
- package/src/index.ts +5 -1
- package/src/rpc.ts +37 -0
- package/src/rules.ts +528 -0
- package/src/shared/rpc.ts +11 -0
- package/src/shared/rules.ts +60 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏「规则」面板(Codebuddy 规则管理形态)。
|
|
4
|
+
* 双 Tab:用户规则(~/.dsh/rules/)与项目规则(<工作区>/.dsh/rules/);
|
|
5
|
+
* 列表行 = 文件名 + 相对提示,第二行类型徽标与描述,右侧 编辑/删除 图标 + 滑动启用开关;
|
|
6
|
+
* 新建/编辑 = 顶部面包屑 + 规则类型下拉(总是/自动/手动)+ 描述/globs 控件,与原始 .mdc
|
|
7
|
+
* 文本域双向同步(控件改写 frontmatter,文本域回读控件),保存走 rules.save。
|
|
8
|
+
* 作者 ddj 2026年09月03号(2026年09月07号 交互性调整)
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react'
|
|
11
|
+
import { rpc } from '../../rpc.js'
|
|
12
|
+
import type { SidebarCtx } from '../types.js'
|
|
13
|
+
import { CACHE_KEY } from '../../paths.js'
|
|
14
|
+
import { parseRuleFm, applyRuleMeta } from '../../rulesMdc.js'
|
|
15
|
+
|
|
16
|
+
/** 新建规则模板(frontmatter 字段与 host parseRuleMdc 对齐)。 */
|
|
17
|
+
const NEW_TEMPLATE = '---\ndescription: \nalwaysApply: true\n---\n\n'
|
|
18
|
+
const TYPE_LABEL = { always: '总是', auto: '自动', manual: '手动' }
|
|
19
|
+
/** 列表行描述的基础显示上限(字符数;宽面板下的封顶值,超出省略号)。 */
|
|
20
|
+
const DESC_MAX = 24
|
|
21
|
+
/** 动态字数下限(极窄面板的保底可见字符数)。 */
|
|
22
|
+
const DESC_CAP_MIN = 8
|
|
23
|
+
/** 动态字数估算:描述之外的行内固定占用 px(动作区/类型标签/徽标/间距/省略余量)。 */
|
|
24
|
+
const DESC_RESERVE_PX = 210
|
|
25
|
+
/** 动态字数估算:单字符平均宽度 px(11px 字号,中英混排)。 */
|
|
26
|
+
const DESC_CHAR_PX = 10
|
|
27
|
+
/** 窄面板阈值:低于此宽度隐藏路径提示(编辑界面面包屑与悬停仍有完整路径)。 */
|
|
28
|
+
const NARROW_W = 420
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 读 localStorage(损坏/不可用安全)。
|
|
32
|
+
* @author ddj 2026年09月03号
|
|
33
|
+
* @param key 键
|
|
34
|
+
* @returns 值或 null
|
|
35
|
+
*/
|
|
36
|
+
function readLocal(key) {
|
|
37
|
+
try { return localStorage.getItem(key) } catch (e) { return null }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 写 localStorage(不可用安全)。
|
|
42
|
+
* @author ddj 2026年09月03号
|
|
43
|
+
* @param key 键
|
|
44
|
+
* @param value 值
|
|
45
|
+
*/
|
|
46
|
+
function writeLocal(key, value) {
|
|
47
|
+
try { localStorage.setItem(key, value) } catch (e) { /* 忽略 */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 类型徽标(总是=绿 / 自动=蓝 / 手动=灰)。
|
|
52
|
+
* @author ddj 2026年09月03号
|
|
53
|
+
* @param type 规则类型
|
|
54
|
+
* @returns 徽标元素
|
|
55
|
+
*/
|
|
56
|
+
function typeBadge(type) {
|
|
57
|
+
return React.createElement('span', { className: 'edrv-rules-type edrv-rules-type-' + type }, TYPE_LABEL[type] ?? type)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 按字符上限截断列表行描述,超出以省略号收尾(上限随面板宽度动态变化;
|
|
62
|
+
* 窄面板下 CSS 省略号仍会按像素提前截断,二者取更小)。
|
|
63
|
+
* @author ddj 2026年09月07号
|
|
64
|
+
* @param text 完整描述
|
|
65
|
+
* @param cap 本帧字符上限
|
|
66
|
+
* @returns 截断后的展示文本
|
|
67
|
+
*/
|
|
68
|
+
function clampDesc(text, cap) {
|
|
69
|
+
const chars = Array.from(String(text ?? '')) // 按码位切分,避免把 emoji 等代理对从中间截断
|
|
70
|
+
const limit = Math.max(4, Number(cap) || DESC_MAX)
|
|
71
|
+
return chars.length > limit ? chars.slice(0, limit).join('') + '…' : chars.join('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 由面板宽度估算描述可用字符数:随宽度动态变化,并与基础上限取小、极窄时保底。
|
|
76
|
+
* @author ddj 2026年09月07号
|
|
77
|
+
* @param panelWidth 面板内容宽度(px)
|
|
78
|
+
* @returns 本帧描述字符上限
|
|
79
|
+
*/
|
|
80
|
+
function descCapOf(panelWidth) {
|
|
81
|
+
const byWidth = Math.floor((Number(panelWidth) - DESC_RESERVE_PX) / DESC_CHAR_PX)
|
|
82
|
+
return Math.max(DESC_CAP_MIN, Math.min(DESC_MAX, byWidth))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 编辑图标(线性铅笔,与 DiffBox chevron 同风格:currentColor 描边装饰图)。
|
|
87
|
+
* @author ddj 2026年09月07号
|
|
88
|
+
* @returns 13×13 装饰性 SVG
|
|
89
|
+
*/
|
|
90
|
+
function pencilIcon() {
|
|
91
|
+
return React.createElement('svg', {
|
|
92
|
+
width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
93
|
+
}, React.createElement('path', {
|
|
94
|
+
d: 'M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z',
|
|
95
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 删除图标(线性垃圾桶,风格同 pencilIcon)。
|
|
101
|
+
* @author ddj 2026年09月07号
|
|
102
|
+
* @returns 13×13 装饰性 SVG
|
|
103
|
+
*/
|
|
104
|
+
function trashIcon() {
|
|
105
|
+
return React.createElement('svg', {
|
|
106
|
+
width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
107
|
+
}, React.createElement('path', {
|
|
108
|
+
d: 'M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14zM10 11v6M14 11v6',
|
|
109
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
110
|
+
}))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 逗号分隔的 globs 输入串 → 数组(去空项)。
|
|
115
|
+
* @author ddj 2026年09月07号
|
|
116
|
+
* @param text 输入串
|
|
117
|
+
* @returns glob 数组
|
|
118
|
+
*/
|
|
119
|
+
function globList(text) {
|
|
120
|
+
return String(text ?? '').split(',').map((item) => item.trim()).filter(Boolean)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 由控件值重建 .mdc 全文:frontmatter 按类型/描述/globs 重写,正文与其余键不动。
|
|
125
|
+
* @author ddj 2026年09月07号
|
|
126
|
+
* @param base 当前全文
|
|
127
|
+
* @param fmType 目标类型(always/auto/manual)
|
|
128
|
+
* @param desc 描述文本
|
|
129
|
+
* @param globsText globs 输入串
|
|
130
|
+
* @returns 重写后的全文
|
|
131
|
+
*/
|
|
132
|
+
function rebuildContent(base, fmType, desc, globsText) {
|
|
133
|
+
return applyRuleMeta(parseRuleFm(base), { type: fmType, description: desc, globs: globList(globsText) })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --region 列表行
|
|
137
|
+
/**
|
|
138
|
+
* 渲染一条规则行:左侧两行(文件名+相对提示 / 类型徽标+描述),右侧 编辑/删除 图标 + 滑动开关。
|
|
139
|
+
* @author ddj 2026年09月03号
|
|
140
|
+
* @param props.rule 规则元信息
|
|
141
|
+
* @param props.onEdit 编辑回调
|
|
142
|
+
* @param props.onRemove 删除回调
|
|
143
|
+
* @param props.onToggle 开关回调
|
|
144
|
+
* @returns 行元素
|
|
145
|
+
*/
|
|
146
|
+
function RuleRow(props) {
|
|
147
|
+
const rule = props.rule
|
|
148
|
+
const desc = [rule.error ? '解析失败:' + rule.error : null, rule.description || (rule.type === 'auto' ? 'globs: ' + rule.globs.join(', ') : '')]
|
|
149
|
+
.filter(Boolean).join(' · ')
|
|
150
|
+
return React.createElement('div', { className: 'edrv-rules-row' + (rule.enabled ? '' : ' off') },
|
|
151
|
+
React.createElement('div', { className: 'edrv-rules-main', onClick: () => props.onEdit(rule), title: rule.absPath },
|
|
152
|
+
React.createElement('div', { className: 'edrv-rules-line1' },
|
|
153
|
+
React.createElement('span', { className: 'edrv-rules-file' }, rule.file),
|
|
154
|
+
!props.narrow ? React.createElement('span', { className: 'edrv-rules-path' }, '[' + rule.relHint + rule.file + ']') : null,
|
|
155
|
+
!rule.enabled ? React.createElement('span', { className: 'edrv-rules-offtag' }, '已停用') : null),
|
|
156
|
+
React.createElement('div', { className: 'edrv-rules-sub' },
|
|
157
|
+
React.createElement('span', { className: 'edrv-rules-sublabel' }, '类型:'),
|
|
158
|
+
typeBadge(rule.type),
|
|
159
|
+
desc ? React.createElement('span', { className: 'edrv-rules-desc', title: desc }, clampDesc(desc, props.descCap)) : null)),
|
|
160
|
+
React.createElement('div', { className: 'edrv-rules-actions' },
|
|
161
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '编辑', onClick: () => props.onEdit(rule) }, pencilIcon()),
|
|
162
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '删除', onClick: () => props.onRemove(rule) }, trashIcon()),
|
|
163
|
+
React.createElement('button', {
|
|
164
|
+
className: 'edrv-rules-switch' + (rule.enabled ? ' on' : ''), role: 'switch', 'aria-checked': rule.enabled,
|
|
165
|
+
title: rule.enabled ? '已启用(点击停用)' : '已停用(点击启用)',
|
|
166
|
+
onClick: () => props.onToggle(rule),
|
|
167
|
+
}, React.createElement('span', { className: 'edrv-rules-knob' }))))
|
|
168
|
+
}
|
|
169
|
+
// --endregion
|
|
170
|
+
|
|
171
|
+
// --region 内嵌编辑器
|
|
172
|
+
/**
|
|
173
|
+
* 新建/编辑表单:面包屑头部 + 规则类型下拉/描述/globs 控件行 + 原始 .mdc 文本域 + 保存/取消。
|
|
174
|
+
* @author ddj 2026年09月03号
|
|
175
|
+
* @param props.form 表单状态(mode/scope/workspacePath/relHint/file/content/fmType/desc/globs/error/saving)
|
|
176
|
+
* @param props.onType 类型下拉变更回调
|
|
177
|
+
* @param props.onDesc 描述输入回调
|
|
178
|
+
* @param props.onGlobs globs 输入回调
|
|
179
|
+
* @param props.onBody 文本域编辑回调
|
|
180
|
+
* @param props.onSave 保存回调
|
|
181
|
+
* @param props.onCancel 取消回调
|
|
182
|
+
* @returns 表单元素
|
|
183
|
+
*/
|
|
184
|
+
function RuleEditor(props) {
|
|
185
|
+
const form = props.form
|
|
186
|
+
const isNew = form.mode === 'new'
|
|
187
|
+
const crumb = (form.scope === 'project' ? '项目规则' : '用户规则') + ' > ' + form.relHint + ' > ' + (form.file || '新建规则.mdc')
|
|
188
|
+
return React.createElement('div', { className: 'edrv-rules-editor' },
|
|
189
|
+
React.createElement('div', { className: 'edrv-rules-editor-head' },
|
|
190
|
+
React.createElement('span', { className: 'edrv-rules-crumb', title: crumb }, crumb),
|
|
191
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '取消', onClick: props.onCancel }, '✕')),
|
|
192
|
+
React.createElement('div', { className: 'edrv-rules-editor-ctl' },
|
|
193
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, '规则类型'),
|
|
194
|
+
React.createElement('select', { className: 'edrv-rules-select', value: form.fmType, onChange: props.onType },
|
|
195
|
+
Object.keys(TYPE_LABEL).map((key) => React.createElement('option', { key, value: key }, TYPE_LABEL[key]))),
|
|
196
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, '描述'),
|
|
197
|
+
React.createElement('input', {
|
|
198
|
+
className: 'edrv-rules-input edrv-rules-desc-input', value: form.desc, spellCheck: false,
|
|
199
|
+
placeholder: isNew ? '规则用途说明(写入 frontmatter description)' : '规则用途说明', onChange: props.onDesc,
|
|
200
|
+
})),
|
|
201
|
+
form.fmType === 'auto' ? React.createElement('div', { className: 'edrv-rules-editor-ctl' },
|
|
202
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, 'globs'),
|
|
203
|
+
React.createElement('input', {
|
|
204
|
+
className: 'edrv-rules-input', value: form.globs, spellCheck: false,
|
|
205
|
+
placeholder: 'src/**/*.ts, *.md(逗号分隔,命中才注入)', onChange: props.onGlobs,
|
|
206
|
+
})) : null,
|
|
207
|
+
React.createElement('textarea', {
|
|
208
|
+
className: 'edrv-rules-body', spellCheck: false,
|
|
209
|
+
value: form.content,
|
|
210
|
+
onChange: props.onBody,
|
|
211
|
+
}),
|
|
212
|
+
React.createElement('div', { className: 'edrv-rules-editor-hint' },
|
|
213
|
+
'类型/描述/globs 与 frontmatter 双向同步;enabled 与其余键原样保留;无 frontmatter 的纯 markdown 视为「手动」规则'),
|
|
214
|
+
form.error ? React.createElement('div', { className: 'edrv-rules-err' }, form.error) : null,
|
|
215
|
+
React.createElement('div', { className: 'edrv-rules-editor-foot' },
|
|
216
|
+
React.createElement('button', { className: 'edrv-rules-save', disabled: form.saving, onClick: props.onSave }, form.saving ? '保存中…' : '保存')))
|
|
217
|
+
}
|
|
218
|
+
// --endregion
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 规则面板主体。
|
|
222
|
+
* @param props.ctx 面板共享上下文(notify 可选)
|
|
223
|
+
*/
|
|
224
|
+
export function RulesPanel(props) {
|
|
225
|
+
const ctx = props?.ctx
|
|
226
|
+
const notify = ctx?.notify ?? ((message) => {})
|
|
227
|
+
// 当前会话工作区(EditorView 从 sessions 快照注入;项目 Tab 自动匹配,不提供手动切换)
|
|
228
|
+
const cwd = ctx?.cwd ?? null
|
|
229
|
+
const [tab, setTab] = React.useState(() => (readLocal(CACHE_KEY.rules + 'tab') === 'project' ? 'project' : 'user'))
|
|
230
|
+
const [data, setData] = React.useState(null) // { user: RuleInfo[], projects: RuleProject[] }
|
|
231
|
+
const [error, setError] = React.useState('')
|
|
232
|
+
const panelRef = React.useRef(null)
|
|
233
|
+
const [panelW, setPanelW] = React.useState(0) // 面板内容宽度(ResizeObserver 跟踪;0=未测得)
|
|
234
|
+
const [form, setForm] = React.useState(null) // 编辑器表单状态;null=列表态
|
|
235
|
+
const mountedRef = React.useRef(true)
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 当前工作区对应的项目聚合(按 ctx.cwd 自动匹配;无会话/未注册返回 null)。
|
|
239
|
+
* @returns RuleProject 或 null
|
|
240
|
+
*/
|
|
241
|
+
const activeProject = () => {
|
|
242
|
+
if (!cwd) return null
|
|
243
|
+
return (data?.projects ?? []).find((p) => p.workspacePath === cwd) ?? null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 拉取规则总列表(rules.list)。 */
|
|
247
|
+
const load = () => {
|
|
248
|
+
rpc('rules.list', {}).then((res) => {
|
|
249
|
+
if (!mountedRef.current) return
|
|
250
|
+
if (res && res.ok) { setData(res); setError('') } else setError(res?.error ?? '读取规则失败')
|
|
251
|
+
}).catch((e) => { if (mountedRef.current) setError('读取规则异常: ' + String(e)) })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
mountedRef.current = true
|
|
256
|
+
load()
|
|
257
|
+
return () => { mountedRef.current = false }
|
|
258
|
+
}, [])
|
|
259
|
+
|
|
260
|
+
// 面板宽度跟踪:描述字数上限与窄面板模式都随宽度动态变化
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
const el = panelRef.current
|
|
263
|
+
if (!el || typeof ResizeObserver === 'undefined') return undefined
|
|
264
|
+
const observer = new ResizeObserver(() => setPanelW(el.clientWidth))
|
|
265
|
+
observer.observe(el)
|
|
266
|
+
setPanelW(el.clientWidth)
|
|
267
|
+
return () => observer.disconnect()
|
|
268
|
+
}, [])
|
|
269
|
+
|
|
270
|
+
// 描述字数上限与窄面板模式(未测得宽度前用基础上限/完整展示)
|
|
271
|
+
const descCap = panelW > 0 ? descCapOf(panelW) : DESC_MAX
|
|
272
|
+
const narrow = panelW > 0 && panelW < NARROW_W
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 切换 Tab(记忆到 localStorage)。
|
|
276
|
+
* @param next 目标 Tab
|
|
277
|
+
*/
|
|
278
|
+
const switchTab = (next) => {
|
|
279
|
+
setTab(next)
|
|
280
|
+
writeLocal(CACHE_KEY.rules + 'tab', next)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 打开新建表单(按当前 Tab 决定作用域;项目规则跟随当前会话工作区)。
|
|
285
|
+
*/
|
|
286
|
+
const openNew = () => {
|
|
287
|
+
const scope = tab === 'project' ? 'project' : 'user'
|
|
288
|
+
if (scope === 'project' && !cwd) { notify('当前会话没有工作区,无法新建项目规则'); return }
|
|
289
|
+
const parsed = parseRuleFm(NEW_TEMPLATE)
|
|
290
|
+
setForm({
|
|
291
|
+
mode: 'new', scope, workspacePath: scope === 'project' ? cwd : undefined,
|
|
292
|
+
relHint: scope === 'project' ? '.dsh/rules/' : 'rules/', file: '', content: NEW_TEMPLATE,
|
|
293
|
+
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 打开编辑表单:先拉全文,再解析 frontmatter 回填类型/描述/globs 控件。
|
|
299
|
+
* @param rule 目标规则
|
|
300
|
+
*/
|
|
301
|
+
const openEdit = (rule) => {
|
|
302
|
+
rpc('rules.read', { scope: rule.scope, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })
|
|
303
|
+
.then((res) => {
|
|
304
|
+
if (!res || !res.ok) { notify(res?.error ?? '读取规则失败'); return }
|
|
305
|
+
const parsed = parseRuleFm(res.content)
|
|
306
|
+
setForm({
|
|
307
|
+
mode: 'edit', scope: rule.scope, workspacePath: rule.workspacePath, relHint: rule.relHint,
|
|
308
|
+
file: rule.file, content: res.content,
|
|
309
|
+
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
310
|
+
})
|
|
311
|
+
}).catch((e) => notify('读取规则异常: ' + String(e)))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 类型下拉变更:frontmatter 按新类型重写,控件同步。 */
|
|
315
|
+
const onTypeChange = (e) => {
|
|
316
|
+
const nextType = e.target.value
|
|
317
|
+
setForm((prev) => ({ ...prev, fmType: nextType, content: rebuildContent(prev.content, nextType, prev.desc, prev.globs), error: '' }))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 描述输入变更:同步 frontmatter description 行。 */
|
|
321
|
+
const onDescChange = (e) => {
|
|
322
|
+
const nextDesc = e.target.value
|
|
323
|
+
setForm((prev) => ({ ...prev, desc: nextDesc, content: rebuildContent(prev.content, prev.fmType, nextDesc, prev.globs), error: '' }))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** globs 输入变更(自动规则):同步 frontmatter globs 行。 */
|
|
327
|
+
const onGlobsChange = (e) => {
|
|
328
|
+
const nextGlobs = e.target.value
|
|
329
|
+
setForm((prev) => ({ ...prev, globs: nextGlobs, content: rebuildContent(prev.content, prev.fmType, prev.desc, nextGlobs), error: '' }))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 文本域直接编辑:回读 frontmatter,同步类型/描述/globs 控件(文本域是唯一真源)。 */
|
|
333
|
+
const onBodyChange = (e) => {
|
|
334
|
+
const parsed = parseRuleFm(e.target.value)
|
|
335
|
+
setForm((prev) => ({ ...prev, content: e.target.value, fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', ') }))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 保存表单(rules.save;文件名非空 + 自动规则必须有 globs)。 */
|
|
339
|
+
const saveForm = () => {
|
|
340
|
+
const next = { ...form, saving: true, error: '' }
|
|
341
|
+
const file = String(form.file ?? '').trim()
|
|
342
|
+
if (!file) { setForm({ ...next, saving: false, error: '文件名不能为空' }); return }
|
|
343
|
+
if (form.fmType === 'auto' && globList(form.globs).length === 0) { setForm({ ...next, saving: false, error: '自动规则需要至少一个 glob(逗号分隔)' }); return }
|
|
344
|
+
setForm(next)
|
|
345
|
+
rpc('rules.save', { scope: form.scope, workspacePath: form.workspacePath, file, content: form.content })
|
|
346
|
+
.then((res) => {
|
|
347
|
+
if (res && res.ok) {
|
|
348
|
+
setForm(null)
|
|
349
|
+
notify('规则已保存:' + file)
|
|
350
|
+
load()
|
|
351
|
+
} else setForm((prev) => ({ ...prev, saving: false, error: res?.error ?? '保存失败' }))
|
|
352
|
+
}).catch((e) => setForm((prev) => ({ ...prev, saving: false, error: '保存异常: ' + String(e) })))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 删除规则(confirm 后走 rules.remove)。
|
|
357
|
+
* @param rule 目标规则
|
|
358
|
+
*/
|
|
359
|
+
const removeRule = (rule) => {
|
|
360
|
+
if (!window.confirm('删除规则 ' + rule.file + '?(不可恢复)')) return
|
|
361
|
+
rpc('rules.remove', { scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file })
|
|
362
|
+
.then((res) => {
|
|
363
|
+
if (res && res.ok) { notify('已删除:' + rule.file); load() } else notify(res?.error ?? '删除失败')
|
|
364
|
+
}).catch((e) => notify('删除异常: ' + String(e)))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 切换启用开关:本地乐观翻转(按 workspacePath+file 定位,失败回滚)。
|
|
369
|
+
* @param rule 目标规则(project 已注入 workspacePath)
|
|
370
|
+
*/
|
|
371
|
+
const toggleRule = (rule) => {
|
|
372
|
+
const enabled = !rule.enabled
|
|
373
|
+
const applyLocal = (on) => setData((prev) => prev ? {
|
|
374
|
+
...prev,
|
|
375
|
+
user: prev.user.map((r) => (r.scope === 'user' && r.file === rule.file ? { ...r, enabled: on } : r)),
|
|
376
|
+
projects: prev.projects.map((p) => (p.workspacePath === rule.workspacePath
|
|
377
|
+
? { ...p, rules: p.rules.map((r) => (r.file === rule.file ? { ...r, enabled: on } : r)) }
|
|
378
|
+
: p)),
|
|
379
|
+
} : prev)
|
|
380
|
+
applyLocal(enabled)
|
|
381
|
+
rpc('rules.toggle', { scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file, enabled })
|
|
382
|
+
.then((res) => {
|
|
383
|
+
if (res && res.ok) { applyLocal(res.rule?.enabled === true); notify(res.rule?.enabled === false ? '已停用:' + rule.file : '已启用:' + rule.file) }
|
|
384
|
+
else { applyLocal(!enabled); notify(res?.error ?? '切换失败') }
|
|
385
|
+
}).catch((e) => { applyLocal(!enabled); notify('切换异常: ' + String(e)) })
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 当前列表数据(按 Tab 取用户规则或当前工作区的项目规则)。 */
|
|
389
|
+
const rowsOf = () => {
|
|
390
|
+
if (!data) return []
|
|
391
|
+
if (tab === 'user') return data.user
|
|
392
|
+
return activeProject()?.rules ?? []
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const renderTabs = () => React.createElement('div', { className: 'edrv-rules-tabs' },
|
|
396
|
+
React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'user' ? ' on' : ''), onClick: () => switchTab('user') }, '用户规则'),
|
|
397
|
+
React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'project' ? ' on' : ''), onClick: () => switchTab('project') }, '项目规则'),
|
|
398
|
+
React.createElement('button', { className: 'edrv-rules-new', title: '新建规则', onClick: openNew }, '+ 新建规则'))
|
|
399
|
+
|
|
400
|
+
/** 项目 Tab 的工作区展示(不可编辑,自动跟随当前会话)。 */
|
|
401
|
+
const renderWsLine = () => {
|
|
402
|
+
if (tab !== 'project') return null
|
|
403
|
+
return React.createElement('div', { className: 'edrv-rules-ws-line', title: cwd ?? '' },
|
|
404
|
+
'工作区:' + (cwd ?? '当前会话未打开工作区'))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const renderEmpty = () => {
|
|
408
|
+
if (tab === 'project') {
|
|
409
|
+
if (!cwd) return '当前会话没有工作区,项目规则不可用(项目规则跟随会话所在工作区)'
|
|
410
|
+
if (!data?.projects?.length) return '当前 DSH 没有已注册的工作区;在目标工作区打开一次会话后再来管理'
|
|
411
|
+
if (!activeProject()) return '当前工作区未注册为 DSH workspace,暂不能管理其项目规则'
|
|
412
|
+
return '<工作区>/.dsh/rules/ 还没有规则,点击「+ 新建规则」创建'
|
|
413
|
+
}
|
|
414
|
+
return '还没有用户规则(~/.dsh/rules/),点击「+ 新建规则」创建'
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const renderBody = () => {
|
|
418
|
+
if (form) {
|
|
419
|
+
return React.createElement(RuleEditor, {
|
|
420
|
+
form, onType: onTypeChange, onDesc: onDescChange, onGlobs: onGlobsChange, onBody: onBodyChange,
|
|
421
|
+
onSave: saveForm, onCancel: () => setForm(null),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
if (!data) return React.createElement('div', { className: 'edrv-rules-empty' }, '加载中…')
|
|
425
|
+
if (error) return React.createElement('div', { className: 'edrv-rules-err' }, error)
|
|
426
|
+
const rows = rowsOf()
|
|
427
|
+
if (!rows.length) return React.createElement('div', { className: 'edrv-rules-empty' }, renderEmpty())
|
|
428
|
+
const common = {
|
|
429
|
+
descCap,
|
|
430
|
+
narrow,
|
|
431
|
+
onEdit: (rule) => openEdit({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
432
|
+
onRemove: (rule) => removeRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
433
|
+
onToggle: (rule) => toggleRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
434
|
+
}
|
|
435
|
+
return React.createElement('div', { className: 'edrv-rules-list' },
|
|
436
|
+
rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + rule.file, rule, ...common })))
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return React.createElement('div', { className: 'edrv-rules-panel', ref: panelRef },
|
|
440
|
+
renderTabs(),
|
|
441
|
+
renderWsLine(),
|
|
442
|
+
React.createElement('div', { className: 'edrv-rules-hint' },
|
|
443
|
+
tab === 'user' ? '用户规则对所有会话生效(存放于 ~/.dsh/rules/)' : '项目规则仅对所在工作区的会话生效(存放于 <工作区>/.dsh/rules/)'),
|
|
444
|
+
renderBody())
|
|
445
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client — 侧边栏面板定义(文件管理 +
|
|
4
|
-
* 作者 ddj 2026-08-26 / 2026-
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏面板定义(文件管理 + 搜索 + 规则)。
|
|
4
|
+
* 作者 ddj 2026-08-26 / 2026-09-03
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react'
|
|
7
7
|
import { FileExplorer } from './FileExplorer.js'
|
|
8
8
|
import { SearchPanel } from './SearchPanel.js'
|
|
9
|
+
import { RulesPanel } from './RulesPanel.js'
|
|
9
10
|
import type { SidebarPanelDef, SidebarCtx } from '../types.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -38,3 +39,18 @@ export function createSearchPanel(): SidebarPanelDef {
|
|
|
38
39
|
render: (ctx: SidebarCtx) => React.createElement(SearchPanel, { ctx }),
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 构造规则管理面板定义(Codebuddy 规则形态:用户规则/项目规则双 Tab + 开关)。
|
|
45
|
+
* @author ddj 2026年09月03号
|
|
46
|
+
* @returns 面板定义(无徽标;活动栏图标 📏,与大纲 📜 区分)
|
|
47
|
+
*/
|
|
48
|
+
export function createRulesPanel(): SidebarPanelDef {
|
|
49
|
+
return {
|
|
50
|
+
id: 'rules',
|
|
51
|
+
title: '规则',
|
|
52
|
+
icon: '📏',
|
|
53
|
+
order: 20,
|
|
54
|
+
render: (ctx: SidebarCtx) => React.createElement(RulesPanel, { ctx }),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -10,6 +10,8 @@ import type { TreeMenuRegistry } from './contextMenu.js'
|
|
|
10
10
|
/** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
|
|
11
11
|
export interface SidebarCtx {
|
|
12
12
|
sessionId?: string
|
|
13
|
+
/** 当前会话工作区 cwd(来自 sessions 快照;规则面板项目 Tab 自动匹配用)。 */
|
|
14
|
+
cwd?: string | null
|
|
13
15
|
openFile: (path: string) => void
|
|
14
16
|
/** 打开文件并跳转到指定行/列(搜索面板命中跳转;缺省仅打开)。 */
|
|
15
17
|
openFileAt?: (path: string, line?: number, column?: number) => void
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 侧边栏最小宽度共享状态。
|
|
3
|
+
* 设置来源:dsh-vscode-mode 命名空间的 sidebarMinWidth 字段(通用设置页可调);
|
|
4
|
+
* 客户端 settings 订阅经 sidebarMinApply 写入,编辑器/侧边栏容器经 getSidebarMinWidth 读取,
|
|
5
|
+
* 变更时由 client/index.ts 派发 edrv:sidebar-min-width 窗口事件通知编辑器重夹。
|
|
6
|
+
* 作者 ddj 2026年09月04号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 默认最小宽度(设置缺失/非法时的兜底值)。 */
|
|
10
|
+
export const SIDEBAR_MIN_DEFAULT = 300
|
|
11
|
+
/** 最小宽度允许下界(与旧版拖拽下限一致)。 */
|
|
12
|
+
export const SIDEBAR_MIN_FLOOR = 180
|
|
13
|
+
/** 最小宽度允许上界(与侧边栏最大宽度 W_MAX 一致,避免区间倒挂)。 */
|
|
14
|
+
export const SIDEBAR_MIN_CEIL = 560
|
|
15
|
+
|
|
16
|
+
let currentMin = SIDEBAR_MIN_DEFAULT
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 归一化最小宽度:非法(NaN/非有限/负数)回退默认,越界夹取到 [180, 560]。
|
|
20
|
+
* @author ddj 2026年09月04号
|
|
21
|
+
* @param value 原始值(设置文档来的任意 JSON 值)
|
|
22
|
+
* @returns 合法最小宽度
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeSidebarMinWidth(value: unknown): number {
|
|
25
|
+
const n = typeof value === 'number' ? value : Number(value)
|
|
26
|
+
if (!Number.isFinite(n) || n <= 0) return SIDEBAR_MIN_DEFAULT
|
|
27
|
+
return Math.max(SIDEBAR_MIN_FLOOR, Math.min(SIDEBAR_MIN_CEIL, Math.round(n)))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 应用设置值(client/index.ts 设置订阅同步调用)。
|
|
32
|
+
* @author ddj 2026年09月04号
|
|
33
|
+
* @param value 设置文档中的 sidebarMinWidth
|
|
34
|
+
* @returns 归一化后的生效值
|
|
35
|
+
*/
|
|
36
|
+
export function sidebarMinApply(value: unknown): number {
|
|
37
|
+
currentMin = normalizeSidebarMinWidth(value)
|
|
38
|
+
return currentMin
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 读取当前生效的最小宽度(编辑器初始/恢复/拖拽夹取共用)。
|
|
43
|
+
* @author ddj 2026年09月04号
|
|
44
|
+
* @returns 当前最小宽度
|
|
45
|
+
*/
|
|
46
|
+
export function getSidebarMinWidth(): number {
|
|
47
|
+
return currentMin
|
|
48
|
+
}
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
[data-edrv-view] .edrv-main-col > .edrv-pathbar,
|
|
207
207
|
[data-edrv-view] .edrv-main-col > .edrv-tabs { flex: 0 0 auto; }
|
|
208
208
|
[data-edrv-view] .edrv-editor-area { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
|
|
209
|
-
[data-edrv-view] .edrv-sidebar { display: flex; flex-direction: row; flex-shrink:
|
|
209
|
+
[data-edrv-view] .edrv-sidebar { display: flex; flex-direction: row; flex-shrink: 1; min-width: 0; height: 100%; background: var(--dsw-alias-bg-layer-1, #ffffff); border-right: 1px solid var(--dsw-alias-border-l1, #e0e6e8); overflow: hidden; }
|
|
210
210
|
[data-edrv-view] .edrv-rail { display: flex; flex-direction: column; align-items: center; width: 44px; flex-shrink: 0; padding: 6px 0; background: var(--dsw-alias-bg-layer-2, #f0f4f4); border-right: 1px solid var(--dsw-alias-border-l1, #e0e6e8); gap: 4px; }
|
|
211
211
|
[data-edrv-view] .edrv-rail-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); cursor: pointer; flex-shrink: 0; }
|
|
212
212
|
[data-edrv-view] .edrv-rail-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
@@ -295,5 +295,57 @@
|
|
|
295
295
|
[data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
|
|
296
296
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
297
297
|
|
|
298
|
+
/* ===== rules panel (sidebar, Codebuddy rules view) ===== */
|
|
299
|
+
[data-edrv-view] .edrv-rules-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; flex: 1; padding: 6px; gap: 4px; }
|
|
300
|
+
[data-edrv-view] .edrv-rules-tabs { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
|
|
301
|
+
[data-edrv-view] .edrv-rules-tab { height: 24px; padding: 0 10px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); font-size: 12px; cursor: pointer; }
|
|
302
|
+
[data-edrv-view] .edrv-rules-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
303
|
+
[data-edrv-view] .edrv-rules-tab.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); font-weight: 600; }
|
|
304
|
+
[data-edrv-view] .edrv-rules-new { margin-left: auto; height: 24px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); border-radius: 6px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #1f2933); font-size: 11px; cursor: pointer; flex-shrink: 0; }
|
|
305
|
+
[data-edrv-view] .edrv-rules-new:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
306
|
+
[data-edrv-view] .edrv-rules-ws-line { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); padding: 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 0 auto; user-select: none; }
|
|
307
|
+
[data-edrv-view] .edrv-rules-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); padding: 0 2px; flex: 0 0 auto; }
|
|
308
|
+
[data-edrv-view] .edrv-rules-list { flex: 1; min-height: 0; overflow: auto; }
|
|
309
|
+
[data-edrv-view] .edrv-rules-row { display: flex; align-items: center; gap: 4px; padding: 6px 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; }
|
|
310
|
+
[data-edrv-view] .edrv-rules-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
311
|
+
[data-edrv-view] .edrv-rules-row.off .edrv-rules-main { opacity: .5; }
|
|
312
|
+
[data-edrv-view] .edrv-rules-main { flex: 1; min-width: 0; cursor: pointer; overflow: hidden; }
|
|
313
|
+
[data-edrv-view] .edrv-rules-line1 { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
314
|
+
[data-edrv-view] .edrv-rules-sub { display: flex; align-items: center; gap: 5px; margin-top: 3px; min-width: 0; }
|
|
315
|
+
[data-edrv-view] .edrv-rules-sublabel { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; }
|
|
316
|
+
[data-edrv-view] .edrv-rules-file { font-weight: 600; font-size: 12px; color: var(--dsw-alias-label-primary, #1f2933); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
317
|
+
[data-edrv-view] .edrv-rules-path { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
318
|
+
[data-edrv-view] .edrv-rules-type { flex-shrink: 0; height: 15px; padding: 0 5px; border-radius: 999px; font-size: 10px; line-height: 15px; }
|
|
319
|
+
[data-edrv-view] .edrv-rules-type-always { background: rgba(15,157,88,.15); color: #0b7c46; }
|
|
320
|
+
[data-edrv-view] .edrv-rules-type-auto { background: rgba(79,140,255,.16); color: #3367c9; }
|
|
321
|
+
[data-edrv-view] .edrv-rules-type-manual { background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); }
|
|
322
|
+
[data-edrv-view] .edrv-rules-offtag { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; }
|
|
323
|
+
[data-edrv-view] .edrv-rules-desc { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 0 1 auto; }
|
|
324
|
+
[data-edrv-view] .edrv-rules-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; margin-left: auto; }
|
|
325
|
+
[data-edrv-view] .edrv-rules-act { min-width: 22px; height: 22px; padding: 0 4px; border: none; border-radius: 5px; background: transparent; font-size: 12px; line-height: 1; cursor: pointer; color: var(--dsw-alias-label-secondary, #52606d); display: inline-flex; align-items: center; justify-content: center; }
|
|
326
|
+
[data-edrv-view] .edrv-rules-act:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
327
|
+
[data-edrv-view] .edrv-rules-switch { position: relative; width: 30px; height: 16px; border-radius: 999px; border: none; background: #b9c4cc; cursor: pointer; transition: background .15s; flex-shrink: 0; padding: 0; }
|
|
328
|
+
[data-edrv-view] .edrv-rules-switch.on { background: #7c5cff; }
|
|
329
|
+
[data-edrv-view] .edrv-rules-knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; transition: left .15s; }
|
|
330
|
+
[data-edrv-view] .edrv-rules-switch.on .edrv-rules-knob { left: 16px; }
|
|
331
|
+
[data-edrv-view] .edrv-rules-empty { padding: 10px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.6; }
|
|
332
|
+
[data-edrv-view] .edrv-rules-err { padding: 6px 8px; border: 1px solid rgba(217,83,79,.4); border-radius: 6px; background: #fdeaea; color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; word-break: break-all; }
|
|
333
|
+
[data-edrv-view] .edrv-rules-editor { display: flex; flex-direction: column; min-height: 0; flex: 1; gap: 6px; }
|
|
334
|
+
[data-edrv-view] .edrv-rules-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); flex: 0 0 auto; }
|
|
335
|
+
[data-edrv-view] .edrv-rules-crumb { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
336
|
+
[data-edrv-view] .edrv-rules-editor-ctl { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
337
|
+
[data-edrv-view] .edrv-rules-ctl-label { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); flex-shrink: 0; }
|
|
338
|
+
[data-edrv-view] .edrv-rules-select { height: 24px; padding: 0 4px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 11px; outline: none; cursor: pointer; flex-shrink: 0; }
|
|
339
|
+
[data-edrv-view] .edrv-rules-select:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
340
|
+
[data-edrv-view] .edrv-rules-desc-input { flex: 1; min-width: 0; }
|
|
341
|
+
[data-edrv-view] .edrv-rules-input { flex: 1; min-width: 0; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 11px; outline: none; font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
342
|
+
[data-edrv-view] .edrv-rules-input:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
343
|
+
[data-edrv-view] .edrv-rules-body { flex: 1; min-height: 0; resize: none; padding: 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 12px; line-height: 1.5; outline: none; font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
344
|
+
[data-edrv-view] .edrv-rules-body:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
345
|
+
[data-edrv-view] .edrv-rules-editor-hint { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.5; flex: 0 0 auto; }
|
|
346
|
+
[data-edrv-view] .edrv-rules-editor-foot { display: flex; justify-content: flex-end; flex: 0 0 auto; }
|
|
347
|
+
[data-edrv-view] .edrv-rules-save { height: 24px; padding: 0 14px; border: none; border-radius: 6px; background: var(--dsw-alias-brand-primary, #0f9d58); color: #fff; font-size: 12px; cursor: pointer; }
|
|
348
|
+
[data-edrv-view] .edrv-rules-save:disabled { opacity: .5; cursor: default; }
|
|
349
|
+
|
|
298
350
|
|
|
299
351
|
|