Skip to content

Commit 628afe2

Browse files
Merge pull request #38 from starsstreaming/fix/issue-37-sidebar-mount
fix(dsh): 修复侧栏背景与设置图标交替闪烁
2 parents 5d42fd8 + e29d741 commit 628afe2

4 files changed

Lines changed: 282 additions & 5 deletions

File tree

integrations/deepseek-harness/console.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,15 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop{background:#f3f0e9;color:
128128

129129
function place() {
130130
const settings = findSettingsTrigger();
131-
if (!settings || !settings.parentElement) {
131+
const row = settings?.parentElement;
132+
const settingsArea = row?.parentElement;
133+
const footArea = settingsArea?.parentElement;
134+
if (!settings || !settingsArea || !footArea) {
132135
if (host.parentElement) host.remove();
133136
return;
134137
}
135-
if (host.parentElement !== settings.parentElement || host.nextElementSibling !== settings) {
136-
settings.parentElement.insertBefore(host, settings);
138+
if (host.parentElement !== footArea || host.nextElementSibling !== settingsArea) {
139+
footArea.insertBefore(host, settingsArea);
137140
}
138141
host.classList.toggle("rail", settings.getBoundingClientRect().width <= 40);
139142
if (!pop.hidden) placePop();

integrations/deepseek-harness/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "beauticode-dsh",
3-
"version": "1.0.22",
3+
"version": "1.0.23",
44
"description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.",
55
"type": "module",
66
"main": "./index.mjs",
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs/promises";
3+
import test from "node:test";
4+
import vm from "node:vm";
5+
6+
/**
7+
* Minimal DOM for console.js placement. Mirrors DSH 0.1.2-alpha.5:
8+
* footArea > settingsArea > triggerRow (horizontal flex) > settings button.
9+
* Putting #beauticode-console inside triggerRow squeezes the settings button
10+
* to zero width, which is Issue #37's flicker loop.
11+
*/
12+
class FakeNode {
13+
constructor(tagName, document) {
14+
this.tagName = String(tagName).toUpperCase();
15+
this.document = document;
16+
this.id = "";
17+
this.className = "";
18+
this.parentElement = null;
19+
this.children = [];
20+
this.attributes = new Map();
21+
this.dataset = {};
22+
this.style = {};
23+
this.hidden = false;
24+
this._innerHTML = "";
25+
this.textContent = "";
26+
this.listeners = new Map();
27+
this.classList = {
28+
toggle: (name, force) => {
29+
const names = new Set(this.className.split(/\s+/).filter(Boolean));
30+
const on = force === undefined ? !names.has(name) : Boolean(force);
31+
if (on) names.add(name);
32+
else names.delete(name);
33+
this.className = [...names].join(" ");
34+
return on;
35+
},
36+
};
37+
}
38+
39+
get nextElementSibling() {
40+
if (!this.parentElement) return null;
41+
const siblings = this.parentElement.children;
42+
return siblings[siblings.indexOf(this) + 1] ?? null;
43+
}
44+
45+
setAttribute(name, value) {
46+
this.attributes.set(name, String(value));
47+
if (name === "id") this.id = String(value);
48+
if (name === "class") this.className = String(value);
49+
if (name.startsWith("data-")) {
50+
const key = name
51+
.slice(5)
52+
.replace(/-([a-z])/g, (_all, ch) => ch.toUpperCase());
53+
this.dataset[key] = String(value);
54+
}
55+
}
56+
57+
getAttribute(name) {
58+
if (name === "id") return this.id || null;
59+
if (name === "class") return this.className || null;
60+
return this.attributes.get(name) ?? null;
61+
}
62+
63+
hasAttribute(name) {
64+
return this.getAttribute(name) != null;
65+
}
66+
67+
set innerHTML(html) {
68+
this._innerHTML = String(html);
69+
for (const child of [...this.children]) child.remove();
70+
const re = /<([a-z0-9]+)([^>]*)>/gi;
71+
let match;
72+
while ((match = re.exec(this._innerHTML))) {
73+
const tag = match[1].toLowerCase();
74+
if (["svg", "path", "rect", "circle", "strong", "small", "h2"].includes(tag)) {
75+
continue;
76+
}
77+
const attrs = match[2];
78+
const classMatch = attrs.match(/class="([^"]*)"/);
79+
if (tag === "span" && !classMatch) continue;
80+
const child = this.document.createElement(tag);
81+
if (classMatch) child.className = classMatch[1];
82+
for (const attr of attrs.matchAll(/([a-z0-9:-]+)="([^"]*)"/gi)) {
83+
if (attr[1] === "class") continue;
84+
child.setAttribute(attr[1], attr[2]);
85+
}
86+
this.append(child);
87+
}
88+
}
89+
90+
get innerHTML() {
91+
return this._innerHTML;
92+
}
93+
94+
append(...nodes) {
95+
for (const node of nodes) {
96+
node.remove();
97+
node.parentElement = this;
98+
this.children.push(node);
99+
}
100+
}
101+
102+
insertBefore(node, ref) {
103+
node.remove();
104+
node.parentElement = this;
105+
const index = ref ? this.children.indexOf(ref) : -1;
106+
if (index >= 0) this.children.splice(index, 0, node);
107+
else this.children.push(node);
108+
return node;
109+
}
110+
111+
remove() {
112+
if (!this.parentElement) return;
113+
const siblings = this.parentElement.children;
114+
const index = siblings.indexOf(this);
115+
if (index >= 0) siblings.splice(index, 1);
116+
this.parentElement = null;
117+
}
118+
119+
addEventListener(name, handler) {
120+
const list = this.listeners.get(name) ?? [];
121+
list.push(handler);
122+
this.listeners.set(name, list);
123+
}
124+
125+
contains(node) {
126+
for (let current = node; current; current = current.parentElement) {
127+
if (current === this) return true;
128+
}
129+
return false;
130+
}
131+
132+
matches(selector) {
133+
const attr = selector.match(/^(\w+)?\[([^=\]]+)=["']([^"']+)["']\]$/);
134+
if (attr) {
135+
const [, tag, name, value] = attr;
136+
if (tag && this.tagName !== tag.toUpperCase()) return false;
137+
return this.getAttribute(name) === value;
138+
}
139+
if (selector.startsWith(".")) {
140+
return this.className.split(/\s+/).includes(selector.slice(1));
141+
}
142+
if (selector.startsWith("#")) return this.id === selector.slice(1);
143+
return this.tagName === selector.toUpperCase();
144+
}
145+
146+
querySelectorAll(selector) {
147+
const matches = [];
148+
for (const child of this.children) {
149+
if (child.matches(selector)) matches.push(child);
150+
matches.push(...child.querySelectorAll(selector));
151+
}
152+
return matches;
153+
}
154+
155+
querySelector(selector) {
156+
return this.querySelectorAll(selector)[0] ?? null;
157+
}
158+
159+
getBoundingClientRect() {
160+
if (this.getAttribute("aria-haspopup") !== "dialog") {
161+
return { width: 36, height: 36, left: 8, top: 724, bottom: 760 };
162+
}
163+
const squeezed = this.parentElement?.children.some(
164+
(child) => child.id === "beauticode-console",
165+
);
166+
if (squeezed) {
167+
return { width: 0, height: 0, left: 8, top: 0, bottom: 0 };
168+
}
169+
return { width: 36, height: 36, left: 8, top: 724, bottom: 760 };
170+
}
171+
}
172+
173+
function createConsoleDocument() {
174+
const document = {
175+
createElement(tagName) {
176+
const node = new FakeNode(tagName, document);
177+
if (tagName === "input") node.type = "";
178+
return node;
179+
},
180+
addEventListener() {},
181+
};
182+
const documentElement = new FakeNode("html", document);
183+
const head = new FakeNode("head", document);
184+
const body = new FakeNode("body", document);
185+
document.documentElement = documentElement;
186+
document.head = head;
187+
document.body = body;
188+
documentElement.append(head, body);
189+
document.querySelectorAll = (selector) => documentElement.querySelectorAll(selector);
190+
document.getElementById = (id) => documentElement.querySelector(`#${id}`);
191+
return document;
192+
}
193+
194+
function mountAlpha5Sidebar(document) {
195+
const footArea = document.createElement("div");
196+
footArea.id = "foot-area";
197+
const settingsArea = document.createElement("div");
198+
settingsArea.id = "settings-area";
199+
const triggerRow = document.createElement("div");
200+
triggerRow.id = "trigger-row";
201+
triggerRow.style.display = "flex";
202+
triggerRow.style.flexDirection = "row";
203+
const settings = document.createElement("button");
204+
settings.id = "dsh-settings";
205+
settings.setAttribute("aria-haspopup", "dialog");
206+
triggerRow.append(settings);
207+
settingsArea.append(triggerRow);
208+
footArea.append(settingsArea);
209+
document.body.append(footArea);
210+
return { footArea, settingsArea, triggerRow, settings };
211+
}
212+
213+
async function loadConsole(document) {
214+
const source = await fs.readFile(new URL("../console.js", import.meta.url), "utf8");
215+
const ticks = [];
216+
const context = {
217+
window: null,
218+
document,
219+
MutationObserver: class {
220+
observe() {}
221+
},
222+
addEventListener() {},
223+
innerHeight: 800,
224+
setInterval: (fn) => {
225+
ticks.push(fn);
226+
return ticks.length;
227+
},
228+
fetch: async () => ({ ok: false, json: async () => ({}) }),
229+
};
230+
context.window = context;
231+
context.globalThis = context;
232+
vm.runInNewContext(source, context);
233+
return {
234+
tick() {
235+
for (const fn of ticks) fn();
236+
},
237+
};
238+
}
239+
240+
test("console mounts above the settings area instead of inside the trigger row", async () => {
241+
const document = createConsoleDocument();
242+
const { footArea, settingsArea, triggerRow, settings } = mountAlpha5Sidebar(document);
243+
const runtime = await loadConsole(document);
244+
245+
const snapshots = [];
246+
const capture = () => {
247+
const host = document.getElementById("beauticode-console");
248+
snapshots.push({
249+
parent: host?.parentElement?.id ?? null,
250+
next: host?.nextElementSibling?.id ?? null,
251+
settingsWidth: settings.getBoundingClientRect().width,
252+
inTriggerRow: triggerRow.children.includes(host),
253+
});
254+
};
255+
256+
capture();
257+
for (let i = 0; i < 6; i += 1) {
258+
runtime.tick();
259+
capture();
260+
}
261+
262+
const host = document.getElementById("beauticode-console");
263+
assert.equal(host?.parentElement?.id, "foot-area");
264+
assert.equal(host?.nextElementSibling?.id, "settings-area");
265+
assert.equal(triggerRow.children.map((child) => child.id).join(","), "dsh-settings");
266+
assert.equal(host.parentElement, footArea);
267+
assert.equal(host.nextElementSibling, settingsArea);
268+
for (const snapshot of snapshots) {
269+
assert.equal(snapshot.parent, "foot-area");
270+
assert.equal(snapshot.next, "settings-area");
271+
assert.equal(snapshot.settingsWidth, 36);
272+
assert.equal(snapshot.inTriggerRow, false);
273+
}
274+
});

integrations/deepseek-harness/test/ui-host.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ test("plugin injects a compact sidebar console script", async (t) => {
117117
assert.doesNotMatch(source, /data-act="infernal"/);
118118
assert.match(source, /data-act="gallery"/);
119119
assert.match(source, /builtin-gallery/);
120-
assert.match(source, /insertBefore/);
120+
assert.match(source, /footArea\.insertBefore\(host, settingsArea\)/);
121121
assert.match(source, /fileInput\.type = "file"/);
122122
assert.doesNotMatch(source, /#beauticode-console\{[^}]*color-scheme/);
123123
});

0 commit comments

Comments
 (0)