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.
@@ -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
- const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
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
- pluginsStatusLine.textContent = L.pluginsUpdating;
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
- pluginsStatusLine.textContent = '';
270
+ setPluginsStatus('');
262
271
  showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
263
272
  return;
264
273
  }
265
- pluginsStatusLine.textContent = res.updated > 0
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) { pluginsStatusLine.textContent = ''; return; }
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
- pluginsStatusLine.textContent = n > 0
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) descEl.replaceChildren(pluginsStatusLine);
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
- const vis = Math.round((themeCfg.bg.visibility ?? 0.8) * 100);
1065
- const visLabel = h('span', { class: 'w-10 text-right text-ui-xs tabular-nums text-foreground-subtle' }, vis + '%');
1066
- const visRange = h('input', {
1067
- type: 'range', min: '0', max: '100', step: '5', value: String(vis),
1068
- class: 'zcodepro-range', 'aria-label': L.themeVisibility, style: 'flex:1',
1069
- });
1070
- let visTimer = null;
1071
- visRange.addEventListener('input', () => {
1072
- const v = Number(visRange.value);
1073
- visLabel.textContent = v + '%';
1074
- clearTimeout(visTimer);
1075
- visTimer = setTimeout(() => { void saveTheme({ bg: { visibility: v / 100 } }); }, 300);
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 items-center gap-3' },
1188
- h('span', { class: 'shrink-0 text-ui-xs font-medium text-foreground-subtle' }, L.themeVisibility),
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', visibility: 0.8 } };
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
- const visibility = typeof bg.visibility === 'number' && Number.isFinite(bg.visibility)
325
- ? Math.min(1, Math.max(0, bg.visibility)) : 0.8;
326
- const alpha = 1 - visibility;
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
- // 表面半透明化:面板不透明度 = 遮罩浓度 + 10% 基础偏移(可见度 100% → 面板
334
- // 10% 底色,0% → 100% 纯色);在设图之前完成,与具体图片无关
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, { mask, rgb, alpha, fit: bg.fit, visibility, seq });
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;background-image:linear-gradient(rgba(${rgb},${alpha}),rgba(${rgb},${alpha})),${image} !important;` +
363
- `${fitCss}background-position:center !important;}`;
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 = 遮罩浓度(= 1 - 可见度),与全局同值,不再反转
417
- const alpha = pre ? pre.alpha : 0.2;
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', visibility: 0.8 } });
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 */ });
@@ -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
- const overlayClass = 'fixed inset-0 isolate z-50 flex items-center justify-center bg-black/60 supports-backdrop-filter:backdrop-blur-xs duration-100 p-4 platform-linux-desktop:top-12';
30
- const overlayClassBare = 'fixed inset-0 isolate z-50 flex items-center justify-center duration-100 p-4 platform-linux-desktop:top-12';
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 {