diff --git a/apps/waterfall-wiki/src/pages/workspace.tsx b/apps/waterfall-wiki/src/pages/workspace.tsx index 451e7e6..4b0494a 100644 --- a/apps/waterfall-wiki/src/pages/workspace.tsx +++ b/apps/waterfall-wiki/src/pages/workspace.tsx @@ -1,7 +1,169 @@ -import { ThemeToggle } from "../components/ThemeToggle"; -import { Brand } from "../components/Brand"; import Head from 'next/head'; -import {useEffect,useState} from 'react'; -import {ArrowUpRight, Layers, Workflow, BookOpen, Link2} from 'lucide-react'; +import {useEffect, useRef, useState} from 'react'; +import {ArrowLeft, ArrowUpRight, BookOpen, Bot, ChevronDown, House, LayoutGrid, Palette, PanelLeft, Plus, Search, Settings, Sparkles, Workflow, X} from 'lucide-react'; +import {Brand} from '../components/Brand'; +import {ThemeToggle} from '../components/ThemeToggle'; import s from '@/styles/workspace.module.css'; -export default function Workspace(){const [email,setEmail]=useState(''),[state,setState]=useState('loading');useEffect(()=>{fetch('/api/account/session').then(async r=>{if(!r.ok)throw Error();const d=await r.json();if(!d.user){window.location.replace('/account?next=/workspace');return}setEmail(d.user.email);setState('ready')}).catch(()=>setState('error'))},[]);return
工作台 · 啡跃 Studio
{state==='loading'?

正在打开你的工作台…

:state==='error'?

暂时无法读取账户。重新登录

:<>
WORKSPACE / OVERVIEW账户设置 ↗

MAKE SOMETHING THAT MATTERS.

今天,让哪个想法成形?

选一个空间,开始你的下一次创作。

GRAPHIC

图形设计

在文字、图形与图像之间,找到你的表达。

Ideas,
in shape.✳
打开编辑器
INFINITE CANVAS

无限画布

收集灵感、连接参考,让思路自由生长。

灵感⟶参考⟶作品
打开画布

带上你的 coding agent

从接入文档开始,使用已有的创作 API。连接由你的 Agent 发起。

查看文档 ↗

编辑器草稿保存在当前浏览器。更换设备前,请先导出项目备份。

}
} + +const tools = [ + {label: '无限画布', detail: '收集灵感,连接文字与参考素材', href: '/infinite_canvas', icon: Workflow}, + {label: '图形设计', detail: '编辑文字、图像与版式', href: '/graphic', icon: Palette}, + {label: '知识与教程', detail: '查找指南和创作方法', href: '/documents', icon: BookOpen}, + {label: 'Agent 接入', detail: '了解如何连接创作工具与 API', href: '/studio/api', icon: Bot}, +]; +const purposes = [ + {label: '工作与团队', detail: '整理项目和工作资料。', icon: LayoutGrid}, + {label: '个人创作', detail: '收集灵感,让想法成形。', icon: House}, + {label: '学习与研究', detail: '梳理资料,记录研究过程。', icon: BookOpen}, +]; +type Space = {name: string; purpose: string}; +const personal = {name: '个人空间', purpose: ''}; + +export default function Workspace() { + const [email, setEmail] = useState(''); + const [state, setState] = useState('loading'); + const [sidebar, setSidebar] = useState(true); + const [spaces, setSpaces] = useState([]); + const [active, setActive] = useState(personal); + const [query, setQuery] = useState(''); + const [step, setStep] = useState<'purpose' | 'name'>('purpose'); + const [purpose, setPurpose] = useState(''); + const [name, setName] = useState(''); + const [error, setError] = useState(''); + const [notice, setNotice] = useState(''); + const dialog = useRef(null); + const switcher = useRef(null); + const search = useRef(null); + + useEffect(() => { + setSidebar(window.matchMedia('(min-width: 761px)').matches); + const request = new AbortController(); + fetch('/api/account/session', {signal: request.signal}).then(async response => { + if (!response.ok) throw Error(); + const data = await response.json(); + if (!data.user) { + window.location.replace('/account?next=/workspace'); + return; + } + setEmail(data.user.email); + try { + const saved = JSON.parse(localStorage.getItem('studio.workspace:' + data.user.email) || 'null'); + if (Array.isArray(saved?.spaces)) { + setSpaces(saved.spaces); + setActive(saved.spaces.find((space: Space) => space.name === saved.active) || personal); + } + } catch {} + setState('ready'); + }).catch(() => { + if (!request.signal.aborted) setState('error'); + }); + return () => request.abort(); + }, []); + + useEffect(() => { + if (!notice) return; + const timeout = window.setTimeout(() => setNotice(''), 4500); + return () => window.clearTimeout(timeout); + }, [notice]); + + function selectSpace(space: Space, next = spaces) { + localStorage.setItem('studio.workspace:' + email, JSON.stringify({spaces: next, active: space.name})); + setSpaces(next); + setActive(space); + switcher.current?.removeAttribute('open'); + } + + function openDialog() { + setStep('purpose'); + setPurpose(''); + setName(''); + setError(''); + dialog.current?.showModal(); + } + + function createSpace(event: React.FormEvent) { + event.preventDefault(); + const clean = name.trim(); + if (!clean) return setError('请填写 Workspace 名称。'); + if ([personal, ...spaces].some(space => space.name === clean)) return setError('已有同名空间。'); + const space = {name: clean, purpose}; + try { + selectSpace(space, [...spaces, space]); + dialog.current?.close(); + setNotice('已创建并切换到「' + clean + '」'); + } catch { + setError('浏览器无法保存此空间,请检查存储权限后重试。'); + } + } + + const visible = tools.filter(tool => (tool.label + tool.detail).includes(query.trim())); + return
+ 工作台 · 啡跃 + + {sidebar && } +
+
+
{active.name} / 创作概览
+ +
+ {state === 'loading' ?

正在打开工作台…

: state === 'error' ?

暂时无法读取账户。 重新登录

: +
+

{active.purpose || '个人空间'}

{active.name}

从画布开始整理想法,或打开图形编辑器继续创作。

打开画布
+
+

创作工具

+
{visible.map(({label, detail, href, icon: Icon}) => {label}{detail})}
+ {!visible.length &&

没有找到匹配的工具。

} +
+
} +
+ +
+ {step === 'name' && } +

{step === 'purpose' ? '这个空间用来做什么?' : '给你的 Workspace 命名'}

+ +
+ {step === 'purpose' ? <> +

选择一个起点。空间设置仅保存在此浏览器。

+
{purposes.map(({label, detail, icon: Icon}) => )}
+ + :
+

{purpose} · 一个空间,装下你的想法与作品。

+
{Array.from(name.trim())[0] || '空'}空间图标
+ + {setName(e.target.value); setError('');}} aria-invalid={!!error} aria-describedby={error ? 'workspace-error' : undefined} placeholder="输入空间名称"/> + {error && } + +
} +
+ {notice &&
{notice}
} +
; +} diff --git a/apps/waterfall-wiki/src/styles/workspace.module.css b/apps/waterfall-wiki/src/styles/workspace.module.css index f3aa71e..831ba3c 100644 --- a/apps/waterfall-wiki/src/styles/workspace.module.css +++ b/apps/waterfall-wiki/src/styles/workspace.module.css @@ -1 +1,63 @@ -.app{display:flex;min-height:100dvh;background:var(--background);color:var(--foreground-strong);font-family:Arial,'PingFang SC',sans-serif}.app *{box-sizing:border-box}.app a{color:inherit;text-decoration:none}.app>aside{width:230px;flex-shrink:0;border-right:1px solid var(--border);background:var(--selection);padding:30px 20px;display:flex;flex-direction:column;gap:9px}.brand{font-size:22px;font-weight:600;margin-bottom:45px}.brand small{font-size:9px;letter-spacing:1px;color:var(--muted-foreground)}.app aside>span{font-size:10px;color:var(--muted-foreground);margin-bottom:10px;padding-left:10px}.app aside>a:not(.brand){display:flex;align-items:center;gap:12px;font-size:12px;padding:12px;border-radius:7px}.app aside>a[aria-current]{background:var(--selection)}.app aside>.account{margin-top:auto;overflow-wrap:anywhere}.app>section{padding:35px 55px;max-width:1250px;width:100%;margin:auto;min-height:100dvh}.app header{display:flex;justify-content:space-between;color:var(--muted-foreground);font-size:10px;letter-spacing:1px}.kicker{font-size:10px;letter-spacing:2px;color:var(--muted-foreground);margin-top:65px}.app h1{font-size:34px;font-weight:500;letter-spacing:-1px;margin:18px 0}.sub{font-size:13px;color:var(--muted-foreground)}.grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:35px}.grid>a{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:26px;transition:transform .2s}.grid>a:hover{transform:translateY(-3px)}.grid small{font-size:9px;letter-spacing:1.5px;margin-left:12px;color:var(--muted-foreground)}.grid>a>svg{vertical-align:middle;width:20px;color:var(--muted-foreground)}.grid h2{font-size:23px;font-weight:500;margin:24px 0 12px}.grid p{font-size:12px;color:var(--muted-foreground);line-height:1.8}.poster,.canvas{height:190px;margin:25px 0;background:#ecf0e3;position:relative;overflow:hidden;border-radius:5px}.poster{padding:25px;font-size:40px;line-height:1;font-weight:600;letter-spacing:-2px}.poster i{font-family:Georgia,serif;font-weight:400}.poster b{position:absolute;right:0;top:12px;font-size:155px;color:#a7b88b}.canvas{display:flex;align-items:center;justify-content:center;gap:12px;background:radial-gradient(var(--selection) 1px,transparent 1px) 0 0/18px 18px,var(--selection);color:var(--muted-foreground)}.canvas span{background:var(--surface);border:1px solid var(--selection-border);padding:20px 13px;font-size:12px;border-radius:6px;color:var(--foreground)}.grid strong{display:flex;justify-content:space-between;font-size:12px;font-weight:400}.agent{display:flex;align-items:center;gap:18px;border:1px solid var(--selection-border);border-radius:10px;padding:23px;margin-top:28px}.agent>svg{color:var(--muted-foreground)}.agent h3{font-size:14px;margin:0 0 8px}.agent p{font-size:11px;color:var(--muted-foreground);margin:0;line-height:1.8}.agent a{margin-left:auto;font-size:12px;white-space:nowrap}.hint{font-size:10px;color:var(--muted-foreground);margin-top:25px}.app a:focus-visible{outline:2px solid var(--ring);outline-offset:4px}@media(max-width:850px){.app>aside{width:180px}.app>section{padding:30px 25px}.grid{gap:15px}.grid>a{padding:20px}.app h1{font-size:28px}.canvas{gap:5px}.canvas span{padding:15px 8px}}@media(max-width:600px){.app{display:block}.app>aside{width:100%;flex-direction:row;flex-wrap:wrap;padding:18px;gap:5px;border-right:0;border-bottom:1px solid var(--border)}.brand{margin:0;width:100%}.app aside>span,.app aside>.account{display:none}.app aside>a:not(.brand){padding:8px;font-size:10px}.grid{grid-template-columns:1fr}.kicker{margin-top:35px}.agent{flex-wrap:wrap}.agent a{margin-left:0}} +.app { display: flex; min-height: 100dvh; background: var(--background); color: var(--foreground-strong); font-size: 14px; } +.rail, .panel { position: sticky; top: 0; height: 100dvh; border-right: 1px solid var(--border); flex-shrink: 0; } +.rail { width: 50px; display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 10px 5px; background: var(--surface-muted); } +.rail > a, .rail > button, .panelTitle button, .top button, .dialogTop button { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border-radius: 7px; color: var(--muted-foreground); } +.rail > :hover, .links a:hover, .top button:hover, .panelTitle button:hover, .dialogTop button:hover, .menu button:hover { background: var(--row-hover); } +.rail [aria-current], .links a[aria-current], .menu button[aria-pressed=true] { background: var(--selection); color: var(--foreground-strong); } +.logo { margin-bottom: 14px; } +.logo :global(.endoleap-wordmark) { display: none; } +.logo :global(.endoleap-mark) { width: 26px; height: 26px; } +.settings { margin-top: auto; } +.panel { width: 248px; padding: 0 12px 14px; display: flex; flex-direction: column; background: var(--surface); } +.identity, .panelTitle { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.identity { min-height: 58px; border-bottom: 1px solid var(--border); } +.panelTitle { padding: 8px 4px; min-height: 50px; } +.avatar { display: grid; place-items: center; flex: 0 0 auto; width: 28px; height: 28px; border-radius: 7px; background: var(--selection); color: var(--foreground-strong); } +.switcher { min-width: 0; position: relative; flex: 1; } +.switcher summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 7px; min-height: 36px; } +.switcher summary::-webkit-details-marker { display: none; } +.switcher strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.switcher summary svg { margin-left: auto; flex-shrink: 0; } +.menu { position: absolute; top: 42px; left: 0; width: 222px; z-index: 2; padding: 6px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); box-shadow: 0 8px 20px var(--shadow); max-height: 55dvh; overflow: auto; } +.menu button, .links a { display: flex; align-items: center; text-align: left; gap: 9px; width: 100%; min-height: 36px; padding: 6px 9px; border-radius: 6px; overflow-wrap: anywhere; } +.search { display: flex; align-items: center; gap: 8px; background: var(--surface-muted); color: var(--muted-foreground); border-radius: 7px; padding: 8px; } +.search input { min-width: 0; width: 100%; background: transparent; color: var(--foreground); border: 0; } +.search:focus-within, .dialog input:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } +.links { margin-top: 12px; overflow: auto; } +.links p, .local, .top, .content p, .tools small, .dialog p, .options small { color: var(--muted-foreground); } +.links p { margin: 24px 8px 5px; font-size: 12px; } +.local { margin-top: auto; padding: 12px 5px 0; border-top: 1px solid var(--border); } +.main { flex: 1; min-width: 0; background-image: radial-gradient(var(--border) .75px, transparent .75px); background-size: 24px 24px; } +.top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin: 12px 16px; } +.top > div { display: flex; gap: 8px; align-items: center; padding: 2px 12px 2px 3px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow-wrap: anywhere; } +.content, .status { max-width: 840px; margin: clamp(30px, 7vh, 90px) auto; padding: 0 clamp(15px, 4vw, 44px); } +.intro { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: clamp(20px, 4vw, 44px); } +.intro > svg { color: var(--primary); } +.intro h1 { margin: 9px 0 14px; font-size: clamp(25px, 4vw, 42px); line-height: 1.2; overflow-wrap: anywhere; } +.intro p { margin: 8px 0; line-height: 1.55; } +.primary { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 42px; padding: 8px 15px; border-radius: 7px; background: var(--primary); color: var(--primary-foreground); } +.primary:hover:not(:disabled) { background: var(--primary-hover); } +.intro .primary { margin-top: 15px; } +.content h2 { font-size: 16px; margin: 28px 0 12px; } +.tools { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } +.tools a, .options button { display: flex; align-items: center; gap: 12px; padding: 16px; text-align: left; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; } +.tools a:hover, .options button:hover { border-color: var(--selection-border); background: var(--row-hover); } +.tools a svg, .options svg { flex-shrink: 0; } +.tools a svg:last-child { margin-left: auto; } +.tools small, .options small { display: block; margin-top: 5px; line-height: 1.5; } +.dialog { width: min(535px, calc(100% - 24px)); max-height: calc(100dvh - 24px); padding: clamp(18px, 4vw, 32px); color: var(--foreground-strong); background: var(--surface); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 18px 48px var(--shadow); } +.dialog::backdrop { background: var(--scrim); } +.dialogTop { display: flex; align-items: center; gap: 6px; } +.dialogTop h2 { flex: 1; font-size: clamp(18px, 3vw, 24px); margin: 0; } +.dialog p { line-height: 1.55; } +.options { display: grid; gap: 12px; margin: 24px 0; } +.options button { min-height: 74px; } +.options button[aria-pressed=true] { background: var(--selection); border-color: var(--primary); } +.dialog .primary { width: 100%; } +.iconPreview { display: flex; align-items: center; gap: 10px; margin: 22px 0; } +.iconPreview .avatar { width: 48px; height: 48px; font-size: 20px; } +.dialog input { width: 100%; min-height: 42px; margin: 8px 0 20px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); color: var(--foreground-strong); } +.dialog input[aria-invalid=true] { border-color: var(--danger); margin-bottom: 0; } +.dialog .error { color: var(--danger); } +.notice { position: fixed; right: 16px; bottom: 16px; z-index: 4; max-width: calc(100vw - 32px); overflow-wrap: anywhere; padding: 14px 18px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); box-shadow: 0 8px 24px var(--shadow); } +@media (max-width: 760px) { .panel { position: fixed; left: 50px; width: min(248px, calc(100% - 50px)); z-index: 3; box-shadow: 8px 0 20px var(--shadow); } .rail { z-index: 4; } .top { position: sticky; top: 0; z-index: 4; margin-left: 8px; } .app:has(.panel) .top { z-index: 2; } .tools { grid-template-columns: 1fr; } }