@changfenhuang/dsh-genui 0.11.4 → 0.12.0
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/CHANGELOG.md +28 -0
- package/README.md +12 -4
- package/README.zh-CN.md +12 -4
- package/SKILL.md +2 -2
- package/lib/assets/mermaid.js +325 -325
- package/lib/assets/standalone-runtime.js +9 -9
- package/lib/client.js +30 -30
- package/lib/index.js +101 -14
- package/lib/types/client/dom-fence.d.ts +2 -1
- package/lib/types/client/file-address.d.ts +4 -0
- package/lib/types/client/file-link-context.d.ts +41 -0
- package/lib/types/client/file-link.d.ts +17 -0
- package/lib/types/client/i18n/en.d.ts +5 -1
- package/lib/types/client/inline.d.ts +1 -1
- package/lib/types/client/mermaid-colors.d.ts +2 -0
- package/lib/types/client/panel.d.ts +6 -1
- package/lib/types/plugin/fence-feedback.d.ts +6 -2
- package/lib/types/plugin/index.d.ts +4 -0
- package/lib/types/shared/fence-repair.d.ts +1 -0
- package/package.json +1 -12
- package/src/client/GenuiBlock.module.css +35 -3
- package/src/client/artifact/ArtifactExport.module.css +34 -3
- package/src/client/artifact/ExportableGenuiBlock.tsx +23 -2
- package/src/client/blocks/charts.tsx +22 -6
- package/src/client/blocks/render-node.tsx +10 -7
- package/src/client/dom-fence.tsx +97 -30
- package/src/client/file-address.ts +37 -0
- package/src/client/file-link-context.tsx +114 -0
- package/src/client/file-link.ts +46 -0
- package/src/client/guard.ts +5 -2
- package/src/client/i18n/en.ts +5 -1
- package/src/client/i18n/zh.ts +5 -1
- package/src/client/index.tsx +23 -5
- package/src/client/inline.ts +17 -6
- package/src/client/mermaid-colors.ts +39 -0
- package/src/client/mermaid-core.ts +7 -3
- package/src/client/panel.tsx +18 -5
- package/src/plugin/fence-feedback.ts +17 -5
- package/src/plugin/index.ts +14 -4
- package/src/plugin/tool.ts +8 -2
- package/src/shared/fence-repair.ts +36 -0
- package/tsdown.config.ts +3 -7
- package/lib/invariant.js +0 -19
- package/lib/types/plugin/invariant.d.ts +0 -15
- package/src/plugin/invariant.ts +0 -30
package/lib/index.js
CHANGED
|
@@ -1547,6 +1547,46 @@ function opt(key, value) {
|
|
|
1547
1547
|
return value === void 0 ? {} : { [key]: value };
|
|
1548
1548
|
}
|
|
1549
1549
|
//#endregion
|
|
1550
|
+
//#region src/client/file-link.ts
|
|
1551
|
+
/** Decode a local file destination and its optional first line fragment. */
|
|
1552
|
+
function parseFileLink(value) {
|
|
1553
|
+
if (value.length > 2048) return void 0;
|
|
1554
|
+
const hash = value.indexOf("#");
|
|
1555
|
+
const destination = hash < 0 ? value : value.slice(0, hash);
|
|
1556
|
+
if (destination.includes("?")) return void 0;
|
|
1557
|
+
let path;
|
|
1558
|
+
try {
|
|
1559
|
+
path = decodeURIComponent(destination);
|
|
1560
|
+
} catch {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (path === "" || /[\u0000-\u001f\u007f]/.test(path) || /^[\\/]{2}/.test(path) || /^[a-z][a-z\d+.-]*:/i.test(path) && !/^[a-z]:[\\/]/i.test(path)) return void 0;
|
|
1564
|
+
if (hash < 0) return { path };
|
|
1565
|
+
const match = /^L([1-9]\d*)(?:-L([1-9]\d*))?$/.exec(value.slice(hash + 1));
|
|
1566
|
+
if (match === null) return void 0;
|
|
1567
|
+
const line = Number(match[1]);
|
|
1568
|
+
const end = match[2] === void 0 ? line : Number(match[2]);
|
|
1569
|
+
if (!Number.isSafeInteger(line) || !Number.isSafeInteger(end) || end < line) return void 0;
|
|
1570
|
+
return {
|
|
1571
|
+
path,
|
|
1572
|
+
line
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
/** Classify a link destination while preserving the existing external URL policy. */
|
|
1576
|
+
function classifyLinkTarget(value) {
|
|
1577
|
+
const external = safeHref(value);
|
|
1578
|
+
if (external !== void 0) return {
|
|
1579
|
+
type: "external",
|
|
1580
|
+
href: external
|
|
1581
|
+
};
|
|
1582
|
+
if (typeof value !== "string") return { type: "invalid" };
|
|
1583
|
+
const file = parseFileLink(value.trim());
|
|
1584
|
+
return file === void 0 ? { type: "invalid" } : {
|
|
1585
|
+
type: "file",
|
|
1586
|
+
file
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
//#endregion
|
|
1550
1590
|
//#region src/client/guard.ts
|
|
1551
1591
|
function fieldKindMatches(value, kind) {
|
|
1552
1592
|
switch (kind) {
|
|
@@ -1791,10 +1831,11 @@ function repairNodeFields(value, ctx, depth) {
|
|
|
1791
1831
|
case "link": {
|
|
1792
1832
|
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
1793
1833
|
if (label === void 0) return null;
|
|
1834
|
+
const target = classifyLinkTarget(v.href);
|
|
1794
1835
|
return {
|
|
1795
1836
|
type: "link",
|
|
1796
1837
|
label,
|
|
1797
|
-
...opt("href",
|
|
1838
|
+
...opt("href", target.type === "external" ? target.href : target.type === "file" ? String(v.href).trim() : void 0)
|
|
1798
1839
|
};
|
|
1799
1840
|
}
|
|
1800
1841
|
case "image": {
|
|
@@ -3270,6 +3311,7 @@ function validateNode(value, depth, at, errors, walk) {
|
|
|
3270
3311
|
* with ASCII quotes (e.g. `对"别名路径"判定失败`), which makes JSON.parse
|
|
3271
3312
|
* fail near that quote with "Expected ',' or ']'...".
|
|
3272
3313
|
* 2. Trailing commas before `}` / `]` or at end of input.
|
|
3314
|
+
* 3. Valueless object keys immediately before a complete key-and-colon pair.
|
|
3273
3315
|
*
|
|
3274
3316
|
* A shared grammar-aware scan distinguishes object keys from values and
|
|
3275
3317
|
* checks the continuation after a potential string terminator. Ambiguous
|
|
@@ -3292,6 +3334,33 @@ const MAX_QUOTE_LOOKAHEAD = 4096;
|
|
|
3292
3334
|
function isJsonSpace(ch) {
|
|
3293
3335
|
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
3294
3336
|
}
|
|
3337
|
+
/** Skip only complete, valueless object keys followed by a valued member. */
|
|
3338
|
+
function skipValuelessKeys(raw, start, limit = raw.length) {
|
|
3339
|
+
let cursor = start;
|
|
3340
|
+
let repairs = 0;
|
|
3341
|
+
while (cursor < limit && raw[cursor] === "\"") {
|
|
3342
|
+
const keyStart = cursor++;
|
|
3343
|
+
while (cursor < limit && raw[cursor] !== "\"") if (raw[cursor] === "\\") cursor += 2;
|
|
3344
|
+
else cursor++;
|
|
3345
|
+
if (cursor >= limit) return null;
|
|
3346
|
+
cursor++;
|
|
3347
|
+
try {
|
|
3348
|
+
JSON.parse(raw.slice(keyStart, cursor));
|
|
3349
|
+
} catch {
|
|
3350
|
+
return null;
|
|
3351
|
+
}
|
|
3352
|
+
while (cursor < limit && isJsonSpace(raw[cursor])) cursor++;
|
|
3353
|
+
if (raw[cursor] === ":") return repairs > 0 ? {
|
|
3354
|
+
start: keyStart,
|
|
3355
|
+
repairs
|
|
3356
|
+
} : null;
|
|
3357
|
+
if (raw[cursor] !== ",") return null;
|
|
3358
|
+
repairs++;
|
|
3359
|
+
cursor++;
|
|
3360
|
+
while (cursor < limit && isJsonSpace(raw[cursor])) cursor++;
|
|
3361
|
+
}
|
|
3362
|
+
return null;
|
|
3363
|
+
}
|
|
3295
3364
|
/**
|
|
3296
3365
|
* Quote closure is contextual: only keys may be followed by `:`, and a comma
|
|
3297
3366
|
* must introduce the next member of the enclosing object/array. Walk closing
|
|
@@ -3330,6 +3399,8 @@ function quoteCanClose(raw, index, key, scopes, complete) {
|
|
|
3330
3399
|
if (raw[cursor] === scope.closer) return true;
|
|
3331
3400
|
if (scope.closer === "]") return /["[{tfn\-0-9]/.test(raw[cursor]);
|
|
3332
3401
|
if (raw[cursor] !== "\"") return false;
|
|
3402
|
+
const valueless = skipValuelessKeys(raw, cursor, limit);
|
|
3403
|
+
if (valueless !== null) cursor = valueless.start;
|
|
3333
3404
|
cursor++;
|
|
3334
3405
|
while (cursor < limit) {
|
|
3335
3406
|
if (raw[cursor] === "\\") {
|
|
@@ -3406,6 +3477,14 @@ function scanFenceCandidate(raw, complete, contentQuotes) {
|
|
|
3406
3477
|
}
|
|
3407
3478
|
if (ch === "\"") {
|
|
3408
3479
|
key = scopes[scopes.length - 1]?.expecting === "key";
|
|
3480
|
+
if (key) {
|
|
3481
|
+
const valueless = skipValuelessKeys(raw, i);
|
|
3482
|
+
if (valueless !== null) {
|
|
3483
|
+
repairs += valueless.repairs;
|
|
3484
|
+
i = valueless.start - 1;
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3409
3488
|
interiorQuote = false;
|
|
3410
3489
|
inString = true;
|
|
3411
3490
|
out += ch;
|
|
@@ -4132,6 +4211,7 @@ function formatProcessWarnings(processed) {
|
|
|
4132
4211
|
const canonicalPath = `${separator < 0 ? "" : warning.path.slice(0, separator + 1)}${warning.canonical}`;
|
|
4133
4212
|
return warning.message.includes("ignored") ? `warning=alias_ignored path=${warning.path} canonical=${canonicalPath}` : `warning=alias_normalized path=${warning.path} canonical=${canonicalPath}`;
|
|
4134
4213
|
}
|
|
4214
|
+
if (warning.kind === "unknown-field") return `warning=unknown_field path=${JSON.stringify(warning.path)} detail=${JSON.stringify(warning.message)}`;
|
|
4135
4215
|
return `warning=process detail=${JSON.stringify(warning.message)}`;
|
|
4136
4216
|
});
|
|
4137
4217
|
}
|
|
@@ -4226,7 +4306,7 @@ function createRenderUiTool() {
|
|
|
4226
4306
|
* before emitting — turning "render a red banner after the fact" into
|
|
4227
4307
|
* "verify before you send". Purely local: no LLM, no network, no DOM.
|
|
4228
4308
|
*/
|
|
4229
|
-
const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns a [genui-validation] protocol block with status, diagnostics, next action, and reply_language=conversation. When invalid JSON is repairable, repaired_json contains the fixed body; emit it only with next=emit_repaired_fence. A warning=block_markdown requires the indicated replacement node and another validation call.";
|
|
4309
|
+
const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns a [genui-validation] protocol block with status, diagnostics, next action, and reply_language=conversation. When invalid JSON is repairable, repaired_json contains the fixed body; emit it only with next=emit_repaired_fence. A warning=unknown_field requires fixing the indicated path and another validation call. A warning=block_markdown requires the indicated replacement node and another validation call.";
|
|
4230
4310
|
const VALIDATE_PARAMETERS = {
|
|
4231
4311
|
type: "object",
|
|
4232
4312
|
properties: { spec: {
|
|
@@ -4342,6 +4422,7 @@ function createValidateDshUiTool() {
|
|
|
4342
4422
|
if (processed.spec !== null && processed.errors.length === 0) {
|
|
4343
4423
|
const warnings = formatProcessWarnings(processed);
|
|
4344
4424
|
const inlineWarnings = formatInlineContentWarnings(processed.spec);
|
|
4425
|
+
const requiresCorrection = inlineWarnings.length > 0 || processed.warnings.some((warning) => warning.kind === "unknown-field");
|
|
4345
4426
|
return `${validationProtocol([
|
|
4346
4427
|
"status=invalid",
|
|
4347
4428
|
"error=invalid_json",
|
|
@@ -4351,7 +4432,7 @@ function createValidateDshUiTool() {
|
|
|
4351
4432
|
...inlineWarnings,
|
|
4352
4433
|
"repair=applied",
|
|
4353
4434
|
`repair_count=${repaired.repairs}`,
|
|
4354
|
-
|
|
4435
|
+
requiresCorrection ? "next=fix_and_revalidate" : "next=emit_repaired_fence"
|
|
4355
4436
|
])}\nrepaired_json:\n\`\`\`\n${repaired.text}\n\`\`\``;
|
|
4356
4437
|
}
|
|
4357
4438
|
}
|
|
@@ -4383,12 +4464,13 @@ function createValidateDshUiTool() {
|
|
|
4383
4464
|
}
|
|
4384
4465
|
const warnings = formatProcessWarnings(processed);
|
|
4385
4466
|
const inlineWarnings = formatInlineContentWarnings(processed.spec);
|
|
4467
|
+
const requiresCorrection = inlineWarnings.length > 0 || processed.warnings.some((warning) => warning.kind === "unknown-field");
|
|
4386
4468
|
return validationProtocol([
|
|
4387
4469
|
"status=valid",
|
|
4388
4470
|
`rendered=${processed.renderedCount}`,
|
|
4389
4471
|
...warnings,
|
|
4390
4472
|
...inlineWarnings,
|
|
4391
|
-
|
|
4473
|
+
requiresCorrection ? "next=fix_and_revalidate" : "next=emit_fence"
|
|
4392
4474
|
]);
|
|
4393
4475
|
},
|
|
4394
4476
|
presentCall() {
|
|
@@ -4631,9 +4713,9 @@ function fenceFailures(text) {
|
|
|
4631
4713
|
}
|
|
4632
4714
|
/** `null` when this fence renders; otherwise the reason it does not. */
|
|
4633
4715
|
function fenceFailureDetail(fence) {
|
|
4634
|
-
if (!fence.closed) return "error=unterminated_fence\nrequired=closing_fence";
|
|
4635
4716
|
const resolution = resolveFence(fence.raw, { settled: true });
|
|
4636
4717
|
if (resolution.spec !== null) return null;
|
|
4718
|
+
if (!fence.closed) return "error=unterminated_fence\nrequired=closing_fence";
|
|
4637
4719
|
if (resolution.processed !== null) return droppedNodeFailure(resolution.processed, resolution.value)?.join("\n") ?? ["error=invalid_spec", ...resolution.processed.errors.map((error) => `diagnostic=${JSON.stringify(error)}`)].join("\n");
|
|
4638
4720
|
return "error=invalid_json\nrepair=failed";
|
|
4639
4721
|
}
|
|
@@ -4710,7 +4792,7 @@ function planFenceFeedback(input) {
|
|
|
4710
4792
|
if (input.validatedThisTurn !== true || input.deliveredThisTurn === true) return null;
|
|
4711
4793
|
if (input.deliveryRemindedTurns?.has(input.turn) === true) return null;
|
|
4712
4794
|
return {
|
|
4713
|
-
text: missingBodyCorrectionText(input.turn, used + 1),
|
|
4795
|
+
text: missingBodyCorrectionText(input.turn, used + 1, input.renderUiToolEnabled ?? true),
|
|
4714
4796
|
fingerprints: [],
|
|
4715
4797
|
turn: input.turn,
|
|
4716
4798
|
kind: "delivery"
|
|
@@ -4722,10 +4804,11 @@ function planFenceFeedback(input) {
|
|
|
4722
4804
|
*
|
|
4723
4805
|
* @param turn - turn that reached the boundary.
|
|
4724
4806
|
* @param attempt - correction number within the shared turn budget.
|
|
4807
|
+
* @param renderUiToolEnabled - whether the correction may recommend `render_ui`.
|
|
4725
4808
|
* @returns the message text to steer into the running turn.
|
|
4726
4809
|
*/
|
|
4727
|
-
function missingBodyCorrectionText(turn, attempt = 1) {
|
|
4728
|
-
return `${`${MARKER_PREFIX}turn-${turn}]\n\n[genui-fence-repair]\nstatus=nothing_delivered\nfences=0\nnext=emit_fence_in_body\nrepeat_rendered_content=false\nreply_language=conversation\n\n`}本轮尚未产生正式回答,也没有通过支持的通道交付结果${attempt <= 1 ? "" : `(第 ${attempt} 次提醒)`}。请根据用户当前请求完成正式答复;需要 UI 时,在回答正文输出你最终选定的 dsh-ui
|
|
4810
|
+
function missingBodyCorrectionText(turn, attempt = 1, renderUiToolEnabled = true) {
|
|
4811
|
+
return `${`${MARKER_PREFIX}turn-${turn}]\n\n[genui-fence-repair]\nstatus=nothing_delivered\nfences=0\nnext=emit_fence_in_body\nrepeat_rendered_content=false\nreply_language=conversation\n\n`}本轮尚未产生正式回答,也没有通过支持的通道交付结果${attempt <= 1 ? "" : `(第 ${attempt} 次提醒)`}。请根据用户当前请求完成正式答复;需要 UI 时,在回答正文输出你最终选定的 dsh-ui 围栏${renderUiToolEnabled ? ",或明确调用 render_ui" : ""}。可以修改或放弃此前候选;不能完成时,请在正文说明原因。\n`;
|
|
4729
4812
|
}
|
|
4730
4813
|
/**
|
|
4731
4814
|
* 将 GenUI 回合中仅含 reasoning 的完整响应转换为宿主已有的可重试空响应错误。
|
|
@@ -4861,8 +4944,9 @@ function isFeedbackSource(source) {
|
|
|
4861
4944
|
*
|
|
4862
4945
|
* @param ctx - 宿主 Context。
|
|
4863
4946
|
* @param enabled - 是否启用同回合围栏修正。
|
|
4947
|
+
* @param renderUiToolEnabled - 是否已注册 `render_ui` 工具。
|
|
4864
4948
|
*/
|
|
4865
|
-
function installFenceFeedback(ctx, enabled) {
|
|
4949
|
+
function installFenceFeedback(ctx, enabled, renderUiToolEnabled = true) {
|
|
4866
4950
|
const sessions = /* @__PURE__ */ new Map();
|
|
4867
4951
|
const stateOf = (sessionId) => {
|
|
4868
4952
|
let state = sessions.get(sessionId);
|
|
@@ -4992,7 +5076,8 @@ function installFenceFeedback(ctx, enabled) {
|
|
|
4992
5076
|
deliveredThisTurn: state.deliveredThisTurn,
|
|
4993
5077
|
aborted: signal.aborted,
|
|
4994
5078
|
correctionsThisTurn: usedThisTurn,
|
|
4995
|
-
correctionsTurn: state.correctionsTurn
|
|
5079
|
+
correctionsTurn: state.correctionsTurn,
|
|
5080
|
+
renderUiToolEnabled
|
|
4996
5081
|
});
|
|
4997
5082
|
if (plan === null) return;
|
|
4998
5083
|
for (const fingerprint of plan.fingerprints) state.correctedSpec.add(fingerprint);
|
|
@@ -5064,6 +5149,7 @@ async function serveGenuiAsset(req, res) {
|
|
|
5064
5149
|
res.end();
|
|
5065
5150
|
}
|
|
5066
5151
|
}
|
|
5152
|
+
const RENDER_UI_TOOL_GUIDANCE = "- Tool channel: render_ui 工具把同一 spec 渲染为工具行卡片;围栏用于回答内联 UI。";
|
|
5067
5153
|
/** The fence language description injected into every assembled system prompt.
|
|
5068
5154
|
* Deliberately slim: the `genui` skill carries the full component→field
|
|
5069
5155
|
* mapping; this section keeps only the contract that must always be
|
|
@@ -5104,7 +5190,7 @@ Rules:
|
|
|
5104
5190
|
- Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;相同内容保留,新内容重置。
|
|
5105
5191
|
- 卷子模式: 每题一个 radio(group+answer+explanation)+ 一个 submit(groups 全列),本地判分。
|
|
5106
5192
|
- Secrets ban: 不索取密码、API Key、Token、恢复码;需要时拒绝并解释。
|
|
5107
|
-
|
|
5193
|
+
${RENDER_UI_TOOL_GUIDANCE}
|
|
5108
5194
|
- 围栏位置:\`dsh-ui\` 只写在**回答正文**;写在 reasoning/思考块里不渲染、用户看不到——思考里验证好 spec,正文再输出同一份。
|
|
5109
5195
|
- Panel: "panel":true 只渲染进会话面板 dock 并原地更新;"append":true 追加合并;上限 200 节点/200 次追加,满了发 replace 重建。面板来的 [genui-action] 只回一个 panel:true 围栏 + 至多一行 10 字内确认。`;
|
|
5110
5196
|
/**
|
|
@@ -5159,15 +5245,16 @@ function bundledSkillProvider() {
|
|
|
5159
5245
|
};
|
|
5160
5246
|
}
|
|
5161
5247
|
function apply(ctx, config) {
|
|
5248
|
+
const renderUiToolEnabled = config?.renderUiTool !== false;
|
|
5162
5249
|
ctx.systemPrompt.section({
|
|
5163
5250
|
name: "genui:fence",
|
|
5164
5251
|
order: ctx.systemPrompt.getSectionOrder("STRUCTURED_OUTPUT"),
|
|
5165
|
-
text: GENUI_SECTION_TEXT
|
|
5252
|
+
text: renderUiToolEnabled ? GENUI_SECTION_TEXT : GENUI_SECTION_TEXT.replace(`${RENDER_UI_TOOL_GUIDANCE}\n`, "")
|
|
5166
5253
|
});
|
|
5167
|
-
installFenceFeedback(ctx, config?.fenceFeedback !== false);
|
|
5254
|
+
installFenceFeedback(ctx, config?.fenceFeedback !== false, renderUiToolEnabled);
|
|
5168
5255
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
5169
5256
|
toolsCtx.effect(function* () {
|
|
5170
|
-
yield toolsCtx.tools.register(createRenderUiTool());
|
|
5257
|
+
if (renderUiToolEnabled) yield toolsCtx.tools.register(createRenderUiTool());
|
|
5171
5258
|
yield toolsCtx.tools.register(createValidateDshUiTool());
|
|
5172
5259
|
}, "dsh-genui: model tools");
|
|
5173
5260
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Root } from 'react-dom/client';
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
|
+
import { type FileLinkAvailability } from './file-link-context.tsx';
|
|
4
5
|
/** Override the React root factory (tests / tuning). */
|
|
5
6
|
export declare function setDomRootFactory(factory: (container: HTMLElement) => Root): void;
|
|
6
7
|
/**
|
|
@@ -27,4 +28,4 @@ export declare function hostFenceIndexOf(row: Element, block: Element): number;
|
|
|
27
28
|
* @param sendAction - plugin-owned relay: (sessionId, action, payload) → the
|
|
28
29
|
* scoped conversation send carrying the `[genui-action]` prompt.
|
|
29
30
|
*/
|
|
30
|
-
export declare function installDomFenceRenderer(ctx: Context, sendAction: (sessionId: SessionId, action: string, payload: Record<string, unknown>) => void): () => void;
|
|
31
|
+
export declare function installDomFenceRenderer(ctx: Context, sendAction: (sessionId: SessionId, action: string, payload: Record<string, unknown>) => void, fileLinkAvailability?: FileLinkAvailability): () => void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Build a session resource address while preserving encoded path segments. */
|
|
2
|
+
export declare function sessionFileAddress(sessionId: string, path: string): string;
|
|
3
|
+
/** Resolve workspace absolute paths to session-relative resource addresses. */
|
|
4
|
+
export declare function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
interface SidebarRightService {
|
|
3
|
+
openResource(address: string, options?: {
|
|
4
|
+
params: {
|
|
5
|
+
line: number;
|
|
6
|
+
};
|
|
7
|
+
}): void;
|
|
8
|
+
}
|
|
9
|
+
interface FileLinkHostContext {
|
|
10
|
+
get(name: 'sidebarRight', strict?: false): SidebarRightService | undefined;
|
|
11
|
+
sessions: {
|
|
12
|
+
list: {
|
|
13
|
+
getSnapshot(): unknown;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
interface FileLinkScope {
|
|
18
|
+
get(name: 'sidebarRight'): SidebarRightService | undefined;
|
|
19
|
+
effect(callback: () => void | (() => void), name?: string): unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface FileLinkCordisContext extends FileLinkHostContext {
|
|
22
|
+
inject(services: ['sidebarRight'], callback: (scope: FileLinkScope) => void): unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface FileLinkAvailability {
|
|
25
|
+
subscribe(listener: () => void): () => void;
|
|
26
|
+
getSnapshot(): boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Track an optional host service through arrival, removal, and plugin unload. */
|
|
29
|
+
export declare function createFileLinkAvailability(ctx: FileLinkCordisContext): FileLinkAvailability;
|
|
30
|
+
/** Render a file link as an accessible host navigation button when available. */
|
|
31
|
+
export declare function FileLink({ path, line, label, children }: {
|
|
32
|
+
path: string;
|
|
33
|
+
line?: number;
|
|
34
|
+
label: string;
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
}): import("react").DetailedReactHTMLElement<{
|
|
37
|
+
title: string;
|
|
38
|
+
}, HTMLElement>;
|
|
39
|
+
/** Add session-scoped file navigation to one fence render tree. */
|
|
40
|
+
export declare function withFileLinkContext(ctx: FileLinkHostContext, availability: FileLinkAvailability, sessionId: string | undefined, streaming: boolean, children: ReactNode): ReactNode;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ParsedFileLink {
|
|
2
|
+
path: string;
|
|
3
|
+
line?: number;
|
|
4
|
+
}
|
|
5
|
+
export type LinkTarget = {
|
|
6
|
+
type: 'external';
|
|
7
|
+
href: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'file';
|
|
10
|
+
file: ParsedFileLink;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'invalid';
|
|
13
|
+
};
|
|
14
|
+
/** Decode a local file destination and its optional first line fragment. */
|
|
15
|
+
export declare function parseFileLink(value: string): ParsedFileLink | undefined;
|
|
16
|
+
/** Classify a link destination while preserving the existing external URL policy. */
|
|
17
|
+
export declare function classifyLinkTarget(value: unknown): LinkTarget;
|
|
@@ -262,6 +262,10 @@ export declare const EN: {
|
|
|
262
262
|
readonly 'label.copyButtonTitle': "{action}; right-click for copy options";
|
|
263
263
|
readonly 'block.copyMarkdown': "Copy Markdown";
|
|
264
264
|
readonly 'block.copyCsv': "Copy CSV";
|
|
265
|
+
readonly 'block.table.expandDetails': "Expand details for {label}";
|
|
266
|
+
readonly 'block.table.collapseDetails': "Collapse details for {label}";
|
|
267
|
+
readonly 'block.table.expandGroup': "Expand group {label}";
|
|
268
|
+
readonly 'block.table.collapseGroup': "Collapse group {label}";
|
|
265
269
|
readonly 'block.total': "Total";
|
|
266
270
|
readonly 'block.filteredRows': "{visible} / {total} rows after filtering";
|
|
267
271
|
readonly 'block.filterMatch': "{shown} / {total} matched";
|
|
@@ -277,7 +281,7 @@ export declare const EN: {
|
|
|
277
281
|
readonly 'block.svgPreview': "Preview";
|
|
278
282
|
readonly 'block.svgSource': "Source";
|
|
279
283
|
readonly 'block.svgError': "SVG could not be displayed — showing the source instead";
|
|
280
|
-
readonly 'block.mermaidError': "
|
|
284
|
+
readonly 'block.mermaidError': "Diagram rendering failed — showing the source instead";
|
|
281
285
|
readonly 'block.mermaidLoading': "Rendering…";
|
|
282
286
|
readonly 'block.scene3dLoading': "Loading 3D scene…";
|
|
283
287
|
readonly 'block.scene3dError': "3D rendering failed";
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
3
|
export declare function hasInlineMarkup(text: string): boolean;
|
|
4
4
|
/** Render safe phrasing content, usable in headings, buttons and labels too. */
|
|
5
|
-
export declare function renderInline(text: string, allowLinks?: boolean, depth?: number): ReactNode;
|
|
5
|
+
export declare function renderInline(text: string, allowLinks?: boolean | 'file', depth?: number): ReactNode;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { type GenuiActionHandler } from './action-context.ts';
|
|
2
|
+
import { type FileLinkAvailability, type FileLinkCordisContext } from './file-link-context.tsx';
|
|
2
3
|
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
4
|
/** Injection face built per session in apply (scoped conversation send). */
|
|
4
5
|
export interface GenuiPanelInjected {
|
|
5
6
|
sessionId: string;
|
|
6
7
|
sendGenuiAction: GenuiActionHandler;
|
|
8
|
+
fileLinkContext?: {
|
|
9
|
+
ctx: FileLinkCordisContext;
|
|
10
|
+
availability: FileLinkAvailability;
|
|
11
|
+
};
|
|
7
12
|
/** Template center "try it": insert the template instruction into the
|
|
8
13
|
* current composer draft (standard conversation.input.for channel). */
|
|
9
14
|
insertTemplate: (text: string) => void;
|
|
@@ -15,4 +20,4 @@ export type GenuiPanelProps = PropsRuntime<'conversation.input.dock'> & GenuiPan
|
|
|
15
20
|
* default so the dock never steals the message flow's scroll room; the
|
|
16
21
|
* header always shows the current panel title.
|
|
17
22
|
*/
|
|
18
|
-
export declare function GenuiPanel({ sessionId, sendGenuiAction, insertTemplate }: GenuiPanelProps): import("react").JSX.Element | null;
|
|
23
|
+
export declare function GenuiPanel({ sessionId, sendGenuiAction, insertTemplate, fileLinkContext }: GenuiPanelProps): import("react").JSX.Element | null;
|
|
@@ -105,6 +105,8 @@ export interface FenceFeedbackPlanInput {
|
|
|
105
105
|
readonly correctionsThisTurn?: number | undefined;
|
|
106
106
|
/** Turn {@link correctionsThisTurn} counts (stale counts are ignored). */
|
|
107
107
|
readonly correctionsTurn?: number | undefined;
|
|
108
|
+
/** Whether the host has registered the `render_ui` tool. Defaults to enabled. */
|
|
109
|
+
readonly renderUiToolEnabled?: boolean | undefined;
|
|
108
110
|
}
|
|
109
111
|
/** A correction the caller must account for before steering. */
|
|
110
112
|
export interface FenceFeedbackPlan {
|
|
@@ -139,13 +141,15 @@ export declare function planFenceFeedback(input: FenceFeedbackPlanInput): FenceF
|
|
|
139
141
|
*
|
|
140
142
|
* @param turn - turn that reached the boundary.
|
|
141
143
|
* @param attempt - correction number within the shared turn budget.
|
|
144
|
+
* @param renderUiToolEnabled - whether the correction may recommend `render_ui`.
|
|
142
145
|
* @returns the message text to steer into the running turn.
|
|
143
146
|
*/
|
|
144
|
-
export declare function missingBodyCorrectionText(turn: number, attempt?: number): string;
|
|
147
|
+
export declare function missingBodyCorrectionText(turn: number, attempt?: number, renderUiToolEnabled?: boolean): string;
|
|
145
148
|
/**
|
|
146
149
|
* 注册 GenUI 回合跟踪、宿主重试判定和可选的围栏修正流程。
|
|
147
150
|
*
|
|
148
151
|
* @param ctx - 宿主 Context。
|
|
149
152
|
* @param enabled - 是否启用同回合围栏修正。
|
|
153
|
+
* @param renderUiToolEnabled - 是否已注册 `render_ui` 工具。
|
|
150
154
|
*/
|
|
151
|
-
export declare function installFenceFeedback(ctx: Context, enabled: boolean): void;
|
|
155
|
+
export declare function installFenceFeedback(ctx: Context, enabled: boolean, renderUiToolEnabled?: boolean): void;
|
|
@@ -31,5 +31,9 @@ export interface GenuiPluginConfig {
|
|
|
31
31
|
* 每回合和每个围栏正文最多请求一次,子代理不触发,每次修正请求会消耗模型步数。
|
|
32
32
|
*/
|
|
33
33
|
fenceFeedback?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* 是否注册 render_ui 模型工具。默认开启,设置为 false 时关闭;不影响 validate_dsh_ui 和 dsh-ui 围栏渲染。
|
|
36
|
+
*/
|
|
37
|
+
renderUiTool?: boolean;
|
|
34
38
|
}
|
|
35
39
|
export declare function apply(ctx: Context, config?: GenuiPluginConfig): void;
|
|
@@ -31,6 +31,7 @@ export declare function describeJsonFailure(raw: string): string | null;
|
|
|
31
31
|
* with ASCII quotes (e.g. `对"别名路径"判定失败`), which makes JSON.parse
|
|
32
32
|
* fail near that quote with "Expected ',' or ']'...".
|
|
33
33
|
* 2. Trailing commas before `}` / `]` or at end of input.
|
|
34
|
+
* 3. Valueless object keys immediately before a complete key-and-colon pair.
|
|
34
35
|
*
|
|
35
36
|
* A shared grammar-aware scan distinguishes object keys from values and
|
|
36
37
|
* checks the continuation after a potential string terminator. Ambiguous
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@changfenhuang/dsh-genui",
|
|
3
3
|
"description": "GenUI for DeepSeek Harness: interactive UI components rendered inline in assistant replies via the ```dsh-ui fence — layout, charts, plots, forms, quizzes, mermaid, 3D scenes, and an action event loop back to the model. Ships the fence-teaching host plugin, the browser renderer (client half), and the genui skill.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/plugin/index.d.ts",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"types": "./lib/types/plugin/index.d.ts",
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./invariant": {
|
|
14
|
-
"types": "./lib/types/plugin/invariant.d.ts",
|
|
15
|
-
"default": "./lib/invariant.js"
|
|
16
|
-
},
|
|
17
13
|
"./client": {
|
|
18
14
|
"types": "./lib/types/client/index.d.ts",
|
|
19
15
|
"default": "./lib/client.js"
|
|
@@ -29,12 +25,10 @@
|
|
|
29
25
|
},
|
|
30
26
|
"files": [
|
|
31
27
|
"lib/index.js",
|
|
32
|
-
"lib/invariant.js",
|
|
33
28
|
"lib/client.js",
|
|
34
29
|
"lib/assets/mermaid.js",
|
|
35
30
|
"lib/assets/three.js",
|
|
36
31
|
"lib/types/plugin/index.d.ts",
|
|
37
|
-
"lib/types/plugin/invariant.d.ts",
|
|
38
32
|
"lib/types/client/index.d.ts",
|
|
39
33
|
"SKILL.md",
|
|
40
34
|
"README.md",
|
|
@@ -86,7 +80,6 @@
|
|
|
86
80
|
"@deepseek-ai/dsh-client-ui-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
87
81
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
88
82
|
"@deepseek-ai/dsh-client-ui-tool": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
89
|
-
"@deepseek-ai/dsh-invariants": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
90
83
|
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
91
84
|
"@deepseek-ai/dsh-session": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
92
85
|
"@deepseek-ai/dsh-skill": "^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0",
|
|
@@ -122,7 +115,6 @@
|
|
|
122
115
|
"@deepseek-ai/dsh-deque": "0.1.2-rc.1",
|
|
123
116
|
"@deepseek-ai/dsh-file-reference": "0.1.2-rc.1",
|
|
124
117
|
"@deepseek-ai/dsh-home-paths": "0.1.2-rc.1",
|
|
125
|
-
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
|
126
118
|
"@deepseek-ai/dsh-jobs": "0.1.2-rc.1",
|
|
127
119
|
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
128
120
|
"@deepseek-ai/dsh-native-command": "0.1.2-rc.1",
|
|
@@ -226,9 +218,6 @@
|
|
|
226
218
|
"@deepseek-ai/dsh-client-ui-tool": {
|
|
227
219
|
"optional": true
|
|
228
220
|
},
|
|
229
|
-
"@deepseek-ai/dsh-invariants": {
|
|
230
|
-
"optional": true
|
|
231
|
-
},
|
|
232
221
|
"@deepseek-ai/dsh-llm": {
|
|
233
222
|
"optional": true
|
|
234
223
|
},
|
|
@@ -421,6 +421,9 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
421
421
|
}
|
|
422
422
|
.inlineLink { color: var(--dsl-g-accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
423
423
|
.inlineLink:hover { text-decoration-thickness: 2px; }
|
|
424
|
+
.inlineFileLink { padding: 0; border: 0; background: none; color: var(--dsl-g-accent); font: inherit; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
|
|
425
|
+
.inlineFileLink:hover { text-decoration-thickness: 2px; }
|
|
426
|
+
.inlineFileLink:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 2px; border-radius: 3px; }
|
|
424
427
|
|
|
425
428
|
/* ---------- badges / stat / progress ---------- */
|
|
426
429
|
|
|
@@ -651,6 +654,18 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
651
654
|
/* An inline code span keeps its own spacing too (a pasted snippet often lives
|
|
652
655
|
inside backticks), matching the block-level code surface. */
|
|
653
656
|
.inlineCode { white-space: pre-wrap; }
|
|
657
|
+
/* Table cells are single-line by default, so a chip inside them must not
|
|
658
|
+
re-open line breaking. Host shells render message bodies inside `.markdown`,
|
|
659
|
+
which makes every chip an atomic `inline-flex` box and inherits
|
|
660
|
+
`overflow-wrap: anywhere` into the cell; together with `pre-wrap` the chip's
|
|
661
|
+
automatic minimum size collapses, the column can then be sized below its
|
|
662
|
+
content, and the still-unbroken text is painted over the next column (#280).
|
|
663
|
+
Keep the cell's own line-breaking contract for every table section. */
|
|
664
|
+
.table > thead > tr > th:not(.tdCode):not(.tdMultiline) .inlineCode,
|
|
665
|
+
.table > tbody > tr:not(.detailRow) > td:not(.tdCode):not(.tdMultiline) .inlineCode,
|
|
666
|
+
.table > tfoot > tr > td:not(.tdCode):not(.tdMultiline) .inlineCode {
|
|
667
|
+
white-space: pre;
|
|
668
|
+
}
|
|
654
669
|
/* Rich-text owners must paint the real newlines kept by renderInline. Scope
|
|
655
670
|
this to prose/labels: table data keeps nowrap and code keeps pre-wrap. For
|
|
656
671
|
flex controls, target the label span so emphasis and plain text share one
|
|
@@ -1127,11 +1142,17 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
1127
1142
|
/* Master-detail rows (table.details): a chevron in the first cell opens a
|
|
1128
1143
|
full-width panel underneath, so a table can drill down without leaving the
|
|
1129
1144
|
page or opening a modal. */
|
|
1130
|
-
.detailCell { padding-left: var(--dsl-g-gap-sm); }
|
|
1145
|
+
.detailCell { position: relative; padding-left: var(--dsl-g-gap-sm); }
|
|
1131
1146
|
.detailToggle {
|
|
1147
|
+
position: relative;
|
|
1148
|
+
z-index: 1;
|
|
1132
1149
|
display: inline-flex;
|
|
1133
1150
|
align-items: center;
|
|
1134
|
-
|
|
1151
|
+
justify-content: center;
|
|
1152
|
+
vertical-align: middle;
|
|
1153
|
+
width: 24px;
|
|
1154
|
+
height: 24px;
|
|
1155
|
+
margin-right: 4px;
|
|
1135
1156
|
padding: 0;
|
|
1136
1157
|
border: none;
|
|
1137
1158
|
background: none;
|
|
@@ -1142,7 +1163,9 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
1142
1163
|
text-align: left;
|
|
1143
1164
|
}
|
|
1144
1165
|
.detailToggle:hover { color: var(--dsl-g-accent); }
|
|
1166
|
+
.detailCell:hover .detailToggle { color: var(--dsl-g-accent); }
|
|
1145
1167
|
.detailToggle:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 2px; border-radius: 4px; }
|
|
1168
|
+
.detailContent { position: relative; z-index: 1; vertical-align: middle; cursor: pointer; }
|
|
1146
1169
|
.detailChevron {
|
|
1147
1170
|
flex-shrink: 0;
|
|
1148
1171
|
font-size: 10px;
|
|
@@ -1182,10 +1205,17 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
1182
1205
|
.table tbody tr.groupRow:first-child td { box-shadow: none; padding-top: 10px; }
|
|
1183
1206
|
.table tbody tr.groupRow:hover { background: none; }
|
|
1184
1207
|
.table tbody tr.groupRow:nth-child(even) { background: none; }
|
|
1208
|
+
.groupRow td { position: relative; }
|
|
1185
1209
|
.groupToggle {
|
|
1210
|
+
position: relative;
|
|
1211
|
+
z-index: 1;
|
|
1186
1212
|
display: inline-flex;
|
|
1187
1213
|
align-items: center;
|
|
1188
|
-
|
|
1214
|
+
justify-content: center;
|
|
1215
|
+
vertical-align: middle;
|
|
1216
|
+
width: 24px;
|
|
1217
|
+
height: 24px;
|
|
1218
|
+
margin-right: 7px;
|
|
1189
1219
|
padding: 0;
|
|
1190
1220
|
border: none;
|
|
1191
1221
|
background: none;
|
|
@@ -1197,7 +1227,9 @@ img.mediaPlayer { aspect-ratio: 16 / 9; object-fit: cover; background: var(--dsl
|
|
|
1197
1227
|
cursor: pointer;
|
|
1198
1228
|
}
|
|
1199
1229
|
.groupToggle:hover { color: var(--dsl-g-accent); }
|
|
1230
|
+
.groupLabel:hover { color: var(--dsl-g-accent); }
|
|
1200
1231
|
.groupToggle:focus-visible { outline: 2px solid var(--dsl-g-accent); outline-offset: 2px; border-radius: 4px; }
|
|
1232
|
+
.groupLabel { position: relative; z-index: 1; display: inline-flex; vertical-align: middle; align-items: center; gap: 7px; cursor: pointer; }
|
|
1201
1233
|
.groupChevron { color: var(--dsw-alias-label-tertiary); font-size: 11px; width: 10px; }
|
|
1202
1234
|
.groupCount {
|
|
1203
1235
|
padding: 0 7px;
|
|
@@ -8,7 +8,37 @@
|
|
|
8
8
|
font: 12px/1.4 system-ui, sans-serif;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.
|
|
11
|
+
/* 导出入口:无边框图标按钮。形状照项目里的 chrome 图标按钮 .panelClose
|
|
12
|
+
(26px 命中区 / 6px 圆角 / 透明底 / 无边框)。 */
|
|
13
|
+
.trigger {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
width: 26px;
|
|
18
|
+
height: 26px;
|
|
19
|
+
padding: 0;
|
|
20
|
+
border: 0;
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
background: transparent;
|
|
23
|
+
color: var(--dsw-alias-label-tertiary);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* 悬浮沿用区块内表格行 / .tab / .accHead 那条配方(--dsl-g-hover):主题文字色
|
|
28
|
+
4% 淡洗 + 文字色升到 label-primary。不用 rgba(255,255,255,…) 与没带兜底的
|
|
29
|
+
--dsw-alias-fill-hover —— 浅色主题下两者都看不见。 */
|
|
30
|
+
.trigger:hover,
|
|
31
|
+
.trigger:focus-visible {
|
|
32
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent);
|
|
33
|
+
color: var(--dsw-alias-label-primary);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* 聚焦轮廓同 .panelClose。 */
|
|
37
|
+
.trigger:focus-visible {
|
|
38
|
+
outline: 1px solid var(--dsw-alias-state-business-primary, #4f8ef7);
|
|
39
|
+
outline-offset: 1px;
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
.item {
|
|
13
43
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
14
44
|
border-radius: 7px;
|
|
@@ -18,9 +48,10 @@
|
|
|
18
48
|
cursor: pointer;
|
|
19
49
|
}
|
|
20
50
|
|
|
21
|
-
|
|
51
|
+
/* 菜单项沿用宿主的 fill-hover;第三方主题可能没有定义该 token(例如浅色
|
|
52
|
+
dream-skin),此时回退到区块内那条 4% 文字色淡洗,悬浮不会没有反馈。 */
|
|
22
53
|
.item:hover {
|
|
23
|
-
background: var(--dsw-alias-fill-hover);
|
|
54
|
+
background: var(--dsw-alias-fill-hover, color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent));
|
|
24
55
|
color: var(--dsw-alias-label-primary);
|
|
25
56
|
}
|
|
26
57
|
|