minecodex 2.0.12 → 2.0.14
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.md +20 -8
- package/apps/dashboard/app.js +4 -4
- package/apps/dashboard/assets/data-transfer.js +129 -0
- package/apps/dashboard/assets/download.svg +15 -0
- package/apps/dashboard/assets/upload.svg +15 -0
- package/apps/dashboard/index.html +10 -0
- package/apps/dashboard/styles.css +50 -3
- package/features/file-navigation/assets/book-open.svg +16 -0
- package/features/file-navigation/assets/folder-tree.svg +16 -0
- package/features/file-navigation/codex-feature.json +27 -0
- package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
- package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
- package/features/file-navigation/src/domain/navigation.mjs +12 -0
- package/features/file-navigation/src/entry.mjs +34 -0
- package/features/file-navigation/src/frontend/navigation.mjs +140 -0
- package/features/file-navigation/src/injection-source.mjs +9 -0
- package/features/file-navigation/src/navigation.css +16 -0
- package/features/file-navigation/vendor/lucide-LICENSE +43 -0
- package/features/hide-upsell-banner/codex-feature.json +4 -4
- package/features/images/codex-feature.json +4 -4
- package/features/images/src/adapters/codex/native-image-modules.mjs +13 -2
- package/features/images/src/library-prompt-source.mjs +2 -2
- package/features/markdown-reading/assets/book-open.svg +16 -0
- package/features/markdown-reading/codex-feature.json +27 -0
- package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
- package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
- package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
- package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
- package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
- package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
- package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
- package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
- package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
- package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
- package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
- package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
- package/features/markdown-reading/src/injection-source.mjs +43 -0
- package/features/markdown-reading/src/markdown-reading.mjs +82 -0
- package/features/markdown-reading/src/reading.css +346 -0
- package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
- package/features/model-slider/codex-feature.json +5 -4
- package/features/model-slider/src/backend/favorites.mjs +54 -0
- package/features/model-slider/src/domain/model-rules.mjs +6 -1
- package/features/model-slider/src/frontend/install.mjs +24 -3
- package/features/notes/assets/prompt-previews.json +26 -0
- package/features/notes/codex-feature.json +6 -5
- package/features/notes/src/backend/transfer.mjs +97 -0
- package/features/notes/src/frontend/entry.mjs +380 -10
- package/features/notes/src/frontend/icons.json +4 -1
- package/features/notes/src/frontend/styles.css +63 -0
- package/features/session-status/codex-feature.json +6 -3
- package/features/session-status/src/domain/session-stats.mjs +22 -8
- package/features/session-status/src/session-status.mjs +2 -2
- package/features/sidebar-focus/codex-feature.json +2 -2
- package/features/sidebar-focus/src/sidebar-focus.mjs +13 -5
- package/features/sites/codex-feature.json +4 -4
- package/features/sites/src/backend/sites-service.mjs +20 -5
- package/features/sites/src/backend/transfer.mjs +43 -0
- package/features/usage-meter/codex-feature.json +6 -3
- package/package.json +11 -3
- package/packages/cli/src/config.mjs +4 -2
- package/packages/cli/src/control-server.mjs +34 -10
- package/packages/cli/src/data-transfer/service.mjs +42 -0
- package/packages/cli/src/runtime-manager.mjs +3 -0
- package/packages/runtime-host/src/codex-injection.mjs +2 -1
- package/packages/runtime-host/src/codex-runtime.mjs +24 -1
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/src/native/theme.mjs +23 -1
- package/packages/ui/src/native/tokens.mjs +4 -4
package/README.md
CHANGED
|
@@ -10,14 +10,16 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
|
|
|
10
10
|
|
|
11
11
|
| 插件 | 说明 |
|
|
12
12
|
| --- | --- |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| **
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
13
|
+
| **模型窗口** · Model Panel | 快速切换模型和思考强度,收藏常用模型。 |
|
|
14
|
+
| **提示词库** · Prompt Library | 分类保存常用提示词,需要时一键填入输入框。 |
|
|
15
|
+
| **额度环** · Usage Ring | 在侧栏查看五小时和每周剩余额度,点击打开用量详情。 |
|
|
16
|
+
| **会话状态** · Session Status | 查看回复速度、Token 用量和上下文占用,了解对话消耗。 |
|
|
17
|
+
| **Markdown 阅读增强** · Markdown Reader | 让 Markdown 文档更易读,支持章节目录、表格展开和代码复制。 |
|
|
18
|
+
| **文件级联浏览** · Cascading File Browser | 点击文件顶部路径,分列浏览文件夹并快速切换文件。 |
|
|
19
|
+
| **图片集合** · Image Collection | 集中浏览生成的图片,查看提示词并复用创作思路。 |
|
|
20
|
+
| **站点收藏** · Saved Sites | 发现项目正在运行的网页,收藏常用网址,随时打开。 |
|
|
21
|
+
| **侧栏聚焦** · Sidebar Focus | 一键收起侧栏中的其他分组,再次点击恢复原来的展开状态。 |
|
|
22
|
+
| **隐藏额度推广** · Hide Usage Promotions | 隐藏输入框上方的套餐升级推广,让对话更清爽。 |
|
|
21
23
|
|
|
22
24
|
## 安装
|
|
23
25
|
|
|
@@ -94,3 +96,13 @@ MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有
|
|
|
94
96
|
## 从账号菜单打开控制台
|
|
95
97
|
|
|
96
98
|
在 Codex Navbar 底部打开账号菜单,点击 Settings 下方的 **MineCodex ↗**,即可在外部默认浏览器打开本地控制台,等同于 `mcx gui`。macOS 默认 Safari、Chrome/Chromium、Edge、Brave 和 Vivaldi 会定位已有控制台标签页,没有时才新建;首次定位可能需要系统自动化权限。其他浏览器与 Windows 仍由系统打开 URL。
|
|
99
|
+
|
|
100
|
+
### 在电脑之间迁移数据
|
|
101
|
+
|
|
102
|
+
在 `mcx gui` 底部打开「数据迁移」,导出 `.mcx.json` 文件,再到另一台 Mac 或 Windows
|
|
103
|
+
电脑的同一入口导入。确认前可查看新增、重复和冲突数量。只补充缺失项,重复项跳过,
|
|
104
|
+
冲突保留本机版本,不删除已有数据。
|
|
105
|
+
|
|
106
|
+
支持 Prompts 与分类、公网 Sites、模型收藏。模型旧收藏在新版模型滑条首次启动后收录,
|
|
107
|
+
导入收藏在下次启动 Codex 后载入。图片文件、本地站点、账号信息与设备设置不包含在文件中;
|
|
108
|
+
可迁移功能的特殊说明可通过 info 查看。这是手动迁移,不是自动云同步。文件包含提示词正文,请妥善保管。
|
package/apps/dashboard/app.js
CHANGED
|
@@ -221,7 +221,7 @@ function updatePluginCard(card, feature) {
|
|
|
221
221
|
const dot = card.querySelector(".plugin-state .status-dot");
|
|
222
222
|
const stateLabel = card.querySelector(".plugin-state-label");
|
|
223
223
|
card.dataset.featureId = feature.id;
|
|
224
|
-
image.src = `${feature.iconUrl}?v=
|
|
224
|
+
image.src = `${feature.iconUrl}?v=resource-1`;
|
|
225
225
|
name.textContent = feature.label;
|
|
226
226
|
description.textContent = feature.description;
|
|
227
227
|
input.checked = Boolean(feature.enabled);
|
|
@@ -232,7 +232,7 @@ function updatePluginCard(card, feature) {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
function findPluginCard(featureId) {
|
|
235
|
-
return [...elements.pluginList.
|
|
235
|
+
return [...elements.pluginList.querySelectorAll("[data-feature-id]")].find((child) => child.dataset.featureId === featureId) ?? null;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
function renderPluginCards(features) {
|
|
@@ -246,11 +246,11 @@ function renderPluginCards(features) {
|
|
|
246
246
|
let card = findPluginCard(feature.id);
|
|
247
247
|
if (!card) {
|
|
248
248
|
card = createPluginCard(feature);
|
|
249
|
-
elements.pluginList.append(card);
|
|
250
249
|
}
|
|
250
|
+
elements.pluginList.append(card);
|
|
251
251
|
updatePluginCard(card, feature);
|
|
252
252
|
}
|
|
253
|
-
for (const card of [...elements.pluginList.
|
|
253
|
+
for (const card of [...elements.pluginList.querySelectorAll("[data-feature-id]")]) {
|
|
254
254
|
if (card.dataset.featureId && !featureIds.has(card.dataset.featureId)) card.remove();
|
|
255
255
|
}
|
|
256
256
|
elements.pluginList.setAttribute("aria-busy", "false");
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { installFeatureTooltips } from './ui-tooltip.js';
|
|
2
|
+
const tooltipLifetime = new AbortController();
|
|
3
|
+
const { tooltipSurface } = installFeatureTooltips(tooltipLifetime.signal);
|
|
4
|
+
window.addEventListener('pagehide', event => { if (!event.persisted) tooltipLifetime.abort(); });
|
|
5
|
+
const zh = document.documentElement.lang.startsWith('zh');
|
|
6
|
+
const t = (cn, en) => zh ? cn : en;
|
|
7
|
+
const dialog = document.querySelector('#data-dialog');
|
|
8
|
+
const entry = document.querySelector('#data-button');
|
|
9
|
+
const list = document.querySelector('#data-list');
|
|
10
|
+
const featureNames = { notes: t('提示词库', 'Prompt Library'), sites: t('站点收藏', 'Saved Sites'), 'model-slider': t('模型窗口', 'Model Panel') };
|
|
11
|
+
const status = document.querySelector('#data-status');
|
|
12
|
+
const exportButton = document.querySelector('#data-export');
|
|
13
|
+
const importButton = document.querySelector('#data-import');
|
|
14
|
+
const input = document.querySelector('#data-file');
|
|
15
|
+
let bundle = null, busy = false;
|
|
16
|
+
entry.textContent = t('数据迁移', 'Transfer data');
|
|
17
|
+
document.querySelector('#data-title').textContent = t('数据迁移', 'Transfer data');
|
|
18
|
+
document.querySelector('#data-policy').textContent = t('仅补充缺失项,重复跳过,冲突保留本机。', 'Add missing items. Skip duplicates. Keep local conflicts.');
|
|
19
|
+
document.querySelector('#data-close').setAttribute('aria-label', t('关闭', 'Close'));
|
|
20
|
+
exportButton.querySelector('.button-label').textContent = t('导出', 'Export');
|
|
21
|
+
importButton.querySelector('.button-label').textContent = t('导入', 'Import');
|
|
22
|
+
async function request(url, data) {
|
|
23
|
+
const response = await fetch(url, data ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) } : {});
|
|
24
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
25
|
+
return response.json();
|
|
26
|
+
}
|
|
27
|
+
function setBusy(value) {
|
|
28
|
+
busy = value;
|
|
29
|
+
exportButton.disabled = value;
|
|
30
|
+
importButton.disabled = value;
|
|
31
|
+
document.querySelector('#data-close').disabled = value;
|
|
32
|
+
list.setAttribute('aria-busy', String(value));
|
|
33
|
+
}
|
|
34
|
+
const featureHints = {
|
|
35
|
+
'model-slider': t('仅匹配相同模型标识。本机缺少的模型保留收藏,不显示选项;导入后下次启动 Codex 生效。', 'Only exact model IDs match. Missing models stay saved but hidden. Imported favorites load next time Codex starts.'),
|
|
36
|
+
notes: t('包含分类、排序与使用次数。', 'Includes categories, order and usage counts.'),
|
|
37
|
+
sites: t('仅迁移公网网址,导入后归为外部网址。', 'Public URLs only; imported as external links.'),
|
|
38
|
+
};
|
|
39
|
+
let hintIndex = 0;
|
|
40
|
+
function row(name, detail, amount, hint = '') {
|
|
41
|
+
const item = document.createElement('div'); item.className = 'data-card';
|
|
42
|
+
const copy = document.createElement('div'), heading = document.createElement('div'), title = document.createElement('strong'), count = document.createElement('span');
|
|
43
|
+
heading.className = 'data-card-heading';
|
|
44
|
+
title.textContent = name; count.textContent = amount; count.className = 'data-count';
|
|
45
|
+
heading.append(title);
|
|
46
|
+
if (hint) {
|
|
47
|
+
const info = document.createElement('button'), icon = document.createElement('span'), tip = document.createElement('div');
|
|
48
|
+
info.type = 'button'; info.className = 'data-info'; info.setAttribute('aria-label', t(`${name} 说明`, `About ${name}`));
|
|
49
|
+
icon.className = 'ui-icon info-icon'; icon.setAttribute('aria-hidden', 'true'); info.append(icon);
|
|
50
|
+
tooltipSurface(tip);
|
|
51
|
+
tip.id = `data-hint-${++hintIndex}`; tip.className = 'data-tooltip'; tip.setAttribute('popover', 'auto'); tip.setAttribute('role', 'tooltip'); tip.textContent = hint;
|
|
52
|
+
info.setAttribute('aria-describedby', tip.id); info.setAttribute('aria-expanded', 'false');
|
|
53
|
+
info.addEventListener('click', () => {
|
|
54
|
+
tip.togglePopover();
|
|
55
|
+
if (!tip.matches(':popover-open')) return;
|
|
56
|
+
const anchor = info.getBoundingClientRect(), box = tip.getBoundingClientRect();
|
|
57
|
+
tip.style.left = `${Math.max(8, Math.min(anchor.left + anchor.width / 2 - box.width / 2, innerWidth - box.width - 8))}px`;
|
|
58
|
+
tip.style.top = `${anchor.top - box.height - 2 >= 8 ? anchor.top - box.height - 2 : anchor.bottom + 2}px`;
|
|
59
|
+
});
|
|
60
|
+
tip.addEventListener('toggle', () => info.setAttribute('aria-expanded', String(tip.matches(':popover-open'))));
|
|
61
|
+
heading.append(info, tip);
|
|
62
|
+
}
|
|
63
|
+
copy.append(heading);
|
|
64
|
+
if (detail) { const desc = document.createElement('p'); desc.textContent = detail; copy.append(desc); }
|
|
65
|
+
item.append(copy, count); list.append(item);
|
|
66
|
+
}
|
|
67
|
+
function showInventory(data) {
|
|
68
|
+
list.replaceChildren();
|
|
69
|
+
for (const feature of data.features.filter(item => item.portable)) {
|
|
70
|
+
const detail = feature.id === 'notes' ? t(`${feature.categories} 个分类`, `${feature.categories} categories`) : '';
|
|
71
|
+
const hint = feature.id === 'model-slider' && !feature.initialized
|
|
72
|
+
? t('旧收藏在新版模型窗口首次启动后收录。', 'Legacy favorites are collected when the updated slider first starts.') + ' ' + featureHints[feature.id]
|
|
73
|
+
: featureHints[feature.id];
|
|
74
|
+
row(featureNames[feature.id] || feature.id, detail, t(`${feature.count} 项`, `${feature.count} items`), hint);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function showPreview(data) {
|
|
78
|
+
list.replaceChildren();
|
|
79
|
+
for (const feature of data.features) row(featureNames[feature.id] || feature.id, t(`${feature.duplicate} 项重复 · ${feature.conflict} 项冲突,保留本机${feature.categoriesAdded ? ` · 新增 ${feature.categoriesAdded} 个分类` : ''}`, `${feature.duplicate} duplicates · ${feature.conflict} conflicts kept locally${feature.categoriesAdded ? ` · ${feature.categoriesAdded} new categories` : ''}`), t(`新增 ${feature.added} 项`, `${feature.added} new`), featureHints[feature.id]);
|
|
80
|
+
if (!data.features.length) row(t('空文件', 'Empty file'), t('没有可导入的数据', 'No data to import'), '0');
|
|
81
|
+
}
|
|
82
|
+
entry.addEventListener('click', async () => {
|
|
83
|
+
if (busy) return;
|
|
84
|
+
bundle = null; input.value = ''; importButton.querySelector('.button-label').textContent = t('导入', 'Import');
|
|
85
|
+
status.textContent = t('正在读取本机数据…', 'Reading local data…');
|
|
86
|
+
list.replaceChildren(); dialog.showModal(); setBusy(true);
|
|
87
|
+
try { showInventory(await request('/api/data')); status.textContent = ''; }
|
|
88
|
+
catch { status.textContent = t('无法读取数据。请关闭后重试。', 'Could not read data. Close and try again.'); }
|
|
89
|
+
finally { setBusy(false); }
|
|
90
|
+
});
|
|
91
|
+
document.querySelector('#data-close').addEventListener('click', () => { if (!busy) dialog.close(); });
|
|
92
|
+
dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); });
|
|
93
|
+
exportButton.addEventListener('click', async () => {
|
|
94
|
+
setBusy(true); status.textContent = t('正在导出…', 'Exporting…');
|
|
95
|
+
try {
|
|
96
|
+
const data = await request('/api/data/export');
|
|
97
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
|
|
98
|
+
const a = document.createElement('a'); a.href = url; a.download = `minecodex-${new Intl.DateTimeFormat('sv-SE').format(new Date())}.mcx.json`; a.click();
|
|
99
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
100
|
+
status.textContent = t('已导出', 'Exported');
|
|
101
|
+
} catch { status.textContent = t('导出失败,请重试。文件最多支持 16 MB。', 'Export failed. Try again. Files are limited to 16 MB.'); }
|
|
102
|
+
finally { setBusy(false); }
|
|
103
|
+
});
|
|
104
|
+
input.addEventListener('change', async () => {
|
|
105
|
+
const file = input.files[0]; if (!file) return;
|
|
106
|
+
bundle = null; setBusy(true); importButton.querySelector('.button-label').textContent = t('导入', 'Import');
|
|
107
|
+
try {
|
|
108
|
+
if (file.size > 16 * 1024 * 1024) throw new Error('size');
|
|
109
|
+
const data = JSON.parse(await file.text());
|
|
110
|
+
const preview = await request('/api/data/preview', data);
|
|
111
|
+
bundle = data; showPreview(preview);
|
|
112
|
+
status.textContent = file.name;
|
|
113
|
+
importButton.querySelector('.button-label').textContent = t('确认导入', 'Confirm import');
|
|
114
|
+
} catch { status.textContent = t('文件无效或超过 16 MB', 'Invalid file or larger than 16 MB'); }
|
|
115
|
+
finally { input.value = ''; setBusy(false); }
|
|
116
|
+
});
|
|
117
|
+
importButton.addEventListener('click', async () => {
|
|
118
|
+
if (!bundle) { input.click(); return; }
|
|
119
|
+
setBusy(true); status.textContent = t('正在导入…', 'Importing…');
|
|
120
|
+
try {
|
|
121
|
+
const result = await request('/api/data/import', bundle);
|
|
122
|
+
list.replaceChildren();
|
|
123
|
+
for (const feature of result.features) row(featureNames[feature.id] || feature.id, feature.status === 'failed' ? t('可重试', 'Retry available') : t(`跳过 ${feature.duplicate} 项重复 · 保留 ${feature.conflict} 项冲突${feature.categoriesAdded ? ` · 新增 ${feature.categoriesAdded} 个分类` : ''}`, `${feature.duplicate} duplicates skipped · ${feature.conflict} conflicts kept${feature.categoriesAdded ? ` · ${feature.categoriesAdded} new categories` : ''}`), feature.status === 'failed' ? t('失败', 'Failed') : t(`新增 ${feature.added} 项`, `${feature.added} added`), featureHints[feature.id]);
|
|
124
|
+
status.textContent = result.complete ? t('导入完成', 'Import complete') : t('部分失败,请重试', 'Partially imported. Retry.');
|
|
125
|
+
if (result.complete) bundle = null;
|
|
126
|
+
importButton.querySelector('.button-label').textContent = result.complete ? t('导入', 'Import') : t('重试导入', 'Retry import');
|
|
127
|
+
} catch { status.textContent = t('未能确认结果,请重试', 'Could not confirm the result. Retry.'); }
|
|
128
|
+
finally { setBusy(false); }
|
|
129
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="24"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
stroke-width="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
>
|
|
12
|
+
<path d="M12 15V3" />
|
|
13
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
14
|
+
<path d="m7 10 5 5 5-5" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="24"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
stroke-width="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
>
|
|
12
|
+
<path d="M12 3v12" />
|
|
13
|
+
<path d="m17 8-5-5-5 5" />
|
|
14
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
15
|
+
</svg>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/styles.css?v=mcx-block-1">
|
|
10
10
|
<script type="module" src="/app.js?v=mcx-block-1"></script>
|
|
11
11
|
<script type="module" src="/assets/terrain.js"></script>
|
|
12
|
+
<script type="module" src="/assets/data-transfer.js"></script>
|
|
12
13
|
</head>
|
|
13
14
|
<body>
|
|
14
15
|
<canvas id="agent-world" aria-hidden="true"></canvas>
|
|
@@ -71,11 +72,20 @@
|
|
|
71
72
|
<dl>
|
|
72
73
|
<div><dt id="version-label">Version</dt><dd id="version-value">—</dd></div>
|
|
73
74
|
</dl>
|
|
75
|
+
<button id="data-button" class="button quiet" type="button">Transfer data</button>
|
|
74
76
|
<button id="diagnostics-button" class="button quiet" type="button">Copy diagnostics</button>
|
|
75
77
|
<button id="update-button" class="button quiet" type="button"><span class="ui-icon refresh-icon" aria-hidden="true"></span><span class="button-label">Check for updates</span></button>
|
|
76
78
|
</footer>
|
|
77
79
|
<p id="update-result" class="update-result" role="status" hidden></p>
|
|
78
80
|
</main>
|
|
81
|
+
<dialog id="data-dialog" class="data-dialog" aria-labelledby="data-title">
|
|
82
|
+
<header class="data-header"><div><h2 id="data-title">Transfer data</h2></div><button id="data-close" class="button quiet" type="button" aria-label="Close"><svg viewBox="0 0 21 21" fill="none" aria-hidden="true"><path d="M14.6549 5.57307C14.9283 5.2997 15.3718 5.2997 15.6451 5.57307C15.9185 5.84643 15.9185 6.28993 15.6451 6.5633L11.3903 10.8182L15.6451 15.0731L15.735 15.1834C15.9141 15.4551 15.8842 15.8242 15.6451 16.0633C15.4061 16.3024 15.0369 16.3322 14.7653 16.1531L14.6549 16.0633L10.4 11.8084L6.14515 16.0633C5.87178 16.3367 5.42828 16.3367 5.15492 16.0633C4.88155 15.7899 4.88155 15.3464 5.15492 15.0731L9.4098 10.8182L5.15492 6.5633L5.06507 6.45295C4.88597 6.18128 4.91584 5.81214 5.15492 5.57307C5.39399 5.33399 5.76313 5.30413 6.0348 5.48322L6.14515 5.57307L10.4 9.82795L14.6549 5.57307Z" fill="currentColor"/></svg></button></header>
|
|
83
|
+
<div id="data-list" class="data-list"></div>
|
|
84
|
+
<p id="data-policy" class="data-policy"></p>
|
|
85
|
+
<p id="data-status" class="data-help data-status" role="status" aria-live="polite"></p>
|
|
86
|
+
<footer class="data-actions"><button id="data-export" class="button secondary" type="button"><span class="ui-icon download-icon" aria-hidden="true"></span><span class="button-label">Export</span></button><button id="data-import" class="button secondary" type="button"><span class="ui-icon upload-icon" aria-hidden="true"></span><span class="button-label">Import</span></button></footer>
|
|
87
|
+
<input id="data-file" type="file" accept=".json,.mcx.json,application/json" hidden>
|
|
88
|
+
</dialog>
|
|
79
89
|
<div id="toast" class="toast" hidden role="status" aria-live="polite"></div>
|
|
80
90
|
</body>
|
|
81
91
|
</html>
|
|
@@ -186,8 +186,8 @@ h2 { font-size: 14px; font-weight: 620; }
|
|
|
186
186
|
.plugin-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--grid-gap); }
|
|
187
187
|
.plugin-card { display: flex; flex-direction: column; align-items: stretch; min-width: 0; min-height: 216px; padding: var(--card-padding); border: 1px solid var(--border); border-radius: var(--card-radius); background: var(--surface); box-shadow: 0 3px 0 var(--block-shadow); }
|
|
188
188
|
.plugin-topline { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
|
189
|
-
.plugin-icon { display: grid; place-items: center; width: 28px; height: 28px; }
|
|
190
|
-
.plugin-icon img { width: 24px; height: 24px; opacity: 0.75; }
|
|
189
|
+
.plugin-icon { display: grid; place-items: center; width: 28px; height: 28px; flex: none; }
|
|
190
|
+
.plugin-icon img { display: block; width: 24px; height: 24px; object-fit: contain; opacity: 0.75; }
|
|
191
191
|
.plugin-copy { flex: 1; }
|
|
192
192
|
.plugin-name { font-size: 16px; font-weight: 600; line-height: 1.5; letter-spacing: -0.01em; }
|
|
193
193
|
.plugin-description { margin-top: 8px; font-size: 14px; line-height: 1.65; }
|
|
@@ -224,7 +224,7 @@ h2 { font-size: 14px; font-weight: 620; }
|
|
|
224
224
|
.diagnostics .button { display: inline-flex; align-items: center; gap: 7px; font-weight: 400; }
|
|
225
225
|
.diagnostics .ui-icon { width: 15px; height: 15px; flex-basis: 15px; }
|
|
226
226
|
.diagnostics dl { display: flex; flex-wrap: wrap; gap: 10px 24px; margin: 0; }
|
|
227
|
-
.diagnostics dl div { display: flex; gap: 6px; }
|
|
227
|
+
.diagnostics dl div { display: flex; align-items: baseline; gap: 6px; }
|
|
228
228
|
.diagnostics dt { color: var(--ink-tertiary); }
|
|
229
229
|
.diagnostics dd { margin: 0; color: var(--ink-secondary); }
|
|
230
230
|
|
|
@@ -265,3 +265,50 @@ h2 { font-size: 14px; font-weight: 620; }
|
|
|
265
265
|
@media (prefers-reduced-motion: reduce) {
|
|
266
266
|
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
/* 数据迁移沿用控制台的不透明表面与卡片尺寸。 */
|
|
271
|
+
.data-dialog { width: min(520px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); padding: 24px; border: 1px solid var(--border); border-bottom: 3px solid var(--block-shadow); border-radius: var(--card-radius); background: var(--surface); color: var(--ink); box-shadow: var(--shadow); overflow-y: auto; }
|
|
272
|
+
.data-dialog::backdrop { background: #10172280; }
|
|
273
|
+
.data-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
274
|
+
.data-header h2 { margin: 0; font-size: 20px; line-height: 28px; font-weight: 600; }
|
|
275
|
+
.data-header #data-close { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 32px; padding: 0; width: 32px; height: 32px; min-height: 32px; }
|
|
276
|
+
.data-header #data-close svg { display: block; width: 16px; height: 16px; }
|
|
277
|
+
.data-list { display: grid; gap: 10px; }
|
|
278
|
+
.data-card { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px; border: 1px solid var(--border); border-radius: var(--card-radius); background: var(--surface); }
|
|
279
|
+
.data-card-heading { display: flex; align-items: center; gap: 6px; }
|
|
280
|
+
.data-card strong { font-size: 14px; font-weight: 600; line-height: 22px; }
|
|
281
|
+
.data-card p { margin: 4px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-tertiary); }
|
|
282
|
+
.data-count { white-space: nowrap; font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
283
|
+
.data-info { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--ink-tertiary); cursor: pointer; }
|
|
284
|
+
.data-info .ui-icon { width: 14px; height: 14px; flex-basis: 14px; }
|
|
285
|
+
.data-info:hover, .data-info[aria-expanded="true"] { background: var(--surface-muted); color: var(--ink-secondary); }
|
|
286
|
+
.data-tooltip { inset: auto; margin: 0; width: max-content; max-width: min(320px, calc(100vw - 16px)); text-align: center; overflow-wrap: anywhere; }
|
|
287
|
+
.data-policy { margin: 14px 0 0; color: var(--ink-tertiary); font-size: 11px; line-height: 1.6; }
|
|
288
|
+
.data-status { margin: 12px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-secondary); overflow-wrap: anywhere; }
|
|
289
|
+
.data-status:empty { display: none; }
|
|
290
|
+
.data-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
|
|
291
|
+
.data-actions .button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-width: 88px; }
|
|
292
|
+
.data-actions .ui-icon { width: 16px; height: 16px; flex-basis: 16px; }
|
|
293
|
+
.download-icon { --icon: url("/assets/download.svg"); }
|
|
294
|
+
.upload-icon { --icon: url("/assets/upload.svg"); }
|
|
295
|
+
@media (max-width: 600px) { .data-dialog { padding: 20px; } .data-card { gap: 12px; } }
|
|
296
|
+
|
|
297
|
+
/* 独立控制台没有原生主题桥,只映射 token;Tooltip 材质由共享组件安装。 */
|
|
298
|
+
:root {
|
|
299
|
+
--mcx-spacing: 4px;
|
|
300
|
+
--mcx-radius-2xl: 16px;
|
|
301
|
+
--mcx-codex-corner-shape: round;
|
|
302
|
+
--mcx-color-background-tooltip: var(--ink);
|
|
303
|
+
--mcx-color-text-tooltip: var(--surface);
|
|
304
|
+
--mcx-color-border-tooltip: color-mix(in oklab, var(--mcx-color-text-tooltip) 5%, transparent);
|
|
305
|
+
--mcx-shadow-tooltip: 0 8px 18px #0f172a33;
|
|
306
|
+
--mcx-font-sans-default: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
307
|
+
--mcx-text-sm: 13px;
|
|
308
|
+
--mcx-tooltip-compact-font-weight: 400;
|
|
309
|
+
--mcx-tracking-tooltip: -.15px;
|
|
310
|
+
}
|
|
311
|
+
:root[data-theme="dark"] { --mcx-color-background-tooltip: var(--surface); --mcx-color-text-tooltip: var(--ink); }
|
|
312
|
+
@supports (corner-shape: superellipse(1.5)) {
|
|
313
|
+
:root { --mcx-codex-corner-shape: superellipse(1.5); --mcx-radius-2xl: 24px; }
|
|
314
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- @license lucide-static v0.577.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-book-open"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2.1"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 7v14" />
|
|
15
|
+
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="24"
|
|
4
|
+
height="24"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
fill="none"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
stroke-width="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
>
|
|
12
|
+
<path d="M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
|
|
13
|
+
<path d="M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
|
|
14
|
+
<path d="M3 5a2 2 0 0 0 2 2h3" />
|
|
15
|
+
<path d="M3 3v13a2 2 0 0 0 2 2h3" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "file-navigation",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"label": {
|
|
6
|
+
"en": "Cascading File Browser",
|
|
7
|
+
"zh-CN": "文件级联浏览"
|
|
8
|
+
},
|
|
9
|
+
"description": {
|
|
10
|
+
"en": "Click the path above a file to browse folders in columns and open another file.",
|
|
11
|
+
"zh-CN": "点击文件顶部路径,分列浏览文件夹并快速切换文件。"
|
|
12
|
+
},
|
|
13
|
+
"entry": {
|
|
14
|
+
"kind": "page-script",
|
|
15
|
+
"script": "src/entry.mjs",
|
|
16
|
+
"sourceFactory": "src/injection-source.mjs",
|
|
17
|
+
"resources": {
|
|
18
|
+
"styles": "src/navigation.css"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"icon": {
|
|
22
|
+
"resource": "assets/folder-tree.svg"
|
|
23
|
+
},
|
|
24
|
+
"placement": {
|
|
25
|
+
"order": 91
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 依照原生 breadcrumb 的依赖角色查找公开模块,不绑定产物 hash 或短变量名。
|
|
2
|
+
export async function loadFileNavigationModules() {
|
|
3
|
+
const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(n => n.href || n.src);
|
|
4
|
+
const breadcrumbUrl = urls.find(url => /\/review-file-source-breadcrumb-[^/]+\.js$/.test(url));
|
|
5
|
+
const mainUrl = urls.find(url => /\/app-main-[^/]+\.js$/.test(url));
|
|
6
|
+
if (!breadcrumbUrl || !mainUrl) throw new Error('file-navigation-modules-unavailable');
|
|
7
|
+
const read = async url => {
|
|
8
|
+
const response = await fetch(url);
|
|
9
|
+
if (!response.ok) throw new Error('file-navigation-source-unavailable');
|
|
10
|
+
return response.text();
|
|
11
|
+
};
|
|
12
|
+
const imports = (source, url) => {
|
|
13
|
+
const result = new Map();
|
|
14
|
+
for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
|
|
15
|
+
for (const entry of match[1].split(',')) {
|
|
16
|
+
const parts = entry.trim().split(/\s+as\s+/);
|
|
17
|
+
result.set(parts.at(-1), { name: parts[0], url: new URL(match[2], url).href });
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
const binding = async (map, name) => {
|
|
22
|
+
const value = map.get(name);
|
|
23
|
+
if (!value) throw new Error('file-navigation-dependency-unavailable');
|
|
24
|
+
return (await import(value.url))[value.name];
|
|
25
|
+
};
|
|
26
|
+
const source = await read(breadcrumbUrl), deps = imports(source, breadcrumbUrl);
|
|
27
|
+
const treeUrl = [...deps.values()].find(v => /\/base-file-tree-/.test(v.url))?.url;
|
|
28
|
+
const queryUrl = [...deps.values()].find(v => /\/workspace-directory-entries-query-/.test(v.url))?.url;
|
|
29
|
+
if (!treeUrl || !queryUrl) throw new Error('file-navigation-contract-changed');
|
|
30
|
+
const [treeSource, querySource, main] = await Promise.all([read(treeUrl), read(queryUrl), read(mainUrl)]);
|
|
31
|
+
const [treeModule, queryModule] = await Promise.all([import(treeUrl), import(queryUrl)]);
|
|
32
|
+
for (const module of [treeModule, queryModule])
|
|
33
|
+
for (const value of Object.values(module))
|
|
34
|
+
if (typeof value === 'function' && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
|
|
35
|
+
const Tree = Object.values(treeModule).find(value => typeof value === 'function' && String(value).includes('contextMenuEnabled:'));
|
|
36
|
+
const reactOwner = treeSource.match(/\(0,([\w$]+)\.useState\)/)?.[1];
|
|
37
|
+
const reactFactory = reactOwner && treeSource.match(new RegExp(`\\b${reactOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
38
|
+
const rootOwner = main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
|
|
39
|
+
const rootFactory = rootOwner && main.match(new RegExp(`\\b${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
40
|
+
const requestName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\(`workspace-directory-entries`/)?.[1];
|
|
41
|
+
const [react, dom, request] = await Promise.all([
|
|
42
|
+
binding(imports(treeSource, treeUrl), reactFactory),
|
|
43
|
+
binding(imports(main, mainUrl), rootFactory),
|
|
44
|
+
binding(imports(querySource, queryUrl), requestName),
|
|
45
|
+
]);
|
|
46
|
+
const React = react(), ReactDOM = dom();
|
|
47
|
+
if (!Tree || !React.createElement || !ReactDOM.createRoot || typeof request !== 'function')
|
|
48
|
+
throw new Error('file-navigation-components-unavailable');
|
|
49
|
+
const sharedUrl = [...imports(treeSource, treeUrl).values()].find(value => /\/app-shared-/.test(value.url))?.url;
|
|
50
|
+
const shared = sharedUrl && await import(sharedUrl);
|
|
51
|
+
const folder = shared && Object.values(shared).find(value => value?.name === 'folder-light-16');
|
|
52
|
+
if (!folder?.canvas?.viewBox || typeof folder.body !== 'string')
|
|
53
|
+
throw new Error('file-navigation-folder-icon-unavailable');
|
|
54
|
+
// 分列导航没有纵向展开状态;通过原生样式入口使用固定的原生文件夹图标。
|
|
55
|
+
const folderImage = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${folder.canvas.viewBox}">${folder.body}</svg>`)}")`;
|
|
56
|
+
const columnStyles = `
|
|
57
|
+
/* 原生树的逻辑焦点也响应鼠标;描边仅保留给键盘可见焦点。 */
|
|
58
|
+
[data-type="item"]:not(:focus-visible)::before { outline:none; }
|
|
59
|
+
[data-item-type="folder"] [data-item-section="icon"] { width:16px; height:16px; transform:none; }
|
|
60
|
+
[data-item-type="folder"] [data-item-section="icon"] svg { display:none; }
|
|
61
|
+
[data-item-type="folder"] [data-item-section="icon"]::before {
|
|
62
|
+
content:""; display:block; width:16px; height:16px; background:currentColor;
|
|
63
|
+
mask:${folderImage} center / contain no-repeat;
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
return { Tree, React, ReactDOM, request, columnStyles };
|
|
67
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loadFileNavigationModules } from './modules.mjs';
|
|
2
|
+
|
|
3
|
+
export function createFileNavigationAdapter(codex, loadModules = loadFileNavigationModules) {
|
|
4
|
+
let pending;
|
|
5
|
+
const modules = () => pending ??= loadModules().catch(error => { pending = null; throw error; });
|
|
6
|
+
function read(nav) {
|
|
7
|
+
const button = [...nav.querySelectorAll('button')].at(-1);
|
|
8
|
+
const props = button && codex.props(button, p => typeof p.root === 'string' && typeof p.activePath === 'string' && typeof p.onSelectFile === 'function');
|
|
9
|
+
if (!props || !nav.parentElement || !props.hostId) return null;
|
|
10
|
+
const rootName = nav.querySelector('button')?.textContent;
|
|
11
|
+
const separator = nav.querySelector('li > span')?.cloneNode(true);
|
|
12
|
+
let boundary = nav.parentElement;
|
|
13
|
+
while (boundary.parentElement && boundary.getBoundingClientRect().height < 160) boundary = boundary.parentElement;
|
|
14
|
+
const key = JSON.stringify([props.hostId, props.root, props.activePath]);
|
|
15
|
+
return {
|
|
16
|
+
key, nav, capsule: nav.parentElement, boundary, rootName, path: props.activePath, separator,
|
|
17
|
+
async list(directory, signal) {
|
|
18
|
+
const { request } = await modules();
|
|
19
|
+
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
|
|
20
|
+
const result = await request('workspace-directory-entries', { params: {
|
|
21
|
+
hostId: props.hostId, workspaceRoot: props.root, directoryPath: directory || null, includeHidden: true,
|
|
22
|
+
}, signal });
|
|
23
|
+
if (!Array.isArray(result?.entries)) throw new Error('file-navigation-directory-contract-changed');
|
|
24
|
+
return result.entries.map(entry => {
|
|
25
|
+
if (typeof entry.path !== 'string' || !['file', 'directory'].includes(entry.type))
|
|
26
|
+
throw new Error('file-navigation-entry-contract-changed');
|
|
27
|
+
return { path: entry.path, name: entry.path.split('/').at(-1), directory: entry.type === 'directory' };
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
open(entry) {
|
|
31
|
+
if (entry.directory) return;
|
|
32
|
+
props.onSelectFile(`${props.root.replace(/[\\/]$/, '')}/${entry.path}`, { isPreview: true });
|
|
33
|
+
},
|
|
34
|
+
async prepare() {
|
|
35
|
+
const native = await modules();
|
|
36
|
+
const providers = [], seen = new Set();
|
|
37
|
+
for (let fiber = codex.currentFiber(nav); fiber; fiber = fiber.return) {
|
|
38
|
+
if (fiber.tag !== 10 || seen.has(fiber.type)) continue;
|
|
39
|
+
providers.push({ type: fiber.type, value: fiber.memoizedProps.value }); seen.add(fiber.type);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
mountColumn(element, entries, selected, onSelect, onBack) {
|
|
43
|
+
const { React, ReactDOM, Tree } = native;
|
|
44
|
+
const root = ReactDOM.createRoot(element);
|
|
45
|
+
const byName = new Map(entries.map(entry => [entry.name, entry]));
|
|
46
|
+
const activate = event => {
|
|
47
|
+
const row = event.nativeEvent.composedPath().find(node => node instanceof Element && node.hasAttribute('data-item-path'));
|
|
48
|
+
const entry = byName.get(row?.getAttribute('data-item-path')?.replace(/\/$/, ''));
|
|
49
|
+
if (entry) onSelect(entry);
|
|
50
|
+
};
|
|
51
|
+
let tree = React.createElement(Tree, {
|
|
52
|
+
cwd: props.root, hostId: props.hostId, contextMenuEnabled: false, surface: 'dropdown', unsafeCSS: native.columnStyles,
|
|
53
|
+
paths: entries.map(entry => ({ path: entry.path, displayPath: entry.name + (entry.directory ? '/' : '') })),
|
|
54
|
+
selectedPath: entries.find(entry => entry.path === selected)?.name,
|
|
55
|
+
initialExpandedPaths: [], flattenEmptyDirectories: false, revealSelectedPath: true, revealSelectedPathScrollOffset: 'center',
|
|
56
|
+
onClick: activate,
|
|
57
|
+
});
|
|
58
|
+
for (const provider of providers) tree = React.createElement(provider.type, { value: provider.value }, tree);
|
|
59
|
+
root.render(tree);
|
|
60
|
+
const keys = event => {
|
|
61
|
+
const row = event.composedPath().find(node => node instanceof Element && node.hasAttribute('data-item-path'));
|
|
62
|
+
const entry = byName.get(row?.getAttribute('data-item-path')?.replace(/\/$/, ''));
|
|
63
|
+
if (event.key === 'ArrowLeft') { event.preventDefault(); event.stopPropagation(); onBack(); }
|
|
64
|
+
if (entry && (event.key === 'Enter' || (event.key === 'ArrowRight' && entry.directory))) {
|
|
65
|
+
event.preventDefault(); event.stopPropagation(); onSelect(entry);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
element.addEventListener('keydown', keys, true);
|
|
69
|
+
return {
|
|
70
|
+
dispose() { element.removeEventListener('keydown', keys, true); root.unmount(); },
|
|
71
|
+
focus() {
|
|
72
|
+
const shadow = element.querySelector('file-tree-container')?.shadowRoot;
|
|
73
|
+
(shadow?.querySelector('[aria-selected="true"]') ?? shadow?.querySelector('[role="treeitem"]'))?.focus();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
scan: () => [...document.querySelectorAll('nav:has(> ol)')].map(read).filter(Boolean),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function fileNavigationPath(rootName, path) {
|
|
2
|
+
const parts = path.split('/').filter(Boolean);
|
|
3
|
+
const names = [rootName, ...parts];
|
|
4
|
+
return {
|
|
5
|
+
labels: names.length > 3 ? ['…', ...names.slice(-2)] : names,
|
|
6
|
+
directories: ['', ...parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join('/'))],
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function selectFileNavigationDirectory(directories, index, path) {
|
|
11
|
+
return [...directories.slice(0, index + 1), path];
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const adapter = createFileNavigationAdapter(config.codex);
|
|
2
|
+
const mounted = new Map();
|
|
3
|
+
const style = document.createElement('style');
|
|
4
|
+
style.setAttribute('data-mcx-file-navigation-style', ''); style.textContent = config.resources.styles;
|
|
5
|
+
document.head.append(style);
|
|
6
|
+
let frame = null, disposed = false;
|
|
7
|
+
function reconcile() {
|
|
8
|
+
frame = null;
|
|
9
|
+
if (disposed) return;
|
|
10
|
+
const views = adapter.scan(), active = new Set(views.map(view => view.nav));
|
|
11
|
+
for (const [nav, item] of mounted) if (!active.has(nav)) { item.session.dispose(); mounted.delete(nav); }
|
|
12
|
+
for (const view of views) {
|
|
13
|
+
const existing = mounted.get(view.nav);
|
|
14
|
+
if (existing?.key === view.key) continue;
|
|
15
|
+
existing?.session.dispose();
|
|
16
|
+
mounted.set(view.nav, { key: view.key, session: installFileNavigation(view, config.ui, config.locale) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const observer = new MutationObserver(records => {
|
|
20
|
+
if (records.every(record => record.target.closest?.('.mcx-file-navigation-popover,.mcx-file-navigation-trigger,.cm-content'))) return;
|
|
21
|
+
if (frame === null && !disposed) frame = requestAnimationFrame(reconcile);
|
|
22
|
+
});
|
|
23
|
+
signal.addEventListener('abort', disposeFileNavigation, { once: true });
|
|
24
|
+
observer.observe(document.body, { childList: true, subtree: true }); reconcile();
|
|
25
|
+
config.registerSelfCheck?.(() => ({
|
|
26
|
+
state: mounted.size ? [...mounted.values()].every(item => item.session.available()) ? 'available' : 'unavailable' : 'not-applicable',
|
|
27
|
+
checks: [...mounted.values()].map(item => ({ name: 'file-navigation-trigger', available: item.session.available() })),
|
|
28
|
+
scopes: [...mounted.keys()].map(nav => ({ name: 'file-navigation', element: nav.parentElement })),
|
|
29
|
+
}));
|
|
30
|
+
function disposeFileNavigation() {
|
|
31
|
+
disposed = true; observer.disconnect(); if (frame !== null) cancelAnimationFrame(frame);
|
|
32
|
+
for (const item of mounted.values()) item.session.dispose(); mounted.clear(); style.remove();
|
|
33
|
+
}
|
|
34
|
+
return disposeFileNavigation;
|