dsh-mobile-ui 0.2.6 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """实验:直接触发上游「打开右侧边栏」,看 0.1.7 自己怎么布局(不改插件代码)。
3
+
4
+ 若上游自带 overlay/抽屉式呈现 ⇒ 插件只需提供入口按钮;
5
+ 若上游把 grid 改回多列挤压正文 ⇒ 插件需补 overlay CSS。
6
+ """
7
+ import sys
8
+
9
+ from playwright.sync_api import sync_playwright
10
+
11
+ BASE, TOKF = sys.argv[1], sys.argv[2]
12
+ TITLE = sys.argv[sys.argv.index("--title") + 1] if "--title" in sys.argv else None
13
+ TOKEN = open(TOKF, encoding="utf-8").read().strip()
14
+
15
+ STATE = """
16
+ () => {
17
+ const rect = (el) => { const r = el.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; };
18
+ const frame = [...document.querySelectorAll("div")].find((e) => getComputedStyle(e).display === "grid" && e.children.length >= 3) || null;
19
+ const rb = document.querySelector("[class*=rightbarCol]");
20
+ const center = document.querySelector("[class*=centerCol]");
21
+ const handle = document.querySelector("[class*=handle]");
22
+ const btns = [...document.querySelectorAll('button[aria-label]')]
23
+ .filter((b) => /右侧边栏|侧边栏/.test(b.getAttribute("aria-label") || ""))
24
+ .map((b) => ({ label: b.getAttribute("aria-label"), cls: (b.className || "").toString().slice(0, 40), rect: rect(b), vis: getComputedStyle(b).visibility }));
25
+ const bodyCls = document.body.className;
26
+ return {
27
+ vw: window.innerWidth,
28
+ gridInline: frame ? frame.style.gridTemplateColumns : null,
29
+ gridComputed: frame ? getComputedStyle(frame).gridTemplateColumns : null,
30
+ frameChildren: frame ? [...frame.children].map((c) => (c.className || "").toString().replace(/^pI_x6G_/, "")) : [],
31
+ rightbar: rb ? { rect: rect(rb), display: getComputedStyle(rb).display, pos: getComputedStyle(rb).position, vis: getComputedStyle(rb).visibility, z: getComputedStyle(rb).zIndex, text: (rb.innerText || "").trim().slice(0, 60) } : null,
32
+ center: center ? { rect: rect(center) } : null,
33
+ handle: handle ? { rect: rect(handle), display: getComputedStyle(handle).display } : null,
34
+ buttons: btns,
35
+ bodyCls: bodyCls.slice(0, 120),
36
+ };
37
+ }
38
+ """
39
+
40
+
41
+ def show(tag, d):
42
+ print(f"\n── {tag} ──")
43
+ print(f" 视口={d['vw']} gridInline={d['gridInline']} gridComputed={d['gridComputed']}")
44
+ print(f" frame 子列={d['frameChildren']}")
45
+ if d["rightbar"]:
46
+ r = d["rightbar"]
47
+ print(f" rightbar: rect={r['rect']} display={r['display']} pos={r['pos']} vis={r['vis']} z={r['z']}")
48
+ print(f" text={r['text']!r}")
49
+ if d["center"]:
50
+ print(f" centerCol: rect={d['center']['rect']}")
51
+ if d["handle"]:
52
+ print(f" handle: rect={d['handle']['rect']} display={d['handle']['display']}")
53
+ for b in d["buttons"]:
54
+ print(f" 按钮 {b['label']!r} vis={b['vis']} rect={b['rect']} cls={b['cls']}")
55
+ print(f" body.class = {d['bodyCls']!r}")
56
+
57
+
58
+ with sync_playwright() as p:
59
+ b = p.chromium.launch()
60
+ ctx = b.new_context(**p.devices.get("Pixel 7", p.devices["Pixel 5"]))
61
+ page = ctx.new_page()
62
+ page.goto(f"{BASE}/?token={TOKEN}", wait_until="domcontentloaded", timeout=60000)
63
+ page.wait_for_timeout(6000)
64
+ page.touchscreen.tap(*page.evaluate("() => { const r = document.querySelector('#dsh-mobile-menu-btn').getBoundingClientRect(); return [r.left+r.width/2, r.top+r.height/2]; }"))
65
+ page.wait_for_timeout(1500)
66
+ rows = page.query_selector_all("[role=treeitem][class*=sessionRow]") or page.query_selector_all("[role=treeitem]")
67
+ picked = t = None
68
+ for row in rows[:12]:
69
+ s = (row.inner_text() or "").strip().replace("\n", " ")
70
+ if TITLE and TITLE in s:
71
+ picked, t = row, s[:26]; break
72
+ if not TITLE and s and "新会话" not in s:
73
+ picked, t = row, s[:26]; break
74
+ picked.click()
75
+ page.wait_for_timeout(1500)
76
+ vw = page.viewport_size["width"]
77
+ for _ in range(6):
78
+ st = page.evaluate("() => ({s: !!document.querySelector('.dsh-mobile-scrim'), d: !!document.querySelector('.dsh-mobile-drawer')})")
79
+ if not st["s"] and not st["d"]:
80
+ break
81
+ page.touchscreen.tap(vw - 30, 500)
82
+ page.wait_for_timeout(800)
83
+ for _ in range(20):
84
+ page.wait_for_timeout(800)
85
+ if page.evaluate("() => !!document.querySelector('#dsh-mobile-tab-tools')"):
86
+ break
87
+ print(f"base={BASE} 会话={t}")
88
+ show("初始(插件已把 grid 改成单列、右侧栏 height:0)", page.evaluate(STATE))
89
+
90
+ # 触发上游「打开右侧边栏」
91
+ ok = page.evaluate("""() => {
92
+ const b = document.querySelector('button[aria-label="打开右侧边栏"]');
93
+ if (!b) return false;
94
+ b.click();
95
+ return true;
96
+ }""")
97
+ print(f"\n触发上游「打开右侧边栏」: {'成功' if ok else '按钮不存在'}")
98
+ page.wait_for_timeout(2000)
99
+ show("点击后(内容已渲染,但 rightbar 仍 height:0 —— 被插件的单列 grid 压掉)", page.evaluate(STATE))
100
+
101
+ # 候选修法:把 rightbarCol 变成覆盖整屏的浮层(不挤压正文)
102
+ CAND = """
103
+ body.dsh-mobile-ui [class*=rightbarCol]{
104
+ position:fixed !important; inset:0 !important; width:auto !important; height:auto !important;
105
+ z-index:2147482950 !important; display:block !important;
106
+ background:var(--dsw-alias-bg-layer-1,#16171b) !important;
107
+ }
108
+ """
109
+ page.add_style_tag(content=CAND)
110
+ page.wait_for_timeout(1000)
111
+ page.evaluate("() => window.dispatchEvent(new Event('resize'))")
112
+ page.wait_for_timeout(1500)
113
+ show("注入 overlay CSS 后", page.evaluate(STATE))
114
+ inner = page.evaluate("""() => {
115
+ const rb = document.querySelector('[class*=rightbarCol]');
116
+ if (!rb) return null;
117
+ const r = rb.getBoundingClientRect();
118
+ const cx = Math.min(Math.max(r.left + r.width/2, 1), window.innerWidth - 1);
119
+ const cy = Math.min(Math.max(r.top + r.height/2, 1), window.innerHeight - 1);
120
+ const hit = document.elementFromPoint(cx, cy);
121
+ const clickables = rb.querySelectorAll('button, [role=button], [role=treeitem]').length;
122
+ return { rect: [r.left, r.top, r.width, r.height].map(Math.round),
123
+ inViewport: r.left >= -1 && r.top >= -1 && r.right <= window.innerWidth + 1 && r.bottom <= window.innerHeight + 1,
124
+ text: (rb.innerText || "").trim().replace(/\\n+/g, " | ").slice(0, 130),
125
+ hitInside: !!(hit && rb.contains(hit)),
126
+ hitDesc: hit ? (hit.className || hit.tagName).toString().slice(0, 46) : null,
127
+ clickables };
128
+ }""")
129
+ print(f"\n 覆盖层面板: {inner}")
130
+ page.screenshot(path="/tmp/rightbar-after.png")
131
+ print("\n截图: /tmp/rightbar-after.png")
132
+ ctx.close(); b.close()
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """多视口宽度扫描:三个 mobile-ui 工具按钮在各宽度下是否完整、是否被裁切/遮挡。
3
+
4
+ 纪律来源 docs/dsh-mobile-ui.md §2.7:宽度相关的点击/显示故障**必须横扫视口**,
5
+ 单宽度探针天生漏检。
6
+
7
+ 用法: python3 width-scan.py <base-url> <token-file> [--title 片段]
8
+ """
9
+ import sys
10
+
11
+ from playwright.sync_api import sync_playwright
12
+
13
+ BASE = sys.argv[1]
14
+ TOKF = sys.argv[2]
15
+ TITLE = sys.argv[sys.argv.index("--title") + 1] if "--title" in sys.argv else None
16
+ TOKEN = open(TOKF, encoding="utf-8").read().strip()
17
+
18
+ WIDTHS = [320, 360, 375, 390, 412, 430]
19
+ BTNS = [
20
+ ("#dsh-mobile-more-proxy", "⋯"),
21
+ ("#dsh-mobile-fold-composer", "▤"),
22
+ ("#dsh-mobile-rightbar-toggle", "▥右侧栏"),
23
+ ("#dsh-mobile-tab-menu", "☰"),
24
+ ]
25
+
26
+ PROBE = """
27
+ (sels) => {
28
+ const out = { vw: window.innerWidth, items: [], group: null };
29
+ const g = document.querySelector("#dsh-mobile-tab-tools");
30
+ if (g) {
31
+ const r = g.getBoundingClientRect();
32
+ const cs = getComputedStyle(g);
33
+ out.group = { rect: [r.left, r.top, r.width, r.height].map(Math.round),
34
+ pos: cs.position, left: cs.left, right: cs.right, cls: (g.className||"").toString().slice(0,40) };
35
+ }
36
+ const clippedBy = (el) => {
37
+ let node = el.parentElement;
38
+ while (node && node !== document.documentElement) {
39
+ const cs = getComputedStyle(node);
40
+ if (cs.overflow !== "visible" || cs.overflowX !== "visible" || cs.overflowY !== "visible") {
41
+ const r = node.getBoundingClientRect(), er = el.getBoundingClientRect();
42
+ if (er.left < r.left - 0.5 || er.right > r.right + 0.5 || er.top < r.top - 0.5 || er.bottom > r.bottom + 0.5) {
43
+ return { cls: (node.className || "").toString().slice(0, 48),
44
+ rect: [r.left, r.top, r.width, r.height].map(Math.round),
45
+ ov: cs.overflow };
46
+ }
47
+ }
48
+ node = node.parentElement;
49
+ }
50
+ return null;
51
+ };
52
+ for (const [sel, name] of sels) {
53
+ const el = document.querySelector(sel);
54
+ if (!el) { out.items.push({ name, exists: false }); continue; }
55
+ const r = el.getBoundingClientRect();
56
+ let op = 1, vis = "visible", pe = "auto", node = el;
57
+ while (node && node !== document.documentElement) {
58
+ const cs = getComputedStyle(node);
59
+ op *= parseFloat(cs.opacity || "1");
60
+ if (cs.visibility === "hidden") vis = "hidden";
61
+ if (cs.pointerEvents === "none") pe = "none";
62
+ node = node.parentElement;
63
+ }
64
+ const cx = Math.min(Math.max(r.left + r.width / 2, 1), window.innerWidth - 1);
65
+ const cy = Math.min(Math.max(r.top + r.height / 2, 1), window.innerHeight - 1);
66
+ const hit = document.elementFromPoint(cx, cy);
67
+ out.items.push({
68
+ name, exists: true,
69
+ rect: [r.left, r.top, r.width, r.height].map(Math.round),
70
+ inViewport: r.left >= -0.5 && r.right <= window.innerWidth + 0.5 && r.top >= -0.5,
71
+ overflowRight: Math.round(r.right - window.innerWidth),
72
+ op: +op.toFixed(2), vis, pe,
73
+ hitSelf: !!(hit && (hit === el || el.contains(hit))),
74
+ hitDesc: hit ? (hit.id || hit.className || hit.tagName).toString().slice(0, 40) : null,
75
+ clippedBy: clippedBy(el),
76
+ });
77
+ }
78
+ return out;
79
+ }
80
+ """
81
+
82
+ with sync_playwright() as p:
83
+ b = p.chromium.launch()
84
+ ctx = b.new_context(viewport={"width": 390, "height": 900}, is_mobile=True, has_touch=True)
85
+ page = ctx.new_page()
86
+ page.goto(f"{BASE}/?token={TOKEN}", wait_until="domcontentloaded", timeout=60000)
87
+ page.wait_for_timeout(6000)
88
+ page.click("#dsh-mobile-menu-btn", timeout=8000)
89
+ page.wait_for_timeout(1500)
90
+ rows = page.query_selector_all("[role=treeitem][class*=sessionRow]") or page.query_selector_all("[role=treeitem]")
91
+ picked = title = None
92
+ for row in rows[:12]:
93
+ t = (row.inner_text() or "").strip().replace("\n", " ")
94
+ if TITLE and TITLE in t:
95
+ picked, title = row, t[:30]
96
+ break
97
+ if not TITLE and t and "新会话" not in t:
98
+ picked, title = row, t[:30]
99
+ break
100
+ if picked is None:
101
+ print("找不到目标会话"); sys.exit(2)
102
+ picked.click()
103
+ page.wait_for_timeout(1500)
104
+
105
+ # ⚠️ 点会话项不会自动关抽屉(closeDrawer 只绑在 scrim 上);关不掉的话
106
+ # scrim(z-index 2147482995)会盖住全屏、把三个按钮的点击全吃掉 ⇒ 全是假阴性。
107
+ # 正确姿势(抄 live-probe.py:122):touchscreen.tap 点右侧空白,并**验证**已关。
108
+ DRAWER_STATE = """
109
+ () => {
110
+ const vis = (el) => {
111
+ if (!el) return false;
112
+ const cs = getComputedStyle(el), r = el.getBoundingClientRect();
113
+ return cs.visibility !== "hidden" && parseFloat(cs.opacity || "1") > 0.5 && r.width > 1 && r.height > 1;
114
+ };
115
+ const scrim = document.querySelector(".dsh-mobile-scrim");
116
+ const drawer = document.querySelector(".dsh-mobile-drawer");
117
+ return { scrim: vis(scrim), drawer: vis(drawer), closed: !vis(scrim) && !vis(drawer) };
118
+ }
119
+ """
120
+ closed = False
121
+ for i in range(6):
122
+ st = page.evaluate(DRAWER_STATE)
123
+ if st["closed"]:
124
+ closed = True
125
+ break
126
+ page.touchscreen.tap(390 - 30, 500)
127
+ page.wait_for_timeout(900)
128
+ print(f"抽屉已关闭: {closed}" + ("" if closed else " ⚠️ 关不掉,下面结果不可信"))
129
+ if not closed:
130
+ sys.exit(3)
131
+
132
+ for _ in range(25):
133
+ page.wait_for_timeout(1000)
134
+ if page.evaluate("() => !!document.querySelector('#dsh-mobile-tab-tools')"):
135
+ break
136
+ print(f"base={BASE} 会话={title}\n")
137
+
138
+ problems = []
139
+ print(f"{'宽度':>5} | " + " | ".join(f"{n:^26}" for _, n in BTNS))
140
+ for w in WIDTHS:
141
+ page.set_viewport_size({"width": w, "height": 880})
142
+ page.wait_for_timeout(2200) # 等插件 resize sync
143
+ d = page.evaluate(PROBE, BTNS)
144
+ cells = []
145
+ for it in d["items"]:
146
+ if not it.get("exists"):
147
+ cells.append("不存在")
148
+ problems.append(f"{w}px: {it['name']} 不存在")
149
+ continue
150
+ flags = []
151
+ if not it["inViewport"]:
152
+ flags.append(f"出视口+{it['overflowRight']}px")
153
+ if it["clippedBy"]:
154
+ flags.append(f"被 {it['clippedBy']['cls'][:22]} 裁(ov={it['clippedBy']['ov']})")
155
+ if it["op"] < 0.9:
156
+ flags.append(f"op={it['op']}")
157
+ if it["vis"] != "visible":
158
+ flags.append("hidden")
159
+ if it["pe"] == "none":
160
+ flags.append("pe:none")
161
+ if not it["hitSelf"]:
162
+ flags.append(f"中心被{it['hitDesc']}吃")
163
+ r = it["rect"]
164
+ cells.append(f"{r[0]},{r[1]} {r[2]}x{r[3]}" + (" ⚠ " + ";".join(flags) if flags else " ✓"))
165
+ if flags:
166
+ problems.append(f"{w}px: {it['name']} → " + "; ".join(flags))
167
+ print(f"{w:>5} | " + " | ".join(f"{c[:26]:^26}" for c in cells))
168
+ if d["group"]:
169
+ g = d["group"]
170
+ print(f" └ 工具组 pos={g['pos']} left={g['left']} right={g['right']} rect={g['rect']} cls={g['cls']}")
171
+
172
+ print()
173
+ if problems:
174
+ print(f"❌ {len(problems)} 项异常:")
175
+ for x in problems:
176
+ print(" - " + x)
177
+ sys.exit(1)
178
+ print("✅ 全部宽度下所有工具按钮均完整、可见、可点")
179
+ ctx.close(); b.close()