Skip to content

Releases: WTStarMark/dsh-myskin

dsh-myskin 0.4.1

Choose a tag to compare

@WTStarMark WTStarMark released this 06 Oct 09:42

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]

Choose a tag to compare

@WTStarMark WTStarMark released this 02 Oct 03:53

新增 · 面板可停靠(左 / 右)

  • 工具条新增 「⇤ 面板靠左 / ⇥ 面板靠右」:面板与页面内缩一起换边,钉在右侧的组件立刻回到视野里、也重新可点。
  • 全流程只有一处状态:<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 标题栏分支的 body margin、面板的分隔线与阴影方向、入场/收起的滑动方向、折叠按钮的箭头。
  • 切换走 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):

    1. 定位:z-index 只对已定位元素(及 flex/grid 子项)生效——元素是 static 时字段下方写明,并给「设为 relative」;
    2. 层叠上下文:祖先里有 transform / opacity < 1 / 它自己的 z-index… 就会形成一个天花板,里面的元素再大的 z-index 也出不去——
      诊断会点名是哪个祖先、因为什么;
    3. 没有重叠:层级只在元素互相重叠时才看得出来——没有竞争者时改数字本来就不该有变化,诊断会这么说。
  • 诊断同时报真实竞争者(与它重叠的兄弟/父级兄弟,排除本插件自己的 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,标题行可折叠):标题行是「解析到的宿主 + ✓/⚠ + 选中/取消选择 + 移除」,
    展开才是它自己的设置(锚点 / 呈现方式 / 不透明度 / 混合模式 / 位置与大小...

Read more

[0.3.9]

Choose a tag to compare

@WTStarMark WTStarMark released this 30 Sep 13:00

新增 · 回收站(移除的组件可恢复)

  • 「移除控件(不占位)」写的是 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 这些字色——亮面配亮字,正是“看不清”。
  • 修法:
    1. 每一档文字都按它真正落在其上的面重算:正文/次级 ≥ 4.5、三级 ≥ 3、caption ≥ 2.6、dimmed ≥ 2.2(DSH 自带的 dimmed 只有 1.07–1.9,我们至少不更差);品牌色在主面上 ≥ 3;主按钮/对比按钮的文字与填充 ≥ 4.5。
    2. 配色重做:三套各自建立单调的层次梯度(浅色=浅底白卡;深色=越往上越亮,与 DSH 自身模型一致)+ 同色系中性色 + 用品牌色着色交互态,而不是“灰底 + 一个跳色”。
      另外新增 brandTinted():DSH 把用户自己的消息气泡、气泡高亮、侧栏选中强调、主色标签都按它自家蓝色推导——不覆盖就会在暖阳/粉黛里突兀地留一块浅蓝,现在三套各自按自己的色相着色。
    3. 预设按需求调整:移除极夜,换成粉黛(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 改动、无需重启,极客模式可见、可单独清除;一次编辑只留一个撤销步。
  • 修正 · 「整组开了但别的行没变」(用户实测反馈)——两个原因叠加:
    1. 切换「整组」本身不会写规则(面板只在字段变化时才写),已改好的样式仍留在单元素规则里。
      现在切换时会把该元素的声明迁移到组规则、并删掉单元素规则(一次撤销...
Read more

[v0.3.8]

Choose a tag to compare

@WTStarMark WTStarMark released this 29 Sep 14:43

修复 · 新对话页那行灰色默认文字点不中(连带「文字改不动」)

  • 空 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/Python zipfile 都能打开)。
    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 会带上切片外的字节)。产物另用 Python zipfile 交叉验证过是合法 zip。

[v0.3.7]

Choose a tag to compare

@WTStarMark WTStarMark released this 29 Sep 05:51

修复 · 桌面壁纸的圆角切割与三处同一不透明度

  • 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、不卸载节点)。
  • 结构回退选择器故意不直接进样式表:位置型路径在兄弟位移后会命中"长得像"的节点,等于把图打到
    别人身上;身份判定交给引擎,样式表只画瞬态标记。