minecodex 2.0.5 → 2.0.7
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 +19 -0
- package/README.md +5 -1
- package/apps/dashboard/DESIGN.md +53 -31
- package/apps/dashboard/app.js +55 -39
- package/apps/dashboard/assets/OFL.txt +93 -0
- package/apps/dashboard/assets/info.svg +15 -0
- package/apps/dashboard/assets/moon.svg +13 -0
- package/apps/dashboard/assets/refresh-cw.svg +16 -0
- package/apps/dashboard/assets/silkscreen.ttf +0 -0
- package/apps/dashboard/assets/sun.svg +21 -0
- package/apps/dashboard/assets/terrain.js +89 -0
- package/apps/dashboard/assets/triangle-alert.svg +15 -0
- package/apps/dashboard/favicon.svg +12 -17
- package/apps/dashboard/index.html +24 -18
- package/apps/dashboard/styles.css +147 -181
- package/assets/brand/README.md +19 -0
- package/assets/brand/minecodex-icon-dark.svg +9 -0
- package/assets/brand/minecodex-icon-light.svg +9 -0
- package/assets/brand/minecodex-icon.svg +8 -0
- package/assets/brand/references/lucide-LICENSE.txt +43 -0
- package/assets/readme/hero.svg +93 -0
- package/features/hide-upsell-banner/README.md +7 -5
- package/features/hide-upsell-banner/codex-feature.json +2 -2
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
- package/features/images/DESIGN.md +18 -3
- package/features/images/README.md +4 -3
- package/features/images/codex-feature.json +2 -2
- package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
- package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
- package/features/images/src/assets/prompts-menu.svg +15 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
- package/features/images/src/frontend/local-image-source.mjs +2 -1
- package/features/images/src/library-prompt-page.mjs +10 -4
- package/features/images/src/library-prompt-source.mjs +4 -5
- package/features/model-slider/codex-feature.json +2 -2
- package/features/notes/DESIGN.md +1 -1
- package/features/notes/codex-feature.json +2 -2
- package/features/notes/src/frontend/entry.mjs +1 -1
- package/features/session-status/codex-feature.json +2 -2
- package/features/sidebar-focus/DESIGN.md +4 -4
- package/features/sidebar-focus/codex-feature.json +3 -2
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
- package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
- package/features/sites/DESIGN.md +25 -3
- package/features/sites/README.md +6 -2
- package/features/sites/codex-feature.json +3 -2
- package/features/sites/src/adapters/codex/renderer.mjs +25 -4
- package/features/sites/src/backend/project-sites.mjs +18 -6
- package/features/sites/src/backend/sites-service.mjs +22 -10
- package/features/sites/src/domain/discovery-candidates.mjs +53 -0
- package/features/sites/src/frontend/entry.mjs +76 -38
- package/features/sites/src/frontend/styles.css +3 -3
- package/features/unread-inbox/DESIGN.md +24 -25
- package/features/unread-inbox/codex-feature.json +3 -2
- package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
- package/features/unread-inbox/src/domain/notifications.mjs +21 -0
- package/features/unread-inbox/src/injection-source.mjs +5 -2
- package/features/unread-inbox/src/unread-inbox.mjs +212 -297
- package/features/usage-meter/codex-feature.json +2 -2
- package/package.json +11 -3
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
- package/packages/cli/native/README.md +31 -0
- package/packages/cli/src/browser-control-panel.mjs +48 -0
- package/packages/cli/src/commands.mjs +3 -1
- package/packages/cli/src/control-server.mjs +6 -3
- package/packages/cli/src/platform.mjs +5 -0
- package/packages/runtime-host/README.md +10 -5
- package/packages/runtime-host/src/codex-injection.mjs +12 -3
- package/packages/runtime-host/src/codex-runtime.mjs +10 -0
- package/packages/ui/DESIGN.md +5 -1
- package/packages/ui/assets/arrow-up-right.svg +13 -0
- package/packages/ui/assets/lucide-LICENSE.txt +43 -0
- package/packages/ui/assets/minecodex-menu-16.svg +7 -0
- package/packages/ui/assets/minecodex-menu-20.svg +7 -0
- package/packages/ui/src/components/account-menu.mjs +88 -0
- package/packages/ui/src/components/page.mjs +2 -2
- package/packages/ui/src/foundation/layers.mjs +1 -0
- package/packages/ui/src/install.mjs +3 -0
- package/packages/ui/src/native/account-menu.mjs +21 -0
- package/packages/ui/src/native/page.mjs +17 -2
- package/packages/ui/src/native/tokens.mjs +1 -0
package/DESIGN.md
CHANGED
|
@@ -116,6 +116,7 @@ sidebar规则分状态:
|
|
|
116
116
|
层级数字是本版本实现值,不是原生CSS设计token;共同约束应集中维护语义角色与挂载点。
|
|
117
117
|
|
|
118
118
|
- 内容页面位于原生sidebar悬浮层下方;MineCodex当前Page使用20,原生左侧栏40。
|
|
119
|
+
- 通知使用共享 `notification` 挂载角色(当前30),位于内容页面之上、原生sidebar与模态浮层之下;通知业务负责在原生侧栏 peeking 时暂停呈现,不抢占原生导航。
|
|
119
120
|
- 原生Dialog、Dropdown、Popover和Composer Floating父容器通常为50;同数值不保证相同层级,仍受挂载顺序与stacking context影响。
|
|
120
121
|
- 模态层挂在窗口根容器/原生Portal,遮罩覆盖整个窗口;不要嵌在Page的低层级容器内。原生`dialog.showModal()`使用top layer,不能仅用z-index推断。
|
|
121
122
|
- Tooltip使用专用角色与层级;现有MineCodex实现值不是原生标准,不传播为通用token。
|
|
@@ -171,3 +172,21 @@ Navbar 在入口右侧居中,使用 `data-minecodex-tooltip-side="right"`;
|
|
|
171
172
|
### Library 多选动作条的实色警戒按钮
|
|
172
173
|
|
|
173
174
|
Codex 26.930.21537(2026-10-02 原生源码与隔离窗口):Library 选中动作条的 Delete 使用 `dangerSolid`、full radius、toolbar size,图标 16px,警戒底与白色配对文字;hover 使用 danger solid 90% alpha,禁用保留原生 40% opacity 与禁止交互。确认弹窗也使用实色警戒提交按钮。共享 DOM 入口为 `ui.action(button, "danger-solid")`,和既有 soft `danger` 分支并存,不迁移原有功能的语义。通用组件只提供视觉和交互状态,删除确认、隐藏/回收站/永久删除的范围由所属功能明确告知。
|
|
175
|
+
|
|
176
|
+
## MineCodex 项目品牌图标
|
|
177
|
+
|
|
178
|
+
项目标识采用实心镐子与 Codex 原生六瓣外圈,唯一可编辑源为 `assets/brand/minecodex-icon.svg`。内部镐子使用 `translate(6.24 6.24) scale(.48)`,纯填充、无描边;保留已确认的留白比例。此标识代表 MineCodex,不替换插件功能图标、模型厂商图标或 Codex 原生应用图标。
|
|
179
|
+
|
|
180
|
+
内联使用 `currentColor` 跟随所在表面的语义文字色;外链明暗版本分别使用 `#0d0d0d` / `#fafafa`,透明背景。favicon 在 SVG 内通过 `prefers-color-scheme` 适配系统主题,Dashboard 标题通过 CSS mask 跟随页面 `--ink`。明暗版本、favicon 和 README 标识由 `npm run build:icons` 从同一源文件生成,修改几何后运行该命令。
|
|
181
|
+
|
|
182
|
+
macOS 后台服务 App 在明确的原生资源修订 1.0.1 中接入实心图标,采用浅色圆角底板,保证在明暗系统表面均可辨认。ICNS 来自同一项目几何。安装器先注销旧后台项,再验证并替换 bundle,最后重新注册;不能直接修改已安装的签名资源。后续普通 JavaScript 发布保留此原生 bundle 的签名身份,升级流程与验收边界见 `packages/cli/native/README.md`。
|
|
183
|
+
|
|
184
|
+
### 账号菜单中的 MineCodex 入口
|
|
185
|
+
|
|
186
|
+
Codex 26.930.21537(2026-10-03 安装包源码与隔离 DOM):`profile-dropdown-items` 的 Settings 使用原生菜单行,紧接 Help 前的 Separator;`onOpenSettings` / `settingsShortcut` 标识其行为,外层 `onClose` 关闭账号面板。MineCodex 行插在 Settings 与 Separator 之间,复制脱离 React 的原生行,保留原始 Settings 及其所属层级。左侧使用实心项目标识,右侧使用 Lucide ArrowUpRight;右侧沿用快捷键区域的间距,图标使用原生 Help 右侧箭头的 `text-tertiary` 色与 opacity 1(当前浅色为 rgba(13,13,13,0.482)),不套左侧图标的 0.75 / hover、focus 时 1 的透明度。隔离 DOM 确认左侧普通态与 Settings 同为 rgb(13,13,13)、opacity 0.75,hover / focus 时为 1。
|
|
187
|
+
|
|
188
|
+
左侧保留原生 CompactSource / LeadingSource 两套结构,只替换其中 SVG;分别继承原生 SVG 的 width、height、class 和 style。菜单标识复用当前 `codex-light-16` / `codex-light-20` 的真实填充外圈路径,引用存于 `assets/brand/references/codex-menu-16.svg` / `codex-menu-20.svg`;内部镐子仍从品牌源生成,由 `npm run build:icons` 输出两套 `packages/ui/assets/minecodex-menu-*.svg`。不能将 24px 品牌外圈缩小代替原生菜单的路径粗细。账号图标沿用原生 `currentColor` 与 `--profile-menu-icon-opacity`(桌面默认 0.75,行 hover / focus 时为 1),不另设固定灰色。此项为安装包源码核对,实际窗口视觉仍待手动验收。
|
|
189
|
+
|
|
190
|
+
右侧箭头的两段几何合并在单个 SVG path 中一次描边,避免 `currentColor` 自带 alpha 时,两个独立 path 在交叉点重复叠色。保留原资源的形状与端点规则;24px viewBox 中 stroke-width 为 1.575,在 16px 显示时实际线宽 1.05px,与原生 Help 箭头的线条粗细对齐。
|
|
191
|
+
|
|
192
|
+
入口随账号菜单重建恢复且只出现一次;补齐 Settings → MineCodex → Help 的上下键导航与 Enter/Space,保留原生 Escape 关闭行为。点击经已认证 Host bridge 调用 `mcx gui`;成功后关闭菜单,失败时显示实际错误。原生 UI 手动验收本次因 Computer Use 拒绝操作应用而未完成;隔离 DOM 契约页验证了位置、明暗颜色、点击、键盘、重建去重与清理。
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
|
|
|
16
16
|
| **Images** | 在原生 Images 中浏览本地 Image Gen 图片,查看并复制提示词 |
|
|
17
17
|
| **Hide Upsell Banner** | 隐藏输入框上方的用量提示横幅,仅优化界面展示,不影响正常的用量统计 |
|
|
18
18
|
| **Session Status** | 在输入框下方查看当前任务的步骤、观测速度、Token 用量与上下文明细 |
|
|
19
|
-
| **Task Switcher** |
|
|
19
|
+
| **Task Switcher** | 会话侧栏不可见时显示通知条,点击跳转到已完成未读或需要处理的会话 |
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
@@ -77,3 +77,7 @@ MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有
|
|
|
77
77
|
### Sidebar Focus / 侧栏聚焦
|
|
78
78
|
|
|
79
79
|
展开侧栏时,点击铃铛与搜索之间的聚焦按钮,保留当前任务路径并收起其他分支。再次点击恢复原先布局;切换任务时跟随路径,手动调整分支时退出聚焦。
|
|
80
|
+
|
|
81
|
+
### 从账号菜单打开控制台
|
|
82
|
+
|
|
83
|
+
在 Codex Navbar 底部打开账号菜单,点击 Settings 下方的 **MineCodex ↗**,即可在外部默认浏览器打开本地控制台,等同于 `mcx gui`。macOS 默认 Safari、Chrome/Chromium、Edge、Brave 和 Vivaldi 会定位已有控制台标签页,没有时才新建;首次定位可能需要系统自动化权限。其他浏览器与 Windows 仍由系统打开 URL。
|
package/apps/dashboard/DESIGN.md
CHANGED
|
@@ -1,45 +1,67 @@
|
|
|
1
1
|
# MineCodex Dashboard Design System
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The standalone control panel follows the root DESIGN.md semantic color and interaction
|
|
4
|
+
contract. It uses local light/dark tokens because no Codex renderer theme bridge exists here.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
appearance, uses one system sans family, and reserves accent color for enabled switches, focus,
|
|
7
|
-
and the primary restart action.
|
|
6
|
+
## Layout and reference
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
Reference: VPN Dashboard settings source, inspected 2026-10-03. Its card grid uses a 16px
|
|
9
|
+
radius/gap, top icon and trailing arrow, followed by a title and description. The live URL
|
|
10
|
+
required login; this reference is source-based, not a live visual match.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
MineCodex uses one centered 1064px column without a sidebar or category groups. The header
|
|
13
|
+
holds the shared brand and appearance control; a runtime notice appears below only when the service needs attention.
|
|
14
|
+
There is no tagline or visible section introduction. Save feedback remains available to screen readers.
|
|
15
|
+
Nine independent cards form a three-column grid, two columns at 960px and one at 600px.
|
|
16
|
+
Each card has an icon and switch at the top, name and purpose below, and a readable actual-state line
|
|
17
|
+
at the bottom of every card. Enabled switches and runtime state remain distinct information. A card is not itself clickable. The footer shows only the MineCodex version
|
|
18
|
+
and adjacent update check (16px gap, regular weight, refresh icon), with persistent update feedback below it. No install path or plugin versions.
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
- Surface: one subtle elevation step from the canvas.
|
|
15
|
-
- Ink: high-contrast neutral foreground.
|
|
16
|
-
- Secondary and tertiary ink: still meet AA for their text size.
|
|
17
|
-
- Border: quiet neutral separator.
|
|
18
|
-
- Accent: Codex-like blue used only for action and active state.
|
|
19
|
-
- Success and warning: paired with labels or icons, never color alone.
|
|
20
|
+
## Tokens and components
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
- Canvas: cool #edf1f7 / deep blue #101722, with a decorative ASCII voxel world.
|
|
23
|
+
- Surface: white / #1b2533, with a hard 3px lower edge for a block-like silhouette; no blur.
|
|
24
|
+
- Border: #d7dfe9 / #334359; blue #3478d4 / #71a8f3 switches and active state accents.
|
|
25
|
+
- Ink, secondary ink, accent, success, warning and danger use the local semantic CSS tokens.
|
|
26
|
+
- `--card-radius`: 8px; `--grid-gap`: 16px; `--card-padding`: 24px.
|
|
27
|
+
- Card minimum height: 216px desktop, 204px mobile; content can grow without clipping.
|
|
28
|
+
- System sans family; pixel brand title 36–64px/700, feature name 16px/600, description 14px/1.65,
|
|
29
|
+
state metadata 13px/1.5. Each description is one short statement of purpose; remove promotional filler while preserving each feature’s actual state.
|
|
30
|
+
- Feature icons: 24px display size, uniform 2.1-unit stroke in a 24×24 viewBox, matching Prompts. The control-server normalizes only dashboard output; original injected assets stay unchanged.
|
|
31
|
+
- Icons: existing manifest assets, with official Lucide bell, panel-left and globe for
|
|
32
|
+
Task Switcher, Sidebar Focus and Sites. Lucide ISC license is retained in
|
|
33
|
+
`packages/ui/assets/lucide-LICENSE.txt`.
|
|
34
|
+
- Checkbox switches expose switch semantics, feature labels, descriptions and state text.
|
|
35
|
+
Track is 36×21px with a 32px-high label hit area. Pending requests disable only their switch;
|
|
36
|
+
failures restore the previous value and focus. Enabled and active remain distinct facts.
|
|
37
|
+
- Appearance uses the VPN reference’s two-segment sun/moon capsule (32px segments, 4px gap and padding). A sliding selected circle and aria-pressed expose selection. It initially follows the system; manual choices persist locally.
|
|
38
|
+
- Restart and error notices use a neutral card, colored left edge, status icon, compact copy and trailing action; toast errors share that material. Restart notice appears only for a pending restart. No automatic restart.
|
|
39
|
+
- Update checks retain loading, result and retryable failure states; installation stays with
|
|
40
|
+
the existing `mcx update` flow.
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
plugin names 15px/600, body text 13px/1.5, and compact metadata 12px. No display type,
|
|
25
|
-
uppercase tracking, or fluid heading sizes.
|
|
42
|
+
## Motion
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
`--ease-out: cubic-bezier(0.23, 1, 0.32, 1)`. Switch thumb: 180ms transform;
|
|
45
|
+
button press: 140ms transform to scale(0.97). No stagger, card lift, layout motion or continuous
|
|
46
|
+
loading shimmer. Hover is limited to fine pointers. Keyboard actions disable transitions;
|
|
47
|
+
reduced-motion preferences disable movement. Focus rings remain visible in both themes.
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
needed, a vertical plugin settings group, and a low-emphasis diagnostics footer. At narrow widths
|
|
31
|
-
the rows wrap their status and switch controls beneath the copy without horizontal scrolling.
|
|
49
|
+
## Project identity
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
The masthead uses self-hosted Silkscreen Bold (SIL OFL, bundled license in assets/OFL.txt),
|
|
52
|
+
with a stone-colored face, dark outline and stepped text shadow inspired by Minecraft lettering.
|
|
53
|
+
The shared pickaxe mark is 76px desktop / 48px mobile. Body and state text retain the legible
|
|
54
|
+
project sans-serif contract. This is a dashboard-specific visual treatment; the brand geometry
|
|
55
|
+
and other injected plugin UI remain governed by root DESIGN.md.
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Restart notice: inline surface between header and plugin group, not a modal.
|
|
38
|
-
- Buttons: one primary restart button and quiet secondary actions with full hover, focus,
|
|
39
|
-
active, disabled, and loading states.
|
|
40
|
-
- Loading: skeleton rows preserve layout; errors use an inline retry surface.
|
|
57
|
+
Dashboard icons are official Lucide SVGs, covered by packages/ui/assets/lucide-LICENSE.txt.
|
|
58
|
+
All font and icon assets are served locally and included in the npm package.
|
|
41
59
|
|
|
42
|
-
##
|
|
60
|
+
## ASCII agent world
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
A fixed, pointer-transparent and aria-hidden canvas renders deterministic terraced terrain,
|
|
63
|
+
ASCII water, paths and coding nodes (>_, {}, git, </>, [AI], $). It rotates around its vertical
|
|
64
|
+
axis once every 240 seconds, rendered at at most 20 fps and device pixel ratio capped at 2.
|
|
65
|
+
The world stays behind opaque cards; text and controls never rotate. Reduced motion freezes
|
|
66
|
+
the view, hidden tabs pause the animation. The terrain module has no external dependency
|
|
67
|
+
or network request and does not consume user or session data.
|
package/apps/dashboard/app.js
CHANGED
|
@@ -3,13 +3,11 @@ document.documentElement.lang = locale;
|
|
|
3
3
|
|
|
4
4
|
const copy = {
|
|
5
5
|
en: {
|
|
6
|
-
subtitle: "Personal plugins for Codex",
|
|
7
6
|
connecting: "Connecting…",
|
|
8
7
|
runtimeActive: "Active in Codex",
|
|
9
8
|
runtimeStopped: "Runtime unavailable",
|
|
10
9
|
runtimeNotInjected: "Runtime running — restart Codex to apply plugins",
|
|
11
|
-
plugins: "
|
|
12
|
-
pluginsCopy: "Choose which enhancements appear in Codex.",
|
|
10
|
+
plugins: "Your extensions",
|
|
13
11
|
restartTitle: "Restart Codex to apply changes",
|
|
14
12
|
restartCopy: "Your plugin choices are saved. Restart when you are ready.",
|
|
15
13
|
restart: "Restart Codex",
|
|
@@ -30,10 +28,9 @@ const copy = {
|
|
|
30
28
|
loadErrorCopy: "Check that the background service is running.",
|
|
31
29
|
retry: "Try again",
|
|
32
30
|
version: "Version",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
stopped: "Stopped",
|
|
31
|
+
darkAppearance: "Dark appearance",
|
|
32
|
+
lightAppearance: "Light appearance",
|
|
33
|
+
updateFailed: "Couldn’t check for updates. Please try again.",
|
|
37
34
|
checkUpdates: "Check for updates",
|
|
38
35
|
checkingUpdates: "Checking…",
|
|
39
36
|
current: "You’re up to date.",
|
|
@@ -43,13 +40,11 @@ const copy = {
|
|
|
43
40
|
failureDetail: "Health or renderer status could not be confirmed.",
|
|
44
41
|
},
|
|
45
42
|
"zh-CN": {
|
|
46
|
-
subtitle: "为 Codex 准备的个人插件",
|
|
47
43
|
connecting: "正在连接…",
|
|
48
44
|
runtimeActive: "已在 Codex 中生效",
|
|
49
45
|
runtimeStopped: "运行服务不可用",
|
|
50
46
|
runtimeNotInjected: "运行服务已启动——重启 Codex 以应用插件",
|
|
51
|
-
plugins: "
|
|
52
|
-
pluginsCopy: "选择要在 Codex 中启用的增强功能。",
|
|
47
|
+
plugins: "功能扩展",
|
|
53
48
|
restartTitle: "重启 Codex 以应用更改",
|
|
54
49
|
restartCopy: "插件选择已经保存,可以在准备好后统一重启。",
|
|
55
50
|
restart: "重启 Codex",
|
|
@@ -70,10 +65,9 @@ const copy = {
|
|
|
70
65
|
loadErrorCopy: "请检查后台服务是否正在运行。",
|
|
71
66
|
retry: "重试",
|
|
72
67
|
version: "版本",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
stopped: "已停止",
|
|
68
|
+
darkAppearance: "深色外观",
|
|
69
|
+
lightAppearance: "浅色外观",
|
|
70
|
+
updateFailed: "暂时无法检查更新,请稍后重试。",
|
|
77
71
|
checkUpdates: "检查更新",
|
|
78
72
|
checkingUpdates: "正在检查…",
|
|
79
73
|
current: "当前已是最新版本。",
|
|
@@ -85,7 +79,6 @@ const copy = {
|
|
|
85
79
|
}[locale];
|
|
86
80
|
|
|
87
81
|
const elements = {
|
|
88
|
-
subtitle: document.querySelector("#subtitle"),
|
|
89
82
|
runtimeDot: document.querySelector("#runtime-dot"),
|
|
90
83
|
runtimeLabel: document.querySelector("#runtime-label"),
|
|
91
84
|
restartNotice: document.querySelector("#restart-notice"),
|
|
@@ -93,7 +86,6 @@ const elements = {
|
|
|
93
86
|
restartCopy: document.querySelector("#restart-copy"),
|
|
94
87
|
restartButton: document.querySelector("#restart-button"),
|
|
95
88
|
pluginsHeading: document.querySelector("#plugins-heading"),
|
|
96
|
-
pluginsCopy: document.querySelector("#plugins-copy"),
|
|
97
89
|
saveState: document.querySelector("#save-state"),
|
|
98
90
|
pluginList: document.querySelector("#plugin-list"),
|
|
99
91
|
loadError: document.querySelector("#load-error"),
|
|
@@ -102,9 +94,9 @@ const elements = {
|
|
|
102
94
|
retryButton: document.querySelector("#retry-button"),
|
|
103
95
|
versionLabel: document.querySelector("#version-label"),
|
|
104
96
|
versionValue: document.querySelector("#version-value"),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
themeToggle: document.querySelector(".theme-toggle"),
|
|
98
|
+
themeButtons: [...document.querySelectorAll("[data-theme-choice]")],
|
|
99
|
+
updateResult: document.querySelector("#update-result"),
|
|
108
100
|
updateButton: document.querySelector("#update-button"),
|
|
109
101
|
toast: document.querySelector("#toast"),
|
|
110
102
|
};
|
|
@@ -114,20 +106,16 @@ let toastTimer = null;
|
|
|
114
106
|
const pendingFeatureIds = new Set();
|
|
115
107
|
|
|
116
108
|
function applyStaticCopy() {
|
|
117
|
-
elements.subtitle.textContent = copy.subtitle;
|
|
118
109
|
elements.runtimeLabel.textContent = copy.connecting;
|
|
119
110
|
elements.restartTitle.textContent = copy.restartTitle;
|
|
120
111
|
elements.restartCopy.textContent = copy.restartCopy;
|
|
121
112
|
elements.restartButton.querySelector(".button-label").textContent = copy.restart;
|
|
122
113
|
elements.pluginsHeading.textContent = copy.plugins;
|
|
123
|
-
elements.pluginsCopy.textContent = copy.pluginsCopy;
|
|
124
114
|
elements.errorTitle.textContent = copy.loadError;
|
|
125
115
|
elements.errorCopy.textContent = copy.loadErrorCopy;
|
|
126
116
|
elements.retryButton.textContent = copy.retry;
|
|
127
117
|
elements.versionLabel.textContent = copy.version;
|
|
128
|
-
elements.
|
|
129
|
-
elements.hostLabel.textContent = copy.runtimeHost;
|
|
130
|
-
elements.updateButton.textContent = copy.checkUpdates;
|
|
118
|
+
elements.updateButton.querySelector(".button-label").textContent = copy.checkUpdates;
|
|
131
119
|
}
|
|
132
120
|
|
|
133
121
|
function featureState(feature) {
|
|
@@ -164,23 +152,25 @@ function createPluginCard(feature) {
|
|
|
164
152
|
icon.setAttribute("aria-hidden", "true");
|
|
165
153
|
const image = element("img");
|
|
166
154
|
image.alt = "";
|
|
167
|
-
image.width =
|
|
168
|
-
image.height =
|
|
155
|
+
image.width = 24;
|
|
156
|
+
image.height = 24;
|
|
169
157
|
icon.append(image);
|
|
170
158
|
|
|
171
159
|
const pluginCopy = element("div", "plugin-copy");
|
|
172
160
|
const titleLine = element("div", "plugin-title-line");
|
|
173
161
|
const name = element("h3", "plugin-name");
|
|
174
|
-
|
|
175
|
-
titleLine.append(name, version);
|
|
162
|
+
titleLine.append(name);
|
|
176
163
|
const description = element("p", "plugin-description");
|
|
177
164
|
pluginCopy.append(titleLine, description);
|
|
178
165
|
|
|
179
|
-
const
|
|
166
|
+
const heading = element("div", "plugin-topline");
|
|
180
167
|
const label = element("label", "switch");
|
|
181
168
|
const input = element("input");
|
|
182
169
|
input.type = "checkbox";
|
|
183
170
|
input.name = `feature-${feature.id}`;
|
|
171
|
+
input.setAttribute("role", "switch");
|
|
172
|
+
input.setAttribute("aria-describedby", `description-${feature.id} state-${feature.id}`);
|
|
173
|
+
description.id = `description-${feature.id}`;
|
|
184
174
|
const track = element("span", "switch-track");
|
|
185
175
|
track.setAttribute("aria-hidden", "true");
|
|
186
176
|
label.append(input, track);
|
|
@@ -188,9 +178,10 @@ function createPluginCard(feature) {
|
|
|
188
178
|
const dot = element("span", "status-dot");
|
|
189
179
|
dot.setAttribute("aria-hidden", "true");
|
|
190
180
|
const stateLabel = element("span", "plugin-state-label");
|
|
181
|
+
stateLabel.id = `state-${feature.id}`;
|
|
191
182
|
pluginState.append(dot, stateLabel);
|
|
192
|
-
|
|
193
|
-
card.append(
|
|
183
|
+
heading.append(icon, label);
|
|
184
|
+
card.append(heading, pluginCopy, pluginState);
|
|
194
185
|
return card;
|
|
195
186
|
}
|
|
196
187
|
|
|
@@ -199,14 +190,12 @@ function updatePluginCard(card, feature) {
|
|
|
199
190
|
const input = card.querySelector('input[type="checkbox"]');
|
|
200
191
|
const image = card.querySelector(".plugin-icon img");
|
|
201
192
|
const name = card.querySelector(".plugin-name");
|
|
202
|
-
const version = card.querySelector(".plugin-version");
|
|
203
193
|
const description = card.querySelector(".plugin-description");
|
|
204
194
|
const dot = card.querySelector(".plugin-state .status-dot");
|
|
205
195
|
const stateLabel = card.querySelector(".plugin-state-label");
|
|
206
196
|
card.dataset.featureId = feature.id;
|
|
207
|
-
image.src = feature.iconUrl
|
|
197
|
+
image.src = `${feature.iconUrl}?v=stroke-2.1`;
|
|
208
198
|
name.textContent = feature.label;
|
|
209
|
-
version.textContent = feature.version;
|
|
210
199
|
description.textContent = feature.description;
|
|
211
200
|
input.checked = Boolean(feature.enabled);
|
|
212
201
|
input.disabled = pendingFeatureIds.has(feature.id);
|
|
@@ -249,13 +238,13 @@ function renderRuntime() {
|
|
|
249
238
|
&& Number(renderer.injectedTargets) > 0,
|
|
250
239
|
);
|
|
251
240
|
const active = runtimeRunning && rendererCurrent;
|
|
241
|
+
elements.runtimeLabel.closest(".overview").hidden = active;
|
|
252
242
|
elements.runtimeDot.className = `status-dot ${active ? "is-active" : "is-warning"}`;
|
|
253
243
|
elements.runtimeLabel.textContent = active
|
|
254
244
|
? copy.runtimeActive
|
|
255
245
|
: runtimeRunning
|
|
256
246
|
? copy.runtimeNotInjected
|
|
257
247
|
: copy.runtimeStopped;
|
|
258
|
-
elements.hostValue.textContent = runtimeRunning ? copy.running : copy.stopped;
|
|
259
248
|
elements.versionValue.textContent = state?.version ?? "—";
|
|
260
249
|
elements.restartNotice.hidden = !state?.pendingRestart;
|
|
261
250
|
}
|
|
@@ -302,6 +291,7 @@ async function load() {
|
|
|
302
291
|
elements.loadError.hidden = false;
|
|
303
292
|
elements.runtimeDot.className = "status-dot is-warning";
|
|
304
293
|
elements.runtimeLabel.textContent = copy.runtimeStopped;
|
|
294
|
+
elements.runtimeLabel.closest(".overview").hidden = true;
|
|
305
295
|
}
|
|
306
296
|
}
|
|
307
297
|
|
|
@@ -369,19 +359,45 @@ elements.retryButton.addEventListener("click", () => {
|
|
|
369
359
|
|
|
370
360
|
elements.updateButton.addEventListener("click", async () => {
|
|
371
361
|
elements.updateButton.disabled = true;
|
|
372
|
-
elements.updateButton.textContent = copy.checkingUpdates;
|
|
362
|
+
elements.updateButton.querySelector(".button-label").textContent = copy.checkingUpdates;
|
|
363
|
+
elements.updateResult.hidden = true;
|
|
373
364
|
try {
|
|
374
365
|
const response = await fetch("/api/update");
|
|
375
366
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
376
367
|
const update = await response.json();
|
|
377
|
-
|
|
368
|
+
elements.updateResult.textContent = update.available ? copy.updateAvailable(update.latest) : copy.current;
|
|
369
|
+
elements.updateResult.hidden = false;
|
|
378
370
|
} catch {
|
|
379
|
-
|
|
371
|
+
elements.updateResult.textContent = copy.updateFailed;
|
|
372
|
+
elements.updateResult.hidden = false;
|
|
380
373
|
} finally {
|
|
381
374
|
elements.updateButton.disabled = false;
|
|
382
|
-
elements.updateButton.textContent = copy.checkUpdates;
|
|
375
|
+
elements.updateButton.querySelector(".button-label").textContent = copy.checkUpdates;
|
|
383
376
|
}
|
|
384
377
|
});
|
|
385
378
|
|
|
379
|
+
const systemTheme = matchMedia("(prefers-color-scheme: dark)");
|
|
380
|
+
let selectedTheme = null;
|
|
381
|
+
try { selectedTheme = localStorage.getItem("minecodex-appearance"); } catch { /* 存储不可用时仍允许本次切换。 */ }
|
|
382
|
+
function applyTheme() {
|
|
383
|
+
const dark = selectedTheme === "dark" || (selectedTheme !== "light" && systemTheme.matches);
|
|
384
|
+
document.documentElement.dataset.theme = dark ? "dark" : "light";
|
|
385
|
+
elements.themeToggle.setAttribute("aria-label", locale === "zh-CN" ? "外观" : "Appearance");
|
|
386
|
+
for (const button of elements.themeButtons) {
|
|
387
|
+
button.setAttribute("aria-label", button.dataset.themeChoice === "light" ? copy.lightAppearance : copy.darkAppearance);
|
|
388
|
+
button.setAttribute("aria-pressed", String(button.dataset.themeChoice === (dark ? "dark" : "light")));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
elements.themeToggle.addEventListener("click", (event) => {
|
|
392
|
+
const button = event.target.closest("[data-theme-choice]");
|
|
393
|
+
if (!button) return;
|
|
394
|
+
selectedTheme = button.dataset.themeChoice;
|
|
395
|
+
try { localStorage.setItem("minecodex-appearance", selectedTheme); } catch { /* 保留内存中的选择。 */ }
|
|
396
|
+
applyTheme();
|
|
397
|
+
});
|
|
398
|
+
systemTheme.addEventListener("change", applyTheme);
|
|
399
|
+
document.addEventListener("keydown", () => { document.documentElement.dataset.input = "keyboard"; });
|
|
400
|
+
document.addEventListener("pointerdown", () => { document.documentElement.dataset.input = "pointer"; });
|
|
386
401
|
applyStaticCopy();
|
|
402
|
+
applyTheme();
|
|
387
403
|
void load();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -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
|
+
<circle cx="12" cy="12" r="10" />
|
|
13
|
+
<path d="M12 16v-4" />
|
|
14
|
+
<path d="M12 8h.01" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
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.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
|
|
13
|
+
</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="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
|
13
|
+
<path d="M21 3v5h-5" />
|
|
14
|
+
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
|
15
|
+
<path d="M8 16H3v5" />
|
|
16
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
<circle cx="12" cy="12" r="4" />
|
|
13
|
+
<path d="M12 2v2" />
|
|
14
|
+
<path d="M12 20v2" />
|
|
15
|
+
<path d="m4.93 4.93 1.41 1.41" />
|
|
16
|
+
<path d="m17.66 17.66 1.41 1.41" />
|
|
17
|
+
<path d="M2 12h2" />
|
|
18
|
+
<path d="M20 12h2" />
|
|
19
|
+
<path d="m6.34 17.66-1.41 1.41" />
|
|
20
|
+
<path d="m19.07 4.93-1.41 1.41" />
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 纯装饰字符地形;不读取会话数据,也不参与界面交互。
|
|
2
|
+
const canvas = document.querySelector('#agent-world');
|
|
3
|
+
const context = canvas.getContext('2d');
|
|
4
|
+
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
|
5
|
+
const points = [];
|
|
6
|
+
const agents = [];
|
|
7
|
+
const heightAt = (x, z) => {
|
|
8
|
+
const ridge = Math.sin(x * .16 + Math.cos(z * .13)) * 2.4 + Math.cos(z * .21) * 1.8;
|
|
9
|
+
const island = Math.max(0, 1 - Math.hypot(x / 31, z / 26));
|
|
10
|
+
return Math.max(-1, Math.floor(island * 9 + ridge - 2));
|
|
11
|
+
};
|
|
12
|
+
for (let z = -28; z <= 28; z++) {
|
|
13
|
+
for (let x = -32; x <= 32; x++) {
|
|
14
|
+
const y = heightAt(x, z);
|
|
15
|
+
if (x * x / 1100 + z * z / 850 > 1.15) continue;
|
|
16
|
+
const path = Math.abs(z - Math.round(Math.sin(x * .12) * 9)) < 1;
|
|
17
|
+
points.push({ x, y, z, glyph: y < 0 ? '~' : path ? ':' : y > 5 ? '#' : '+', kind: path ? 'path' : 'land' });
|
|
18
|
+
// 用垂直字符层显示台阶断面,保留体素地形的厚度。
|
|
19
|
+
const bottom = Math.min(heightAt(x + 1, z), heightAt(x, z + 1));
|
|
20
|
+
for (let h = bottom + 1; h < y; h++) points.push({ x, y: h, z, glyph: '|', kind: 'side' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const [x, z, label] of [[-20, -9, '>_'], [20, 9, '{ }'], [-8, 20, 'git'], [10, -20, '</>'], [-23, 15, '[AI]'], [24, -12, '$']]) {
|
|
24
|
+
const base = heightAt(x, z);
|
|
25
|
+
for (let y = base + 1; y <= base + 5; y++) points.push({ x, y, z, glyph: y === base + 5 ? '[]' : '|', kind: 'node' });
|
|
26
|
+
agents.push({ x, y: base + 7, z, glyph: label, kind: 'node' });
|
|
27
|
+
}
|
|
28
|
+
let width = 0, height = 0, frame = 0, angle = .65, last = 0;
|
|
29
|
+
let dark = false;
|
|
30
|
+
function resize() {
|
|
31
|
+
width = innerWidth;
|
|
32
|
+
height = innerHeight;
|
|
33
|
+
const ratio = Math.min(devicePixelRatio || 1, 2);
|
|
34
|
+
canvas.width = Math.round(width * ratio);
|
|
35
|
+
canvas.height = Math.round(height * ratio);
|
|
36
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
37
|
+
draw();
|
|
38
|
+
}
|
|
39
|
+
function draw() {
|
|
40
|
+
if (!context || !width) return;
|
|
41
|
+
context.clearRect(0, 0, width, height);
|
|
42
|
+
const scale = Math.max(width / 62, height / 42);
|
|
43
|
+
const sin = Math.sin(angle), cos = Math.cos(angle);
|
|
44
|
+
const project = (point) => {
|
|
45
|
+
const x = point.x * cos - point.z * sin;
|
|
46
|
+
const depth = point.x * sin + point.z * cos;
|
|
47
|
+
return { ...point, sx: width / 2 + x * scale, sy: height * .58 + (depth * .48 - point.y * .95) * scale, depth };
|
|
48
|
+
};
|
|
49
|
+
const visible = [...points, ...agents].map(project).sort((a, b) => a.depth - b.depth);
|
|
50
|
+
const fontSize = Math.max(10, Math.min(14, scale * .52));
|
|
51
|
+
context.font = `${fontSize}px ui-monospace, SFMono-Regular, Consolas, monospace`;
|
|
52
|
+
context.textAlign = 'center';
|
|
53
|
+
// 同一字符格只保留前景点,避免山体投影堆叠成杂乱笔画。
|
|
54
|
+
const cells = new Map();
|
|
55
|
+
for (const point of visible) cells.set(`${Math.round(point.sx / (fontSize * .7))}:${Math.round(point.sy / fontSize)}`, point);
|
|
56
|
+
for (const p of cells.values()) {
|
|
57
|
+
if (p.sx < -30 || p.sx > width + 30 || p.sy < -20 || p.sy > height + 20) continue;
|
|
58
|
+
const alpha = p.kind === 'node' ? .48 : p.kind === 'path' ? .34 : p.kind === 'side' ? .14 : .22;
|
|
59
|
+
context.fillStyle = dark ? `rgba(112,166,245,${alpha})` : `rgba(52,101,174,${alpha})`;
|
|
60
|
+
context.fillText(p.glyph, p.sx, p.sy);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function tick(time) {
|
|
64
|
+
if (time - last >= 50) {
|
|
65
|
+
if (last) angle = (angle + Math.min(time - last, 100) * Math.PI * 2 / 240000) % (Math.PI * 2);
|
|
66
|
+
last = time;
|
|
67
|
+
draw();
|
|
68
|
+
}
|
|
69
|
+
frame = requestAnimationFrame(tick);
|
|
70
|
+
}
|
|
71
|
+
function syncMotion() {
|
|
72
|
+
cancelAnimationFrame(frame);
|
|
73
|
+
last = 0;
|
|
74
|
+
if (!document.hidden && !reduced.matches) frame = requestAnimationFrame(tick);
|
|
75
|
+
else draw();
|
|
76
|
+
}
|
|
77
|
+
function syncTheme() {
|
|
78
|
+
dark = document.documentElement.dataset.theme === 'dark';
|
|
79
|
+
draw();
|
|
80
|
+
}
|
|
81
|
+
new MutationObserver(syncTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
82
|
+
addEventListener('resize', resize);
|
|
83
|
+
document.addEventListener('visibilitychange', syncMotion);
|
|
84
|
+
reduced.addEventListener('change', syncMotion);
|
|
85
|
+
addEventListener('pagehide', () => cancelAnimationFrame(frame));
|
|
86
|
+
addEventListener('pageshow', syncMotion);
|
|
87
|
+
syncTheme();
|
|
88
|
+
resize();
|
|
89
|
+
syncMotion();
|