From 7091f4ca32487d4490ec12bf4d63e201e5e4542d Mon Sep 17 00:00:00 2001 From: GCWing Date: Thu, 19 Mar 2026 22:15:00 +0800 Subject: [PATCH 1/2] fix(desktop): disable window decorations on Windows --- src/apps/desktop/src/theme.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/apps/desktop/src/theme.rs b/src/apps/desktop/src/theme.rs index d3d4aca5..a93023f4 100644 --- a/src/apps/desktop/src/theme.rs +++ b/src/apps/desktop/src/theme.rs @@ -254,6 +254,11 @@ pub fn create_main_window(app_handle: &tauri::AppHandle) { .hidden_title(true); } + #[cfg(target_os = "windows")] + { + builder = builder.decorations(false); + } + match builder.build() { Ok(window) => { #[cfg(debug_assertions)] From f81802e9a4818b1aa2ef6a6184f2ca12c3d6c634 Mon Sep 17 00:00:00 2001 From: GCWing Date: Thu, 19 Mar 2026 17:43:38 +0800 Subject: [PATCH 2/2] feat(web-ui): redesign cards with glassmorphism effect --- .../GalleryLayout/GalleryDetailModal.scss | 21 +- .../GalleryLayout/GalleryLayout.scss | 4 +- .../src/app/components/NavPanel/MainNav.tsx | 29 +- .../src/app/components/NavPanel/NavPanel.scss | 94 ++--- .../NavPanel/components/MiniAppEntry.tsx | 6 +- .../components/PersistentFooterActions.tsx | 220 ++++++----- .../sections/sessions/SessionsSection.scss | 20 + .../sections/sessions/SessionsSection.tsx | 58 ++- .../src/app/scenes/agents/AgentsScene.tsx | 4 +- .../scenes/agents/components/AgentCard.scss | 343 ++++++------------ .../scenes/agents/components/AgentCard.tsx | 68 ++-- .../agents/components/AgentTeamCard.scss | 249 ++++++------- .../agents/components/AgentTeamCard.tsx | 46 +-- .../agents/components/CoreAgentCard.scss | 125 ++++--- .../agents/components/CoreAgentCard.tsx | 1 + .../miniapps/components/MiniAppCard.scss | 273 +++++++++----- .../miniapps/components/MiniAppCard.tsx | 81 +++-- .../miniapps/views/MiniAppGalleryView.tsx | 4 +- .../app/scenes/my-agent/InsightsScene.scss | 308 +++++++++------- .../src/app/scenes/skills/SkillsScene.tsx | 4 +- .../scenes/skills/components/SkillCard.scss | 183 ++++++---- .../scenes/skills/components/SkillCard.tsx | 67 ++-- src/web-ui/src/shared/utils/cardGradients.ts | 19 +- tests/e2e/specs/insights-screenshot.spec.ts | 28 ++ 24 files changed, 1214 insertions(+), 1041 deletions(-) create mode 100644 tests/e2e/specs/insights-screenshot.spec.ts diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.scss index f158f3a6..71354664 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryDetailModal.scss @@ -10,8 +10,10 @@ display: flex; gap: $size-gap-4; align-items: flex-start; + position: relative; } + // Small decorative gradient behind icon &__icon { --gallery-detail-gradient: linear-gradient(135deg, rgba(59,130,246,0.28) 0%, rgba(139,92,246,0.18) 100%); @@ -24,7 +26,24 @@ flex-shrink: 0; color: var(--color-text-primary); background: var(--gallery-detail-gradient); - border: 1px solid color-mix(in srgb, var(--border-subtle) 60%, transparent); + border: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + + // Blurred glow behind icon + &::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; + border-radius: 50%; + background: var(--gallery-detail-gradient); + filter: blur(20px); + z-index: -1; + opacity: 0.6; + } } &__summary { diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss index 41e058c4..88c77d94 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss @@ -101,7 +101,7 @@ $content-max: 1480px; .gallery-zones { display: flex; flex-direction: column; - gap: $size-gap-5; + gap: $size-gap-8; width: min(100%, $content-max); padding: $size-gap-6 $gutter $size-gap-8; margin: 0 auto; @@ -153,7 +153,7 @@ $content-max: 1480px; .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--gallery-grid-min, 320px), 1fr)); - gap: $size-gap-2; + gap: $size-gap-3; align-content: start; &--skeleton { diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 666612ea..416fed70 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -13,7 +13,7 @@ import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; -import { Plus, FolderOpen, FolderPlus, History, Check, Bot, Clock3 } from 'lucide-react'; +import { Plus, FolderOpen, FolderPlus, History, Check, Clock3 } from 'lucide-react'; import { Badge, Tooltip } from '@/component-library'; import { useApp } from '../../hooks/useApp'; import { useSceneManager } from '../../hooks/useSceneManager'; @@ -875,21 +875,18 @@ const MainNav: React.FC = ({ })} - {isAssistantNavMode && ( -
- - - -
- )} +
+ + + +
= ({ aria-label={t('scenes.miniApps')} > - {t('scenes.miniApps')} - Beta + Beta diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx index 9ec14984..7818513e 100644 --- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx @@ -10,7 +10,6 @@ import { useToolbarModeContext } from '@/flow_chat/components/toolbar-mode/Toolb import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { useNotification } from '@/shared/notification-system'; import NotificationButton from '../../TitleBar/NotificationButton'; -import InsightsButton from '../../TitleBar/InsightsButton'; import { AboutDialog } from '../../AboutDialog'; import { RemoteConnectDialog } from '../../RemoteConnectDialog'; import { @@ -126,126 +125,125 @@ const PersistentFooterActions: React.FC = () => { return (
-
- - - - - {menuOpen && ( - <> -
-
+
+ + + + + {menuOpen && ( + <> +
+
+ + + +
- -
- -
- - -
- - )} -
+
+ + +
+ + )} +
- - - - - - - - - - - setShowAbout(false)} /> - setShowRemoteConnect(false)} /> - setShowRemoteDisclaimer(false)} - title={t('remoteConnect.disclaimerTitle')} - showCloseButton - size="large" - contentInset - > - setShowRemoteDisclaimer(false)} - onAgree={handleAgreeDisclaimer} - /> - + + + + + + +
+ +
+ +
+ setShowAbout(false)} /> + setShowRemoteConnect(false)} /> + setShowRemoteDisclaimer(false)} + title={t('remoteConnect.disclaimerTitle')} + showCloseButton + size="large" + contentInset + > + setShowRemoteDisclaimer(false)} + onAgree={handleAgreeDisclaimer} + /> + ); }; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index fdcef05f..0850441b 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -148,6 +148,11 @@ color: color-mix(in srgb, var(--color-accent-500) 70%, var(--color-text-muted)); } + &.is-running { + color: var(--color-primary); + animation: bitfun-nav-session-spin 1s linear infinite; + } + .bitfun-nav-panel__inline-item:hover &, .bitfun-nav-panel__inline-item.is-active & { opacity: 1; @@ -450,6 +455,16 @@ } } +// Session running spin animation +@keyframes bitfun-nav-session-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + @media (prefers-reduced-motion: reduce) { .bitfun-nav-panel { &__inline-item, @@ -457,4 +472,9 @@ transition: none; } } + + .bitfun-nav-panel__inline-item-icon.is-running { + animation: none; + opacity: 0.8; + } } diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index d974244d..b618d35b 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { Pencil, Trash2, Check, X, Bot, Code2, Users, MoreHorizontal } from 'lucide-react'; +import { Pencil, Trash2, Check, X, Bot, Code2, Users, MoreHorizontal, Loader2 } from 'lucide-react'; import { IconButton, Input, Tooltip } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import { flowChatStore } from '../../../../../flow_chat/store/FlowChatStore'; @@ -25,6 +25,8 @@ import { } from '@/flow_chat/services/openBtwSession'; import { resolveSessionRelationship } from '@/flow_chat/utils/sessionMetadata'; import { compareSessionsForDisplay } from '@/flow_chat/utils/sessionOrdering'; +import { stateMachineManager } from '@/flow_chat/state-machine'; +import { SessionExecutionState } from '@/flow_chat/state-machine/types'; import './SessionsSection.scss'; const MAX_VISIBLE_SESSIONS = 8; @@ -72,9 +74,30 @@ const SessionsSection: React.FC = ({ const [showAll, setShowAll] = useState(false); const [openMenuSessionId, setOpenMenuSessionId] = useState(null); const [sessionMenuPosition, setSessionMenuPosition] = useState<{ top: number; left: number } | null>(null); + const [runningSessionIds, setRunningSessionIds] = useState>(new Set()); const editInputRef = useRef(null); const sessionMenuPopoverRef = useRef(null); + // Subscribe to state machine changes for running status + useEffect(() => { + const updateRunningSessions = () => { + const running = new Set(); + for (const session of flowChatState.sessions.values()) { + const machine = stateMachineManager.get(session.sessionId); + if (machine && machine.getCurrentState() === SessionExecutionState.PROCESSING) { + running.add(session.sessionId); + } + } + setRunningSessionIds(running); + }; + + updateRunningSessions(); + const unsubscribe = stateMachineManager.subscribeGlobal(() => { + updateRunningSessions(); + }); + return () => unsubscribe(); + }, [flowChatState.sessions]); + useEffect(() => { const unsub = flowChatStore.subscribe(s => setFlowChatState(s)); return () => unsub(); @@ -343,6 +366,7 @@ const SessionsSection: React.FC = ({ : sessionModeKey === 'claw' ? Bot : Code2; + const isRunning = runningSessionIds.has(session.sessionId); const isRowActive = activeBtwSessionData?.childSessionId ? session.sessionId === activeBtwSessionData.childSessionId : activeTabId === AGENT_SCENE && session.sessionId === activeSessionId; @@ -359,17 +383,27 @@ const SessionsSection: React.FC = ({ .join(' ')} onClick={() => handleSwitch(session.sessionId)} > - + {isRunning ? ( + + ) : ( + + )} {isEditing ? (
e.stopPropagation()}> diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index c228a625..a2d56624 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -384,7 +384,7 @@ const AgentsHomeView: React.FC = () => { ) : null} {!loading && filteredAgents.length > 0 ? ( - + {filteredAgents.map((agent, index) => ( { message={agentTeams.length === 0 ? t('teamsZone.empty.noTeams') : t('teamsZone.empty.noMatch')} /> ) : ( - + {filteredTeams.map((team, index) => { const caps = computeAgentTeamCapabilities(team, allAgents); const topCaps = CAPABILITY_CATEGORIES diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss index bafca7bb..e0f94dde 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss @@ -1,25 +1,43 @@ @use '../../../../component-library/styles/tokens' as *; .agent-card { - display: flex; - border-radius: $size-radius-lg; + width: 360px; + height: 200px; + border-radius: 15px; background: var(--element-bg-soft); - border: 1px solid transparent; - cursor: pointer; + display: flex; + flex-direction: column; position: relative; + overflow: hidden; + cursor: pointer; animation: agent-card-in 0.22s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 35ms); transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, - transform $motion-fast $easing-standard; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.35s ease; + + // Top gradient overlay - shows on hover, covers entire card except footer + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + background: var(--agent-card-gradient); + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 0; + } &:hover { - background: var(--element-bg-medium); - border-color: var(--border-subtle); - transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + transform: translateY(-4px) scale(1.02); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + + &::before { + opacity: 0.4; + } } &:focus-visible { @@ -28,97 +46,98 @@ } &--disabled { - .agent-card__name, - .agent-card__desc, - .agent-card__meta { - opacity: 0.8; - } + opacity: 0.7; + } + + // ── Header with icon ── + &__header { + display: flex; + align-items: center; + gap: $size-gap-3; + padding: $size-gap-3; + padding-bottom: 0; + position: relative; + z-index: 1; } &__icon-area { - width: 56px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; - background: var(--agent-card-gradient); - border-radius: $size-radius-lg 0 0 $size-radius-lg; + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); } &__icon { color: var(--color-text-primary); } - &__body { + &__header-info { flex: 1; min-width: 0; display: flex; flex-direction: column; - padding: $size-gap-3; - gap: $size-gap-2; + gap: 4px; overflow: hidden; } - &__header { - display: flex; - align-items: flex-start; - gap: $size-gap-2; - } - - &__header-main { - flex: 1; - min-width: 0; - } - &__title-row { display: flex; - align-items: flex-start; + align-items: center; + justify-content: space-between; gap: $size-gap-2; - margin-bottom: 4px; } &__name { - flex: 1; - min-width: 0; - font-size: $font-size-sm; - font-weight: $font-weight-semibold; + font-size: 1.2em; + font-weight: 900; color: var(--color-text-primary); - line-height: $line-height-tight; - word-break: break-word; + line-height: $line-height-base; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &__badges { - display: inline-flex; - align-items: center; - gap: $size-gap-1; + display: flex; + gap: 4px; flex-wrap: wrap; - justify-content: flex-end; flex-shrink: 0; } + // ── Body ── + &__body { + flex: 1; + padding: $size-gap-2 $size-gap-3; + display: flex; + flex-direction: column; + gap: 4px; + overflow: hidden; + position: relative; + z-index: 1; + } + &__desc { margin: 0; - font-size: $font-size-xs; - color: var(--color-text-secondary); + font-size: 0.85em; + font-weight: 300; + color: rgba(var(--color-text-secondary), 0.85); line-height: $line-height-relaxed; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; } - &__actions { - display: inline-flex; - align-items: center; - gap: 2px; - flex-shrink: 0; - } - &__meta { display: flex; align-items: center; - gap: $size-gap-2; + gap: $size-gap-1; flex-wrap: wrap; margin-top: auto; padding-top: $size-gap-2; @@ -136,7 +155,7 @@ &__cap-chips { display: inline-flex; align-items: center; - gap: $size-gap-1; + gap: 4px; flex-wrap: wrap; } @@ -145,208 +164,66 @@ align-items: center; padding: 2px 8px; border-radius: $size-radius-full; - border: 1px solid var(--border-subtle); + border: 1px solid; background: rgba(255, 255, 255, 0.04); font-size: 10px; font-weight: $font-weight-medium; white-space: nowrap; } - &__detail { - overflow: hidden; - min-height: 0; - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding-top: $size-gap-2; - color: var(--color-text-secondary); - font-size: $font-size-xs; - line-height: $line-height-relaxed; - } - - &__cap-grid { - display: flex; - flex-direction: column; - gap: $size-gap-2; - } - - &__cap-row { + // ── Footer with actions ── + &__footer { display: flex; align-items: center; - gap: $size-gap-3; - } - - &__cap-label { - min-width: 28px; - font-size: $font-size-xs; - font-weight: $font-weight-medium; - } - - &__cap-bar { - display: flex; - gap: 3px; - } - - &__cap-pip { - width: 8px; - height: 8px; - border-radius: 2px; - background: var(--element-bg-medium); - } - - &__cap-level { - min-width: 24px; - font-size: 10px; - color: var(--color-text-muted); + width: 100%; + border-radius: 0 0 15px 15px; + overflow: hidden; + position: relative; + z-index: 1; + + // Bottom gradient blur background matching card color + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--agent-card-gradient); + opacity: 0.5; + transition: opacity 0.35s ease; + pointer-events: none; + } } - &__section { - display: flex; - flex-direction: column; - gap: $size-gap-2; - padding-top: $size-gap-3; - border-top: 1px dashed var(--border-subtle); + &:hover &__footer::after { + opacity: 1; } - &__section-head { + &__footer-actions { display: flex; align-items: center; gap: $size-gap-2; - } - - &__section-title { - display: inline-flex; - align-items: center; - gap: $size-gap-2; - color: var(--color-text-secondary); - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - } - - &__section-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 16px; - height: 16px; - padding: 0 4px; - border-radius: $size-radius-full; - background: var(--element-bg-medium); - border: 1px solid var(--border-subtle); - font-size: 10px; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); - line-height: 1; - } - - &__section-actions { - margin-left: auto; - display: inline-flex; - align-items: center; - gap: 2px; - } - - &__chip-grid, - &__token-grid { - display: flex; - flex-wrap: wrap; - gap: $size-gap-1; - } - - &__chip { - display: inline-flex; - align-items: center; - padding: 2px 7px; - border-radius: $size-radius-sm; - background: var(--element-bg-base); - border: 1px solid var(--border-subtle); - font-size: 10px; - font-weight: $font-weight-medium; - color: var(--color-text-secondary); - font-family: $font-family-mono; - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__token-grid { - padding: $size-gap-2; - background: var(--element-bg-subtle); - border-radius: $size-radius-base; - border: 1px solid var(--border-subtle); - max-height: 220px; - overflow-y: auto; - - &::-webkit-scrollbar { - width: 4px; - } - - &::-webkit-scrollbar-thumb { - background: var(--border-subtle); - border-radius: 2px; - } - } - - &__token { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - border-radius: $size-radius-sm; - border: 1px solid var(--border-subtle); - background: var(--element-bg-base); - font-size: $font-size-xs; - color: var(--color-text-secondary); - cursor: pointer; - transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - color $motion-fast $easing-standard; - - &:hover { - background: var(--element-bg-medium); - border-color: var(--border-medium); - color: var(--color-text-primary); - } - - &.is-on { - border-color: var(--color-primary); - background: rgba(var(--color-primary-rgb, 99 102 241) / 0.08); - color: var(--color-text-primary); - } - } - - &__token-name { - font-family: $font-family-mono; - font-size: 11px; - } - - &__empty-inline { - font-size: 10px; - color: var(--color-text-tertiary); - font-style: italic; - opacity: 0.7; + flex: 1; + padding: $size-gap-2 $size-gap-3; + position: relative; + z-index: 1; } } +// ── Responsive ── @media (max-width: 720px) { .agent-card { - &__icon-area { - width: 48px; - } + width: 100%; + min-height: 180px; &__header { flex-direction: column; } - - &__actions { - width: 100%; - justify-content: flex-end; - } } } +// ── Animations ── @keyframes agent-card-in { from { opacity: 0; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx index 56c3320a..65df8c39 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.tsx @@ -52,41 +52,35 @@ const AgentCard: React.FC = ({ onKeyDown={(e) => e.key === 'Enter' && openDetails()} aria-label={agent.name} > -
-
- + {/* Header: icon + name */} +
+
+
+ +
-
- -
-
-
-
- {agent.name} -
- - {agent.agentKind === 'mode' ? : } - {badge.label} - - {!agent.enabled ? ( - {t('agentCard.badges.disabled', '已禁用')} - ) : null} - {agent.model ? ( - {agent.model} - ) : null} -
+
+
+ {agent.name} +
+ + {agent.agentKind === 'mode' ? : } + {badge.label} + + {!agent.enabled ? ( + {t('agentCard.badges.disabled', '已禁用')} + ) : null} + {agent.model ? ( + {agent.model} + ) : null}
-

{agent.description?.trim() || '—'}

-
- -
e.stopPropagation()}> - onToggleSolo(agent.id, !soloEnabled)} - size="small" - />
+
+ + {/* Body: description + meta */} +
+

{agent.description?.trim() || '—'}

@@ -115,7 +109,19 @@ const AgentCard: React.FC = ({ ) : null}
+ + {/* Footer: switch */} +
+
e.stopPropagation()}> + onToggleSolo(agent.id, !soloEnabled)} + size="small" + /> +
+
); }; + export default AgentCard; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss index ab89779f..e88366fc 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.scss @@ -1,25 +1,43 @@ @use '../../../../component-library/styles/tokens' as *; .agent-team-card { - display: flex; - border-radius: $size-radius-lg; + width: 360px; + height: 200px; + border-radius: 15px; background: var(--element-bg-soft); - border: 1px solid transparent; - cursor: pointer; + display: flex; + flex-direction: column; position: relative; + overflow: hidden; + cursor: pointer; animation: agent-team-card-in 0.22s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 35ms); transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, - transform $motion-fast $easing-standard; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.35s ease; + + // Top gradient overlay - shows on hover, covers entire card except footer + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + background: var(--agent-team-card-gradient); + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 0; + } &:hover { - background: var(--element-bg-medium); - border-color: var(--border-subtle); - transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + transform: translateY(-4px) scale(1.02); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + + &::before { + opacity: 0.4; + } } &:focus-visible { @@ -27,79 +45,55 @@ outline-offset: 2px; } + // ── Header with icon ── + &__header { + display: flex; + align-items: center; + gap: $size-gap-3; + padding: $size-gap-3; + padding-bottom: 0; + position: relative; + z-index: 1; + } + &__icon-area { - width: 56px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.1) 0%, - rgba(255, 255, 255, 0.04) 100% - ); - border-radius: $size-radius-lg 0 0 $size-radius-lg; + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); } &__icon { color: var(--agent-team-card-accent); } - &__body { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: $size-gap-2; - padding: $size-gap-3; - overflow: hidden; - } - - &__header { - display: flex; - align-items: flex-start; - gap: $size-gap-2; - } - - &__header-main { + &__header-info { flex: 1; min-width: 0; - } - - &__title-row { display: flex; - align-items: flex-start; + align-items: center; + justify-content: space-between; gap: $size-gap-2; - margin-bottom: 4px; } &__name { - flex: 1; - min-width: 0; - font-size: $font-size-sm; - font-weight: $font-weight-semibold; + font-size: 1.2em; + font-weight: 900; color: var(--color-text-primary); - line-height: $line-height-tight; - word-break: break-word; - } - - &__desc { - margin: 0; - font-size: $font-size-xs; - color: var(--color-text-secondary); - line-height: $line-height-relaxed; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; + line-height: $line-height-base; + white-space: nowrap; overflow: hidden; - word-break: break-word; + text-overflow: ellipsis; } &__actions { - display: inline-flex; + display: flex; align-items: center; - gap: 2px; - flex-shrink: 0; } &__icon-btn { @@ -110,23 +104,48 @@ justify-content: center; border: none; border-radius: $size-radius-sm; - background: transparent; - color: var(--color-text-muted); + background: rgba(255, 255, 255, 0.1); + color: var(--color-text-secondary); cursor: pointer; transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard; &:hover { - background: var(--element-bg-strong); + background: rgba(255, 255, 255, 0.25); color: var(--color-text-primary); } } + // ── Body ── + &__body { + flex: 1; + padding: $size-gap-2 $size-gap-3; + display: flex; + flex-direction: column; + gap: 4px; + overflow: hidden; + position: relative; + z-index: 1; + } + + &__desc { + margin: 0; + font-size: 0.85em; + font-weight: 300; + color: rgba(var(--color-text-secondary), 0.85); + line-height: $line-height-relaxed; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; + } + &__meta { display: flex; align-items: center; - gap: $size-gap-2; + gap: $size-gap-1; flex-wrap: wrap; margin-top: auto; padding-top: $size-gap-2; @@ -168,16 +187,8 @@ &__cap-chips { display: inline-flex; align-items: center; - gap: $size-gap-1; - flex-wrap: wrap; - } - - &__state-badges { - display: inline-flex; - align-items: center; - gap: $size-gap-1; + gap: 4px; flex-wrap: wrap; - margin-left: auto; } &__cap-chip { @@ -185,89 +196,65 @@ align-items: center; padding: 2px 8px; border-radius: $size-radius-full; - border: 1px solid var(--border-subtle); + border: 1px solid; background: rgba(255, 255, 255, 0.04); font-size: 10px; font-weight: $font-weight-medium; white-space: nowrap; } - &__detail { - overflow: hidden; - min-height: 0; - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding-top: $size-gap-2; - color: var(--color-text-secondary); - font-size: $font-size-xs; - line-height: $line-height-relaxed; - } - - &__section { - display: flex; - flex-direction: column; - gap: $size-gap-2; - padding-top: $size-gap-3; - border-top: 1px dashed var(--border-subtle); - } - - &__section-title { - font-size: $font-size-xs; - font-weight: $font-weight-semibold; - color: var(--color-text-secondary); - } - - &__member-list { - display: flex; - flex-wrap: wrap; - gap: $size-gap-2; - } - - &__member-chip { + &__state-badges { display: inline-flex; align-items: center; gap: 4px; - padding: 3px 8px 3px 6px; - border-radius: $size-radius-sm; - background: var(--element-bg-medium); - color: var(--color-text-primary); + flex-wrap: wrap; + margin-left: auto; + padding: $size-gap-2 $size-gap-3; } - &__member-name { - max-width: 140px; + // ── Footer for badges ── + &__footer { + display: flex; + align-items: center; + width: 100%; + border-radius: 0 0 15px 15px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + position: relative; + z-index: 1; + + // Bottom gradient blur background matching card color + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--agent-team-card-gradient); + opacity: 0.5; + transition: opacity 0.35s ease; + pointer-events: none; + } } - &__member-role { - font-size: 10px; - color: var(--color-text-muted); - padding: 0 4px; - border-radius: 2px; - background: var(--element-bg-base); - margin-left: 2px; + &:hover &__footer::after { + opacity: 1; } } +// ── Responsive ── @media (max-width: 720px) { .agent-team-card { - &__icon-area { - width: 48px; - } + width: 100%; + min-height: 180px; &__header { flex-direction: column; } - - &__actions { - width: 100%; - justify-content: flex-end; - } } } +// ── Animations ── @keyframes agent-team-card-in { from { opacity: 0; diff --git a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx index 784da188..a006a83c 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx +++ b/src/web-ui/src/app/scenes/agents/components/AgentTeamCard.tsx @@ -47,6 +47,7 @@ const AgentTeamCard: React.FC = ({ style={{ '--card-index': index, '--agent-team-card-accent': accent, + '--agent-team-card-gradient': `linear-gradient(135deg, ${accent}40 0%, ${accent}15 100%)`, } as React.CSSProperties} onClick={openDetails} role="button" @@ -54,21 +55,15 @@ const AgentTeamCard: React.FC = ({ onKeyDown={(e) => e.key === 'Enter' && openDetails()} aria-label={team.name} > -
-
- -
-
- -
-
-
-
- {team.name} -
-

{team.description?.trim() || '—'}

+ {/* Header: icon + name */} +
+
+
+
- +
+
+ {team.name}
e.stopPropagation()}>
+
+ + {/* Body: description + meta */} +
+

{team.description?.trim() || '—'}

@@ -117,13 +117,17 @@ const AgentTeamCard: React.FC = ({ ))}
) : null} -
- {isExample ? {t('teamCard.badges.example', '示例')} : null} - {strategyLabel} - {team.shareContext ? ( - {t('teamCard.badges.sharedContext', '共享上下文')} - ) : null} -
+
+
+ + {/* Footer: badges */} +
+
+ {isExample ? {t('teamCard.badges.example', '示例')} : null} + {strategyLabel} + {team.shareContext ? ( + {t('teamCard.badges.sharedContext', '共享上下文')} + ) : null}
diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss index 51b5c9d6..e7d383b3 100644 --- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.scss @@ -1,45 +1,42 @@ @use '../../../../component-library/styles/tokens' as *; .core-agent-card { + width: 360px; + height: 200px; + border-radius: 15px; + background: var(--element-bg-soft); display: flex; flex-direction: column; - border-radius: $size-radius-xl; - background: var(--element-bg-soft); - border: 1px solid transparent; - cursor: pointer; position: relative; overflow: hidden; + cursor: pointer; animation: core-card-in 0.28s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 60ms); transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, - transform $motion-fast $easing-standard; - - // Subtle top accent glow - &::after { - content: ''; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.35s ease; + + // Top gradient overlay - shows on hover, covers entire card except footer + &::before { + content: ""; position: absolute; - inset: 0; - background: radial-gradient( - ellipse 80% 40% at 50% 0%, - color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 10%, transparent) 0%, - transparent 70% - ); + top: 0; + left: 0; + right: 0; + bottom: 40px; + background: var(--core-card-gradient); + opacity: 0; + transition: opacity 0.35s ease; pointer-events: none; + z-index: 0; } &:hover { - background: var(--element-bg-medium); - border-color: color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 30%, transparent); - transform: translateY(-2px); - box-shadow: - 0 8px 24px rgba(0, 0, 0, 0.14), - 0 0 0 1px color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 20%, transparent); - - .core-agent-card__icon-wrap { - transform: scale(1.08); + transform: translateY(-4px) scale(1.02); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + + &::before { + opacity: 0.4; } } @@ -56,24 +53,25 @@ } } - // ── Top section ────────────────────────────────────────────────── + // ── Header ──────────────────────────────────────────────────────── &__top { display: flex; align-items: center; gap: $size-gap-3; - padding: $size-gap-4 $size-gap-4 $size-gap-3 $size-gap-4; - background: var(--core-accent-bg, rgba(99, 102, 241, 0.08)); - border-bottom: 1px solid color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 15%, transparent); + padding: $size-gap-3; + padding-bottom: 0; + position: relative; + z-index: 1; } &__icon-wrap { flex-shrink: 0; - width: 44px; - height: 44px; - border-radius: $size-radius-lg; - background: color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 18%, var(--element-bg-soft)); - border: 1px solid color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 30%, transparent); + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; @@ -87,13 +85,14 @@ display: flex; flex-direction: column; gap: 4px; + overflow: hidden; } &__name { - font-size: $font-size-base; - font-weight: $font-weight-semibold; + font-size: 1.2em; + font-weight: 900; color: var(--color-text-primary); - line-height: $line-height-tight; + line-height: $line-height-base; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -113,16 +112,19 @@ &__body { flex: 1; - padding: $size-gap-3 $size-gap-4 $size-gap-2 $size-gap-4; + padding: $size-gap-2 $size-gap-3; + position: relative; + z-index: 1; } &__desc { margin: 0; - font-size: $font-size-xs; - color: var(--color-text-secondary); + font-size: 0.85em; + font-weight: 300; + color: rgba(var(--color-text-secondary), 0.85); line-height: $line-height-relaxed; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; @@ -135,8 +137,29 @@ align-items: center; justify-content: space-between; gap: $size-gap-2; - padding: $size-gap-2 $size-gap-4 $size-gap-3 $size-gap-4; - border-top: 1px dashed color-mix(in srgb, var(--core-accent, var(--color-accent-500)) 18%, var(--border-subtle)); + padding: $size-gap-2 $size-gap-3; + border-radius: 0 0 15px 15px; + overflow: hidden; + position: relative; + z-index: 1; + + // Bottom gradient blur background matching card color + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--core-card-gradient); + opacity: 0.5; + transition: opacity 0.35s ease; + pointer-events: none; + } + } + + &:hover &__footer::after { + opacity: 1; } &__tag { @@ -145,6 +168,8 @@ gap: 5px; font-size: 10px; color: var(--color-text-muted); + position: relative; + z-index: 1; strong { font-weight: $font-weight-semibold; @@ -157,6 +182,8 @@ display: inline-flex; align-items: center; gap: $size-gap-2; + position: relative; + z-index: 1; } &__meta-item { @@ -172,8 +199,8 @@ .core-agents-grid { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: $size-gap-4; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: $size-gap-2; width: 100%; } @@ -182,7 +209,7 @@ @keyframes core-card-in { from { opacity: 0; - transform: translateY(14px) scale(0.97); + transform: translateY(10px) scale(0.98); } to { @@ -202,7 +229,7 @@ @media (max-width: 1080px) { .core-agents-grid { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); } } diff --git a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx index de6be029..d076dbd4 100644 --- a/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx +++ b/src/web-ui/src/app/scenes/agents/components/CoreAgentCard.tsx @@ -47,6 +47,7 @@ const CoreAgentCard: React.FC = ({ '--card-index': index, '--core-accent': meta.accentColor, '--core-accent-bg': meta.accentBg, + '--core-card-gradient': `linear-gradient(135deg, ${meta.accentColor}40 0%, ${meta.accentColor}15 100%)`, } as React.CSSProperties} onClick={openDetails} role="button" diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss index d241dc32..541d1c97 100644 --- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss +++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.scss @@ -1,26 +1,43 @@ @use '../../../../component-library/styles/tokens' as *; .miniapp-card { - display: flex; - flex-direction: row; - align-items: stretch; - border-radius: $size-radius-lg; + width: 360px; + min-height: 200px; + border-radius: 15px; background: var(--element-bg-soft); - border: none; - cursor: pointer; + display: flex; + flex-direction: column; position: relative; overflow: hidden; + cursor: pointer; animation: miniapp-card-in 0.22s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 35ms); transition: - background $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, - transform $motion-fast $easing-standard; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.35s ease; + + // Top gradient overlay - shows on hover, covers entire card except footer + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + background: var(--miniapp-card-gradient); + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 0; + } &:hover { - background: var(--element-bg-medium); - transform: translateY(-1px); - box-shadow: 0 3px 12px rgba(0, 0, 0, 0.14); + transform: translateY(-4px) scale(1.02); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + + &::before { + opacity: 0.4; + } } &:active { @@ -34,70 +51,79 @@ } &--running { - box-shadow: 0 0 0 1.5px rgba(52, 211, 153, 0.5); + // Running state - green border removed, relies on run-dot indicator + } - &:hover { - box-shadow: - 0 0 0 1.5px rgba(52, 211, 153, 0.65), - 0 3px 12px rgba(0, 0, 0, 0.14); - } + // ── Header with icon ── + &__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: $size-gap-3; + padding-bottom: 0; + position: relative; + z-index: 1; } &__icon-area { - width: 64px; - flex-shrink: 0; display: flex; align-items: center; justify-content: center; - position: relative; + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); } &__icon { - color: var(--color-text-secondary); + color: var(--color-text-primary); animation: miniapp-icon-pop 0.5s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 35ms + 60ms); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } - &:hover &__icon { - transform: scale(1.18); - } - &__run-dot { - display: inline-block; - flex-shrink: 0; - width: 7px; - height: 7px; + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; border-radius: 50%; background: #34d399; - box-shadow: 0 0 5px rgba(52, 211, 153, 0.65); + box-shadow: 0 0 6px rgba(52, 211, 153, 0.65); + animation: pulse 2s ease-in-out infinite; } + // ── Body ── &__body { flex: 1; - min-width: 0; + padding: $size-gap-2 $size-gap-3 0; display: flex; flex-direction: column; - padding: 0; + gap: 4px; + overflow: hidden; + position: relative; + z-index: 1; } &__row { display: flex; align-items: center; + justify-content: space-between; gap: $size-gap-2; - min-width: 0; - padding: $size-gap-2 $size-gap-3; } &__name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; + font-size: 1.2em; + font-weight: 900; color: var(--color-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; + line-height: $line-height-base; } &__version { @@ -109,95 +135,127 @@ flex-shrink: 0; } + &__desc { + flex: 1; + overflow: hidden; + } + + &__desc-inner { + font-size: 0.85em; + font-weight: 300; + color: rgba(var(--color-text-secondary), 0.85); + line-height: $line-height-relaxed; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + &__tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: auto; + padding-top: $size-gap-2; + } + + &__tag { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 $size-gap-2; + border-radius: $size-radius-full; + font-size: 10px; + color: var(--color-text-secondary); + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); + white-space: nowrap; + } + + // ── Footer with actions ── + &__footer { + display: flex; + align-items: center; + width: 100%; + border-radius: 0 0 15px 15px; + overflow: hidden; + position: relative; + z-index: 1; + + // Bottom gradient blur background - visible by default as divider + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--miniapp-card-gradient); + opacity: 0.5; + transition: opacity 0.35s ease; + pointer-events: none; + } + } + + &:hover &__footer::after { + opacity: 1; + } + &__actions { display: flex; align-items: center; - gap: 2px; - flex-shrink: 0; + gap: $size-gap-1; + padding: $size-gap-2 $size-gap-3; + position: relative; + z-index: 1; } &__action-btn { display: flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; + width: 30px; + height: 30px; border-radius: $size-radius-sm; border: none; - background: transparent; + background: rgba(247, 234, 234, 0.3); + color: var(--color-text-secondary); cursor: pointer; transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard; - &--primary { - color: var(--color-text-muted); - - &:hover { - background: var(--element-bg-strong); - color: var(--color-accent-400); - } + &:not(:last-child) { + margin-right: 4px; } - &--danger { - color: var(--color-text-muted); - - &:hover { - background: var(--element-bg-strong); - color: var(--color-error); - } + &--primary:hover { + background: rgba(247, 234, 234, 0.6); + color: var(--color-accent-400); } - &--stop { - color: var(--color-text-muted); - - &:hover { - background: var(--element-bg-strong); - color: #34d399; - } + &--danger:hover { + background: rgba(247, 234, 234, 0.6); + color: var(--color-error); } - } - &__desc { - padding: $size-gap-1 $size-gap-3 $size-gap-3; - - &-inner { - font-size: $font-size-xs; - color: var(--color-text-secondary); - line-height: $line-height-relaxed; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + &--stop:hover { + background: rgba(247, 234, 234, 0.6); + color: #34d399; } } - - &__tags { - display: flex; - flex-wrap: wrap; - gap: $size-gap-1; - padding: $size-gap-2 $size-gap-3 $size-gap-3; - } - - &__tag { - display: inline-flex; - align-items: center; - height: 20px; - padding: 0 $size-gap-2; - border-radius: $size-radius-full; - font-size: $font-size-xs; - color: var(--color-text-secondary); - background: var(--element-bg-medium); - border: 1px solid var(--border-default); - white-space: nowrap; - } } +// ── Animations ── @keyframes miniapp-card-in { - 0% { opacity: 0; transform: translateY(10px) scale(0.96); } - 55% { opacity: 1; transform: translateY(-3px) scale(1.01); } - 75% { transform: translateY(1px) scale(0.995); } - 100% { transform: translateY(0) scale(1); } + from { + opacity: 0; + transform: translateY(10px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } } @keyframes miniapp-icon-pop { @@ -208,10 +266,27 @@ 100% { transform: scale(1) rotate(0deg); } } +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +// ── Responsive ── +@media (max-width: 720px) { + .miniapp-card { + width: 100%; + min-height: 180px; + } +} + @media (prefers-reduced-motion: reduce) { .miniapp-card, .miniapp-card__icon { animation: none; transition: none; } + + .miniapp-card__run-dot { + animation: none; + } } diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.tsx b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.tsx index 6b216a3e..88bfc64a 100644 --- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.tsx +++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppCard.tsx @@ -52,53 +52,33 @@ const MiniAppCard: React.FC = ({ ] .filter(Boolean) .join(' ')} - style={{ '--card-index': index } as React.CSSProperties} + style={{ + '--card-index': index, + '--miniapp-card-gradient': isRunning + ? 'linear-gradient(135deg, rgba(52, 211, 153, 0.28) 0%, rgba(16, 185, 129, 0.18) 100%)' + : 'linear-gradient(135deg, rgba(59, 130, 246, 0.28) 0%, rgba(139, 92, 246, 0.18) 100%)', + } as React.CSSProperties} onClick={handleOpenDetails} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && handleOpenDetails()} aria-label={app.name} > -
-
- {renderMiniAppIcon(app.icon || 'box', 28)} + {/* Header with icon */} +
+
+
+ {renderMiniAppIcon(app.icon || 'box', 20)} +
+ {isRunning && }
+ {/* Body: name + description + tags */}
{app.name} - {isRunning && } v{app.version} -
- - {isRunning && onStop ? ( - - ) : ( - - )} -
{app.description ? (
@@ -113,6 +93,39 @@ const MiniAppCard: React.FC = ({
) : null}
+ + {/* Footer with actions */} +
+
e.stopPropagation()}> + + {isRunning && onStop ? ( + + ) : ( + + )} +
+
); }; diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx index a8724b6d..d2cf14a7 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx @@ -186,7 +186,7 @@ const MiniAppGalleryView: React.FC = () => { } return ( - + {filtered.map((app, index) => ( { tools={runningApps.length > 0 ? {runningApps.length} : null} > {runningApps.length > 0 ? ( - + {runningApps.map((app, index) => ( { ) : null} {!installed.loading && !installed.error && installed.filteredSkills.length > 0 ? ( - + {installed.filteredSkills.map(renderInstalledCard)} ) : null} @@ -304,7 +304,7 @@ const SkillsScene: React.FC = () => { ) : null} {!market.marketLoading && !market.marketError && market.marketSkills.length > 0 ? ( - + {market.marketSkills.map(renderMarketCard)} ) : null} diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss index c1d4919b..d54f419e 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillCard.scss +++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.scss @@ -1,38 +1,60 @@ @use '../../../../component-library/styles/tokens' as *; /* ─────────────────────────────────────────────────── - SkillCard — horizontal card with icon column + body + SkillCard — vertical card with glassmorphism effect DOM: .skill-card - .skill-card__icon-area + ::before (decorative circle blur) + .skill-card__header + .skill-card__icon-area + .skill-card__icon + .skill-card__badges .skill-card__body - .skill-card__header (name + badges + actions) + .skill-card__name .skill-card__desc .skill-card__meta - .skill-card__detail-wrap + .skill-card__footer + .skill-card__actions ─────────────────────────────────────────────────── */ .skill-card { - display: flex; - flex-direction: row; - border-radius: $size-radius-lg; + width: 360px; + height: 200px; + border-radius: 15px; background: var(--element-bg-soft); - border: 1px solid transparent; - cursor: pointer; + display: flex; + flex-direction: column; position: relative; + overflow: hidden; + cursor: pointer; animation: skill-card-in 0.22s $easing-decelerate both; animation-delay: calc(var(--card-index, 0) * 35ms); transition: - background $motion-fast $easing-standard, - border-color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, - transform $motion-fast $easing-standard; + transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.35s ease; + + // Top gradient overlay - shows on hover, covers entire card except footer + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 40px; + background: var(--skill-card-gradient); + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 0; + } &:hover { - background: var(--element-bg-medium); - border-color: var(--border-subtle); - transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + transform: translateY(-4px) scale(1.02); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); + + &::before { + opacity: 0.4; + } } &:focus-visible { @@ -40,15 +62,26 @@ outline-offset: 2px; } - // ── Icon column ── + // ── Header with icon ── + &__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: $size-gap-3; + padding-bottom: 0; + position: relative; + z-index: 1; + } + &__icon-area { - width: 56px; - flex-shrink: 0; display: flex; align-items: center; justify-content: center; - background: var(--skill-card-gradient); - border-radius: $size-radius-lg 0 0 $size-radius-lg; + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); } &__icon { @@ -57,55 +90,46 @@ animation-delay: calc(var(--card-index, 0) * 35ms + 50ms); } - // ── Body column (vertical flow) ── - &__body { - flex: 1; - min-width: 0; + &__badges { display: flex; - flex-direction: column; - padding: $size-gap-3; gap: 4px; - overflow: hidden; - } - - // ── Header: name + badges + action buttons ── - &__header { - display: flex; - align-items: flex-start; - gap: $size-gap-2; + flex-wrap: wrap; + justify-content: flex-end; } - &__header-text { + // ── Body ── + &__body { flex: 1; - min-width: 0; + padding: $size-gap-2 $size-gap-3; display: flex; - align-items: baseline; - gap: $size-gap-2; - flex-wrap: wrap; + flex-direction: column; + gap: 4px; + overflow: hidden; + position: relative; + z-index: 1; } &__name { - font-size: $font-size-sm; - font-weight: $font-weight-semibold; + font-size: 1.2em; + font-weight: 900; color: var(--color-text-primary); line-height: $line-height-base; word-break: break-word; } - // ── Description ── &__desc { margin: 0; - font-size: $font-size-xs; - color: var(--color-text-secondary); + font-size: 0.85em; + font-weight: 300; + color: rgba(var(--color-text-secondary), 0.85); line-height: $line-height-relaxed; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; } - // ── Meta row ── &__meta { display: flex; align-items: center; @@ -118,29 +142,61 @@ line-height: $line-height-base; } - // ── Action buttons ── + // ── Footer with actions ── + &__footer { + display: flex; + align-items: center; + width: 100%; + border-radius: 0 0 15px 15px; + overflow: hidden; + position: relative; + z-index: 1; + + // Bottom gradient blur background matching card color + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--skill-card-gradient); + opacity: 0.5; + transition: opacity 0.35s ease; + pointer-events: none; + } + } + + &:hover &__footer::after { + opacity: 1; + } + &__actions { display: flex; align-items: center; - gap: 2px; - flex-shrink: 0; + width: 100%; + position: relative; + z-index: 1; } &__action-btn { + flex: 1; display: inline-flex; align-items: center; justify-content: center; - width: 26px; - height: 26px; + height: 35px; padding: 0; border: none; - border-radius: $size-radius-sm; - background: transparent; - color: var(--color-text-muted); + background: rgba(255, 255, 255, 0.08); + color: var(--color-text-secondary); cursor: pointer; transition: - background $motion-fast $easing-standard, - color $motion-fast $easing-standard; + background 0.25s ease, + color 0.25s ease; + + &:not(:last-child) { + border-right: 1px solid rgba(255, 255, 255, 0.06); + } &:disabled { opacity: 0.45; @@ -148,7 +204,7 @@ } &:not(:disabled):hover { - background: var(--element-bg-strong); + background: rgba(255, 255, 255, 0.15); color: var(--color-text-primary); } @@ -163,15 +219,18 @@ &--danger:not(:disabled):hover { color: var(--color-error); } + + &--muted { + background: transparent; + } } } // ── Responsive ── @media (max-width: 720px) { .skill-card { - &__icon-area { - width: 48px; - } + width: 100%; + min-height: 180px; } } diff --git a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx index afdebe6d..bb19b65d 100644 --- a/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx +++ b/src/web-ui/src/app/scenes/skills/components/SkillCard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Package, Puzzle } from 'lucide-react'; -import { getCardGradient } from '@/shared/utils/cardGradients'; +import { getCardGradient, getCardColorRgb } from '@/shared/utils/cardGradients'; import './SkillCard.scss'; type SkillCardActionTone = 'primary' | 'danger' | 'success' | 'muted'; @@ -47,6 +47,7 @@ const SkillCard: React.FC = ({ style={{ '--card-index': index, '--skill-card-gradient': getCardGradient(accentSeed ?? name), + '--skill-card-color-rgb': getCardColorRgb(accentSeed ?? name), } as React.CSSProperties} onClick={openDetails} role="button" @@ -54,46 +55,48 @@ const SkillCard: React.FC = ({ onKeyDown={(e) => e.key === 'Enter' && openDetails()} aria-label={name} > -
-
- + {/* Header: icon + badges */} +
+
+
+ +
+ {badges &&
{badges}
}
+ {/* Body: name + description + meta */}
-
-
- {name} - {badges} -
- {actions.length > 0 && ( -
e.stopPropagation()}> - {actions.map((action) => ( - - ))} -
- )} -
- + {name} {description?.trim() && (

{description.trim()}

)} - {meta &&
{meta}
}
+ + {/* Footer: action buttons */} + {actions.length > 0 && ( +
+
e.stopPropagation()}> + {actions.map((action) => ( + + ))} +
+
+ )}
); }; diff --git a/src/web-ui/src/shared/utils/cardGradients.ts b/src/web-ui/src/shared/utils/cardGradients.ts index 2637e404..66d7c01d 100644 --- a/src/web-ui/src/shared/utils/cardGradients.ts +++ b/src/web-ui/src/shared/utils/cardGradients.ts @@ -1,14 +1,19 @@ const ICON_GRADIENTS = [ - 'linear-gradient(135deg, rgba(59,130,246,0.28) 0%, rgba(139,92,246,0.18) 100%)', - 'linear-gradient(135deg, rgba(16,185,129,0.24) 0%, rgba(59,130,246,0.18) 100%)', - 'linear-gradient(135deg, rgba(245,158,11,0.22) 0%, rgba(239,68,68,0.16) 100%)', - 'linear-gradient(135deg, rgba(139,92,246,0.28) 0%, rgba(236,72,153,0.18) 100%)', - 'linear-gradient(135deg, rgba(6,182,212,0.22) 0%, rgba(59,130,246,0.18) 100%)', + { rgb: '59 130 246', gradient: 'linear-gradient(135deg, rgba(59,130,246,0.28) 0%, rgba(139,92,246,0.18) 100%)' }, + { rgb: '16 185 129', gradient: 'linear-gradient(135deg, rgba(16,185,129,0.24) 0%, rgba(59,130,246,0.18) 100%)' }, + { rgb: '245 158 11', gradient: 'linear-gradient(135deg, rgba(245,158,11,0.22) 0%, rgba(239,68,68,0.16) 100%)' }, + { rgb: '139 92 246', gradient: 'linear-gradient(135deg, rgba(139,92,246,0.28) 0%, rgba(236,72,153,0.18) 100%)' }, + { rgb: '6 182 212', gradient: 'linear-gradient(135deg, rgba(6,182,212,0.22) 0%, rgba(59,130,246,0.18) 100%)' }, ]; function getCardGradient(seed: string): string { const first = seed.trim().charCodeAt(0) || 0; - return ICON_GRADIENTS[first % ICON_GRADIENTS.length]; + return ICON_GRADIENTS[first % ICON_GRADIENTS.length].gradient; } -export { ICON_GRADIENTS, getCardGradient }; +function getCardColorRgb(seed: string): string { + const first = seed.trim().charCodeAt(0) || 0; + return ICON_GRADIENTS[first % ICON_GRADIENTS.length].rgb; +} + +export { ICON_GRADIENTS, getCardGradient, getCardColorRgb }; diff --git a/tests/e2e/specs/insights-screenshot.spec.ts b/tests/e2e/specs/insights-screenshot.spec.ts new file mode 100644 index 00000000..147cf88d --- /dev/null +++ b/tests/e2e/specs/insights-screenshot.spec.ts @@ -0,0 +1,28 @@ +/** + * Quick screenshot test for insights scene. + */ + +import { browser } from '@wdio/globals'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('Insights Screenshot', () => { + it('should take screenshot of insights scene', async () => { + console.log('[Screenshot] Waiting for app to load...'); + await browser.pause(5000); + + const screenshotsDir = path.resolve(__dirname, 'reports', 'screenshots'); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir, { recursive: true }); + } + + const filePath = path.join(screenshotsDir, 'insights-scene.png'); + await browser.saveScreenshot(filePath); + console.log(`[Screenshot] Saved to: ${filePath}`); + }); +});