feat: expand graph canvas and restore original Endoleap branding
@@ -52,7 +52,7 @@ Google / Slack 的提供方回调仍是 Foundry 后端地址;后端的项目
|
||||
- 底部按钮、账户菜单和设置面板使用 Foundry `profile.avatar_url` 提供的 Google 头像;无头像或图片加载失败时显示姓名首字。
|
||||
- 新建页面使用 Foundry Core 的 `POST /workspaces/{workspace}/materials`,提交 `kind: board`、默认名称“未命名图谱”和内容版本;由后端生成 ID。没有空间时先创建“个人空间”,成功后跳转到图谱的独立地址。
|
||||
- 本地演示提供示例图谱、搜索、新建和浏览器保存,支持图谱独立地址与刷新。
|
||||
- 图形编辑、连接线、分享、Agent、收藏及完整布局持久化尚未接入。图谱页面目前只显示已保存的名称和空白图谱区域;演示数据不写入线上。
|
||||
- 图形编辑、连接线、分享、Agent、收藏及完整布局持久化尚未接入。图谱页面移除顶部标题栏,画布覆盖整个视口,侧栏和快捷浮框覆盖其上;演示数据不写入线上。
|
||||
- 本地演示仅用于查看界面,不提供后端认证与权限。
|
||||
|
||||
Figma 原始 Logo、人物插画与图标已下载到 public/assets,不依赖临时 Figma URL。品牌保持啡跃 Endoleap;shadcn Button / Dialog 使用官方 registry 源码。依赖版本固定,package-lock.json 可用于 npm ci。
|
||||
@@ -73,3 +73,11 @@ API 测试使用本机临时后端和独立前端进程,检查会话 Cookie、
|
||||
|
||||
性能分析、实测耗时和修复说明见 [PERFORMANCE.md](PERFORMANCE.md)。
|
||||
流程回归可在支持 TypeScript 类型擦除的 Node 版本执行:`node --experimental-strip-types --test tests/workspace.test.mjs`。
|
||||
|
||||
## 品牌资源
|
||||
|
||||
`public/brand/` 原样复用旧 PROMPT 仓库 `Zayden-Jung/probably-rendering-one-more-pixel-tonight` 的 `packages/brand/assets/`,来源提交 `47467874841ea7f7b4b362d4ec0e66f236c6a7d6`。
|
||||
|
||||
共 88 个原始资源:6 套 PNG(logo/icon/mono 的 light 与 dark 版本,每套 16、24、32、48、64、128、180、192、256、512、1024、2048px),8 个 SVG,以及 8 个应用图标文件(含 3 种 ICO、Apple Touch 和 PWA PNG)。所有文件与来源 Git blob 哈希一致。
|
||||
|
||||
收起侧栏后的浮框使用 Logo 返回 `/`;展开侧栏后仍保留新建页面按钮。首页与账户页品牌组件使用原始 SVG,根布局声明 ICO、深浅色 PNG favicon 和 Apple Touch 图标。
|
||||
|
||||
@@ -48,3 +48,12 @@
|
||||
- Foundry 主线已保存 OAuth picture 并通过 profile.avatar_url 返回,本次补齐前端类型和头像渲染,无需修改后端。图片失败或缺失时回退首字,图片使用 no-referrer。
|
||||
- 类型检查与 6 项 workspace 测试通过;隔离临时目录的 Webpack 生产构建及 API 冒烟检查通过,新增头像 URL 透传、资料修改后保留头像的检查。
|
||||
- 沿用用户“不访问浏览器,由用户试用反馈”的要求,未运行本次浏览器视觉、动画及真实 Google 头像加载验收;未修改真实账户资料。
|
||||
|
||||
## 全屏画布与原始品牌资源(2026-11-06)
|
||||
|
||||
- 删除图谱顶部标题栏及相关 CSS;画布 `position: fixed; inset: 0; height: 100dvh`,侧栏和浮框保留在更高层级,加载与错误消息以浮层显示。
|
||||
- 收起侧栏后,将原新建按钮位置替换为 Logo 链接,目标为主页 `/`;Logo 提供 1x/2x/3x PNG,展开侧栏的新建按钮不变。
|
||||
- 从旧 PROMPT 提交 `47467874841ea7f7b4b362d4ec0e66f236c6a7d6` 复制 88 个原始 PNG/ICO/SVG,逐一核对 Git blob 哈希完全一致;PNG 尺寸为 16px 至 2048px。
|
||||
- 全局 Brand 组件与根布局 favicon/Apple Touch 元信息使用迁入资源。
|
||||
- 按用户约定不访问浏览器,视觉与点击交互由用户试用反馈;自动检查覆盖资源 HTTP 响应和字节一致性、图标元信息及既有接口流程。
|
||||
- 本次类型检查、隔离 Webpack 生产构建、6 项 workspace 测试及扩展 API 冒烟检查均通过;88 个品牌资源的 HTTP 状态和响应字节与本地原文件一致。
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 527 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 981 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 943 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 613 B |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 780 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 665 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 849 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 416 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 646 B |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 808 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 681 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 82 KiB |
@@ -148,7 +148,11 @@ export default function DashboardSidebar() {
|
||||
exit={{ opacity: 0, y: reduceMotion ? 0 : -6 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.14 }}>
|
||||
<IconButton ref={expandButton} icon={PanelLeftOpen} label="展开侧边栏" aria-expanded={false} aria-controls="workspace-sidebar" onClick={() => toggleSidebar(false)} />
|
||||
{createButton}
|
||||
<Link href="/" className={styles.logoLink} aria-label="啡跃 Endoleap 主页" title="返回主页">
|
||||
<img src="/brand/png/logo-light/endoleap-16.png"
|
||||
srcSet="/brand/png/logo-light/endoleap-16.png 1x, /brand/png/logo-light/endoleap-32.png 2x, /brand/png/logo-light/endoleap-48.png 3x"
|
||||
width={16} height={16} alt="" />
|
||||
</Link>
|
||||
{error && <p className={styles.floatingError} role="alert">{error}</p>}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,14 @@ import "@/styles/globals.css";
|
||||
export const metadata: Metadata = {
|
||||
title: "啡跃 Endoleap · 让灵感有形",
|
||||
description: "收藏灵感、连接知识,在自己的创作空间让想法慢慢成形。",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/brand/icons/favicon.ico", type: "image/x-icon", sizes: "any" },
|
||||
{ url: "/brand/png/icon-light/endoleap-32.png", type: "image/png", sizes: "32x32", media: "(prefers-color-scheme: light)" },
|
||||
{ url: "/brand/png/icon-dark/endoleap-32.png", type: "image/png", sizes: "32x32", media: "(prefers-color-scheme: dark)" },
|
||||
],
|
||||
apple: [{ url: "/brand/icons/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export function Brand({ light = false }: { light?: boolean }) {
|
||||
aria-label="啡跃 Endoleap 首页"
|
||||
>
|
||||
<img
|
||||
src="/assets/landingcontext-imgFrame.svg"
|
||||
src={light ? "/brand/svg/logo-dark.svg" : "/brand/svg/logo-light.svg"}
|
||||
alt=""
|
||||
width="36"
|
||||
height="36"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Network } from "lucide-react";
|
||||
import { api, errorText } from "@/lib/api";
|
||||
import { readDemoBoards } from "@/lib/demo-workspace";
|
||||
import type { Board } from "@/lib/workspace";
|
||||
@@ -46,13 +45,10 @@ export default function GraphPage({ workspaceId, boardId }: { workspaceId: strin
|
||||
}, [workspaceId, boardId, demo, router]);
|
||||
|
||||
return (
|
||||
<main className={styles.graph} aria-label="无限图谱">
|
||||
{error ? <p className={styles.graphStatus} role="alert">{error}</p> : board ? (
|
||||
<>
|
||||
<header className={styles.graphHeader}><Network size={16} /><h1>{board.title}</h1>{demo && <span>演示</span>}</header>
|
||||
<div className={styles.graphSurface} aria-label={board.title + "的空白图谱"} />
|
||||
</>
|
||||
) : <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
|
||||
<main className={styles.graph} aria-label={board ? board.title + " · 无限图谱" : "无限图谱"} aria-busy={!board && !error}>
|
||||
<div className={styles.graphSurface} aria-label={board ? board.title + "的画布" : "画布"} />
|
||||
{error ? <p className={styles.graphStatus} role="alert">{error}</p>
|
||||
: !board && <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,12 +105,14 @@
|
||||
.settingsActions button { padding: 6px 12px; border: 1px solid #e2ddd6; border-radius: 6px; font-size: 13px; }
|
||||
.settingsActions button[type="submit"] { background: #37352f; color: white; border-color: #37352f; }
|
||||
.settingsActions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.graph { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 100dvh; }
|
||||
.graphHeader { display: flex; align-items: center; gap: 10px; padding: 20px 24px 20px 110px; min-height: 64px; border-bottom: 1px solid #efede8; font-size: 13px; }
|
||||
.graphHeader h1 { margin: 0; font-size: 14px; font-weight: 500; }
|
||||
.graphHeader span { color: #aaa299; }
|
||||
.graphSurface { flex: 1; background-image: radial-gradient(#b8aea245 1px, transparent 1px); background-size: 22px 22px; }
|
||||
.graphStatus { padding: 80px 24px; color: #9a948c; text-align: center; font-size: 13px; }
|
||||
.graph { position: fixed; inset: 0; z-index: 0; display: flex; width: 100%; height: 100dvh; background: #fffefb; }
|
||||
.graphSurface { width: 100%; height: 100%; background-image: radial-gradient(#b8aea245 1px, transparent 1px); background-size: 22px 22px; }
|
||||
.graphStatus { position: absolute; left: 50%; top: 72px; transform: translateX(-50%); max-width: calc(100vw - 48px); padding: 8px 12px; border-radius: 6px; background: #fffefbe6; color: #9a948c; text-align: center; font-size: 13px; }
|
||||
@keyframes account-enter { from { opacity: 0; transform: translateY(4px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
||||
@media (max-width: 600px) { .shell { --sidebar-width: 240px; } .graphHeader { padding: 20px 14px; } }
|
||||
@media (max-width: 600px) { .shell { --sidebar-width: 240px; } }
|
||||
@media (prefers-reduced-motion: reduce) { .accountPopup:popover-open { animation: none; } }
|
||||
|
||||
.logoLink { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 28px; height: 24px; padding: 4px 6px; border-radius: 5px; }
|
||||
.logoLink img { display: block; width: 16px; height: 16px; }
|
||||
.logoLink:hover { background: #e9e7e3; }
|
||||
.logoLink:focus-visible { outline: 2px solid #9d7d66; outline-offset: 2px; }
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createServer } from "node:http";
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
const host = "http://127.0.0.1:3005";
|
||||
const graphs = new Map();
|
||||
let mockProfile = {
|
||||
@@ -95,6 +96,16 @@ try {
|
||||
assert.match(html, /啡跃 Endoleap · 让灵感有形/);
|
||||
assert.match(html, /name="description"/);
|
||||
assert.match(html, /name="viewport"/);
|
||||
assert(html.includes('/brand/icons/favicon.ico'));
|
||||
assert(html.includes('/brand/icons/apple-touch-icon.png'));
|
||||
}
|
||||
const brandRoot = new URL("../public/brand/", import.meta.url);
|
||||
const assets = (await readdir(brandRoot, { recursive: true })).filter((file) => /\.(png|ico|svg)$/.test(file));
|
||||
assert.equal(assets.length, 88);
|
||||
for (const asset of assets) {
|
||||
const response = await fetch(host + "/brand/" + asset);
|
||||
assert.equal(response.status, 200, asset);
|
||||
assert.deepEqual(Buffer.from(await response.arrayBuffer()), await readFile(new URL(asset, brandRoot)), asset);
|
||||
}
|
||||
assert.equal((await fetch(host + "/missing-page")).status, 404);
|
||||
const res = await fetch(host + "/api/foundry/auth/login", {
|
||||
@@ -195,7 +206,7 @@ try {
|
||||
assert.equal(await logout.text(), "");
|
||||
assert.match(logout.headers.get("set-cookie"), /Max-Age=0/);
|
||||
console.log(
|
||||
"PASS: avatar and profile updates, App Router pages and metadata, body validation, method and query filtering, OAuth redirects, logout, session cookie, token removal, authenticated proxy, invalid session, cross-origin rejection, path allowlist, OAuth validation",
|
||||
"PASS: 88 original brand assets, favicon metadata, avatar and profile updates, App Router pages and metadata, body validation, method and query filtering, OAuth redirects, logout, session cookie, token removal, authenticated proxy, invalid session, cross-origin rejection, path allowlist, OAuth validation",
|
||||
);
|
||||
} finally {
|
||||
const exited = once(app, "exit");
|
||||
|
||||