Releases: FOX4096/dsh-custom-css
Release list
v0.1.7
0.1.7 — 查找替换、格式化、编辑器快捷键、变量体检与历史版本
查找 / 替换
表头多了一个**「查找」按钮(Ctrl+F 等效),点开是一条查找栏**:查找、替换为、命中计数、区分大小写、上一个 / 下一个、替换、全部替换、关闭。
- Ctrl+F 会把编辑器里选中的那段文字当作查询,光标同时落到栏指向的那条命中上(从表中间打开栏,给你的是眼前那条,不是文件顶部)。
- 命中在彩色层里被圈出来,当前那一条更重;计数写作
3 / 12,一条都没有时整块变红写「无命中」。 - Enter / Shift+Enter 走命中:在查找框里、在编辑器里都行(栏打开时 Enter 不再往表里插换行 —— 刚敲完查询的那一下最可能是想跳到下一条)。
- 替换换掉当前那条并前进一条,全部替换一次换完。两者都是一次编辑,所以 Ctrl+Z 一次就全撤回。
- 查询是纯文本,不是正则:
a.b找的就是a.b。Aa关掉时大小写不敏感。 Aa按下去看得出是按下的:品牌色文字 + 按下态底色 + 去掉描边 + 加粗(实测两态在底色 / 文字色 / 描边 / 字重四项上都不同)。第一版只带了aria-pressed、没带任何样式 —— 用户一眼就报了出来:「点击之后和之前按钮没有区别,这么看有没有点击呢?」。一个看不出状态的开关等于没有开关,aria-pressed是给读屏器的,不是给人的。- 命中是文本偏移,而彩色层是转义过的标记 —— 这两套坐标只在「这一段渲染出来和原文一样长」时对得上。落不准的命中宁可不标(栏照旧数它、textarea 照旧选中它),也不标偏几个字符把标签切两半。大小写折叠可能改变长度(
İ折成两个码元),所以折叠串里的命中要走一张回映射表落回原表,否则那个字符之后的每一条都偏一位。两条都补了用例。 - 表头的「查找」是常驻按钮,不是收进「更多操作」:查找的价值在于光标在文里时随手能用,而菜单到不了光标所在的每一种状态。
查找栏的位置是量出来的,不是看出来的。 栏里两个输入框加六个控件,内容固定 610px;设置行正常 600px、侧栏打开时 440px —— 所以第一版在任何宽度下都换行(实测 600 / 520 / 440 / 380 全部两行)。现在两级让步:640px 以下隐去命中计数(命中已经高亮,数字是冗余的),540px 以下
替换/全部替换只留 ↦ / ⇉(tooltip 与 aria-label 都在,控件绝不会变成不可达)。阈值取在仍有富余的地方,而不是取在已经绷紧的宽度上。改完 900 / 700 / 641 / 640 / 600 / 541 / 540 / 520 / 440 / 380px 全部单行、0 溢出、没有控件被压成 0。探针:%TEMP%\css-probe\findbar-fit.mjs(读真实 ROW_CSS,视口宽 = 行宽)。
格式化
「更多操作 → 格式化」把整张表重排:一条规则一行、每层缩进两空格、去掉空行。
它只动行的排布,不动一行里写的东西:缺的分号、{ 前少的空格都留给校验器去报,格式化器去改写声明就等于替用户改代码。花括号在字符串里不算括号(content: "}" 是字符串的一个字符),括号不配对的表原样返回 —— 该报错的是校验器,把坏表重排一遍只会把坏的地方挪到别处。已经是格式化过的样子时菜单项置灰(点了什么都不会发生的菜单项读起来像坏了)。
「更多操作」的顺序
六项按它们是什么排,不按加进来的先后:三个文件动作(打开文件 / 导入 / 导出)连着,然后是整张表的动作(格式化 / 历史版本),「重置」放到最底下 —— 那是唯一会毁掉工作的入口,让它离鼠标远一格、别在去别处的路上被点到。
编辑器有撤销了(以及 Tab、Ctrl+S)
受控的 <textarea> 没有可用的撤销栈:React 每敲一个键就写一次 value,而这个赋值会清掉引擎自己记下的历史 —— 所以在这个编辑器里按 Ctrl+Z 一直是什么都不发生。现在插件自己记:
- Ctrl/Cmd+Z 撤销、Ctrl/Cmd+Shift+Z 或 Ctrl+Y 重做。连续敲键算一步(在自然停顿处分段),所以撤回一整段输入不用按十几次;面板改属性、拾取器插入规则这类结构性改动各自算一步。
- 撤销会连同光标位置一起回到当时的位置(并像插入规则那样滚到可见处)。
- Ctrl/Cmd+S 立即写盘,不等那 400ms 的防抖 —— 也顺手挡掉浏览器自己的保存对话框。
- Tab 缩进两格、Shift+Tab 退两格。补全列表打开时 Tab 仍是「采用建议」(那是它一直的行为)。
历史按每张表分开,最多 200 步;切换表不会把上一张表的撤销带过来。
规则大纲:表里有哪些规则,以及它们到底命中什么
文件栏多了一个**「大纲」下拉:整张表的顶层规则按顺序列出来**,点一下跳到那条规则并打开它的面板(光标落在选择器上,编辑器滚到可见处)。表越长越省事 —— 以前只能在几百行里用肉眼看。
每条后面跟着这个选择器在当前页面上命中了几个元素:
.a · 命中 2 .b · 命中 1 .gone · 无命中
这个数字是它另一半的价值:命中 0 个的规则就是什么都没做的规则,而它看起来和正常的规则一模一样。结构性路径尤其如此 —— 熬过了一次改版、没熬过下一次,然后就一直安静地躺在那儿。选择器被引擎拒绝时写「选择器无效」,计数还没算出来时留空(它在编辑器停顿之后才算,见下)。
往文件栏加控件有个坑,记在这里免得下次再踩:下拉用的那套外壳(
.dshCc_selectWrap)是给规则面板的网格单元做的,它写着width:100%—— 放进文件栏就会要整行宽度,把右边的开关挤出去(实测溢出 75px、开关文字右边缘越过栏右边缘 65px)。文件栏里的副本必须显式width:auto,并跟栏内其它控件一样高(28px),而不是跟设置行的字段一样高(36px)。修完横条溢出 0px,440px 窄宽度下同样为 0。
菜单本身也有两个坑,同一个根子:大纲的触发按钮在文件栏最右端。原来的菜单一律
left: 触发按钮的左边缘往右展开,于是 320px 的菜单有 250px 跑到窗口外面;而未给确定宽度时菜单会跟着内容长(条目是white-space:nowrap的选择器),长选择器把框越撑越宽。现在定位交给placeMenu():右边缘放不下就改成靠右对齐(向左展开),宽度取「想要的宽度」与「窗口可用宽度」的较小值,内容驱动的菜单也套一个max-width上限。菜单条目被省略号截断时,悬停可以看全文。
同一轮顺手查了文件下拉的菜单(它在行头,不是文件栏):左边缘没问题(它按
right:0向左展开),但它没有高度上限——表一多就长出屏幕底部,而且滚不动(实测 24 张表 = 968px 高的菜单,800px 窗口里 236px 在屏幕外,700px 窗口里 362px)。现在统一max-height:min(60vh,320px)+ 可滚动,并加了一条审计:插件里任何写overflow-y:auto的选择器都必须声明max-height(没有上限的滚动容器就是会长出窗口的列表)。
滚动条不要再自己写样式了:DSH 有一份全局样式表统一给所有元素画滚动条(8px 宽、圆角 4px 的拇指,颜色取
--dsh-scrollbar-thumb/--dsh-scrollbar-thumb-hover)。插件里原先给菜单和规则面板写过scrollbar-width: thin—— 在 Chromium 里,元素上一旦出现这种标准属性,该元素的::-webkit-scrollbar样式就会被忽略,于是全应用只有这两处掉回平台默认的灰条。现在这两句都去掉了,只补上一处 DSH 自己的可滚动面板也在用的轨道内缩(::-webkit-scrollbar-track{margin:6px},菜单 / 拾取器面板 / 规则面板),否则拇指的行程顶到两端、末端会从面板的圆角外面露出去。审计(scrollbar-width/scrollbar-color只能是none或位于 webkit 特性查询内)守着这两句不回来。
外部改动不再被静默覆盖
样式表是磁盘上的真文件,而「打开文件」这个动作存在的意义就是让你在别的编辑器里改它 —— 那么这份编辑器手里的副本就可能悄悄过期,而下一次防抖写盘会把别人的改动直接盖掉,还不报错。这是这一行里唯一会丢数据的失败方式,所以现在写盘前先读一次:内容对不上就不写,在页脚问你要哪一份:
- 重新载入 —— 以磁盘上的那一份为准(编辑器内容与应用中的样式一起换过去);
- 覆盖它 —— 明确用编辑器里这份盖掉磁盘(此时才写)。
另外,窗口重新回到前台时也会检查一次(visibilitychange)—— 你在 VS Code 里改完切回来,问题会在你下次打字之前就摆出来。整条路径只有写盘前多一次读,且写盘只走一个出口(writeSheet()):编辑器的防抖、拾取器插入、都从这里过,所以「地盘有没有动过」只在一个地方问。
历史版本:每次覆盖写盘前留一份,随时回得去
防抖写盘意味着每一次停顿都在覆盖上一次的内容。文件本身是不可变的过去 —— 一旦写完,前一份就没了。所以现在每次覆盖之前,host 先把被替换掉的那份存下来:
~/.dsh/custom-css/.history/custom.css/1758793204123.css
「更多操作 → 历史版本」列出当前这张表的全部版本(本地时间 + 字节数,时间倒序),点一条就把它恢复进编辑器并落盘。刚才那份不会被牺牲掉:恢复前同样先留档,所以恢复本身也是可逆的 —— 点错了再点回去就行。
几条刻意的取舍:
- 内容与最新一份相同时不留档。自动保存会反复写同一份内容,每次都留的话,历史里很快全是同一份表的副本,真正有价值的版本被挤出去 —— 留档按内容判断,不按次数。
- 每张表最多 20 份,超出按时间从旧的开始剪。一个写一天的样式表不能无上限长下去。
- 恢复会清空编辑器的撤销栈(就是上面那套 Ctrl+Z 的历史)。不清的话,Ctrl+Z 会把用户刚刚决定离开的那一版又变回来 —— 恢复是一个明确的选择,不是一次输入。
- 恢复走 host 写盘,写完页面重读一次表,而不是把请求里的文本直接塞进编辑器:恢复是一次写盘,编辑器该显示的是盘上真正的东西。
- 快照放在
.history/子目录里,而/list只读顶层 —— 快照永远不会被当成一张样式表列出来。
顺手修掉的两个缺陷
菜单变成了空壳。 重构「更多操作」的条目时,菜单那层 createElement 的 children 参数漏了逗号 —— 菜单节点本身还在,里面是空的。这个缺陷测试没抓住:那条断言查的是 renderedText(渲染过程中收集到的文本),而文本是在按钮被创建时就收集的,跟这些按钮有没有真的挂到菜单上无关。现在这条断言改成按节点找按钮(buttonWith),菜单里没有 button 节点就会红。
同一类问题的第二个面:rememberEdit(撤销栈)只在真的发生改动时才记录。一次「内容与盘上完全相同」的编辑不会留下撤销点 —— 写测试时正好踩到这点:那次编辑的内容和磁盘一模一样,于是「恢复后 Ctrl+Z 不该撤销恢复」这条断言成立的原因是栈本来就是空的,而不是恢复清过它。测试改成先做一次真正改变内容的编辑(并让它落盘),断言才真的在测这件事。
变量体检:不起作用的声明现在会说话
自定义属性是继承的,所以「这个变量有没有值」是个关于元素的问题,不是关于文本的问题。别的插件在自己的卡片上定义了 --dsl-terminal-line-height,那么在卡片外面用它的声明会被引擎整条丢掉——没有报错,没有提示,什么都没发生。
现在编辑器在停顿之后(600ms)去问文档:把规则的选择器在页面里匹配一遍,再从命中的元素上读这个属性。读不到就进状态行的问题列表,和括号、值错误放在一起:
第 49 行 变量 --dsl-g-shadow-card 在这条规则命中的元素上读不到
两种情况故意不报:规则没命中任何元素(没得问 —— 样式表本来就会写那些当前不在屏幕上的状态),以及 var(--x, 备用值)(写了备用值就没有可丢的东西)。一个会为这两种情况乱叫的检查,一天之内就会被人关掉,然后什么都抓不到。
(这条是拿一份真实在用的 764 行样式表验的:它有两处用了另一个插件的卡片变量,正是这条检查报出来的。)
v0.1.6
0.1.6 — 元素拾取器
在界面里点选一个元素,自动给出一条跨版本尽量稳的选择器,并把该元素实际生效的 DSH 令牌一起摆出来。
这一版还修了一批缺陷,其中几条是复审与用户实测报出来的:拾取之后状态栏卡在「保存中」、复用已有规则时面板不打开、坏选择器因为「查不出来」而胜出、点击后高亮框左上角多一条横线、插入规则后编辑器不滚动、以及令牌建议会给出别种类的令牌。下面每一节都写了根因,并给出实测量到的数据(这一版的结论基本都不用「看起来」来支撑)。
为什么选择器要「挑」而不是「算」
设计系统用的是 CSS Modules:_card_1fywu_26、_boot_1fywu_3 —— 哈希随构建变化。所以拾取器不会输出 ._card_1fywu_26(下次 DSH 升级就失效),而是按「什么能活下来」排序候选:
| 优先级 | 类型 | 例 | 说明 |
|---|---|---|---|
| 1 | [data-*] 钩子 |
[data-dockkit-strip] |
DSH 自己也在用它查询,跨版本最稳 |
| 2 | [aria-label] |
div[aria-label="QQ 音乐"] |
语义层,不会随便改 |
| 3 | 手写语义类名 | div.dsh-music-qq-head |
插件作者自己维护 |
| 4 | 哈希容错 | div[class*="_card_"] |
不接哈希,但依赖命名习惯 |
| 5 | 结构路径 | body > div:nth-child(2) |
兜底,界面改版会失效 |
冲突不是隐藏而是标注:命中多个元素的候选会被降权并显示「N 个命中」,但不会掉到结构兜底之后 —— 一次性给一族元素写样式是正当需求,而结构路径注定会在升级时断。唯一性决定默认选项,证据层级只用来打破平局。
交互
- 表头动作组新增**「拾取元素」**(在「打开文件」左侧,置灰条件与文件模式一致)。
- 点它 → 设置面板自己让路:优先找
[role="dialog"][aria-modal="true"],找不到就向上找覆盖视口的祖先,把它pointer-events:none+ 降透明度。高亮框与提示条挂在 document 上,否则会被一起降掉。(这条「让路」的做法后来被推翻了 —— 见下面「拾取器改成浮动面板(架构修正)」一节。) - 浮层跟随指针,实时显示
标签 · 宽×高;Esc 或「取消拾取」退出;卸载时自动复原(属性、监听、浮层都不会残留)。 - 点选后弹出候选卡:三条候选(可切换)+ 该元素实际解析到的令牌 chips + 「插入规则」。
- 「插入规则」:表里已有同名规则就打开它(不重复追加),否则就地追加一条空规则并把选中的令牌写成声明、光标停在新块内。
令牌反查
令牌表不是手写的颜色表,而是从 CSSOM 现读(文档里各样式表中 --* 自定义属性及其取值),所以新增令牌、主题改值都会自动跟上。拾取时把元素 getComputedStyle 的结果反查回令牌名,插入的是 var(--dsw-alias-bg-layer-1) 而不是它解析出的 #101010。
这张表后来加了两条约束(见「令牌建议不再给出『种类不符』或『别处定义』的令牌」一节):只有定义规则能匹配该元素或其祖先的令牌才算数,而且只有名字读起来像该属性的才会被建议 —— 否则一个 22px 的圆角会被建议成 --dsl-terminal-line-height。
动作按钮折起来了
表头原来有六个控件(文件下拉 + 拾取元素 + 打开文件 + 导入 + 导出 + 重置),一行放不下。现在只留两个常驻入口 + 一个菜单:
- 拾取元素(新功能留在手边)、文件下拉(不变)、更多操作 → 打开文件 / 导入 / 导出 / 重置。
菜单用的就是文件下拉那套 chrome(.dshCc_trigger + .dshCc_menu + .dshCc_menuItem,而它们是从 DSH 自己的 settings-row 下拉逐值抄来的),所以和界面里其它下拉一致;新增的只有 11px 的单行说明与危险项的 --dsw-alias-state-error-primary。
拾取器:能退出,且能调层级
- 修掉一个会让界面看起来「死掉」的缺陷:武装状态下设置面被
pointer-events:none,而那条提示条自己也是pointer-events:none—— 除了按 Esc,用户没有任何可点的出口。现在工具栏可点:上一级 / 下一级 / 上一个 / 下一个 / 取消;点击工具栏不会被当成一次拾取(否则点「取消」反而会选中工具栏本身)。 - 层级调整(AdGuard 那种手感):
↑/W上一级、↓/S下一级、←/A上一个同级、→/D下一个同级,Enter确认,Esc取消。移动鼠标会重新选中指针下的元素——键盘抬高或降低层级之后,鼠标是最快的退回方式。工具栏实时显示高亮位置(如section > div.dsh-music-qq-head · 320×180)。 - 一次点击不再可能什么都不发生:真实点击前总有
pointermove,但触屏与合成事件没有——这种情况下回落到指针下的元素,而不是静默忽略。
拾取器改成浮动面板(架构修正)
反馈两条:拾取时点不了设置页的关闭按钮、不能去别的页面拾取。这不是 bug 而是设计错误:我把整个设置面 pointer-events:none 来给拾取让路,于是设置页的一切交互都被按死;而且会话挂在行组件上——关掉设置页,它就随组件卸载而死。
- 会话移到模块级,不再依附设置行:关掉设置页(这是去界面其它地方的唯一办法)后拾取照常工作;行组件卸载后用它拾取依然能写进样式表(有测试守着)。
- 不再按死任何东西:设置面不动、不拦截点击(
clickToPick: false),拾取靠悬停选择,Enter 或面板上的「插入规则」确认。真实浏览器实测:拾取前后设置面的pointer-events都是auto,属性一个没变。 - 浮动面板(挂在
body上,独立于设置页):实时显示高亮元素的描述与尺寸、三个候选选择器(可切换)、该元素解析到的令牌 chips(可关掉不想写的)、层级滑杆、上一级 / 下一级、快捷键悬浮提示、插入与取消。 - 高亮框
pointer-events:none,不挡住它标出的元素(实测elementFromPoint仍返回目标)。
按钮和界面一致了
反馈是「按钮样式还是不对」。去查 DSH 自己的 CSS 才看清:它的按钮是 _button_cfgyt_4 —— 无边框 + 18px 圆角 + 透明底 + 主色文字;而 .dshCc_btn 是我自己发明的(8px 圆角 + 1px 边框 + 次色文字),所以和界面里任何控件都对不上。现在逐值对齐 DSH 的按钮与关闭按钮(后者 28×28 / 8px 圆角),只保留「选择器用填充胶囊」这一处差异来区分当前值与动作——三个控件的实测几何:custom.css 18px 填充、拾取元素 与 更多操作 18px 透明。
拾取之后状态栏不再卡在「保存中」
打完字 400 ms 内点选一个元素,状态栏会一直停在「保存中」,直到你下一次敲键盘 —— 而样式表其实早就写进磁盘了。
原因是「接管」只做了一半:拾取器写盘前会取消编辑器那条防抖中的写(否则过期的文本会盖回旧内容、把这次拾取悄悄丢掉),可取消掉的正是状态栏在等的那次写,而没有任何人接替它回报结果。现在这条交接是双向契约:
takeOver()取消挂起写,并让被取代的那次写失去发言权 —— 已经发到宿主上的请求取消不了,它的响应仍会回来,不这样做它会在拾取自己的写还在途中时就宣布「已保存」。settled(ok)在拾取这次写落盘后回报,只有成功才说「已保存」;失败时照旧留下「保存失败:…」而不是假装完成。
拾取已有规则时,面板当场打开
拾取一个表里已经有规则的元素,「插入规则」的承诺是「打开它」—— 但打开这一步迟迟不发生:面板不出现,而且在下一次无关编辑时才突然跳到那条规则、顺手把光标也挪过去。
原因和上一条同源,是回跳通道的通知方式:拾取器把「要打开的规则」留在模块级变量里,行组件靠监听 state.css 变化去消费它。复用已有规则不写任何文本,于是那次变化根本没发生 —— 消费者不重跑,请求就一直挂着,最后由一次无关的编辑把它消费掉。
修法是把这条通道的通知变成它自己的状态(openRequest 计数器,由拾取器在交接后递增):行组件的消费者同时观察它与 state.css。顺带纠正同一处的一个错:光标原本也被请求放进已有规则里,而该规则不是这次手势创建的 —— 把光标丢进用户没打算编辑的文本里是惊吓而不是帮忙,现在光标只属于新追加的空规则(与本节开头写的「否则…光标停在新块内」一致)。
拾取器不会再把「查不出来」当成「只有一个」
选择器的好坏由命中数排序:只命中一个元素的候选才算「唯一」,可以当默认。问题是命中数靠 querySelectorAll 探针问出来,而探针抛错时旧代码返回 1 —— 探针越查不出来(选择器越可疑),候选反而越像「唯一」,最后拿到默认位并写进样式表。
最典型的触发方式是属性值里的换行:aria-label 是自由文本,它可以带 \n,而 CSS 字符串里的裸换行会结束这个字符串 —— 生成出来的选择器根本不是选择器(div[aria-label="第一行 到这里就断了)。浏览器对它一律抛 SyntaxError,于是那条坏选择器因为「查不出来」而胜出、被写进表里,然后什么都不生效。两处都修了:
cssString现在把换行与其他控制字符按 CSS 的规则转义(\a这种十六进制转义,后随空格用来收尾),一次扫描完成,不会把新加的反斜杠再转义一遍。probeMatches把未知与一个区分开:探针抛错、或压根没有探针时返回「未知」,候选按「非唯一」罚位,永远不会因为查不出来而占优。面板上如实写「命中数未知」,而不是把失败打扮成一个数字。- 顺带:未知也不是 0。「查不出来」和「一个都匹配不上」是两回事,后者是结论,前者是没结论。
点击选中元素后,高亮框左上方不再多一条横线
点选一个元素之后,它的高亮框左上方会多出一条很细的横线(12×2 像素)。它只在点击之后出现 —— 因为它是锁定框画出来的。
高亮框有两个:浅色的一直跟着指针(上面那条标签写着「宽×高」),实心的那个只在你点击之后出现、标住被选中的元素。标签是一条 CSS 规则:content: attr(data-label)。标签内容是拾取时写进 data-label 的,而锁定框没有这个属性 —— 关键就在这里:attr() 取一个不存在的属性得到的是空字符串,而不是 none,所以伪元素照样生成;又因为伪元素的 box-sizing 是 content-box(它不继承),那个空盒子缩成了它自己的内边距:宽 6+6、高 1+1 = 12×2,位置正是 left:0; top:-22px —— 高亮框左上角的正上方一格。于是它就成了一条莫名其妙的横线。
实测(无头 Edge,量的是真实像素而不是样式声明):修之前,锁定框左上方 24 个品牌色像素、外框恰是 12×2;修之后 0 个,而真标签(54×20)一点没变。
修法是一行:把标签规则用属性做门(.dshCc_pickBox[data-label]::after)—— 没有标签的框,规则不适用,伪元素根本不存在。测试里加了一条样式审计守着它:把拾取器自己的样式表逐条与「还没被打上标签的悬停框」「锁定框」两个元素比对,任何一条匹配得上、又用 attr() 去取它们没有的属性的规则都会被点名;同时断言「有标签的框照样画得出标签」,免得「什么都不画」也能过。
「插入规则」之后,编辑器会滚到新规则
拾取一个元素点「插入规则」,规则确实被追加到了表尾 —— 但编辑器停在原地,看起来像什么都没发生;表越长越明显(60 行的表里,光标落在第 62 行,而视野还停在第 1 行)。
原因有两层,都补上了:
- 没人把光标那一行带进视野。之前只做了
focus()+setSelectionRange():浏览器确实会因此把 textarea 滚到光标处(实测过),但这个编辑器把文字画了两遍 —— 透明 textarea 负责光标与滚动,彩色层与行号 gutter 是靠transform跟着 textarea 的scrollTop走的,那次滚动既然是我们自己造成的,就该由我们同步。现在显式把光标行滚进可视区(上方留两行余量,只在真的看不见时才动,所以不会和浏览器打架),并在同一次就把两层同步过去,不再依赖scroll事件。 - 文本可能还没到。拾取器能在设置页关闭时继续工作,行组件重新挂载时样式表可能还在读 —— 这时把光标塞进一个空 textarea 会被钳到 0,视野一动不动。现在光标请求会等到文本里真的有这个偏移才消费。
令牌建议不再给出「种类不符」或「别处定义」的令牌
拾取器会把元素实际生效的颜色反查成设计令牌,插入 var(--dsw-…) 而不是字面值。那张「值 → 令牌名」的表只以值作键,于是不同用途的令牌会挤在同一个键上 —— DSH 里 --dsl-terminal-line-height: 22px 就挨在所有 22px 旁边。旧代码在找不到「名字读起来像这个属性」的令牌时会回退到第一个,实测结果是:为一个 border-radius: 22px 的元素建议 border-radius: var(--dsl-terminal-line-height)。这条规则不但语义莫名其妙,而且在用户样式表的作用域里根本解析不出来(该令牌只定义在终端卡片自己的类上)。
现在两处都收紧了:
- 种类:名字读起来不像这个属性就不建议(宁可什么都不给,也不给一个解析不出来的)—— 只保留能与属性名对上的那几类(
bg/label·text·fg/border/corner·radius)。 - 作用域:一个令牌只有当定义它的规则能匹配该元素或其祖先时才算数 —— 别的组件卡片内部的定义,对它外头的元素不成立。
实测(无头 Edge,拿 DSH 真实的编译后样式表):修之前 border-radius: 22px 得到 --dsl-terminal-line-height,修之后不再给出任何令牌;而作用域与种类都对的令牌照旧会被建议(有断言守着这一半,免得「什么都不给」也算通过)。
测试
- 夹具新增一个够用的 DOM 桩(节点树、属性、几何、监听器),于是拾取器能在测试床里端到端跑:点按钮 → 武装 → 悬停报告尺寸 → 点击解析 → 候选排序 → 插入规则。
- 覆盖:冲突的 aria 候选被降权且不默认选中、哈希类名不给过、插入带令牌而非字面值、已有规则被复用而不是重复追加、Esc 之后监听与浮层都被清干净。
- 写通道的交接由两条用例守着:打字后 400 ms 内拾取,断言挂起的那次写被取消、没有单独落盘、表里是拾取后的内容、状态栏当场就是「已保存」;以及已发出的那次写先回来时,断言它不能替还在途中的拾取宣布「已保存」。两条都反向打过补丁确认会变红。
- 回跳通道同样两条:拾取已有规则时断言面板当场打开、表没被再写一次、且没有光标被丢进那条已有规则;以及交接当次就被消费掉,下一次无关编辑既不开陈旧的面板、也不挪光标(反向打补丁:去掉通知 → 第一条红;恢复「把光标放进已有规则」→ 第二条红)。
- 选择器两条:属性值里的换行必须转义成
\a、裸换行一个字都不许进样式表;以及一个「探针查不出来」的候选不许拿下默认位(后者反向打补丁后能复现:坏候选被写进表里)。面板的「命中数未知」也有断言守着 —— 打回旧写法时它会显示undefined 个命中。 - 测试床的 DOM 桩不再忽略入参(这一条是测试保真度,不是产品缺陷):
elementFromPoint以前无视坐标、querySelectorAll以前无视选择器,于是「指针命中哪个元素」「这个选择器命中几个」在测试里等于常量 —— 断言看着在守行为,其实只守了「桩被配置成了什么」。现在测试床自带一个小选择器匹配器(标签 /.类/#id/[attr]/[attr*=…]/:nth-child()/ 后代与子选择器 / 逗号分组,且拒绝字符串里的裸换行,与浏览器的分词一致)与按矩形命中测试(文档序里最后一个包含该点的节点,正是浏览器的叠放规则),指针与选择器都由用例真正给进去。- 把这两处改回常量桩,套件立刻变红(第一处就死在「hovering previews the element under the pointer」)—— 这就是它以前守不住东西的证据。
- 顺带露出两个旧洞里没有记下的:
tests/loader-smoke.cjs的「已有规则不重复追加」用例从来没有真正提交过(只 dispatch 了pointermove+Enter,而Enter确认的是点击锁定的目标,悬停不算),因此它的断言一直空转;现在它先断言「悬停 + Enter 什么都不做」,再点击 + Enter 走真流程。另外「同名 aria 标签」从桩里编的 3 个变成树里真的两个元素(新增一个同标签的兄弟节点)。
tests/host-api-smoke.mjs:fs.symlink在无权限的机器上抛错时,用例以前静默跳过,而汇总仍打印「symlink refusal verified」。现在跳过会显式打印SKIPPED、并从汇总的「已覆盖」里摘掉该项(非 Windows 或非 EPERM 的失败照旧直接抛)。列表上限那条断言原本是「回复与自己的排序副本比较」,它并非恒真(把宿主排序反写成降序,套件会红),但它说不出回来了哪些文件、被固定的活动文件落在哪;现在改成对照独立期望值(由用例自己写下的 200 个名字 + 被固定的那个)。- 拾取器样式审计(守住上面那条横线):遍历拾取器自己的样式表,与「悬停框(未打标签)」「锁定框」两个元素逐条比对 —— 匹配得上、又用
attr()取它们没有的属性的规则一律点名;并断言有标签的框照样画得出标签,防止「把规则删光」也算通过。反向打补丁(去掉[data-label]门)会红,并报出具体规则与两个变体。 - 编辑器滚动:60 行的表里拾取并插入规则,断言光标所在行落在滚动窗口内、
scrollTop真的动了、彩色层与 gutter 的transform与之一致(反向打补丁会红在「光标行在窗口内」上,报出line 62 at 1178, port 0..140)。测试床的节点桩也补上了matches,选择器匹配器补上了:root。 - 令牌建议两条:一个与属性种类不符的令牌(
--dsl-terminal-line-height之于border-radius)不许出现;一个定义在别的组件类上的令牌,对作用域外的元素不许出现;同时断言作用域与种类都对的令牌照样出现。两处反向打补丁各打红各自那条。 - 这条横线本身是真机量出来的:
%TEMP%\css-probe\下新增stray-label-line.mjs(同一份 ROW_CSS 渲染两种变体、按像素数锁定框左上方的品牌色像素)与empty-label-isolated.mjs(把「缺属性的attr()到底画不画」单独隔离)。两次第一次尝试都被自己证伪过:一开始只看计算样式(伪元素的width/height报 0,看着像没画),后来像素扫描又把同色的框边算进去了 —— 量到的东西必须能解释得通才算数。 - 死代码清理 + 孤儿规则审计:删掉 11 条「有规则、没有任何代码去命中」的 CSS(
.dshCc_danger两条、.dshCc_panelHint、.dshCc_pickBarInfo、行内结果卡时代留下的 7 条.dshCc_picked*),以及只定义、从未被调用的surfaceToMoveAside。加了一条审计守着它:把样式表里出现的每个类与样式表之外的源码比对,任何无人命中的类都会被点名(它同时也是"类名拼错"的探测器 —— 那种错误同样不可见)。- 审计必须认得出拼接出来的类名:
'dshCc_tok' + kindOf()会拼出dshCc_tokProp/dshCc_tokVal。第一版审计把这两个报成了死代码 —— 假警报,而且是差点删掉正在用的语法配色;现在它把这类前缀也算作"被命中"。
- 审计必须认得出拼接出来的类名:
- 另有一项真浏览器 DOM 管线验证(本地仿真页 + 真 Edge):确认让路后面板后面的元素确实点得到、我们自己的高亮框不会挡住它(
pointer-events:none+position:fixed)、会话用真实几何报告目标。
v0.1.5
0.1.5 — 补全路径与最后一圈字符串盲区
0.1.4 把「字符串不是结构」铺到了解析器、校验器、高亮器、读取器和写入器。复审指出补全这一路还没铺,顺着这条线又扫了一遍,最后三处盲区是同一个病根。
补全路径(用户直接撞得上)
insideBlock裸数花括号:content: "}"是清浮动的标准写法,字符串里的那个}被当成真的闭括号,于是它之后的所有补全静默失灵(在规则里打color:什么都不弹)。触发后用户只会觉得「补全在某些规则之后就不灵了」,很难自己定位。现在与其余扫描器共用同一次结构遍历。completionsFor用lastIndexOf(';')找声明起点:字符串里的分号被当成声明分隔符,光标所在的那条「当前声明」切片错位 —— 该给值的地方弹出属性名列表,或者干脆什么都不给。现在按结构找最后一条真实声明边界。- 未闭合的字符串或注释里不再补全:光标在值里面的字符串中间时,属性名列表纯属噪音。
- 「转义引号结尾」的漏判:判断字符串是否闭合的规则原本是「停在末尾、且末字符不是引号」——
"ab\"这种末尾恰好是转义引号的文本会被判成已闭合,补全于是从字符串内部弹出值列表。现在改由scanString/scanComment自己报告closed:是否闭合只由做扫描的那一处决定,调用方不再重新推断。顺带把十处手写的「注释到哪结束」收进scanComment(此前每处都在indexOf('*/')上重新推导一遍)。
最后三处盲区(同一病根)
- normaliser 往注释里插分号:
normaliseBlock的正则扫整段 body,注释里「上一行以字结尾、下一行看起来像属性名」会被当成缺分号边界 —— 于是改任意一条声明都会改到注释的文字(注释里凭空多一个;,对页面无害,但那是用户没写的东西)。现在只处理字符串、注释、url()、嵌套块之间的结构间隙。 - 删除声明时把字符串当注释复活:
content: "/* x */"被删掉后,字符串内容被当作「与它同块的注释」保留下来,样式表里凭空出现一条注释。现在只保留真实注释。 color/* x */: red被判成两条无法解析的声明:注释是词法空白,不是语句边界。校验器现在把注释按等长空白并入缓冲区。
测试床(评审指出的两个偏差)
useEffect现在认 deps:给了数组只在某项变化时重跑,不给数组则每次提交都跑 —— 与 React 一致。此前每次渲染都会重跑所有 effect,测试可能在依赖 React 根本不会产生的行为。- 「新一轮渲染」的信号不再假设
useSyncExternalStore是第一个 hook:任何类型的第一个 hook 都会标记新的一轮。行内 hook 顺序是它自己的事,在它前面加一个 hook 就会静默错位。
测试
- 新增 7 条用例:
content: "}"之后补全仍然工作、字符串里的分号不当作声明边界、未闭合字符串里不补全、转义引号结尾仍算未闭合(且正常闭合的字符串不会连带压掉它后面的补全,避免修成单向门)、属性名与冒号之间的注释合法、注释不被当成缺分号边界、删除声明不复活字符串内容。每条都用「先把补丁反向打回去、确认测试会红」验证过。 - 复审之外的独立探针又跑了一遍:20 条刁钻输入、真实样式表的 251 次声明写回往返(0 报错、结构不变量全成立)。
v0.1.4
0.1.4 — 写入器、扫描器与宿主接口修复
外部评审提了四条,逐条到代码里核对过 —— 全部属实,已修;并补上了它们赖以躲过测试的盲区。
会改坏用户样式表的两条(优先)
- 写入偏移错位:
writeDeclaration的快速路径在normaliseBlock之后用规范化后的偏移去切原始文本。规范化会插入;让 body 变长,偏移因此整体前移,切掉的区间变成目标声明前面的一段字符 —— 结果不是写错值,而是把后面的内容(包括收尾的})吃掉,样式表结构直接断。触发条件是「块内有一条缺分号的声明排在目标之前」。现在整条路径只在规范化后的 body 上算偏移并把该 body 写回,写入时的规范化成为唯一口径。 - 注释让声明认不出来:分块按
;切、正则要求块首就是属性名,/* note */ display: block这种同处一块的写法块首是/,匹配失败 → 原声明没被替换、又追加一条 → 出现重复声明(页面表现随之改变)。现在分块前把注释按长度空白化再匹配(注释本身仍留在原文里);被替换的声明若与注释同块,注释保留为独立一行。 - 顺带修了读数侧的不对称:
readDeclarations此前不做规范化,于是「缺分号的块」在面板里显示为一条声明都没有(写侧会规范化,两侧口径不一致,也正因如此上一条 bug 在 UI 上难以触达)。现在两侧都走normaliseBlock。
状态与交互两条
- 「已保存」会说假话:定时器已触发后
clearTimeout无效,旧的flushSave().then(setSaved(true))会在用户又输入(setSaved(false))之后把状态改回「已保存」,直到新写回完成为止。现在每次编辑递增saveEpoch,写回完成时只有自己那一代才允许改状态。 - 清空选择器后无法恢复:面板靠选择器定位规则,选择器被改成空串后规则不可解析 →
openRule为 null → 面板连同它的输入框一起消失,代码区留下一个没有选择器的{ … }且点无可点,只能去 textarea 手动补。现在输入框保留本地草稿(清空时显示为空,但不把空选择器写进样式表),失焦或回车后回落到已提交的选择器。
测试
评审指出根因是测试盲区:假 React 的 useEffect 是空实现、setTimeout 直接返回 0,所以 debounce、flushSave、卸载 flush 与 saved 状态机从未真正执行。测试床已升级:
useEffect记录回调与 cleanup,可按需执行或模拟卸载(runEffects/unmount)setTimeout变成本地队列,测试用runTimers()手动触发,clearTimeout会标记取消- 新增四个复现用例:快速路径不吞
}(且缺分号被补上)、注释下的声明只被替换一次且注释保留、慢写回不冒充「已保存」、清空选择器后面板仍在且表中选择器不变
自查:扫描器要认得字符串
- 四个扫描器此前都不认字符串:
parseRules、validateCss、highlightCss与声明分块都按裸字符判断结构,于是content: "}"里的括号被当成真括号(校验报「第 1 行 多余的 }」,规则偏移全部往后错位、点到的是别的块),url("data:…;base64,…")里的分号被当成声明分隔符(值被截成两半)。现在共用skipString/blankComments/splitDeclarations,四处口径一致。 - 测试床自己也在骗人:
hookSlots跨boot()复用,新实例拿到的是上一个实例的useRef对象和残留 cleanup,于是「卸载时补齐未保存的编辑」这条用例真正执行的是上一个 boot 的 cleanup(它读共享 ref 拿到新文本、却用旧 sandbox 的 fetch 发写请求),断言必然数到 0 次写入。现在每个 boot 重置 hook 槽,新提交的 effect 会清掉旧 cleanup,用例才跑在它自己的组件上。
二轮:两路对抗式代码复审
客户端 10 条、宿主与流水线 14 条,逐条到代码里手工复核(每条修复都用「把补丁反向打回去、确认测试变红」验证过测试有效性)。
会改坏用户样式表的(优先)
url()不算不透明:未加引号的url(data:image/svg+xml;charset=utf8,%3Csvg/%3E)里的分号被当作声明分隔符 —— 面板显示半截值,校验对完全合法的写法报「无法解析的声明」,慢路径重建时还会把碎块焊成一条废声明。现在url(...)(引号内引号外)整体跳过。- 嵌套块不是声明区:CSS 嵌套(
.a{ color: red; &:hover{ … } })的内层声明会被面板当成外层声明列出来,删掉那一行会把内层的}一起带走 → 大括号失衡、浏览器从此丢弃后面所有规则。现在分块遇到顶层{就把整个嵌套块切成不透明的一块,面板只列外层声明。 @import "x";之后的第一条规则点不动:前奏缓冲没在顶层;处收尾 ——@font-face被当成普通规则判(对合法描述符报错),下一条规则的选择器里仍然带着@import、以@开头被当作 at-rule 跳过(编辑器里点不动、面板打不开)。校验与解析现在都按语句边界收尾。- 未闭合的块少一个字符:
.a { color: red(正在输入时就是这个状态)的 body 末尾被判成文本末字符,面板显示re,写回还把那漏掉的字符粘到新值后面变成废值。现在找不到闭合括号时 body 一直到文本末尾。 - 同名规则会绑错面板:两张
.a的样式表里点第二条,面板(以及之后每一次写入)都落在第一条上,且看不出任何异常。现在面板按规则序号记住自己属于哪一条。 - 补全与光标脱节:补全列表不会因为「在同一个 textarea 里点了一下」而关闭,按回车时用旧单词配新光标去切字符串 → 直接删掉几个字符、再把补全插到错的位置,样式表被改坏且无声。现在接受补全前按当前光标重算单词,对不上就丢弃。
- 伪类选择器点不动:高亮按标点切分,
.a:hover被拆成.a/:/hover,而只有「正好结束于选择器末尾」的那一段是可点区域 → 规则名点不动、hover反而能点。现在选择器范围内每一段都是该规则的可点区域。 - 冒号后的注释会被删掉:值是从「注释已空白化」的副本里读的,面板显示的是文件里并不存在的文本;写回时头部正则又匹配不到冒号后的注释,注释静默消失。现在属性名仍用空白化副本匹配,值取自原文。
- 写失败也报「已保存」:写回失败时状态行会同时出现「已保存」和它的失败提示;随后切换文件还会顺手把失败提示清掉,丢了编辑却什么都不显示。现在只有写成功才允许改状态,切换 / 导入不再抹掉失败提示。
宿主、安全与流水线
- 超限请求体会销毁连接:提前
return会触发for await迭代器的return(),Node 据此 tear down 请求 —— 浏览器看到的是网络错误(Failed to fetch)而不是 400。现在读满即丢弃并继续排空,回包仍落在活着的 socket 上。 active.json改为临时文件 + rename:直接写会先截断,中途断电留下的半截文件会被读成「没有当前文件、没有关闭项」,下一次改动就把这个空状态固化;改动也改为串行执行,两个标签页同时开关不再互相覆盖。- 幽灵关闭项:指向已删除文件的关闭项在每次写入时清理,因此「关掉 → 删文件 → 重建同名文件」不会再一出生就是关闭状态;
/create//import之后该文件是启用状态。 - 文件名判定统一为一个
isSheetName(长度上限、..、斜杠、Windows 设备名):此前目录扫描比路由宽松 —— 手工放进目录的超长文件名会被列出、却在每条路由上都 400(切换失败:bad-name),nul.css这类设备名甚至能「写入成功」但什么也没落盘。现在「列出来的名字一定可用」有一条断言盯着。 - 符号链接拒绝:字面量包含性校验看不穿软链,目录内一个指向外部的
link.css就能让写入落到目录之外、让读取返回非样式表文件。读、写、创建、导入、切换、打开之前都先lstat。 - 「名字非法」与「文件不存在」分开:
/active、/open缺文件现在是 404not-found(以前是 400bad-name,客户端于是显示「切换失败:bad-name」)。 /list的上限不会吞掉当前文件:200 个文件的上限之外仍必定列出当前文件,否则行会退回到列表第一个、每次打开页面都悄悄换掉用户选的样式表。- 发布流水线:新增「tag 必须等于
package.json版本」的门禁(不一致时:tag 落后会 npm 报错而 Release 建不出来,版本超前则 registry 与 Release 各说一个版本);预发布版本改用--tag next发布,不再把latest指给候选版。 - README 里指向
.github/workflows/ci.yml的相对链接在 npm 页面上是死链(files不含.github/),改为仓库绝对地址。
测试
- 客户端新增用例:不透明
url()、嵌套块、@import语句边界、未闭合块、同名规则面板绑定、补全与光标一致性、伪类选择器整体可点、冒号后注释保留、写失败不冒充「已保存」。 - 宿主新增:
/active的三条路径(命中 / 缺失 / 保留开关)、每条 POST 路由的穿越拒绝、Windows 设备名、错误方法、超限请求体(并确认文件未被改动、连接随后仍可用)、围栏拦住写请求、active.json损坏后的恢复、删文件后关闭项清理、列表上限不吞当前文件、符号链接拒绝,以及「列表里每个文件都能读」的一致性断言。
v0.1.3
0.1.3 — 编辑器面板重做 · 已发布 2026-09-13
这一版把「设置 → 通用 → 自定义 CSS」这一行的形态定下来:一个容器、三段布局(表头 / 正文 / 状态行),属性面板移到代码下方并以网格铺开,同时补上 112 条属性字典与简写属性的分量输入。
编辑器行
- 新增文件条:
#图标 + 文件名 +CSS徽章 + 状态点 + 开关胶囊。 - 开关语义是临时停用当前样式表:样式不再注入页面,而文件内容、当前选择、校验状态全都不动。状态写进
active.json的disabled列表,多个浏览器与重启后表现一致;host 半新增POST /toggle {name, enabled}、GET /list增加disabled字段(旧的{"active": …}文件仍可读,缺失即视为全部启用)。旧 host 半(尚未重启 dsh)会让开关回退到本浏览器localStorage,功能不失效。 - 整块编辑器收进一个容器:表头 / 正文(代码 + 属性面板)/ 状态行三段同框。容器是单列 grid(
grid-template-columns:minmax(0,1fr)),每一块都写明width:100%+box-sizing:border-box+min-width:0,不再依赖 flex 默认的align-items:stretch。
规则面板
- 面板从代码右侧移到代码下方,声明与模板按自适应网格铺开(声明
minmax(200px,1fr)、模板minmax(124px,1fr)),面板限高260px内滚动;代码区成为容器内独立的一层表面。 - 属性字典 14 → 112 条,分十组(布局 / 弹性与对齐 / 网格 / 尺寸 / 间距 / 文本 / 背景 / 描边与圆角 / 效果与动效 / 交互);"+ 添加属性…" 菜单用
<optgroup>分组,整组设满时该组自动隐藏。 - 字典支持三种形态:
- 枚举型 → 中文值下拉(写入的是 CSS 值本身);
- 自由型(
gap、margin-top、font-size、box-shadow、grid-template-columns…)→ 中文标签 + 自由输入框,添加时写入字典里的种子值,占位文本即取值提示; - 分量型 →
flex(放大 / 收缩 / 基准尺寸)、gap(行 / 列)、margin/padding/inset/border-radius(上 / 右 / 下 / 左)、aspect-ratio(宽 / 高)。读取按 CSS 简写展开规则拆分(margin: 8px 12px→ 8px / 12px / 8px / 12px),写回收敛为最短等价形式(四值相同 →margin: 8px);分量名是浅底小标签(--dsw-alias-markdown-tag+label-secondary),与值成对排在同一行;border-radius的斜杠形式保留自由输入框。
- 面板里的下拉全部改为自绘菜单:原生
<select>的弹出层由操作系统绘制、CSS 控制不到(方角 + 系统配色),现在用与文件下拉同一套的 DSH 菜单(--dsw-specific-menu底 +--dsw-elevation-prominent阴影 + 20px 圆角 + 10px 圆角菜单项)。菜单以position: fixed按触发按钮定位,既不会被容器的overflow:hidden裁掉、也不会被面板自身的滚动区截断;视口下方不足 240px 时自动向上展开。 - 控件规格对齐 DSH 自身的设置行:36px 高 / 18px 圆角胶囊 / 14px 字号 —— 属性值输入框、下拉触发按钮、选择器字段、行内文件名输入框、同行动作按钮统一 36px(字段为胶囊、动作为 8px 矩形,保持层级);菜单项 36px 高、模板 chip 36px 高。
- 选择器字段与
×合为一个控件:×从输入框右侧的独立按钮改为框内的圆形按钮(26px),输入框因此铺满面板宽度;聚焦环移到外层字段、输入框本身无边框。「+ 添加属性…」位于属性列表下方,铺满面板宽度、文案靠左、箭头贴右端。
修复
- 孤儿声明让样式表后半段全部失效:删
.dshCc_propValue规则时只删掉了选择器行、留下两行声明,使注入的样式表大括号失衡({112 个、}113 个)—— 浏览器会从失衡处丢弃后面所有规则,面板输入框因此退回浏览器默认外观。测试新增大括号必须平衡的断言:文本断言看不出解析错误,这条能。 - 控件被容器压缩:面板是
max-height:260px的纵向 flex 容器,子元素默认flex-shrink:1,内容一超高就把直接子元素挤扁(选择器字段实测被压到 26px,而样式表写的是 36px)。现在.dshCc_panel>*{flex:none},行高由样式表决定、超出部分由面板滚动。 - 网格项 / flex 项的固有宽度溢出:两者默认
min-width:auto(等于内容的 max-content),而<select>会把最宽选项报成固有宽度,一个长选项就能撑破面板。整条链路补min-width:0与max-width:100%,面板另加overflow-x:hidden兜底。 - 容器内的 16px 溢出:正文行设了
padding:8px却没写box-sizing:border-box,盒宽 = 容器宽 + 16px,把代码区与属性面板一起顶出右边界。 - 代码编辑区最小高度恢复 140px(过程中曾误改为 120px,拖拽手柄因此能把编辑区缩到比自身高度还小)。
测试与验证
- 浏览器半新增:开关(渲染 → 关闭后样式标签被移除 → 恢复后样式回到页面 → host 无该路由时仍可用并落到 localStorage)、简写分量(
flex三分量、margin两值展开成四框、gap两值收敛回单值、圆角斜杠回退)、结构断言(容器三段互为直接子节点、面板在代码下方、添加属性控件在网格下方)、样式守卫(大括号平衡、盒子与收缩规则)。 - host 半新增:
/toggle的持久化、遍历名 400、缺失文件 404、非布尔值 400。 - 用无头 Edge + puppeteer-core 做真实排版测量(容器 600 / 420px 两档):控件统一 36px、编辑区 140px、分量行与网格同宽、
scrollWidth等于容器宽、无元素越界。 - 兼容性未变:
0.1.2-rc.1与0.1.5-rc.1已真机验证,0.0.1-rc.5/0.1.0-rc.7/0.1.1-rc.2接口断言通过(npm run compat)。
v0.1.2
0.1.2 — 兼容性证据与换代门禁 · 已发布 2026-09-13
- 新增
tests/compat-matrix.mjs与compat.json:逐个 DSH 版本断言插件依赖的平台契约(槽位settings.general.item与dsh.client.inject的两个 id)。npm run compat本地与 CI 均可复跑;CI 新增该步骤(单档)——DSH 换代改名时会先在这里红,而不是等用户看到模块表报错。 - 真机验证扩到
0.1.2-rc.1:独立DSH_HOME起实例,/dsh-custom-css/list、POST /write、GET /read全部 200,文件确实落盘;0.0.1-rc.5/0.1.0-rc.7/0.1.1-rc.2通过接口契约断言。README 新增「兼容性」一节(含验证矩阵与两种失效模式的判别)。 - README「已知坑」补一条:跨代插件的
missed the module table报错成因与判别方法。 - 无运行时改动:
lib/client.js与lib/index.js与 0.1.1 逐字节相同,本版只增加兼容性证据、门禁与文档。
v0.1.1
0.1.1 — 发布通道与文档 · 已发布 2026-09-13
- README:新增 npm 徽章(version / unpacked size);安装章节改为「npm 优先」,GitHub 装法与
link:开发安装顺延为方式二、方式三;顶部元信息补 npm 包链接。 - README:注释里记下下载量徽章暂缺的原因(npm downloads API 对刚发布的包要等约一天才有数据),届时补
img.shields.io/npm/dm/dsh-custom-css。 publish.yml:打开push: tags: ['v*']触发(OIDC 受信发布 + provenance 签名),并把唯一前置写进注释 —— 先在 npmjs.com 给这个包配置 Trusted Publisher。- 无运行时改动:
lib/client.js与lib/index.js与 0.1.0 逐字节相同,本版只为刷新 npm 页面上的 README 并走通自动发版流水线。
v0.1.0 — 首个公开版本
0.1.0 — 首个公开版本 · 已发布 2026-09-13
设置行 / 编辑器
- 在 设置 → 通用 → 外观 下方新增一行「自定义 CSS」(
settings.general.item,order: 12)。 - 样式表以普通
.css文件存放在~/.dsh/custom-css/,当前文件记在active.json;编辑即时生效(400ms 防抖)。 - 编辑器照 DevTools Styles 标签页做:行号 gutter、shiki 同色语法高亮、键入时补全。
- 补全数据取自浏览器自身:属性名枚举自
CSSStyleDeclaration.prototype,值的候选逐个过CSS.supports()。 - 补全只在真正键入时出现(看
InputEvent.inputType),且一行里有多条声明时只认光标所在的那条。 - 规则面板:点选择器就地展开 —— 可重命名选择器、逐条列出已有声明(枚举型给中文名 + 中文值下拉)、未设置的属性收进「+ 添加属性…」、底部是 14 个声明模板。
- 改属性是替换而非叠加:写入走逐条声明的扫描 + 块规范化,不会堆出
display: flex; display: grid;。
这一行上的控件
- 文件下拉(DSH 原生样式,末项「+ 新建…」)、打开文件(调起系统默认程序)、导入、导出(另存为 / 回退下载)、重置(错误色,语义破坏性)。
host 侧
/dsh-custom-css前缀下的 JSON 接口:/list、/read、/write、/create、/import、/active、/open。- 全部走 Connection 服务的请求围栏;拿不到围栏时 503 fail-closed。
- 文件名限定单个路径段 +
.css(≤64 字符),目录外路径二次校验。 - 围栏不可达时浏览器半自动降级到
localStorage,编辑器仍可用。
校验
- 每次渲染扫一遍样式表:括号与注释闭合、声明可解析、值被引擎接受;状态行给出行首问题 + 总数(最多 5 处)。
- 描述符块单独处理:
@property的syntax/inherits/initial-value是 descriptor 而非 CSS 属性,CSS.supports()一律不认 —— 早期实现把它们当声明判,合法的@property会被报成 3 处错误。现在@property按描述符规则校(syntax必须带引号、inherits只能布尔),@font-face/@page/@counter-style/@viewport/@font-palette-values/@font-feature-values整块交给引擎。 - 回归测试覆盖:合法
@property零报错、非法描述符必须报出、!important不算值的一部分。
文档
- README 补齐:从 npm / GitHub 安装(
dsh plugin --profile web add+dsh.profile.bundles两步)、本机link:安装、仓库结构、以及一节已知坑(注入顺序与!important、类名哈希、:has()不可嵌套、描述符不是属性、host 半需重启、自定义属性动画需@property)。
发布
0.1.0于 2026-09-13 发布到 npm(npm i dsh-custom-css/registry.npmjs.org,10 个文件,带 registry 签名)。首次发布走本机npm publish+ 浏览器 OTP;此后由.github/workflows/publish.yml以受信发布(OIDC + provenance)完成,不需要长期令牌,也不会再弹动态码。