mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23d9f94a9 | ||
|
|
fa058f1b15 | ||
|
|
0cad80096c |
No files matched your search
@@ -76,7 +76,7 @@ Do:
|
||||
- Use coral only for acceptance/active/focus moments — it must stay rare.
|
||||
- Keep the trace-line metaphor consistent: continuous rounded line, round terminals, one bright end point.
|
||||
- Respect `prefers-reduced-motion`: static frame, final states, no marquee.
|
||||
- Ground every claim in README facts (12 CLIs, 3 modes, local-first, human gate, V1.0.3).
|
||||
- Ground every claim in README facts (12 CLIs, 3 modes, local-first, human gate, V1.0.4).
|
||||
|
||||
Don't:
|
||||
- No gradient/shimmer headlines; no glassmorphism everywhere; no neon multi-color.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Meldwork Landing
|
||||
|
||||
单屏全屏背景 Landing Page,静态 HTML + CSS + 原生 JS,无构建步骤。风格参照「视频背景 + 点阵显示字体 + 白色 pill 导航」的参考 prompt,视觉系统完全适配 Meldwork 品牌。
|
||||
多章节 Landing Page,静态 HTML + CSS + 原生 JS,无构建步骤。保留首屏视频与点阵显示字体,下方章节使用低对比度 PixelBlast 背景。
|
||||
|
||||
## 打开方式
|
||||
|
||||
@@ -25,20 +25,39 @@ npx serve . # 或 python3 -m http.server
|
||||
## 设计决策
|
||||
|
||||
- 配色取自产品暗色主题 token(`#121516` 底、`#93a0a5` 弱化文字)与 logo 珊瑚色 `#EF5A45`;导航激活的三点指示器刻意使用珊瑚色,呼应 Trace mark 的「人类采用点」。
|
||||
- 显示字体 BubbledotICG-FinePos(点阵复古风,OnlineWebFonts CDN)仅覆盖拉丁字形;中文标题自动降级为思源黑体 900,字距单独调整。
|
||||
- 显示字体 BubbledotICG-FinePos(OnlineWebFonts CDN)仅覆盖拉丁字形;中文标题使用本地 Fusion Pixel 12px 补充,正文使用苹方等无衬线字体。字体来源与 OFL 许可位于 `assets/fonts/fusion-pixel/`。
|
||||
- 统计条数字为产品真实事实:12 个已支持 CLI、3 种协作模式、100% 本地工作单元、1 道人工采用门。
|
||||
- 动效遵循 `prefers-reduced-motion` 降级;弹窗/移动菜单支持 Escape 与遮罩关闭并锁定背景滚动。
|
||||
|
||||
## 双语
|
||||
|
||||
右上角 `EN / 中` 切换,文案以 `data-en` / `data-zh` 属性内联,选择记忆在 localStorage(不可用时仅本次会话生效)。默认语言跟随浏览器 `navigator.language`。
|
||||
页面默认英文,显式使用 `?lang=zh` 时显示中文。右上角 `EN / 中` 原页切换,并同步 URL 参数。`locale.js` 复用已有 `data-en` / `data-zh` 和翻译字典,保留阅读锚点、FAQ 展开项、演示模式和暂停状态;标题点阵在切换后重建。不刷新页面,不写入 localStorage。
|
||||
|
||||
## 升级为视频背景(可选)
|
||||
## 页面交互
|
||||
|
||||
当前背景为关键帧 + Ken Burns 缓慢推拉动画(`styles.css` 的 `.bg-still`)。生成一段与 `bg-keyframe.png` 同构的 10s 循环视频(Seedance image_to_video,提示词:coral trace line gently drawing forward, particles drifting, slow push-in, no text),保存为:
|
||||
- 下载文案与结构化版本为 V1.0.4,DMG 链接指向发布页的 `Meldwork-0.1.4-arm64.dmg`。
|
||||
- FAQ 默认打开第一项,点击其他问题会收起旧答案;使用可中断的 280ms 高度动画,减少动态效果时立即切换。
|
||||
- `run-demo.js` 用中英文聊天气泡分别演示直接会话的用户追问、多个 Agent 同时回复的独立建议,以及自动讨论的 @ 接续。新消息淡入并在固定高度的聊天区内滚动,示例文本不代表真实会话记录。模式选中样式仅包含文字变化和橙色下划线,无背景块或灰色边框。
|
||||
- 模式标签支持方向键、Home、End;演示支持暂停,离开可视区或进入后台时停止计时,减少动态效果时呈现完成状态。
|
||||
- 顶部导航无边框:首屏横跨页面,向下滚动超过 64px 后收窄为圆角毛玻璃浮栏。Logo 与章节入口靠左,下载、主题和语言切换靠右;窄屏使用左侧菜单和右侧下载图标。
|
||||
|
||||
```
|
||||
assets/meldwork-bg.mp4
|
||||
## 背景动效
|
||||
|
||||
首屏继续使用 `assets/meldwork-bg-v2.mp4`,保留原版循环与视差效果。视频失败或系统减少动态效果时使用静态关键帧。
|
||||
|
||||
`chapter-scene.js` 原样复用桌面 Agent CLI 加载页 `frontend/src/components/PixelBlast.vue` 的 GLSL 片元着色器(源自 React Bits PixelBlast),直接通过 WebGL2 绘制。沿用点阵尺寸 3.7px、密度 1.26、透明度 0.42、缩放 2.2、主题配色与每毫秒 0.00036 的时间推进速度,设备像素比上限为 1.5。点阵持续自行流动、聚散,不监听鼠标事件,也不随滚动重置动画。测试逐字比较加载页与 Landing 的片元着色器,防止效果再次偏离。来源许可见 `assets/react-bits-LICENSE.md`。
|
||||
|
||||
右下角按钮暂停/继续章节背景;首屏范围内、后台标签页及系统减少动态效果时不运行章节动画循环。减少动态效果模式保留静态背景。按钮图标沿用产品的 Ionicons 图形。
|
||||
|
||||
## 验证
|
||||
|
||||
在仓库根目录运行(需要已安装 frontend 开发依赖):
|
||||
|
||||
```bash
|
||||
node --test meldwork-landing/tests/landing.test.cjs
|
||||
node --check meldwork-landing/locale.js
|
||||
node --check meldwork-landing/chapter-scene.js
|
||||
node --check meldwork-landing/run-demo.js
|
||||
```
|
||||
|
||||
`<video>` 层会自动覆盖静态层,无需改代码;视频加载失败时仍回退到静态关键帧。
|
||||
安装 Playwright 后运行 `node meldwork-landing/tests/browser-check.cjs`;也可用 `PLAYWRIGHT_MODULE` 指定已有 Playwright 模块路径。检查 FAQ、原页语言切换、三个模式与 @ 接续、暂停、减少动态效果,以及 1440/900/390/320px 视口;截图写入系统临时目录。
|
||||
@@ -0,0 +1,96 @@
|
||||
Ark Pixel Font
|
||||
https://github.com/TakWolf/ark-pixel-font
|
||||
|
||||
Copyright (c) 2021, TakWolf (https://takwolf.com).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,108 @@
|
||||
[Cubic 11]
|
||||
These fonts are free software.
|
||||
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
|
||||
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
|
||||
此字型是免費的。
|
||||
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
|
||||
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
|
||||
[JF Dot M+H 12]
|
||||
Copyright(c) 2005 M+ FONTS PROJECT
|
||||
[M+ BITMAP FONTS]
|
||||
Copyright (C) 2002-2004 COZ
|
||||
These fonts are free software.
|
||||
Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
|
||||
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
|
||||
これらのフォントはフリー(自由な)ソフトウエアです。
|
||||
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2019–2025 Lee Minseo ([email protected])
|
||||
|
||||
This font software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
Fusion Pixel Font
|
||||
https://github.com/TakWolf/fusion-pixel-font
|
||||
|
||||
Copyright (c) 2022, TakWolf (https://takwolf.com).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Fusion Pixel
|
||||
|
||||
Chinese display fallback: Fusion Pixel 12px Proportional, Simplified Chinese.
|
||||
Release: 2026.09.01.
|
||||
Source: https://github.com/TakWolf/fusion-pixel-font/releases/tag/2026.09.01
|
||||
|
||||
The unmodified WOFF2 and its bundled license notices are distributed under the
|
||||
SIL Open Font License 1.1. Latin display text continues to use the existing
|
||||
Bubbledot font. Body copy does not use this pixel font.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
MIT + Commons Clause License Condition v1.0
|
||||
|
||||
Copyright (c) 2026 David Haz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, and distribute the Software **as part of an application, website, or product**, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
## Commons Clause Restriction
|
||||
|
||||
You may use this Software, including for any commercial purpose, **so long as you do not sell, sublicense, or redistribute the components themselves-whether alone, in a bundle, or as a ported version.**
|
||||
|
||||
## No Warranty
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,255 @@
|
||||
/* Exact PixelBlast fragment shader and motion parameters from
|
||||
* frontend/src/components/PixelBlast.vue (React Bits / David Haz).
|
||||
* assets/react-bits-LICENSE.md. Rendered directly with WebGL2 for the static page.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
var canvas = document.getElementById("chapterCanvas");
|
||||
var scene = document.querySelector(".chapter-scene");
|
||||
var toggle = document.getElementById("chapterMotionToggle");
|
||||
var hero = document.querySelector(".hero");
|
||||
var gl = canvas.getContext("webgl2", {
|
||||
alpha: true, antialias: false, premultipliedAlpha: false,
|
||||
powerPreference: "high-performance", preserveDrawingBuffer: true
|
||||
});
|
||||
if (!gl) { toggle.hidden = true; return; }
|
||||
|
||||
var FRAGMENT_SHADER = `
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uPixelSize;
|
||||
uniform float uScale;
|
||||
uniform float uDensity;
|
||||
uniform float uPixelJitter;
|
||||
uniform float uEdgeFade;
|
||||
uniform float uOpacity;
|
||||
uniform vec3 uPrimaryColor;
|
||||
uniform vec3 uSecondaryColor;
|
||||
uniform vec3 uTertiaryColor;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float Bayer2(vec2 a) {
|
||||
a = floor(a);
|
||||
return fract(a.x / 2.0 + a.y * a.y * 0.75);
|
||||
}
|
||||
|
||||
#define Bayer4(a) (Bayer2(0.5 * (a)) * 0.25 + Bayer2(a))
|
||||
#define Bayer8(a) (Bayer4(0.5 * (a)) * 0.25 + Bayer2(a))
|
||||
|
||||
float hash11(float n) {
|
||||
return fract(sin(n) * 43758.5453);
|
||||
}
|
||||
|
||||
float vnoise(vec3 p) {
|
||||
vec3 ip = floor(p);
|
||||
vec3 fp = fract(p);
|
||||
float n000 = hash11(dot(ip + vec3(0.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n100 = hash11(dot(ip + vec3(1.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n010 = hash11(dot(ip + vec3(0.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n110 = hash11(dot(ip + vec3(1.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n001 = hash11(dot(ip + vec3(0.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n101 = hash11(dot(ip + vec3(1.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n011 = hash11(dot(ip + vec3(0.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
||||
float n111 = hash11(dot(ip + vec3(1.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
||||
vec3 weight = fp * fp * fp * (fp * (fp * 6.0 - 15.0) + 10.0);
|
||||
float x00 = mix(n000, n100, weight.x);
|
||||
float x10 = mix(n010, n110, weight.x);
|
||||
float x01 = mix(n001, n101, weight.x);
|
||||
float x11 = mix(n011, n111, weight.x);
|
||||
return mix(mix(x00, x10, weight.y), mix(x01, x11, weight.y), weight.z) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
float fbm(vec2 uv, float time) {
|
||||
vec3 point = vec3(uv * uScale, time);
|
||||
float amplitude = 1.0;
|
||||
float frequency = 1.0;
|
||||
float sum = 1.0;
|
||||
for (int index = 0; index < 5; ++index) {
|
||||
sum += amplitude * vnoise(point * frequency);
|
||||
frequency *= 1.25;
|
||||
}
|
||||
return sum * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 fragCoord = gl_FragCoord.xy - uResolution * 0.5;
|
||||
float aspectRatio = uResolution.x / uResolution.y;
|
||||
vec2 pixelId = floor(fragCoord / uPixelSize);
|
||||
vec2 pixelUv = fract(fragCoord / uPixelSize);
|
||||
float cellPixelSize = 8.0 * uPixelSize;
|
||||
vec2 cellCoord = floor(fragCoord / cellPixelSize) * cellPixelSize;
|
||||
vec2 fieldUv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
|
||||
|
||||
float motionTime = uTime * (1.0 + 0.12 * sin(uTime * 0.17));
|
||||
vec2 drift = vec2(sin(uTime * 0.41), cos(uTime * 0.33)) * 0.045;
|
||||
float field = fbm(fieldUv + drift, motionTime * 0.05) * 0.5 - 0.65;
|
||||
float feed = field + (uDensity - 0.5) * 0.3;
|
||||
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
|
||||
float enabled = step(0.5, feed + bayer);
|
||||
float hash = fract(sin(dot(pixelId, vec2(127.1, 311.7))) * 43758.5453);
|
||||
float jitter = 1.0 + (hash - 0.5) * uPixelJitter;
|
||||
float inset = step(0.08, pixelUv.x) * step(0.08, pixelUv.y)
|
||||
* step(pixelUv.x, 0.92) * step(pixelUv.y, 0.92);
|
||||
float mask = enabled * jitter * inset;
|
||||
|
||||
vec2 normalized = gl_FragCoord.xy / uResolution;
|
||||
float edge = min(min(normalized.x, normalized.y), min(1.0 - normalized.x, 1.0 - normalized.y));
|
||||
mask *= smoothstep(0.0, uEdgeFade, edge);
|
||||
float centerDistance = length((normalized - 0.5) * vec2(aspectRatio, 1.0));
|
||||
// Keep the message area visually calm while preserving the animated field
|
||||
// around the edges of the discovery screen.
|
||||
mask *= mix(0.1, 1.0, smoothstep(0.14, 0.46, centerDistance));
|
||||
|
||||
float colorNoise = clamp(0.5 + 0.5 * vnoise(vec3((fieldUv + drift * 1.8) * 1.35, motionTime * 0.035)), 0.0, 1.0);
|
||||
vec3 color = mix(uPrimaryColor, uSecondaryColor, colorNoise);
|
||||
color = mix(color, uTertiaryColor, smoothstep(0.56, 1.0, normalized.y + colorNoise * 0.18));
|
||||
fragColor = vec4(color, mask * uOpacity);
|
||||
}
|
||||
`;
|
||||
var VERTEX_SHADER = `
|
||||
in vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
var program;
|
||||
var uniforms = {};
|
||||
var motion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
var paused = false, visible = false, lost = false;
|
||||
var frame = 0, previousFrame = 0;
|
||||
var time = Math.random() * 1000;
|
||||
|
||||
function compile(type, source) {
|
||||
var shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, "#version 300 es\n" + source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
var error = gl.getShaderInfoLog(shader);
|
||||
gl.deleteShader(shader);
|
||||
throw new Error(error);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
var vertex = compile(gl.VERTEX_SHADER, VERTEX_SHADER);
|
||||
var fragment = compile(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
||||
program = gl.createProgram();
|
||||
gl.attachShader(program, vertex);
|
||||
gl.attachShader(program, fragment);
|
||||
gl.linkProgram(program);
|
||||
gl.deleteShader(vertex);
|
||||
gl.deleteShader(fragment);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
|
||||
gl.useProgram(program);
|
||||
var buffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||||
var position = gl.getAttribLocation(program, "position");
|
||||
gl.enableVertexAttribArray(position);
|
||||
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
||||
["uResolution", "uTime", "uPixelSize", "uScale", "uDensity", "uPixelJitter",
|
||||
"uEdgeFade", "uOpacity", "uPrimaryColor", "uSecondaryColor", "uTertiaryColor"].forEach(function (name) {
|
||||
uniforms[name] = gl.getUniformLocation(program, name);
|
||||
});
|
||||
gl.uniform1f(uniforms.uScale, 2.2);
|
||||
gl.uniform1f(uniforms.uDensity, 1.26);
|
||||
gl.uniform1f(uniforms.uPixelJitter, 0.32);
|
||||
gl.uniform1f(uniforms.uEdgeFade, 0.12);
|
||||
gl.uniform1f(uniforms.uOpacity, 0.42);
|
||||
canvas.dataset.renderer = "webgl2";
|
||||
}
|
||||
|
||||
// Match THREE.Color's sRGB-to-linear conversion for the desktop theme colors.
|
||||
function color(name, hex) {
|
||||
var rgb = hex.match(/[a-f0-9]{2}/gi).map(function (channel) {
|
||||
var value = parseInt(channel, 16) / 255;
|
||||
return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
gl.uniform3fv(uniforms[name], rgb);
|
||||
}
|
||||
function updateTheme() {
|
||||
if (lost) return;
|
||||
var light = document.documentElement.getAttribute("data-theme") === "light";
|
||||
color("uPrimaryColor", light ? "#d92f24" : "#ff836d");
|
||||
color("uSecondaryColor", light ? "#007d91" : "#54f5ff");
|
||||
color("uTertiaryColor", light ? "#ed512f" : "#ffa36f");
|
||||
paint();
|
||||
}
|
||||
function paint() {
|
||||
if (lost) return;
|
||||
gl.uniform1f(uniforms.uTime, time);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
}
|
||||
function updateButton() {
|
||||
toggle.hidden = !visible || motion.matches || lost;
|
||||
toggle.setAttribute("aria-pressed", String(paused));
|
||||
var label = paused ? "Resume background animation" : "Pause background animation";
|
||||
label = window.MeldworkLocale ? window.MeldworkLocale.t(label) : label;
|
||||
toggle.setAttribute("aria-label", label);
|
||||
toggle.title = label;
|
||||
}
|
||||
function running() { return visible && !paused && !motion.matches && !document.hidden && !lost; }
|
||||
function tick(now) {
|
||||
frame = 0;
|
||||
if (!running()) return;
|
||||
if (previousFrame) time += Math.min(32, now - previousFrame) * 0.00036;
|
||||
previousFrame = now;
|
||||
paint();
|
||||
frame = requestAnimationFrame(tick);
|
||||
}
|
||||
function sync() {
|
||||
var entrance = Math.max(0, Math.min(1, (window.scrollY - hero.offsetHeight * 0.55) / (window.innerHeight * 0.45)));
|
||||
visible = entrance > 0;
|
||||
scene.style.opacity = entrance.toFixed(3);
|
||||
updateButton();
|
||||
if (running()) {
|
||||
if (!frame) frame = requestAnimationFrame(tick);
|
||||
} else {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = 0;
|
||||
previousFrame = 0;
|
||||
}
|
||||
}
|
||||
function resize() {
|
||||
if (lost) return;
|
||||
var ratio = Math.min(window.devicePixelRatio || 1, 1.5);
|
||||
canvas.width = Math.round(window.innerWidth * ratio);
|
||||
canvas.height = Math.round(window.innerHeight * ratio);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);
|
||||
gl.uniform1f(uniforms.uPixelSize, 3.7 * ratio);
|
||||
paint();
|
||||
sync();
|
||||
}
|
||||
|
||||
try { initialize(); } catch (error) {
|
||||
toggle.hidden = true;
|
||||
console.error("PixelBlast initialization failed:", error);
|
||||
return;
|
||||
}
|
||||
toggle.addEventListener("click", function () { paused = !paused; sync(); });
|
||||
window.addEventListener("scroll", sync, { passive: true });
|
||||
window.addEventListener("resize", resize);
|
||||
document.addEventListener("visibilitychange", sync);
|
||||
motion.addEventListener("change", sync);
|
||||
window.addEventListener("meldwork:languagechange", updateButton);
|
||||
new ResizeObserver(sync).observe(hero);
|
||||
new MutationObserver(updateTheme).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
||||
canvas.addEventListener("webglcontextlost", function (event) {
|
||||
event.preventDefault();
|
||||
lost = true;
|
||||
sync();
|
||||
});
|
||||
canvas.addEventListener("webglcontextrestored", function () {
|
||||
lost = false;
|
||||
initialize();
|
||||
updateTheme();
|
||||
resize();
|
||||
});
|
||||
updateTheme();
|
||||
resize();
|
||||
})();
|
||||
+506
-251
@@ -11,63 +11,84 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Meldwork",
|
||||
"applicationCategory": "DeveloperApplication",
|
||||
"operatingSystem": "macOS (Apple silicon)",
|
||||
"softwareVersion": "1.0.3",
|
||||
"description": "Meldwork is a local-first work cell for AI coding agents. It orchestrates agent CLIs such as Codex, Claude Code, and Gemini CLI around one frozen task snapshot, records every finding as evidence, and puts a human adoption gate before any workspace change.",
|
||||
"featureList": "Local Agent discovery, Multi-agent review, Auto Discussion V4, Direct mode, Human Gate, Evidence Trail, Frozen Context",
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
|
||||
"author": { "@type": "Organization", "name": "Meldwork", "url": "https://github.com/Ryder-MHumble/Meldwork" },
|
||||
"downloadUrl": "https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.3"
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Meldwork",
|
||||
"applicationCategory": "DeveloperApplication",
|
||||
"operatingSystem": "macOS (Apple silicon)",
|
||||
"softwareVersion": "1.0.4",
|
||||
"description": "Meldwork is a local-first work cell for AI coding agents. It orchestrates agent CLIs such as Codex, Claude Code, and Gemini CLI around one frozen task snapshot, records every finding as evidence, and puts a human adoption gate before any workspace change.",
|
||||
"featureList": "Local Agent discovery, Multi-agent review, Auto Discussion V4, Direct mode, Human Gate, Evidence Trail, Frozen Context",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is Meldwork?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Meldwork is a local-first work cell for AI coding agents. It orchestrates the agent CLIs you already run — Codex, Claude Code, Gemini CLI, and 9 more — around one frozen task snapshot, records every finding as evidence, and puts a human adoption gate before any workspace change." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Does Meldwork send my code to a remote service?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "No, not by itself. Meldwork runs locally in your Electron work cell. Selected Agents send prompts, attachments, or Skills to their own configured Provider — but Meldwork itself is a local review UI, not a cloud relay. Conversations stay on your machine." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Which AI coding agents does Meldwork support?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Twelve today — Codex, Claude Code, Gemini CLI, Qwen Code, Kimi Code, MiMo Code, OpenCode, OpenCodeReview, Hermes, Pi Agent, OpenClaw, and WorkBuddy. Approved Agent Connectors can be added through the Agent Connector SDK; custom executables use the desktop custom-Agent path." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What does Auto Discussion V4 actually do?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Selected Agents independently propose, then critique with evidence, negotiate responsibilities, execute dependency-aware work, and verify the result. Every round is preserved — you can read the full Case later and adopt only the parts you approve." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How is Meldwork different from a terminal or IDE?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "A terminal runs one Agent at a time and shows raw output. Meldwork scopes a single task across multiple Agents, captures their independent findings, attaches evidence, and keeps the human adoption decision visible as a first-class artifact." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is Meldwork free? What's the license?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "Source-available under the Meldwork Non-Commercial Source License 1.0. You can inspect, run, modify, and share it for non-commercial purposes. Commercial use requires prior written permission." }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Does Meldwork work on Intel macOS, Windows, or Linux?",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "The current V1.0.3 preview targets Apple silicon macOS (ad-hoc signed, not notarized — open Open Anyway in System Settings → Privacy & Security on first launch). Cross-platform builds are on the roadmap; the app is Electron so the same source compiles everywhere once those targets land." }
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "Meldwork",
|
||||
"url": "https://github.com/Ryder-MHumble/Meldwork"
|
||||
},
|
||||
"downloadUrl": "https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.4"
|
||||
},
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is Meldwork?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Meldwork is a local-first desktop workspace for multi-Agent research and review. It connects user-selected Agent CLIs through direct conversations, concurrent responses, and multi-round discussion."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What do I need before I start?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "An Apple silicon Mac and at least one supported Agent CLI installed and authenticated. The preview includes 12 built-in Agent adapters. Detection and capabilities depend on the installed CLI and its compatible version; model access and usage charges remain with your chosen provider."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How does Auto Discussion work?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "You choose the participants. Agents begin with independent proposals, then continue in sequence or route the next turn to selected peers in Agent-led mode. Native sessions continue when the adapter supports them. You can inspect each Agent's replies and run status."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Does local-first mean fully offline?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "No. Meldwork stores its conversations and run records locally and has no Meldwork-hosted conversation service. Selected Agents or connectors may send prompts, attachments, or Skills to their configured providers. Their data handling and network access follow their own configuration and terms."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Can Agents change my files?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Workspace writes are opt-in. Set the working directory and permissions before a run, and respond to any Human Gate that requires your input. These are workflow controls, not an operating-system sandbox; enforcement also depends on the selected CLI."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Which platforms and license does the preview support?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "V1.0.4 is an Apple silicon macOS preview, ad-hoc signed and not notarized. Other platforms are not supported packaged targets in this preview. Meldwork uses Apache License 2.0, which permits commercial use under its terms. Agent CLIs, providers, and third-party assets may have separate terms."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/meldwork-favicon-32.png" />
|
||||
@@ -87,6 +108,7 @@
|
||||
|
||||
<!-- ================= NAV ================= -->
|
||||
<header class="topnav" id="topnav">
|
||||
<div class="nav-start">
|
||||
<a class="logo-badge" href="#top" aria-label="Meldwork home">
|
||||
<img src="assets/meldwork-mark-v3-dark.svg" alt="" width="52" height="52" data-theme-src="assets/meldwork-mark-v3.svg" />
|
||||
</a>
|
||||
@@ -98,16 +120,21 @@
|
||||
<a href="#compare" class="nav-link" data-spy="compare"><span>How it differs</span></a>
|
||||
<a href="#faq" class="nav-link" data-spy="faq"><span>FAQ</span></a>
|
||||
</nav>
|
||||
<a class="cta-dark magnet" href="https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.3" target="_blank" rel="noopener">
|
||||
<span class="cta-status"></span> Download V1.0.3
|
||||
<button type="button" class="burger" id="burger" aria-label="Menu" aria-expanded="false" aria-controls="mobileMenu">
|
||||
<span class="burger-bar"></span><span class="burger-bar"></span><span class="burger-bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<a class="cta-dark" href="https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.4" target="_blank" rel="noopener" aria-label="Download V1.0.4">
|
||||
<span class="cta-status"></span><span class="nav-download-label">Download V1.0.4</span>
|
||||
<svg class="nav-download-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</a>
|
||||
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
|
||||
<svg class="sun-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
|
||||
<svg class="moon-icon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
||||
</button>
|
||||
<button type="button" class="burger" id="burger" aria-label="Menu" aria-expanded="false" aria-controls="mobileMenu">
|
||||
<span class="burger-bar"></span><span class="burger-bar"></span><span class="burger-bar"></span>
|
||||
</button>
|
||||
<a class="locale-toggle" id="localeToggle" href="?lang=en" aria-label="Switch to English">EN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="overlay" id="overlay" hidden></div>
|
||||
@@ -118,7 +145,7 @@
|
||||
<a href="#scenario" class="m-link"><span>In a run</span></a>
|
||||
<a href="#compare" class="m-link"><span>How it differs</span></a>
|
||||
<a href="#faq" class="m-link"><span>FAQ</span></a>
|
||||
<a href="https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.3" target="_blank" rel="noopener" class="m-link m-signin"><span>Download V1.0.3</span></a>
|
||||
<a href="https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.4" target="_blank" rel="noopener" class="m-link m-signin"><span>Download V1.0.4</span></a>
|
||||
</nav>
|
||||
|
||||
<!-- ================= HERO ================= -->
|
||||
@@ -147,9 +174,9 @@
|
||||
<p class="subhead">Meldwork builds the <strong>organization layer</strong> for AI agents — a local work cell that keeps participants explicit, context scoped, run state inspectable, and human review in the loop.</p>
|
||||
|
||||
<div class="cta-row">
|
||||
<a class="cta-primary magnet" href="https://github.com/Ryder-MHumble/Meldwork/releases/download/Meldwork-V1.0.3/Meldwork-0.1.3-arm64.dmg">
|
||||
<a class="cta-primary magnet" href="https://github.com/Ryder-MHumble/Meldwork/releases/download/Meldwork-V1.0.4/Meldwork-0.1.4-arm64.dmg">
|
||||
<span class="cta-label">Download for macOS</span>
|
||||
<span class="cta-meta">V1.0.3 · Apple silicon</span>
|
||||
<span class="cta-meta">V1.0.4 · Apple silicon</span>
|
||||
</a>
|
||||
<a class="cta-ghost" href="https://github.com/Ryder-MHumble/Meldwork" target="_blank" rel="noopener">View on GitHub <span aria-hidden="true">↗</span></a>
|
||||
</div>
|
||||
@@ -194,6 +221,11 @@
|
||||
</section>
|
||||
|
||||
<!-- ================= AGENT LOGO LOOP ================= -->
|
||||
<div class="chapter-scene" aria-hidden="true"><canvas id="chapterCanvas"></canvas></div>
|
||||
<button class="chapter-motion-toggle" id="chapterMotionToggle" type="button" aria-label="Pause background animation" aria-pressed="false" hidden>
|
||||
<svg class="pause-icon" viewBox="0 0 512 512" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 96h16v320h-16z M320 96h16v320h-16z" /></svg>
|
||||
<svg class="play-icon" viewBox="0 0 512 512" aria-hidden="true"><path d="M112 111v290c0 17.44 17 28.52 31 20.16l247.9-148.37c12.12-7.25 12.12-26.33 0-33.58L143 90.84c-14-8.36-31 2.72-31 20.16z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32" /></svg>
|
||||
</button>
|
||||
<section class="section agents" id="agents">
|
||||
<p class="eyebrow reveal" data-reveal>DETECTED ON YOUR MACHINE</p>
|
||||
<h2 class="section-title reveal" data-reveal>Works with the agents you already run.</h2>
|
||||
@@ -351,82 +383,49 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= SCENARIO (animated stepper) ================= -->
|
||||
<!-- ================= COLLABORATION DEMO ================= -->
|
||||
<section class="section scenario" id="scenario">
|
||||
<p class="eyebrow reveal" data-reveal>INSIDE A REAL RUN</p>
|
||||
<h2 class="section-title reveal" data-reveal>Launch Readiness Council — three rounds, one gate.</h2>
|
||||
<p class="section-sub reveal" data-reveal>Below: the actual evidence from a V1.0.3 session where Codex, Hermes, and OpenCodeReview reviewed a private-beta release decision. Scroll or wait — the timeline advances on its own.</p>
|
||||
|
||||
<div class="scenario-stage reveal" data-reveal data-scenario>
|
||||
<div class="mac-titlebar">
|
||||
<span class="mac-lights" aria-hidden="true"><i class="light-r"></i><i class="light-y"></i><i class="light-g"></i></span>
|
||||
<div class="scenario-meta">
|
||||
<span class="meta-pill"><span class="meta-dot"></span> Auto Discussion V4</span>
|
||||
<span class="meta-name">Launch Readiness Council</span>
|
||||
</div>
|
||||
<div class="titlebar-right">
|
||||
<span class="meta-round" data-round>Round 1 · Propose</span>
|
||||
<div class="seg-control" role="group" aria-label="Playback mode">
|
||||
<button type="button" class="seg-btn is-on" data-mode="auto">Auto</button>
|
||||
<button type="button" class="seg-btn" data-mode="order">Sequence</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="eyebrow" data-en="INSIDE A REAL RUN" data-zh="协作进行时">INSIDE A REAL RUN</p>
|
||||
<h2 class="section-title" data-en="One task. Three ways forward." data-zh="同一个任务,三种协作方式。">One task. Three ways forward.</h2>
|
||||
<p class="section-sub" data-en="A focused conversation, independent perspectives, or Agents building on each other's work." data-zh="与一个 Agent 深入对话,让多个 Agent 独立作答,或让它们接续彼此的工作。">A focused conversation, independent perspectives, or Agents building on each other's work.</p>
|
||||
<div class="run-demo" data-run-demo data-mode="auto" data-phase="0">
|
||||
<div class="run-tabs" role="tablist" aria-label="Collaboration modes">
|
||||
<button id="run-direct" class="run-tab" role="tab" aria-selected="false" aria-controls="run-panel" tabindex="-1" data-run-mode="direct">
|
||||
<span class="run-tab-number">01</span><span data-en="Direct" data-zh="直接会话">Direct</span>
|
||||
<small data-en="You + one Agent" data-zh="你与一个 Agent">You + one Agent</small>
|
||||
</button>
|
||||
<button id="run-concurrent" class="run-tab" role="tab" aria-selected="false" aria-controls="run-panel" tabindex="-1" data-run-mode="concurrent">
|
||||
<span class="run-tab-number">02</span><span data-en="Concurrent Responses" data-zh="并发回复">Concurrent Responses</span>
|
||||
<small data-en="One brief, independent replies" data-zh="同一任务,独立回复">One brief, independent replies</small>
|
||||
</button>
|
||||
<button id="run-auto" class="run-tab" role="tab" aria-selected="true" aria-controls="run-panel" data-run-mode="auto">
|
||||
<span class="run-tab-number">03</span><span data-en="Auto Discussion" data-zh="自动讨论">Auto Discussion</span>
|
||||
<small data-en="Agents lead the next turn" data-zh="Agent 自主接续讨论">Agents lead the next turn</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scenario-body">
|
||||
<div class="timeline-track" data-timeline>
|
||||
<div class="timeline-rail" aria-hidden="true">
|
||||
<span class="rail-line"></span>
|
||||
<span class="rail-progress" data-rail></span>
|
||||
</div>
|
||||
<ol class="timeline" data-timeline-list>
|
||||
<li class="t-step is-active" data-step="0">
|
||||
<span class="t-node" data-node></span>
|
||||
<span class="t-time">R1 · 05:34</span>
|
||||
<div class="t-bubble t-bubble-codex">
|
||||
<header><img src="assets/agents/codex.svg" alt="" /><strong>Codex</strong><span class="t-role">Proposal</span></header>
|
||||
<p>Ship a <strong>private, invitation-only preview</strong> with one promise: keep a real task coherent while users move between Agents. Don't market workflow automation, universal compatibility, or public distribution readiness yet.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="t-step" data-step="1">
|
||||
<span class="t-node" data-node></span>
|
||||
<span class="t-time">R1 · 05:38</span>
|
||||
<div class="t-bubble t-bubble-hermes">
|
||||
<header><img src="assets/agents/hermes.svg" alt="" /><strong>Hermes</strong><span class="t-role">Evidence challenge</span></header>
|
||||
<p>The beta should <strong>test behavior</strong>, not enthusiasm. Measure whether users resume the same task, bring in a second Agent for a clear reason, and use the trace before accepting a result. Without those signals, the category story is still unproven.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="t-step" data-step="2">
|
||||
<span class="t-node" data-node></span>
|
||||
<span class="t-time">R2 · 05:44</span>
|
||||
<div class="t-bubble t-bubble-ocr">
|
||||
<header><img src="assets/agents/opencodereview.svg" alt="" /><strong>OpenCodeReview</strong><span class="t-role">Engineering review</span></header>
|
||||
<p>The MVP <strong>is</strong> stable enough for a controlled evaluation, but not for a public download. Tests and packaging pass; the blocker is trusted macOS signing and notarization. Keep the first release private and label unsupported platforms explicitly.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="t-step" data-step="3">
|
||||
<span class="t-node" data-node></span>
|
||||
<span class="t-time">R2 · 05:52</span>
|
||||
<div class="t-bubble t-bubble-you">
|
||||
<header><span class="you-dot">You</span><strong>You</strong><span class="t-role">Reconciliation</span></header>
|
||||
<p>Round 2: reconcile the disagreement. Keep only decisions we can verify this week and give me <strong>one acceptance gate</strong>.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="t-step" data-step="4">
|
||||
<span class="t-node" data-node></span>
|
||||
<span class="t-time">Adoption</span>
|
||||
<div class="t-bubble t-bubble-gate">
|
||||
<header><span class="gate-dot"></span><strong>Human Gate</strong><span class="t-role">Decision</span></header>
|
||||
<p>Ship V1.0.3 to the private preview only. <strong>No-Go for public release.</strong> Reopen after Developer ID signing and notarization pass on a clean Mac.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<div id="run-panel" class="run-panel" role="tabpanel" aria-labelledby="run-auto" tabindex="0">
|
||||
<div class="run-explanation">
|
||||
<p data-run-description></p>
|
||||
<span class="run-example-label" data-en="Illustrative workflow" data-zh="协作流程示意">Illustrative workflow</span>
|
||||
</div>
|
||||
|
||||
<div class="scenario-controls">
|
||||
<button type="button" class="rail-btn" data-prev aria-label="Previous step">‹</button>
|
||||
<span class="rail-status" data-status>1 / 5</span>
|
||||
<button type="button" class="rail-btn" data-next aria-label="Next step">›</button>
|
||||
<div class="run-window">
|
||||
<div class="run-window-bar" aria-hidden="true">
|
||||
<span class="run-window-lights"><i></i><i></i><i></i></span>
|
||||
<span class="run-window-title" data-en="Local run window" data-zh="本地协作窗口">Local run window</span>
|
||||
</div>
|
||||
<div class="run-window-body">
|
||||
<div class="run-chat" tabindex="0" aria-label="Example conversation">
|
||||
<ol class="run-messages" data-run-messages></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="run-playback">
|
||||
<div class="run-progress" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<span data-run-status></span>
|
||||
<button class="run-play-toggle" type="button" aria-label="Pause demo" title="Pause demo" aria-pressed="false" data-run-pause>
|
||||
<svg class="pause-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H5v16h4zM19 4h-4v16h4z" fill="currentColor"/></svg>
|
||||
<svg class="play-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 4 14 8-14 8z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -549,105 +548,286 @@
|
||||
</section>
|
||||
|
||||
<!-- ================= TECH SPECS / BOUNDARY ================= -->
|
||||
<section class="section boundary" id="boundary">
|
||||
<p class="eyebrow reveal" data-reveal>THE BOUNDARY</p>
|
||||
<h2 class="section-title reveal" data-reveal>What Meldwork is, and isn't.</h2>
|
||||
<div class="bnd-grid">
|
||||
<div class="bnd-col reveal" data-reveal style="--d:0s">
|
||||
<h3><span class="bnd-glyph bnd-yes"></span> Is</h3>
|
||||
<ul>
|
||||
<li>A local-first Electron app for desktop review workflows.</li>
|
||||
<li>A work cell that orchestrates the Agent CLIs you already have.</li>
|
||||
<li>An evidence-first review record (Finding → Evidence → Decision → Disposition).</li>
|
||||
<li>Workspace writes as opt-in workflow controls.</li>
|
||||
<li>Source-available under the Meldwork Non-Commercial Source License 1.0.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bnd-col reveal" data-reveal style="--d:0.12s">
|
||||
<h3><span class="bnd-glyph bnd-no"></span> Isn't</h3>
|
||||
<ul>
|
||||
<li>Not a hosted Agent fleet or general Agent framework.</li>
|
||||
<li>Not automatic participant selection from a larger roster.</li>
|
||||
<li>Not enterprise SSO / RBAC / governance tooling.</li>
|
||||
<li>Not an operating-system sandbox — writes happen through your shell.</li>
|
||||
<li>Not fully offline — a selected Agent may send prompts to its configured Provider.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="section boundary"
|
||||
id="boundary"
|
||||
aria-labelledby="boundary-title"
|
||||
>
|
||||
<p
|
||||
class="eyebrow"
|
||||
data-en="ARCHITECTURE AND PERMISSIONS"
|
||||
data-zh="架构与权限"
|
||||
>
|
||||
ARCHITECTURE AND PERMISSIONS
|
||||
</p>
|
||||
<h2
|
||||
class="section-title"
|
||||
id="boundary-title"
|
||||
data-en="Local workspace. Explicit boundaries."
|
||||
data-zh="工作空间在本地,权限边界说清楚。"
|
||||
>
|
||||
Local workspace. Explicit boundaries.
|
||||
</h2>
|
||||
<ol class="architecture-path">
|
||||
<li>
|
||||
<span class="architecture-step">01</span>
|
||||
<h3 data-en="Meldwork on your Mac" data-zh="Mac 上的 Meldwork">
|
||||
Meldwork on your Mac
|
||||
</h3>
|
||||
<p
|
||||
data-en="The desktop app keeps conversations, run records, and user controls in local storage."
|
||||
data-zh="桌面应用在本地保存会话、运行记录与用户设置。"
|
||||
>
|
||||
The desktop app keeps conversations, run records, and user
|
||||
controls in local storage.
|
||||
</p>
|
||||
<span class="boundary-label" data-en="Local" data-zh="本地"
|
||||
>Local</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="architecture-step">02</span>
|
||||
<h3 data-en="Your selected Agent CLI" data-zh="你选择的 Agent CLI">
|
||||
Your selected Agent CLI
|
||||
</h3>
|
||||
<p
|
||||
data-en="A local process receives the scoped task and permissions through its adapter."
|
||||
data-zh="本地进程通过适配器接收限定的任务与权限。"
|
||||
>
|
||||
A local process receives the scoped task and permissions through
|
||||
its adapter.
|
||||
</p>
|
||||
<span
|
||||
class="boundary-label"
|
||||
data-en="Local process"
|
||||
data-zh="本地进程"
|
||||
>Local process</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="architecture-step">03</span>
|
||||
<h3 data-en="Its configured provider" data-zh="它配置的模型服务商">
|
||||
Its configured provider
|
||||
</h3>
|
||||
<p
|
||||
data-en="Prompts, attachments, or Skills may be sent to external services under their own terms."
|
||||
data-zh="提示词、附件或技能内容可能发送到外部服务,并受其条款约束。"
|
||||
>
|
||||
Prompts, attachments, or Skills may be sent to external services
|
||||
under their own terms.
|
||||
</p>
|
||||
<span
|
||||
class="boundary-label"
|
||||
data-en="May be remote"
|
||||
data-zh="可能在远端"
|
||||
>May be remote</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="bnd-grid">
|
||||
<div class="bnd-col">
|
||||
<h3 data-en="You choose the scope." data-zh="由你决定范围。">
|
||||
You choose the scope.
|
||||
</h3>
|
||||
<ul>
|
||||
<li
|
||||
data-en="Select the participants and working directory."
|
||||
data-zh="选择参与者与工作目录。"
|
||||
>
|
||||
Select the participants and working directory.
|
||||
</li>
|
||||
<li
|
||||
data-en="Workspace writes are opt-in; Human Gates request input when required."
|
||||
data-zh="工作区写入需主动启用;需要时通过人工审批门请求你的输入。"
|
||||
>
|
||||
Workspace writes are opt-in; Human Gates request input when
|
||||
required.
|
||||
</li>
|
||||
<li
|
||||
data-en="Inspect available replies, run states, and execution traces."
|
||||
data-zh="查看可用的回复、运行状态与执行轨迹。"
|
||||
>
|
||||
Inspect available replies, run states, and execution traces.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bnd-col">
|
||||
<h3 data-en="Know the limits." data-zh="明确当前限制。">
|
||||
Know the limits.
|
||||
</h3>
|
||||
<ul>
|
||||
<li
|
||||
data-en="Local-first does not mean fully offline."
|
||||
data-zh="本地优先不等于完全离线。"
|
||||
>
|
||||
Local-first does not mean fully offline.
|
||||
</li>
|
||||
<li
|
||||
data-en="Permission controls are not an operating-system sandbox."
|
||||
data-zh="权限控制不是操作系统级沙箱。"
|
||||
>
|
||||
Permission controls are not an operating-system sandbox.
|
||||
</li>
|
||||
<li
|
||||
data-en="The preview does not automatically select a team or provide enterprise SSO and RBAC."
|
||||
data-zh="预览版不提供自动选人组队或企业级单点登录与角色权限治理。"
|
||||
>
|
||||
The preview does not automatically select a team or provide
|
||||
enterprise SSO and RBAC.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork/blob/main/architecture.md"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-en="Read the architecture and data boundaries"
|
||||
data-zh="阅读架构与数据边界说明"
|
||||
>Read the architecture and data boundaries</a
|
||||
>
|
||||
</section>
|
||||
|
||||
<!-- ================= FAQ ================= -->
|
||||
<section class="section faq" id="faq">
|
||||
<p class="eyebrow reveal" data-reveal>ANSWERS</p>
|
||||
<h2 class="section-title reveal" data-reveal>Questions teams ask before adopting.</h2>
|
||||
<p class="section-sub reveal" data-reveal>Short, honest answers — the long versions live in <a href="https://github.com/Ryder-MHumble/Meldwork/blob/main/architecture.md" target="_blank" rel="noopener">architecture.md ↗</a>.</p>
|
||||
|
||||
<ul class="acc-list">
|
||||
<li class="acc-item reveal" data-reveal style="--d:0s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>What is Meldwork?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>Meldwork is a <strong>local-first work cell for AI coding agents</strong>. It orchestrates the agent CLIs you already run — Codex, Claude Code, Gemini CLI, and 9 more — around one frozen task snapshot, records every finding as evidence, and puts a human adoption gate before any workspace change.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.06s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>Does Meldwork send my code to a remote service?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>No, not by itself. Meldwork runs locally in your Electron work cell. Selected Agents send prompts, attachments, or Skills to <em>their own</em> configured Provider — but Meldwork itself is a local review UI, not a cloud relay. Conversations stay on your machine.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.06s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>Which AI coding agents does Meldwork support?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>Twelve today — Codex, Claude Code, Gemini CLI, Qwen Code, Kimi Code, MiMo Code, OpenCode, OpenCodeReview, Hermes, Pi Agent, OpenClaw, and WorkBuddy. Approved Agent Connectors can be added through the Agent Connector SDK; custom executables use the desktop custom-Agent path.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.12s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>What does Auto Discussion V4 actually do?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>Selected Agents independently propose, then critique with evidence, negotiate responsibilities, execute dependency-aware work, and verify the result. Every round is preserved — you can read the full Case later and adopt only the parts you approve.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.18s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>How is Meldwork different from a terminal or IDE?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>A terminal runs <em>one</em> Agent at a time and shows raw output. Meldwork scopes a single task across multiple Agents, captures their independent findings, attaches evidence, and keeps the human adoption decision visible as a first-class artifact.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.24s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>Is Meldwork free? What's the license?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>Source-available under the Meldwork Non-Commercial Source License 1.0. You can inspect, run, modify, and share it for non-commercial purposes. Commercial use requires prior written permission — see <a href="https://github.com/Ryder-MHumble/Meldwork/blob/main/COMMERCIAL_USE.md" target="_blank" rel="noopener">COMMERCIAL_USE.md ↗</a>.</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="acc-item reveal" data-reveal style="--d:0.3s">
|
||||
<button type="button" class="acc-trigger" aria-expanded="false">
|
||||
<span>Does it work on Intel macOS, Windows, or Linux?</span>
|
||||
<span class="acc-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="acc-panel">
|
||||
<p>The current V1.0.3 preview targets Apple silicon macOS (ad-hoc signed, not notarized — open <em>Open Anyway</em> in <em>System Settings → Privacy & Security</em> on first launch). Cross-platform builds are on the roadmap; the app is Electron so the same source compiles everywhere once those targets land.</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="section faq" id="faq" aria-labelledby="faq-title">
|
||||
<h2
|
||||
class="section-title"
|
||||
id="faq-title"
|
||||
data-en="Before you get started."
|
||||
data-zh="开始前,你可能想了解。"
|
||||
>
|
||||
Before you get started.
|
||||
</h2>
|
||||
<div class="faq-list">
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="What is Meldwork?"
|
||||
data-zh="Meldwork 是什么?"
|
||||
>
|
||||
What is Meldwork?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-schema-description
|
||||
data-en="Meldwork is a local-first desktop workspace for multi-Agent research and review. It connects user-selected Agent CLIs through direct conversations, concurrent responses, and multi-round discussion."
|
||||
data-zh="Meldwork 是本地优先的多 Agent 研究与复核桌面工作空间,通过直接会话、并发回复和多轮讨论连接用户选择的 Agent CLI。"
|
||||
>
|
||||
Meldwork is a local-first desktop workspace for multi-Agent
|
||||
research and review. It connects user-selected Agent CLIs
|
||||
through direct conversations, concurrent responses, and
|
||||
multi-round discussion.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="What do I need before I start?"
|
||||
data-zh="开始前需要准备什么?"
|
||||
>
|
||||
What do I need before I start?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-en="An Apple silicon Mac and at least one supported Agent CLI installed and authenticated. The preview includes 12 built-in Agent adapters. Detection and capabilities depend on the installed CLI and its compatible version; model access and usage charges remain with your chosen provider."
|
||||
data-zh="一台 Apple 芯片 Mac,以及至少一个已安装并完成认证的受支持 Agent CLI。预览版内置 12 个 Agent 适配器,检测与能力取决于已安装的 CLI 及兼容版本;模型访问与使用费用由你选择的服务商提供和收取。"
|
||||
>
|
||||
An Apple silicon Mac and at least one supported Agent CLI
|
||||
installed and authenticated. The preview includes 12 built-in
|
||||
Agent adapters. Detection and capabilities depend on the
|
||||
installed CLI and its compatible version; model access and usage
|
||||
charges remain with your chosen provider.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="How does Auto Discussion work?"
|
||||
data-zh="自动讨论如何运行?"
|
||||
>
|
||||
How does Auto Discussion work?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-en="You choose the participants. Agents begin with independent proposals, then continue in sequence or route the next turn to selected peers in Agent-led mode. Native sessions continue when the adapter supports them. You can inspect each Agent's replies and run status."
|
||||
data-zh="由你选择参与者。Agent 先形成独立提案,再按顺序继续,或在 Agent 主导模式下将下一轮交给已选参与者。适配器支持时会延续原生会话,你可以查看各 Agent 的回复与运行状态。"
|
||||
>
|
||||
You choose the participants. Agents begin with independent
|
||||
proposals, then continue in sequence or route the next turn to
|
||||
selected peers in Agent-led mode. Native sessions continue when
|
||||
the adapter supports them. You can inspect each Agent's replies
|
||||
and run status.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="Does local-first mean fully offline?"
|
||||
data-zh="本地优先是否意味着完全离线?"
|
||||
>
|
||||
Does local-first mean fully offline?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-en="No. Meldwork stores its conversations and run records locally and has no Meldwork-hosted conversation service. Selected Agents or connectors may send prompts, attachments, or Skills to their configured providers. Their data handling and network access follow their own configuration and terms."
|
||||
data-zh="不是。Meldwork 在本地保存会话与运行记录,没有 Meldwork 托管的会话服务。所选 Agent 或连接器可能向配置的服务商发送提示词、附件或技能内容,其数据处理与网络访问遵循各自配置及条款。"
|
||||
>
|
||||
No. Meldwork stores its conversations and run records locally
|
||||
and has no Meldwork-hosted conversation service. Selected Agents
|
||||
or connectors may send prompts, attachments, or Skills to their
|
||||
configured providers. Their data handling and network access
|
||||
follow their own configuration and terms.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="Can Agents change my files?"
|
||||
data-zh="Agent 能修改我的文件吗?"
|
||||
>
|
||||
Can Agents change my files?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-en="Workspace writes are opt-in. Set the working directory and permissions before a run, and respond to any Human Gate that requires your input. These are workflow controls, not an operating-system sandbox; enforcement also depends on the selected CLI."
|
||||
data-zh="工作区写入需主动启用。运行前请设置工作目录与权限,并处理需要你输入的人工审批门。这些是工作流控制,并非操作系统级沙箱,执行约束也取决于所选 CLI。"
|
||||
>
|
||||
Workspace writes are opt-in. Set the working directory and
|
||||
permissions before a run, and respond to any Human Gate that
|
||||
requires your input. These are workflow controls, not an
|
||||
operating-system sandbox; enforcement also depends on the
|
||||
selected CLI.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="faq-item">
|
||||
<summary
|
||||
class="faq-question"
|
||||
data-en="Which platforms and license does the preview support?"
|
||||
data-zh="预览版支持哪些平台,使用什么许可证?"
|
||||
>
|
||||
Which platforms and license does the preview support?
|
||||
</summary>
|
||||
<div class="faq-answer">
|
||||
<p
|
||||
data-en="V1.0.4 is an Apple silicon macOS preview, ad-hoc signed and not notarized. Other platforms are not supported packaged targets in this preview. Meldwork uses Apache License 2.0, which permits commercial use under its terms. Agent CLIs, providers, and third-party assets may have separate terms."
|
||||
data-zh="V1.0.4 是 Apple 芯片 macOS 预览版,使用临时签名且尚未公证;此预览版未提供其他平台的受支持安装包。Meldwork 使用 Apache License 2.0,按其条款允许商业使用。Agent CLI、服务商与第三方素材可能有各自条款。"
|
||||
>
|
||||
V1.0.4 is an Apple silicon macOS preview, ad-hoc signed and not
|
||||
notarized. Other platforms are not supported packaged targets in
|
||||
this preview. Meldwork uses Apache License 2.0, which permits
|
||||
commercial use under its terms. Agent CLIs, providers, and
|
||||
third-party assets may have separate terms.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= VELOCITY MARQUEE ================= -->
|
||||
<div class="velocity" data-velocity aria-hidden="true">
|
||||
@@ -658,24 +838,99 @@
|
||||
</div>
|
||||
|
||||
<!-- ================= CTA / FOOTER ================= -->
|
||||
<footer class="cta-final" id="download">
|
||||
<h2 class="final-title" data-split>Put your agents on the record.</h2>
|
||||
<p class="final-sub">Download the Apple silicon macOS preview, or read the architecture first.</p>
|
||||
<div class="cta-row">
|
||||
<a class="cta-primary magnet" href="https://github.com/Ryder-MHumble/Meldwork/releases/download/Meldwork-V1.0.3/Meldwork-0.1.3-arm64.dmg">
|
||||
<span class="cta-label">Download V1.0.3</span>
|
||||
<span class="cta-meta">Apple silicon · 78 MB</span>
|
||||
</a>
|
||||
<a class="cta-ghost" href="https://github.com/Ryder-MHumble/Meldwork#readme" target="_blank" rel="noopener">Read the README <span aria-hidden="true">↗</span></a>
|
||||
</div>
|
||||
<div class="foot-meta">
|
||||
<img class="foot-wordmark" src="assets/meldwork-wordmark-v3-dark.svg" alt="Meldwork" data-theme-src="assets/meldwork-wordmark-v3.svg" />
|
||||
<span>Source-available · Meldwork Non-Commercial Source License 1.0</span>
|
||||
<span>Local-first · Electron · macOS preview</span>
|
||||
<a class="foot-link" href="https://github.com/Ryder-MHumble/Meldwork" target="_blank" rel="noopener">github.com/Ryder-MHumble/Meldwork</a>
|
||||
</div>
|
||||
</footer>
|
||||
<section class="cta-final" id="download" aria-labelledby="download-title">
|
||||
<h2
|
||||
class="final-title"
|
||||
id="download-title"
|
||||
data-en="Bring your next question to Meldwork."
|
||||
data-zh="把下一个问题带到 Meldwork。"
|
||||
>
|
||||
Bring your next question to Meldwork.
|
||||
</h2>
|
||||
<p
|
||||
class="final-sub"
|
||||
data-en="Start with the Agents you already use. Choose a task, compare their perspectives, and decide your next step."
|
||||
data-zh="从你已经在用的 Agent 开始,选择任务,比较不同判断,再决定下一步。"
|
||||
>
|
||||
Start with the Agents you already use. Choose a task, compare their
|
||||
perspectives, and decide your next step.
|
||||
</p>
|
||||
<div class="cta-row">
|
||||
<a
|
||||
class="cta-primary"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork/releases/download/Meldwork-V1.0.4/Meldwork-0.1.4-arm64.dmg"
|
||||
><span
|
||||
class="cta-label"
|
||||
data-en="Download for macOS"
|
||||
data-zh="下载 macOS 版"
|
||||
>Download for macOS</span
|
||||
><span
|
||||
class="cta-meta"
|
||||
data-en="V1.0.4 preview · Apple silicon"
|
||||
data-zh="V1.0.4 预览版 · Apple 芯片"
|
||||
>V1.0.4 preview · Apple silicon</span
|
||||
></a
|
||||
><a
|
||||
class="cta-ghost"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork/releases/tag/Meldwork-V1.0.4"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-en="Release notes"
|
||||
data-zh="发行说明"
|
||||
>Release notes</a
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
class="download-note"
|
||||
data-en="Ad-hoc signed and not notarized. On first launch, macOS may require Open Anyway in System Settings > Privacy & Security."
|
||||
data-zh="使用临时签名,尚未公证。首次打开时,macOS 可能需要在「系统设置 > 隐私与安全性」中选择「仍要打开」。"
|
||||
>
|
||||
Ad-hoc signed and not notarized. On first launch, macOS may require
|
||||
Open Anyway in System Settings > Privacy & Security.
|
||||
</p>
|
||||
</section>
|
||||
<footer class="foot-meta">
|
||||
<a
|
||||
href="#top"
|
||||
aria-label="Meldwork home"
|
||||
data-en-aria-label="Meldwork home"
|
||||
data-zh-aria-label="Meldwork 首页"
|
||||
><img
|
||||
class="foot-wordmark"
|
||||
src="assets/meldwork-wordmark-v3-dark.svg"
|
||||
alt="Meldwork"
|
||||
data-theme-src="assets/meldwork-wordmark-v3.svg"
|
||||
/></a>
|
||||
<a
|
||||
class="foot-link"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-en="Apache License 2.0"
|
||||
data-zh="Apache 2.0 许可证"
|
||||
>Apache License 2.0</a
|
||||
>
|
||||
<a
|
||||
class="foot-link"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork/blob/main/architecture.md"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-en="Architecture"
|
||||
data-zh="产品架构"
|
||||
>Architecture</a
|
||||
>
|
||||
<a
|
||||
class="foot-link"
|
||||
href="https://github.com/Ryder-MHumble/Meldwork"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>GitHub</a
|
||||
>
|
||||
</footer>
|
||||
|
||||
<script src="locale.js" defer></script>
|
||||
<script src="main.js" defer></script>
|
||||
<script src="chapter-scene.js" defer></script>
|
||||
<script src="run-demo.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -0,0 +1,258 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var doc = document;
|
||||
var url = new URL(window.location.href);
|
||||
var language = url.searchParams.get("lang") === "zh" ? "zh" : "en";
|
||||
var copy = {
|
||||
"Workflow": "工作流程",
|
||||
"Modes": "协作模式",
|
||||
"Capabilities": "核心能力",
|
||||
"In a run": "运行实例",
|
||||
"How it differs": "产品差异",
|
||||
"FAQ": "常见问题",
|
||||
"Download V1.0.4": "下载 V1.0.4",
|
||||
"LOCAL-FIRST · MULTI-AGENT WORK CELL": "本地优先 · 多 Agent 协作空间",
|
||||
"Multi-Agent Work,": "多 Agent 协作,",
|
||||
"Legible & Accountable": "过程清晰,责任可循",
|
||||
"Meldwork builds the": "Meldwork 为 AI Agent 构建",
|
||||
"organization layer": "协作组织层",
|
||||
"for AI agents — a local work cell that keeps participants explicit, context scoped, run state inspectable, and human review in the loop.": ":在本地工作空间中明确参与者、限定上下文、查看运行状态,并由你参与审核。",
|
||||
"Download for macOS": "下载 macOS 版",
|
||||
"V1.0.4 · Apple silicon": "V1.0.4 · Apple 芯片",
|
||||
"View on GitHub": "在 GitHub 查看",
|
||||
"One task — across the Agent CLIs you already run.": "一个任务,连接你已经在用的 Agent CLI。",
|
||||
"Supported Agent CLIs": "支持的 Agent CLI",
|
||||
"Collaboration Modes": "协作模式",
|
||||
"Local-first work cell": "本地优先的协作空间",
|
||||
"Human adoption gate": "人工确认采纳",
|
||||
"DETECTED ON YOUR MACHINE": "发现本机 AGENT",
|
||||
"Works with the agents you already run.": "连接你已经在用的 Agent。",
|
||||
"Meldwork detects an installed command when its adapter and the CLI version are compatible. Approved Agent Connectors can be added through the Agent Connector SDK.": "适配器与 CLI 版本兼容时,Meldwork 可检测本机已安装的命令行工具。也可通过 Agent Connector SDK 添加获准使用的连接器。",
|
||||
"THE SHORT ANSWER": "关于 MELDWORK",
|
||||
"What is Meldwork?": "Meldwork 是什么?",
|
||||
"Meldwork is a": "Meldwork 是一个",
|
||||
"local-first work cell for AI coding agents": "本地优先的 AI 编程 Agent 协作空间",
|
||||
"— it orchestrates the agent CLIs you already run around one frozen task snapshot, records every finding as evidence, and puts a": ",围绕同一份冻结的任务快照组织你已有的 Agent CLI,保留发现与证据,并在工作区发生变更前设置",
|
||||
"human adoption gate": "人工采纳确认",
|
||||
"before any workspace change.": "。",
|
||||
"Supported agents": "支持的 Agent",
|
||||
"Codex, Claude Code, Gemini CLI, Qwen Code, Kimi Code, MiMo Code, OpenCode, OpenCodeReview, Hermes, Pi Agent, OpenClaw, WorkBuddy — detected natively on your machine.": "Codex、Claude Code、Gemini CLI、Qwen Code、Kimi Code、MiMo Code、OpenCode、OpenCodeReview、Hermes、Pi Agent、OpenClaw、WorkBuddy,直接检测本机安装。",
|
||||
"Direct · Concurrent Responses · Auto Discussion V4": "直接会话 · 并发回复 · 自动讨论 V4",
|
||||
"Review record": "复核记录",
|
||||
"Finding": "发现",
|
||||
"Evidence": "证据",
|
||||
"Decision": "决策",
|
||||
"Disposition": "处置",
|
||||
"Attached to every result — dissent stays visible, never hidden behind a manager.": "每个结果都关联记录,保留不同意见,不让汇总掩盖分歧。",
|
||||
"Runs on": "运行平台",
|
||||
"macOS · Apple silicon · Electron": "macOS · Apple 芯片 · Electron",
|
||||
"100% local-first work cell": "100% 本地优先的协作空间",
|
||||
"THE WORK CELL": "从任务到结果",
|
||||
"Four steps from task to adopted result.": "四步完成任务,确认采纳结果。",
|
||||
"Select": "选择参与者",
|
||||
"Choose the local Agents and participants. Participants are always selected by you — never auto-assigned from a roster.": "选择本地 Agent 与参与者。由你决定谁参与,不从名单中自动分配。",
|
||||
"Scope": "限定范围",
|
||||
"Bound the goal, working directory, context, and permissions. Every run starts from a frozen task snapshot.": "明确目标、工作目录、上下文和权限。每次运行都从冻结的任务快照开始。",
|
||||
"Run": "开始协作",
|
||||
"Direct, Concurrent Responses, or Auto Discussion V4 — from a single focused session to negotiated multi-round work.": "选择直接会话、并发回复或自动讨论 V4,从单个 Agent 的专注工作到多轮协商。",
|
||||
"Review & adopt": "复核与采纳",
|
||||
"Inspect findings, evidence, and any Human Gate before the workspace changes. Nothing writes itself.": "工作区变更前,查看发现、证据与人工审批请求。写入由你确认。",
|
||||
"COLLABORATION MODES": "协作模式",
|
||||
"Three ways agents work together.": "三种模式,匹配不同协作深度。",
|
||||
"Same Case, same evidence trail — different depth of agreement. Pick the mode that matches how much review the decision deserves.": "同一个任务、同一条证据链,可以有不同深度的讨论。根据决策需要的复核程度选择模式。",
|
||||
"Direct": "直接会话",
|
||||
"One selected Agent keeps its conversation and native session when supported.": "与一个选定的 Agent 对话,支持时延续其原生会话。",
|
||||
"Best for focused work with one Agent.": "适合与单个 Agent 专注完成任务。",
|
||||
"Concurrent Responses": "并发回复",
|
||||
"Selected Agents receive the same frozen task snapshot and return independent replies in stable order.": "所选 Agent 接收同一份冻结任务快照,独立回复,并按固定顺序呈现。",
|
||||
"Best for comparing approaches before choosing one.": "适合先比较不同方案,再做选择。",
|
||||
"Auto Discussion V4": "自动讨论 V4",
|
||||
"Agents propose, challenge, negotiate responsibilities, execute dependency-aware work, then verify the result.": "Agent 提出方案、相互质询、协商分工,按依赖关系执行,再验证结果。",
|
||||
"Best for multi-round work with explicit responsibility.": "适合分工明确、需要多轮讨论的任务。",
|
||||
"WHAT YOU CONTROL": "协作由你掌控",
|
||||
"Four guarantees that hold the work together.": "四项核心保障,让协作有据可查。",
|
||||
"These four invariants are what make multi-agent output reviewable instead of just decorative.": "从权限到证据,为多 Agent 的每次输出保留可复核的依据。",
|
||||
"Human Gate": "人工审批",
|
||||
"Workspace writes are opt-in. Nothing changes until": "工作区写入需主动启用,由",
|
||||
"you": "你",
|
||||
"adopt the result — agents don't.": "采纳结果后再变更,不由 Agent 自行决定。",
|
||||
"Opt-in workflow controls": "主动授权的工作流控制",
|
||||
"Frozen Context": "冻结上下文",
|
||||
"Every run starts from a snapshot. All selected Agents see the same brief, in stable order.": "每次运行从快照开始。所选 Agent 获得同一份任务说明,顺序保持一致。",
|
||||
"Case-scoped, reproducible": "任务范围明确,可复现",
|
||||
"Evidence Trail": "证据链",
|
||||
"Finding → Evidence → Decision → Disposition, attached to each result. Dissent stays visible.": "发现 → 证据 → 决策 → 处置,与每个结果关联,不同意见始终可见。",
|
||||
"No hidden manager": "不让汇总掩盖分歧",
|
||||
"Local-first Boundary": "本地优先的边界",
|
||||
"Electron work cell on your machine. No remote Agent fleet. Conversations stay local.": "Electron 工作空间运行在你的电脑上。没有远程 Agent 集群,会话保存在本地。",
|
||||
"Your CLI, your data": "你的 CLI,你的数据",
|
||||
"INSIDE THE WORK CELL": "走进工作空间",
|
||||
"Inspectable from discovery to adoption.": "从发现 Agent 到采纳结果,全程可查。",
|
||||
"Local Agent discovery": "发现本地 Agent",
|
||||
"Pick the participants before any work runs. Meldwork detects installed CLIs and shows their adapter status.": "运行前选择参与者。Meldwork 检测已安装的 CLI,并显示适配器状态。",
|
||||
"Multi-agent review": "多 Agent 复核",
|
||||
"Findings, evidence, and adoption decisions live in one Case. Dissent stays visible — never hidden behind a manager.": "发现、证据与采纳决策归入同一个任务。保留不同意见,不让汇总掩盖分歧。",
|
||||
"Propose, negotiate, verify — across rounds. Every Agent's position is preserved for later review.": "跨轮次提出方案、协商、验证。每个 Agent 的立场都被保留,供后续复核。",
|
||||
"Direct mode": "直接会话",
|
||||
"One Agent, its native session preserved. Focused work with the full evidence trail still attached.": "一个 Agent,延续原生会话。专注处理任务,同时保留完整证据链。",
|
||||
"Example: give Codex, Claude Code, and Gemini CLI the same change-review context, compare their independent findings, then adopt only the evidence-backed result you approve.": "例如:将同一份变更复核上下文交给 Codex、Claude Code 和 Gemini CLI,比较各自的独立发现,只采纳你认可且有证据支持的结果。",
|
||||
"HOW MELDWORK DIFFERS": "MELDWORK 的不同",
|
||||
"An organization layer, not another runner.": "组织协作,不止运行命令。",
|
||||
"Meldwork connects the local Agent tools you already use to a decision-ready review workflow — and keeps independent findings, evidence, responsibility, and the human adoption decision visible in one local work cell.": "Meldwork 将你已有的本地 Agent 工具接入面向决策的复核流程,在同一工作空间中保留独立发现、证据、责任与人工采纳决策。",
|
||||
"Finding → Evidence → Decision → Disposition": "发现 → 证据 → 决策 → 处置",
|
||||
"Dissent stays visible. Disagreement, re-checks, and adoption records are tracked — never hidden behind a central manager.": "不同意见始终可见。分歧、复查与采纳都有记录,不被统一汇总掩盖。",
|
||||
"Human Gate before workspace writes": "写入工作区前,由人确认",
|
||||
"Workspace writes are opt-in workflow controls. You adopt the result — the agents don't.": "工作区写入由你主动授权。是否采纳结果,由你决定,不由 Agent 代替。",
|
||||
"Local-first boundaries": "本地优先,边界明确",
|
||||
"Runs in your Electron work cell with existing CLIs. No cloud Agent fleet, no required server, conversations stay local.": "在本地 Electron 工作空间中调用已有 CLI。没有云端 Agent 集群,无需必备服务器,会话保存在本地。",
|
||||
"Frozen context, independent judgment": "冻结上下文,独立做判断",
|
||||
"Case-scoped independent judgments with evidence-backed decisions — not terminal count or worktree throughput.": "围绕任务形成独立判断,以证据支持决策,而不是追求终端数量或工作树吞吐量。",
|
||||
"WHERE MELDWORK FITS": "产品定位",
|
||||
"The categories you usually compare against.": "与常见 Agent 工具有什么不同?",
|
||||
"A condensed map of how Meldwork positions itself next to terminal runners, cloud fleets, communication networks, and framework-based orchestrators. Full per-project comparison lives in the": "将 Meldwork 与终端运行器、云端集群、通信网络和编排框架放在一起比较。具体项目的完整对比见",
|
||||
"README ↗": "项目说明 ↗",
|
||||
"Category": "工具类别",
|
||||
"What it focuses on": "主要关注",
|
||||
"Meldwork's difference": "Meldwork 的侧重",
|
||||
"Terminal / IDE runners": "终端 / IDE 运行器",
|
||||
"Run a command, view output, navigate worktrees.": "运行命令、查看输出、切换工作树。",
|
||||
"Adds evidence-backed review and a human adoption gate on top of the same CLIs.": "在相同 CLI 之上,加入证据驱动的复核与人工采纳确认。",
|
||||
"Cloud Agent fleets": "云端 Agent 集群",
|
||||
"Hosted sandboxes, remote execution, multi-tenant routing.": "托管沙箱、远程执行、多租户路由。",
|
||||
"Local Electron work cell using the CLIs already installed on your machine.": "本地 Electron 工作空间,使用电脑上已安装的 CLI。",
|
||||
"Agent communication networks": "Agent 通信网络",
|
||||
"Identity, channels, events, persistent collaboration.": "身份、频道、事件与持久协作。",
|
||||
"Case-scoped independent judgments, each attached to evidence and a Decision / Disposition.": "任务范围内的独立判断,每项都关联证据、决策与处置记录。",
|
||||
"Parallel coding workspaces": "并行编码工作空间",
|
||||
"Multiple worktrees, parallel coding Agents, merge queues.": "多工作树、并行编码 Agent、合并队列。",
|
||||
"Review across heterogeneous CLIs — not worktree throughput.": "跨不同 CLI 复核,而非追求工作树吞吐量。",
|
||||
"Multi-Agent frameworks": "多 Agent 框架",
|
||||
"Graphs, routing, approvals, and DSLs you build yourself.": "自行构建执行图、路由、审批和领域语言。",
|
||||
"A ready-to-use review workflow — no orchestration framework to assemble.": "开箱即用的复核流程,无需自行组装编排框架。",
|
||||
"LEGIBLE": "清晰",
|
||||
"ACCOUNTABLE": "可追溯",
|
||||
"REUSABLE": "可复用",
|
||||
"Meldwork home": "Meldwork 首页",
|
||||
"Primary": "主导航",
|
||||
"Mobile": "移动端导航",
|
||||
"Toggle theme": "切换明暗主题",
|
||||
"Menu": "菜单",
|
||||
"Scroll to workflow": "查看工作流程",
|
||||
"Playback mode": "播放模式",
|
||||
"Previous step": "上一步",
|
||||
"Next step": "下一步",
|
||||
"Pause background animation": "暂停背景动画",
|
||||
"Resume background animation": "继续背景动画",
|
||||
"Meldwork detecting local Agent CLIs": "Meldwork 检测本地 Agent CLI",
|
||||
"Multi-agent review with evidence and human review": "包含证据与人工审核的多 Agent 复核",
|
||||
"Auto Discussion V4 group collaboration": "自动讨论 V4 群组协作",
|
||||
"Direct multimodal session with local files": "使用本地文件的多模态直接会话",
|
||||
"Meldwork · The organization layer for AI agents": "Meldwork · AI Agent 的协作组织层",
|
||||
"Meldwork is a local-first work cell for AI coding agents — orchestrate Codex, Claude Code, Gemini CLI and 9 more agent CLIs around one frozen task snapshot, with evidence trails and a human adoption gate before any workspace change.": "Meldwork 是本地优先的 AI 编程 Agent 协作空间。围绕同一份冻结任务快照组织 Codex、Claude Code、Gemini CLI 等 12 个 Agent CLI,保留证据链,并在工作区变更前由人确认采纳。",
|
||||
"A local-first work cell for AI coding agents. Orchestrate the agent CLIs you already run, keep every finding as evidence, and gate workspace writes behind human adoption.": "本地优先的 AI 编程 Agent 协作空间。连接你已有的 Agent CLI,保留发现与证据,工作区写入由人确认采纳。",
|
||||
"Meldwork is a local-first work cell for AI coding agents. It orchestrates agent CLIs such as Codex, Claude Code, and Gemini CLI around one frozen task snapshot, records every finding as evidence, and puts a human adoption gate before any workspace change.": "Meldwork 是本地优先的 AI 编程 Agent 协作空间。围绕同一份冻结任务快照组织 Codex、Claude Code、Gemini CLI 等工具,记录发现与证据,并在工作区变更前设置人工采纳确认。",
|
||||
"Local Agent discovery, Multi-agent review, Auto Discussion V4, Direct mode, Human Gate, Evidence Trail, Frozen Context": "发现本地 Agent、多 Agent 复核、自动讨论 V4、直接会话、人工审批、证据链、冻结上下文"
|
||||
};
|
||||
|
||||
function normalize(value) {
|
||||
return value.trim().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function t(english) {
|
||||
var key = normalize(english);
|
||||
return language === "zh" && Object.prototype.hasOwnProperty.call(copy, key) ? copy[key] : english;
|
||||
}
|
||||
|
||||
var bilingual = Array.from(doc.querySelectorAll("[data-en][data-zh]"));
|
||||
bilingual.forEach(function (element) { copy[normalize(element.dataset.en)] = element.dataset.zh; });
|
||||
var texts = [];
|
||||
var walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
||||
var node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.parentElement.closest("script, style, [data-zh], #localeToggle, [data-split]")) continue;
|
||||
texts.push({ node: node, english: node.nodeValue });
|
||||
}
|
||||
var splitLines = Array.from(doc.querySelectorAll(".hl-line, [data-split-line]"), function (element) {
|
||||
return { element: element, english: element.textContent };
|
||||
});
|
||||
var attributes = [];
|
||||
doc.querySelectorAll("[aria-label], [alt], [data-text]").forEach(function (element) {
|
||||
["aria-label", "alt", "data-text"].forEach(function (name) {
|
||||
if (element.hasAttribute(name)) attributes.push({ element: element, name: name, english: element.getAttribute(name) });
|
||||
});
|
||||
});
|
||||
var title = doc.title;
|
||||
var metadata = Array.from(doc.querySelectorAll('meta[name="description"], meta[property="og:title"], meta[property="og:description"]'),
|
||||
function (element) { return { element: element, english: element.content }; });
|
||||
var schemas = Array.from(doc.querySelectorAll('script[type="application/ld+json"]'),
|
||||
function (element) { return { element: element, english: element.textContent }; });
|
||||
var toggle = doc.getElementById("localeToggle");
|
||||
|
||||
function applyLanguage(next) {
|
||||
language = next;
|
||||
window.MeldworkLocale.language = language;
|
||||
doc.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
||||
bilingual.forEach(function (element) { element.textContent = language === "zh" ? element.dataset.zh : element.dataset.en; });
|
||||
texts.forEach(function (record) {
|
||||
record.node.nodeValue = record.english.match(/^\s*/)[0] + t(normalize(record.english)) + record.english.match(/\s*$/)[0];
|
||||
});
|
||||
splitLines.forEach(function (record) { record.element.textContent = t(record.english); });
|
||||
attributes.forEach(function (record) {
|
||||
record.element.setAttribute(record.name, language === "zh" ?
|
||||
record.element.getAttribute("data-zh-" + record.name) || t(record.english) : record.english);
|
||||
});
|
||||
doc.querySelectorAll("[data-decrypt]").forEach(function (element) {
|
||||
element.textContent = element.dataset.text;
|
||||
});
|
||||
doc.title = t(title);
|
||||
metadata.forEach(function (record) { record.element.content = t(record.english); });
|
||||
schemas.forEach(function (record) {
|
||||
var schema = JSON.parse(record.english);
|
||||
(schema["@graph"] || []).forEach(function (entity) {
|
||||
if (entity["@type"] === "SoftwareApplication") {
|
||||
entity.description = t(entity.description);
|
||||
entity.featureList = t(entity.featureList);
|
||||
}
|
||||
if (entity["@type"] === "FAQPage") {
|
||||
entity.mainEntity = Array.from(doc.querySelectorAll(".faq-item"), function (item) {
|
||||
return { "@type": "Question", name: normalize(item.querySelector(".faq-question").textContent),
|
||||
acceptedAnswer: { "@type": "Answer", text: normalize(item.querySelector(".faq-answer").textContent) } };
|
||||
});
|
||||
}
|
||||
});
|
||||
record.element.textContent = JSON.stringify(schema);
|
||||
});
|
||||
if (toggle) {
|
||||
var alternate = new URL(window.location.href);
|
||||
alternate.searchParams.set("lang", language === "zh" ? "en" : "zh");
|
||||
toggle.textContent = language === "zh" ? "EN" : "中";
|
||||
toggle.href = alternate.href;
|
||||
toggle.setAttribute("aria-label", language === "zh" ? "Switch to English" : "切换为中文");
|
||||
toggle.setAttribute("lang", language === "zh" ? "en" : "zh-CN");
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent("meldwork:languagechange"));
|
||||
}
|
||||
window.MeldworkLocale = { language: language, t: t };
|
||||
applyLanguage(language);
|
||||
if (toggle) toggle.addEventListener("click", function (event) {
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
// Preserve the reading anchor when translated paragraphs change height.
|
||||
var anchor = Array.from(doc.querySelectorAll("section[id] h2, section[id] p, .faq-question"))
|
||||
.find(function (element) {
|
||||
var rect = element.getBoundingClientRect();
|
||||
return rect.top >= 100 && rect.top < window.innerHeight && rect.height > 0;
|
||||
});
|
||||
var offset = anchor ? anchor.getBoundingClientRect().top : 0;
|
||||
var scroll = window.scrollY;
|
||||
var next = language === "zh" ? "en" : "zh";
|
||||
var currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.set("lang", next);
|
||||
// file:// previews do not consistently allow history mutation in every browser.
|
||||
try { window.history.replaceState(null, "", currentUrl.href); } catch (error) {}
|
||||
applyLanguage(next);
|
||||
var delta = anchor ? anchor.getBoundingClientRect().top - offset : 0;
|
||||
window.scrollTo({ top: anchor ? window.scrollY + delta : scroll, behavior: "instant" });
|
||||
});
|
||||
})();
|
||||
+49
-109
@@ -10,7 +10,6 @@
|
||||
· ScrollVelocity (scroll-aware marquee speed)
|
||||
· Magnet (mouse-follow on CTAs)
|
||||
· GlareHover (CSS-driven shine)
|
||||
· Stepper (scenario timeline)
|
||||
· Accordion (FAQ)
|
||||
· ScrollProgress + ScrollSpy
|
||||
· Mobile menu + Escape + a11y
|
||||
@@ -109,6 +108,11 @@
|
||||
var duration = 900;
|
||||
var start = performance.now();
|
||||
function frame(now) {
|
||||
if (el.dataset.text && el.dataset.text !== target) {
|
||||
el.textContent = el.dataset.text;
|
||||
el.classList.remove("is-scrambling");
|
||||
return;
|
||||
}
|
||||
var p = Math.min((now - start) / duration, 1);
|
||||
var revealCount = Math.floor(target.length * p);
|
||||
var out = "";
|
||||
@@ -305,116 +309,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
9. SCENARIO STEPPER — auto-cycle the timeline
|
||||
========================================================= */
|
||||
var scenario = doc.querySelector("[data-scenario]");
|
||||
if (scenario) {
|
||||
var steps = scenario.querySelectorAll(".t-step");
|
||||
var total = steps.length;
|
||||
var current = 0;
|
||||
var isInView = false;
|
||||
var autoTimer = null;
|
||||
var autoDelay = 1600;
|
||||
var scnMode = "auto"; // "auto" = step-by-step playback, "order" = full transcript
|
||||
var railFill = scenario.querySelector("[data-rail]");
|
||||
var railStatus = scenario.querySelector("[data-status]");
|
||||
var roundLabel = scenario.querySelector("[data-round]");
|
||||
var roundNames = [
|
||||
"Round 1 · Propose",
|
||||
"Round 1 · Critique",
|
||||
"Round 2 · Propose",
|
||||
"Round 2 · Reconcile",
|
||||
"Adoption · Human Gate"
|
||||
];
|
||||
|
||||
function setStep(idx, revealAll) {
|
||||
current = (idx + total) % total;
|
||||
steps.forEach(function (s, i) {
|
||||
if (revealAll || i <= current) s.classList.add("is-visible");
|
||||
else s.classList.remove("is-visible");
|
||||
s.classList.toggle("is-active", i === current);
|
||||
});
|
||||
if (railFill) railFill.style.height = (((current + 1) / total) * 100) + "%";
|
||||
if (railStatus) railStatus.textContent = (current + 1) + " / " + total;
|
||||
if (roundLabel) roundLabel.textContent = roundNames[current];
|
||||
/* FAQ uses native details semantics, with interruptible open/close animation. */
|
||||
var faqAnimations = new Map();
|
||||
function expandFaq(item, open) {
|
||||
var previous = faqAnimations.get(item);
|
||||
var startHeight = item.getBoundingClientRect().height;
|
||||
if (previous) previous.cancel();
|
||||
item.dataset.expanded = String(open);
|
||||
item.querySelector("summary").setAttribute("aria-expanded", String(open));
|
||||
item.open = true;
|
||||
var endHeight = item.querySelector("summary").getBoundingClientRect().height +
|
||||
(open ? item.querySelector(".faq-answer").getBoundingClientRect().height : 0);
|
||||
function finish() {
|
||||
item.open = open;
|
||||
item.style.height = "";
|
||||
faqAnimations.delete(item);
|
||||
}
|
||||
function startAuto() {
|
||||
stopAuto();
|
||||
autoTimer = setInterval(function () {
|
||||
setStep(current + 1);
|
||||
}, autoDelay);
|
||||
}
|
||||
function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
|
||||
|
||||
scenario.querySelector("[data-prev]").addEventListener("click", function () {
|
||||
setStep(current - 1); startAuto();
|
||||
});
|
||||
scenario.querySelector("[data-next]").addEventListener("click", function () {
|
||||
setStep(current + 1); startAuto();
|
||||
});
|
||||
scenario.addEventListener("mouseenter", stopAuto);
|
||||
scenario.addEventListener("mouseleave", function () {
|
||||
if (isInView && scnMode === "auto") startAuto();
|
||||
});
|
||||
|
||||
// macOS segmented control — Auto vs Sequence
|
||||
var segBtns = scenario.querySelectorAll(".seg-btn");
|
||||
function setMode(mode) {
|
||||
scnMode = mode;
|
||||
scenario.classList.toggle("is-order", mode === "order");
|
||||
segBtns.forEach(function (b) {
|
||||
b.classList.toggle("is-on", b.dataset.mode === mode);
|
||||
});
|
||||
if (mode === "order") {
|
||||
stopAuto();
|
||||
setStep(total - 1, true); // full transcript, rail at 100%, gate highlighted
|
||||
} else {
|
||||
setStep(0);
|
||||
if (isInView && !reduced) startAuto();
|
||||
}
|
||||
}
|
||||
segBtns.forEach(function (b) {
|
||||
b.addEventListener("click", function () { setMode(b.dataset.mode); });
|
||||
});
|
||||
|
||||
if ("IntersectionObserver" in win) {
|
||||
var scnObs = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
isInView = entry.isIntersecting;
|
||||
if (isInView) {
|
||||
if (scnMode === "auto") {
|
||||
setStep(0);
|
||||
if (!reduced) startAuto();
|
||||
} else {
|
||||
setStep(total - 1, true);
|
||||
}
|
||||
} else stopAuto();
|
||||
});
|
||||
}, { threshold: 0.25 });
|
||||
scnObs.observe(scenario);
|
||||
} else setStep(0);
|
||||
if (win.matchMedia("(prefers-reduced-motion: reduce)").matches || !item.animate) { finish(); return; }
|
||||
var animation = item.animate({ height: [startHeight + "px", endHeight + "px"] },
|
||||
{ duration: 280, easing: "cubic-bezier(0.2, 0.7, 0.2, 1)" });
|
||||
faqAnimations.set(item, animation);
|
||||
animation.onfinish = finish;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
10. FAQ ACCORDION
|
||||
========================================================= */
|
||||
doc.querySelectorAll(".acc-item").forEach(function (item) {
|
||||
var trigger = item.querySelector(".acc-trigger");
|
||||
var panel = item.querySelector(".acc-panel");
|
||||
trigger.addEventListener("click", function () {
|
||||
var isOpen = item.classList.contains("is-open");
|
||||
// close siblings
|
||||
doc.querySelectorAll(".acc-item.is-open").forEach(function (o) {
|
||||
if (o !== item) {
|
||||
o.classList.remove("is-open");
|
||||
o.querySelector(".acc-trigger").setAttribute("aria-expanded", "false");
|
||||
o.querySelector(".acc-panel").style.maxHeight = "";
|
||||
}
|
||||
doc.querySelectorAll(".faq-item").forEach(function (item, index) {
|
||||
item.open = index === 0;
|
||||
item.dataset.expanded = String(item.open);
|
||||
item.querySelector("summary").setAttribute("aria-expanded", String(item.open));
|
||||
item.querySelector("summary").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
var open = item.dataset.expanded !== "true";
|
||||
doc.querySelectorAll('.faq-item[data-expanded="true"]').forEach(function (other) {
|
||||
if (other !== item) expandFaq(other, false);
|
||||
});
|
||||
item.classList.toggle("is-open", !isOpen);
|
||||
trigger.setAttribute("aria-expanded", String(!isOpen));
|
||||
panel.style.maxHeight = !isOpen ? panel.scrollHeight + "px" : "";
|
||||
expandFaq(item, open);
|
||||
});
|
||||
});
|
||||
win.addEventListener("meldwork:languagechange", function () {
|
||||
faqAnimations.forEach(function (animation, item) {
|
||||
animation.cancel();
|
||||
item.open = item.dataset.expanded === "true";
|
||||
item.style.height = "";
|
||||
});
|
||||
faqAnimations.clear();
|
||||
doc.querySelectorAll("[data-split]").forEach(function (el) {
|
||||
delete el.dataset.splitDone;
|
||||
splitText(el);
|
||||
el.classList.add("play");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,7 +364,7 @@
|
||||
var nav = doc.getElementById("topnav");
|
||||
function setNavScrolled() {
|
||||
if (!nav) return;
|
||||
if (win.scrollY > 8) nav.classList.add("scrolled");
|
||||
if (win.scrollY > 64) nav.classList.add("scrolled");
|
||||
else nav.classList.remove("scrolled");
|
||||
}
|
||||
setNavScrolled();
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
var root = document.querySelector("[data-run-demo]");
|
||||
if (!root) return;
|
||||
var tabs = Array.from(root.querySelectorAll("[data-run-mode]"));
|
||||
var feed = root.querySelector(".run-chat");
|
||||
var list = root.querySelector("[data-run-messages]");
|
||||
var renderedKey = "", renderedCount = 0;
|
||||
var pause = root.querySelector("[data-run-pause]");
|
||||
var motion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
var mode = "auto", phase = 0, timer = 0, visible = false, paused = false;
|
||||
var descriptions = {
|
||||
direct: ["Work with one selected Agent. Follow up in the same conversation; you decide the next question.", "与一个选定的 Agent 持续对话,由你提问、追问并决定下一步。"],
|
||||
concurrent: ["Send the same task to your selected Agents at once. Each replies independently, so you can compare their approaches.", "把同一个任务同时发给所选 Agent。它们各自独立回复,由你比较不同方案。"],
|
||||
auto: ["Agents start with independent ideas, then build on the discussion and @mention peers to invite the next reply. You choose who participates.", "Agent 先独立提出想法,再自主接续讨论,通过 @ 邀请其他 Agent 回应。参与者由你选择。"]
|
||||
};
|
||||
var task = ["You", ["Review this product launch plan.", "评审这份产品发布方案。"]];
|
||||
var conversations = {
|
||||
direct: [
|
||||
[task],
|
||||
[["Codex", null]],
|
||||
[["Codex", ["Start with a small pilot. Test onboarding and define a rollback plan before expanding.", "建议先小范围试用,验证引导流程,并在扩大发布前明确回退方案。"]]],
|
||||
[["You", ["What should we measure in the pilot?", "试用阶段应该关注哪些指标?"]]],
|
||||
[["Codex", ["Track first-task completion, repeat use, and unresolved issues. Review the results after one week.", "关注首次任务完成率、重复使用和未解决问题,一周后集中复盘。"]]]
|
||||
],
|
||||
concurrent: [
|
||||
[task],
|
||||
[["Codex", null], ["Claude", null], ["Gemini", null]],
|
||||
[["Codex", ["Check installation, error recovery, and the rollback path before launch.", "发布前检查安装流程、异常恢复和回退路径。"]],
|
||||
["Claude", ["Narrow the launch message to one clear use case. Avoid promising too much.", "发布信息聚焦一个明确场景,避免过度承诺。"]],
|
||||
["Gemini", ["Recruit a small pilot group and compare task completion with their current workflow.", "招募小规模试用用户,对比新旧流程的任务完成情况。"]]],
|
||||
[],
|
||||
[["You", ["Three useful perspectives. I'll combine the engineering checks, focused message, and pilot plan.", "三个角度都很有用,我会整合工程检查、核心信息和试用计划。"]]]
|
||||
],
|
||||
auto: [
|
||||
[task, ["Codex", ["I'd begin with a limited pilot.", "我建议从小范围试用开始。"]],
|
||||
["Claude", ["The audience and launch message need a sharper focus.", "目标用户和发布信息还需要更聚焦。"]],
|
||||
["Gemini", ["We need evidence of repeat use before expanding.", "扩大发布前,需要验证用户会持续使用。"]]],
|
||||
[["Codex", ["Let's make the pilot the first milestone. @Claude can you challenge the positioning?", "把试用作为第一个里程碑。@Claude 请复核一下产品定位。"]]],
|
||||
[["Claude", ["Lead with multi-Agent review, not full automation. @Gemini what evidence would support that promise?", "重点突出多 Agent 复核,避免承诺全自动化。@Gemini 需要哪些证据支撑这个定位?"]]],
|
||||
[["Gemini", ["Measure whether users compare replies and return to the same task. @Codex can you add those checks?", "衡量用户是否比较回复、是否继续同一个任务。@Codex 能把这些检查加入方案吗?"]]],
|
||||
[["Codex", ["Updated: a one-week pilot focused on review, with repeat-use metrics and a rollback plan.", "已完善:进行一周复核场景试用,加入重复使用指标和回退方案。"]]]
|
||||
]
|
||||
};
|
||||
function renderMessages() {
|
||||
var language = window.MeldworkLocale.language;
|
||||
var key = mode + ":" + phase + ":" + language;
|
||||
if (key === renderedKey) return;
|
||||
var sameRun = renderedKey.startsWith(mode + ":") && renderedKey.endsWith(":" + language);
|
||||
var messages = [];
|
||||
for (var step = 0; step <= phase; step++) {
|
||||
conversations[mode][step].forEach(function (message) {
|
||||
if (message[1] || step === phase) messages.push(message);
|
||||
});
|
||||
}
|
||||
var previousCount = sameRun ? renderedCount : 0;
|
||||
var languageOnly = renderedKey.split(":").slice(0, 2).join(":") === mode + ":" + phase;
|
||||
var oldScroll = feed.scrollTop;
|
||||
list.replaceChildren();
|
||||
messages.forEach(function (message, index) {
|
||||
var row = document.createElement("li");
|
||||
row.className = "run-message" + (message[0] === "You" ? " is-user" : "");
|
||||
if (!languageOnly && index >= previousCount && !motion.matches && !paused && visible) row.classList.add("is-new");
|
||||
row.dataset.speaker = message[0];
|
||||
var avatar = document.createElement("span");
|
||||
avatar.className = "run-chat-avatar";
|
||||
if (message[0] === "You") avatar.textContent = say(["You", "你"]);
|
||||
else {
|
||||
var image = document.createElement("img");
|
||||
image.src = "assets/agents/" + ({ Codex: "codex.svg", Claude: "claude.png", Gemini: "gemini.svg" })[message[0]];
|
||||
image.alt = "";
|
||||
avatar.appendChild(image);
|
||||
}
|
||||
var bubble = document.createElement("div");
|
||||
bubble.className = "run-chat-bubble";
|
||||
var author = document.createElement("strong");
|
||||
author.textContent = message[0] === "You" ? say(["You", "你"]) : message[0];
|
||||
bubble.appendChild(author);
|
||||
if (message[1]) {
|
||||
var paragraph = document.createElement("p");
|
||||
say(message[1]).split(/(@Codex|@Claude|@Gemini)/g).forEach(function (part) {
|
||||
var node = document.createElement("span");
|
||||
if (part.startsWith("@")) node.className = "run-mention";
|
||||
node.textContent = part;
|
||||
paragraph.appendChild(node);
|
||||
});
|
||||
bubble.appendChild(paragraph);
|
||||
} else {
|
||||
var typing = document.createElement("span");
|
||||
typing.className = "run-typing";
|
||||
typing.setAttribute("aria-label", say(["Replying", "正在回复"]));
|
||||
for (var dot = 0; dot < 3; dot++) typing.appendChild(document.createElement("i"));
|
||||
bubble.appendChild(typing);
|
||||
}
|
||||
row.append(avatar, bubble);
|
||||
list.appendChild(row);
|
||||
});
|
||||
feed.setAttribute("aria-label", say(["Example conversation", "示例对话"]));
|
||||
feed.scrollTop = languageOnly ? oldScroll : feed.scrollHeight;
|
||||
renderedKey = key;
|
||||
renderedCount = messages.filter(function (message) { return message[1]; }).length;
|
||||
}
|
||||
var stages = {
|
||||
direct: [["Send to Codex", "发给 Codex"], ["Codex works on your task", "Codex 处理任务"], ["Read the reply", "查看回复"], ["You ask a follow-up", "你继续追问"], ["Continue the same conversation", "延续同一段会话"]],
|
||||
concurrent: [["One task reaches three Agents", "同一个任务到达三个 Agent"], ["Three Agents work independently", "三个 Agent 独立处理"], ["Replies arrive as Agents finish", "各 Agent 完成后陆续回复"], ["Each perspective stays separate", "每个观点独立保留"], ["Compare the three replies", "比较三份回复"]],
|
||||
auto: [["Everyone proposes independently", "各自提出初步想法"], ["Codex invites Claude to review", "Codex 邀请 Claude 复核"], ["Claude asks Gemini to check evidence", "Claude 请 Gemini 核查依据"], ["Gemini asks Codex to revise", "Gemini 请 Codex 完善方案"], ["Codex incorporates the peer feedback", "Codex 结合同伴反馈完善方案"]]
|
||||
};
|
||||
function say(pair) { return pair[window.MeldworkLocale.language === "zh" ? 1 : 0]; }
|
||||
function render() {
|
||||
root.dataset.mode = mode;
|
||||
root.dataset.phase = String(phase);
|
||||
root.classList.toggle("is-paused", paused || motion.matches || !visible || document.hidden);
|
||||
tabs.forEach(function (tab) {
|
||||
var selected = tab.dataset.runMode === mode;
|
||||
tab.setAttribute("aria-selected", String(selected));
|
||||
tab.tabIndex = selected ? 0 : -1;
|
||||
});
|
||||
root.querySelector("[role=tabpanel]").setAttribute("aria-labelledby", "run-" + mode);
|
||||
root.querySelector("[data-run-description]").textContent = say(descriptions[mode]);
|
||||
root.querySelector("[data-run-status]").textContent = say(stages[mode][phase]);
|
||||
renderMessages();
|
||||
root.querySelectorAll(".run-progress i").forEach(function (segment, index) { segment.classList.toggle("is-complete", index <= phase); });
|
||||
var label = say(paused ? ["Resume demo", "继续演示"] : ["Pause demo", "暂停演示"]);
|
||||
pause.setAttribute("aria-label", label);
|
||||
pause.title = label;
|
||||
pause.setAttribute("aria-pressed", String(paused));
|
||||
pause.hidden = motion.matches;
|
||||
}
|
||||
function schedule() {
|
||||
clearTimeout(timer);
|
||||
render();
|
||||
if (visible && !paused && !motion.matches && !document.hidden) {
|
||||
timer = setTimeout(function () { phase = (phase + 1) % 5; schedule(); }, phase === 4 ? 3400 : 2400);
|
||||
}
|
||||
}
|
||||
function select(tab) { mode = tab.dataset.runMode; phase = motion.matches ? 4 : 0; schedule(); }
|
||||
tabs.forEach(function (tab, index) {
|
||||
tab.addEventListener("click", function () { select(tab); });
|
||||
tab.addEventListener("keydown", function (event) {
|
||||
var next = index;
|
||||
if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
|
||||
else if (event.key === "ArrowLeft") next = (index + tabs.length - 1) % tabs.length;
|
||||
else if (event.key === "Home") next = 0;
|
||||
else if (event.key === "End") next = tabs.length - 1;
|
||||
else return;
|
||||
event.preventDefault(); tabs[next].focus(); select(tabs[next]);
|
||||
});
|
||||
});
|
||||
pause.addEventListener("click", function () { paused = !paused; schedule(); });
|
||||
document.addEventListener("visibilitychange", schedule);
|
||||
window.addEventListener("meldwork:languagechange", render);
|
||||
motion.addEventListener("change", function () { phase = motion.matches ? 4 : 0; schedule(); });
|
||||
new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; schedule(); }, { threshold: 0.15 }).observe(root);
|
||||
if (motion.matches) phase = 4;
|
||||
render();
|
||||
})();
|
||||
+245
-388
@@ -9,6 +9,15 @@
|
||||
|
||||
/* ================= DESIGN TOKENS ================= */
|
||||
|
||||
@font-face {
|
||||
font-family: "Fusion Pixel";
|
||||
src: url("assets/fonts/fusion-pixel/fusion-pixel-12px-proportional-zh_hans.otf.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+2E80-9FFF, U+F900-FAFF, U+FF00-FFEF;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Dark theme (default) */
|
||||
--bg: #08090b;
|
||||
@@ -40,6 +49,11 @@
|
||||
--r-card-lg: 22px;
|
||||
}
|
||||
|
||||
html[lang="zh-CN"] {
|
||||
--font-sans: "Inter", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
--font-display: "BubbledotICG-FinePos", "Fusion Pixel", "PingFang SC", monospace;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #faf9f7;
|
||||
--bg-deep: #f3f1ed;
|
||||
@@ -116,21 +130,27 @@ body.menu-open { overflow: hidden; }
|
||||
.topnav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
transform: translateX(-50%);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
backdrop-filter: blur(0);
|
||||
-webkit-backdrop-filter: blur(0);
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: clamp(10px, 1.6vw, 18px);
|
||||
padding: clamp(12px, 2vh, 22px) clamp(14px, 3vw, 32px);
|
||||
transition: background 0.4s ease, backdrop-filter 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
.topnav.scrolled {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 20px 32px;
|
||||
transition: width 0.4s var(--ease-out), top 0.4s var(--ease-out), padding 0.4s var(--ease-out), border-radius 0.4s ease, background 0.4s ease, backdrop-filter 0.4s ease, box-shadow 0.4s ease;
|
||||
}
|
||||
.topnav.scrolled { top: 16px; width: min(1160px, calc(100% - 48px)); padding: 10px 18px; border-radius: 22px; background: var(--glass-bg); backdrop-filter: blur(22px) saturate(140%); -webkit-backdrop-filter: blur(22px) saturate(140%); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.18); }
|
||||
.nav-start, .nav-actions { display: flex; align-items: center; gap: 14px; }
|
||||
.nav-start { min-width: 0; }
|
||||
.nav-actions { flex-shrink: 0; margin-left: auto; }
|
||||
.nav-download-icon { display: none; width: 20px; height: 20px; }
|
||||
|
||||
.logo-badge {
|
||||
position: relative;
|
||||
@@ -164,15 +184,13 @@ body.menu-open { overflow: hidden; }
|
||||
.nav-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
flex: 0 1 620px;
|
||||
flex: 0 0 auto;
|
||||
height: 42px;
|
||||
padding: 4px 6px;
|
||||
background: var(--glass-bg);
|
||||
background: transparent;
|
||||
border-radius: var(--r-pill);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
animation: navItemIn 0.6s var(--ease-out) 0.2s forwards;
|
||||
@@ -1213,370 +1231,99 @@ body.menu-open { overflow: hidden; }
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ---- Scenario (redesigned, no borders) ---- */
|
||||
.scenario-stage {
|
||||
margin-top: clamp(40px, 6vh, 60px);
|
||||
position: relative;
|
||||
padding: 0;
|
||||
background: var(--surface);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-3);
|
||||
/* Three collaboration modes share one stable stage. */
|
||||
#scenario { padding-top: 64px; scroll-margin-top: 100px; }
|
||||
.run-demo { margin-top: 40px; }
|
||||
.run-tabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; padding-bottom: 12px; }
|
||||
.run-tab { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 5px 10px; align-content: center; min-height: 84px; padding: 14px 18px; border: 0; border-radius: 0; background: transparent; text-align: left; color: var(--muted); font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: 0; transition: color 180ms ease; }
|
||||
.run-tab small { grid-column: 2; font-size: 12px; font-weight: 400; }
|
||||
.run-tab-number { color: var(--faint); font-size: 12px; padding-top: 2px; font-variant-numeric: tabular-nums; }
|
||||
.run-tab[aria-selected="true"] { color: var(--ink); }
|
||||
.run-tab[aria-selected="true"]::after { content: ""; position: absolute; bottom: -13px; left: 18px; right: 18px; height: 2px; background: var(--coral); }
|
||||
.run-tab[aria-selected="true"] .run-tab-number { color: var(--coral); }
|
||||
.run-tab:hover { color: var(--ink); }
|
||||
.run-tab:focus-visible, .run-play-toggle:focus-visible, .faq-question:focus-visible { outline: 2px solid var(--coral); outline-offset: 4px; }
|
||||
.run-explanation { display: flex; align-items: flex-start; gap: 32px; padding: 24px 0; min-height: 100px; }
|
||||
.run-explanation p { flex: 1; margin: 0; max-width: 750px; font-size: 15px; line-height: 1.7; color: var(--ink-soft); }
|
||||
.run-example-label { margin-left: auto; padding-top: 4px; flex-shrink: 0; color: var(--faint); font-size: 11px; }
|
||||
.run-window { max-width: 860px; margin: 12px auto 0; overflow: hidden; border: 1px solid var(--line-strong); border-radius: 18px; background: color-mix(in srgb, var(--surface) 86%, transparent); box-shadow: 0 22px 70px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.06); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); }
|
||||
[data-theme="light"] .run-window { background: rgba(255, 255, 255, 0.78); box-shadow: 0 18px 50px rgba(33, 26, 20, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.8); }
|
||||
.run-window-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 42px; padding: 0 52px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, color-mix(in srgb, var(--surface-raised) 84%, transparent), color-mix(in srgb, var(--surface) 72%, transparent)); }
|
||||
[data-theme="light"] .run-window-bar { background: linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(245, 243, 240, 0.72)); }
|
||||
.run-window-lights { position: absolute; left: 16px; top: 50%; display: flex; gap: 8px; transform: translateY(-50%); }
|
||||
.run-window-lights i { width: 11px; height: 11px; border-radius: 50%; box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.18); }
|
||||
.run-window-lights i:nth-child(1) { background: #ff5f57; }
|
||||
.run-window-lights i:nth-child(2) { background: #ffbd2e; }
|
||||
.run-window-lights i:nth-child(3) { background: #28c840; }
|
||||
.run-window-title { color: var(--faint); font-size: 12px; font-weight: 500; line-height: 1; }
|
||||
.run-window-body { background: linear-gradient(180deg, color-mix(in srgb, var(--bg-deep) 34%, transparent), transparent 42%); }
|
||||
.run-chat { height: 420px; padding: 20px 16px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; overflow-anchor: none; }
|
||||
.run-messages { display: flex; flex-direction: column; gap: 16px; list-style: none; padding: 0; margin: 0; }
|
||||
.run-message { display: flex; align-items: flex-start; gap: 10px; max-width: 88%; flex-shrink: 0; }
|
||||
.run-chat-avatar { display: grid; place-items: center; width: 32px; height: 32px; padding: 5px; flex-shrink: 0; border-radius: 8px; background: #fff; color: var(--coral); font-size: 10px; }
|
||||
.run-chat-avatar img { width: 100%; height: 100%; object-fit: contain; }
|
||||
.run-chat-bubble { min-width: 0; padding: 13px 17px; background: var(--surface); border-radius: 0 8px 8px 8px; color: var(--ink-soft); }
|
||||
.run-chat-bubble strong { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--ink); }
|
||||
.run-chat-bubble p { margin: 0; font-size: 14px; line-height: 1.65; overflow-wrap: anywhere; }
|
||||
.run-message.is-user { align-self: flex-end; flex-direction: row-reverse; }
|
||||
.run-message.is-user .run-chat-avatar { background: var(--surface); }
|
||||
.run-message.is-user .run-chat-bubble { background: color-mix(in srgb, var(--surface) 90%, var(--coral)); border-radius: 8px 0 8px 8px; }
|
||||
.run-message.is-user strong, .run-mention { color: var(--coral); }
|
||||
.run-mention { font-weight: 500; }
|
||||
.run-message.is-new { animation: run-message-in 320ms var(--ease-out) both; }
|
||||
.run-typing { display: flex; align-items: center; gap: 4px; height: 22px; min-width: 48px; }
|
||||
.run-typing i { width: 4px; height: 4px; border-radius: 50%; background: var(--muted); animation: run-typing 1s ease-in-out infinite; }
|
||||
.run-typing i:nth-child(2) { animation-delay: 150ms; }
|
||||
.run-typing i:nth-child(3) { animation-delay: 300ms; }
|
||||
@keyframes run-message-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes run-typing { 0%, 80%, 100% { opacity: 0.35; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
|
||||
.run-playback { display: flex; align-items: center; gap: 14px; margin-top: 16px; padding: 16px 0; border-top: 1px solid var(--line); font-size: 12px; color: var(--muted); min-height: 66px; }
|
||||
.run-progress { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
.run-progress i { width: 14px; height: 3px; background: var(--line-strong); transition: background 300ms ease; }
|
||||
.run-progress i.is-complete { background: var(--coral); }
|
||||
.run-play-toggle { flex-shrink: 0; display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--line-strong); border-radius: 50%; margin-left: auto; color: var(--ink-soft); }
|
||||
.run-play-toggle svg { width: 14px; height: 14px; }
|
||||
.run-play-toggle .play-icon, .run-play-toggle[aria-pressed="true"] .pause-icon { display: none; }
|
||||
.run-play-toggle[aria-pressed="true"] .play-icon { display: block; }
|
||||
.run-play-toggle[hidden] { display: none; }
|
||||
.run-demo.is-paused *, .run-demo.is-paused *::after { animation-play-state: paused !important; }
|
||||
.run-demo.is-paused .run-message.is-new { animation: none; }
|
||||
@media (max-width: 1100px) {
|
||||
.topnav .nav-pill { display: none; }
|
||||
.topnav .burger { display: block; }
|
||||
}
|
||||
.scenario-stage::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -40%;
|
||||
right: -20%;
|
||||
width: 60%;
|
||||
height: 80%;
|
||||
background: radial-gradient(circle, rgba(239,90,69,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
@media (max-width: 720px) {
|
||||
#scenario { padding-top: 48px; }
|
||||
#scenario > .section-sub { font-size: 13px; line-height: 1.6; }
|
||||
.run-demo { margin-top: 28px; }
|
||||
.topnav { padding: 14px 18px; gap: 12px; }
|
||||
.topnav.scrolled { top: 10px; width: calc(100% - 24px); border-radius: 18px; padding: 8px 12px; }
|
||||
.nav-start, .nav-actions { gap: 8px; }
|
||||
.nav-actions .cta-dark { display: grid; place-items: center; width: 42px; height: 42px; padding: 0; }
|
||||
.nav-actions .cta-status, .nav-download-label { display: none; }
|
||||
.nav-download-icon { display: block; }
|
||||
.run-tabs { gap: 4px; }
|
||||
.run-tab { display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 5px; padding: 10px 8px; font-size: 12px; min-height: 112px; }
|
||||
.run-tab small { font-size: 10px; line-height: 1.4; }
|
||||
.run-tab-number { font-size: 10px; }
|
||||
.run-explanation { display: block; padding: 20px 0 12px; min-height: 142px; }
|
||||
.run-explanation p { font-size: 13px; }
|
||||
.run-example-label { display: block; margin-top: 8px; }
|
||||
.run-window { margin-top: 10px; border-radius: 15px; }
|
||||
.run-window-bar { height: 36px; padding: 0 44px; }
|
||||
.run-window-lights { left: 13px; gap: 6px; }
|
||||
.run-window-lights i { width: 9px; height: 9px; }
|
||||
.run-window-title { font-size: 11px; }
|
||||
.run-chat { height: 440px; padding: 16px 10px; }
|
||||
.run-message { max-width: 96%; gap: 8px; }
|
||||
.run-chat-avatar { width: 26px; height: 26px; border-radius: 6px; padding: 4px; font-size: 9px; }
|
||||
.run-chat-bubble { padding: 11px 13px; }
|
||||
.run-chat-bubble p { font-size: 12px; }
|
||||
.run-playback { gap: 10px; font-size: 10px; min-height: 66px; }
|
||||
.run-progress { gap: 3px; }
|
||||
.run-progress i { width: 8px; }
|
||||
}
|
||||
|
||||
/* macOS window chrome */
|
||||
.mac-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 13px 20px;
|
||||
background: linear-gradient(180deg, var(--surface-raised), var(--surface));
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.mac-lights { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
.mac-lights i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
transition: filter 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
.light-r { background: #ff5f57; box-shadow: inset 0 0 2px rgba(0,0,0,0.18); }
|
||||
.light-y { background: #febc2e; box-shadow: inset 0 0 2px rgba(0,0,0,0.18); }
|
||||
.light-g { background: #28c840; box-shadow: inset 0 0 2px rgba(0,0,0,0.18); }
|
||||
.scenario-stage:hover .mac-lights i { filter: saturate(1.25) brightness(1.08); }
|
||||
|
||||
.titlebar-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* macOS segmented control */
|
||||
.seg-control {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
border-radius: 9px;
|
||||
background: var(--bg-deep);
|
||||
box-shadow: inset 0 0 0 1px var(--line);
|
||||
}
|
||||
.seg-btn {
|
||||
padding: 5px 14px;
|
||||
border-radius: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.01em;
|
||||
transition: background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
.seg-btn:hover { color: var(--ink-soft); }
|
||||
.seg-btn.is-on {
|
||||
background: var(--surface-raised);
|
||||
color: var(--ink);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.28), inset 0 0 0 1px var(--line-strong);
|
||||
}
|
||||
[data-theme="light"] .seg-btn.is-on { box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1), inset 0 0 0 1px var(--line-strong); }
|
||||
|
||||
.scenario-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.meta-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 12px;
|
||||
background: rgba(239,90,69,0.1);
|
||||
border-radius: var(--r-pill);
|
||||
color: var(--coral);
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.meta-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--coral);
|
||||
animation: statusPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
.meta-name { font-size: 14px; font-weight: 600; color: var(--ink); white-space: nowrap; }
|
||||
.meta-round {
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
color: var(--coral);
|
||||
padding: 5px 12px;
|
||||
background: rgba(239,90,69,0.06);
|
||||
border-radius: var(--r-pill);
|
||||
transition: opacity 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scenario-body { position: relative; z-index: 1; padding: clamp(20px, 3vw, 32px); }
|
||||
|
||||
/* Sequence mode — full transcript, auto playback disabled */
|
||||
.scenario-stage.is-order .t-step { opacity: 1; transform: translateX(0); }
|
||||
.scenario-stage.is-order .t-node { background: var(--surface-hover); }
|
||||
.scenario-stage.is-order .scenario-controls { display: none; }
|
||||
|
||||
.timeline-track {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
}
|
||||
.timeline-rail {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rail-line {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--line);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.rail-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background: linear-gradient(180deg, var(--coral), #ffb29b);
|
||||
border-radius: 2px;
|
||||
transition: height 0.35s var(--ease-out);
|
||||
box-shadow: 0 0 12px var(--coral-glow);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
.t-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
opacity: 0;
|
||||
transform: translateX(-16px);
|
||||
transition: opacity 0.35s var(--ease-out), transform 0.35s var(--ease-out);
|
||||
position: relative;
|
||||
}
|
||||
.t-step.is-visible { opacity: 0.42; transform: translateX(0); }
|
||||
.t-step.is-active { opacity: 1; transform: translateX(0); }
|
||||
|
||||
.t-node {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg);
|
||||
z-index: 2;
|
||||
transition: box-shadow 0.35s ease, transform 0.35s var(--ease-out), background 0.35s ease;
|
||||
}
|
||||
.t-step.is-visible .t-node { background: var(--surface-hover); }
|
||||
.t-step.is-active .t-node {
|
||||
background: var(--coral);
|
||||
box-shadow: 0 0 0 4px rgba(239,90,69,0.15), 0 0 16px var(--coral-glow);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
.t-step.is-active .t-node::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border: 1.5px solid var(--coral);
|
||||
border-radius: 50%;
|
||||
animation: nodePulseRing 1.8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes nodePulseRing {
|
||||
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||
50% { transform: scale(1.4); opacity: 0; }
|
||||
}
|
||||
|
||||
.t-time {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 11.5px;
|
||||
color: var(--muted);
|
||||
padding-top: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
.t-step.is-active .t-time { color: var(--coral); }
|
||||
|
||||
.t-bubble {
|
||||
flex: 1;
|
||||
background: var(--bg-deep);
|
||||
border-radius: 14px;
|
||||
padding: 16px 20px;
|
||||
position: relative;
|
||||
transition: box-shadow 0.28s ease, transform 0.28s var(--ease-out), opacity 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.t-step.is-active .t-bubble {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(239, 90, 69, 0.08),
|
||||
0 8px 28px rgba(0, 0, 0, 0.2),
|
||||
0 0 40px rgba(239, 90, 69, 0.03);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.t-step.is-active .t-bubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -20%;
|
||||
width: 40%;
|
||||
height: 200%;
|
||||
background: linear-gradient(105deg, transparent 40%, rgba(239,90,69,0.06) 50%, transparent 60%);
|
||||
animation: bubbleSweep 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes bubbleSweep {
|
||||
0%, 100% { transform: translateX(-30%); opacity: 0; }
|
||||
50% { transform: translateX(250%); opacity: 1; }
|
||||
}
|
||||
.t-bubble header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.t-bubble header img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background: var(--surface);
|
||||
padding: 1px;
|
||||
transition: box-shadow 0.35s ease;
|
||||
}
|
||||
.t-step.is-active .t-bubble header img {
|
||||
box-shadow: 0 0 0 2px rgba(239, 90, 69, 0.3), 0 0 12px var(--coral-glow);
|
||||
}
|
||||
.t-bubble header strong { color: var(--ink); font-weight: 600; font-size: 13.5px; }
|
||||
.t-bubble p { margin: 0; color: var(--ink-soft); font-size: 13.5px; line-height: 1.6; }
|
||||
.t-bubble strong { color: var(--ink); font-weight: 600; }
|
||||
.you-dot, .gate-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--coral);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.you-dot::after { content: "Y"; color: #fff; }
|
||||
.gate-dot { background: var(--coral); box-shadow: 0 0 10px var(--coral-glow); position: relative; }
|
||||
.gate-dot::after { content: ""; position: absolute; inset: -3px; border: 1px solid var(--coral); border-radius: 50%; animation: gatePulseRing 1.6s ease-in-out infinite; }
|
||||
@keyframes gatePulseRing {
|
||||
0%, 100% { transform: scale(1); opacity: 0.6; }
|
||||
50% { transform: scale(1.15); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Per-agent accent system: tinted bubble wash + matching role badge */
|
||||
.t-bubble-codex {
|
||||
background: linear-gradient(135deg, rgba(108, 140, 255, 0.07), var(--bg-deep) 60%);
|
||||
box-shadow: inset 3px 0 0 rgba(108, 140, 255, 0.55);
|
||||
}
|
||||
.t-bubble-hermes {
|
||||
background: linear-gradient(135deg, rgba(255, 157, 82, 0.07), var(--bg-deep) 60%);
|
||||
box-shadow: inset 3px 0 0 rgba(255, 157, 82, 0.55);
|
||||
}
|
||||
.t-bubble-ocr {
|
||||
background: linear-gradient(135deg, rgba(106, 212, 139, 0.07), var(--bg-deep) 60%);
|
||||
box-shadow: inset 3px 0 0 rgba(106, 212, 139, 0.55);
|
||||
}
|
||||
.t-bubble-you {
|
||||
background: linear-gradient(135deg, rgba(239, 90, 69, 0.06), var(--bg-deep) 60%);
|
||||
box-shadow: inset 3px 0 0 rgba(239, 90, 69, 0.45);
|
||||
}
|
||||
.t-bubble-codex .t-role { color: #93a9ff; background: rgba(108, 140, 255, 0.1); }
|
||||
.t-bubble-hermes .t-role { color: #ffb27e; background: rgba(255, 157, 82, 0.1); }
|
||||
.t-bubble-ocr .t-role { color: #8ce0ac; background: rgba(106, 212, 139, 0.1); }
|
||||
.t-bubble-you .t-role, .t-bubble-gate .t-role { color: var(--coral); background: rgba(239, 90, 69, 0.1); }
|
||||
.t-role {
|
||||
margin-left: auto;
|
||||
padding: 3px 9px;
|
||||
border-radius: 6px;
|
||||
font-size: 10.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.t-step.is-active .t-role { filter: brightness(1.12); }
|
||||
.t-bubble-gate {
|
||||
background: linear-gradient(135deg, rgba(239,90,69,0.08), rgba(239,90,69,0.02));
|
||||
box-shadow: inset 3px 0 0 var(--coral), 0 0 24px rgba(239,90,69,0.06);
|
||||
}
|
||||
.t-step.is-active .t-bubble-gate {
|
||||
box-shadow:
|
||||
inset 3px 0 0 var(--coral),
|
||||
0 8px 28px rgba(0, 0, 0, 0.2),
|
||||
0 0 50px rgba(239, 90, 69, 0.08);
|
||||
}
|
||||
|
||||
.scenario-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin-top: 24px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.rail-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-deep);
|
||||
color: var(--ink-soft);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.rail-btn:hover {
|
||||
background: rgba(239,90,69,0.1);
|
||||
color: var(--coral);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.rail-btn:active { transform: scale(0.95); }
|
||||
|
||||
/* ---- CardSwap screenshots (React Bits port) ---- */
|
||||
.card-swap-stage {
|
||||
margin-top: clamp(48px, 7vh, 72px);
|
||||
@@ -1982,7 +1729,6 @@ body.menu-open { overflow: hidden; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.topnav { justify-content: space-between; }
|
||||
.hero { padding: 100px 18px 96px; }
|
||||
.headline { font-size: clamp(26px, 7.4vw, 44px); letter-spacing: -0.05em; }
|
||||
.hl-line { white-space: normal; }
|
||||
@@ -1997,20 +1743,13 @@ body.menu-open { overflow: hidden; }
|
||||
.cmp-row > div:nth-child(3) { grid-column: auto; padding-top: 0; border-top: none; }
|
||||
.cmp-head > div:nth-child(3) { display: block; }
|
||||
.scroll-cue { display: none; }
|
||||
.t-step { flex-direction: column; gap: 4px; }
|
||||
.t-time { width: auto; padding-top: 0; }
|
||||
.scenario-controls { font-size: 11px; }
|
||||
.t-role { display: none; }
|
||||
.meta-round { font-size: 11.5px; }
|
||||
.mac-titlebar { gap: 10px; padding: 12px 16px; }
|
||||
.titlebar-right { margin-left: 0; width: 100%; justify-content: space-between; }
|
||||
.section { padding-top: clamp(72px, 11vh, 110px); }
|
||||
.why-left { position: static; }
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.headline { font-size: 26px; letter-spacing: -0.06em; }
|
||||
.nav-pill, .topnav > .cta-dark { display: none; }
|
||||
.nav-pill { display: none; }
|
||||
.burger { display: block; }
|
||||
}
|
||||
|
||||
@@ -2031,6 +1770,124 @@ body.menu-open { overflow: hidden; }
|
||||
.cta-primary::after, .spark { display: none; }
|
||||
.bg-video { display: none; }
|
||||
.cs-card { transition: none !important; }
|
||||
.t-bubble::after, .logo-badge::before { display: none; }
|
||||
.logo-badge::before { display: none; }
|
||||
.logo-badge, .nav-pill, .cta-dark, .theme-toggle { opacity: 1 !important; transform: none !important; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Retained boundary, FAQ, and download layouts; headings use the original display styles. */
|
||||
#boundary, #faq, #download, body > .foot-meta {
|
||||
--line: rgba(226, 235, 240, 0.13);
|
||||
--muted: #a1afb6;
|
||||
--faint: #8b989f;
|
||||
line-height: 1.6;
|
||||
}
|
||||
[data-theme="light"] #boundary, [data-theme="light"] #faq,
|
||||
[data-theme="light"] #download, [data-theme="light"] body > .foot-meta {
|
||||
--line: rgba(24, 33, 39, 0.16);
|
||||
--muted: #536670;
|
||||
--faint: #60717a;
|
||||
}
|
||||
.architecture-path { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; margin: 30px 0 0; padding: 0; list-style: none; }
|
||||
.architecture-path > * { position: relative; border-top: 2px solid var(--line); padding: 20px 0 0; min-width: 0; }
|
||||
.architecture-path > :first-child { border-top-color: var(--coral); }
|
||||
.architecture-path h3, .architecture-path strong { display: block; font-size: 16px; line-height: 1.5; font-weight: 600; margin: 0 0 8px; }
|
||||
.architecture-path p, .architecture-path span { color: var(--muted); font-size: 13px; line-height: 1.7; margin: 0; }
|
||||
.architecture-path .architecture-step { display: block; margin-bottom: 14px; color: var(--coral); font-size: 12px; font-weight: 600; }
|
||||
.architecture-path .boundary-label { display: block; margin-top: 14px; font-size: 11px; color: var(--ink-soft); }
|
||||
.bnd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 54px; margin-top: 36px; }
|
||||
.bnd-col { min-width: 0; padding-top: 22px; border-top: 1px solid var(--line); }
|
||||
.bnd-col h3 { font-size: 18px; font-weight: 600; line-height: 1.5; margin: 0 0 18px; }
|
||||
.bnd-col ul { padding-left: 18px; margin: 0; }
|
||||
.bnd-col li { padding-left: 3px; font-size: 14px; line-height: 1.75; margin-top: 10px; color: var(--muted); }
|
||||
.bnd-col li::marker { color: var(--coral); }
|
||||
.bnd-glyph { display: none; }
|
||||
.faq { padding-top: 96px; }
|
||||
.faq-list { margin-top: 30px; }
|
||||
.faq-item { border-bottom: 1px solid var(--line); overflow: hidden; box-sizing: content-box; }
|
||||
.faq-item:first-of-type { margin-top: 28px; border-top: 1px solid var(--line); }
|
||||
.faq-question { position: relative; padding: 21px 40px 21px 0; font-size: 16px; line-height: 1.5; font-weight: 500; cursor: pointer; list-style: none; }
|
||||
.faq-question::-webkit-details-marker { display: none; }
|
||||
.faq-question::after { content: "+"; position: absolute; right: 4px; top: 21px; color: var(--muted); font-size: 21px; font-weight: 400; line-height: 1.2; }
|
||||
.faq-item[open] .faq-question::after { content: "-"; color: var(--coral); }
|
||||
.faq-question:hover { color: var(--coral); }
|
||||
[data-theme="light"] .faq-question:hover { color: var(--coral); }
|
||||
.faq-answer { max-width: 860px; padding: 0 40px 22px 0; color: var(--muted); font-size: 14px; line-height: 1.8; }
|
||||
.faq-answer p { margin: 0 0 12px; }
|
||||
.faq-answer p:last-child { margin-bottom: 0; }
|
||||
.faq-answer strong { color: var(--ink-soft); }
|
||||
.cta-final { z-index: 1; max-width: 1160px; margin: 0 auto; padding: 104px 32px 28px; text-align: center; }
|
||||
#download .final-title { max-width: 780px; margin: 0 auto; text-wrap: balance; }
|
||||
.final-sub { max-width: 650px; margin: 18px auto 0; color: var(--muted); font-size: 15px; }
|
||||
.download-note { max-width: 680px; margin: 18px auto 0; color: var(--muted); font-size: 12px; line-height: 1.7; }
|
||||
.text-link { color: var(--ink-soft); text-decoration: underline; text-underline-offset: 4px; }
|
||||
.text-link:hover { color: var(--ink); }
|
||||
.section > .text-link { display: inline-flex; align-items: center; min-height: 44px; margin-top: 20px; font-size: 13px; }
|
||||
.foot-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 14px 24px; padding-top: 26px; margin-top: 74px; border-top: 1px solid var(--line); color: var(--faint); font-size: 11px; line-height: 1.7; }
|
||||
body > .foot-meta { position: relative; z-index: 1; max-width: calc(1160px - 64px); margin: 40px auto 0; padding: 26px 0 30px; }
|
||||
.foot-wordmark { width: 112px; height: 25px; object-fit: contain; }
|
||||
.foot-link { color: var(--muted); overflow-wrap: anywhere; }
|
||||
.foot-link:hover { color: var(--ink); text-decoration: underline; }
|
||||
|
||||
|
||||
#boundary .bnd-col { padding: 22px 0 0; border-radius: 0; background: none; box-shadow: none; }
|
||||
#boundary .bnd-col h3 { display: block; text-transform: none; letter-spacing: 0; }
|
||||
#boundary .bnd-col ul { display: block; list-style: disc; }
|
||||
#boundary .bnd-col li::before { content: none; }
|
||||
#boundary .bnd-col a, #faq .faq-answer a { text-decoration: underline; text-underline-offset: 4px; }
|
||||
#faq summary:focus-visible { outline: 2px solid var(--coral); outline-offset: 4px; }
|
||||
#download .cta-primary { display: inline-flex; flex-direction: column; justify-content: center; align-items: center; gap: 1px; min-height: 58px; padding: 11px 24px; border-radius: 7px; background: #fff; color: #20282d; border: 1px solid #fff; text-align: center; box-shadow: none; }
|
||||
#download .cta-primary::after { content: none; }
|
||||
#download .cta-primary:hover { background: #edf1f3; border-color: #edf1f3; transform: none; box-shadow: none; }
|
||||
#download .cta-label { font-weight: 600; font-size: 15px; line-height: 1.5; }
|
||||
#download .cta-meta { color: inherit; font-weight: 400; font-size: 11px; line-height: 1.5; opacity: 0.8; }
|
||||
#download .cta-ghost { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; gap: 8px; font-size: 14px; font-weight: 500; color: var(--ink-soft); }
|
||||
#download .cta-ghost:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 5px; }
|
||||
@media (max-width: 860px) {
|
||||
#boundary .architecture-path { grid-template-columns: 1fr; gap: 22px; }
|
||||
#boundary .architecture-path > * { padding-top: 16px; }
|
||||
#boundary .bnd-grid { grid-template-columns: 1fr; gap: 28px; margin-top: 28px; }
|
||||
#faq .faq-question { font-size: 15px; }
|
||||
#faq .faq-answer { padding-right: 8px; }
|
||||
#download { padding: 74px 24px 24px; }
|
||||
body > .foot-meta { margin-left: 24px; margin-right: 24px; gap: 10px 18px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#download { padding-left: 20px; padding-right: 20px; }
|
||||
body > .foot-meta { flex-direction: column; }
|
||||
}
|
||||
|
||||
/* Chinese keeps the display character of the original, with readable body copy. */
|
||||
html[lang="zh-CN"] .headline { font-size: 60px; line-height: 1.4; letter-spacing: 0; }
|
||||
html[lang="zh-CN"] .section-title { font-size: 36px; line-height: 1.5; letter-spacing: 0; text-wrap: balance; }
|
||||
html[lang="zh-CN"] .final-title { font-size: 48px; line-height: 1.5; letter-spacing: 0; }
|
||||
html[lang="zh-CN"] .section-sub, html[lang="zh-CN"] .subhead { line-height: 1.9; }
|
||||
html[lang="zh-CN"] .eyebrow { letter-spacing: 0; }
|
||||
html[lang="zh-CN"] .hl-line .word { display: inline; white-space: normal; }
|
||||
html[lang="zh-CN"] .t-time { font-family: var(--font-sans); }
|
||||
|
||||
.locale-toggle { display: grid; place-items: center; flex-shrink: 0; width: 42px; height: 42px; border-radius: 50%; background: var(--glass-bg); color: var(--ink-soft); font: 500 12px var(--font-sans); }
|
||||
.locale-toggle:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
|
||||
/* One viewport-sized scene bridges every chapter without image-size seams. */
|
||||
html { overflow-x: clip; }
|
||||
body { isolation: isolate; }
|
||||
.chapter-scene { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; opacity: 0; }
|
||||
.chapter-scene canvas { display: block; width: 100%; height: 100%; }
|
||||
.chapter-motion-toggle { position: fixed; bottom: 22px; right: 22px; z-index: 35; display: grid; place-items: center; width: 40px; height: 40px; padding: 11px; border: 1px solid var(--line-strong); border-radius: 50%; background: var(--bg); color: var(--muted); }
|
||||
.chapter-motion-toggle[hidden] { display: none; }
|
||||
.chapter-motion-toggle:hover { color: var(--coral); }
|
||||
.chapter-motion-toggle svg { width: 18px; height: 18px; }
|
||||
.chapter-motion-toggle .play-icon, .chapter-motion-toggle[aria-pressed="true"] .pause-icon { display: none; }
|
||||
.chapter-motion-toggle[aria-pressed="true"] .play-icon { display: block; }
|
||||
.chapter-motion-toggle::after { content: attr(aria-label); position: absolute; right: 48px; white-space: nowrap; padding: 6px 10px; border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; opacity: 0; pointer-events: none; }
|
||||
.chapter-motion-toggle:hover::after, .chapter-motion-toggle:focus-visible::after { opacity: 1; }
|
||||
@media (max-width: 860px) {
|
||||
html[lang="zh-CN"] .headline { font-size: 48px; }
|
||||
html[lang="zh-CN"] .section-title { font-size: 30px; }
|
||||
html[lang="zh-CN"] .final-title { font-size: 36px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
html[lang="zh-CN"] .headline { font-size: 30px; line-height: 1.5; }
|
||||
html[lang="zh-CN"] .section-title, html[lang="zh-CN"] .final-title { font-size: 24px; }
|
||||
.chapter-motion-toggle { bottom: 14px; right: 14px; width: 36px; height: 36px; padding: 9px; }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
|
||||
const url = pathToFileURL(path.join(__dirname, '..', 'index.html')).href;
|
||||
const output = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-landing-check-'));
|
||||
console.log('Screenshots: ' + output);
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const errors = [];
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(url);
|
||||
assert.equal(await page.locator('html').getAttribute('lang'), 'en');
|
||||
await page.waitForTimeout(600);
|
||||
const expandedNav = await page.locator('#topnav').boundingBox();
|
||||
assert.equal(expandedNav.x, 0);
|
||||
assert.equal(expandedNav.width, 1440);
|
||||
assert.equal(await page.locator('#topnav').evaluate(el => getComputedStyle(el).borderTopWidth), '0px');
|
||||
await page.screenshot({ path: path.join(output, 'nav-home.png') });
|
||||
await page.evaluate(() => { document.documentElement.style.scrollBehavior = 'auto'; window.sameDocument = true; });
|
||||
await page.locator('#scenario').evaluate(el => window.scrollTo(0, el.offsetTop - 110));
|
||||
await page.waitForTimeout(500);
|
||||
const compactNav = await page.locator('#topnav').boundingBox();
|
||||
assert.ok(compactNav.width < expandedNav.width && compactNav.height < expandedNav.height);
|
||||
await page.screenshot({ path: path.join(output, 'nav-scrolled.png') });
|
||||
const demo = page.locator('[data-run-demo]');
|
||||
await page.locator('[data-run-pause]').click();
|
||||
const before = await demo.getAttribute('data-phase');
|
||||
await page.waitForTimeout(2600);
|
||||
assert.equal(await demo.getAttribute('data-phase'), before, 'pause freezes the phase');
|
||||
for (const mode of ['direct', 'concurrent', 'auto']) {
|
||||
await page.locator('[data-run-mode=' + mode + ']').click();
|
||||
assert.equal(await demo.getAttribute('data-mode'), mode);
|
||||
assert.ok(await page.locator('.run-message').count() > 0);
|
||||
const tabStyle = await page.locator('[data-run-mode=' + mode + ']').evaluate(el => ({ background: getComputedStyle(el).backgroundColor, border: getComputedStyle(el).borderTopWidth }));
|
||||
assert.equal(tabStyle.background, 'rgba(0, 0, 0, 0)');
|
||||
assert.equal(tabStyle.border, '0px');
|
||||
assert.ok((await page.locator('[data-run-description]').textContent()).length > 60);
|
||||
await page.screenshot({ path: path.join(output, 'desktop-' + mode + '.png') });
|
||||
}
|
||||
await page.locator('[data-run-mode=direct]').focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
assert.equal(await demo.getAttribute('data-mode'), 'concurrent', 'tabs support arrow keys');
|
||||
await page.locator('[data-run-mode=auto]').click();
|
||||
await page.locator('[data-run-pause]').click();
|
||||
await page.waitForFunction(() => document.querySelector('[data-run-demo]').dataset.phase === '1');
|
||||
assert.equal(await page.locator('.run-mention').last().textContent(), '@Claude');
|
||||
await page.screenshot({ path: path.join(output, 'desktop-handoff.png') });
|
||||
await page.waitForFunction(() => document.querySelector('[data-run-demo]').dataset.phase === '2');
|
||||
assert.equal(await page.locator('.run-mention').last().textContent(), '@Gemini');
|
||||
await page.waitForFunction(() => document.querySelector('[data-run-demo]').dataset.phase === '3');
|
||||
assert.equal(await page.locator('.run-mention').last().textContent(), '@Codex');
|
||||
await page.locator('[data-run-pause]').click();
|
||||
|
||||
const question = page.locator('.faq-question');
|
||||
await question.nth(1).click();
|
||||
await page.waitForTimeout(350);
|
||||
assert.equal(await page.locator('.faq-item[open]').count(), 1);
|
||||
await question.nth(2).click();
|
||||
assert.ok(await page.locator('.faq-item').evaluateAll(items => items.some(item => item.getAnimations().length > 0)), 'FAQ animates');
|
||||
await page.waitForTimeout(350);
|
||||
assert.equal(await page.locator('.faq-item[open]').count(), 1);
|
||||
assert.equal(await page.locator('.faq-item').nth(2).getAttribute('open'), '');
|
||||
await question.nth(2).click();
|
||||
await question.nth(3).click();
|
||||
await question.nth(1).click();
|
||||
await page.waitForTimeout(350);
|
||||
assert.equal(await page.locator('.faq-item[open]').count(), 1, 'rapid clicks retain exclusivity');
|
||||
|
||||
const anchor = await page.evaluate(() => {
|
||||
const elements = Array.from(document.querySelectorAll('section[id] h2, section[id] p, .faq-question'));
|
||||
const index = elements.findIndex(el => { const r = el.getBoundingClientRect(); return r.top >= 100 && r.top < innerHeight && r.height > 0; });
|
||||
return { index, top: elements[index].getBoundingClientRect().top };
|
||||
});
|
||||
await page.locator('#localeToggle').click();
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(await page.locator('html').getAttribute('lang'), 'zh-CN');
|
||||
assert.equal(await page.evaluate(() => window.sameDocument), true, 'language does not reload');
|
||||
const after = await page.evaluate(index => document.querySelectorAll('section[id] h2, section[id] p, .faq-question')[index].getBoundingClientRect().top, anchor.index);
|
||||
assert.ok(Math.abs(after - anchor.top) < 3, 'reading anchor stays in place: ' + anchor.top + ' -> ' + after);
|
||||
assert.equal(await page.locator('.faq-item[open]').count(), 1);
|
||||
assert.equal(await demo.getAttribute('data-mode'), 'auto');
|
||||
await page.screenshot({ path: path.join(output, 'faq-zh.png') });
|
||||
|
||||
for (const [name, width, height] of [['desktop', 1440, 1000], ['tablet', 900, 1000], ['mobile', 390, 844], ['narrow', 320, 740]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.locator('#scenario').evaluate(el => window.scrollTo(0, el.offsetTop - 110));
|
||||
await page.waitForTimeout(300);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, name + ' has no overflow');
|
||||
const nav = await page.locator('#topnav').boundingBox();
|
||||
assert.ok(nav.x >= 10 && nav.x + nav.width <= width - 10, name + ' floating nav');
|
||||
const left = await page.locator('.nav-start').boundingBox();
|
||||
const right = await page.locator('.nav-actions').boundingBox();
|
||||
assert.ok(left.x + left.width < right.x, name + ' navigation groups do not overlap');
|
||||
assert.ok(right.x + right.width >= nav.x + nav.width - 20, name + ' actions align right');
|
||||
assert.equal(await page.locator('.nav-actions .cta-dark').isVisible(), true);
|
||||
assert.equal(await page.locator('.run-chat-avatar img').evaluateAll(images => images.length > 0 && images.every(img => img.complete && img.naturalWidth > 0)), true, 'logos load');
|
||||
const canvas = page.locator('#chapterCanvas');
|
||||
const pixels = await canvas.evaluate(c => {
|
||||
const gl = c.getContext('webgl2');
|
||||
const data = new Uint8Array(c.width * c.height * 4);
|
||||
gl.readPixels(0, 0, c.width, c.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
let count = 0;
|
||||
for (let i = 3; i < data.length; i += 4) if (data[i]) count++;
|
||||
return count;
|
||||
});
|
||||
assert.ok(pixels > 100, name + ' background is nonblank');
|
||||
const first = await canvas.evaluate(c => c.toDataURL());
|
||||
await page.waitForTimeout(500);
|
||||
assert.notEqual(await canvas.evaluate(c => c.toDataURL()), first, name + ' background moves');
|
||||
await page.screenshot({ path: path.join(output, name + '-zh.png') });
|
||||
await page.locator('#localeToggle').click();
|
||||
await page.screenshot({ path: path.join(output, name + '-en.png') });
|
||||
await page.locator('#themeToggle').click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(output, name + '-light.png') });
|
||||
await page.locator('#themeToggle').click();
|
||||
await page.locator('#localeToggle').click();
|
||||
}
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.waitForFunction(() => document.querySelector('[data-run-pause]').hidden);
|
||||
assert.equal(await page.locator('[data-run-pause]').isVisible(), false);
|
||||
assert.equal(await demo.getAttribute('data-phase'), '4');
|
||||
const reduced = await page.locator('#chapterCanvas').evaluate(c => c.toDataURL());
|
||||
await page.waitForTimeout(600);
|
||||
assert.equal(await page.locator('#chapterCanvas').evaluate(c => c.toDataURL()), reduced);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: FAQ, locale anchor, release UI, mode tabs, @handoff, pause, reduced motion, responsive canvases and logos.');
|
||||
console.log('Screenshots: ' + output);
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,211 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { JSDOM } = require('../../frontend/node_modules/jsdom');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
|
||||
const locale = fs.readFileSync(path.join(root, 'locale.js'), 'utf8');
|
||||
|
||||
function page(query = '') {
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'file:///landing/index.html' + query,
|
||||
runScripts: 'outside-only',
|
||||
});
|
||||
dom.window.eval(locale);
|
||||
return dom;
|
||||
}
|
||||
|
||||
test('explicit Chinese mode retains links, emphasis, and animation hooks', () => {
|
||||
const before = new JSDOM(html).window.document;
|
||||
const { document } = page('?lang=zh').window;
|
||||
assert.equal(document.documentElement.lang, 'zh-CN');
|
||||
for (const selector of ['strong', 'svg', 'a', '[data-split]', '[data-card-swap]']) {
|
||||
assert.equal(document.querySelectorAll(selector).length, before.querySelectorAll(selector).length);
|
||||
}
|
||||
assert.match(document.querySelector('#headline').textContent, /多 Agent/);
|
||||
assert.equal(document.querySelector('#boundary-title').textContent, '工作空间在本地,权限边界说清楚。');
|
||||
assert.match(document.querySelector('[data-decrypt]').dataset.text, /本地优先/);
|
||||
assert.equal(document.querySelector('#download .cta-primary').href, before.querySelector('#download .cta-primary').href);
|
||||
});
|
||||
|
||||
test('English is the default and can switch to Chinese with its anchor', () => {
|
||||
const { document, MeldworkLocale } = page('#faq').window;
|
||||
assert.equal(document.documentElement.lang, 'en');
|
||||
assert.match(document.querySelector('#headline').textContent, /Multi-Agent Work,/);
|
||||
assert.equal(MeldworkLocale.t('Round 1 · Propose'), 'Round 1 · Propose');
|
||||
assert.equal(document.querySelector('#localeToggle').href, 'file:///landing/index.html?lang=zh#faq');
|
||||
});
|
||||
|
||||
test('mode and motion labels use the selected language', () => {
|
||||
const { MeldworkLocale } = page('?lang=zh').window;
|
||||
assert.equal(MeldworkLocale.t('Direct'), '直接会话');
|
||||
assert.equal(MeldworkLocale.t('Pause background animation'), '暂停背景动画');
|
||||
assert.equal(MeldworkLocale.t('Resume background animation'), '继续背景动画');
|
||||
});
|
||||
|
||||
test('language switches in place, round trips copy, and preserves open FAQ state', () => {
|
||||
const dom = page('?lang=en#faq');
|
||||
const win = dom.window;
|
||||
win.scrollTo = () => {};
|
||||
const answer = win.document.querySelector('.faq-item');
|
||||
answer.open = true;
|
||||
const original = win.document.querySelector('#headline').textContent;
|
||||
win.document.getElementById('localeToggle').click();
|
||||
assert.equal(win.document.documentElement.lang, 'zh-CN');
|
||||
assert.equal(answer.open, true);
|
||||
assert.match(win.document.querySelector('#headline').textContent, /多 Agent/);
|
||||
win.document.getElementById('localeToggle').click();
|
||||
assert.equal(win.document.documentElement.lang, 'en');
|
||||
assert.equal(win.document.querySelector('#headline').textContent, original);
|
||||
assert.equal(win.location.hash, '#faq');
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
test('release metadata and download links target V1.0.4 and its actual DMG asset', () => {
|
||||
assert.doesNotMatch(html + locale, /1\.0\.3|0\.1\.3/);
|
||||
assert.match(html, /Meldwork-V1\.0\.4\/Meldwork-0\.1\.4-arm64\.dmg/);
|
||||
assert.match(html, /"softwareVersion": "1\.0\.4"/);
|
||||
});
|
||||
|
||||
test('FAQ structured answers match the visible Chinese answers', () => {
|
||||
const { document } = page('?lang=zh').window;
|
||||
const schema = JSON.parse(document.querySelector('script[type="application/ld+json"]').textContent);
|
||||
const faq = schema['@graph'].find(item => item['@type'] === 'FAQPage');
|
||||
const items = Array.from(document.querySelectorAll('.faq-item'));
|
||||
assert.equal(faq.mainEntity.length, items.length);
|
||||
items.forEach((item, index) => {
|
||||
assert.equal(faq.mainEntity[index].name, item.querySelector('summary').textContent.trim());
|
||||
assert.equal(faq.mainEntity[index].acceptedAnswer.text, item.querySelector('.faq-answer').textContent.trim().replace(/\s+/g, ' '));
|
||||
});
|
||||
});
|
||||
|
||||
test('chat demos distinguish direct follow-ups, concurrent replies and Agent mentions', () => {
|
||||
const dom = page();
|
||||
const win = dom.window;
|
||||
win.scrollTo = () => {};
|
||||
let advance;
|
||||
win.setTimeout = callback => { advance = callback; return 1; };
|
||||
win.clearTimeout = () => {};
|
||||
win.matchMedia = () => ({ matches: false, addEventListener() {} });
|
||||
Object.defineProperty(win.document, 'hidden', { value: false });
|
||||
win.IntersectionObserver = class { constructor(callback) { this.callback = callback; } observe() { this.callback([{ isIntersecting: true }]); } };
|
||||
win.eval(fs.readFileSync(path.join(root, 'run-demo.js'), 'utf8'));
|
||||
const demo = win.document.querySelector('[data-run-demo]');
|
||||
function select(mode) { demo.querySelector('[data-run-mode=' + mode + ']').click(); }
|
||||
select('direct');
|
||||
advance();
|
||||
assert.equal(demo.querySelectorAll('.run-typing').length, 1);
|
||||
advance(); advance(); advance();
|
||||
assert.match(demo.textContent, /What should we measure/);
|
||||
assert.equal(demo.querySelectorAll('[data-speaker="Claude"]').length, 0);
|
||||
select('concurrent');
|
||||
advance();
|
||||
assert.equal(demo.querySelectorAll('.run-typing').length, 3);
|
||||
advance();
|
||||
assert.equal(demo.querySelectorAll('.run-message:not(.is-user)').length, 3);
|
||||
assert.equal(demo.querySelectorAll('.run-mention').length, 0);
|
||||
select('auto');
|
||||
advance(); advance(); advance();
|
||||
assert.deepEqual(Array.from(demo.querySelectorAll('.run-mention'), el => el.textContent), ['@Claude', '@Gemini', '@Codex']);
|
||||
demo.querySelector('[data-run-pause]').click();
|
||||
win.document.getElementById('localeToggle').click();
|
||||
assert.equal(demo.dataset.phase, '3');
|
||||
assert.match(demo.textContent, /需要哪些证据/);
|
||||
assert.equal(demo.querySelectorAll('.run-message.is-new').length, 0);
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
function scenePage(reduced = false) {
|
||||
const dom = page();
|
||||
const win = dom.window;
|
||||
const callbacks = new Map();
|
||||
const uniforms = {};
|
||||
let id = 0;
|
||||
let rendered = null;
|
||||
Object.defineProperty(win.document, 'hidden', { value: false, configurable: true });
|
||||
Object.defineProperty(win.document.querySelector('.hero'), 'offsetHeight', { value: 900 });
|
||||
win.scrollY = 1200;
|
||||
win.Math.random = () => 0.3;
|
||||
win.matchMedia = () => ({ matches: reduced, addEventListener() {} });
|
||||
win.ResizeObserver = class { observe() {} };
|
||||
win.requestAnimationFrame = callback => { callbacks.set(++id, callback); return id; };
|
||||
win.cancelAnimationFrame = frame => callbacks.delete(frame);
|
||||
const gl = new Proxy({
|
||||
getShaderParameter: () => true,
|
||||
getProgramParameter: () => true,
|
||||
getUniformLocation: (_, name) => name,
|
||||
uniform1f: (name, value) => { uniforms[name] = value; },
|
||||
drawArrays: () => { rendered = { ...uniforms }; },
|
||||
}, { get: (target, key) => target[key] || (() => ({})) });
|
||||
win.HTMLCanvasElement.prototype.getContext = type => { assert.equal(type, 'webgl2'); return gl; };
|
||||
win.eval(fs.readFileSync(path.join(root, 'chapter-scene.js'), 'utf8'));
|
||||
return {
|
||||
dom,
|
||||
rendered: () => rendered,
|
||||
pending: () => callbacks.size,
|
||||
tick(time) {
|
||||
const pending = Array.from(callbacks.values());
|
||||
callbacks.clear();
|
||||
pending.forEach(callback => callback(time));
|
||||
},
|
||||
pointer(x, y) {
|
||||
win.dispatchEvent(new win.MouseEvent('pointermove', { clientX: x, clientY: y }));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('landing uses the exact CLI loading fragment shader and visual parameters', () => {
|
||||
const desktop = fs.readFileSync(path.join(root, '../frontend/src/components/PixelBlast.vue'), 'utf8');
|
||||
const landing = fs.readFileSync(path.join(root, 'chapter-scene.js'), 'utf8');
|
||||
const shader = /(?:const|var) FRAGMENT_SHADER = `([\s\S]*?)`/;
|
||||
assert.equal(landing.match(shader)[1], desktop.match(shader)[1]);
|
||||
assert.doesNotMatch(landing, /pointermove|mousemove|bulgeStrength|cursorRadius/);
|
||||
const scene = scenePage();
|
||||
assert.equal(scene.rendered().uScale, 2.2);
|
||||
assert.equal(scene.rendered().uDensity, 1.26);
|
||||
assert.equal(scene.rendered().uOpacity, 0.42);
|
||||
assert.equal(scene.rendered().uPixelSize, 3.7);
|
||||
scene.dom.window.close();
|
||||
});
|
||||
|
||||
test('PixelBlast advances without mouse input at CLI loading speed; scroll does not reset its clock', () => {
|
||||
const scene = scenePage();
|
||||
const win = scene.dom.window;
|
||||
const initial = scene.rendered().uTime;
|
||||
scene.tick(1000);
|
||||
scene.tick(1016);
|
||||
assert.ok(Math.abs(scene.rendered().uTime - initial - 16 * 0.00036) < 1e-8);
|
||||
const beforeScroll = scene.rendered();
|
||||
win.scrollY += 200;
|
||||
win.dispatchEvent(new win.Event('scroll'));
|
||||
scene.pointer(400, 300);
|
||||
assert.deepEqual(scene.rendered(), beforeScroll);
|
||||
scene.tick(1032);
|
||||
assert.ok(Math.abs(scene.rendered().uTime - initial - 32 * 0.00036) < 1e-8);
|
||||
win.document.getElementById('chapterMotionToggle').click();
|
||||
const paused = scene.rendered();
|
||||
scene.pointer(500, 400);
|
||||
scene.tick(2000);
|
||||
assert.deepEqual(scene.rendered(), paused);
|
||||
assert.equal(scene.pending(), 0);
|
||||
win.document.getElementById('chapterMotionToggle').click();
|
||||
assert.equal(scene.pending(), 1);
|
||||
win.scrollY = 0;
|
||||
win.dispatchEvent(new win.Event('scroll'));
|
||||
assert.equal(scene.pending(), 0);
|
||||
scene.dom.window.close();
|
||||
});
|
||||
|
||||
test('PixelBlast reduced-motion mode renders once and schedules no frames', () => {
|
||||
const scene = scenePage(true);
|
||||
const initial = scene.rendered();
|
||||
assert.ok(initial);
|
||||
scene.pointer(200, 200);
|
||||
scene.tick(1000);
|
||||
assert.deepEqual(scene.rendered(), initial);
|
||||
assert.equal(scene.pending(), 0);
|
||||
assert.equal(scene.dom.window.document.getElementById('chapterMotionToggle').hidden, true);
|
||||
scene.dom.window.close();
|
||||
});
|
||||
Reference in new issue
Block a user