zcode-pro 1.4.0 → 1.5.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/README.en.md +7 -6
- package/README.md +11 -10
- package/dist/inject.js +446 -108
- package/package.json +1 -1
- package/src/host/helper.mjs +15 -6
- package/src/inject/core.js +70 -48
- package/src/inject/features/composer-provider.js +112 -0
- package/src/inject/features/settings-dialog.js +73 -31
- package/src/inject/features/table-copy.js +148 -0
- package/src/inject/features/theme.js +25 -13
- package/src/inject/index.js +4 -0
- package/src/inject/ui.js +19 -3
|
@@ -244,8 +244,17 @@ export function openSettingsDialog() {
|
|
|
244
244
|
h('span', { class: 'text-foreground-subtle/70' }, `${health.version} · ${HELPER_URL}`)
|
|
245
245
|
);
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
// 双列网格:功能开关行短小(名称+开关+折行描述),两列排布把 15+ 行压到
|
|
248
|
+
// 一屏内。列间加分隔线、行距拉开(gap-y-1),格内不留底 padding——
|
|
249
|
+
// 悬停底色由行自身圆角承担,避免一格一格的补丁感
|
|
250
|
+
const rows = h('div', { class: 'grid grid-cols-2 gap-x-0 gap-y-1 rounded-xl border border-border p-2 divide-x divide-border' });
|
|
248
251
|
const pluginsStatusLine = h('span', { class: 'max-w-40 truncate text-ui-xs/relaxed text-foreground-subtle' }, '…');
|
|
252
|
+
// 状态文字写入统一入口:文本与悬停 title 一起更新(描述位被动态内容替换后
|
|
253
|
+
// 仍保持悬停全文约定)
|
|
254
|
+
const setPluginsStatus = (text) => {
|
|
255
|
+
setPluginsStatus(text);
|
|
256
|
+
pluginsStatusLine.setAttribute('title', text);
|
|
257
|
+
};
|
|
249
258
|
const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
|
|
250
259
|
let pluginsUpdating = false;
|
|
251
260
|
const runPluginsUpdate = async () => {
|
|
@@ -253,18 +262,18 @@ export function openSettingsDialog() {
|
|
|
253
262
|
if (pluginsUpdating) return;
|
|
254
263
|
pluginsUpdating = true;
|
|
255
264
|
pluginsBtn.disabled = true;
|
|
256
|
-
|
|
265
|
+
setPluginsStatus(L.pluginsUpdating);
|
|
257
266
|
const res = await rpc('/plugins/update', { method: 'POST', body: {} });
|
|
258
267
|
pluginsUpdating = false;
|
|
259
268
|
pluginsBtn.disabled = false;
|
|
260
269
|
if (!res.ok) {
|
|
261
|
-
|
|
270
|
+
setPluginsStatus('');
|
|
262
271
|
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
263
272
|
return;
|
|
264
273
|
}
|
|
265
|
-
|
|
274
|
+
setPluginsStatus(res.updated > 0
|
|
266
275
|
? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
|
|
267
|
-
: L.pluginsUpToDate;
|
|
276
|
+
: L.pluginsUpToDate);
|
|
268
277
|
showToast(pluginsStatusLine.textContent, 'success');
|
|
269
278
|
};
|
|
270
279
|
void (async () => {
|
|
@@ -272,15 +281,15 @@ export function openSettingsDialog() {
|
|
|
272
281
|
// 状态行如实显示过程与结果;开关关闭时只检查并显示“N 个可更新”
|
|
273
282
|
const cfg = await getConfig(true).catch(() => null);
|
|
274
283
|
const res = await rpc('/plugins/status');
|
|
275
|
-
if (!res.ok) {
|
|
284
|
+
if (!res.ok) { setPluginsStatus(''); return; }
|
|
276
285
|
const n = (res.updates || []).length;
|
|
277
286
|
if (cfg && (cfg.features || {}).autoUpdatePlugins === true && n > 0) {
|
|
278
287
|
void runPluginsUpdate();
|
|
279
288
|
return;
|
|
280
289
|
}
|
|
281
|
-
|
|
290
|
+
setPluginsStatus(n > 0
|
|
282
291
|
? L.pluginsUpdatesFound.replaceAll('{n}', String(n))
|
|
283
|
-
: L.pluginsUpToDate;
|
|
292
|
+
: L.pluginsUpToDate);
|
|
284
293
|
})();
|
|
285
294
|
|
|
286
295
|
// 会话快捷切换:入口键位(“快捷键”标签页可改);此处定义在 refreshRows 之前,
|
|
@@ -360,6 +369,16 @@ export function openSettingsDialog() {
|
|
|
360
369
|
if (await setFeature('imageCopy', next)) f.imageCopy = next;
|
|
361
370
|
refreshRows();
|
|
362
371
|
}),
|
|
372
|
+
settingRow(L.featureTableCopy, L.featureTableCopyDesc, f.tableCopy !== false, async () => {
|
|
373
|
+
const next = !(f.tableCopy !== false);
|
|
374
|
+
if (await setFeature('tableCopy', next)) f.tableCopy = next;
|
|
375
|
+
refreshRows();
|
|
376
|
+
}),
|
|
377
|
+
settingRow(L.featureComposerProvider, L.featureComposerProviderDesc, f.composerProvider !== false, async () => {
|
|
378
|
+
const next = !(f.composerProvider !== false);
|
|
379
|
+
if (await setFeature('composerProvider', next)) f.composerProvider = next;
|
|
380
|
+
refreshRows();
|
|
381
|
+
}),
|
|
363
382
|
settingRow(L.featureSandboxReclaim, L.featureSandboxReclaimDesc, f.sandboxReclaim !== false, async () => {
|
|
364
383
|
const next = !(f.sandboxReclaim !== false);
|
|
365
384
|
if (await setFeature('sandboxReclaim', next)) f.sandboxReclaim = next;
|
|
@@ -381,7 +400,7 @@ export function openSettingsDialog() {
|
|
|
381
400
|
refreshRows();
|
|
382
401
|
}),
|
|
383
402
|
(() => {
|
|
384
|
-
// 标题 = “自动更新 duanluan/zcode-plugins
|
|
403
|
+
// 标题 = “自动更新 duanluan/zcode-plugins”,仓库名可点击跳转仓库
|
|
385
404
|
const repoLink = h('span', {
|
|
386
405
|
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
387
406
|
title: PLUGINS_REPO_URL,
|
|
@@ -392,17 +411,19 @@ export function openSettingsDialog() {
|
|
|
392
411
|
if (await setFeature('autoUpdatePlugins', next)) f.autoUpdatePlugins = next;
|
|
393
412
|
refreshRows();
|
|
394
413
|
}, h('div', { class: 'flex items-center gap-2' }, pluginsBtn));
|
|
395
|
-
// 标题行:自动更新
|
|
414
|
+
// 标题行:自动更新 <链接>;描述位放状态字(检查结果/更新进度)
|
|
396
415
|
const nameEl = row.querySelector('.text-ui-sm.font-medium');
|
|
397
416
|
if (nameEl) {
|
|
398
417
|
nameEl.replaceChildren(
|
|
399
418
|
document.createTextNode(L.featureAutoUpdatePlugins + ' '),
|
|
400
419
|
repoLink,
|
|
401
|
-
document.createTextNode(' 插件'),
|
|
402
420
|
);
|
|
403
421
|
}
|
|
404
422
|
const descEl = row.querySelector('[data-zcodepro-row-desc]');
|
|
405
|
-
if (descEl)
|
|
423
|
+
if (descEl) {
|
|
424
|
+
descEl.replaceChildren(pluginsStatusLine);
|
|
425
|
+
descEl.setAttribute('title', descEl.textContent.trim());
|
|
426
|
+
}
|
|
406
427
|
return row;
|
|
407
428
|
})(),
|
|
408
429
|
);
|
|
@@ -560,12 +581,13 @@ export function openSettingsDialog() {
|
|
|
560
581
|
const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
561
582
|
const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
562
583
|
const panes = { features: paneFeatures, shortcuts: paneShortcuts, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
584
|
+
// 一级 tab 顺序:功能 → 外观 → 行为 → 性能 → 快捷键 → 代理/同步/插件页
|
|
563
585
|
const tabDefs = [
|
|
564
586
|
['features', L.tabFeatures],
|
|
565
|
-
['shortcuts', L.tabShortcuts],
|
|
566
|
-
['agents', L.tabBehavior],
|
|
567
587
|
['styles', L.tabStyles],
|
|
588
|
+
['agents', L.tabBehavior],
|
|
568
589
|
['perf', L.tabPerformance],
|
|
590
|
+
['shortcuts', L.tabShortcuts],
|
|
569
591
|
['proxy', L.tabProxy],
|
|
570
592
|
['sync', L.tabSync],
|
|
571
593
|
['vision', L.tabVision],
|
|
@@ -869,15 +891,20 @@ export function openSettingsDialog() {
|
|
|
869
891
|
// —— “颜色”子页(paneStylesSub3):预设皮肤卡片 + 自定义主题编辑 ——
|
|
870
892
|
// —— “背景图”子页(paneStylesSub4):无背景 / 图库 / 文件夹轮换 / 网址 ——
|
|
871
893
|
let themeCfg = structuredClone(config.theme || {});
|
|
894
|
+
const numOrBg = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
872
895
|
themeCfg.skin = themeCfg.skin || 'default';
|
|
873
896
|
themeCfg.custom = {
|
|
874
897
|
light: (themeCfg.custom && themeCfg.custom.light) || {},
|
|
875
898
|
dark: (themeCfg.custom && themeCfg.custom.dark) || {},
|
|
876
899
|
};
|
|
877
900
|
themeCfg.bg = {
|
|
878
|
-
mode: 'none', file: '', folder: '', folderRandom: true, url: '', intervalMin: 0, fit: 'cover', visibility: 0.8,
|
|
879
901
|
...(themeCfg.bg || {}),
|
|
902
|
+
mode: themeCfg.bg?.mode || 'none',
|
|
903
|
+
maskOpacity: numOrBg(themeCfg.bg?.maskOpacity, 0.82),
|
|
904
|
+
blurPx: numOrBg(themeCfg.bg?.blurPx, 0),
|
|
905
|
+
panelOpacity: numOrBg(themeCfg.bg?.panelOpacity, 0.3),
|
|
880
906
|
};
|
|
907
|
+
// file/folder/url/fit/folderRandom/intervalMin/regions 由 helper 清洗保证形态,原样保留
|
|
881
908
|
themeCfg.bg.regions = (themeCfg.bg.regions && typeof themeCfg.bg.regions === 'object') ? themeCfg.bg.regions : {};
|
|
882
909
|
let bgFiles = [];
|
|
883
910
|
let bgLoading = false;
|
|
@@ -1061,19 +1088,35 @@ export function openSettingsDialog() {
|
|
|
1061
1088
|
valueCtrl || h('span', { class: 'text-ui-xs text-foreground-subtle' }, ''));
|
|
1062
1089
|
};
|
|
1063
1090
|
const renderThemeBg = () => {
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1091
|
+
// 三个调节参数(滑杆行构造器:label + 范围 + 单位 + 提示 + 防抖保存)
|
|
1092
|
+
const sliderRow = (label, title, min, max, step, val, unit, commit, hint) => {
|
|
1093
|
+
// 标签列固定宽(容最长标签六字;内联样式——应用的 Tailwind 按需编译
|
|
1094
|
+
// 不包含 w-[72px] 这类任意值类):多行滑杆的轨道起点对齐同一竖线
|
|
1095
|
+
const lab = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs font-medium text-foreground-subtle', title, style: 'width:72px' }, label);
|
|
1096
|
+
const valLabel = h('span', { class: 'w-12 text-right text-ui-xs tabular-nums text-foreground-subtle' }, val + unit);
|
|
1097
|
+
const range = h('input', {
|
|
1098
|
+
type: 'range', min: String(min), max: String(max), step: String(step), value: String(val),
|
|
1099
|
+
class: 'zcodepro-range', 'aria-label': label, style: 'flex:1',
|
|
1100
|
+
});
|
|
1101
|
+
let timer = null;
|
|
1102
|
+
range.addEventListener('input', () => {
|
|
1103
|
+
const v = Number(range.value);
|
|
1104
|
+
valLabel.textContent = v + unit;
|
|
1105
|
+
clearTimeout(timer);
|
|
1106
|
+
timer = setTimeout(() => commit(v), 300);
|
|
1107
|
+
});
|
|
1108
|
+
return h('div', { class: 'flex flex-col gap-0.5' },
|
|
1109
|
+
h('div', { class: 'flex items-center gap-3' }, lab, range, valLabel),
|
|
1110
|
+
hint ? h('span', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, hint) : null);
|
|
1111
|
+
};
|
|
1112
|
+
// 数值兜底:null(旧配置带入)与 undefined 都要落默认,?? 只兜 undefined
|
|
1113
|
+
const numOr = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
1114
|
+
const maskRow = () => sliderRow(L.themeBgMask, L.themeBgMaskHint, 0, 99, 1, Math.round(numOr(themeCfg.bg.maskOpacity, 0.82) * 100), '%',
|
|
1115
|
+
(v) => { void saveTheme({ bg: { maskOpacity: v / 100 } }); }, L.themeBgMaskHint2);
|
|
1116
|
+
const blurRow = () => sliderRow(L.themeBgBlur, '', 0, 24, 1, numOr(themeCfg.bg.blurPx, 0), 'px',
|
|
1117
|
+
(v) => { void saveTheme({ bg: { blurPx: v } }); }, '');
|
|
1118
|
+
const panelRow = () => sliderRow(L.themeBgPanel, '', 0, 100, 1, Math.round(numOr(themeCfg.bg.panelOpacity, 0.3) * 100), '%',
|
|
1119
|
+
(v) => { void saveTheme({ bg: { panelOpacity: v / 100 } }); }, L.themeBgPanelHint);
|
|
1077
1120
|
const modeBtn = (id, label) => h('button', {
|
|
1078
1121
|
type: 'button', class: 'zcodepro-tab',
|
|
1079
1122
|
'data-state': themeCfg.bg.mode === id ? 'active' : 'inactive',
|
|
@@ -1184,9 +1227,8 @@ export function openSettingsDialog() {
|
|
|
1184
1227
|
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.themeBgFit),
|
|
1185
1228
|
fitBtn('cover', L.themeBgFitCover), fitBtn('contain', L.themeBgFitContain),
|
|
1186
1229
|
fitBtn('fill', L.themeBgFitFill), fitBtn('tile', L.themeBgFitTile)),
|
|
1187
|
-
h('div', { class: 'mt-3 flex
|
|
1188
|
-
|
|
1189
|
-
visRange, visLabel),
|
|
1230
|
+
h('div', { class: 'mt-3 flex flex-col gap-2' },
|
|
1231
|
+
maskRow(), blurRow(), panelRow()),
|
|
1190
1232
|
bgInput,
|
|
1191
1233
|
);
|
|
1192
1234
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// 回答内容表格工具条的「复制表格」按钮:把表格以富文本(text/html)写入
|
|
2
|
+
// 剪贴板,可粘贴进 Excel / 富文本编辑器并被识别为真正的表格(分列成格)。
|
|
3
|
+
// 官方已有「复制 Markdown / 下载 CSV / 预览」,但没有富文本形态——手动框选
|
|
4
|
+
// 复制虽也是 text/html,但带上了应用给单元格的横线与主题背景色,粘贴出来
|
|
5
|
+
// 脏样式。这里克隆表格后剥掉全部内联样式与 class,只保留表格结构本身。
|
|
6
|
+
// text/plain 同步写 TSV(Tab 分列、换行分行):Excel / Numbers / 粘贴为纯
|
|
7
|
+
// 文本的场景都能正确分列;富文本编辑器取 text/html 得到干净表格。
|
|
8
|
+
import { ensureStyle, showToast } from '../ui.js';
|
|
9
|
+
import { t, getConfig } from '../core.js';
|
|
10
|
+
|
|
11
|
+
let installed = false;
|
|
12
|
+
let enabled = true; // helper features.tableCopy(默认开)
|
|
13
|
+
|
|
14
|
+
// 剥样式:克隆树上去掉 style/class/width 等表现属性,只留结构(含 rowspan/
|
|
15
|
+
// colspan——Excel 识别合并单元格)。thead/tbody 保留,th/td 语义也保留
|
|
16
|
+
function cleanTable(table) {
|
|
17
|
+
const clone = table.cloneNode(false);
|
|
18
|
+
const walk = (src, dst) => {
|
|
19
|
+
for (const node of src.childNodes) {
|
|
20
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
21
|
+
if (node.textContent.trim()) dst.append(document.createTextNode(node.textContent));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
25
|
+
const tag = node.tagName;
|
|
26
|
+
// 嵌套的行内标记(strong/em/code/a/span)保留文本与标签但不带任何属性
|
|
27
|
+
const el = document.createElement(tag.toLowerCase());
|
|
28
|
+
if (tag === 'TD' || tag === 'TH') {
|
|
29
|
+
// 合并单元格信息必须保留(Excel/编辑器据此铺格)
|
|
30
|
+
for (const a of ['colspan', 'rowspan']) {
|
|
31
|
+
const v = node.getAttribute(a);
|
|
32
|
+
if (v && v !== '1') el.setAttribute(a, v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
dst.append(el);
|
|
36
|
+
walk(node, el);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(table, clone);
|
|
40
|
+
return clone;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// TSV 转义:单元格内的 Tab/换行替换为空格(TSV 无法表达内嵌二者)
|
|
44
|
+
function tableToTsv(table) {
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const tr of table.rows) {
|
|
47
|
+
const cells = [];
|
|
48
|
+
for (const cell of tr.cells) {
|
|
49
|
+
cells.push((cell.textContent || '').replace(/[\t\r\n]+/g, ' ').trim());
|
|
50
|
+
}
|
|
51
|
+
rows.push(cells.join('\t'));
|
|
52
|
+
}
|
|
53
|
+
return rows.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function copyTable(table) {
|
|
57
|
+
const L = t();
|
|
58
|
+
const clean = cleanTable(table);
|
|
59
|
+
const html = '<meta charset="utf-8"><table>' + clean.innerHTML + '</table>';
|
|
60
|
+
const tsv = tableToTsv(table);
|
|
61
|
+
try {
|
|
62
|
+
if (typeof ClipboardItem !== 'undefined' && navigator.clipboard && navigator.clipboard.write) {
|
|
63
|
+
await navigator.clipboard.write([
|
|
64
|
+
new ClipboardItem({
|
|
65
|
+
'text/html': new Blob([html], { type: 'text/html' }),
|
|
66
|
+
'text/plain': new Blob([tsv], { type: 'text/plain' }),
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
showToast(L.tableCopied, 'success');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
} catch { /* 走下面的兜底 */ }
|
|
73
|
+
// 兜底:自建 Selection + execCommand('copy'),剪贴板由浏览器按 DOM 生成
|
|
74
|
+
// text/html(无样式)与 text/plain
|
|
75
|
+
try {
|
|
76
|
+
const holder = document.createElement('div');
|
|
77
|
+
holder.contentEditable = 'true';
|
|
78
|
+
holder.style.cssText = 'position:fixed;left:-9999px;top:0;';
|
|
79
|
+
holder.append(clean);
|
|
80
|
+
document.body.append(holder);
|
|
81
|
+
const range = document.createRange();
|
|
82
|
+
range.selectNodeContents(holder);
|
|
83
|
+
const sel = window.getSelection();
|
|
84
|
+
sel.removeAllRanges();
|
|
85
|
+
sel.addRange(range);
|
|
86
|
+
const ok = document.execCommand('copy');
|
|
87
|
+
sel.removeAllRanges();
|
|
88
|
+
holder.remove();
|
|
89
|
+
if (ok) { showToast(L.tableCopied, 'success'); return; }
|
|
90
|
+
} catch { /* ignore */ }
|
|
91
|
+
showToast(L.failed, 'error');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 与官方按钮同款的 ghost 图标按钮:lucide table 图标 + tooltip 用 aria-label
|
|
95
|
+
//(应用的 tooltip 组件由 React 挂载,注入按钮只能靠原生 title/aria——这里
|
|
96
|
+
// 用 aria-label 保持与邻居一致的可访问性,悬停提示用原生 title)
|
|
97
|
+
function makeButton(onClick) {
|
|
98
|
+
const L = t();
|
|
99
|
+
const btn = document.createElement('button');
|
|
100
|
+
btn.type = 'button';
|
|
101
|
+
btn.setAttribute('aria-label', L.tableCopyBtn);
|
|
102
|
+
btn.title = L.tableCopyBtn;
|
|
103
|
+
btn.className = 'group/button inline-flex items-center justify-center border border-transparent bg-clip-padding whitespace-nowrap transition-colors outline-none select-none text-foreground hover:bg-hover hover:text-foreground size-7 rounded-lg [&_svg:not([class*=\'size-\'])]:size-4 shrink-0';
|
|
104
|
+
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-table size-3.5" aria-hidden="true"><path d="M12 3v18"></path><path d="M3 9h18"></path><path d="M3 15h18"></path><path d="M5 21h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2"></path></svg>';
|
|
105
|
+
btn.addEventListener('click', (e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
onClick();
|
|
108
|
+
});
|
|
109
|
+
return btn;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function startTableCopy() {
|
|
113
|
+
if (installed || typeof document === 'undefined') return;
|
|
114
|
+
installed = true;
|
|
115
|
+
ensureStyle();
|
|
116
|
+
|
|
117
|
+
// 定位工具条对应的表格:工具条与表格在同一个包裹容器(.my-0.flex 容器,
|
|
118
|
+
// 官方结构)里;先在兄弟里找,找不到再向上两层内找
|
|
119
|
+
const tableOf = (bar) => {
|
|
120
|
+
let scope = bar.parentElement;
|
|
121
|
+
for (let i = 0; i < 3 && scope; i++) {
|
|
122
|
+
const table = scope.querySelector('table');
|
|
123
|
+
if (table) return table;
|
|
124
|
+
scope = scope.parentElement;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const enhance = (bar) => {
|
|
130
|
+
if (bar.dataset.zcodeproTableCopy) return;
|
|
131
|
+
bar.dataset.zcodeproTableCopy = '1';
|
|
132
|
+
const table = tableOf(bar);
|
|
133
|
+
if (!table) return; // 没有对应表格就没意义(工具条总在表格旁,防御而已)
|
|
134
|
+
bar.append(makeButton(() => { void copyTable(table); }));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const scan = () => {
|
|
138
|
+
for (const bar of document.querySelectorAll('[data-markdown-table-toolbar]')) enhance(bar);
|
|
139
|
+
};
|
|
140
|
+
scan();
|
|
141
|
+
// 会话滚动/新回答渲染都会新增表格:body 子树观察器兜底(现有各功能同款模式)
|
|
142
|
+
void getConfig().then((cfg) => { enabled = cfg.features ? cfg.features.tableCopy !== false : true; }).catch(() => {});
|
|
143
|
+
window.addEventListener('zcodepro:config-changed', () => {
|
|
144
|
+
void getConfig(true).then((cfg) => { enabled = cfg.features ? cfg.features.tableCopy !== false : true; }).catch(() => {});
|
|
145
|
+
});
|
|
146
|
+
const observer = new MutationObserver(() => { if (enabled) scan(); });
|
|
147
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
148
|
+
}
|
|
@@ -194,7 +194,7 @@ let skinStyleEl = null; // 皮肤 token 覆盖
|
|
|
194
194
|
let bgStyleEl = null; // html 背景层(图 + 遮罩)
|
|
195
195
|
let injectedKeys = new Set(); // 半透明化写进 root.style 的变量(重算前先撤)
|
|
196
196
|
let classObserver = null; // 明暗切换(root class 变化)→ 重算遮罩色与透明度
|
|
197
|
-
let currentTheme = { skin: 'default', custom: null, bg: { mode: 'none',
|
|
197
|
+
let currentTheme = { skin: 'default', custom: null, bg: { mode: 'none', maskOpacity: 0.82, blurPx: 0, panelOpacity: 0.3 } };
|
|
198
198
|
let lastModeSig = ''; // 观察器只对明暗模式变化重算:applySkin/applyBackground
|
|
199
199
|
// 自己也会改 root class(zcodepro-skin-*/zcodepro-bg-on),不带这条过滤会自触发循环
|
|
200
200
|
let bgTimer = null; // 文件夹轮换 / 网址刷新定时器(重应用前先清)
|
|
@@ -321,18 +321,23 @@ async function applyBackground(bg) {
|
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
// 三参数(对齐 codeg 语义):maskOpacity 遮罩不透明度(直接作为遮罩 α);
|
|
325
|
+
// blurPx 图片模糊(CSS filter,只糊图片层不糊遮罩);panelOpacity 面板
|
|
326
|
+
// 不透明度(侧栏/面板/标签栏表面混入底色的比例,独立于遮罩)
|
|
327
|
+
const num01 = (v, dflt) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : dflt);
|
|
328
|
+
const maskOpacity = num01(bg.maskOpacity, 0.82);
|
|
329
|
+
const alpha = Math.min(0.99, maskOpacity);
|
|
330
|
+
const blurPx = typeof bg.blurPx === 'number' && Number.isFinite(bg.blurPx)
|
|
331
|
+
? Math.min(24, Math.max(0, Math.round(bg.blurPx))) : 0;
|
|
332
|
+
const panelOpacity = num01(bg.panelOpacity, 0.3);
|
|
327
333
|
// 遮罩色跟随当前明暗的皮肤底色:在皮肤已应用、半透明化未写入时读解析色
|
|
328
334
|
const resolvedBg = resolvedColor('--color-background');
|
|
329
335
|
const mHex = resolvedBg.match(/^rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)/);
|
|
330
336
|
const rgb = mHex
|
|
331
337
|
? [Math.round(+mHex[1]), Math.round(+mHex[2]), Math.round(+mHex[3])].join(',')
|
|
332
338
|
: (isDarkMode() ? '13,13,13' : '248,248,248');
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
const panelMix = Math.min(100, Math.round(alpha * 100) + 10);
|
|
339
|
+
// 表面半透明化:面板不透明度直接用 panelOpacity(0=全透图,1=纯色)
|
|
340
|
+
const panelMix = Math.round(panelOpacity * 100);
|
|
336
341
|
if (panelMix < 100) {
|
|
337
342
|
for (const t of SURFACE_TOKENS) {
|
|
338
343
|
const v = resolvedColor(t);
|
|
@@ -342,7 +347,7 @@ async function applyBackground(bg) {
|
|
|
342
347
|
}
|
|
343
348
|
}
|
|
344
349
|
}
|
|
345
|
-
await applyRegionBackgrounds(bg, {
|
|
350
|
+
await applyRegionBackgrounds(bg, { rgb, alpha, fit: bg.fit, seq });
|
|
346
351
|
if (seq !== applySeq) return; // 分区文件夹列表等待期间配置又变了
|
|
347
352
|
if (!bgActive) return; // 只有分区背景:到此为止,html 层不设图
|
|
348
353
|
|
|
@@ -357,10 +362,17 @@ async function applyBackground(bg) {
|
|
|
357
362
|
tile: 'background-size:100% 100%,auto !important;background-repeat:no-repeat,repeat !important;',
|
|
358
363
|
};
|
|
359
364
|
const fitCss = FIT_CSS[bg.fit] || FIT_CSS.cover;
|
|
365
|
+
// 模糊只作用于背景层:图与遮罩画在 html::before(fixed 铺满视口、z 轴最底),
|
|
366
|
+
// filter 写在伪元素上——写在 html 自身会把整个界面子树一起糊掉。
|
|
367
|
+
// 模糊会把伪元素边缘拉开出现透明缝,按模糊半径外扩 inset 收边
|
|
368
|
+
const blurCss = blurPx > 0 ? `filter:blur(${blurPx}px);` : '';
|
|
369
|
+
const spread = blurPx > 0 ? Math.max(20, blurPx * 2) : 0;
|
|
360
370
|
const setImage = (image) => {
|
|
361
371
|
bgStyleEl.textContent =
|
|
362
|
-
`html.zcodepro-bg-on{background-color:rgb(${rgb}) !important;
|
|
363
|
-
|
|
372
|
+
`html.zcodepro-bg-on{background-color:rgb(${rgb}) !important;}` +
|
|
373
|
+
`html.zcodepro-bg-on::before{content:'' !important;position:fixed;inset:-${spread}px;z-index:-1;pointer-events:none;` +
|
|
374
|
+
`background-image:linear-gradient(rgba(${rgb},${alpha}),rgba(${rgb},${alpha})),${image} !important;` +
|
|
375
|
+
`${fitCss}background-position:center !important;${blurCss}}`;
|
|
364
376
|
root.classList.add('zcodepro-bg-on');
|
|
365
377
|
};
|
|
366
378
|
const helperUrl = liveBoot().helperUrl || HELPER_URL;
|
|
@@ -413,8 +425,8 @@ async function applyRegionBackgrounds(bg, pre) {
|
|
|
413
425
|
regionTimers = [];
|
|
414
426
|
const rules = new Map(); // region -> 规则文本(轮换时覆盖同分区旧规则,不无限追加)
|
|
415
427
|
const flush = () => { regionStyleEl.textContent = [...rules.values()].join(''); };
|
|
416
|
-
// pre.alpha =
|
|
417
|
-
const alpha = pre ? pre.alpha : 0.
|
|
428
|
+
// pre.alpha = 遮罩不透明度(直接配置值),与全局同值
|
|
429
|
+
const alpha = pre ? pre.alpha : 0.82;
|
|
418
430
|
const rgb = pre ? pre.rgb : '13,13,13';
|
|
419
431
|
const FIT_CSS = {
|
|
420
432
|
cover: '100% 100%,cover', contain: '100% 100%,contain', fill: '100% 100%,100% 100%', tile: '100% 100%,auto',
|
|
@@ -468,7 +480,7 @@ function applyAll(theme) {
|
|
|
468
480
|
|
|
469
481
|
export function startTheme() {
|
|
470
482
|
if (!document.head && !document.documentElement) return;
|
|
471
|
-
applyAll({ skin: 'default', custom: null, bg: { mode: 'none',
|
|
483
|
+
applyAll({ skin: 'default', custom: null, bg: { mode: 'none', maskOpacity: 0.82, blurPx: 0, panelOpacity: 0.3 } });
|
|
472
484
|
void getConfig().then((cfg) => applyAll(cfg.theme)).catch(() => { /* 读不到配置保持应用默认 */ });
|
|
473
485
|
window.addEventListener('zcodepro:config-changed', () => {
|
|
474
486
|
void getConfig(true).then((cfg) => applyAll(cfg.theme)).catch(() => { /* ignore */ });
|
package/src/inject/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { startToolbarIcons } from './features/toolbar-icons.js';
|
|
|
23
23
|
import { startPinnedCollapse } from './features/pinned-collapse.js';
|
|
24
24
|
import { startStyleAdjustments } from './features/styles.js';
|
|
25
25
|
import { startTheme } from './features/theme.js';
|
|
26
|
+
import { startTableCopy } from './features/table-copy.js';
|
|
27
|
+
import { startComposerProvider } from './features/composer-provider.js';
|
|
26
28
|
import { startPluginUpdateCheck } from './features/plugin-updates.js';
|
|
27
29
|
import { ensureStyle } from './ui.js';
|
|
28
30
|
|
|
@@ -72,6 +74,8 @@ import { ensureStyle } from './ui.js';
|
|
|
72
74
|
try { startPinnedCollapse(); } catch { /* ignore */ }
|
|
73
75
|
try { startStyleAdjustments(); } catch { /* ignore */ }
|
|
74
76
|
try { startTheme(); } catch { /* ignore */ }
|
|
77
|
+
try { startTableCopy(); } catch { /* ignore */ }
|
|
78
|
+
try { startComposerProvider(); } catch { /* ignore */ }
|
|
75
79
|
try { startSettingsEntry(); } catch { /* ignore */ }
|
|
76
80
|
// 插件更新检查慢(可能 git pull),异步进行,不阻塞其他功能
|
|
77
81
|
try { void startPluginUpdateCheck(); } catch { /* ignore */ }
|
package/src/inject/ui.js
CHANGED
|
@@ -26,8 +26,10 @@ export function showToast(text, kind = 'info') {
|
|
|
26
26
|
// 注意:绝不能用 bg-surface 做面板底色——该 token 在应用里是 ~3% 不透明度的着色层(#0d0d0d08),
|
|
27
27
|
// 会导致整卡透明。关键视觉另在 ensureStyle 里以自有样式表兜底(ID 选择器 + 主题变量 + 字面量回退),
|
|
28
28
|
// 应用 Tailwind 类随版本更新变动时遮罩/面板仍能正确绘制。
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// 垂直方向 items-start + 卡片 margin-top:切 tab 内容高度变化时卡片顶边不动
|
|
30
|
+
// (此前 items-center 上下双向伸缩,顶部随内容高度跳动,顶部 tab 没法稳着点)
|
|
31
|
+
const overlayClass = 'fixed inset-0 isolate z-50 flex items-start justify-center bg-black/60 supports-backdrop-filter:backdrop-blur-xs duration-100 p-4 platform-linux-desktop:top-12';
|
|
32
|
+
const overlayClassBare = 'fixed inset-0 isolate z-50 flex items-start justify-center duration-100 p-4 platform-linux-desktop:top-12';
|
|
31
33
|
const contentClass = 'w-full sm:max-w-md rounded-2xl border-none bg-popover/98 p-5 text-ui-base/relaxed text-foreground ring-border shadow-2xl';
|
|
32
34
|
|
|
33
35
|
// overlay: 'dim'(默认,半透明遮罩)| 'none'(透明遮罩)——设置弹窗用 'none',方便边调样式边看会话。
|
|
@@ -59,6 +61,7 @@ export function openDialog({ title, description, onMount, onClose, width = 'sm:m
|
|
|
59
61
|
role: 'dialog',
|
|
60
62
|
'aria-modal': 'true',
|
|
61
63
|
class: contentClass.replace('sm:max-w-md', width) + ' zcodepro-card',
|
|
64
|
+
style: 'margin-top:12vh;max-height:calc(88vh - 3rem);overflow-y:auto;',
|
|
62
65
|
});
|
|
63
66
|
const overlayEl = h('div', {
|
|
64
67
|
'data-overlay': overlay,
|
|
@@ -358,7 +361,7 @@ export function settingRow(name, desc, checked, onToggle, extra) {
|
|
|
358
361
|
},
|
|
359
362
|
h('div', { class: 'min-w-0 flex-1' },
|
|
360
363
|
h('div', { class: 'text-ui-sm font-medium text-foreground' }, name),
|
|
361
|
-
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle', 'data-zcodepro-row-desc': '' }, desc ?? '')),
|
|
364
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle zcodepro-row-desc', 'data-zcodepro-row-desc': '', title: desc ?? '' }, desc ?? '')),
|
|
362
365
|
...(extraEl ? [extraEl] : []),
|
|
363
366
|
h('button', {
|
|
364
367
|
type: 'button',
|
|
@@ -400,6 +403,14 @@ export function ensureStyle() {
|
|
|
400
403
|
/* 滚到底不再把滚动传给弹窗后面的页面 */
|
|
401
404
|
overscroll-behavior: contain;
|
|
402
405
|
}
|
|
406
|
+
/* 双列网格(功能页)里的行描述最多两行省略:长描述撑出的参差行高是
|
|
407
|
+
网格观感差的根源;单列场景(其他面板)不受影响 */
|
|
408
|
+
.grid.grid-cols-2 > div > div > .zcodepro-row-desc {
|
|
409
|
+
display: -webkit-box;
|
|
410
|
+
-webkit-line-clamp: 2;
|
|
411
|
+
-webkit-box-orient: vertical;
|
|
412
|
+
overflow: hidden;
|
|
413
|
+
}
|
|
403
414
|
/* 开关(设置弹窗):几何固定写入,颜色随主题变量 */
|
|
404
415
|
[data-zcodepro-switch] {
|
|
405
416
|
position: relative;
|
|
@@ -566,6 +577,8 @@ export function ensureStyle() {
|
|
|
566
577
|
-webkit-appearance: none;
|
|
567
578
|
appearance: none;
|
|
568
579
|
width: 100%;
|
|
580
|
+
/* 不给 padding:padding 会缩小 thumb 的可滑区间(拖不到最左/最右)。
|
|
581
|
+
轨道起点对齐由弹窗里的固定标签列宽负责 */
|
|
569
582
|
height: 4px;
|
|
570
583
|
border-radius: 9999px;
|
|
571
584
|
background: color-mix(in oklab, var(--color-foreground, #888) 18%, transparent);
|
|
@@ -694,6 +707,9 @@ export function ensureStyle() {
|
|
|
694
707
|
height: 12px;
|
|
695
708
|
flex-shrink: 0;
|
|
696
709
|
opacity: 0.6;
|
|
710
|
+
/* 11px 字体的视觉中心(x-height 中点)高于行盒几何中心约 1px(下伸部
|
|
711
|
+
预留),几何居中的图标看起来偏下——整体上移 1px 对齐视觉中心 */
|
|
712
|
+
transform: translateY(-1px);
|
|
697
713
|
}
|
|
698
714
|
.zcodepro-statusbar b { font-weight: 600; color: var(--color-foreground-subtle, #666); }
|
|
699
715
|
.zcodepro-statusbar i {
|