minecodex 2.0.4 → 2.0.5
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/DESIGN.md +25 -1
- package/README.md +4 -2
- package/features/images/DESIGN.md +106 -41
- package/features/images/README.md +25 -16
- package/features/images/codex-feature.json +6 -3
- package/features/images/src/adapters/codex/image-prompt.mjs +24 -2
- package/features/images/src/adapters/codex/image-sources.mjs +19 -0
- package/features/images/src/adapters/codex/library-api.mjs +2 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +26 -2
- package/features/images/src/adapters/codex/native-image-source.mjs +556 -0
- package/features/images/src/backend/local-image-source.mjs +138 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +39 -0
- package/features/images/src/frontend/local-image-source.mjs +194 -0
- package/features/images/src/library-prompt-page.mjs +158 -53
- package/features/images/src/library-prompt-source.mjs +11 -3
- package/features/images/src/prompt-index.mjs +14 -2
- package/features/model-slider/DESIGN.md +2 -0
- package/features/model-slider/src/frontend/install.mjs +78 -11
- package/features/notes/DESIGN.md +7 -3
- package/features/notes/src/frontend/entry.mjs +26 -9
- package/features/session-status/DESIGN.md +4 -2
- package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
- package/features/session-status/src/domain/session-stats.mjs +17 -9
- package/features/session-status/src/session-status.mjs +47 -11
- package/package.json +1 -1
- package/packages/codex-adapter/README.md +2 -1
- package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
- package/packages/runtime-host/src/codex-injection.mjs +3 -6
- package/packages/runtime-host/src/codex-runtime.mjs +16 -5
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/DESIGN.md +5 -2
- package/packages/ui/src/components/button.mjs +2 -0
- package/packages/ui/src/components/navigation-rail.mjs +18 -10
- package/packages/ui/src/components/page.mjs +9 -2
- package/packages/ui/src/install.mjs +3 -1
- package/packages/ui/src/native/navigation-rail.mjs +6 -1
- package/packages/ui/src/native/page.mjs +23 -0
- package/packages/ui/src/native/tokens.mjs +3 -0
|
@@ -9,6 +9,7 @@ let button = null;
|
|
|
9
9
|
let addButton = null;
|
|
10
10
|
let composerRoot = null;
|
|
11
11
|
let composerThread = null;
|
|
12
|
+
let composerEditable = null;
|
|
12
13
|
let panel = null;
|
|
13
14
|
let items = [];
|
|
14
15
|
let generation = 0;
|
|
@@ -169,7 +170,7 @@ function renderList() {
|
|
|
169
170
|
async () => {
|
|
170
171
|
const current = generation;
|
|
171
172
|
try {
|
|
172
|
-
window.__codexPersonalRuntime.insertText(item.body);
|
|
173
|
+
window.__codexPersonalRuntime.insertText(item.body, composerEditable);
|
|
173
174
|
// 插入同步成功后再记一次调用;失败和单纯浏览不计数。
|
|
174
175
|
close();
|
|
175
176
|
await api(`/api/prompts/${encodeURIComponent(item.id)}/use`, {
|
|
@@ -224,7 +225,7 @@ function isOpenComposerMenu(node) {
|
|
|
224
225
|
function sync() {
|
|
225
226
|
if (disposed || signal.aborted) return;
|
|
226
227
|
syncNavigation();
|
|
227
|
-
const nativeComposer = config.codex.composer(null,{
|
|
228
|
+
const nativeComposer = config.codex.composer(null,{purpose:"insertion"});
|
|
228
229
|
const add = nativeComposer?.add;
|
|
229
230
|
const card = nativeComposer?.root;
|
|
230
231
|
const thread = nativeComposer?.threadId ?? null;
|
|
@@ -232,13 +233,14 @@ function sync() {
|
|
|
232
233
|
close();
|
|
233
234
|
composerThread = thread;
|
|
234
235
|
}
|
|
235
|
-
if (add !== addButton || card !== composerRoot) {
|
|
236
|
+
if (add !== addButton || card !== composerRoot || nativeComposer?.editable !== composerEditable) {
|
|
236
237
|
close();
|
|
237
238
|
button?.remove();
|
|
238
239
|
button = null;
|
|
239
240
|
addButton = add;
|
|
240
241
|
composerRoot = card;
|
|
241
242
|
}
|
|
243
|
+
composerEditable = nativeComposer?.editable ?? null;
|
|
242
244
|
if (!add) return;
|
|
243
245
|
if (
|
|
244
246
|
panel &&
|
|
@@ -325,11 +327,12 @@ function syncNavigationSelection() {
|
|
|
325
327
|
function restoreNavigationSelection() {
|
|
326
328
|
navigationRail?.select(false);
|
|
327
329
|
}
|
|
328
|
-
function closePage(restoreFocus = false) {
|
|
330
|
+
function closePage(restoreFocus = false, restoreNavigation = true) {
|
|
329
331
|
dragState?.cancel();
|
|
330
332
|
pageGeneration++;
|
|
331
333
|
closePagePopup();
|
|
332
334
|
closeEditor(false);
|
|
335
|
+
if (page) config.ui.releasePage(page, { restoreNavigation });
|
|
333
336
|
page?.remove();
|
|
334
337
|
page = null;
|
|
335
338
|
restoreNavigationSelection();
|
|
@@ -362,7 +365,7 @@ function openPage() {
|
|
|
362
365
|
pageFocus = document.activeElement;
|
|
363
366
|
page = element("section");
|
|
364
367
|
page.setAttribute(MARKER, "page");
|
|
365
|
-
config.ui.page(page);
|
|
368
|
+
config.ui.page(page, { sidebar:"none" });
|
|
366
369
|
page.setAttribute("aria-label", text("Prompts", "提示词"));
|
|
367
370
|
document.body.append(page);
|
|
368
371
|
// 只隔离主内容的焦点,保留导航栏与原生窗口控件。
|
|
@@ -726,6 +729,18 @@ function orderControls(item, ordered, isCategory, row) {
|
|
|
726
729
|
const parent = row.parentElement;
|
|
727
730
|
const siblings = [...parent.children].filter(node => node.dataset.orderGroup === group);
|
|
728
731
|
const originalNext = row.nextSibling;
|
|
732
|
+
const rect = row.getBoundingClientRect();
|
|
733
|
+
const radius = getComputedStyle(row).borderRadius;
|
|
734
|
+
const dragImage = row.cloneNode(true);
|
|
735
|
+
dragImage.removeAttribute("data-order-group");
|
|
736
|
+
dragImage.removeAttribute("data-order-id");
|
|
737
|
+
dragImage.setAttribute("aria-hidden", "true");
|
|
738
|
+
Object.assign(dragImage.style, { position:"fixed", left:"-10000px", top:"0", width:`${rect.width}px`,
|
|
739
|
+
height:`${rect.height}px`, margin:"0", boxSizing:"border-box", pointerEvents:"none",
|
|
740
|
+
background:"var(--mcx-color-surface)", borderRadius:radius, overflow:"hidden", clipPath:`inset(0 round ${radius})` });
|
|
741
|
+
// 显式裁切拖影,避免浏览器自动截图留下矩形外边界。
|
|
742
|
+
parent.append(dragImage);
|
|
743
|
+
event.dataTransfer.setDragImage(dragImage, event.clientX - rect.left, event.clientY - rect.top);
|
|
729
744
|
const placeholder = element("div", "pm-drop-placeholder");
|
|
730
745
|
placeholder.style.height = `${row.getBoundingClientRect().height}px`;
|
|
731
746
|
placeholder.setAttribute("aria-hidden", "true");
|
|
@@ -752,6 +767,7 @@ function orderControls(item, ordered, isCategory, row) {
|
|
|
752
767
|
if (finished) return;
|
|
753
768
|
finished = true;
|
|
754
769
|
lifetime.abort();
|
|
770
|
+
dragImage.remove();
|
|
755
771
|
dragState = null;
|
|
756
772
|
row.draggable = false;
|
|
757
773
|
if (!started) return;
|
|
@@ -768,8 +784,9 @@ function orderControls(item, ordered, isCategory, row) {
|
|
|
768
784
|
dragState = { cancel: () => finish(false) };
|
|
769
785
|
event.dataTransfer.effectAllowed = "move";
|
|
770
786
|
event.dataTransfer.setData("text/plain", item.id);
|
|
771
|
-
//
|
|
787
|
+
// 等浏览器截取拖影后再清理替身、隐藏原行。
|
|
772
788
|
requestAnimationFrame(() => {
|
|
789
|
+
dragImage.remove();
|
|
773
790
|
if (finished || !row.isConnected) return;
|
|
774
791
|
row.before(placeholder);
|
|
775
792
|
row.classList.add("pm-dragging");
|
|
@@ -1075,7 +1092,7 @@ function manageCategories() {
|
|
|
1075
1092
|
const target = editorShell(text("Categories", "分类"));
|
|
1076
1093
|
const footer = config.ui.footer(element("div", "pm-footer"));
|
|
1077
1094
|
footer.append(
|
|
1078
|
-
pageControl(text("New category", "新建分类"), () => editCategory()
|
|
1095
|
+
pageControl(text("New category", "新建分类"), () => editCategory()),
|
|
1079
1096
|
);
|
|
1080
1097
|
for (const category of categories) {
|
|
1081
1098
|
const row = element("div", "pm-row");
|
|
@@ -1291,11 +1308,11 @@ document.addEventListener(
|
|
|
1291
1308
|
event.target.closest("[data-app-navigation-rail] button") &&
|
|
1292
1309
|
!navButton?.contains(event.target)
|
|
1293
1310
|
)
|
|
1294
|
-
closePage();
|
|
1311
|
+
closePage(false, false);
|
|
1295
1312
|
},
|
|
1296
1313
|
{ signal, capture: true },
|
|
1297
1314
|
);
|
|
1298
|
-
window.addEventListener("popstate", () => closePage(), { signal });
|
|
1315
|
+
window.addEventListener("popstate", () => closePage(false, false), { signal });
|
|
1299
1316
|
window.addEventListener("resize", positionPage, { signal });
|
|
1300
1317
|
|
|
1301
1318
|
const observer = new MutationObserver(() => {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
## Token 与布局
|
|
10
10
|
|
|
11
|
+
状态栏实例跟随可见正式会话的原生 Composer root,不跟随 contenteditable 编辑节点。发送时编辑区暂时不可交互或重建,同一外壳、同一会话的状态栏继续显示并保留展开选择和统计;不以延时隐藏掩盖变化。真正切换会话时清除旧统计,进入 home、隐藏外壳或迁入浮动输入框时移除实例。宿主读取身份与 Renderer 使用同一个 `composerSurface` 契约。
|
|
12
|
+
|
|
11
13
|
| 角色 | Codex token | 用途 |
|
|
12
14
|
| --- | --- | --- |
|
|
13
15
|
| 次要文字 | `--color-text-tertiary` | 状态行文字与分隔符 |
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
界面文案跟随 Codex 的 `html[lang]`:中文语言使用简体中文,其他语言使用英文;运行中切换语言时同步更新状态行和已打开的明细浮窗。MineCodex Web 使用同样的中英文规则提供本功能开关。
|
|
26
28
|
|
|
27
|
-
TPS 根据 Codex 会话事件计算有效样本的会话平均约值,以 `N tok/s`
|
|
29
|
+
TPS 根据 Codex 会话事件计算有效样本的会话平均约值,以 `N tok/s` 呈现;分母仅扣除完整落在同一响应计时范围内、且不与已观测模型输出重叠的工具区间(含迟到事件,重叠区间仅扣一次);跨响应运行或与生成并行的后台工具不扣时,模型输出缺少结束时间时保守保留该样本的观测时长,不将其视为实时解码速度;缺少有效计时样本时显示 `—`。累计 token 与缓存比例来自 Codex 用量记录。上下文使用原生 Codex 桌面版的口径:最近请求 `last.totalTokens / modelContextWindow`,直接订阅原生会话的 `latestTokenUsageInfo`(`thread/tokenUsage/updated` 更新),压缩后与原生同步刷新,不依赖日志轮询;原生数据不可用时显示占位;状态栏显示百分环和数字,弹层显示用量、窗口容量和压缩次数。二级浮窗的 token 数按十进制换算为 K/M,省略 `tok` 后缀;TPS 仍保留 `tok/s`。日志可见历史只列有数据的指令、消息、工具交互三个短标签,估算值以 `~` 开头;工具结果同时接受字符串与内容数组,压缩后的 replacement_history 采用同一文本解析。6px 进度条使用 Codex 图标选择器的紫、蓝、绿配色,标签圆点保持同色;当前页面未暴露对应变量时使用 Codex 亮/暗主题原值。剩余的原生占用以浅灰表示;若估算合计超过原生占用,按比例缩放彩色段,让进度条终点始终对应原生总占用。不显示无法取得的工具定义,也不放解释性长文案。
|
|
28
30
|
|
|
29
31
|
上下文弹层按视觉归属排列:进度条下紧接三个可见历史分段,不加分段副标题;分隔线之后列原生占用合计、窗口容量和压缩次数。合计取原生最近请求用量,不是三个估算分段的求和。
|
|
30
32
|
|
|
@@ -35,7 +37,7 @@ TPS 标签后放置 12px Lucide info 图标,继承标签颜色,间距 6px。
|
|
|
35
37
|
### 折叠入口
|
|
36
38
|
- 首次安装默认展开;用户收起后在模型选择左侧显示 12px context 环,24px 热区;原生 context 环开启时隐藏原生重复入口,卸载时恢复。
|
|
37
39
|
- Tooltip 沿用原生 context 内容:上下文窗口、使用/剩余比例、已用/容量(k tokens);样式遵循通用 Tooltip。
|
|
38
|
-
- 点击环展开输入框下方详情,右侧 chevron-up
|
|
40
|
+
- 点击环展开输入框下方详情,右侧 chevron-up 收起。原生布局一次切换到目标状态,测量切换前后的位置,仅用 transform 衔接输入框与模型控件;状态行入场配合4px位移与透明度过渡;中途反向按当前位移衔接。展开220ms、收起160ms,曲线 `cubic-bezier(0.23,1,0.32,1)`。反向操作从当前可见位置和透明度继续,取消旧动画;退出内容临时脱离布局,结束后恢复并 display:none,不保留父级 gap。reduced-motion 立即切换,收起内容不参与键盘焦点。展开状态跨会话、主聊天和 side chat 共享并持久保存,展开时隐藏输入框内的 context 环;每个输入框订阅自己的会话上下文和统计。
|
|
39
41
|
- Cache 按原始 cachedInputTokens / inputTokens 计算:大于99%显示一位小数,其余取整数,状态栏和详情共用格式。
|
|
40
42
|
|
|
41
43
|
输入框实例仅绑定可见的 thread composer。全屏标签页中的隐藏聊天、浮动 Quick Chat(含 right-panel composer overlay)、inert 区域不注入;原生移动输入框或切换可见性时销毁实例并取消订阅,返回聊天后重建。
|
|
@@ -7,6 +7,7 @@ function estimatedTextTokens(value) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function estimatedContentTokens(content) {
|
|
10
|
+
if (typeof content === "string") return estimatedTextTokens(content);
|
|
10
11
|
if (!Array.isArray(content)) return 0;
|
|
11
12
|
return content.reduce((total, item) => {
|
|
12
13
|
if (item?.type === "input_image" || item?.type === "output_image") return total + IMAGE_TOKENS_ESTIMATE;
|
|
@@ -71,7 +72,7 @@ export function sessionFacts(record) {
|
|
|
71
72
|
if (p.type === "task_complete" && Number.isFinite(p.time_to_first_token_ms)) emit("first-token-measured", { elapsed:p.time_to_first_token_ms });
|
|
72
73
|
if (p.type === "item_completed") {
|
|
73
74
|
if (p.item?.type === "ContextCompaction") emit("compaction-finished");
|
|
74
|
-
if (MODEL_OUTPUT_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms)) emit("output-observed", { start:p.started_at_ms });
|
|
75
|
+
if (MODEL_OUTPUT_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms)) emit("output-observed", { start:p.started_at_ms, end:p.completed_at_ms });
|
|
75
76
|
if (TOOL_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms) && Number.isFinite(p.completed_at_ms)) emit("tool-measured", { start:p.started_at_ms, end:p.completed_at_ms });
|
|
76
77
|
}
|
|
77
78
|
if (p.type === "token_count") emit("usage-observed", { total:usage(p.info?.total_token_usage), last:usage(p.info?.last_token_usage), contextWindow:p.info?.model_context_window });
|
|
@@ -14,6 +14,7 @@ export function createSessionState() {
|
|
|
14
14
|
last: null,
|
|
15
15
|
contextWindow: null,
|
|
16
16
|
firstOutputAt: null,
|
|
17
|
+
pendingOutputIntervals: [],
|
|
17
18
|
pendingToolIntervals: [],
|
|
18
19
|
speedMeasurements: [],
|
|
19
20
|
ttftTotalMs: 0,
|
|
@@ -30,14 +31,17 @@ export function createSessionState() {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
function boundedToolWaitMs(intervals, start, end, outputs) {
|
|
35
|
+
// 仅扣同一响应内、与已观测生成不重叠的工具区间;后台进程存活时间不是等待时间。
|
|
36
|
+
if (outputs.some(([, outputEnd]) => !Number.isFinite(outputEnd))) return 0;
|
|
37
|
+
const waits = intervals
|
|
38
|
+
.filter(([toolStart, toolEnd]) => toolStart >= start && toolEnd <= end
|
|
39
|
+
&& !outputs.some(([outputStart, outputEnd]) => toolStart < outputEnd && toolEnd > outputStart))
|
|
36
40
|
.filter(([left, right]) => right > left)
|
|
37
41
|
.sort((left, right) => left[0] - right[0]);
|
|
38
42
|
let elapsed = 0;
|
|
39
43
|
let rightEdge = start;
|
|
40
|
-
for (const [left, right] of
|
|
44
|
+
for (const [left, right] of waits) {
|
|
41
45
|
elapsed += Math.max(0, right - Math.max(left, rightEdge));
|
|
42
46
|
rightEdge = Math.max(rightEdge, right);
|
|
43
47
|
}
|
|
@@ -45,7 +49,7 @@ function overlappingToolMs(intervals, start, end) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
export function applySessionStatusEvent(state, event) {
|
|
48
|
-
const reset = () => { state.firstOutputAt=null; state.pendingToolIntervals=[]; };
|
|
52
|
+
const reset = () => { state.firstOutputAt=null; state.pendingOutputIntervals=[]; state.pendingToolIntervals=[]; };
|
|
49
53
|
switch (event.kind) {
|
|
50
54
|
case "history-replaced":
|
|
51
55
|
state.compactionRecords++; state.last=null; reset();
|
|
@@ -63,11 +67,14 @@ export function applySessionStatusEvent(state, event) {
|
|
|
63
67
|
case "turn-ended": reset(); if (event.id) state.runningTurns.delete(event.id); else state.runningTurns.clear(); break;
|
|
64
68
|
case "first-token-measured": state.ttftTotalMs+=event.elapsed; state.ttftSamples++; break;
|
|
65
69
|
case "compaction-finished": state.compactionCompleted++; break;
|
|
66
|
-
case "output-observed":
|
|
70
|
+
case "output-observed":
|
|
71
|
+
state.firstOutputAt=Math.min(state.firstOutputAt ?? event.start,event.start);
|
|
72
|
+
state.pendingOutputIntervals.push([event.start,event.end]);
|
|
73
|
+
break;
|
|
67
74
|
case "tool-measured":
|
|
68
75
|
state.toolMs+=Math.max(0,event.end-event.start);
|
|
69
76
|
state.pendingToolIntervals.push([event.start,event.end]);
|
|
70
|
-
//
|
|
77
|
+
// 工具记录可能晚到;是否属于可扣除等待区间,由样本的生成边界判断。
|
|
71
78
|
for (const sample of state.speedMeasurements) if (event.start < sample.end && event.end > sample.start) sample.tools.push([event.start,event.end]);
|
|
72
79
|
break;
|
|
73
80
|
case "usage-observed":
|
|
@@ -81,7 +88,8 @@ export function applySessionStatusEvent(state, event) {
|
|
|
81
88
|
state.responses.add(event.id);
|
|
82
89
|
const output=event.last?.output;
|
|
83
90
|
if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null && Number.isFinite(event.end) && event.end > state.firstOutputAt)
|
|
84
|
-
state.speedMeasurements.push({ start:state.firstOutputAt,end:event.end,output,
|
|
91
|
+
state.speedMeasurements.push({ start:state.firstOutputAt,end:event.end,output,
|
|
92
|
+
outputs:state.pendingOutputIntervals.slice(),tools:state.pendingToolIntervals.slice() });
|
|
85
93
|
}
|
|
86
94
|
reset();
|
|
87
95
|
if (event.total) { state.total=event.total; state.hasResponseUsage=true; }
|
|
@@ -96,7 +104,7 @@ export function sessionStatusSnapshot(state, threadId) {
|
|
|
96
104
|
let decodeMs = 0;
|
|
97
105
|
let speedSamples = 0;
|
|
98
106
|
for (const sample of state.speedMeasurements) {
|
|
99
|
-
const elapsed = sample.end - sample.start -
|
|
107
|
+
const elapsed = sample.end - sample.start - boundedToolWaitMs(sample.tools, sample.start, sample.end, sample.outputs);
|
|
100
108
|
if (elapsed <= 0 || elapsed >= 600_000) continue;
|
|
101
109
|
decodeTokens += sample.output;
|
|
102
110
|
decodeMs += elapsed;
|
|
@@ -35,9 +35,7 @@ style.id = STYLE_ID;
|
|
|
35
35
|
style.textContent = `
|
|
36
36
|
[${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 4px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--mcx-font-sans-default,inherit); font-size:var(--mcx-text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--mcx-color-text-tertiary); }
|
|
37
37
|
[${MARKER}][hidden] { display:none; }
|
|
38
|
-
[data-minecodex-session-reveal] { display:grid; grid-template-rows:
|
|
39
|
-
[data-minecodex-session-reveal][data-expanded] { grid-template-rows:1fr; opacity:1; }
|
|
40
|
-
@starting-style { [data-minecodex-session-reveal][data-expanded] { grid-template-rows:0fr; opacity:0; } }
|
|
38
|
+
[data-minecodex-session-reveal] { display:grid; grid-template-rows:1fr; }
|
|
41
39
|
[data-minecodex-session-reveal][data-expanded]:not([hidden]) { margin-top:-4px; }
|
|
42
40
|
[data-minecodex-session-reveal] > div { min-height:0; overflow:hidden; }
|
|
43
41
|
[data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
|
|
@@ -102,7 +100,7 @@ window.addEventListener("storage", (event) => {
|
|
|
102
100
|
}, { signal });
|
|
103
101
|
// Codex 会把离屏输入框搬到 Quick Chat 浮层;节点仍连接不代表当前页面是聊天。
|
|
104
102
|
function isActiveComposer(composer) {
|
|
105
|
-
return Boolean(config.codex.
|
|
103
|
+
return Boolean(config.codex.composerSurface(composer));
|
|
106
104
|
}
|
|
107
105
|
function createInstance(composerElement) {
|
|
108
106
|
let copy = selectedCopy();
|
|
@@ -156,15 +154,30 @@ document.body.append(tooltip);
|
|
|
156
154
|
let expanded = globalExpanded;
|
|
157
155
|
let collapseTimer = null;
|
|
158
156
|
let collapsing = false;
|
|
157
|
+
const motion = new Set();
|
|
158
|
+
function clearMotion() {
|
|
159
|
+
for (const animation of motion) animation.cancel();
|
|
160
|
+
motion.clear();
|
|
161
|
+
for (const key of ["position", "left", "top", "width"]) reveal.style.removeProperty(key);
|
|
162
|
+
}
|
|
159
163
|
const nativeIndicators = new Set();
|
|
160
164
|
function setExpanded(value) {
|
|
165
|
+
const card = reveal.previousElementSibling;
|
|
166
|
+
const model = card?.querySelector('[data-composer-navigation-target="reasoning"]');
|
|
167
|
+
const controls = model?.parentElement;
|
|
168
|
+
const before = card?.getBoundingClientRect();
|
|
169
|
+
const controlsBefore = controls?.getBoundingClientRect();
|
|
170
|
+
const revealBefore = reveal.getBoundingClientRect();
|
|
171
|
+
const opacity = reveal.hidden ? 0 : Number(getComputedStyle(reveal).opacity);
|
|
172
|
+
const animate = value !== expanded && !root.hidden && !matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
161
173
|
clearTimeout(collapseTimer);
|
|
162
|
-
|
|
174
|
+
clearMotion();
|
|
175
|
+
collapsing = animate && !value;
|
|
163
176
|
expanded = value;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
177
|
+
// 退出内容暂时脱离布局,原生输入框一次到达目标位置,再以位移衔接。
|
|
178
|
+
if (collapsing) Object.assign(reveal.style, {
|
|
179
|
+
position:"fixed", left:`${revealBefore.left}px`, top:`${revealBefore.top}px`, width:`${revealBefore.width}px`,
|
|
180
|
+
});
|
|
168
181
|
reveal.toggleAttribute("data-expanded", expanded);
|
|
169
182
|
clip.inert = !expanded;
|
|
170
183
|
trigger.setAttribute("aria-expanded", String(expanded));
|
|
@@ -172,6 +185,25 @@ function setExpanded(value) {
|
|
|
172
185
|
if (expanded) reveal.hidden = false;
|
|
173
186
|
syncVisibility(findComposer());
|
|
174
187
|
if (!expanded) closePanel();
|
|
188
|
+
if (!animate) return;
|
|
189
|
+
const duration = expanded ? 220 : 160;
|
|
190
|
+
const options = { duration, easing:"cubic-bezier(0.23,1,0.32,1)", fill:"both" };
|
|
191
|
+
const play = (node, frames) => { if (node) motion.add(node.animate(frames, options)); };
|
|
192
|
+
const after = card.getBoundingClientRect();
|
|
193
|
+
play(card, [{transform:`translateY(${before.top-after.top}px)`},{transform:"translateY(0)"}]);
|
|
194
|
+
if (controlsBefore) {
|
|
195
|
+
const after = controls.getBoundingClientRect();
|
|
196
|
+
play(controls, [{transform:`translateX(${controlsBefore.left-after.left}px)`},{transform:"translateX(0)"}]);
|
|
197
|
+
}
|
|
198
|
+
const offset = expanded && revealBefore.height ? revealBefore.top-reveal.getBoundingClientRect().top : -4;
|
|
199
|
+
play(reveal, [{opacity,transform:`translateY(${expanded ? offset : 0}px)`},
|
|
200
|
+
{opacity:expanded ? 1 : 0,transform:`translateY(${expanded ? 0 : -4}px)`}]);
|
|
201
|
+
if (!expanded) play(trigger, [{opacity:0},{opacity:1}]);
|
|
202
|
+
collapseTimer = setTimeout(() => {
|
|
203
|
+
collapsing = false;
|
|
204
|
+
clearMotion();
|
|
205
|
+
syncVisibility(findComposer());
|
|
206
|
+
}, duration);
|
|
175
207
|
}
|
|
176
208
|
trigger.addEventListener("click", () => setGlobalExpanded(!expanded), { signal });
|
|
177
209
|
function hideTooltip() { tooltip.hidden = true; trigger.removeAttribute("aria-describedby"); }
|
|
@@ -401,7 +433,7 @@ function findComposer() {
|
|
|
401
433
|
return isActiveComposer(composerElement) ? composerElement : null;
|
|
402
434
|
}
|
|
403
435
|
function currentThreadId(composer) {
|
|
404
|
-
return config.codex.
|
|
436
|
+
return composer ? config.codex.composerSurface(composer)?.threadId ?? null : null;
|
|
405
437
|
}
|
|
406
438
|
// 与原生 Context 指示器订阅同一会话状态;不以日志百分比覆盖它。
|
|
407
439
|
const emptyNativeContext = () => ({ contextUsedTokens:null,contextWindow:null,contextPercent:null });
|
|
@@ -445,6 +477,9 @@ function ensurePosition() {
|
|
|
445
477
|
const composer = findComposer();
|
|
446
478
|
const threadId = currentThreadId(composer);
|
|
447
479
|
if (threadId !== activeThreadId) {
|
|
480
|
+
clearTimeout(collapseTimer);
|
|
481
|
+
clearMotion();
|
|
482
|
+
collapsing = false;
|
|
448
483
|
activeThreadId = threadId;
|
|
449
484
|
nativeContext = emptyNativeContext();
|
|
450
485
|
// 会话建立后先展示占位,不让日志落盘速度决定状态栏是否出现。
|
|
@@ -547,6 +582,7 @@ return { update: receiver.update, setExpanded, dispose() {
|
|
|
547
582
|
nativeUsage.dispose();
|
|
548
583
|
clearTimeout(refreshTimer);
|
|
549
584
|
clearTimeout(collapseTimer);
|
|
585
|
+
clearMotion();
|
|
550
586
|
observer.disconnect();
|
|
551
587
|
localeObserver.disconnect();
|
|
552
588
|
closePanel();
|
|
@@ -560,7 +596,7 @@ function syncInstances() {
|
|
|
560
596
|
for (const [composer, instance] of instances) {
|
|
561
597
|
if (!isActiveComposer(composer)) { instance.dispose(); instances.delete(composer); }
|
|
562
598
|
}
|
|
563
|
-
for (const composer of document.querySelectorAll('[data-codex-composer
|
|
599
|
+
for (const composer of document.querySelectorAll('[data-codex-composer-root]')) {
|
|
564
600
|
if (isActiveComposer(composer) && !instances.has(composer)) instances.set(composer, createInstance(composer));
|
|
565
601
|
}
|
|
566
602
|
}
|
package/package.json
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
| 接口 | 结果与约束 |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| `composer(element?, {threadOnly}?)` | 默认仅可见的 thread 输入框;Prompts
|
|
9
|
+
| `composer(element?, {threadOnly, purpose}?)` | 默认仅可见的 Codex thread 输入框;Prompts 使用 `purpose:"insertion"` 支持 Chat、Work、Codex、Temporary Chat 与面板 chat bar。优先当前焦点所在输入框,并可显式绑定插入目标。返回 editable/root/body/add/threadId;非thread不提供身份,隐藏祖先返回 null |
|
|
10
10
|
| `currentThreadId(element?)` | 从合格输入框取得 UUID;不从残留 sidebar 选中项猜当前线程 |
|
|
11
|
+
| `composerSurface(element?)` | 返回可见正式会话外壳 root/threadId,独立于编辑区是否存在或暂时 inert;供会话状态呈现与宿主统计订阅使用。真正隐藏、移入浮层、切换 home 或卸载时返回 null;不能作为文字插入许可 |
|
|
11
12
|
| `rail()` / `railSlot(ownGroup)` | 可见原生 rail 与模板/滚动区/固定组句柄;不包含插件菜单策略 |
|
|
12
13
|
| `currentFiber(element)` / `props(element,predicate,limit?)` | 仅供 native Adapter 使用;优先匹配当前 committed props,支持最近祖先的 scope |
|
|
13
14
|
| `report(name,available,reason?)` / `diagnostics()` | 最新能力状态的副本;不持有凭据、原始账户数据或用户内容 |
|
|
@@ -18,12 +18,17 @@ export function createCodexEnvironment() {
|
|
|
18
18
|
}
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
function composer(element = null, {threadOnly=true} = {}) {
|
|
22
|
-
const
|
|
21
|
+
function composer(element = null, {threadOnly=true, purpose="thread"} = {}) {
|
|
22
|
+
const insertion = purpose === "insertion";
|
|
23
|
+
const rootSelector = insertion ? '[data-codex-composer-root], [data-chatgpt-composer]' : '[data-codex-composer-root]';
|
|
24
|
+
const candidates = element ? [element] : [...document.querySelectorAll(insertion
|
|
25
|
+
? '[data-codex-composer="true"][contenteditable="true"], [data-chatgpt-composer] [data-composer-markdown][contenteditable="true"]'
|
|
26
|
+
: '[data-codex-composer="true"][contenteditable="true"]')];
|
|
27
|
+
if (!element && insertion) candidates.sort((a,b) => Number(Boolean(b.closest(rootSelector)?.contains(document.activeElement))) - Number(Boolean(a.closest(rootSelector)?.contains(document.activeElement))));
|
|
23
28
|
for (const editable of candidates) {
|
|
24
|
-
const root = editable.closest(
|
|
25
|
-
if (!editable.isConnected || !root || (threadOnly && root.getAttribute('data-composer-placement') !== 'thread')
|
|
26
|
-
|| editable.closest('[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
|
|
29
|
+
const root = editable.closest(rootSelector);
|
|
30
|
+
if (!editable.isConnected || !root || (!insertion && threadOnly && root.getAttribute('data-composer-placement') !== 'thread')
|
|
31
|
+
|| editable.closest(insertion ? '[hidden], [inert], [aria-hidden="true"]' : '[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
|
|
27
32
|
|| !editable.checkVisibility({ visibilityProperty:true })) continue;
|
|
28
33
|
const value = root.getAttribute('data-composer-placement') === 'thread' ? root.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id') : null;
|
|
29
34
|
const threadId = value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
|
|
@@ -34,6 +39,19 @@ export function createCodexEnvironment() {
|
|
|
34
39
|
report("composer", false, "active-thread-composer-unavailable");
|
|
35
40
|
return null;
|
|
36
41
|
}
|
|
42
|
+
// 状态属于会话外壳;发送时编辑区可能 inert 或重建,不代表离开会话。
|
|
43
|
+
function composerSurface(element = null) {
|
|
44
|
+
const roots = element ? [element.closest('[data-codex-composer-root]')] : document.querySelectorAll('[data-codex-composer-root]');
|
|
45
|
+
for (const root of roots) {
|
|
46
|
+
if (!root?.isConnected || root.getAttribute('data-composer-placement') !== 'thread'
|
|
47
|
+
|| root.closest('[hidden], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
|
|
48
|
+
|| !root.checkVisibility({visibilityProperty:true})) continue;
|
|
49
|
+
const value = root.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
|
|
50
|
+
const threadId = value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
|
|
51
|
+
return {root,threadId};
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
37
55
|
function rail() {
|
|
38
56
|
const node = [...document.querySelectorAll('[data-app-navigation-rail]')].find(node => node.getBoundingClientRect().width > 0) ?? null;
|
|
39
57
|
report("navigation-rail", Boolean(node), node ? null : "rail-unavailable");
|
|
@@ -47,7 +65,7 @@ export function createCodexEnvironment() {
|
|
|
47
65
|
const group = [...(scroll?.children ?? [])].find(child => child !== ownGroup && child.classList.contains('flex-col') && child.classList.contains('shrink-0'));
|
|
48
66
|
return node && template && scroll ? { rail:node, template, scroll, group } : null;
|
|
49
67
|
}
|
|
50
|
-
return { composer, rail, railSlot, currentThreadId:element => composer(element)?.threadId ?? null,
|
|
68
|
+
return { composer, composerSurface, rail, railSlot, currentThreadId:element => composer(element)?.threadId ?? null,
|
|
51
69
|
// 仅供 UI/插件 native Adapter 使用,交互层不读取 React props。
|
|
52
70
|
currentFiber, props, report, diagnostics:() => [...capabilities.values()].map(item => ({...item})) };
|
|
53
71
|
}
|
|
@@ -812,10 +812,7 @@ export function createInjectionSource(features, {
|
|
|
812
812
|
}
|
|
813
813
|
|
|
814
814
|
function currentComposer() {
|
|
815
|
-
|
|
816
|
-
if (marked) return marked;
|
|
817
|
-
return Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]'))
|
|
818
|
-
.find((element) => isVisibleToolbarControl(element)) ?? null;
|
|
815
|
+
return codex.composer(null, { purpose: "insertion" })?.editable ?? null;
|
|
819
816
|
}
|
|
820
817
|
|
|
821
818
|
function rememberComposerSelection() {
|
|
@@ -828,9 +825,9 @@ export function createInjectionSource(features, {
|
|
|
828
825
|
savedComposerThread = threadIdentity();
|
|
829
826
|
}
|
|
830
827
|
|
|
831
|
-
function insertComposerText(text) {
|
|
828
|
+
function insertComposerText(text, target = null) {
|
|
832
829
|
if (typeof text !== "string" || !text) throw new Error("Text is required");
|
|
833
|
-
const composer = currentComposer();
|
|
830
|
+
const composer = target ? codex.composer(target, { purpose: "insertion" })?.editable : currentComposer();
|
|
834
831
|
if (!composer) throw new Error("The active Composer was not found");
|
|
835
832
|
const selection = document.getSelection();
|
|
836
833
|
let fallback = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LocalImageSource } from "../../../features/images/src/backend/local-image-source.mjs";
|
|
1
2
|
import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
|
|
2
3
|
// CodexRuntime:Codex Renderer 生命周期与 host action 执行。
|
|
3
4
|
// 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
} from "./host-actions.mjs";
|
|
35
36
|
import { SitesService } from "../../../features/sites/src/backend/sites-service.mjs";
|
|
36
37
|
import { SessionStatusReader, threadIdFromLocation } from "../../../features/session-status/src/adapters/codex/session-reader.mjs";
|
|
38
|
+
import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
// 保持公共入口兼容:历史 import 从本文件继续可用。
|
|
@@ -48,11 +50,9 @@ export {
|
|
|
48
50
|
} from "./codex-injection.mjs";
|
|
49
51
|
|
|
50
52
|
export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return card.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
|
|
55
|
-
}).filter(Boolean))];
|
|
53
|
+
const codex = (${createCodexEnvironment.toString()})();
|
|
54
|
+
return [...new Set([...document.querySelectorAll('[data-codex-composer-root]')]
|
|
55
|
+
.map(root => codex.composerSurface(root)?.threadId).filter(Boolean))];
|
|
56
56
|
})()`;
|
|
57
57
|
|
|
58
58
|
export class CodexRuntime {
|
|
@@ -79,6 +79,7 @@ export class CodexRuntime {
|
|
|
79
79
|
}) {
|
|
80
80
|
this.features = features;
|
|
81
81
|
this.sitesService = sitesService ?? (features.some(feature => feature.id === "sites") ? new SitesService() : null);
|
|
82
|
+
this.localImageSource = features.some(feature => feature.id === "images") ? new LocalImageSource({dataDir:path.join(profileDir,"minecodex-image-gen")}) : null;
|
|
82
83
|
this.profileDir = profileDir;
|
|
83
84
|
this.appPath = appPath;
|
|
84
85
|
this.cdpPort = cdpPort;
|
|
@@ -549,6 +550,15 @@ export class CodexRuntime {
|
|
|
549
550
|
});
|
|
550
551
|
return;
|
|
551
552
|
}
|
|
553
|
+
if (feature.id === "images" && request.action === "local-image-source") {
|
|
554
|
+
try {
|
|
555
|
+
const result = await this.localImageSource.action(request.payload);
|
|
556
|
+
await this.resolveHostAction(client, request.requestId, {ok:true,result});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
await this.resolveHostAction(client, request.requestId, {ok:false,error:{code:"LOCAL_IMAGE_SOURCE_FAILED",message:error.message}});
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
552
562
|
if (feature.id === "sites" && request.action === "sites") {
|
|
553
563
|
try {
|
|
554
564
|
const result = await this.sitesService.action(request.payload);
|
|
@@ -812,6 +822,7 @@ export class CodexRuntime {
|
|
|
812
822
|
if (this.stopPromise) return this.stopPromise;
|
|
813
823
|
this.stopPromise = (async () => {
|
|
814
824
|
this.stopping = true;
|
|
825
|
+
await this.localImageSource?.close();
|
|
815
826
|
this.targetRefreshQueued = false;
|
|
816
827
|
this.monitorWake?.();
|
|
817
828
|
await this.monitorPromise?.catch(() => {});
|
package/packages/ui/DESIGN.md
CHANGED
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
- 原生适配层:解析实际token、主题、zoom和能力状态;共享锚点仅包含真实跨插件需求。原生事实遵循根规范,不在每个组件重新猜测。
|
|
14
14
|
- 材质与层级基础:按Dialog、Dropdown、Popover、ComposerMenu、Page的原生分支提供有限选项,将遮罩与Portal规则同所选组件匹配。
|
|
15
15
|
- Dialog:提供容器、标题、Close及操作区的结构;插件填表单、列表或确认内容。
|
|
16
|
-
- Button
|
|
17
|
-
- Navbar:使用成对 default/selected 图标管理展示状态;原生图标与选中状态适配集中在 native 层,保留原生路由与 sidebar hover
|
|
16
|
+
- Button:通过 `action(node, "danger-solid")` 提供原生实色警戒分支,保留既有 `danger` soft 分支;通用 token 与适用规则见根 DESIGN.md。表达操作角色和状态,保留Cancel ghost、实际次级outline与主操作solid的语义区分;业务动作由插件传入。
|
|
17
|
+
- Navbar:使用成对 default/selected 图标管理展示状态;原生图标与选中状态适配集中在 native 层,保留原生路由与 sidebar hover。退出注入页面时按当前 committed 原生状态恢复选中属性,不回放进入时的旧状态;原生目的地变化时及时移除替身图标,原节点复用或重渲染后仍保证只显示一个图标。
|
|
18
18
|
- Tooltip:处理声明式标记、hover/focus、aria关联、关闭与卸载,不接管原生Tooltip。
|
|
19
|
+
- Page 的 `sidebar:"none"` 分支通过 native/page Adapter 取得原生无上下文侧栏页面壳;`releasePage` 释放时区分返回原页面和用户主动导航,禁用时清理。
|
|
19
20
|
- 页面、菜单与浮层:提供对应类型的外壳、挂载和通用交互,不读取提示词、会话或站点业务数据。
|
|
20
21
|
|
|
21
22
|
## 接入约束
|
|
22
23
|
|
|
23
24
|
当前page-script通过 `config.ui` 使用公共能力;现有API包括 `heading`、`footer`、`action`、`overlay`、`surface`。`surface(node, kind, options)` 显式区分原生材质;`page`、`mount`、`trapFocus`、`composerMenu`、`tooltipSurface`、`navigationRail` 维护实际复用的层级、焦点与定位。`entry.resources` 通过 `config.resources` 提供插件自身 CSS 与图标,通用视觉仍由组件库维护。
|
|
24
25
|
|
|
26
|
+
独立全页面且无专属二级导航的功能使用 `config.ui.page(node, { sidebar:"none" })`。移除页面前调用 `config.ui.releasePage(node)`;若用户正在主动选择原生导航,则传 `{ restoreNavigation:false }`,避免恢复动作覆盖用户选择。默认 `sidebar:"inherit"` 保留现有接入行为,不自动迁移其他功能。页面壳的产品规则与验收标准见根 DESIGN.md 的「全页面功能的页面壳选择」。
|
|
27
|
+
|
|
25
28
|
Renderer安装函数经序列化注入,必须自包含。组件代码与Node/CDP隔离,宿主负责组装注入源码、生命周期和iframe主题桥;不为整理目录引入不必要的打包系统。
|
|
26
29
|
|
|
27
30
|
通用生命周期契约由组件库维护。原生已经提供的焦点圈定、Escape和关闭机制优先复用;插件继续拥有业务状态、内容更新和具体回调。迁移时同步触发点焦点恢复、异步卸载及原生节点归属验证。
|
|
@@ -11,6 +11,8 @@ export function installButton(signal, ui) {
|
|
|
11
11
|
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=primary]:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-primary-solid-hover); }
|
|
12
12
|
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger] { box-shadow:none; background:var(--mcx-color-background-danger-soft-alpha); color:var(--mcx-color-text-danger); }
|
|
13
13
|
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger]:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-danger-soft-alpha-hover); }
|
|
14
|
+
[data-mcx-ui-action=danger-solid], [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger-solid] { box-shadow:none; border-color:transparent; background:var(--mcx-color-background-danger-solid); color:var(--mcx-color-text-danger-solid); }
|
|
15
|
+
[data-mcx-ui-action=danger-solid]:hover:not(:disabled):not([aria-disabled=true]), [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger-solid]:hover:not(:disabled):not([aria-disabled=true]) { background:color-mix(in oklab,var(--mcx-color-background-danger-solid) 90%,transparent); }
|
|
14
16
|
[data-mcx-ui-surface=dialog] button:disabled { opacity:.4; cursor:default; }
|
|
15
17
|
[data-mcx-ui-surface=dialog] [data-mcx-ui-close]:focus-visible, [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>button:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:0; }
|
|
16
18
|
`;
|
|
@@ -5,16 +5,19 @@ export function installNavigationRail(signal, ui, native) {
|
|
|
5
5
|
style.setAttribute("data-minecodex-ui-styles", "navigation-rail");
|
|
6
6
|
style.textContent = `[data-mcx-native-rail-icon-hidden]{display:none!important}`;
|
|
7
7
|
document.head.append(style);
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
function restoreNode(node, state) {
|
|
9
|
+
state.icon?.removeAttribute("data-mcx-native-rail-icon-hidden");
|
|
10
|
+
state.replacement?.remove();
|
|
11
|
+
if (!node.isConnected) return;
|
|
12
|
+
// 路由可能已变化,恢复当前 committed props,不回放旧选中态。
|
|
13
|
+
const attributes = native.attributes(node, [...state.attributes.keys()]) ?? state.attributes;
|
|
14
|
+
for (const [name, value] of attributes) {
|
|
15
|
+
if (value === null) node.removeAttribute(name);
|
|
16
|
+
else node.setAttribute(name, value);
|
|
17
17
|
}
|
|
18
|
+
}
|
|
19
|
+
function restore() {
|
|
20
|
+
for (const [node, state] of snapshots) restoreNode(node, state);
|
|
18
21
|
snapshots.clear();
|
|
19
22
|
}
|
|
20
23
|
function selection(button, selected) {
|
|
@@ -38,6 +41,10 @@ export function installNavigationRail(signal, ui, native) {
|
|
|
38
41
|
if (!icon) continue;
|
|
39
42
|
const item = native.item(node);
|
|
40
43
|
const active = item ? item.isCurrentDestination : node.getAttribute("aria-current") === "page";
|
|
44
|
+
if (!active && snapshots.has(node)) {
|
|
45
|
+
restoreNode(node, snapshots.get(node));
|
|
46
|
+
snapshots.delete(node);
|
|
47
|
+
}
|
|
41
48
|
if (!active && !snapshots.has(node)) native.remember(node, icon);
|
|
42
49
|
if (!selected || (!active && !node.hasAttribute("data-selected"))) continue;
|
|
43
50
|
let state = snapshots.get(node);
|
|
@@ -46,7 +53,7 @@ export function installNavigationRail(signal, ui, native) {
|
|
|
46
53
|
.map(name => [name, node.getAttribute(name)])) };
|
|
47
54
|
snapshots.set(node, state);
|
|
48
55
|
}
|
|
49
|
-
if (active && state.icon !== icon) {
|
|
56
|
+
if (active && (state.icon !== icon || !state.replacement?.isConnected)) {
|
|
50
57
|
state.icon?.removeAttribute("data-mcx-native-rail-icon-hidden");
|
|
51
58
|
state.replacement?.remove();
|
|
52
59
|
state.icon = icon;
|
|
@@ -57,6 +64,7 @@ export function installNavigationRail(signal, ui, native) {
|
|
|
57
64
|
icon.setAttribute("data-mcx-native-rail-icon-hidden", "");
|
|
58
65
|
}
|
|
59
66
|
}
|
|
67
|
+
if (state.replacement?.isConnected) state.icon?.setAttribute("data-mcx-native-rail-icon-hidden", "");
|
|
60
68
|
node.removeAttribute("data-selected");
|
|
61
69
|
node.removeAttribute("aria-current");
|
|
62
70
|
if (!node.hasAttribute("data-suppress-active-style")) node.setAttribute("data-suppress-active-style", "");
|