Repository navigation
Releases: WTStarMark/dsh-myskin
Release list
dsh-myskin 0.4.1
dsh-myskin 0.4.1 / DSH 全兼容皮肤可视化编辑框架
新增
- 兼容模式:「皮肤管理」页新增开关,开启后皮肤不接管背景,可与壁纸 / 玻璃 / 桌面壳特效类插件同时使用。
具体行为:不画canvas.background与为此服务的表面透明规则;不写背景类令牌(--dsw-alias-bg-*、--dsw-specific-sidebar-fill)——
正是不透明表面会盖住别人的背景;不发布html[data-dsh-skin],因此不要求壁纸插件让路(让路会清掉用户的壁纸)。
前景令牌、css规则、文字替换、注入图层、嵌入图照常生效。
背景图与背景类令牌仍保存在文档中,关掉即恢复;绘制模式的实时预览遵守同一套规则,令牌面板把跳过的条目标注为「兼容模式:不写」。
开关状态写成文档css里:root上的标记--dsh-myskin-compat(与背景强度 / 壁纸锚点共用一条规则),
因此不需要重启 DSH,旧宿主也能保存。 - 适配 DSH 原生右侧侧边栏:「区域外观」新增「右侧侧边栏」区域,锚点
[data-dockkit-pane]。
DSH 0.2 自带的右侧栏(文档预览 / 文件 / 浏览器 / 终端等面板)由dsh-client-ui-sidebar-right渲染,
data-dockkit-*只此一包使用(对着本机安装核对过),因此不写死构建哈希;容器.panel[data-sidebar-right-panel]故意不做锚点——
它是面板背后那条全高定位带,染它等于给一块用户看不到边的区域上色,而可视面是 pane 本身。
变体的质感与底色同步覆盖右侧栏,避免"左右两侧材质不一致"。 - 变体拆成两个轴,并新增「作用对象」:「卡片材质」拆为质感(模糊 / 边框 / 阴影)与底色(玻璃白 / 卡片色 / 透明)——
一个面由"边框模糊阴影"和"底色"两件事组成,回答的是不同问题;绑在一起时「有玻璃边缘但不盖背景」无法表达,
而这正是与壁纸类插件共存的正解。卡片顶部新增「作用对象」:全部区域,或单独一处(对话区 / 左侧栏 / 右侧侧边栏 /
输入框 / 设置页 / 消息列表),选中一处后这一轮点击只改那一处。
选择按处记录(--dsh-myskin-variant的条目形如radius=pill@composer),读回时「全部区域」打底、本处覆盖;
排版令牌与强调色没有分区形态,始终全局,卡片里写明。旧文档的material=…自动映射到两个新轴,界面照旧显示真实选择。 - 圆角可四角单独定义(「区域外观」与「变体」都有):统一值(
border-radius)之外,左上 / 右上 / 右下 / 左下各一行
(border-top-left-radius等长属性)。某一角留空=跟随统一值,单独填了只改那一角。
两条值同时存在且都能读回:写角时把简写提到声明块最前面(简写会一次性设置四角,写在后面就会把角盖掉),
只重排、不改写其它声明;整套预设仍然描述整个面(会把四角清掉,回到统一值)。
「变体」里四角是四个子轴(折叠在「圆角」下,点选方式与其它维度同构):每个只写自己的长属性,没点过的角显示「跟随统一圆角」,
作用对象 / 撤销 / 恢复默认对它们与普通轴完全一致。 - 装了壁纸插件就自动进入兼容模式:检测到
dsh-plugin-wallpaper-engine时(只读它常挂的data-we-glass-page/data-we-adapter/data-we-wallpaper),
文档里没表过态的皮肤自动不接管背景:装了就说明背景有人管,不必先让用户去找开关。
开关一旦被点过就是明确的决定(--dsh-myskin-compat: 1/: 0),永远盖过自动检测;「还原默认」清空文档后回到自动。
检测只看 DOM,不读对方配置、不写对方标记;插件中途装上或卸下都会即时生效。
变更
- 皮肤包格式改名为
.dshframework(原.dshskin):「导出皮肤」产出的文件名与导入口的接受列表同步更新,
包内manifest.json的format字段写作dshframework、资源引用写作dshframework:assets/…。
旧包照旧能导入:format: "dshskin"与dshskin:assets/…两种写法都认,.dshskin/.json也仍在接受列表里——
磁盘上已有的文件不该因为改名而失效。模块随之改名为src/client/dshframework.ts(测试tests/dshframework.test.mjs)。 - 定位改为「DSH 全兼容皮肤可视化编辑框架」:README、包描述、设置页首行说明、随包技能与本文档同步。
- 声明兼容适配 dsh-wallpaper-engine:README「兼容性」新增专节、随包技能 §10.1 写出上游链接与三条不变量;
对照版本 1.3.0-r2(自动兼容模式 / 不发布让路标记 / 读它的在台标记,三条都有测试)。
移除
- 「实时预览」按钮:它做的只是
enabled: true+ 关闭设置弹窗,与上方「启用皮肤」开关重复,而名字承诺了它没有的"不写入文档"。
绘制模式内的即时预览不受影响。preview文案键一并删除(中英)。
修复
- 绘制模式的「还原默认」无法还原:它此前只把草稿清空。已写入的皮肤仍由引擎画在页面上(自有样式表 +
body上的令牌变量),
而绘制模式这一层只能做加法,因此点完按钮页面「什么都没发生」。现在它把空文档写进皮肤文档(走「保存」同一条通道),
引擎随之撤掉旧皮肤,页面真正回到原生外观;撤销栈留一步,Ctrl+Z 可把整套皮肤取回草稿继续编辑。 - 极客模式的「还原默认」无法还原:它此前只清空代码框,规则本身仍留在草稿(并已画在页面上),下一次编辑又会把框填回来。
现在它连同规则一起删除,并让面板放弃对该元素各字段的占用——否则预览会把这些属性再写回去。 - 「还原默认」会吃掉皮肤库:皮肤管理页那一个写入的是整份空文档,
library一并清空,用户保存过的命名皮肤(连同其画布与图片)
一次点击即丢失。现在还原写入的是"保留皮肤库的空文档"(resetSkin),皮肤库是用户数据,不属于"当前外观"。 - 还原的二次确认按钮叫「保存」:该按钮此前复用了命名皮肤弹窗的文案键,于是"确定丢弃草稿?"下面写着「保存」。
新增独立文案「确定还原 / Restore now」,并把提示改写为它真正做的事(清空整套皮肤,保留皮肤库)。 - 写入失败被画成绿色成功:保存 / 还原被宿主拒绝时,提示行用的是成功色。现在按告警色显示并点名被拒字段。
- 变体的「玻璃」其实一直没有模糊:区域字段自带模板
blur({value}),而变体传的是blur(18px),
写出来是backdrop-filter: blur(blur(18px))——无效 CSS,等于没有模糊(同类的blur(none)也一样)。
现在传裸数字、关闭时清掉声明,并加了一条直接断言画到页面上的 CSS 的测试(旧测试从模板读回,正好把这个错盖住了)。 - 「右侧侧边栏」改了没反应:右侧栏不是一个元素,而上一版只锚了已停靠的 pane(
[data-dockkit-pane])。
侧栏刚打开时显示的是「开始」引导页,此时没有任何 pane 挂载 ⇒ 命中 0,改圆角自然毫无反应。
现在锚点补齐为三个:面板本身[data-sidebar-right-panel][data-sidebar-right-open](引导态与已打开态都在)、
已停靠 pane[data-dockkit-pane]、浮层 pane[data-dockkit-float](与面板拿同一个圆角,否则方角子元素会盖住面板圆角)。
[data-sidebar-right-open]这半截是必须的:面板容器关闭时仍留在布局里(只把子元素visibility:hidden),
只锚裸属性会在关闭状态留下一块色板——壁纸引擎的样式表里写着同一个坑,并做了同样的护栏。 npm run check:compat在本机直接崩溃:该脚本把 pnpm store 里每个@deepseek-ai+dsh-*包的文本拼成一个字符串再查找,
而本机同时装着 0.1.5 / 0.1.7 / 0.2.0 三代 DSH(合计约 827 MB 文本),拼接超出 V8 的字符串长度上限,
抛RangeError: Invalid string length,一条契约都没检查。改为逐个包扫描(needle 只可能落在单个包内,语义等价),
现在跑完并 PASS。
变更
- 令牌面板在兼容模式下把背景类条目标为「兼容模式:不写」并置灰:设置项"改了没反应"必须当场有解释。
- 绘制模式画面页在兼容模式开启时说明背景不生效及其原因。
- README / 随包技能同步:新增「兼容模式(不接管背景)」小节,并把那条自动互操作(对方壁纸在台则不抢画布)写成兼容模式的手动退出。
- 兼容模式同时管住区域 / 面板底色:区域卡写下的
background-color现在带一条自有标记(--dsh-myskin-panel),
引擎在兼容模式下只摘掉这条声明与标记,保留同一规则里的圆角 / 模糊 / 边框 / 内边距;手写规则不受影响。
区域卡与「变体」的底色都走这条路径,因此在壁纸前自动让开;两处卡片在兼容模式下都会写明"底色不写"。 - 绘制模式的实时预览改用引擎同一套过滤(
paintableRules):预览不再承诺一个保存后不会出现的底色。 - 变体的标记格式扩为
axis=option[@scope],并只记录真正被点的那一个轴:旧行为会把没点过的轴按"第一个选项"一并写进标记,
有了作用对象之后那就是在替用户宣称一个没人做过的选择("全部区域:直角"而输入框是胶囊)。
兼容
- 与壁纸插件
dsh-plugin-wallpaper-engine共存:新增src/client/interop.ts,按对方 1.3.0-r2 公布的契约(只读 DOM 标记,双向都不 import):
皮肤在台上时挂html[data-dsh-skin="dsh-myskin"],对方据此让路(清壁纸层 + 摘玻璃整族,并记住用户选的壁纸以便放回);
停用 / 还原 / 卸载时摘掉,另一个皮肤插件写的同名标记只读不写。 - 对方壁纸在台时不抢画布:读
body[data-we-wallpaper],命中时本插件不画自己的画布壁纸与表面透明层(其余令牌 / 规则 / 文字 / 图层照常),
并观察该标记的变化——对方让出画布后壁纸自动回来。
测试
-
新增
tests/reset.test.mjs(还原文档的形状、皮肤库保留、宿主 schema 接受)、
tests/reset-ui.test.mjs(三个「还原默认」按钮 + 撤销,跑真实组件)、tests/interop.test.mjs(两个标记的读 / 写 / 不越界)。 -
新增
tests/compat.test.mjs与tests/compat-ui.test.mjs(兼容模式:令牌分类 / 标记共用 / 引擎组合 / 面板底色过滤 / 开关端到端 / 令牌面板标注);
tests/regions.test.mjs增加右侧栏锚点与底色标记,tests/variants.test.mjs增加右侧栏材质覆盖。
UI 组件测试的 jsdom 外壳抽成tests/helpers/section-harness.mjs,由reset-ui与compat-ui共用。 -
变体重做:
tests/variants.test.mjs(8 项:两轴独立、作用对象、全局令牌、旧标记映射、整套、清理)与
tests/variant-ui.test.mjs(3 项:作用对象端到端、玻璃边缘不盖背景、兼容模式下底色被点名且不画)。 -
自动兼容:
tests/interop.test.mjs增加"插件是否已加载"的标记识别与观察,tests/compat.test.mjs增加
on/off/auto三态与"装了就自动进入"(含用户明确关闭的反例),tests/compat-ui.test.mjs增加自动开启的界面表现。
tests/regions.test.mjs另外钉住四角独立写入、简写提序与整套预设对四角的清理。
tests/variants.test.mjs与tests/variant-ui.test.mjs钉住变体卡的四角子轴(只写自己那条长属性、统一值仍在、界面标注"跟随统一圆角")。
tests/dshframework.test.mjs另外钉住旧名包的导入(format: "dshskin"+dshskin:assets/…)与"新包只写新名"。
合计 245 项通过;npm run check:types对本机 DSH 0.2.0-rc.2 通过,npm run check:compat通过。 -
打包:
npm run pack:share产出/home/share/dsh-myskin-<版本>-rN.zip(含预构建lib/,脚本见scripts/pack-share.mjs)。
升级说明
- 客户端半区有改动:更新后刷新页面(或在 DSH Desktop 重新打开窗口)即可生效,无需重启 DSH,皮肤文档结构未变。
- 「还原默认」的语义变化:不再删除皮肤库里已保存的皮肤。需要连库一起清空时,逐条删除皮肤库条目即可。
- 「兼容模式」的状态同样写在文档
css的:root标记(--dsh-myskin-compat)里,不是新字段,旧宿主也能保存。
[0.4.0]
新增 · 面板可停靠(左 / 右)
- 工具条新增 「⇤ 面板靠左 / ⇥ 面板靠右」:面板与页面内缩一起换边,钉在右侧的组件立刻回到视野里、也重新可点。
- 全流程只有一处状态:
<html data-dsh-myskin-dock="left|right">。帧样式表仍然在打开绘制模式时只写一次,
两个版式都写在里面,切换只是改一个属性——不重建样式表,也就没有"只换了一半"的中间态。
面板占用的宽度发布为--dsh-myskin-inset-x(由它指向--dsh-myskin-inset-left/right,编辑器把两侧都写成同一个实测值),
于是与侧别无关的规则(设置弹窗的高度与宽度上限)只读一个名字,只有body的水平 margin 写两遍。 - 跟着换边的还有:上游设置弹窗的收口规则(
:has([data-shortcut-modal])的left/right与max-width)、
Windows 标题栏分支的bodymargin、面板的分隔线与阴影方向、入场/收起的滑动方向、折叠按钮的箭头。 - 切换走
useLayoutEffect:属性与面板在同一次提交里翻面。若写成普通 effect,会有一帧"面板已经在左边、页面还空着右边"的
340px 跳变(挂载时则是面板短暂压住它本该让出的那一条)。 - 偏好记在浏览器
localStorage(键dsh-myskin.dock),不进皮肤文档:它描述的是这台机器上的插件、不是皮肤,
也天然绕开宿主 schema,所以本版不需要重启 DSH。坏值 / 隐私模式 / 存储被拒一律退回默认(右侧),读写全程 try/catch。
新增 · 被面板压住的东西,面板自己说
- 打开绘制模式后,编辑器每 1.5 秒(不是每帧)在面板区域内按 2×4 的网格做几次命中测试:
跳过本插件自己的层与html/body/#root,剩下的第一个元素就是"被面板压住的东西"。
工具条第 2 行随即出现⚠ 面板挡住了 <元素名> · ⇤ 面板靠左,点一下即换边。 - 判定与绘制模式的选择共用同一个
isOwnElement(本轮从组件内提到模块级,并改用ownerDocument):
悬停框、点击选中、层级按钮与遮挡扫描不可能各说各话。 - 纯函数(
occludingElement/panelSamplePoints/sameElements)与 DOM 包装分开:
没有命中测试能力的环境(jsdom、异形内嵌)返回"没被挡住"而不是抛错;扫描结果不变时保持同一个数组引用,
工具条不会每 1.5 秒重渲染一次。 - 这是诊断,不是裁决:它只报出"面板挡住了谁"和一个可逆的换边入口——不改任何元素、不猜 class 与 z-index、不往页面写东西。
新增 · 「全站」作用域(跨界面改同一个组件)
- 起因:部分插件的界面每个界面都出现一份——对话里在
#root内,插件页 / 设置页是 portal(挂在#root旁边)。
画布的选择器是结构化的(#root > div:nth-of-type(3) > …/body > div:nth-of-type(n) > …),一份只能覆盖一处;
而body > :nth-of-type(n)那条路还会因为多开一个弹窗而整体错位。整组对这种形状是故意拒绝的(散落各处的类不成组),
所以这类组件此前根本没有"改一次、到处生效"的办法。 - 面板「编辑范围」新增第三个档位 全站(
src/client/site-scope.ts):只用元素自身的标识写选择器——
生成的类名、共享的data-*、role——不带任何祖先路径、不带位置。于是:别的界面里的实例照样命中,
之后新建的实例自动生效,界面结构变化(开弹窗、换视图)也不会让它失效。 - 两条底线,都是"说出来"而不是"悄悄做":没有标识就说没有(只有标签名 → 按钮不可用并说明原因);
太泛就拒绝(命中超过SITE_SCOPE_MAX=64 处,说明它是布局/工具类而不是组件 —— 报出命中数,绝不写入)。 - 范围始终可见:按钮上写 当前命中 N 处,画布给命中的其余元素描虚线框(最多 24 个,其余由数字承担),面板照常显示"将写入"的完整选择器;
只命中 1 处时也会说明——「这是"这个组件",不是"只有这一个"」,其他界面的实例出现时照样匹配。 - 切换作用域时,已写在该元素上的声明跟着搬到新选择器(
moveRuleToBlock,一次快照可撤销),与「整组」同一条路。 - 它写的仍然是普通
css条目:没有 schema 字段、没有引擎分支,极客模式可见、可单独清除,删除即完全还原。 - 只在设置 / 插件页出现的元素怎么选:切「交互」把设置弹窗打开,再切回「选择」即可点选(弹窗遵守同一条内缩规则,落在应用区里)。
修复 · 类名识别不认本机正在用的 CSS-module 形态(潜在静默失效)
-
stableClasses此前只认_row_1abc2_34(0.1.x)与bhn1Oq_projectRow(哈希在前),而本机 DSH 0.2.0-rc.2 产出的是
pI_x6G_frame(两个哈希段)——两种形态都不匹配。结果是它退回"全部类名",把is-active这类状态类写进选择器,
于是"改一个、同类跟着变"的选择器在别的实例上永不匹配(用户只会看到"改了没反应")。 -
现在三种形态都认(
MODULE_CLASS/MODULE_CLASS_HASHED),并且状态类(is-*/has-*/active/selected/disabled…)
在两条分支上都被排除。整组与全站共用这一处判断。新增 · 「界面显示」:按界面决定这个组件显示 / 不显示
-
起因:部分插件的界面每个界面都在(对话、插件页、设置页…),而「全站一起改」在这个场景里是反的——用户要的是
「对话里留着、插件页里别出现」(或反过来)。 -
界面是推导出来的,不是写死的两个桶:卡片列出 ① 设置弹窗(上游
data-shortcut-modal:设置对话框 + 快捷键编辑器,
portal 到<body>)与 ② 当前页面(主区域槽位[data-slot="main"]里当前活动的那一页,用它自己的语义标记描述——
插件页的页面根是<section data-plugin-panel>,对话页是data-conversation-region一类的属性)。
规则 =body:has(<界面标记>) <组件标识> { display: none !important },标识来自「全站」(site-scope.ts)。
规则主语始终是这个组件自己,所以两种插件形态都管得住:每个界面各渲染一份的,和一个全局节点盖在所有界面上的。 -
卡片标出当前界面,并显示每一行的界面标记(鼠标停在「显示 / 不显示」上就能看到这条规则挂在什么上);
「当前页面」由遮挡扫描同一条 1.5 秒节拍刷新——换页是上游的状态,不会让编辑器重渲染。 -
写的仍然是普通
css条目,并且只在display上动手:同一选择器上别的声明原样保留,删空了整条消失;
开关只是「文档说了什么」的一个视图,不存在第二份状态。 -
常用预设:三个整状态按钮,省掉逐个界面点——
「只在本页显示」(写一条body:not(:has(<本页标记>))的反向规则:连还没打开过的页面也覆盖,
并且额外给"浮层类界面"(设置弹窗可以在本页仍挂载时盖在上面)补一条规则,否则开着设置时它还会露出来)、
「到处都不显示」(写在组件标识上的钝规则)、「全部恢复显示」。 -
访问过的页面自动记住(
localStorage键dsh-myskin.surfaces,最多 8 个):页面标记只能从真正挂载过的页面推导,
记住它就把「本页」变成一份固定清单——下次不用再切过去也能开关。存储值是可写的用户数据,读回时只接受本插件能产出的标记形状。 -
每一行的「显示 / 不显示」说的是真话(
hiddenWhile):它同时看本界面自己的规则、那条钝规则、以及别的界面的反向规则——
所以「只在本页显示」之后,设置那一行会如实显示为「不显示」,点它的「显示」就是"这里也要显示"(反向规则随之删掉)。 -
表达不了的就不表达:页面没有语义标记(只有构建期类哈希)时,「本页」这一行不出现并说明原因——
按类哈希或按导航序号写,会在 DSH 升级 / 别的插件改动之后悄悄隐藏错的东西。
修复 · 「界面显示」在插件页不生效(0.4.0 开发中的设计错误,据反馈)
- 现象:在设置里隐藏生效,插件页照旧显示。
- 根因:最初的实现把界面写成「设置弹窗开着 / 没开」两个桶。但上游的插件页不是设置弹窗的一页——它是主区域里的另一个面板
(PANEL_ID = "plugins"注册在main槽位,和对话并列),插件页在屏幕上时设置弹窗压根没挂载,
于是body:has([data-shortcut-modal])不成立,那条规则什么也管不到。 - 修法:每个页面用自己的语义标记表达(见上),各页互不干扰;设置弹窗仍然是独立的一行。
新增 · 「我把所有显示都关了」怎么后悔
-
隐藏规则就是普通
css条目,所以三条路都通:
① 卡片里的 「全部恢复显示」:一次删掉所有隐藏这个组件的规则——包括旧版本写下的(例如那条body:not(:has(...))),
整条空了就消失,同选择器上的其它声明原样保留;
② 每个界面都隐藏时就地警告:每个界面都隐藏了:画布上已经点不中它。用这个按钮恢复,或用面板底部的「回收站」。
③ 回收站永远在(它只列规则,不需要先选中元素):恢复一条 ≡ 把那个界面切回「显示」(有测试钉住两条路结果相同)。新增 · 层级(z-index)自定义
-
「位置与缩放」组里新增 层级(z-index) 字段:数值输入 + 滚轮微调(±1,Shift ±10)+ 独立 ↺ 清除,
右边一排常用值快捷键(0 / 10 / 100 / 1000)——层级值通常是整数档位,点一下比打字快。 -
z-index进INSPECTOR_PROPERTIES(受管属性):字段预览按属性合并时会接管它,清空字段就会把z-index从规则里删掉,
不会留下一条"清不掉的数字"跟别的规则吵架。 -
不生效要说出来:z-index 只对已定位元素(以及 flex/grid 子项)生效。选中
position: static的元素时,
字段下方直接写明这一点,并给一个 「设为 relative」 按钮(把position: relative !important合并进同一条规则,不动别的声明)。新增 · 层级为什么不生效:叠放诊断 + 「置顶 / 置底」
-
反馈:z-index 字段填了数字看不到变化。原因有三类,都不是「没写进去」,所以现在直接说出来(
src/client/stacking.ts):- 定位:z-index 只对已定位元素(及 flex/grid 子项)生效——元素是
static时字段下方写明,并给「设为 relative」; - 层叠上下文:祖先里有
transform/opacity < 1/ 它自己的 z-index… 就会形成一个天花板,里面的元素再大的 z-index 也出不去——
诊断会点名是哪个祖先、因为什么; - 没有重叠:层级只在元素互相重叠时才看得出来——没有竞争者时改数字本来就不该有变化,诊断会这么说。
- 定位:z-index 只对已定位元素(及 flex/grid 子项)生效——元素是
-
诊断同时报真实竞争者(与它重叠的兄弟/父级兄弟,排除本插件自己的 UI):每一个的 z-index 都列出来,
并给出两个一定有效果的数字:「置顶(max+1)」/「置底(min-1)」;元素是 static 时这两个按钮顺手补上position: relative
——否则「让它在别人上面」这个动作,正好会产生用户抱怨的那种「什么都没有发生」。 -
诊断在选中变化与每次草稿写入后重算(数字必须描述此刻的页面),邻居最多列 6 个(这是个面板,不是审计工具)。
修复 · 侧边栏收起再展开后,嵌入图消失(但有框)
-
现象:在「交互」模式下把侧边栏收起、再展开,嵌入到侧边栏的图片看不见了,选中框还在(据反馈)。
-
根因:绘制模式里图片是按标记画的(
[data-dsh-myskin-embed="<id>"]::after),而标记是打在真实节点上的属性——
收起/展开会重建那棵子树,标记随节点一起消失。引擎对已提交的皮肤有重试循环,但编辑器打开期间它故意跳过
(否则会把草稿里删掉的图复活),而编辑器自己只在「锚点变化」时重打标记:重建之后没人补标记,
规则匹配不到任何元素 → 图没了,容器本身还在 → 框还在。 -
修法:编辑器补上它欠的那次重试——rAF 合并的
MutationObserver(只监听childList/subtree;它自己只写属性,不会自激),
DOM 一变就按草稿重新解析锚点并重打标记。图片因此和引擎那条路一样,跟着组件走、跨重建。
修复 · 嵌入图盖住了容器自己的内容(工作区行 / 按钮)
-
现象:侧边栏(工作区)里的嵌入图盖在工作区行与按钮之上(据截图)。
-
根因:
::after用的是z-index: 1——正数画在内容之上,而它自己的文档注释写的却是「背景之上、内容之下」。 -
修法:宿主加
isolation: isolate(只为建立层叠上下文,不动 z-index、不改布局),::after改z-index: -1——
于是它落在容器背景之上、内容之下,行与按钮回到最上层,正是文档一直声称的行为。
引擎与编辑器预览用同一份声明(预览不能讲另一个故事),两点都由tests/engine.test.mjs钉住。修复 · 四种混合模式看起来一样(0.4.0 引入的回归)
-
现象:
normal / 正片叠底 / 滤色 / 叠加选哪个都差不多(上一版是正常的)。 -
根因:上一轮为了把嵌入图放到内容之下,给宿主加了
isolation: isolate。它会隔离元素的混合组:
mix-blend-mode只能和同一个组内、画在它下面的东西混——隔离之后那一层就只剩容器自己的背景(多数容器是透明的),
于是四种模式都在和「什么都没有」混。 -
修法:由混合模式决定这一层画在哪里——
normal→z-index: -1+ 宿主isolation: isolate:画在容器背景之上、内容之下,不挡工作区行与按钮;
任何真实混合模式 →z-index: 1且不加 isolation:画在内容之上,背面是整个页面,四种模式的区别回来了。
两者不可能同时成立(要和背后混,就必须画在内容之上),所以让混合模式来选,并在字段旁写明这件事。
变更 · 嵌入图片的设置独立成卡(不必先找到它嵌在哪儿)
-
以前这张卡在 Inspector 里,而 Inspector 只在选中了某个元素时才存在——想改一张图的不透明度或混合模式,
得先找到、还得能点中它嵌进去的那个容器;嵌在收起的侧边栏、不好点的行、甚至别的页面上的图,基本等于改不了。 -
现在面板里有一张独立的「嵌入图片」卡(编辑模式下始终在,没有选中也显示):每张图一块,带锚点、
✓/⚠(锚点此刻是否解析得到)、「选中容器」(把画布带到它身上,不用自己找)、锚点 / 呈现方式 / 不透明度 / 混合模式,
以及新增的位置与大小数值框(滚轮微调)——不拖动也能精确定位。 -
「用选中元素 / 取选中文字」保留,没有选中时置灰并说明。
变更 · 图片选中与组件选中同级(不再同时出现两套框 / 两套面板)
-
现象:拖动/点中一张嵌入图时,页面上同时有图片的虚线框和组件的选中框,面板里也同时有元素 Inspector 与图片卡——看起来是两套东西叠在一起。
-
根因:图片根本没有选中态。它的框只是画在页面上(
data-dsh-myskin-ui,z-index 10001),与元素选择各画各的;面板也各显示各的。 -
修法:图片成为一个真正的选中对象,与组件选择互斥:
selectedImage状态:点图片框即选中它(拖动仍然是拖动);- 选中图片时,元素的选中框、两个变换手柄、整组描边、悬停框全部让位——页面上只剩图片这一个框(选中的框是实线,未选中是虚线);
- 面板顶部换成图片头卡(名称 / ✓⚠ / 「选中容器」/「取消选择」),元素 Inspector 在这期间不显示;
- 「嵌入图片」卡里对应那一行高亮,并多一个「选中 / 取消选择」按钮——列表即索引,点一下就把画布切到那张图;
- Esc 的优先级与选择层级一致:先退图片、再退组件、再退编辑器;点页面任何其它元素也会退出图片编辑;
- 图片被删除/撤销掉时,选中态自动清除(否则元素 chrome 会为一个不存在的框一直让位,页面上一个选中框都没有)。
-
元素的选中没有丢,只是「退到后台」:它仍然是「用选中元素 / 取选中文字」的作用对象——所以锚定操作在编辑图片时照样可用。
变更 · 嵌入图片的位置/大小改成竖向,数值只到小数点后一位
-
两个输入并排会把面板撑出边界(DSH 的
Input自带最小宽度),改成一个轴一行:X/Y/宽/高,
每行一个输入(滚轮微调照旧)。 -
数值显示到小数点后一位,写进文档的也是这个值:
0.1px已经是这套编辑器的精度上限,
而拖动与滚轮会产出12.340000000000001这种噪声(round1/formatOne)。
新增 · 壁纸可选「跟随对话区」(侧边栏展开 / 收起时重新居中)
-
默认仍是整页壁纸:
body上background-attachment: fixed,以窗口为参照——侧边栏一收,对话区挪了、图没动。 -
勾上「壁纸跟随对话区」后,对话区那一列自己再画一份同样的图,锚定在它自己的框上(
background-attachment: scroll,不是fixed):
列一有新的框它就重新居中,不需要任何脚本(没有 resize 监听、没有 rAF)。 -
侧边栏仍是整页那一份,所以两边取景会不同——这是「只在对话区居中」的固有代价,也是它做成可选项的原因;面板里的说明就这么写。
-
存储用标记声明(
css里的--dsh-myskin-bg-anchor: conversation,默认 viewport = 不写)而不是新的canvas字段:
css从第一版起就在 schema 里,新字段则要等 Host 重启才存得住(旧 Host 会把不认识的字段丢掉)。 -
顺手把
:root上的标记改成合并写入(withRootMarker):强度、锚定、两个字体角色共用同一条:root,谁都不许整条替换,
并由测试钉住「文档里永远只有一条:root」。新增 · 嵌入动图(GIF 保留动画)
-
症结:图片一律走 canvas 重编码(超过 2048px 才触发),而 canvas 只有一帧——GIF 一旦被重编码,动画就没了。
-
从字节判断,不看扩展名(静止 GIF 重编码成 WebP 会小得多):
src/client/gif.ts按 GIF89a 的块结构走一遍
(不解码像素、全程边界检查),≥2 帧或有 NETSCAPE/ANIMEXTS 循环块即视为动图;截断文件如实回 false 而不抛错。 -
动图按原样嵌入:
image/gif的 data URL,不缩放、不重编码,动画完整。预算单独放宽到约 3 MB
(MAX_ANIMATED_URL_LENGTH):在「文档更大」与「丢掉动起来这件事」之间,动图选前者。
超过预算才退回静止图,并在状态行明说「只保留第一帧(动画丢失)」——静默丢动画看起来就像 bug。 -
壁纸同理(同一个读取函数):动图壁纸也保留动画。
-
两条附带优化:
- 非 GIF 只读文件头 16 字节再决定要不要整份读入——20 MB 的照片不再被读两遍;
- 新嵌入的图片按自身比例放进 320×240 的框(不再一律拉成 320×200——动图被拉扁尤其难看),且绝不放大小图。
-
.dshskin的 MIME↔扩展名映射本来就认image/gif,现在有测试钉住:导出→导入不会把动图变成杂项文件。变更 · 每张图片一个面板(不再共用一个)
-
之前所有图片的设置挤在同一张卡里:每张图的字段把别人的字段一路往下推,谁也看不出「我正在改的是哪一张」。
-
现在每张图 = 它自己的面板(
.dsh-myskin-card,标题行可折叠):标题行是「解析到的宿主 + ✓/⚠ + 选中/取消选择 + 移除」,
展开才是它自己的设置(锚点 / 呈现方式 / 不透明度 / 混合模式 / 位置与大小...
[0.3.9]
新增 · 回收站(移除的组件可恢复)
- 「移除控件(不占位)」写的是
display: none——元素不再参与命中测试,画布上再也点不中它:一旦保存,这条路就是单行道。
绘制模式右侧面板底部现在有「回收站」,列出当前皮肤移除掉的每一个组件,可单项恢复 / 全部恢复。
它只存在于绘制模式——回收站是"把东西拿回来"的动作,属于正在改皮肤的那只手,不该出现在只读的皮肤管理页上。 - 列表从皮肤文档的
css派生(removedControls),不是第二份状态:无论移除是怎么进去的、隔了多久、刷新多少次,它都在,
也就不存在"列表与实际不符"的可能。元素仍在 DOM 里(display: none只是不渲染),所以每条能给出一眼认得出的名字,而不是冷冰冰的选择器。 - 恢复只删掉那条
display(withControlRestored):元素上其它自定义原样保留;移除是它唯一自定义时,整条规则一起消失,不留空壳。 - 恢复立刻可见:只改草稿是不够的——已保存的那份皮肤是另一张同选择器的样式表,它仍然
display: none !important。
新增naturalDisplayOf():把两张皮肤样式表同步摘出文档(中间不可能发生绘制)量出元素本来该用的display,再按原位置放回;
编辑器把量到的值写进自己的预览层,直到下次保存把移除彻底带走。撤销、重新移除、保存、应用、还原都会自动清掉这个临时覆盖。
修复 · 组件位移抽搐(新坐标与旧坐标来回切换)
三个真实成因,逐个拆掉:
- 拖动时丢声明:手势过去用
withManagedDeclarations合并规则,而它会丢掉全部受管属性、只把transform加回来——
被拖的元素每帧丢一次宽高/内边距/字号,面板预览又每帧补回来,肉眼就是抽搐。新增transformEdit():只替换transform一个属性,其余(含手写极客规则)逐字节保留。 - 两个写者互相覆盖:
liveTransform/liveApply都以渲染闭包里的draft为基准,而一次手势跨好几帧——旧基准会把别人刚写的改动悄悄回滚。两者改为setDraft(prev => …),始终基于 React 待提交的最新状态。 - 草稿样式表排在已提交皮肤前面:
applySkin每次接受设置变更都新建并追加一张样式表,而选择器相同的规则按文档顺序决胜——
于是「保存」之后,已提交的旧坐标开始压过草稿的新坐标,一拖就弹回去。编辑器现在用 MutationObserver 保证自己的草稿样式表始终在<head>最后(退出编辑器即恢复原状,什么都不留下)。 - 面板不再回声:X/Y/缩放输入框在画布手势期间只是只读镜像(天生慢一帧),过去却被当成事实来源写回规则。新增
transformPreview()把"谁是写者"变成显式参数:
只有用户真在面板里改过(输入 / 滚轮 / 滑块 / ↺ 重置),字段才是来源;否则原样重发规则里那条transform——既不回声,也不会把已有的位移弄丢。这条有专门的回归测试。
新增 · 本机字体列表
- 新增
src/client/fonts.ts与「本机字体」按钮:读取这台机器上真正安装的字体,列成可搜索列表,每个名字用它自己的字体渲染,点一下应用到当前元素。 - 主路径是 Chromium 的 Local Font Access API(
queryLocalFonts,需要一次用户手势 + 授权);拿不到就退化为候选字体探测(canvas 文本度量,逐个判断候选是否真的存在,候选覆盖 CJK / 无衬线 / 衬线 / 等宽)。 - 「这份列表是哪来的」写在列表顶部(本机 / 探测 / 授权被拒),不把两种来源混为一谈;
scanFonts()永不抛错,环境再差也只是列表短一点。 - 本机字体不进皮肤文档:列表是本机事实,皮肤里存的仍然只是
font-family字符串。
新增 · 嵌入图片的锚定(跟随 文字 / 内置组件 / 元素)
- 过去一张嵌入图只能跟着"当初嵌入时选中的那个容器"(一条结构选择器),页面一改版就可能跑到别的节点上或干脆消失。
现在每张图都有自己的 锚点(EmbeddedImage.anchor),面板上可随时改:- 元素:结构选择器(原来的行为,也是旧文档的默认);
- 文字:跟着这段文案——按钮换了措辞、React 重建了节点,只要那行字还在,图就还在它身上("选中那行字 → 取选中文字"一步完成);
- 内置组件:从目录里挑固定部位(整个应用 / 应用外框 / 对话主列 / 会话内容区 / 输入区 / 输入框 / 新对话占位文字 / 工作区会话树),不用自己去页面上找。
- 解析顺序是 锚点 → 冻结的结构选择器 → 上次命中的节点/指纹:锚点暂时找不到时(比如那页还没挂载)图片不会乱贴,而是保持不动,
由共享的 MutationObserver 在 DOM 变化后重试——这正是"锚点"和"一次性选择器"的区别。 - 目录(
src/client/anchors.ts)只收本包已经在别处依赖、且被 check:compat 盯住的标记(data-composer-*/data-slot/role="tree"/#root/ CSS-module 名):
一个跨 DSH 版本会漂移的部位,写进目录只会让图片静默消失。 - 编辑器里边改边生效:打开绘制模式时会按草稿里的锚点重新标记真实页面(换锚点=旧元素摘标记、新元素打标记),不必先保存。
- 面板会显示每张图当前是否锚得上:
✓ 已锚定 · <名字>/⚠ 当前页面上找不到这个锚点,图片不会显示——把"图没了"变成一句能看懂的话。
修复 · 嵌入图的锚点标记没有随皮肤一起撤掉(0.3.9 之前就有)
- 引擎只断开 MutationObserver,却把打在真实节点上的
data-dsh-myskin-embed="<id>"留在了页面上:关掉皮肤后属性还在,
下次 apply 还要跟一个过期 id 抢位置。现在 dispose 会精确摘掉本次 apply 自己的那些标记(只按本皮肤的图片 id 摘,不误伤)。 - 这正是锚定功能的必经路径,所以随本次一起修;
tests/anchors.test.mjs的往返用例断言 dispose 后属性为 null。
变更 · 「关闭」= 直接关闭并舍弃未保存的改动
- ✕ 关闭(以及第二次 Esc)过去会先保存再退出:一个半成品草稿会被悄悄写进皮肤文档,用户以为"我只是关掉看看"。
现在两条出口语义干净地分开:保存 / 应用 才会写入,✕ / Esc 一律"离开,什么都不改"。 - 舍弃必须是完整的回退,所以编辑器卸载时会把嵌入标记交还给已提交文档:草稿里被移动/删除/换锚点的图片标记逐一收回,
已提交文档里的图片按它自己的锚点重新标记——✕ 之后页面回到"文档说什么就是什么"。 - 配套:编辑器打开期间由编辑器独占嵌入标记(引擎的重标记循环会跳过)。页面 DOM 动得很频繁,引擎若按已提交文档重标记,
一秒后就会把"草稿里刚删掉的那张图"复活回来。
新增 · 嵌入图片分化为「组件嵌入」与「组件锚定」
-
每张图多了一个 呈现方式(
EmbeddedImage.mode,缺省embed= 旧文档行为):- 组件嵌入:画在组件内部(锚点元素上的
::after,在背景之上、内容之下),随组件一起被裁剪——就是 0.3.8 的行为; - 组件锚定:画在组件之外的皮肤自有图层上(
position: fixed覆盖层,pointer-events: none,z-index 900:盖得住应用表面、盖不住上游对话框),
可以负偏移露到组件外面,且完全不动宿主元素的position/overflow——不做"改宿主样式"这种侵入式操作。
- 组件嵌入:画在组件内部(锚点元素上的
-
锚定层的矩形在 scroll(capture,任何滚动容器)/ resize / DOM 变更时重算,rAF 合并为每帧一次,且只在值真的变了才写样式;
锚点解析不到(那一页没挂载、pageKey 不匹配)时节点display: none——宁可暂时不显示,也不会贴错到别的节点上。 -
编辑器用同一个实现预览(
mountImageOverlay),所以拖出来的位置就是保存后的位置;组件嵌入仍走 CSS + 标记那条路。 -
键位说明:工具栏按钮仍叫「嵌入图片」= 默认组件嵌入;要"露在外面"在右侧面板把呈现方式切成组件锚定。
修复 · 本机字体列表「一个都看不到」(0.3.9 引入的回归,据用户截图复现)
-
现象:面板显示「本机字体 · 224」,下方却是能滚动但空无一字的列表,还多出一条横向滚动条。
-
定位(对截图做像素级扫描:搜索框到滚动条之间没有任何非背景像素,但容器带纵向滚动条):
行是<button className="dsh-myskin-fontpick">,位于display:flex; flexDirection:column的容器里——
flex 子项一旦带overflow: hidden,其自动最小尺寸就是 0,224 行因此被压到只剩 padding 的 ~8px;
而行为省略号而设的overflow: hidden又把 20px 行盒整行裁掉:行还在、还能滚,字一个不剩。
横向滚动条则来自width: 100%+ padding/border 的 content-box 溢出。 -
修法三层,互不依赖:①行
flex: none+min-height: 26px+box-sizing: border-box;
②列表容器改为定高块级滚动区(display: block; flex: none; height: 156px),行不再是 flex 子项,收缩这条路直接不存在;
③.dsh-myskin-card与 Inspector 根节点flex: none——卡片与编辑列是内容而不是橡皮筋,滚动交给面板。 -
顺带:搜索时显示「命中 / 总数」,列表状态一眼可见。
-
回归测试:
tests/canvas-ui.test.mjs断言行规则必须flex: none|flex-shrink: 0+min-height+box-sizing: border-box,卡片必须flex: none。
变更 · 字体分化为「界面 / 正文 / 代码」三个作用域
-
原来只有「应用到整页」一条路——它写一条
body { font-family: … !important },会把代码块一起抢走(等宽脸被正文脸顶掉),
也无法只改界面或只改正文。现在拆成三个独立作用域,各自写进皮肤文档的普通css条目(无新 schema、无新引擎路径,可单独读/改/清除):作用域 通道 为什么 界面 :root { --dsw-font-family: … }DSH 自己的全局变量, body与大量font: <size>/<lh> var(--dsw-font-family)简写都读它正文 对话内容/markdown/输入框选择器上的 font-family正文没有变量(继承 + markdown 逐块),只能显式覆盖;不写 !important,否则段落里的行内 code 也会被从代码脸里拽出来代码 :root { --ds-font-family-code: …(+ --dsw-font-markdown-code-font-family) }pre、行内 code、代码编辑器都读这个变量;只改变量、不碰元素规则 -
两个
:root角色按属性合并进同一条目(背景强度标记也在里面),不会互相覆盖;清除某一项时只删自己的属性,条目空了才消失。 -
「本机字体」列表变成一个控件四个目标:当前选中元素 + 三个作用域;点哪一行的「本机」就写进哪一项,列表高亮当前生效的那一行。
写入作用域时会附带同类兜底字体(界面/正文 →system-ui, sans-serif,代码 →monospace)——作用域是要跟随皮肤包换机器的设置,解析不到时也应该落到同一种脸,而不是浏览器的默认衬线体。 -
旧的
body { font-family }规则不受影响:它仍是普通 css 条目,照常生效(想改由作用域接管,清掉那条即可)。 -
兼容性守卫:
check:compat新增契约「font roles resolve」——--dsw-font-family/--ds-font-family-code必须仍被该代 DSH 读取,
否则界面/代码字体会看起来应用了却毫无效果(这种失败没有任何报错)。
优化 · 预设主题(可读性 + 配色重做,极夜 → 粉黛)
- 问题已量化(把 DSH 主题包里的令牌表解析出来,对每个预设×每个模式做全量模拟):
- 主按钮文字对比度不足:
button-primary-fill在 DSH 里就等于brand-primary,它的文字是label-primary-foreground——深海/暗色只有 2.84:1、暖阳/浅色 3.56:1(AA 要 4.5);
浅色下label-caption(2.66)与label-dimmed(1.84)也几乎贴住背景。 - 极夜在浅色模式下漏出 12 个亮面 + 4 个暗字:它把浅色列也写成深色(“始终深色”),却没有覆盖 DSH 从浅色调色板解析出来的那批面——
文档预览#ebeef2、对话气泡#edf3fe、文件 diff 增删背景、菜单面板、ghost/toolbar 填充、侧栏选中强调色,以及
label-primary-dimmed/label-primary-bluish/menu-icon这些字色——亮面配亮字,正是“看不清”。
- 主按钮文字对比度不足:
- 修法:
- 每一档文字都按它真正落在其上的面重算:正文/次级 ≥ 4.5、三级 ≥ 3、caption ≥ 2.6、dimmed ≥ 2.2(DSH 自带的 dimmed 只有 1.07–1.9,我们至少不更差);品牌色在主面上 ≥ 3;主按钮/对比按钮的文字与填充 ≥ 4.5。
- 配色重做:三套各自建立单调的层次梯度(浅色=浅底白卡;深色=越往上越亮,与 DSH 自身模型一致)+ 同色系中性色 + 用品牌色着色交互态,而不是“灰底 + 一个跳色”。
另外新增brandTinted():DSH 把用户自己的消息气泡、气泡高亮、侧栏选中强调、主色标签都按它自家蓝色推导——不覆盖就会在暖阳/粉黛里突兀地留一块浅蓝,现在三套各自按自己的色相着色。 - 预设按需求调整:移除极夜,换成粉黛(
rose:日间玫瑰纸面 + 夜间深梅子)。
移除原因不是配色,而是它的“强制深色”模型:要在浅色模式下也保持深色,就必须额外覆盖 16 个 DSH 只在浅色模式下才会解析的令牌(清单见 SKILL 4.12 第 3 条),
漏一个就是一块白底 + 白字;三套预设现在一律跟随应用的明暗模式,没有这个出错面。
已保存的旧「极夜」皮肤不受影响:皮肤文档存的是令牌值本身而不是预设 id(界面把它显示为「自定义皮肤」),颜色照旧。
- 守卫:新增
tests/preset-contrast.test.mjs三例——①对比度底线(纯计算,不依赖安装);②每个预设令牌必须真的存在于该代 DSH
(本次就抓出一个写错的--dsw-alias-menu-surface-fill,正确名是--dsw-menu-surface-fill:写错不会报错,只会静默无效);
③预设不得留下另一套模式的亮面/暗字(对着 DSH 的浅/深两套默认值做模拟,允许列表只放“故意反色”的 toast/tooltip/骨架/反色标签)——
这条现在就守着“将来若再加强制模式的预设”这个坑。
另新增tests/helpers/dsh-tokens.mjs:从安装的 DSH 主题包解析出完整令牌表(含var()链解析),供测试复用。
新增 · 组块编辑(改一个,同类一起变)
- 在此之前所有编辑都是单元素的:画布生成的是结构选择器(
#root > div > … > button:nth-child(3)),所以改了「工作区行」只有那一个变,
其他工作区与之后新建的工作区都保持原样。 - 新增
src/client/groups.ts与面板顶部的「编辑范围:仅此元素 / 整组」。整组时写入的是同类元素的公共选择器,因此天然覆盖将来的实例;
它仍是普通css条目(无 schema、无引擎改动),在极客模式里可见、可单独清除。 - 工作区 vs 对话按 ARIA 语义区分(不靠 class 哈希):侧栏树是
[role="tree"],工作区行 =[role="treeitem"][aria-expanded],
对话行 =[role="treeitem"]:not([aria-expanded])——正是引擎的树装饰器与check:compat已经在依赖的那套契约。 - 会话行嵌在工作区行内部,所以组选择器用「行 + 相对路径」而不是后代选择器:
[role="treeitem"][aria-expanded] > button.icon_hash[aria-label="…"]。否则一个后代选择器会把对话行的按钮也吞进工作区组——
这正是需求里要求区分的那件事,有专门测试钉住。 - 行内的图标按钮常常共用同一个组件类(截图里的「…」和「+」就是),因此用可访问名(
aria-label/title)把它们分开;
若该值是按实例变化的,候选会退化回类选择器,并把真实命中数量显示出来(不假装精确)。 - 树以外的情况:只有当同类元素重复在同一个父类之下(每一行、每一张卡片)时才算一组,散落在无关父节点之间的一律拒绝——
给一个"悄悄改掉半个界面"的选择器还不如不给。 - 命中数 ≤ 1 时返回"无组"(「整组」按钮置灰),不会伪造一个只有一个成员的分组;文字替换保持单元素,面板会明确提示。
- 画布拖动同样跟随编辑范围:整组时拖一下,整块一起移动,与在 X 字段里改数字等价。
- 修正(按真实 DOM 核对后):读 DSH 的
dsh-client-ui-workspace源码确认工作区行确实是
<div role="treeitem" aria-expanded={row.expanded} data-row-key="workspace:…">,判别方向正确;但初版加了一条“命中数必须 > 1 才算一组”的保险,
于是当前只有一个工作区时「整组」直接置灰——恰好违背“只编辑一个、同步到之后新建的”这条需求。现在识别到的种类(工作区行/对话行/侧栏树)即使只有一个成员也照样成组;
只有“同类元素”这种靠重复推断出来的分组才要求 ≥ 2。 - 类名识别补齐 0.2.x 的 CSS-module 形态(
bhn1Oq_projectRow,哈希在前),此前只认 0.1.x 的_row_1abc2_34。 - 让范围看得见:整组时画布上给其余成员逐个描虚线框,选中标签追加「整组 N」——改之前就能看见这次编辑会覆盖谁。
- 修正 · 保存失败变得可诊断:
保存失败:canvas只报了字段名,两种完全不同的原因看起来一模一样。
新增src/client/save-report.ts:失败信息现在会带上画布数据大小 + 图片数 + 整组锚点数,并给出可执行的指引——
「宿主 schema 比客户端旧(装了新版但没重启 DSH)→ 可先把该图片锚点改回「元素」保存,或重启 DSH」/
「画布约 N KB(图片以 data URL 内嵌):换更小的图」。
另加一条宿主 schema 测试钉住anchor.kind = "group"在源码 schema 下确实能存住——
于是"运行中的宿主拒了它"就等价于"运行中的宿主是旧的",文案的判断才有依据。 - 新增 · 整组间隔:「编辑范围」下方多出一个间隔字段(只在该块存在时出现):数字即像素,滚轮 ±1(Shift ±10),
也接受0.5rem/-4px这样的写法,↺ 清除。写入的是组选择器 + 组选择器 { margin-top: … !important }——
只动成员彼此之间,第一个成员与容器顶的距离不变,之后新建的成员自动纳入;这也正是 DSH 自己的分行方式
([role=treeitem]+[role=treeitem] { margin-top: 2px }/groupSection > * + *),皮肤只覆盖距离,不与宿主争布局。
成员若不是相邻兄弟(例如分属不同工作区的对话行),"彼此之间"没有定义:字段不出现并说明原因,而不是给一个点了没反应的控件。
间隔是普通css条目——无 schema 改动、无需重启,极客模式可见、可单独清除;一次编辑只留一个撤销步。 - 修正 · 「整组开了但别的行没变」(用户实测反馈)——两个原因叠加:
- 切换「整组」本身不会写规则(面板只在字段变化时才写),已改好的样式仍留在单元素规则里。
现在切换时会把该元素的声明迁移到组规则、并删掉单元素规则(一次撤销...
- 切换「整组」本身不会写规则(面板只在字段变化时才写),已改好的样式仍留在单元素规则里。
[v0.3.8]
修复 · 新对话页那行灰色默认文字点不中(连带「文字改不动」)
- 空 composer 的占位文字是上游的
<div data-composer-placeholder>,CSS 为
position:absolute; inset:4px 8px auto 14px; pointer-events:none。elementsFromPoint()不返回不接受
指针事件的元素,所以点击那行灰字拿到的是它背后空的[contenteditable](没有任何文字节点),
「编辑文字」因此永远报「没有可直接编辑的文字」。 - 选择器改为引擎里的纯函数
pickElementAt():自有 UI 穿透 → 最近交互祖先优先 → 指针不可见的文字浮层
优先于背后容器(先认[data-composer-placeholder]标记,再退回「pointer-events:none+ 直接文字 +
位于指针下」的通用判定;浏览器里display:none的候选因无几何被拒,无布局的 jsdom 跳过覆盖面判定,
因此该逻辑可单测)。选中后面板标注「灰色默认文字」,改色/隐藏/改文案都作用在它自己身上。
修复 · 编辑文字不能靠猜
- 文字以前只由「编辑文字」按钮写入,而输入框的标签与按钮同名——在输入框里打字页面上毫无反馈,读起来就是
「无法修改文字」。现在边输入边预览(250ms 去抖,沿用样式字段「一次手势一条历史」的规则,不会每个
按键发一条 undo),回车或点「应用文字」写入草稿;改回原文即自动撤销该条覆盖。 - 去抖写入在撤回/重选/卸载时取消,并在写入前核对 host 仍是当初那个元素。
绘制模式 · 选择与信息
- 悬停虚线框 + 标签胶囊(
tag#id.class · 文字片段):与点击共用同一个pickElementAt(),
预选与实际选中不可能不一致;选中框改实线 + 同款胶囊,落点有一次性动效。 - 面板顶部 ↑ 父级 / ↓ 子级(Alt+↑/↓):按真实 DOM 一次一级(
parentTarget/childTargetIn),
点错一级往上退即可。 - 工具条分组:选择/交互改分段控件、撤销/重做带快捷键提示、保存状态胶囊(灰/琥珀/红,悬停显示失败
字段)、提示文案移入独立第二行(超长省略号)不再挤按钮;右侧面板可折叠(折叠后真实页面自动铺满,
内缩由 ResizeObserver 复算);「还原默认」二次确认(它紧挨「应用」,误点会丢整个草稿)。
绘制模式 · 字段与编辑
- 16 个样式字段拆成 文字 / 盒子 / 外观 / 位置与缩放 / 元素操作 可折叠分组(标题带「自定义 N」计数);
中文标签全部走 i18n(英文界面不再半中半英);每个生效字段可 × 单独清除。 - 预览只重写自己拥有的属性(引擎
withManagedDeclarations+INSPECTOR_PROPERTIES):以前整条规则
替换,导致"隐藏控件"后一改字号元素又冒出来;现在visibility/display与极客模式手写的声明都保留。 - 没变就不写(引擎
sameDeclarations,忽略顺序与!important):选中带规则的元素不再把草稿标成
"有未保存的更改",也不再往撤销栈塞空步。极客模式仍是"整条替换"(用户自己写了整条规则)。 - 快捷键:Esc(先取消选择,再按退出并保存)、Ctrl/Cmd+Z、Ctrl/Cmd+Shift+Z;输入框/可编辑区与上游弹窗优先。
绘制模式 · 字体
- 「字体」字段写
font-family(受管属性):可直填任意家族栈/系统字体,或用建议列表(PingFang SC /
Microsoft YaHei / Noto Sans CJK SC / HarmonyOS Sans / JetBrains Mono …)。 - 嵌入字体文件:
.woff2/.woff/.ttf/.otf,按扩展名判定format(),以css里
selector='@font-face'的条目存入(引擎的selector { rule }渲染天然成立,没有新增 schema 字段;
删除该条目即卸载字体)。 - 上限 400 KB → 30 MB:允许整份 CJK 字体,但 >2 MB 明确提示代价(文档里是 base64、每次编辑整份重发、
保存会变慢;导出的.dshskin里它是原样文件)。 - 应用到整页:把当前字体写成一条
body规则(body无法在画布上点选,所以单给一个入口)。
绘制模式 · 位置与缩放
- 新分组「位置与缩放」:X / Y 位移 + 等比缩放(带滑块)。值由
transformValue()生成——恒等分量省略、
全恒等返回''即删除transform(不留transform: none);parseTransform()反向解析供面板回显
(二者互逆,有单测)。只做视觉位移/缩放,不改变布局流。 - 画布手势:选中框左上角 ✥ 拖动=双轴移动、右下角手柄拖动=等比缩放;拖拽 rAF 合并为每帧一次
草稿写入、一次手势只压一条撤销,面板数字跟随(正在输入时不覆盖)。 - 滚轮微调:X/Y ±1px(Shift ±10px)、缩放 ±0.05(Shift ±0.25,夹 0.2–3)。监听器必须原生
{ passive: false }——React 的onWheel挂在 root 上是 passive 的,preventDefault()被忽略、
面板会在改数值的同时跟着滚;单测直接断言defaultPrevented === true(attachWheelNudge)。 - 每个轴独立重置:X / Y / 缩放各一个
↺(无值时禁用),另有「重置变换(三轴)」;步进/取整/夹取统一走
引擎stepValue()。
绘制模式 · 对齐线(低敏吸附)
- 移动 / 缩放时把元素的三条边(起、中、末)与同级元素、父级容器、视口中线比对,4px 内吸附并画出对齐线;
磁力刻意做小(SNAP_THRESHOLD = 4)——10px 的磁铁没法精确定位,1px 又在触控板上看不见。 - 几何全部是引擎里的纯函数:
snapAxis()(单轴最近候选)、snapMove()(双轴平移修正 + 要画的线)、
snapScale()(绕中心等比缩放时让某条边落到线上,取像素误差最小的候选);snapTargetsFor()每个手势只采集一次
——元素是用transform移动的,不会 reflow,目标线在拖动期间不可能动。三个函数都有单测(含阈值边界、
最近候选、中心另一侧的线不算候选、退化盒不产生 NaN/Infinity)。 - 交互:工具条「对齐」总开关(默认开),拖动时按住 Alt 临时关闭;对齐线是 1–2 个 fixed 的 1px div,
淡入 90ms、reduced-motion 下不淡入,拖拽结束即清除。
绘制模式 · 保存 / 应用 / 关闭
- 保存 = 写入皮肤文档并留在绘制模式(不改
enabled,不会把故意停用的皮肤偷偷打开);
应用 = 写入 +enabled: true并退出绘制模式;✕ 关闭 有未保存改动时先保存再退出。
在此之前只有"写入即退出"两条路,无法"先存下再继续调",这是本次拆分的直接动因。
绘制模式 · 动效与性能
- 新增
src/client/canvas-ui.ts(编辑器自有样式表,随编辑器挂载/卸载,可逆):工具条入场、面板滑入、
悬停/选中框落点、分组手风琴(只转箭头)、保存点(仅写入中呼吸)、提示行淡入;卡片表面、胶囊、分隔线、
细滚动条、滑杆与折叠标记的原生表面风格、空状态卡片、:focus-visible焦点环;颜色只用--dsw-alias-*。 - 性能与动效同等重要:①动效只动
transform/opacity(CSS-only,无 JS 逐帧回调);
②禁止backdrop-filter(给实时页面做模糊最贵);③面板折叠不动画宽度(宽度经 ResizeObserver
驱动整页内缩,动画宽度=每帧重排整个应用);④滚动/缩放重绘 rAF 合并为每帧一次(原来每个 scroll
事件都重渲染编辑器);⑤悬停命中测试每帧最多一次、仅 edit 模式装载;⑥prefers-reduced-motion全关。
新测试把这些不变量钉死(关键帧只允许 opacity/transform、禁backdrop-filter、禁transition: all、
必须有 reduced-motion 分支、颜色不得写死十六进制)。
皮肤包 · .dshskin(类 zip 容器)
- 「导出皮肤」改产
.dshskin:标准 ZIP(STORE 写入,7-Zip/unzip/Pythonzipfile都能打开)。
manifest.json= 完整配置(format/formatVersion/generator/name/createdAt/assets/
stats+skin全量文档);assets/<kind>-<n>.<ext>= 原样字节(壁纸 / 嵌入图 / 嵌入字体);
README.txt= 包内说明。文档里的引用写成dshskin:assets/…,只在包内存在——运行时文档永远是
真 data URL,因此没有 schema 变更、没有新的应用路径。 - 收益(实测):一份主题 351 KB JSON → 141 KB 包(−59.8%),壁纸按原字节还原;同一图片被引用 3 次只存
1 份;解开 zip 换掉assets/里的同名文件即可再导入。 - 读写策略:只写 STORE,读兼容 DEFLATE(普通 zip 工具重打包过的包照样导入);逐条目 CRC 校验;
缺资源点名报错而不是导入半个皮肤;旧版.json导出继续支持;包名与下载名取自「保存为皮肤」的命名输入。 - 零依赖:ZIP 写/读、CRC-32、data URL 编解码全部手写在
src/client/dshskin.ts;toArrayBuffer()供
Blob使用(子数组的buffer会带上切片外的字节)。产物另用 Pythonzipfile交叉验证过是合法 zip。
[v0.3.7]
修复 · 桌面壁纸的圆角切割与三处同一不透明度
- Windows 下壁纸改画在对话列上(
[class*="_centerCol"],染色作为 gradient 压在壁纸之上):列自己的
border-radius+overflow:hidden把壁纸切在 16px 圆角内;frame 不再画图,保留 DSH 自己的
--dsw-specific-sidebar-fill去填那 16px 缺口——既不再露原生窗口底色(0.3.2 的黑块),
也不会把圆角糊掉(0.3.6 的副作用)。 - 列内只保留唯一画布层:
--dsw-alias-bg-base在列内被置为transparent,只有内容槽
([data-slot="conversation.session"]/[data-slot^="conversation.view"])与 composer 座位里的卡片
把 token 拿回去;composerSeat 自身background: none,去掉发送栏多出来的那层渐变。 - 依据是实测反解:一张截图 + 壁纸原图,逐像素解
observed = α·surface + (1-α)·wallpaper
(映射按 cover+fixed+center 拟合,残差 0.2/255)。修前同一个「背景强度」下:顶栏 0.94、
对话区 0.75、发送栏 ≈1.00;修后三处是同一个 α,滑块语义变直白(0.5 = 壁纸透出 50%)。
修复 · 绘制模式下打开设置,弹窗被工具条/面板遮挡
- 上游设置界面是 portal 到
body的整层全视口浮层(position:fixed;inset:0,z-index 1000),
而绘制画布层是 9999,弹窗因此被压在工具条与右侧面板之下。 - 修法:让弹窗遵守和主界面同一条内缩规则——模态浮层按
--dsh-myskin-inset-top/right
(Windows 再叠加--dsh-myskin-chrome-top)内缩,并按同一组内缩收口面板尺寸,弹窗落在应用区里居中。 - 曾试过"模态一开就隐藏画布层",被否:用户仍在绘制,工具条与面板正是要用的东西。上游
useModalLayer只圈 Tab 与 Escape、不锁鼠标,所以内缩后两边都能用。
修复 · 侧栏收起/展开后嵌入图消失
- 嵌入图靠瞬态属性
[data-dsh-myskin-embed]定位,React 重建节点会把它抹掉。补打标现在:
多匹配时取可见者(不再"不唯一就放弃",侧栏收放时 rail 与 panel 可能同时挂着);节点被重建时按
祖先链指纹(TAG.class逐级到body,刻意不含兄弟序号)在文档里重找,且只认唯一命中;
观察器同时盯class/hidden(收起可能只换布局 class、不卸载节点)。 - 结构回退选择器故意不直接进样式表:位置型路径在兄弟位移后会命中"长得像"的节点,等于把图打到
别人身上;身份判定交给引擎,样式表只画瞬态标记。