minecodex 2.0.2 → 2.0.3
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 +149 -0
- package/apps/dashboard/DESIGN.md +45 -0
- package/features/images/DESIGN.md +71 -0
- package/features/images/README.md +2 -0
- package/features/images/codex-feature.json +2 -1
- package/features/images/src/adapters/codex/cloud-import.mjs +52 -0
- package/features/images/src/adapters/codex/image-prompt.mjs +22 -0
- package/features/images/src/adapters/codex/library-api.mjs +55 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +18 -0
- package/features/images/src/adapters/codex/prompt-resolver-source.mjs +8 -0
- package/features/images/src/backend/cloud-library.mjs +10 -0
- package/features/images/src/domain/library-prompt.mjs +7 -0
- package/features/images/src/library-prompt-page.mjs +7 -75
- package/features/images/src/library-prompt-source.mjs +10 -0
- package/features/images/web/app.js +9 -9
- package/features/images/web/styles.css +219 -219
- package/features/model-slider/DESIGN.md +34 -0
- package/features/model-slider/src/adapters/codex/controller.mjs +93 -0
- package/features/model-slider/src/domain/model-rules.mjs +143 -0
- package/features/model-slider/src/frontend/install.mjs +2110 -0
- package/features/model-slider/src/injection-source.mjs +10 -0
- package/features/notes/DESIGN.md +13 -15
- package/features/notes/README.md +7 -1
- package/features/notes/assets/icons/book-open-nav-fill.svg +1 -0
- package/features/notes/codex-feature.json +5 -1
- package/features/notes/src/{default-prompts.mjs → backend/default-prompts.mjs} +1 -1
- package/features/notes/src/{product.mjs → backend/product.mjs} +1 -1
- package/features/notes/src/{quick-prompts-server.mjs → backend/quick-prompts-server.mjs} +2 -2
- package/features/notes/src/{quick-prompts-page.mjs → frontend/entry.mjs} +64 -257
- package/features/notes/src/frontend/icons.json +13 -0
- package/features/notes/src/frontend/styles.css +110 -0
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/vendor/phosphor-LICENSE +21 -0
- package/features/session-status/DESIGN.md +7 -7
- package/features/session-status/README.md +4 -0
- package/features/session-status/codex-feature.json +1 -1
- package/features/session-status/src/adapters/codex/context-usage.mjs +39 -0
- package/features/session-status/src/adapters/codex/session-log.mjs +81 -0
- package/features/session-status/src/adapters/codex/session-reader.mjs +106 -0
- package/features/session-status/src/domain/session-stats.mjs +141 -0
- package/features/session-status/src/injection-source.mjs +6 -0
- package/features/session-status/src/session-status.mjs +41 -101
- package/features/sidebar-focus/DESIGN.md +6 -5
- package/features/sidebar-focus/codex-feature.json +2 -1
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +28 -0
- package/features/sidebar-focus/src/injection-source.mjs +6 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +19 -66
- package/features/sites/DESIGN.md +60 -0
- package/features/sites/README.md +27 -0
- package/features/sites/codex-feature.json +28 -0
- package/features/sites/src/adapters/codex/catalog.mjs +26 -0
- package/features/sites/src/adapters/codex/renderer.mjs +83 -0
- package/features/sites/src/backend/discovery.mjs +73 -0
- package/features/sites/src/backend/project-sites.mjs +34 -0
- package/features/sites/src/backend/site-launcher.mjs +54 -0
- package/features/sites/src/backend/sites-preview.mjs +53 -0
- package/features/sites/src/backend/sites-service.mjs +162 -0
- package/features/sites/src/domain/site.mjs +40 -0
- package/features/sites/src/frontend/entry.mjs +383 -0
- package/features/sites/src/frontend/icons.json +12 -0
- package/features/sites/src/frontend/styles.css +72 -0
- package/features/sites/src/injection-source.mjs +7 -0
- package/features/sites/vendor/lucide-LICENSE +35 -0
- package/features/unread-inbox/DESIGN.md +6 -2
- package/features/unread-inbox/codex-feature.json +2 -1
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +66 -0
- package/features/unread-inbox/src/injection-source.mjs +6 -0
- package/features/unread-inbox/src/unread-inbox.mjs +34 -89
- package/features/usage-meter/DESIGN.md +9 -9
- package/features/usage-meter/README.md +2 -1
- package/features/usage-meter/codex-feature.json +3 -3
- package/features/usage-meter/src/adapters/codex/renderer.mjs +61 -0
- package/features/usage-meter/src/injection-source.mjs +6 -0
- package/features/usage-meter/src/usage-meter.mjs +27 -66
- package/package.json +17 -4
- package/packages/cli/src/config.mjs +2 -1
- package/packages/codex-adapter/README.md +15 -0
- package/packages/codex-adapter/package.json +1 -0
- package/packages/codex-adapter/src/renderer/environment.mjs +53 -0
- package/packages/runtime-host/README.md +27 -0
- package/packages/runtime-host/src/codex-injection.mjs +32 -2393
- package/packages/runtime-host/src/codex-runtime.mjs +21 -3
- package/packages/runtime-host/src/feature-registry.mjs +28 -3
- package/packages/ui/DESIGN.md +31 -0
- package/packages/ui/package.json +8 -0
- package/packages/ui/src/components/button.mjs +21 -0
- package/packages/ui/src/components/composer-menu.mjs +18 -0
- package/packages/ui/src/components/dialog.mjs +55 -0
- package/packages/ui/src/components/navigation-rail.mjs +77 -0
- package/packages/ui/src/components/page.mjs +8 -0
- package/packages/ui/src/components/tooltip.mjs +96 -0
- package/packages/ui/src/foundation/layers.mjs +14 -0
- package/packages/ui/src/foundation/surfaces.mjs +34 -0
- package/packages/ui/src/install.mjs +22 -0
- package/packages/ui/src/native/navigation-rail.mjs +33 -0
- package/packages/ui/src/native/theme.mjs +35 -0
- package/packages/{runtime-host/src/codex-design-contract.mjs → ui/src/native/tokens.mjs} +67 -24
- package/packages/runtime-host/src/cloud-images.mjs +0 -123
- package/packages/runtime-host/src/feature-tooltips.mjs +0 -65
- package/packages/runtime-host/src/session-status-reader.mjs +0 -340
- /package/features/notes/src/{prompt-schema.mjs → backend/prompt-schema.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloudImagesAction } from "
|
|
1
|
+
import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
|
|
2
2
|
// CodexRuntime:Codex Renderer 生命周期与 host action 执行。
|
|
3
3
|
// 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
|
|
4
4
|
|
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
attachFilesToComposer,
|
|
33
33
|
createImageEditThread,
|
|
34
34
|
} from "./host-actions.mjs";
|
|
35
|
-
import {
|
|
35
|
+
import { SitesService } from "../../../features/sites/src/backend/sites-service.mjs";
|
|
36
|
+
import { SessionStatusReader, threadIdFromLocation } from "../../../features/session-status/src/adapters/codex/session-reader.mjs";
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
// 保持公共入口兼容:历史 import 从本文件继续可用。
|
|
@@ -73,9 +74,11 @@ export class CodexRuntime {
|
|
|
73
74
|
featureInstanceId = null,
|
|
74
75
|
runtimeSessionId = randomBytes(16).toString("hex"),
|
|
75
76
|
onStatusChange = null,
|
|
77
|
+
sitesService = null,
|
|
76
78
|
onManagedCodexPidChange = null,
|
|
77
79
|
}) {
|
|
78
80
|
this.features = features;
|
|
81
|
+
this.sitesService = sitesService ?? (features.some(feature => feature.id === "sites") ? new SitesService() : null);
|
|
79
82
|
this.profileDir = profileDir;
|
|
80
83
|
this.appPath = appPath;
|
|
81
84
|
this.cdpPort = cdpPort;
|
|
@@ -546,6 +549,15 @@ export class CodexRuntime {
|
|
|
546
549
|
});
|
|
547
550
|
return;
|
|
548
551
|
}
|
|
552
|
+
if (feature.id === "sites" && request.action === "sites") {
|
|
553
|
+
try {
|
|
554
|
+
const result = await this.sitesService.action(request.payload);
|
|
555
|
+
await this.resolveHostAction(client, request.requestId, { ok: true, result });
|
|
556
|
+
} catch (error) {
|
|
557
|
+
await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "SITES_REQUEST_FAILED", message: error.message } });
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
549
561
|
if (feature.id === "notes" && request.action === "quick-prompts") {
|
|
550
562
|
try {
|
|
551
563
|
const { route, method = "GET", body } = request.payload ?? {};
|
|
@@ -575,7 +587,12 @@ export class CodexRuntime {
|
|
|
575
587
|
}
|
|
576
588
|
if (["list-cloud-images", "import-cloud-image"].includes(request.action)) {
|
|
577
589
|
try {
|
|
578
|
-
let result = await cloudImagesAction(
|
|
590
|
+
let result = await cloudImagesAction(
|
|
591
|
+
async (expression, options) => evaluationValue(await client.send("Runtime.evaluate", {
|
|
592
|
+
expression, awaitPromise:true, returnByValue:true,
|
|
593
|
+
}, options)),
|
|
594
|
+
request.action === "list-cloud-images" ? "list" : "import", request.payload,
|
|
595
|
+
);
|
|
579
596
|
if (request.action === "import-cloud-image" && !result.skipped) {
|
|
580
597
|
result = await this.importGeneratedImage(feature, result);
|
|
581
598
|
}
|
|
@@ -791,6 +808,7 @@ export class CodexRuntime {
|
|
|
791
808
|
}
|
|
792
809
|
|
|
793
810
|
async stop() {
|
|
811
|
+
this.sitesService?.launcher?.stop();
|
|
794
812
|
if (this.stopPromise) return this.stopPromise;
|
|
795
813
|
this.stopPromise = (async () => {
|
|
796
814
|
this.stopping = true;
|
|
@@ -3,9 +3,11 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
|
|
7
8
|
const MANIFEST_NAME = "codex-feature.json";
|
|
8
9
|
const HOST_ACTIONS = new Set([
|
|
10
|
+
"sites",
|
|
9
11
|
"quick-prompts",
|
|
10
12
|
"insert-text",
|
|
11
13
|
"attach-file",
|
|
@@ -177,11 +179,34 @@ async function normalizePageScript(manifest, projectDir) {
|
|
|
177
179
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
178
180
|
throw new Error("page-script resource must stay inside the feature project");
|
|
179
181
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
let source = await readFile(scriptPath, "utf8");
|
|
183
|
+
const factory = manifest.entry?.sourceFactory;
|
|
184
|
+
if (factory) {
|
|
185
|
+
const factoryPath = path.resolve(projectDir, factory);
|
|
186
|
+
const relativeFactory = path.relative(projectDir, factoryPath);
|
|
187
|
+
if (relativeFactory.startsWith("..") || path.isAbsolute(relativeFactory)) {
|
|
188
|
+
throw new Error("page-script source factory must stay inside the feature project");
|
|
189
|
+
}
|
|
190
|
+
const { createPageScriptSource } = await import(pathToFileURL(factoryPath).href);
|
|
191
|
+
if (typeof createPageScriptSource !== "function") throw new Error("page-script source factory must export createPageScriptSource");
|
|
192
|
+
source = await createPageScriptSource();
|
|
193
|
+
}
|
|
194
|
+
if (typeof source !== "string" || !source.trim()) {
|
|
182
195
|
throw new Error("page-script resource must be a non-empty script file");
|
|
183
196
|
}
|
|
184
|
-
|
|
197
|
+
// 资源随脚本一次传入 Renderer,不增加 HTTP 加载或模块闭包依赖。
|
|
198
|
+
const resources = {};
|
|
199
|
+
for (const [name, file] of Object.entries(manifest.entry?.resources ?? {})) {
|
|
200
|
+
if (typeof file !== "string") throw new Error("page-script resources must be file paths");
|
|
201
|
+
const resourcePath = path.resolve(projectDir, file);
|
|
202
|
+
const relativeResource = path.relative(projectDir, resourcePath);
|
|
203
|
+
if (relativeResource.startsWith("..") || path.isAbsolute(relativeResource)) {
|
|
204
|
+
throw new Error("page-script resources must stay inside the feature project");
|
|
205
|
+
}
|
|
206
|
+
const content = await readFile(resourcePath, "utf8");
|
|
207
|
+
resources[name] = file.endsWith(".json") ? JSON.parse(content) : content;
|
|
208
|
+
}
|
|
209
|
+
return { resource, source, resources };
|
|
185
210
|
}
|
|
186
211
|
|
|
187
212
|
async function normalizeFeature(manifest, projectDir, env) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# MineCodex UI 组件库的局部约定
|
|
2
|
+
|
|
3
|
+
通用视觉与原生适配规则只维护在根 [DESIGN.md](../../DESIGN.md)。内部文档与代码路由见根 [AGENTS.md](../../AGENTS.md),架构见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
4
|
+
|
|
5
|
+
本级只负责共享组件自身的结构、接入与实现边界,不维护另一份颜色、字体、材质、遮罩或sidebar规则。
|
|
6
|
+
|
|
7
|
+
## 当前阶段
|
|
8
|
+
|
|
9
|
+
`packages/ui` 已建立独立私有 workspace,组件、token、安装源码与验证均在本级维护;实际目录见架构文档。未使用的原生组件分支不会仅因规范列出而预建空壳。
|
|
10
|
+
|
|
11
|
+
## 组件职责
|
|
12
|
+
|
|
13
|
+
- 原生适配层:解析实际token、主题、zoom和能力状态;共享锚点仅包含真实跨插件需求。原生事实遵循根规范,不在每个组件重新猜测。
|
|
14
|
+
- 材质与层级基础:按Dialog、Dropdown、Popover、ComposerMenu、Page的原生分支提供有限选项,将遮罩与Portal规则同所选组件匹配。
|
|
15
|
+
- Dialog:提供容器、标题、Close及操作区的结构;插件填表单、列表或确认内容。
|
|
16
|
+
- Button:表达操作角色和状态,保留Cancel ghost、实际次级outline与主操作solid的语义区分;业务动作由插件传入。
|
|
17
|
+
- Navbar:使用成对 default/selected 图标管理展示状态;原生图标与选中状态适配集中在 native 层,保留原生路由与 sidebar hover。
|
|
18
|
+
- Tooltip:处理声明式标记、hover/focus、aria关联、关闭与卸载,不接管原生Tooltip。
|
|
19
|
+
- 页面、菜单与浮层:提供对应类型的外壳、挂载和通用交互,不读取提示词、会话或站点业务数据。
|
|
20
|
+
|
|
21
|
+
## 接入约束
|
|
22
|
+
|
|
23
|
+
当前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
|
+
Renderer安装函数经序列化注入,必须自包含。组件代码与Node/CDP隔离,宿主负责组装注入源码、生命周期和iframe主题桥;不为整理目录引入不必要的打包系统。
|
|
26
|
+
|
|
27
|
+
通用生命周期契约由组件库维护。原生已经提供的焦点圈定、Escape和关闭机制优先复用;插件继续拥有业务状态、内容更新和具体回调。迁移时同步触发点焦点恢复、异步卸载及原生节点归属验证。
|
|
28
|
+
|
|
29
|
+
## 验证边界
|
|
30
|
+
|
|
31
|
+
组件验证关注可注入性、结构与回调、状态清理、挂载/焦点以及所选原生类型的实际行为,不镜像每条CSS写实现测试。布局、材质和原生状态变化按风险做隔离验收;只移动文件时验证导入、序列化、发布名单与已有功能回归。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 随组件库组装为自包含 Renderer 安装函数。
|
|
2
|
+
export function installButton(signal, ui) {
|
|
3
|
+
const style = document.createElement("style");
|
|
4
|
+
style.setAttribute("data-minecodex-ui-styles", "installButton");
|
|
5
|
+
style.textContent = ` [data-mcx-ui-surface=dialog] [data-mcx-ui-footer] { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:12px; margin-top:12px; }
|
|
6
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>button { display:inline-flex; align-items:center; justify-content:center; gap:var(--mcx-button-gap-lg); height:var(--mcx-control-size-lg); flex-shrink:0; padding:0 calc(var(--mcx-control-gutter-md) * var(--mcx-control-gutter-pill-scaling)); border:0; border-radius:var(--mcx-radius-full); font:inherit; font-size:var(--mcx-control-font-size-md); font-weight:400; line-height:1; white-space:nowrap; color:var(--mcx-color-text-primary); background:transparent; box-shadow:inset 0 0 0 1px var(--mcx-color-border-primary-outline); cursor:var(--cursor-interaction); }
|
|
7
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>button:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-primary-outline-hover); }
|
|
8
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=ghost] { color:var(--mcx-color-text-secondary); box-shadow:none; }
|
|
9
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=ghost]:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-primary-ghost-hover); }
|
|
10
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=primary] { box-shadow:none; background:var(--mcx-color-background-primary-solid); color:var(--mcx-color-text-primary-solid); }
|
|
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
|
+
[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
|
+
[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-surface=dialog] button:disabled { opacity:.4; cursor:default; }
|
|
15
|
+
[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
|
+
`;
|
|
17
|
+
document.head.append(style);
|
|
18
|
+
signal.addEventListener("abort", () => style.remove(), { once:true });
|
|
19
|
+
const mark = (node, role, value = "") => { node.setAttribute(`data-mcx-ui-${role}`, value); return node; };
|
|
20
|
+
return { footer:node=>mark(node,"footer"), action:(node,tone="secondary")=>mark(node,"action",tone) };
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// 与原生 Add 一样锚定输入框主体;diff 与请求队列不参与位置计算。
|
|
2
|
+
export function installComposerMenu(signal, ui) {
|
|
3
|
+
return {
|
|
4
|
+
composerMenu(node, anchor, { gap = 4, maxHeight = 400 } = {}) {
|
|
5
|
+
const body = anchor.closest("[data-composer-body]");
|
|
6
|
+
if (!body) throw new Error("Composer body is unavailable");
|
|
7
|
+
ui.surface(node, "composer-menu");
|
|
8
|
+
ui.layer(node, "floating");
|
|
9
|
+
const rect = body.getBoundingClientRect();
|
|
10
|
+
const zoom = Number.parseFloat(getComputedStyle(node).zoom) || 1;
|
|
11
|
+
node.style.left = `${rect.left / zoom}px`;
|
|
12
|
+
node.style.top = `${(rect.top - gap) / zoom}px`;
|
|
13
|
+
node.style.width = `${rect.width / zoom}px`;
|
|
14
|
+
node.style.maxHeight = `${Math.max(0, Math.min(maxHeight, (rect.top - 16) / zoom))}px`;
|
|
15
|
+
return node;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 随组件库组装为自包含 Renderer 安装函数。
|
|
2
|
+
export function installDialog(signal, ui) {
|
|
3
|
+
const style = document.createElement("style");
|
|
4
|
+
style.setAttribute("data-minecodex-ui-styles", "installDialog");
|
|
5
|
+
style.textContent = ` [data-mcx-ui-surface=dialog]:not(dialog) { position:relative; }
|
|
6
|
+
[data-mcx-ui-surface=dialog] { box-sizing:border-box; padding:var(--mcx-modal-container-inner-padding); border:0; border-radius:var(--mcx-radius-3xl); corner-shape:round; color:var(--mcx-color-text-primary); font-family:var(--mcx-font-sans-default); font-size:var(--mcx-text-base); line-height:1.5; outline:none; }
|
|
7
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-heading] { display:flex; align-items:center; gap:8px; padding-right:28px; margin:0 0 12px; }
|
|
8
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-heading] h2 { flex:1; min-width:0; margin:0; font-size:var(--mcx-text-heading-md); line-height:1.4; font-weight:var(--mcx-font-weight-semibold); letter-spacing:normal; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
9
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-close] { position:absolute; top:16px; right:16px; display:flex; align-items:center; justify-content:center; width:24px; height:24px; padding:4px; border:0; border-radius:4px; background:transparent; color:color-mix(in srgb,var(--mcx-color-text-primary) 80%,transparent); cursor:var(--cursor-interaction); }
|
|
10
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-close] svg { display:block; width:16px; height:16px; }
|
|
11
|
+
[data-mcx-ui-surface=dialog] [data-mcx-ui-close]:hover { background:var(--mcx-color-background-primary-ghost-hover); }
|
|
12
|
+
`;
|
|
13
|
+
document.head.append(style);
|
|
14
|
+
signal.addEventListener("abort", () => style.remove(), { once:true });
|
|
15
|
+
function heading(container, { title, closeLabel, onClose, back, variant = "opaque" }) {
|
|
16
|
+
ui.surface(container, "dialog", { variant });
|
|
17
|
+
const header = document.createElement("div");
|
|
18
|
+
header.setAttribute("data-mcx-ui-heading", "");
|
|
19
|
+
const h = document.createElement("h2"); h.textContent = title;
|
|
20
|
+
if (back) header.append(back);
|
|
21
|
+
header.append(h);
|
|
22
|
+
const close = document.createElement("button");
|
|
23
|
+
close.setAttribute("data-mcx-ui-close", "");
|
|
24
|
+
close.type = "button"; close.setAttribute("aria-label", closeLabel);
|
|
25
|
+
// 取自新版 Sites 原生 Close,保留其真实路径。
|
|
26
|
+
close.innerHTML = '<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>';
|
|
27
|
+
close.addEventListener("click", onClose);
|
|
28
|
+
container.append(header, close);
|
|
29
|
+
return header;
|
|
30
|
+
}
|
|
31
|
+
function trapFocus(getContainer, { onClose, isBlocked = () => false }) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const stop = () => { controller.abort(); signal.removeEventListener("abort", stop); };
|
|
34
|
+
signal.addEventListener("abort", stop, { once:true });
|
|
35
|
+
document.addEventListener("keydown", event => {
|
|
36
|
+
const container = getContainer();
|
|
37
|
+
if (!container || isBlocked()) return;
|
|
38
|
+
if (event.key === "Escape") {
|
|
39
|
+
event.preventDefault(); event.stopImmediatePropagation(); onClose(); return;
|
|
40
|
+
}
|
|
41
|
+
if (event.key !== "Tab") return;
|
|
42
|
+
const controls = [...container.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex="0"]')]
|
|
43
|
+
.filter(node => !node.closest("[inert]") && node.getClientRects().length);
|
|
44
|
+
const first = controls[0], last = controls.at(-1);
|
|
45
|
+
if (!first) { event.preventDefault(); container.focus(); }
|
|
46
|
+
else if (event.shiftKey && (document.activeElement === first || document.activeElement === container)) {
|
|
47
|
+
event.preventDefault(); last.focus();
|
|
48
|
+
} else if (!event.shiftKey && (document.activeElement === last || document.activeElement === container)) {
|
|
49
|
+
event.preventDefault(); first.focus();
|
|
50
|
+
}
|
|
51
|
+
}, { signal:controller.signal, capture:true });
|
|
52
|
+
return stop;
|
|
53
|
+
}
|
|
54
|
+
return { heading, trapFocus };
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// 导航只覆盖展示状态;原生路由与 sidebar hover 行为仍由 Codex 管理。
|
|
2
|
+
export function installNavigationRail(signal, ui, native) {
|
|
3
|
+
const snapshots = new Map();
|
|
4
|
+
const style = document.createElement("style");
|
|
5
|
+
style.setAttribute("data-minecodex-ui-styles", "navigation-rail");
|
|
6
|
+
style.textContent = `[data-mcx-native-rail-icon-hidden]{display:none!important}`;
|
|
7
|
+
document.head.append(style);
|
|
8
|
+
function restore() {
|
|
9
|
+
for (const [node, state] of snapshots) {
|
|
10
|
+
state.icon?.removeAttribute("data-mcx-native-rail-icon-hidden");
|
|
11
|
+
state.replacement?.remove();
|
|
12
|
+
if (!node.isConnected) continue;
|
|
13
|
+
for (const [name, value] of state.attributes) {
|
|
14
|
+
if (value === null) node.removeAttribute(name);
|
|
15
|
+
else node.setAttribute(name, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
snapshots.clear();
|
|
19
|
+
}
|
|
20
|
+
function selection(button, selected) {
|
|
21
|
+
if (selected) {
|
|
22
|
+
if (!button.hasAttribute("data-selected")) button.setAttribute("data-selected", "");
|
|
23
|
+
if (button.getAttribute("aria-current") !== "page") button.setAttribute("aria-current", "page");
|
|
24
|
+
} else {
|
|
25
|
+
button.removeAttribute("data-selected");
|
|
26
|
+
button.removeAttribute("aria-current");
|
|
27
|
+
}
|
|
28
|
+
for (const glyph of button.querySelectorAll("[data-mcx-rail-glyph]")) {
|
|
29
|
+
glyph.style.display = glyph.getAttribute("data-mcx-rail-glyph") === (selected ? "selected" : "default") ? "" : "none";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function reconcile(button, selected) {
|
|
33
|
+
selection(button, selected);
|
|
34
|
+
if (!selected) restore();
|
|
35
|
+
const rail = button.closest("[data-app-navigation-rail]");
|
|
36
|
+
for (const node of rail?.querySelectorAll("[data-sidebar-destination]") ?? []) {
|
|
37
|
+
const icon = node.querySelector("svg:not([data-mcx-native-rail-icon])");
|
|
38
|
+
if (!icon) continue;
|
|
39
|
+
const item = native.item(node);
|
|
40
|
+
const active = item ? item.isCurrentDestination : node.getAttribute("aria-current") === "page";
|
|
41
|
+
if (!active && !snapshots.has(node)) native.remember(node, icon);
|
|
42
|
+
if (!selected || (!active && !node.hasAttribute("data-selected"))) continue;
|
|
43
|
+
let state = snapshots.get(node);
|
|
44
|
+
if (!state) {
|
|
45
|
+
state = { attributes:new Map(["data-selected", "aria-current", "data-suppress-active-style"]
|
|
46
|
+
.map(name => [name, node.getAttribute(name)])) };
|
|
47
|
+
snapshots.set(node, state);
|
|
48
|
+
}
|
|
49
|
+
if (active && state.icon !== icon) {
|
|
50
|
+
state.icon?.removeAttribute("data-mcx-native-rail-icon-hidden");
|
|
51
|
+
state.replacement?.remove();
|
|
52
|
+
state.icon = icon;
|
|
53
|
+
state.replacement = native.icon(node, icon);
|
|
54
|
+
if (state.replacement) {
|
|
55
|
+
state.replacement.setAttribute("data-mcx-native-rail-icon", "");
|
|
56
|
+
icon.after(state.replacement);
|
|
57
|
+
icon.setAttribute("data-mcx-native-rail-icon-hidden", "");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
node.removeAttribute("data-selected");
|
|
61
|
+
node.removeAttribute("aria-current");
|
|
62
|
+
if (!node.hasAttribute("data-suppress-active-style")) node.setAttribute("data-suppress-active-style", "");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function navigationRail(button, { defaultIcon, selectedIcon }) {
|
|
66
|
+
defaultIcon.setAttribute("data-mcx-rail-glyph", "default");
|
|
67
|
+
selectedIcon.setAttribute("data-mcx-rail-glyph", "selected");
|
|
68
|
+
defaultIcon.setAttribute("aria-hidden", "true");
|
|
69
|
+
selectedIcon.setAttribute("aria-hidden", "true");
|
|
70
|
+
defaultIcon.after(selectedIcon);
|
|
71
|
+
button.setAttribute("data-minecodex-tooltip-side", "right");
|
|
72
|
+
selection(button, false);
|
|
73
|
+
return { select:selected => reconcile(button, selected), restore };
|
|
74
|
+
}
|
|
75
|
+
signal.addEventListener("abort", () => { restore(); style.remove(); }, { once:true });
|
|
76
|
+
return { navigationRail };
|
|
77
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// 自包含函数随运行时注入;只处理 MineCodex 标记,不接管原生 tooltip。
|
|
2
|
+
export function installFeatureTooltips(signal) {
|
|
3
|
+
const attribute = "data-minecodex-tooltip";
|
|
4
|
+
const tip = document.createElement("div");
|
|
5
|
+
tip.id = "minecodex-feature-tooltip";
|
|
6
|
+
tip.setAttribute("role", "tooltip");
|
|
7
|
+
tip.hidden = true;
|
|
8
|
+
const style = document.createElement("style");
|
|
9
|
+
style.setAttribute("data-minecodex-ui-styles", "tooltip");
|
|
10
|
+
style.textContent = `
|
|
11
|
+
[data-mcx-ui-tooltip] { position:fixed;z-index:2147483001;pointer-events:none;box-sizing:border-box;
|
|
12
|
+
padding:calc(var(--mcx-spacing) * 1.25) calc(var(--mcx-spacing) * 3);border:1px solid var(--mcx-color-border-tooltip);border-radius:var(--mcx-radius-2xl);corner-shape:var(--mcx-codex-corner-shape);
|
|
13
|
+
background:var(--mcx-color-background-tooltip);color:var(--mcx-color-text-tooltip);box-shadow:var(--mcx-shadow-tooltip);
|
|
14
|
+
font-family:var(--mcx-font-sans-default);font-size:var(--mcx-text-sm);line-height:18px;
|
|
15
|
+
font-weight:var(--mcx-tooltip-compact-font-weight);letter-spacing:var(--mcx-tracking-tooltip);user-select:none; }
|
|
16
|
+
`;
|
|
17
|
+
document.head.append(style);
|
|
18
|
+
function tooltipSurface(node) { node.setAttribute("data-mcx-ui-tooltip", ""); return node; }
|
|
19
|
+
tooltipSurface(tip);
|
|
20
|
+
tip.style.cssText = `zoom:var(--codex-window-zoom,1);max-width:min(320px,calc(100vw - 16px));
|
|
21
|
+
text-align:center;overflow-wrap:anywhere;`;
|
|
22
|
+
let anchor = null;
|
|
23
|
+
let timer;
|
|
24
|
+
let frame;
|
|
25
|
+
let lastClosedAt = -Infinity;
|
|
26
|
+
function hide() {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
cancelAnimationFrame(frame);
|
|
29
|
+
if (anchor) {
|
|
30
|
+
const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(id => id && id !== tip.id);
|
|
31
|
+
if (ids.length) anchor.setAttribute("aria-describedby", ids.join(" "));
|
|
32
|
+
else anchor.removeAttribute("aria-describedby");
|
|
33
|
+
}
|
|
34
|
+
if (!tip.hidden) lastClosedAt = Date.now();
|
|
35
|
+
anchor = null;
|
|
36
|
+
tip.hidden = true;
|
|
37
|
+
}
|
|
38
|
+
function position() {
|
|
39
|
+
if (!anchor?.isConnected || !anchor.getClientRects().length || anchor.getAttribute("aria-expanded") === "true") return hide();
|
|
40
|
+
const zoom = Number.parseFloat(getComputedStyle(tip).zoom) || 1;
|
|
41
|
+
const r = anchor.getBoundingClientRect();
|
|
42
|
+
const w = tip.offsetWidth, h = tip.offsetHeight;
|
|
43
|
+
// navbar 固定向右;普通控件跟随宿主的上下侧偏好,统一 2px 间距。
|
|
44
|
+
const side = anchor.getAttribute("data-minecodex-tooltip-side")
|
|
45
|
+
|| (anchor.closest("[data-app-navigation-rail]") ? "right"
|
|
46
|
+
: getComputedStyle(anchor).getPropertyValue("--side-tooltip").trim() || "top");
|
|
47
|
+
const gap = 2 / zoom;
|
|
48
|
+
let x = (r.left + r.width / 2) / zoom - w / 2;
|
|
49
|
+
let y = side === "bottom" ? r.bottom / zoom + gap : r.top / zoom - h - gap;
|
|
50
|
+
if (side === "right" || side === "left") {
|
|
51
|
+
x = side === "right" ? r.right / zoom + gap : r.left / zoom - w - gap;
|
|
52
|
+
y = (r.top + r.height / 2) / zoom - h / 2;
|
|
53
|
+
if (x + w > innerWidth / zoom - 8) x = r.left / zoom - w - gap;
|
|
54
|
+
else if (x < 8) x = r.right / zoom + gap;
|
|
55
|
+
} else if (y < 8) y = r.bottom / zoom + gap;
|
|
56
|
+
else if (y + h > innerHeight / zoom - 8) y = r.top / zoom - h - gap;
|
|
57
|
+
tip.style.left = `${Math.max(8, Math.min(x, innerWidth / zoom - w - 8))}px`;
|
|
58
|
+
tip.style.top = `${Math.max(8, Math.min(y, innerHeight / zoom - h - 8))}px`;
|
|
59
|
+
frame = requestAnimationFrame(position);
|
|
60
|
+
}
|
|
61
|
+
function show(target, immediate = false) {
|
|
62
|
+
if (!target || target === anchor) return;
|
|
63
|
+
hide();
|
|
64
|
+
if (target.getAttribute("aria-expanded") === "true" || !target.getAttribute(attribute)) return;
|
|
65
|
+
anchor = target;
|
|
66
|
+
timer = setTimeout(() => {
|
|
67
|
+
if (!anchor?.isConnected) return hide();
|
|
68
|
+
if (!tip.isConnected) document.body.append(tip);
|
|
69
|
+
const theme = getComputedStyle(anchor);
|
|
70
|
+
for (const name of ["--radius-2xl", "--codex-corner-shape", "--color-background-tooltip", "--color-text-tooltip",
|
|
71
|
+
"--color-border-tooltip", "--shadow-tooltip", "--tooltip-compact-font-weight", "--tracking-tooltip",
|
|
72
|
+
"--spacing", "--text-sm", "--font-sans-default"]) {
|
|
73
|
+
const value = theme.getPropertyValue(name).trim();
|
|
74
|
+
if (value) tip.style.setProperty(name, value);
|
|
75
|
+
else tip.style.removeProperty(name);
|
|
76
|
+
}
|
|
77
|
+
tip.textContent = anchor.getAttribute(attribute);
|
|
78
|
+
tip.hidden = false;
|
|
79
|
+
const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean);
|
|
80
|
+
anchor.setAttribute("aria-describedby", [...new Set([...ids, tip.id])].join(" "));
|
|
81
|
+
position();
|
|
82
|
+
}, immediate || Date.now() - lastClosedAt < 300 ? 0 : 200);
|
|
83
|
+
}
|
|
84
|
+
const targetOf = event => event.target instanceof Element ? event.target.closest(`[${attribute}]`) : null;
|
|
85
|
+
document.addEventListener("pointerover", event => show(targetOf(event)), { signal });
|
|
86
|
+
document.addEventListener("pointerout", event => {
|
|
87
|
+
if (anchor && !anchor.contains(event.relatedTarget)) hide();
|
|
88
|
+
}, { signal });
|
|
89
|
+
document.addEventListener("focusin", event => show(targetOf(event), true), { signal });
|
|
90
|
+
document.addEventListener("focusout", hide, { signal });
|
|
91
|
+
document.addEventListener("pointerdown", hide, { signal, capture: true });
|
|
92
|
+
document.addEventListener("keydown", event => { if (event.key === "Escape") hide(); }, { signal });
|
|
93
|
+
window.addEventListener("blur", hide, { signal });
|
|
94
|
+
signal.addEventListener("abort", () => { hide(); tip.remove(); style.remove(); }, { once: true });
|
|
95
|
+
return { tooltipSurface };
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 挂载角色统一维护;模态 top layer 仍由原生 dialog 负责。
|
|
2
|
+
export function installLayers(signal) {
|
|
3
|
+
const style = document.createElement("style");
|
|
4
|
+
style.setAttribute("data-minecodex-ui-styles", "layers");
|
|
5
|
+
style.textContent = `
|
|
6
|
+
[data-mcx-ui-layer=page] { position:fixed; z-index:20; }
|
|
7
|
+
[data-mcx-ui-layer=floating], [data-mcx-ui-layer=modal] { position:fixed; z-index:50; }
|
|
8
|
+
`;
|
|
9
|
+
document.head.append(style);
|
|
10
|
+
signal.addEventListener("abort", () => style.remove(), { once:true });
|
|
11
|
+
function layer(node, role) { node.setAttribute("data-mcx-ui-layer", role); return node; }
|
|
12
|
+
function mount(node, role, parent = document.body) { layer(node, role); parent.append(node); return node; }
|
|
13
|
+
return { layer, mount };
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 材质按原生组件分支选择,定位和业务内容由调用方提供。
|
|
2
|
+
export function installSurfaces(signal, ui) {
|
|
3
|
+
const style = document.createElement("style");
|
|
4
|
+
style.setAttribute("data-minecodex-ui-styles", "surfaces");
|
|
5
|
+
style.textContent = `
|
|
6
|
+
[data-mcx-ui-surface] { background:var(--mcx-color-surface-elevated-secondary); backdrop-filter:none; -webkit-backdrop-filter:none; }
|
|
7
|
+
[data-mcx-ui-surface=dialog] { box-shadow:0 0 0 .5px var(--mcx-color-border),var(--mcx-shadow-lg); }
|
|
8
|
+
[data-mcx-ui-surface=dialog][data-mcx-ui-variant=default] { background:color-mix(in oklab,var(--mcx-color-surface-elevated-secondary) 90%,transparent); backdrop-filter:blur(var(--mcx-blur-xl)); -webkit-backdrop-filter:blur(var(--mcx-blur-xl)); }
|
|
9
|
+
[data-mcx-ui-overlay], [data-mcx-ui-surface=dialog]::backdrop { background:var(--lightningcss-light,#e3e3e380) var(--lightningcss-dark,#00000080); backdrop-filter:var(--mcx-dialog-backdrop-fade-filter); -webkit-backdrop-filter:var(--mcx-dialog-backdrop-fade-filter); }
|
|
10
|
+
[data-mcx-ui-overlay=default], [data-mcx-ui-surface=dialog][data-mcx-ui-variant=default]::backdrop { background:var(--mcx-color-dialog-overlay); backdrop-filter:none; -webkit-backdrop-filter:none; }
|
|
11
|
+
[data-mcx-ui-surface=dropdown] { background:color-mix(in oklab,var(--mcx-color-surface-elevated-secondary) 90%,transparent); box-shadow:0 0 0 var(--mcx-dropdown-overlay-ring-width,.5px) var(--mcx-color-border-overlay),var(--mcx-shadow-dropdown-overlay,var(--mcx-shadow-xl-spread)); backdrop-filter:blur(var(--mcx-blur-sm)); -webkit-backdrop-filter:blur(var(--mcx-blur-sm)); }
|
|
12
|
+
[data-mcx-ui-surface=dropdown][data-mcx-ui-variant=opaque] { background:var(--mcx-color-surface-elevated-secondary); backdrop-filter:none; -webkit-backdrop-filter:none; }
|
|
13
|
+
[data-mcx-ui-surface=popover] { box-shadow:0 0 0 var(--mcx-popover-overlay-ring-width,.5px) var(--mcx-color-border-overlay),var(--mcx-shadow-popover-overlay,var(--mcx-shadow-lg)); }
|
|
14
|
+
[data-mcx-ui-surface=popover][data-mcx-ui-variant=translucent] { background:color-mix(in oklab,var(--mcx-color-surface-elevated-secondary) 90%,transparent); }
|
|
15
|
+
[data-mcx-ui-surface=composer-menu] { border:var(--mcx-suggestion-menu-border-width,1px) solid var(--mcx-color-border); border-radius:var(--mcx-radius-suggestion-menu,var(--mcx-radius-2xl)); box-shadow:var(--mcx-suggestion-menu-shadow,none); }
|
|
16
|
+
[data-mcx-ui-surface=page] { border-radius:var(--mcx-radius-xl-base); background:var(--mcx-color-surface); box-shadow:none; }
|
|
17
|
+
[data-mcx-ui-surface=page][data-mcx-ui-variant=transparent] { background:transparent; }
|
|
18
|
+
`;
|
|
19
|
+
document.head.append(style);
|
|
20
|
+
signal.addEventListener("abort", () => style.remove(), { once:true });
|
|
21
|
+
function surface(node, kind = "popover", { variant, opaque = true } = {}) {
|
|
22
|
+
const defaults = { dialog:"opaque", dropdown:"menu", popover:opaque ? "opaque" : "translucent", "composer-menu":"floating", page:"opaque" };
|
|
23
|
+
if (!(kind in defaults)) throw new Error(`Unknown UI surface: ${kind}`);
|
|
24
|
+
if (kind !== "dialog" && kind !== "page") ui.layer(node, "floating");
|
|
25
|
+
node.setAttribute("data-mcx-ui-surface", kind);
|
|
26
|
+
node.setAttribute("data-mcx-ui-variant", variant ?? defaults[kind]);
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
function overlay(node, variant = "opaque") {
|
|
30
|
+
node.setAttribute("data-mcx-ui-overlay", variant);
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
return { surface, overlay };
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createNativeThemeAdapter } from "./native/theme.mjs";
|
|
2
|
+
import { createNativeRailAdapter } from "./native/navigation-rail.mjs";
|
|
3
|
+
import { installSurfaces } from "./foundation/surfaces.mjs";
|
|
4
|
+
import { installLayers } from "./foundation/layers.mjs";
|
|
5
|
+
import { installDialog } from "./components/dialog.mjs";
|
|
6
|
+
import { installButton } from "./components/button.mjs";
|
|
7
|
+
import { installPage } from "./components/page.mjs";
|
|
8
|
+
import { installComposerMenu } from "./components/composer-menu.mjs";
|
|
9
|
+
import { installNavigationRail } from "./components/navigation-rail.mjs";
|
|
10
|
+
import { installFeatureTooltips } from "./components/tooltip.mjs";
|
|
11
|
+
|
|
12
|
+
// 所有依赖在组装时显式写入源码,Renderer 不依赖 Node 模块闭包。
|
|
13
|
+
export function createUIInstallerSource() {
|
|
14
|
+
const installers = [installSurfaces, installLayers, installDialog, installButton, installPage, installComposerMenu];
|
|
15
|
+
return `function installFeatureUI(signal, context) {
|
|
16
|
+
const ui = {theme:(${createNativeThemeAdapter.toString()})(context.themeTokens,context.themeTokenAliases,context.themeBindings)};
|
|
17
|
+
${installers.map(install => `Object.assign(ui, (${install.toString()})(signal, ui));`).join("\n")}
|
|
18
|
+
Object.assign(ui, (${installNavigationRail.toString()})(signal, ui, (${createNativeRailAdapter.toString()})(context.codex)));
|
|
19
|
+
Object.assign(ui, (${installFeatureTooltips.toString()})(signal));
|
|
20
|
+
return ui;
|
|
21
|
+
}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 读取原生导航资源;不修改 React props,也不调用带 Hooks 的图标组件。
|
|
2
|
+
export function createNativeRailAdapter(codex) {
|
|
3
|
+
const defaultIcons = new WeakMap();
|
|
4
|
+
function nativeItem(node) {
|
|
5
|
+
for (let fiber = codex.currentFiber(node); fiber; fiber = fiber.return) {
|
|
6
|
+
const item = fiber.memoizedProps?.item;
|
|
7
|
+
if (item?.id === node.getAttribute("data-sidebar-destination")) return item;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function defaultIcon(node, current) {
|
|
12
|
+
const item = nativeItem(node);
|
|
13
|
+
const resource = item?.railIcons?.default;
|
|
14
|
+
const factory = resource?.createElement ?? resource?.type?.createElement;
|
|
15
|
+
// 原生图标工厂显式暴露无 Hooks 的 DOM API,包含 optical 与主题处理。
|
|
16
|
+
if (factory) return factory(document, current.getAttribute("class") || "");
|
|
17
|
+
if (resource && resource === item.railIcons.selected) return current.cloneNode(true);
|
|
18
|
+
const asset = resource?.props?.asset;
|
|
19
|
+
// 新版原生 IconAsset 的单色资源可直接读取,不调用带 Hooks 的 React 组件。
|
|
20
|
+
if (asset?.paint?.kind === "monochrome" && typeof asset.body === "string") {
|
|
21
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
22
|
+
for (const [name, value] of Object.entries({ width:asset.canvas.width, height:asset.canvas.height,
|
|
23
|
+
viewBox:asset.canvas.viewBox, "aria-hidden":"true", class:current.getAttribute("class") || "" })) {
|
|
24
|
+
svg.setAttribute(name, String(value));
|
|
25
|
+
}
|
|
26
|
+
svg.innerHTML = asset.body;
|
|
27
|
+
return svg;
|
|
28
|
+
}
|
|
29
|
+
return defaultIcons.get(node)?.cloneNode(true) ?? null;
|
|
30
|
+
}
|
|
31
|
+
return { item:nativeItem, icon:defaultIcon,
|
|
32
|
+
remember:(node, icon) => defaultIcons.set(node, icon.cloneNode(true)) };
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// 将外部 token 名称解析为 MineCodex 变量;主题变化与卸载由此维护。
|
|
2
|
+
export function createNativeThemeAdapter(themeTokenNames,themeTokenAliases,themeBindings) {
|
|
3
|
+
const previousThemeBindings=new Map();
|
|
4
|
+
function read() {
|
|
5
|
+
const root = document.documentElement;
|
|
6
|
+
const style = getComputedStyle(root);
|
|
7
|
+
const tokens = Object.fromEntries(
|
|
8
|
+
themeTokenNames
|
|
9
|
+
.map((name) => [name, style.getPropertyValue(name).trim()])
|
|
10
|
+
.filter(([, value]) => value),
|
|
11
|
+
);
|
|
12
|
+
// 按语义和命名空间解析,不依赖构建哈希或猜测近似名称。
|
|
13
|
+
for (const name of themeTokenNames) {
|
|
14
|
+
if (tokens[name]) continue;
|
|
15
|
+
const semanticNames = [name, ...(themeTokenAliases[name] ?? [])];
|
|
16
|
+
const candidates = semanticNames.flatMap((candidate) => candidate.startsWith("--color-")
|
|
17
|
+
? [candidate, candidate.replace("--color-", "--app-color-")]
|
|
18
|
+
: [candidate]);
|
|
19
|
+
const value = candidates.map((candidate) => style.getPropertyValue(candidate).trim()).find(Boolean);
|
|
20
|
+
if (value) tokens[name] = value;
|
|
21
|
+
}
|
|
22
|
+
for (const [nativeName,internalName] of Object.entries(themeBindings)) {
|
|
23
|
+
if (!previousThemeBindings.has(internalName)) previousThemeBindings.set(internalName,root.style.getPropertyValue(internalName));
|
|
24
|
+
if (tokens[nativeName]) {tokens[internalName]=tokens[nativeName];if(root.style.getPropertyValue(internalName)!==tokens[nativeName]) root.style.setProperty(internalName,tokens[nativeName]);}
|
|
25
|
+
else if(root.style.getPropertyValue(internalName)) root.style.removeProperty(internalName);
|
|
26
|
+
}
|
|
27
|
+
const mode = root.dataset.theme === "dark" || root.classList.contains("electron-dark") || style.colorScheme === "dark"
|
|
28
|
+
? "dark"
|
|
29
|
+
: "light";
|
|
30
|
+
return { root, style, tokens, mode };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dispose() {for (const [name,value] of previousThemeBindings) { if(value) document.documentElement.style.setProperty(name,value); else document.documentElement.style.removeProperty(name); } }
|
|
34
|
+
return {read,dispose};
|
|
35
|
+
}
|