zcode-pro 1.4.1 → 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.
@@ -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
+ }
@@ -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
@@ -361,7 +361,7 @@ export function settingRow(name, desc, checked, onToggle, extra) {
361
361
  },
362
362
  h('div', { class: 'min-w-0 flex-1' },
363
363
  h('div', { class: 'text-ui-sm font-medium text-foreground' }, name),
364
- 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 ?? '')),
365
365
  ...(extraEl ? [extraEl] : []),
366
366
  h('button', {
367
367
  type: 'button',
@@ -403,6 +403,14 @@ export function ensureStyle() {
403
403
  /* 滚到底不再把滚动传给弹窗后面的页面 */
404
404
  overscroll-behavior: contain;
405
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
+ }
406
414
  /* 开关(设置弹窗):几何固定写入,颜色随主题变量 */
407
415
  [data-zcodepro-switch] {
408
416
  position: relative;
@@ -699,6 +707,9 @@ export function ensureStyle() {
699
707
  height: 12px;
700
708
  flex-shrink: 0;
701
709
  opacity: 0.6;
710
+ /* 11px 字体的视觉中心(x-height 中点)高于行盒几何中心约 1px(下伸部
711
+ 预留),几何居中的图标看起来偏下——整体上移 1px 对齐视觉中心 */
712
+ transform: translateY(-1px);
702
713
  }
703
714
  .zcodepro-statusbar b { font-weight: 600; color: var(--color-foreground-subtle, #666); }
704
715
  .zcodepro-statusbar i {