Merge remote-tracking branch 'origin/pr/5' into codex/pr5-rework
This commit is contained in:
72
CHANGELOG.md
Normal file
72
CHANGELOG.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 滑动变祖 · 社区优化版 0.2.0
|
||||
|
||||
基于上游 [kingOfSoySauce/dsh-liang-skin](https://github.com/kingOfSoySauce/dsh-liang-skin) v0.1.5 优化,
|
||||
已针对 DeepSeek Harness `0.1.0-rc.6` 验证。本包为**本地优化构建**,未推送上游仓库。
|
||||
|
||||
## 优化清单
|
||||
|
||||
1. **图像过渡更自然(交叉淡化)**
|
||||
- 原版:切换推理等级时直接硬换人像 `src`,跳变生硬。
|
||||
- 现版:双图层交叉淡化(备用层先加载、加载完成才淡入,不出现白闪),并叠加轻微
|
||||
缩放(1.018→1)缓动。拖动中快速过渡(120ms,跟手),松手落定时慢速过渡
|
||||
(340ms ease-out);`prefers-reduced-motion` 用户自动降级为直切。
|
||||
|
||||
2. **滑块位置固定,切换等级不再移位**
|
||||
- 根源:原生「模型 · 等级」座位宽度随等级文本变化,在右锚定 flex 行里把滑块挤动。
|
||||
- 修复:① 原生座位按钮固定 180px 足迹(模型名省略号吸收变化);② 滑块自身
|
||||
`flex: 0 0 124px` 固定尺寸;③ 交互/提交期间 rAF 逐帧检测漂移并用 `translateX`
|
||||
补偿,把滑块钉在屏幕上,结束后 200ms 平滑回位。
|
||||
- 注意:固定宽度的选择器必须限定为 `button[class*="_7KE1Ra_trigger"]`——宿主的
|
||||
`_7KE1Ra_triggerLabel`/`_7KE1Ra_triggerEffort` 子元素类名包含同样子串,宽选择器
|
||||
会误伤并把等级文本挤出按钮。
|
||||
|
||||
3. **原生推理等级与滑块实时同步(新增)**
|
||||
- 拖动预览期间把最近档位乐观回写共享 directory store(`SnapshotStore.update`),
|
||||
原生座位实时跟随滑块档位变化;松手才经官方 `ModelDirectory.select` 提交,
|
||||
失败 / Esc / 取消时自动回滚到交互前快照。
|
||||
|
||||
4. **滑动更丝滑**
|
||||
- 移除图片序列模式下每帧对隐藏 `<video>` 的无谓 seek(原版最大卡顿源),
|
||||
仅视频回退模式保留 seek;24 张人像锚点全量预加载 + `decoding="async"`;
|
||||
交叉淡化走 opacity/transform 合成器动画(`will-change`),不触发重排。
|
||||
|
||||
5. **滑块自定义光标**
|
||||
- 使用 `.cur` 标准光标(type=cursor、热点 16,16 居中),挂载于滑块交互区,
|
||||
加载失败自动回退 `ew-resize`。源素材为 32×32 的 `.ico`,已在打包时一并附带。
|
||||
|
||||
6. **修复:模型名与推理等级拆开显示(回归修复)**
|
||||
- 问题:固定原生座位宽度时子串选择器误伤子元素,等级文本被挤出按钮、孤立显示。
|
||||
- 修复:选择器限定按钮本身,「DeepSeek-V4-Pro · 高」恢复一体显示。
|
||||
|
||||
## 与上游 v0.1.5 的差异
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `src/client/index.tsx` | 双图层 crossfade 呈现器、seek 条件化、预览乐观同步 + 回滚、位置锁定 |
|
||||
| `src/client/skin.css` | crossfade 过渡、滑块固定尺寸、按钮级固定宽度、自定义光标 |
|
||||
| `src/index.js` | 静态路由新增 `liang-cursor.cur` / `liang-cursor.ico` |
|
||||
| `assets/liang-cursor.cur` / `.ico` | 新增光标资产 |
|
||||
| `lib/client.js` / `.map` | 重新构建的客户端产物 |
|
||||
|
||||
## 安装(tarball)
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add ./dsh-client-liang-intensity-skin-0.2.0.tgz
|
||||
```
|
||||
|
||||
安装后重启 DSH Web 生效;入口仍为「设置 → 通用设置 → 外观皮肤 → 滑动变祖」,
|
||||
「滑动变祖绑定思考等级」开关控制滑块是否联动推理等级。
|
||||
|
||||
## 卸载 / 回退
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web remove dsh-client-liang-intensity-skin
|
||||
```
|
||||
|
||||
## 构建与测试
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm test # 8/8 通过(vitest,沙箱环境需 --pool=threads)
|
||||
npm run build # esbuild 产出 lib/client.js
|
||||
```
|
||||
BIN
assets/liang-cursor.cur
Normal file
BIN
assets/liang-cursor.cur
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/liang-cursor.ico
Normal file
BIN
assets/liang-cursor.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1523
lib/client.js
1523
lib/client.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "dsh-client-liang-intensity-skin",
|
||||
"version": "0.1.5",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "滑动变祖:an opt-in adaptive reasoning slider skin for DeepSeek Harness",
|
||||
"description": "滑动变祖(社区优化版):an opt-in adaptive reasoning slider skin for DeepSeek Harness",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/kingOfSoySauce/dsh-liang-skin.git"
|
||||
@@ -17,6 +17,8 @@
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"assets/liang-cursor.cur",
|
||||
"assets/liang-cursor.ico",
|
||||
"assets/liang-evolution.mp4",
|
||||
"assets/liang-evolution.webm",
|
||||
"assets/liang-poster.png",
|
||||
@@ -44,6 +46,7 @@
|
||||
"assets/portrait-source-v2/level-28.png",
|
||||
"assets/portrait-source-v2/level-29.png",
|
||||
"assets/portrait-source-v2/stage-30.png",
|
||||
"CHANGELOG.md",
|
||||
"cordis.patch.yml",
|
||||
"lib/client.js",
|
||||
"lib/client.js.map",
|
||||
|
||||
@@ -54,6 +54,8 @@ const PORTRAIT_ANCHORS = [
|
||||
{ level: 30, file: "stage-30.png" },
|
||||
] as const;
|
||||
|
||||
const ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level);
|
||||
|
||||
interface SkinSettings {
|
||||
enabled: boolean;
|
||||
bindEffort: boolean;
|
||||
@@ -141,13 +143,16 @@ class SkinPresenter {
|
||||
private readonly root: HTMLDivElement;
|
||||
private readonly video: HTMLVideoElement;
|
||||
private readonly poster: HTMLImageElement;
|
||||
private readonly portrait: HTMLImageElement;
|
||||
private readonly frames: HTMLImageElement[];
|
||||
private readonly preloads: HTMLImageElement[];
|
||||
private enabled = true;
|
||||
// Default to the max frame so the first paint after load is the dark shell;
|
||||
// starting at 0 flashed the light palette before the directory resolved.
|
||||
private frame = PREVIEW_MAX_FRAME;
|
||||
private pendingFrame = PREVIEW_MAX_FRAME;
|
||||
private pendingMode: "drag" | "settle" = "settle";
|
||||
private activeFrame = 0;
|
||||
private pendingSource: string | null = null;
|
||||
private raf = 0;
|
||||
private disposed = false;
|
||||
private unsubscribe: () => void;
|
||||
@@ -159,6 +164,7 @@ class SkinPresenter {
|
||||
this.root.className = "liang-skin-backdrop";
|
||||
this.root.dataset.plugin = PACKAGE_ID;
|
||||
this.root.dataset.media = "sequence";
|
||||
this.root.dataset.crossfade = "slow";
|
||||
this.root.setAttribute("aria-hidden", "true");
|
||||
|
||||
this.video = document.createElement("video");
|
||||
@@ -167,7 +173,7 @@ class SkinPresenter {
|
||||
this.video.preload = "auto";
|
||||
// A video poster stays painted while a paused video is scrubbed in some
|
||||
// browsers, which would make every effort show the same portrait. Keep the
|
||||
// separate image element as the true load-error fallback instead.
|
||||
// separate image elements as the true load-error fallback instead.
|
||||
const webm = document.createElement("source");
|
||||
webm.src = `${ASSET_PREFIX}/liang-evolution.webm`;
|
||||
webm.type = "video/webm";
|
||||
@@ -181,19 +187,29 @@ class SkinPresenter {
|
||||
this.poster.src = `${ASSET_PREFIX}/liang-poster.png`;
|
||||
this.poster.alt = "";
|
||||
|
||||
this.portrait = document.createElement("img");
|
||||
this.portrait.className = "liang-skin-sequence-frame";
|
||||
this.portrait.alt = "";
|
||||
this.portrait.draggable = false;
|
||||
this.portrait.addEventListener("error", this.handleSequenceError);
|
||||
// Two stacked crossfade layers: the standby layer loads the next portrait
|
||||
// while the active one stays painted, then an opacity/scale transition
|
||||
// hands over — no hard src swap, no white flash.
|
||||
this.frames = [];
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
const image = document.createElement("img");
|
||||
image.className = "liang-skin-sequence-frame";
|
||||
image.alt = "";
|
||||
image.draggable = false;
|
||||
image.decoding = "async";
|
||||
image.addEventListener("error", this.handleSequenceError);
|
||||
image.addEventListener("load", () => this.handleFrameLoaded(image));
|
||||
this.frames.push(image);
|
||||
}
|
||||
|
||||
this.preloads = PORTRAIT_ANCHORS.map(({ file }) => {
|
||||
const image = new Image();
|
||||
image.decoding = "async";
|
||||
image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`;
|
||||
return image;
|
||||
});
|
||||
|
||||
this.root.append(this.video, this.portrait, this.poster);
|
||||
this.root.append(this.video, this.frames[0], this.frames[1], this.poster);
|
||||
document.body.prepend(this.root);
|
||||
|
||||
this.video.addEventListener("loadedmetadata", this.handleMetadata);
|
||||
@@ -246,9 +262,10 @@ class SkinPresenter {
|
||||
}
|
||||
}
|
||||
|
||||
setFrame(frame: number) {
|
||||
setFrame(frame: number, mode: "drag" | "settle" = "drag") {
|
||||
this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame)));
|
||||
this.frame = this.pendingFrame;
|
||||
this.pendingMode = mode;
|
||||
if (!this.enabled) return;
|
||||
if (this.raf !== 0) return;
|
||||
this.raf = requestAnimationFrame(() => {
|
||||
@@ -278,8 +295,11 @@ class SkinPresenter {
|
||||
body.style.setProperty("--liang-accent-hover", palette.accentHover);
|
||||
body.style.setProperty("--liang-hover", palette.hover);
|
||||
body.style.setProperty("--liang-portrait-opacity", palette.portraitOpacity);
|
||||
this.root.dataset.crossfade = this.pendingMode === "settle" ? "slow" : "fast";
|
||||
this.updatePortrait(palette.level);
|
||||
this.seek(this.frame);
|
||||
// Seeking a hidden <video> is pure cost (and visibly janky) during drags
|
||||
// in the default image-sequence mode; only the video fallback needs it.
|
||||
if (this.root.dataset.media === "video") this.seek(this.frame);
|
||||
}
|
||||
|
||||
syncNativeTheme(theme: NativeThemeId = paletteForFrame(this.frame).stage === 5 ? "dark" : "light") {
|
||||
@@ -288,15 +308,47 @@ class SkinPresenter {
|
||||
}
|
||||
|
||||
private updatePortrait(level: number) {
|
||||
if (this.root.dataset.media !== "sequence") return;
|
||||
const { lowerIndex, upperIndex, mix } = portraitBlendForLevel(
|
||||
level,
|
||||
PORTRAIT_ANCHORS.map((anchor) => anchor.level),
|
||||
ANCHOR_LEVELS,
|
||||
);
|
||||
const lower = PORTRAIT_ANCHORS[lowerIndex];
|
||||
const upper = PORTRAIT_ANCHORS[upperIndex];
|
||||
const selected = mix >= 0.5 ? upper : lower;
|
||||
const source = `${ASSET_PREFIX}/portrait-source-v2/${selected.file}`;
|
||||
if (this.portrait.getAttribute("src") !== source) this.portrait.src = source;
|
||||
const active = this.frames[this.activeFrame];
|
||||
if (active.getAttribute("src") === source) {
|
||||
// Already showing this anchor; drop any stale queued handover.
|
||||
this.pendingSource = null;
|
||||
return;
|
||||
}
|
||||
if (this.pendingSource === source) return;
|
||||
const standbyIndex = 1 - this.activeFrame;
|
||||
const standby = this.frames[standbyIndex];
|
||||
this.pendingSource = source;
|
||||
if (standby.getAttribute("src") !== source) standby.src = source;
|
||||
// Cache hit (preloaded): hand over immediately — the CSS transition still
|
||||
// eases the swap, so a fast drag stays glued to the pointer.
|
||||
if (standby.complete && standby.naturalWidth > 0) {
|
||||
this.activate(standbyIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleFrameLoaded = (image: HTMLImageElement) => {
|
||||
if (
|
||||
this.pendingSource === null
|
||||
|| image.getAttribute("src") !== this.pendingSource
|
||||
) return;
|
||||
this.activate(this.frames.indexOf(image));
|
||||
};
|
||||
|
||||
private activate(index: number) {
|
||||
if (index < 0 || index === this.activeFrame) return;
|
||||
this.frames[this.activeFrame].classList.remove("is-active");
|
||||
this.activeFrame = index;
|
||||
this.frames[index].classList.add("is-active");
|
||||
this.pendingSource = null;
|
||||
}
|
||||
|
||||
private seek(frame: number) {
|
||||
@@ -324,7 +376,10 @@ class SkinPresenter {
|
||||
this.video.pause();
|
||||
this.video.removeEventListener("loadedmetadata", this.handleMetadata);
|
||||
this.video.removeEventListener("error", this.handleVideoError);
|
||||
this.portrait.removeEventListener("error", this.handleSequenceError);
|
||||
for (const image of this.frames) {
|
||||
image.removeEventListener("error", this.handleSequenceError);
|
||||
image.src = "";
|
||||
}
|
||||
this.poster.removeEventListener("error", this.handlePosterError);
|
||||
for (const image of this.preloads) image.src = "";
|
||||
this.root.remove();
|
||||
@@ -376,6 +431,13 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
const [interacting, setInteracting] = useState(false);
|
||||
const dragging = useRef(false);
|
||||
const dragStartFrame = useRef(frame);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const interactionArmed = useRef(false);
|
||||
const dragStartCommittedId = useRef<string | undefined>(undefined);
|
||||
const dragStartCurrent = useRef<ModelSelection | null>(null);
|
||||
const previewSyncedId = useRef<string | undefined>(undefined);
|
||||
const lockRaf = useRef(0);
|
||||
const lockBaseLeft = useRef<number | null>(null);
|
||||
const enabled = skin.enabled;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -385,9 +447,44 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
useEffect(() => {
|
||||
if (!bindEffort || dragging.current || pending) return;
|
||||
setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame, "settle");
|
||||
}, [bindEffort, committedFrame, pending, presenter]);
|
||||
|
||||
// While the user interacts (or a commit is in flight), the native model
|
||||
// seat to the right may change its label width and reflow this flex row.
|
||||
// Pin the control's on-screen position each frame and compensate any drift
|
||||
// with a transform, then glide back smoothly when the interaction ends.
|
||||
const locked = interacting || pending;
|
||||
useEffect(() => {
|
||||
if (!locked) return;
|
||||
const root = rootRef.current;
|
||||
if (root === null) return;
|
||||
root.style.transition = "";
|
||||
root.style.willChange = "transform";
|
||||
lockBaseLeft.current = root.getBoundingClientRect().left;
|
||||
const step = () => {
|
||||
lockRaf.current = requestAnimationFrame(step);
|
||||
const base = lockBaseLeft.current;
|
||||
if (base === null) return;
|
||||
const drift = base - root.getBoundingClientRect().left;
|
||||
if (Math.abs(drift) > 0.5) {
|
||||
root.style.transform = `translateX(${drift.toFixed(2)}px)`;
|
||||
}
|
||||
};
|
||||
lockRaf.current = requestAnimationFrame(step);
|
||||
return () => {
|
||||
cancelAnimationFrame(lockRaf.current);
|
||||
lockRaf.current = 0;
|
||||
root.style.transition = "transform 200ms ease-out";
|
||||
root.style.transform = "";
|
||||
window.setTimeout(() => {
|
||||
root.style.transition = "";
|
||||
root.style.willChange = "";
|
||||
}, 220);
|
||||
lockBaseLeft.current = null;
|
||||
};
|
||||
}, [locked]);
|
||||
|
||||
if (!enabled) return null;
|
||||
if (bindEffort && (reasoning === null || efforts.length < 2)) return null;
|
||||
|
||||
@@ -396,21 +493,62 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
const progressRatio = frame / PREVIEW_MAX_FRAME;
|
||||
const tooltipLabel = indicatorLabel(frame, bindEffort ? efforts : []);
|
||||
|
||||
const commit = async (rawFrame: number) => {
|
||||
if (!bindEffort) {
|
||||
const beginInteraction = () => {
|
||||
if (interactionArmed.current) return;
|
||||
interactionArmed.current = true;
|
||||
dragging.current = true;
|
||||
dragStartFrame.current = frame;
|
||||
const snapshot = directory.getSnapshot();
|
||||
dragStartCurrent.current = snapshot.current;
|
||||
const snapshotReasoning = modelReasoning(snapshot);
|
||||
dragStartCommittedId.current = snapshotReasoning?.selection.reasoningEffort
|
||||
?? snapshotReasoning?.defaultEffort;
|
||||
previewSyncedId.current = undefined;
|
||||
};
|
||||
|
||||
// Roll the shared directory store back to the value captured before this
|
||||
// interaction (undoes any optimistic preview echo).
|
||||
const restorePreviewSync = () => {
|
||||
const original = dragStartCurrent.current;
|
||||
if (original === null || previewSyncedId.current === undefined) return;
|
||||
directory.store.update((draft) => {
|
||||
draft.current = original;
|
||||
});
|
||||
previewSyncedId.current = undefined;
|
||||
};
|
||||
|
||||
const cancelInteraction = () => {
|
||||
setInteracting(false);
|
||||
interactionArmed.current = false;
|
||||
dragging.current = false;
|
||||
setFrame(dragStartFrame.current);
|
||||
presenter.setFrame(dragStartFrame.current, "settle");
|
||||
restorePreviewSync();
|
||||
};
|
||||
|
||||
const commit = async (rawFrame: number) => {
|
||||
interactionArmed.current = false;
|
||||
dragging.current = false;
|
||||
if (!bindEffort) {
|
||||
setFrame(rawFrame);
|
||||
presenter.setFrame(rawFrame);
|
||||
presenter.setFrame(rawFrame, "settle");
|
||||
return;
|
||||
}
|
||||
const targetIndex = nearestEffortIndex(rawFrame, efforts);
|
||||
const target = efforts[targetIndex];
|
||||
if (target === undefined) return;
|
||||
if (target === undefined) {
|
||||
restorePreviewSync();
|
||||
return;
|
||||
}
|
||||
const targetFrame = frameForEffort(targetIndex, efforts.length);
|
||||
dragging.current = false;
|
||||
setFrame(targetFrame);
|
||||
presenter.setFrame(targetFrame);
|
||||
if (targetIndex === committedIndex || pending) return;
|
||||
presenter.setFrame(targetFrame, "settle");
|
||||
if (target.id === dragStartCommittedId.current) {
|
||||
// No real change: just undo any optimistic preview echo.
|
||||
restorePreviewSync();
|
||||
return;
|
||||
}
|
||||
if (pending) return;
|
||||
setPending(true);
|
||||
setFailed(false);
|
||||
const accepted = await select({
|
||||
@@ -421,13 +559,25 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
setPending(false);
|
||||
if (!accepted) {
|
||||
setFailed(true);
|
||||
setFrame(committedFrame);
|
||||
presenter.setFrame(committedFrame);
|
||||
restorePreviewSync();
|
||||
const rollbackIndex = selectedEffortIndex(
|
||||
efforts,
|
||||
dragStartCommittedId.current,
|
||||
undefined,
|
||||
);
|
||||
const rollbackFrame = rollbackIndex < 0
|
||||
? PREVIEW_MAX_FRAME
|
||||
: frameForEffort(rollbackIndex, efforts.length);
|
||||
setFrame(rollbackFrame);
|
||||
presenter.setFrame(rollbackFrame, "settle");
|
||||
} else {
|
||||
previewSyncedId.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className="liang-effort-control"
|
||||
data-plugin={PACKAGE_ID}
|
||||
data-state={failed ? "error" : pending ? "pending" : "ready"}
|
||||
@@ -461,25 +611,38 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
aria-label={bindEffort ? "思考等级" : "皮肤进度"}
|
||||
aria-valuetext={bindEffort ? previewEffort?.name ?? "" : tooltipLabel}
|
||||
onPointerDown={() => {
|
||||
dragging.current = true;
|
||||
dragStartFrame.current = frame;
|
||||
beginInteraction();
|
||||
setInteracting(true);
|
||||
}}
|
||||
onInput={(event) => {
|
||||
dragging.current = true;
|
||||
const next = Number(event.currentTarget.value);
|
||||
setFrame(next);
|
||||
presenter.setFrame(next);
|
||||
presenter.setFrame(next, "drag");
|
||||
if (!bindEffort || reasoning === null) return;
|
||||
// Optimistically echo the nearest preview effort into the shared
|
||||
// directory store so the native model seat updates in lockstep
|
||||
// while dragging; commit/rollback settles the final value.
|
||||
const index = nearestEffortIndex(next, efforts);
|
||||
const effort = efforts[index];
|
||||
if (effort === undefined || effort.id === previewSyncedId.current) return;
|
||||
previewSyncedId.current = effort.id;
|
||||
directory.store.update((draft) => {
|
||||
const current = draft.current;
|
||||
if (
|
||||
current === null
|
||||
|| current.provider !== reasoning.selection.provider
|
||||
|| current.model !== reasoning.selection.model
|
||||
) return;
|
||||
current.reasoningEffort = effort.id;
|
||||
});
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
setInteracting(false);
|
||||
void commit(Number(event.currentTarget.value));
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
setInteracting(false);
|
||||
dragging.current = false;
|
||||
setFrame(dragStartFrame.current);
|
||||
presenter.setFrame(dragStartFrame.current);
|
||||
cancelInteraction();
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
setInteracting(false);
|
||||
@@ -491,11 +654,9 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
setInteracting(true);
|
||||
beginInteraction();
|
||||
if (event.key === "Escape" && !pending) {
|
||||
setInteracting(false);
|
||||
dragging.current = false;
|
||||
setFrame(dragStartFrame.current);
|
||||
presenter.setFrame(dragStartFrame.current);
|
||||
cancelInteraction();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -41,7 +41,24 @@ body[data-liang-skin="on"] .liang-skin-backdrop {
|
||||
|
||||
.liang-skin-backdrop[data-media="sequence"] .liang-skin-sequence-frame {
|
||||
display: block;
|
||||
/* Crossfade layers: inactive sits transparent and slightly enlarged, the
|
||||
active one eases back to a calm 1:1. Dragging swaps fast so the preview
|
||||
stays glued to the pointer; settling swaps slow for a gentle landing. */
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scale(1.018);
|
||||
transition:
|
||||
opacity 120ms ease-out,
|
||||
transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.liang-skin-backdrop[data-media="sequence"] .liang-skin-sequence-frame.is-active {
|
||||
opacity: var(--liang-portrait-opacity, 0.82);
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
.liang-skin-backdrop[data-crossfade="slow"] .liang-skin-sequence-frame {
|
||||
transition-duration: 340ms, 520ms;
|
||||
}
|
||||
|
||||
.liang-skin-backdrop[data-media="video"] .liang-skin-sequence-frame,
|
||||
@@ -337,7 +354,14 @@ body[data-liang-skin="on"]:is(
|
||||
--liang-control-rail: var(--dsw-alias-border-l3, rgb(0 0 0 / 16%));
|
||||
position: relative;
|
||||
display: flex;
|
||||
/* Fixed footprint: the native model seat to the right changes its label
|
||||
width when the reasoning effort changes, which reflows the trailing flex
|
||||
row. Pin this control's own size so it never contributes to that shift
|
||||
(absolute position is additionally locked during interaction in JS). */
|
||||
flex: 0 0 124px;
|
||||
width: 124px;
|
||||
min-width: 124px;
|
||||
max-width: 124px;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
margin: 0 3px;
|
||||
@@ -349,6 +373,19 @@ body[data-liang-skin="on"] .liang-effort-control {
|
||||
--liang-control-rail: color-mix(in srgb, var(--liang-secondary) 28%, transparent);
|
||||
}
|
||||
|
||||
/* The native model seat (composer input.model) widens with the effort label
|
||||
text; give it a stable footprint under the Liang skin so switching effort
|
||||
never reflows the slider out of position. The label ellipsizes instead.
|
||||
Button-scoped on purpose: the inner label/effort spans also contain this
|
||||
class substring and must never be sized (sizing them pushed the effort text
|
||||
out of the button, splitting the model+effort pair apart). */
|
||||
body[data-liang-skin="on"] button[class*="_7KE1Ra_trigger"] {
|
||||
box-sizing: border-box;
|
||||
width: 180px;
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.liang-effort-control__ticks {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
@@ -415,7 +452,9 @@ body[data-liang-skin="on"] .liang-effort-control {
|
||||
width: calc(100% - 7px);
|
||||
height: 28px;
|
||||
margin: 0 3.5px;
|
||||
cursor: ew-resize;
|
||||
/* Branded pointer (centered hotspot baked into the .cur); fall back to the
|
||||
native resize cursor if the asset fails to load. */
|
||||
cursor: url("/plugins/dsh-client-liang-intensity-skin/assets/liang-cursor.cur") 16 16, ew-resize;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
touch-action: pan-y;
|
||||
@@ -620,7 +659,10 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.liang-effort-control {
|
||||
flex-basis: 92px;
|
||||
width: 92px;
|
||||
min-width: 92px;
|
||||
max-width: 92px;
|
||||
}
|
||||
|
||||
.liang-skin-backdrop video,
|
||||
@@ -636,6 +678,7 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
|
||||
.liang-skin-backdrop,
|
||||
.liang-skin-backdrop video,
|
||||
.liang-skin-backdrop img,
|
||||
.liang-skin-backdrop .liang-skin-sequence-frame,
|
||||
body[data-liang-skin="on"] button,
|
||||
body[data-liang-skin="on"] input,
|
||||
body[data-liang-skin="on"] textarea {
|
||||
|
||||
@@ -9,6 +9,8 @@ export const SETTINGS_NAMESPACE = "liang-intensity-skin";
|
||||
export const ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets/`;
|
||||
|
||||
const ASSET_SPECS = [
|
||||
["liang-cursor.cur", "image/x-icon"],
|
||||
["liang-cursor.ico", "image/x-icon"],
|
||||
["liang-evolution.webm", "video/webm"],
|
||||
["liang-evolution.mp4", "video/mp4"],
|
||||
["liang-poster.png", "image/png"],
|
||||
|
||||
Reference in New Issue
Block a user