mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-02 06:09:52 +08:00
feat(engine-select): Monolith 对角线全屏选择屏
替换原卡片网格为「左上 OpenClaw(石墨黑)vs 右下 Hermes(象牙白)」对角线全屏设计。
启动屏 / 引擎切换时给用户一个有冲击力的「选择时刻」。
## 核心设计
- position: fixed 跳出 #content 范围,覆盖整个 viewport(含 sidebar)
- 双三角形 clip-path: polygon (0 0, 0 100%, 100% 0) / (100% 0, 100% 100%, 0 100%)
- 内容用 absolute 定位到三角形质心(左上 11%/6.5% / 右下 11%/6.5%),永不重叠
- 微妙的 60px 网格纹 + 角落极光(紫蓝 / 橙金)+ 极细中线分割
- clamp(80px, 13vw, 200px) 巨字标题 + 序号 + Logo + tagline + 特性列表 + CTA
## 交互
- hover 联动:用 [data-hover] attribute 替代 :has(),兼容旧 WebKit
- 鼠标悬停一侧 → 该侧亮起 + 内容平移 + CTA 反白;另一侧变暗 + 内容模糊缩小
- 点击三角形 → 三角形 clip-path 扩满(0.8s)→ 中心圆扩散(0.9s)→ 进入主页
- reveal 节点 attach 到 body,跨路由切换存活,新页面渲染后再淡出
## Both / Later 处理
- 两个次级选项保留,做成底部居中的玻璃 pill 链接(不抢戏)
- 不走对角线扩散动画,点击后直接 applyEngineSelection + navigate
## 兼容性
- prefers-reduced-motion: reduce → 关闭所有动画
- 移动端响应式:< 760px 调整字号 / 边距 / 角标
- 用 Vite define 注入的 __APP_VERSION__ 显示版本号(与 main.js / sidebar.js 一致)
## i18n
- engine.choiceTopBanner / choiceCtaEnter
- choiceOpenclaw{Tagline,Feat1,Feat2,Feat3,Category}
- choiceHermes{Tagline,Feat1,Feat2,Feat3,Category}
- choiceSecondary{Both,Later}
- 三语完整(zh-CN / en / zh-TW)
## 抽卡 prototype
保留 docs/engine-select-mockups/ 下的 V2 4 张设计 + 索引页(v2-monolith.html
即本次接入的最终版本)。
This commit is contained in:
@@ -29,6 +29,21 @@ export default {
|
||||
choiceLaterDesc: _('先停留在引擎选择页,不立即安装任何引擎。准备好后再选择即可。', 'Stay on the engine chooser without installing an engine now. Pick one when you are ready.', '先停留在引擎選擇頁,不立即安裝任何引擎。準備好後再選擇即可。'),
|
||||
choiceLaterMeta: _('不会启动安装流程', 'No setup flow will start', '不會啟動安裝流程'),
|
||||
choiceLaterBadge: _('跳过', 'Skip', '跳過'),
|
||||
// —— Monolith 启动选择屏(对角线分割版) ——
|
||||
choiceTopBanner: _('— 选择你的引擎 —', '— Choose your engine —', '— 選擇你的引擎 —'),
|
||||
choiceCtaEnter: _('立即进入', 'Enter', '立即進入'),
|
||||
choiceOpenclawTagline: _('从模型到智能体,一站式打造你的 AI 工作台。', 'From models to agents — your full AI workbench in one panel.', '從模型到智能體,一站式打造你的 AI 工作台。'),
|
||||
choiceOpenclawFeat1: _('多模型 / 多渠道并行管理', 'Multi-model · multi-channel orchestration', '多模型 / 多渠道並行管理'),
|
||||
choiceOpenclawFeat2: _('持久化记忆 + 上下文工程', 'Persistent memory + context engineering', '持久化記憶 + 上下文工程'),
|
||||
choiceOpenclawFeat3: _('无代码搭建智能体', 'No-code agent builder', '無程式碼搭建智能體'),
|
||||
choiceOpenclawCategory: _('通用助理', 'Universal Assistant', '通用助理'),
|
||||
choiceHermesTagline: _('让 Agent 真正能干活。工具调用、Profile、Kanban 一应俱全。', 'Make agents actually work — tool calling, profiles, Kanban built-in.', '讓 Agent 真正能幹活。工具調用、Profile、Kanban 一應俱全。'),
|
||||
choiceHermesFeat1: _('原生工具调用 + Approval Flow', 'Native tool calling + approval flow', '原生工具調用 + Approval Flow'),
|
||||
choiceHermesFeat2: _('多 Profile 隔离 + 多 Gateway', 'Multi-profile isolation + multi-gateway', '多 Profile 隔離 + 多 Gateway'),
|
||||
choiceHermesFeat3: _('内置 Kanban / Skills / OAuth', 'Kanban / Skills / OAuth built-in', '內建 Kanban / Skills / OAuth'),
|
||||
choiceHermesCategory: _('Agent 工作流', 'Agent Workflow', 'Agent 工作流'),
|
||||
choiceSecondaryBoth: _('两个都要 ↗', 'Use both ↗', '兩個都要 ↗'),
|
||||
choiceSecondaryLater: _('稍后再说', 'Decide later', '稍後再說'),
|
||||
hermesSetupDesc: _('安装并配置 Hermes Agent', 'Install and configure Hermes Agent', '安裝並配置 Hermes Agent'),
|
||||
hermesPhaseClickHint: _('点击可返回此步骤', 'Click to go back to this step', '點擊可返回此步驟', 'このステップに戻るにはクリック', '이 단계로 돌아가려면 클릭'),
|
||||
hermesSetupIntro: _(
|
||||
|
||||
@@ -3,27 +3,24 @@ import { t } from '../lib/i18n.js'
|
||||
import { applyEngineSelection } from '../lib/engine-manager.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
|
||||
const OPTIONS = [
|
||||
const PRIMARY_OPTIONS = [
|
||||
{
|
||||
id: 'openclaw',
|
||||
key: 'Openclaw',
|
||||
icon: 'layers',
|
||||
activeEngineId: 'openclaw',
|
||||
enabledEngineIds: ['openclaw'],
|
||||
targetRoute: '/setup',
|
||||
},
|
||||
{
|
||||
id: 'hermes',
|
||||
key: 'Hermes',
|
||||
icon: 'bolt',
|
||||
activeEngineId: 'hermes',
|
||||
enabledEngineIds: ['hermes'],
|
||||
targetRoute: '/h/setup',
|
||||
},
|
||||
]
|
||||
|
||||
const SECONDARY_OPTIONS = [
|
||||
{
|
||||
id: 'both',
|
||||
key: 'Both',
|
||||
icon: 'spark',
|
||||
activeEngineId: 'openclaw',
|
||||
enabledEngineIds: ['openclaw', 'hermes'],
|
||||
engineMode: 'both',
|
||||
@@ -31,8 +28,6 @@ const OPTIONS = [
|
||||
},
|
||||
{
|
||||
id: 'later',
|
||||
key: 'Later',
|
||||
icon: 'clock',
|
||||
activeEngineId: 'openclaw',
|
||||
enabledEngineIds: [],
|
||||
deferred: true,
|
||||
@@ -41,83 +36,212 @@ const OPTIONS = [
|
||||
]
|
||||
|
||||
const ICONS = {
|
||||
layers: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
||||
bolt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>',
|
||||
spark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2l1.7 6.3L20 10l-6.3 1.7L12 18l-1.7-6.3L4 10l6.3-1.7L12 2z"/><path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15z"/></svg>',
|
||||
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
|
||||
openclaw: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
||||
hermes: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>',
|
||||
}
|
||||
|
||||
let _busy = false
|
||||
let _revealEl = null
|
||||
let _homeEl = null
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page engine-select-page'
|
||||
page.className = 'page engine-select-page es-monolith'
|
||||
page.innerHTML = `
|
||||
<section class="engine-select-hero">
|
||||
<div class="engine-select-kicker">${esc(t('engine.choiceKicker'))}</div>
|
||||
<h1>${esc(t('engine.choiceTitle'))}</h1>
|
||||
<p>${esc(t('engine.choiceSubtitle'))}</p>
|
||||
</section>
|
||||
<section class="engine-choice-grid">
|
||||
${OPTIONS.map(renderOption).join('')}
|
||||
</section>
|
||||
<section class="engine-choice-note">
|
||||
<div class="engine-choice-note-title">${esc(t('engine.choiceNoteTitle'))}</div>
|
||||
<div>${esc(t('engine.choiceNoteDesc'))}</div>
|
||||
</section>
|
||||
<div class="es-stage">
|
||||
<div class="es-panel es-panel-openclaw" data-engine="openclaw">
|
||||
<div class="es-glow es-glow-openclaw"></div>
|
||||
</div>
|
||||
<div class="es-panel es-panel-hermes" data-engine="hermes">
|
||||
<div class="es-glow es-glow-hermes"></div>
|
||||
</div>
|
||||
<div class="es-divider"></div>
|
||||
|
||||
<div class="es-top-banner">${esc(t('engine.choiceTopBanner'))}</div>
|
||||
<div class="es-corner-mark es-corner-tl">CLAWPANEL</div>
|
||||
<div class="es-corner-mark es-corner-br" data-version-tag>v—</div>
|
||||
|
||||
${renderContent('openclaw')}
|
||||
${renderContent('hermes')}
|
||||
|
||||
<div class="es-secondary">
|
||||
<button type="button" class="es-secondary-link" data-secondary="both">${esc(t('engine.choiceSecondaryBoth'))}</button>
|
||||
<span class="es-secondary-sep" aria-hidden="true">·</span>
|
||||
<button type="button" class="es-secondary-link" data-secondary="later">${esc(t('engine.choiceSecondaryLater'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
page.addEventListener('click', async (event) => {
|
||||
const card = event.target.closest('.engine-choice-card')
|
||||
if (!card) return
|
||||
const option = OPTIONS.find(item => item.id === card.dataset.choice)
|
||||
if (!option || card.classList.contains('loading')) return
|
||||
await chooseOption(page, card, option)
|
||||
})
|
||||
// 注入版本号(package.json 同步,Vite define 注入)
|
||||
try {
|
||||
const v = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : ''
|
||||
const tag = page.querySelector('[data-version-tag]')
|
||||
if (tag && v) tag.textContent = `v${v}`
|
||||
} catch (_) {}
|
||||
|
||||
bindHover(page)
|
||||
bindClick(page)
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
function renderOption(option) {
|
||||
const badge = t(`engine.choice${option.key}Badge`)
|
||||
function renderContent(id) {
|
||||
const num = id === 'openclaw' ? '01' : '02'
|
||||
const cap = id === 'openclaw' ? 'OpenClaw' : 'Hermes'
|
||||
const cat = id === 'openclaw' ? t('engine.choiceOpenclawCategory') : t('engine.choiceHermesCategory')
|
||||
const tagline = id === 'openclaw' ? t('engine.choiceOpenclawTagline') : t('engine.choiceHermesTagline')
|
||||
const feats = id === 'openclaw'
|
||||
? [t('engine.choiceOpenclawFeat1'), t('engine.choiceOpenclawFeat2'), t('engine.choiceOpenclawFeat3')]
|
||||
: [t('engine.choiceHermesFeat1'), t('engine.choiceHermesFeat2'), t('engine.choiceHermesFeat3')]
|
||||
const cta = `${t('engine.choiceCtaEnter')} ${cap}`
|
||||
|
||||
// OpenClaw(左上):序号在前 / Hermes(右下):序号在后
|
||||
const productRow = id === 'openclaw'
|
||||
? `<span class="es-product-icon">${ICONS[id]}</span><span class="es-product-tag">${esc(num)} · ${esc(cat)}</span>`
|
||||
: `<span class="es-product-tag">${esc(cat)} · ${esc(num)}</span><span class="es-product-icon">${ICONS[id]}</span>`
|
||||
|
||||
return `
|
||||
<button class="engine-choice-card" data-choice="${option.id}">
|
||||
<span class="engine-choice-icon">${ICONS[option.icon] || ''}</span>
|
||||
<span class="engine-choice-content">
|
||||
<span class="engine-choice-title-row">
|
||||
<span class="engine-choice-title">${esc(t(`engine.choice${option.key}Title`))}</span>
|
||||
${badge && badge !== `engine.choice${option.key}Badge` ? `<span class="engine-choice-badge">${esc(badge)}</span>` : ''}
|
||||
</span>
|
||||
<span class="engine-choice-desc">${esc(t(`engine.choice${option.key}Desc`))}</span>
|
||||
<span class="engine-choice-meta">${esc(t(`engine.choice${option.key}Meta`))}</span>
|
||||
</span>
|
||||
<span class="engine-choice-arrow">→</span>
|
||||
</button>
|
||||
<div class="es-content es-content-${id}" data-engine-content="${id}">
|
||||
<div class="es-product-row">${productRow}</div>
|
||||
<div class="es-title">${esc(cap)}</div>
|
||||
<div class="es-tagline">${esc(tagline)}</div>
|
||||
<ul class="es-feature-list">
|
||||
${feats.map(f => `<li>${esc(f)}</li>`).join('')}
|
||||
</ul>
|
||||
<button type="button" class="es-cta" data-engine-cta="${id}" tabindex="-1">
|
||||
${id === 'openclaw' ? `<span>${esc(cta)}</span><span class="es-cta-arrow">→</span>` : `<span class="es-cta-arrow">→</span><span>${esc(cta)}</span>`}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
async function chooseOption(page, card, option) {
|
||||
setBusy(page, card, true)
|
||||
try {
|
||||
await applyEngineSelection({
|
||||
activeEngineId: option.activeEngineId,
|
||||
enabledEngineIds: option.enabledEngineIds,
|
||||
deferred: !!option.deferred,
|
||||
choice: option.id,
|
||||
engineMode: option.engineMode || '',
|
||||
function bindHover(page) {
|
||||
// 用 attribute 替代 :has() — 兼容性更好(旧 WebKit / Linux WebKitGTK)
|
||||
const stage = page.querySelector('.es-stage')
|
||||
page.querySelectorAll('.es-panel').forEach(panel => {
|
||||
const engine = panel.dataset.engine
|
||||
panel.addEventListener('mouseenter', () => {
|
||||
if (_busy) return
|
||||
stage.dataset.hover = engine
|
||||
})
|
||||
toast(t('engine.choiceSaved'), 'success')
|
||||
navigate(option.targetRoute)
|
||||
} catch (error) {
|
||||
console.error('[engine-select] choose failed:', error)
|
||||
toast(t('engine.choiceSaveFailed'), 'error')
|
||||
setBusy(page, card, false)
|
||||
panel.addEventListener('mouseleave', () => {
|
||||
if (_busy) return
|
||||
delete stage.dataset.hover
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function bindClick(page) {
|
||||
const stage = page.querySelector('.es-stage')
|
||||
|
||||
// 主区:点击三角形选引擎
|
||||
stage.addEventListener('click', (event) => {
|
||||
if (_busy) return
|
||||
const panel = event.target.closest('.es-panel')
|
||||
if (!panel) return
|
||||
const engine = panel.dataset.engine
|
||||
const option = PRIMARY_OPTIONS.find(o => o.id === engine)
|
||||
if (option) chooseWithAnimation(page, panel, option, engine)
|
||||
})
|
||||
|
||||
// 次级链接:两个都要 / 稍后再说(无对角线动画,直接走选择)
|
||||
page.querySelectorAll('[data-secondary]').forEach(btn => {
|
||||
btn.addEventListener('click', async (event) => {
|
||||
event.stopPropagation()
|
||||
if (_busy) return
|
||||
const id = btn.dataset.secondary
|
||||
const option = SECONDARY_OPTIONS.find(o => o.id === id)
|
||||
if (!option) return
|
||||
_busy = true
|
||||
btn.classList.add('loading')
|
||||
try {
|
||||
await applyEngineSelection({
|
||||
activeEngineId: option.activeEngineId,
|
||||
enabledEngineIds: option.enabledEngineIds,
|
||||
deferred: !!option.deferred,
|
||||
choice: option.id,
|
||||
engineMode: option.engineMode || '',
|
||||
})
|
||||
toast(t('engine.choiceSaved'), 'success')
|
||||
navigate(option.targetRoute)
|
||||
} catch (error) {
|
||||
console.error('[engine-select] secondary choose failed:', error)
|
||||
toast(t('engine.choiceSaveFailed'), 'error')
|
||||
_busy = false
|
||||
btn.classList.remove('loading')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function chooseWithAnimation(page, panel, option, engine) {
|
||||
_busy = true
|
||||
const stage = page.querySelector('.es-stage')
|
||||
delete stage.dataset.hover
|
||||
stage.dataset.expanding = engine
|
||||
|
||||
// 先把 reveal / home mock 节点 attach 到 body — 路由切换时它们不会被销毁
|
||||
ensureRevealNodes()
|
||||
|
||||
// 阶段 1: 三角形扩满(CSS 通过 [data-expanding] 触发 clip-path 变化)
|
||||
// 阶段 2: 600ms 后开始中心圆扩散
|
||||
setTimeout(() => {
|
||||
_revealEl.dataset.engine = engine
|
||||
_revealEl.classList.add('es-reveal-active')
|
||||
}, 600)
|
||||
|
||||
// 阶段 3: 1300ms 后保存选择 + 切换路由
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await applyEngineSelection({
|
||||
activeEngineId: option.activeEngineId,
|
||||
enabledEngineIds: option.enabledEngineIds,
|
||||
deferred: !!option.deferred,
|
||||
choice: option.id,
|
||||
engineMode: option.engineMode || '',
|
||||
})
|
||||
navigate(option.targetRoute)
|
||||
// 给新页面一点渲染时间后淡出 reveal 层
|
||||
setTimeout(() => {
|
||||
if (_revealEl) {
|
||||
_revealEl.classList.add('es-reveal-fadeout')
|
||||
setTimeout(() => removeRevealNodes(), 600)
|
||||
}
|
||||
}, 280)
|
||||
} catch (error) {
|
||||
console.error('[engine-select] choose failed:', error)
|
||||
toast(t('engine.choiceSaveFailed'), 'error')
|
||||
// 失败回退:移除动画层 + 解除 busy
|
||||
removeRevealNodes()
|
||||
delete stage.dataset.expanding
|
||||
_busy = false
|
||||
}
|
||||
}, 1300)
|
||||
}
|
||||
|
||||
function ensureRevealNodes() {
|
||||
if (!_revealEl) {
|
||||
_revealEl = document.createElement('div')
|
||||
_revealEl.className = 'es-reveal'
|
||||
document.body.appendChild(_revealEl)
|
||||
}
|
||||
if (!_homeEl) {
|
||||
_homeEl = document.createElement('div')
|
||||
_homeEl.className = 'es-reveal-home'
|
||||
document.body.appendChild(_homeEl)
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(page, activeCard, busy) {
|
||||
page.querySelectorAll('.engine-choice-card').forEach(card => {
|
||||
card.disabled = busy
|
||||
card.classList.toggle('loading', busy && card === activeCard)
|
||||
})
|
||||
function removeRevealNodes() {
|
||||
if (_revealEl) { _revealEl.remove(); _revealEl = null }
|
||||
if (_homeEl) { _homeEl.remove(); _homeEl = null }
|
||||
_busy = false
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
// 路由切走时不主动销毁 reveal 节点(动画完成后会自行淡出)
|
||||
// 这里仅重置 busy(防卡死)
|
||||
_busy = false
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
|
||||
@@ -2657,189 +2657,399 @@
|
||||
engines/hermes/style/hermes.css)。原来 .hm-memory-* 那一坨规则已无任何
|
||||
JS / CSS 引用,已在此处删除。 */
|
||||
|
||||
.engine-select-page {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 32px;
|
||||
/* === Engine Select · Monolith 对角线全屏 ===
|
||||
左上三角 OpenClaw(石墨黑)vs 右下三角 Hermes(象牙白)。
|
||||
用 position: fixed 跳出 #content 范围,覆盖整个 viewport(含 sidebar)。 */
|
||||
|
||||
.engine-select-page.es-monolith {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
overflow: hidden;
|
||||
background: #f5f3ee; /* 兜底色 */
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
animation: es-fadein 0.5s ease;
|
||||
}
|
||||
@keyframes es-fadein { from { opacity: 0 } to { opacity: 1 } }
|
||||
|
||||
.es-stage { position: absolute; inset: 0 }
|
||||
|
||||
/* —— 两个三角形 —— */
|
||||
.es-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
cursor: pointer;
|
||||
transition: filter 0.6s ease, clip-path 0.8s cubic-bezier(0.7, 0, 0.3, 1);
|
||||
}
|
||||
.es-panel-openclaw {
|
||||
clip-path: polygon(0 0, 0 100%, 100% 0);
|
||||
background: #0c0d12;
|
||||
}
|
||||
/* 微妙网格纹(OpenClaw 侧) */
|
||||
.es-panel-openclaw::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
background-position: -1px -1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.engine-select-hero {
|
||||
max-width: 720px;
|
||||
.es-panel-hermes {
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 100%);
|
||||
background: #f5f3ee;
|
||||
}
|
||||
.es-panel-hermes::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.04) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
background-position: -1px -1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* —— 角落极光发光 —— */
|
||||
.es-glow {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.es-glow-openclaw {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background: radial-gradient(circle, rgba(110, 100, 255, 0.4) 0%, transparent 60%);
|
||||
filter: blur(40px);
|
||||
clip-path: polygon(0 0, 100% 0, 0 100%);
|
||||
}
|
||||
.es-glow-hermes {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background: radial-gradient(circle, rgba(220, 130, 60, 0.28) 0%, transparent 60%);
|
||||
filter: blur(40px);
|
||||
clip-path: polygon(100% 100%, 0 100%, 100% 0);
|
||||
}
|
||||
|
||||
/* —— 中线(极细发光) —— */
|
||||
.es-divider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg, transparent calc(50% - 0.5px), rgba(180, 180, 180, 0.6) 50%, transparent calc(50% + 0.5px));
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
/* —— 顶部 banner + 角标 —— */
|
||||
.es-top-banner {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(180, 180, 180, 0.78);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.es-corner-mark {
|
||||
position: absolute;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.32em;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.es-corner-tl { top: 32px; left: 36px; color: rgba(255, 255, 255, 0.45) }
|
||||
.es-corner-br { bottom: 32px; right: 36px; color: rgba(0, 0, 0, 0.45) }
|
||||
|
||||
/* —— 内容定位(重心放在三角形质心) —— */
|
||||
.es-content {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.5s, filter 0.5s;
|
||||
}
|
||||
.es-content-openclaw {
|
||||
top: 11%;
|
||||
left: 6.5%;
|
||||
color: #fafafa;
|
||||
text-align: left;
|
||||
max-width: 44vw;
|
||||
}
|
||||
.es-content-hermes {
|
||||
bottom: 11%;
|
||||
right: 6.5%;
|
||||
color: #18171a;
|
||||
text-align: right;
|
||||
max-width: 44vw;
|
||||
}
|
||||
|
||||
/* —— hover 联动(用 [data-hover] attribute 替代 :has()) —— */
|
||||
.es-stage[data-hover='openclaw'] .es-panel-hermes { filter: brightness(0.94) saturate(0.85) }
|
||||
.es-stage[data-hover='hermes'] .es-panel-openclaw { filter: brightness(0.6) }
|
||||
.es-stage[data-hover='openclaw'] .es-content-hermes,
|
||||
.es-stage[data-hover='hermes'] .es-content-openclaw {
|
||||
opacity: 0.25;
|
||||
filter: blur(2px);
|
||||
}
|
||||
.es-stage[data-hover='openclaw'] .es-content-openclaw { transform: translateX(8px) }
|
||||
.es-stage[data-hover='hermes'] .es-content-hermes { transform: translateX(-8px) }
|
||||
|
||||
/* —— 顶部 logo + 序号 —— */
|
||||
.es-product-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.es-content-hermes .es-product-row { flex-direction: row-reverse }
|
||||
|
||||
.es-product-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.es-content-hermes .es-product-icon {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.es-product-icon svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.6 }
|
||||
|
||||
.es-product-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* —— 巨字标题 —— */
|
||||
.es-title {
|
||||
font-size: clamp(80px, 13vw, 200px);
|
||||
font-weight: 200;
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 0.92;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.engine-select-kicker {
|
||||
/* —— 副标题 —— */
|
||||
.es-tagline {
|
||||
font-size: clamp(20px, 1.8vw, 28px);
|
||||
line-height: 1.4;
|
||||
font-weight: 300;
|
||||
max-width: 540px;
|
||||
margin-bottom: 32px;
|
||||
opacity: 0.78;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.es-content-hermes .es-tagline { margin-left: auto }
|
||||
|
||||
/* —— 特性列表 —— */
|
||||
.es-feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.es-content-hermes .es-feature-list { align-items: flex-end }
|
||||
.es-feature-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.es-content-hermes .es-feature-list li { flex-direction: row-reverse }
|
||||
.es-feature-list li::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* —— CTA 按钮 —— */
|
||||
.es-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
background: var(--bg-glass);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.engine-select-hero h1 {
|
||||
margin: 0 0 12px;
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(28px, 4vw, 44px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.engine-select-hero p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.engine-choice-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.engine-choice-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: 172px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 22px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(99, 102, 241, 0.12), transparent 34%),
|
||||
var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
gap: 12px;
|
||||
padding: 16px 28px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition: transform .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease;
|
||||
pointer-events: none;
|
||||
transition: all 0.35s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.engine-choice-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--accent);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(99, 102, 241, 0.18), transparent 38%),
|
||||
var(--bg-card-hover);
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.12);
|
||||
.es-content-openclaw .es-cta {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.engine-choice-card:disabled {
|
||||
cursor: wait;
|
||||
opacity: .72;
|
||||
.es-content-hermes .es-cta {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #18171a;
|
||||
border: 1px solid rgba(0, 0, 0, 0.18);
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.engine-choice-card.loading .engine-choice-arrow {
|
||||
animation: engine-choice-pulse .8s ease-in-out infinite alternate;
|
||||
.es-stage[data-hover='openclaw'] .es-content-openclaw .es-cta {
|
||||
background: #fff;
|
||||
color: #0a0a0a;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.engine-choice-icon {
|
||||
.es-stage[data-hover='hermes'] .es-content-hermes .es-cta {
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
border-color: #0a0a0a;
|
||||
}
|
||||
.es-cta-arrow {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 16px;
|
||||
color: var(--accent);
|
||||
background: var(--bg-glass);
|
||||
border: 1px solid var(--border-primary);
|
||||
transition: transform 0.3s;
|
||||
font-weight: 400;
|
||||
}
|
||||
.es-stage[data-hover='openclaw'] .es-content-openclaw .es-cta-arrow { transform: translateX(4px) }
|
||||
.es-stage[data-hover='hermes'] .es-content-hermes .es-cta-arrow { transform: translateX(-4px) }
|
||||
|
||||
.engine-choice-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.engine-choice-content {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.engine-choice-title-row {
|
||||
/* —— 底部次级链接(两个都要 / 稍后再说) —— */
|
||||
.es-secondary {
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.engine-choice-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.engine-choice-badge {
|
||||
padding: 2px 8px;
|
||||
gap: 14px;
|
||||
z-index: 4;
|
||||
padding: 8px 18px;
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-muted, rgba(99, 102, 241, 0.12));
|
||||
background: rgba(150, 150, 150, 0.08);
|
||||
backdrop-filter: blur(20px) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
||||
border: 1px solid rgba(180, 180, 180, 0.18);
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.es-secondary-link {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(150, 150, 150, 0.85);
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
.es-secondary-link:hover {
|
||||
color: rgba(20, 20, 20, 0.95);
|
||||
background: rgba(180, 180, 180, 0.18);
|
||||
}
|
||||
.es-secondary-link.loading {
|
||||
opacity: 0.5;
|
||||
cursor: wait;
|
||||
}
|
||||
.es-secondary-sep {
|
||||
color: rgba(150, 150, 150, 0.5);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.engine-choice-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
/* —— 选中态:三角形扩满(数据属性触发) —— */
|
||||
.es-stage[data-expanding='openclaw'] .es-panel-openclaw,
|
||||
.es-stage[data-expanding='hermes'] .es-panel-hermes {
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
|
||||
z-index: 5;
|
||||
}
|
||||
.es-stage[data-expanding='openclaw'] .es-panel-hermes,
|
||||
.es-stage[data-expanding='hermes'] .es-panel-openclaw {
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.es-stage[data-expanding] .es-content,
|
||||
.es-stage[data-expanding] .es-divider,
|
||||
.es-stage[data-expanding] .es-top-banner,
|
||||
.es-stage[data-expanding] .es-corner-mark,
|
||||
.es-stage[data-expanding] .es-secondary {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.engine-choice-meta {
|
||||
margin-top: auto;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.engine-choice-arrow {
|
||||
margin-left: auto;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.engine-choice-note {
|
||||
margin-top: 18px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 16px;
|
||||
background: var(--bg-glass);
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.engine-choice-note-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@keyframes engine-choice-pulse {
|
||||
from { transform: translateX(0); opacity: .5; }
|
||||
to { transform: translateX(4px); opacity: 1; }
|
||||
/* —— 全屏 reveal(attach 到 body,跨路由切换存活) —— */
|
||||
.es-reveal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9000;
|
||||
pointer-events: none;
|
||||
background: #0c0d12;
|
||||
transition: width 0.9s cubic-bezier(0.65, 0, 0.35, 1), height 0.9s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.6s ease;
|
||||
}
|
||||
.es-reveal[data-engine='openclaw'] { background: #0c0d12 }
|
||||
.es-reveal[data-engine='hermes'] { background: #f5f3ee }
|
||||
.es-reveal.es-reveal-active { width: 260vmax; height: 260vmax }
|
||||
.es-reveal.es-reveal-fadeout { opacity: 0 }
|
||||
|
||||
/* —— 移动端响应式 —— */
|
||||
@media (max-width: 760px) {
|
||||
.engine-select-page {
|
||||
padding: 28px 18px;
|
||||
}
|
||||
.es-content-openclaw { top: 8%; left: 5% }
|
||||
.es-content-hermes { bottom: 8%; right: 5% }
|
||||
.es-title { font-size: clamp(56px, 14vw, 120px) }
|
||||
.es-tagline { font-size: 16px; margin-bottom: 20px }
|
||||
.es-feature-list { gap: 8px; font-size: 12.5px; margin-bottom: 28px }
|
||||
.es-cta { padding: 12px 22px; font-size: 12px }
|
||||
.es-corner-tl, .es-corner-br { font-size: 10px; letter-spacing: 0.24em }
|
||||
.es-corner-tl { top: 16px; left: 18px }
|
||||
.es-corner-br { bottom: 16px; right: 18px }
|
||||
.es-top-banner { top: 18px; font-size: 10px; letter-spacing: 0.32em }
|
||||
.es-secondary { bottom: 16px }
|
||||
}
|
||||
|
||||
.engine-choice-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.engine-choice-card {
|
||||
min-height: auto;
|
||||
padding: 18px;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.engine-select-page.es-monolith,
|
||||
.es-panel,
|
||||
.es-content,
|
||||
.es-cta,
|
||||
.es-glow,
|
||||
.es-divider,
|
||||
.es-reveal,
|
||||
.es-top-banner,
|
||||
.es-corner-mark,
|
||||
.es-secondary {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user