dsh-question 0.0.0-stage → 0.1.3

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.
package/lib/client.js ADDED
@@ -0,0 +1,433 @@
1
+ /* dsh-question — 浏览器侧 bundle(CJS 工厂,供 DSH Web 客户端 ModuleLoader 注入)。
2
+ *
3
+ * 纯呈现层插件:不改模型可见的工具 schema,也不替换提问卡片的 React 组件,只做两件事:
4
+ * 1. 样式:选项行只显示重点(label),详细说明(description)收进悬停浮窗,「其他:」替掉铅笔图标;
5
+ * 2. 事件委托 + 固定定位浮窗:悬停/聚焦选项时按需读取 DOM 里仍在的 description 文本。
6
+ * 不在 React 树里插节点,所以选择、翻页、重渲染都不会把增强冲掉;
7
+ * apply 幂等(window.__DSH_QUESTION_CARD__ 记录上一代 disposer),HMR 重载不会叠加监听器。
8
+ *
9
+ * ⚠️ 本文件由 scripts/build.cjs 生成,不要手改;改 src/ 下的片段后重新 build。
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: "dsh-question",
13
+ factory: function (require) {
14
+ var module = { exports: {} };
15
+ var exports = module.exports;
16
+
17
+ "use strict";
18
+
19
+ var GLOBAL_KEY = "__DSH_QUESTION_CARD__";
20
+ var STYLE_NAME = "dsh-question/dsh-question.css";
21
+ var TIP_CLASS = "dsh-tq-tip";
22
+ var SHOW_DELAY_MS = 160;
23
+ var CSS = "/* dsh-question — 悬停式提问卡片(样式层)\n *\n * 作用域:只命中 ask_user_question 的提问卡片——它的外层 .frame 带 data-question-key,\n * 这是 DSH 客户端的稳定语义钩子;其余界面(如 plan-review 卡片)不受影响。\n *\n * 设计:\n * 1. 选项行里原本内联的 description 收进悬停浮窗,行内只留「重点」(label);\n * 2. label 最多两行,超出省略——完整内容在浮窗里看;\n * 3. 自定义答案行以「其他:」开头(替掉铅笔图标),让自由输入一眼可见;\n * 4. 带详细说明的选项右上角显示一个 ⓘ 提示可悬停。\n */\n\n/* ① 行内详细说明不再占位:文本仍留在 DOM 里,由 client.js 的浮窗读取。 */\n[data-question-key] button[role=\"radio\"] [class*=\"_description\"],\n[data-question-key] button[role=\"checkbox\"] [class*=\"_description\"] {\n display: none !important;\n}\n\n/* ② 选项重点最多两行,超出用省略号。 */\n[data-question-key] button[role=\"radio\"] [class*=\"_optionLabel\"],\n[data-question-key] button[role=\"checkbox\"] [class*=\"_optionLabel\"] {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n text-overflow: ellipsis;\n overflow-wrap: anywhere;\n}\n\n/* ③ 悬停提示的可视暗示:有详细说明的选项在行尾显示 ⓘ。 */\n[data-question-key] button[role=\"radio\"]:has([class*=\"_description\"])::after,\n[data-question-key] button[role=\"checkbox\"]:has([class*=\"_description\"])::after {\n content: \"\\24D8\";\n flex: 0 0 auto;\n margin-left: auto;\n padding-left: 8px;\n color: var(--dsw-alias-label-tertiary, #8a8a8a);\n font-size: 12px;\n line-height: 24px;\n font-weight: 400;\n}\n\n/* ④ 单选:序号圆片改成圆形单选框(未选=空心圆环,选中=实心圆)。 */\n[data-question-key] button[role=\"radio\"] > [class*=\"_number\"] {\n box-sizing: border-box;\n width: 26px;\n height: 26px;\n flex: 0 0 26px;\n aspect-ratio: 1 / 1;\n padding: 0;\n margin-top: -1px;\n border-radius: 50%;\n corner-shape: round;\n border: 2px solid var(--dsw-alias-border-l4, rgba(255, 255, 255, 0.3));\n background: transparent;\n color: var(--dsw-alias-label-secondary, #9a9a9a);\n font-size: 13px;\n line-height: 1;\n font-weight: 500;\n display: grid;\n place-items: center;\n}\n\n[data-question-key] button[role=\"radio\"][aria-checked=\"true\"] > [class*=\"_number\"] {\n border-color: var(--dsw-alias-state-business-primary, #4f8cff);\n background: var(--dsw-alias-state-business-primary, #4f8cff);\n color: var(--dsw-alias-label-primary-foreground, #111);\n}\n\n/* ⑤ 「其他:」替掉铅笔/序号图标。 */\n[data-question-key] [class*=\"_customRow\"]::before {\n content: var(--dsh-question-other-prefix, \"\\5176\\4ED6\\FF1A\");\n flex: 0 0 auto;\n color: var(--dsw-alias-label-secondary, #9a9a9a);\n font-size: 14px;\n line-height: 24px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n[data-question-key] [class*=\"_customRow\"] > [class*=\"_number\"],\n[data-question-key] [class*=\"_customRow\"] > [class*=\"_checkbox\"] {\n display: none !important;\n}\n\n/* ⑤ 浮窗:由 client.js 定位到选项行旁边。 */\n.dsh-tq-tip {\n position: fixed;\n z-index: 2147483000;\n box-sizing: border-box;\n max-width: min(392px, calc(100vw - 24px));\n padding: 10px 12px;\n border-radius: 12px;\n border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12)));\n background: var(--dsw-alias-bg-module-platform, #353638);\n color: var(--dsw-alias-label-primary, #ededed);\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42), 0 2px 8px rgba(0, 0, 0, 0.28);\n font-size: 13px;\n line-height: 20px;\n letter-spacing: 0.01em;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n pointer-events: none;\n opacity: 0;\n transform: translateY(3px);\n transition: opacity 0.12s ease, transform 0.12s ease;\n}\n\n.dsh-tq-tip[data-show=\"1\"] {\n opacity: 1;\n transform: translateY(0);\n}\n\n.dsh-tq-tip .dsh-tq-tip__title {\n margin-bottom: 4px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #ededed);\n}\n\n.dsh-tq-tip .dsh-tq-tip__body {\n color: var(--dsw-alias-label-secondary, #b9b9b9);\n}\n\n/* ⑥ 问题导航:让「上一题 / 下一题」在卡片里明确可点,多题批次来回切换不靠猜。 */\n[data-question-key] [class*=\"_pager\"] {\n gap: 8px;\n}\n\n[data-question-key] [class*=\"_pager\"] button {\n width: 28px;\n height: 28px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16));\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #9d9d9d);\n transition: background-color 0.12s, border-color 0.12s, color 0.12s;\n}\n\n[data-question-key] [class*=\"_pager\"] button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.07));\n border-color: var(--dsw-alias-border-l4, rgba(255, 255, 255, 0.2));\n color: var(--dsw-alias-label-primary, #e8e8e8);\n}\n\n[data-question-key] [class*=\"_pager\"] button:disabled {\n opacity: 0.35;\n cursor: default;\n}\n\n[data-question-key] [class*=\"_progress\"] {\n color: var(--dsw-alias-label-secondary, #9d9d9d);\n font-variant-numeric: tabular-nums;\n font-size: 13px;\n}\n";
24
+
25
+ function classNameOf(el) {
26
+ try {
27
+ var value = el && el.className;
28
+ return typeof value === "string" ? value : "";
29
+ } catch (error) {
30
+ return "";
31
+ }
32
+ }
33
+
34
+ function hasToken(el, suffix) {
35
+ var classes = classNameOf(el).split(/\s+/);
36
+ for (var i = 0; i < classes.length; i += 1) {
37
+ var token = classes[i];
38
+ if (token === suffix) return true;
39
+ if (token.slice(-(suffix.length + 1)) === "_" + suffix) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function textOf(el) {
45
+ return (el && el.textContent ? String(el.textContent) : "").replace(/\s+/g, " ").trim();
46
+ }
47
+
48
+ /**
49
+ * The “other answer” prefix in the active UI language. DSH keeps <html lang> in
50
+ * sync with the locale service, so the plugin follows the GUI without config.
51
+ */
52
+ function otherPrefix() {
53
+ var language = "";
54
+ try {
55
+ language = (document.documentElement.getAttribute("lang") || "").toLowerCase();
56
+ } catch (error) {
57
+ language = "";
58
+ }
59
+ if (language === "") {
60
+ try {
61
+ language = (navigator.language || "").toLowerCase();
62
+ } catch (error) {
63
+ language = "";
64
+ }
65
+ }
66
+ return language.indexOf("zh") === 0 ? "\u5176\u4ed6\uff1a" : "Other: ";
67
+ }
68
+
69
+ function closestOption(node) {
70
+ var el = node && node.nodeType === 3 ? node.parentElement : node;
71
+ if (!el || typeof el.closest !== "function") return null;
72
+ if (!el.closest("[data-question-key]")) return null;
73
+ var button = el.closest('button[role="radio"], button[role="checkbox"]');
74
+ return button && hasToken(button, "option") ? button : null;
75
+ }
76
+
77
+ function readOption(button) {
78
+ var spans = button.querySelectorAll("span");
79
+ var labelEl = null;
80
+ var descEl = null;
81
+ for (var i = 0; i < spans.length; i += 1) {
82
+ var span = spans[i];
83
+ if (labelEl === null && hasToken(span, "optionLabel")) {
84
+ labelEl = span;
85
+ } else if (descEl === null && hasToken(span, "description")) {
86
+ descEl = span;
87
+ }
88
+ }
89
+ if (labelEl === null) return null;
90
+ var clamped = labelEl.scrollHeight > labelEl.clientHeight + 1;
91
+ var description = descEl === null ? "" : textOf(descEl);
92
+ if (!description && !clamped) return null;
93
+ return { label: textOf(labelEl), description: description, clamped: clamped };
94
+ }
95
+
96
+ function createTip() {
97
+ var tip = document.createElement("div");
98
+ tip.className = TIP_CLASS;
99
+ tip.setAttribute("role", "tooltip");
100
+ tip.setAttribute("data-show", "0");
101
+ return tip;
102
+ }
103
+
104
+ function renderTip(tip, data) {
105
+ tip.textContent = "";
106
+ if (data.clamped && data.label) {
107
+ var title = document.createElement("div");
108
+ title.className = "dsh-tq-tip__title";
109
+ title.textContent = data.label;
110
+ tip.appendChild(title);
111
+ }
112
+ if (data.description) {
113
+ var body = document.createElement("div");
114
+ body.className = "dsh-tq-tip__body";
115
+ body.textContent = data.description;
116
+ tip.appendChild(body);
117
+ }
118
+ }
119
+
120
+ function placeTip(tip, button) {
121
+ tip.style.left = "0px";
122
+ tip.style.top = "0px";
123
+ var rect = button.getBoundingClientRect();
124
+ var tipRect = tip.getBoundingClientRect();
125
+ var gap = 8;
126
+ var viewportWidth = window.innerWidth || document.documentElement.clientWidth;
127
+ var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
128
+ var top = rect.top - tipRect.height - gap;
129
+ if (top < 8) top = rect.bottom + gap;
130
+ if (top + tipRect.height > viewportHeight - 8) top = Math.max(8, viewportHeight - tipRect.height - 8);
131
+ var left = Math.min(rect.left, viewportWidth - tipRect.width - 12);
132
+ if (left < 12) left = 12;
133
+ tip.style.left = Math.round(left) + "px";
134
+ tip.style.top = Math.round(top) + "px";
135
+ }
136
+
137
+ function injectStyle() {
138
+ var selector = "style[data-plugin-css=" + JSON.stringify(STYLE_NAME) + "]";
139
+ var existing = document.querySelector(selector);
140
+ if (existing !== null) return existing;
141
+ var tag = document.createElement("style");
142
+ tag.dataset.plugin = "dsh-question";
143
+ tag.dataset.pluginCss = STYLE_NAME;
144
+ tag.textContent = CSS;
145
+ document.head.appendChild(tag);
146
+ return tag;
147
+ }
148
+
149
+ function apply(ctx) {
150
+ var previous = window[GLOBAL_KEY];
151
+ if (previous && typeof previous.dispose === "function") {
152
+ try {
153
+ previous.dispose();
154
+ } catch (error) {
155
+ /* a previous generation is already gone */
156
+ }
157
+ }
158
+
159
+ var disposers = [];
160
+ var disposed = false;
161
+ var tip = createTip();
162
+ var styleTag = injectStyle();
163
+ var currentButton = null;
164
+ var visible = false;
165
+ var showTimer = 0;
166
+
167
+ var mountRoot = document.body || document.documentElement;
168
+ mountRoot.appendChild(tip);
169
+
170
+ function syncOtherPrefix() {
171
+ try {
172
+ document.documentElement.style.setProperty("--dsh-question-other-prefix", JSON.stringify(otherPrefix()));
173
+ } catch (error) {
174
+ /* non-browser environments: the CSS fallback still applies */
175
+ }
176
+ }
177
+ syncOtherPrefix();
178
+ var languageObserver = null;
179
+ if (typeof MutationObserver === "function") {
180
+ languageObserver = new MutationObserver(syncOtherPrefix);
181
+ languageObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
182
+ }
183
+ disposers.push(function () {
184
+ if (languageObserver !== null) languageObserver.disconnect();
185
+ document.documentElement.style.removeProperty("--dsh-question-other-prefix");
186
+ });
187
+ disposers.push(function () {
188
+ if (tip.parentNode !== null) tip.parentNode.removeChild(tip);
189
+ });
190
+ disposers.push(function () {
191
+ if (styleTag.parentNode !== null) styleTag.parentNode.removeChild(styleTag);
192
+ });
193
+
194
+ function cancel() {
195
+ if (showTimer !== 0) {
196
+ window.clearTimeout(showTimer);
197
+ showTimer = 0;
198
+ }
199
+ currentButton = null;
200
+ if (visible) {
201
+ visible = false;
202
+ tip.setAttribute("data-show", "0");
203
+ }
204
+ }
205
+
206
+ function showFor(button, immediate) {
207
+ if (disposed || button === currentButton) return;
208
+ cancel();
209
+ var data = readOption(button);
210
+ if (data === null) return;
211
+ currentButton = button;
212
+ var fire = function () {
213
+ showTimer = 0;
214
+ if (disposed || currentButton !== button || !button.isConnected) return;
215
+ renderTip(tip, data);
216
+ placeTip(tip, button);
217
+ visible = true;
218
+ tip.setAttribute("data-show", "1");
219
+ };
220
+ if (immediate) fire();
221
+ else showTimer = window.setTimeout(fire, SHOW_DELAY_MS);
222
+ }
223
+
224
+ function onMouseOver(event) {
225
+ var button = closestOption(event.target);
226
+ if (button === null) {
227
+ if (currentButton !== null && !(event.relatedTarget && currentButton.contains(event.relatedTarget))) cancel();
228
+ return;
229
+ }
230
+ showFor(button, false);
231
+ }
232
+
233
+ function onMouseOut(event) {
234
+ var button = closestOption(event.target);
235
+ if (button === null) return;
236
+ if (event.relatedTarget && button.contains(event.relatedTarget)) return;
237
+ if (button === currentButton) cancel();
238
+ }
239
+
240
+ function onFocusIn(event) {
241
+ var button = closestOption(event.target);
242
+ if (button !== null) showFor(button, true);
243
+ }
244
+
245
+ function onFocusOut(event) {
246
+ var button = closestOption(event.target);
247
+ if (button !== null && button === currentButton) cancel();
248
+ }
249
+
250
+ function onClick(event) {
251
+ if (closestOption(event.target) !== null) cancel();
252
+ }
253
+
254
+ function onViewportChange() {
255
+ cancel();
256
+ }
257
+
258
+ document.addEventListener("mouseover", onMouseOver, true);
259
+ document.addEventListener("mouseout", onMouseOut, true);
260
+ document.addEventListener("focusin", onFocusIn, true);
261
+ document.addEventListener("focusout", onFocusOut, true);
262
+ document.addEventListener("click", onClick, true);
263
+ document.addEventListener("scroll", onViewportChange, true);
264
+ window.addEventListener("resize", onViewportChange, true);
265
+
266
+ disposers.push(installKeyboard());
267
+
268
+ disposers.push(function () {
269
+ document.removeEventListener("mouseover", onMouseOver, true);
270
+ document.removeEventListener("mouseout", onMouseOut, true);
271
+ document.removeEventListener("focusin", onFocusIn, true);
272
+ document.removeEventListener("focusout", onFocusOut, true);
273
+ document.removeEventListener("click", onClick, true);
274
+ document.removeEventListener("scroll", onViewportChange, true);
275
+ window.removeEventListener("resize", onViewportChange, true);
276
+ });
277
+
278
+ function dispose() {
279
+ if (disposed) return;
280
+ disposed = true;
281
+ cancel();
282
+ for (var i = disposers.length - 1; i >= 0; i -= 1) {
283
+ try {
284
+ disposers[i]();
285
+ } catch (error) {
286
+ /* keep releasing the rest */
287
+ }
288
+ }
289
+ disposers.length = 0;
290
+ if (window[GLOBAL_KEY] && window[GLOBAL_KEY].dispose === dispose) delete window[GLOBAL_KEY];
291
+ }
292
+
293
+ if (ctx && typeof ctx.effect === "function") {
294
+ try {
295
+ ctx.effect(function () {
296
+ return dispose;
297
+ }, "dsh-question: hover tooltip");
298
+ } catch (error) {
299
+ /* older client runners: the window guard still keeps HMR idempotent */
300
+ }
301
+ }
302
+ window[GLOBAL_KEY] = { dispose: dispose };
303
+ }
304
+
305
+ /** Whether a node is a text-entry surface where plain keys must stay untouched. */
306
+ function isTypingTarget(node) {
307
+ var el = node && node.nodeType === 3 ? node.parentElement : node;
308
+ if (!el || typeof el.closest !== "function") return false;
309
+ return el.closest("input, textarea, select, [contenteditable=\"true\"], [contenteditable=\"\"]") !== null;
310
+ }
311
+
312
+ /** The visible question card, or null when none is mounted. */
313
+ function visibleQuestionCard() {
314
+ var cards = document.querySelectorAll("[data-question-key]");
315
+ for (var i = 0; i < cards.length; i += 1) {
316
+ var card = cards[i];
317
+ if (card.isConnected && card.getClientRects().length > 0) return card;
318
+ }
319
+ return null;
320
+ }
321
+
322
+ /** Option buttons in display order (single-select radios or multi-select checkboxes). */
323
+ function optionButtonsOf(card) {
324
+ return card.querySelectorAll('button[role="radio"], button[role="checkbox"]');
325
+ }
326
+
327
+ /** Pager buttons in order: [previous, next]; empty when the batch has one question. */
328
+ function pagerButtonsOf(card) {
329
+ var pager = card.querySelector('[class*="_pager"]');
330
+ if (pager === null) return [];
331
+ return pager.querySelectorAll("button");
332
+ }
333
+
334
+ /** Move one question backward (-1) or forward (1) through the card's own pager. */
335
+ function stepQuestion(card, delta) {
336
+ var buttons = pagerButtonsOf(card);
337
+ if (buttons.length === 0) return false;
338
+ var button = delta < 0 ? buttons[0] : buttons[buttons.length - 1];
339
+ if (button.disabled) return false;
340
+ button.click();
341
+ return true;
342
+ }
343
+
344
+ /** The primary footer action (submit / next). Last button inside the action group. */
345
+ function primaryActionOf(card) {
346
+ var group = card.querySelector('[class*="_footerActions"]');
347
+ if (group === null) return null;
348
+ var buttons = group.querySelectorAll("button");
349
+ return buttons.length === 0 ? null : buttons[buttons.length - 1];
350
+ }
351
+
352
+ /** The collapse control: the header icon button carrying aria-expanded. */
353
+ function collapseControlOf(card) {
354
+ var header = card.querySelector("header");
355
+ if (header === null) return null;
356
+ var buttons = header.querySelectorAll("button");
357
+ for (var i = 0; i < buttons.length; i += 1) {
358
+ if (buttons[i].hasAttribute("aria-expanded")) return buttons[i];
359
+ }
360
+ return null;
361
+ }
362
+
363
+ /** Select the nth option by clicking it, so every native side effect stays intact. */
364
+ function clickOption(card, index) {
365
+ var options = optionButtonsOf(card);
366
+ if (index < 0 || index >= options.length) return false;
367
+ var button = options[index];
368
+ if (button.disabled) return false;
369
+ button.click();
370
+ return true;
371
+ }
372
+
373
+ /**
374
+ * Keyboard shortcuts for the open question card: 1-9 pick an option, Enter runs
375
+ * the primary footer action, Escape collapses the card (never cancels a question).
376
+ * Text entry surfaces are left completely alone, so typing a digit or Enter into
377
+ * the custom answer behaves natively.
378
+ */
379
+ function installKeyboard() {
380
+ function onKeyDown(event) {
381
+ if (event.defaultPrevented || event.isComposing) return;
382
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
383
+ if (isTypingTarget(event.target)) return;
384
+ var card = visibleQuestionCard();
385
+ if (card === null) return;
386
+ var key = event.key;
387
+ if (key !== null && key.length === 1 && key >= "1" && key <= "9") {
388
+ if (clickOption(card, key.charCodeAt(0) - 49)) {
389
+ event.preventDefault();
390
+ event.stopPropagation();
391
+ }
392
+ return;
393
+ }
394
+ if (key === "ArrowLeft" || key === "PageUp") {
395
+ if (stepQuestion(card, -1)) {
396
+ event.preventDefault();
397
+ event.stopPropagation();
398
+ }
399
+ return;
400
+ }
401
+ if (key === "ArrowRight" || key === "PageDown") {
402
+ if (stepQuestion(card, 1)) {
403
+ event.preventDefault();
404
+ event.stopPropagation();
405
+ }
406
+ return;
407
+ }
408
+ if (key === "Enter") {
409
+ var action = primaryActionOf(card);
410
+ if (action === null || action.disabled) return;
411
+ event.preventDefault();
412
+ event.stopPropagation();
413
+ action.click();
414
+ return;
415
+ }
416
+ if (key === "Escape") {
417
+ var collapse = collapseControlOf(card);
418
+ if (collapse === null || collapse.disabled) return;
419
+ event.preventDefault();
420
+ event.stopPropagation();
421
+ collapse.click();
422
+ }
423
+ }
424
+ document.addEventListener("keydown", onKeyDown, true);
425
+ return function () {
426
+ document.removeEventListener("keydown", onKeyDown, true);
427
+ };
428
+ }
429
+
430
+ module.exports = { name: "question", inject: [], apply: apply };
431
+ return module.exports;
432
+ },
433
+ });
@@ -0,0 +1,154 @@
1
+ /* dsh-question — 悬停式提问卡片(样式层)
2
+ *
3
+ * 作用域:只命中 ask_user_question 的提问卡片——它的外层 .frame 带 data-question-key,
4
+ * 这是 DSH 客户端的稳定语义钩子;其余界面(如 plan-review 卡片)不受影响。
5
+ *
6
+ * 设计:
7
+ * 1. 选项行里原本内联的 description 收进悬停浮窗,行内只留「重点」(label);
8
+ * 2. label 最多两行,超出省略——完整内容在浮窗里看;
9
+ * 3. 自定义答案行以「其他:」开头(替掉铅笔图标),让自由输入一眼可见;
10
+ * 4. 带详细说明的选项右上角显示一个 ⓘ 提示可悬停。
11
+ */
12
+
13
+ /* ① 行内详细说明不再占位:文本仍留在 DOM 里,由 client.js 的浮窗读取。 */
14
+ [data-question-key] button[role="radio"] [class*="_description"],
15
+ [data-question-key] button[role="checkbox"] [class*="_description"] {
16
+ display: none !important;
17
+ }
18
+
19
+ /* ② 选项重点最多两行,超出用省略号。 */
20
+ [data-question-key] button[role="radio"] [class*="_optionLabel"],
21
+ [data-question-key] button[role="checkbox"] [class*="_optionLabel"] {
22
+ display: -webkit-box;
23
+ -webkit-box-orient: vertical;
24
+ -webkit-line-clamp: 2;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ overflow-wrap: anywhere;
28
+ }
29
+
30
+ /* ③ 悬停提示的可视暗示:有详细说明的选项在行尾显示 ⓘ。 */
31
+ [data-question-key] button[role="radio"]:has([class*="_description"])::after,
32
+ [data-question-key] button[role="checkbox"]:has([class*="_description"])::after {
33
+ content: "\24D8";
34
+ flex: 0 0 auto;
35
+ margin-left: auto;
36
+ padding-left: 8px;
37
+ color: var(--dsw-alias-label-tertiary, #8a8a8a);
38
+ font-size: 12px;
39
+ line-height: 24px;
40
+ font-weight: 400;
41
+ }
42
+
43
+ /* ④ 单选:序号圆片改成圆形单选框(未选=空心圆环,选中=实心圆)。 */
44
+ [data-question-key] button[role="radio"] > [class*="_number"] {
45
+ box-sizing: border-box;
46
+ width: 26px;
47
+ height: 26px;
48
+ flex: 0 0 26px;
49
+ aspect-ratio: 1 / 1;
50
+ padding: 0;
51
+ margin-top: -1px;
52
+ border-radius: 50%;
53
+ corner-shape: round;
54
+ border: 2px solid var(--dsw-alias-border-l4, rgba(255, 255, 255, 0.3));
55
+ background: transparent;
56
+ color: var(--dsw-alias-label-secondary, #9a9a9a);
57
+ font-size: 13px;
58
+ line-height: 1;
59
+ font-weight: 500;
60
+ display: grid;
61
+ place-items: center;
62
+ }
63
+
64
+ [data-question-key] button[role="radio"][aria-checked="true"] > [class*="_number"] {
65
+ border-color: var(--dsw-alias-state-business-primary, #4f8cff);
66
+ background: var(--dsw-alias-state-business-primary, #4f8cff);
67
+ color: var(--dsw-alias-label-primary-foreground, #111);
68
+ }
69
+
70
+ /* ⑤ 「其他:」替掉铅笔/序号图标。 */
71
+ [data-question-key] [class*="_customRow"]::before {
72
+ content: var(--dsh-question-other-prefix, "\5176\4ED6\FF1A");
73
+ flex: 0 0 auto;
74
+ color: var(--dsw-alias-label-secondary, #9a9a9a);
75
+ font-size: 14px;
76
+ line-height: 24px;
77
+ font-weight: 500;
78
+ white-space: nowrap;
79
+ }
80
+
81
+ [data-question-key] [class*="_customRow"] > [class*="_number"],
82
+ [data-question-key] [class*="_customRow"] > [class*="_checkbox"] {
83
+ display: none !important;
84
+ }
85
+
86
+ /* ⑤ 浮窗:由 client.js 定位到选项行旁边。 */
87
+ .dsh-tq-tip {
88
+ position: fixed;
89
+ z-index: 2147483000;
90
+ box-sizing: border-box;
91
+ max-width: min(392px, calc(100vw - 24px));
92
+ padding: 10px 12px;
93
+ border-radius: 12px;
94
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12)));
95
+ background: var(--dsw-alias-bg-module-platform, #353638);
96
+ color: var(--dsw-alias-label-primary, #ededed);
97
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42), 0 2px 8px rgba(0, 0, 0, 0.28);
98
+ font-size: 13px;
99
+ line-height: 20px;
100
+ letter-spacing: 0.01em;
101
+ white-space: pre-wrap;
102
+ overflow-wrap: anywhere;
103
+ pointer-events: none;
104
+ opacity: 0;
105
+ transform: translateY(3px);
106
+ transition: opacity 0.12s ease, transform 0.12s ease;
107
+ }
108
+
109
+ .dsh-tq-tip[data-show="1"] {
110
+ opacity: 1;
111
+ transform: translateY(0);
112
+ }
113
+
114
+ .dsh-tq-tip .dsh-tq-tip__title {
115
+ margin-bottom: 4px;
116
+ font-weight: 600;
117
+ color: var(--dsw-alias-label-primary, #ededed);
118
+ }
119
+
120
+ .dsh-tq-tip .dsh-tq-tip__body {
121
+ color: var(--dsw-alias-label-secondary, #b9b9b9);
122
+ }
123
+
124
+ /* ⑥ 问题导航:让「上一题 / 下一题」在卡片里明确可点,多题批次来回切换不靠猜。 */
125
+ [data-question-key] [class*="_pager"] {
126
+ gap: 8px;
127
+ }
128
+
129
+ [data-question-key] [class*="_pager"] button {
130
+ width: 28px;
131
+ height: 28px;
132
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.16));
133
+ border-radius: 999px;
134
+ background: transparent;
135
+ color: var(--dsw-alias-label-secondary, #9d9d9d);
136
+ transition: background-color 0.12s, border-color 0.12s, color 0.12s;
137
+ }
138
+
139
+ [data-question-key] [class*="_pager"] button:hover:not(:disabled) {
140
+ background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.07));
141
+ border-color: var(--dsw-alias-border-l4, rgba(255, 255, 255, 0.2));
142
+ color: var(--dsw-alias-label-primary, #e8e8e8);
143
+ }
144
+
145
+ [data-question-key] [class*="_pager"] button:disabled {
146
+ opacity: 0.35;
147
+ cursor: default;
148
+ }
149
+
150
+ [data-question-key] [class*="_progress"] {
151
+ color: var(--dsw-alias-label-secondary, #9d9d9d);
152
+ font-variant-numeric: tabular-nums;
153
+ font-size: 13px;
154
+ }
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Host 半侧:本插件只改 Web 客户端的提问卡片呈现,没有 host 侧逻辑。
2
+ // 保留一个空 apply,让 Loader 条目能正常创建/激活(客户端 bundle 由
3
+ // @deepseek-ai/dsh-client-modules 依据 package.json 的 dsh.client 提供)。
4
+ export const name = 'question-card'
5
+
6
+ export function apply() {}
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "dsh-question",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.3",
4
+ "description": "Ask-and-wait question cards for DeepSeek Harness: key-point options, hover details, free-form answers, keyboard shortcuts, automatic Chinese/English.",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "ask-user-question",
10
+ "client-plugin",
11
+ "ui"
12
+ ],
13
+ "type": "module",
14
+ "main": "./lib/index.js",
15
+ "exports": {
16
+ ".": "./lib/index.js",
17
+ "./client": "./lib/client.js",
18
+ "./cordis.patch.yml": "./cordis.patch.yml",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "lib/",
23
+ "src/",
24
+ "scripts/",
25
+ "cordis.patch.yml",
26
+ "README.md",
27
+ "README.en.md",
28
+ "LICENSE",
29
+ "docs/"
30
+ ],
31
+ "scripts": {
32
+ "build": "node scripts/build.cjs",
33
+ "check": "node --check lib/client.js && node --check lib/index.js",
34
+ "test": "node --test",
35
+ "prepublishOnly": "npm run build && npm run check && npm test"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "client": {
42
+ "platform": "web"
43
+ }
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "author": "1234ab484",
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/1234ab484/dsh-question.git"
53
+ },
54
+ "homepage": "https://github.com/1234ab484/dsh-question#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/1234ab484/dsh-question/issues"
57
+ }
58
+ }