From 6d2193b856ca669267365a3fc335b9a7d079f6f7 Mon Sep 17 00:00:00 2001 From: GCWing Date: Thu, 26 Mar 2026 16:10:40 +0800 Subject: [PATCH] feat(web-ui): add panel-view scene with isolated canvas store Introduce PanelViewScene and panel-view canvas mode with a dedicated Zustand store for popped panels. Update content-canvas tabs and editor wiring, slate theme and ThemeService, nav and scene bar styles, i18n strings, and installer theme alignment. --- .../src/theme/installerThemesData.ts | 8 +- .../src/app/components/NavPanel/NavPanel.scss | 40 ++---- .../sections/sessions/SessionsSection.scss | 10 +- .../workspaces/WorkspaceListSection.scss | 6 +- .../src/app/components/SceneBar/SceneBar.scss | 4 + .../src/app/components/SceneBar/types.ts | 1 + .../panels/content-canvas/ContentCanvas.tsx | 6 +- .../content-canvas/editor-area/EditorArea.tsx | 3 + .../editor-area/EditorGroup.tsx | 12 ++ .../components/panels/content-canvas/index.ts | 2 +- .../content-canvas/stores/canvasStore.ts | 4 +- .../panels/content-canvas/stores/index.ts | 1 + .../panels/content-canvas/tab-bar/Tab.scss | 28 ++++ .../panels/content-canvas/tab-bar/Tab.tsx | 23 ++- .../panels/content-canvas/tab-bar/TabBar.tsx | 4 + src/web-ui/src/app/scenes/SceneViewport.tsx | 4 + .../app/scenes/panel-view/PanelViewScene.scss | 7 + .../app/scenes/panel-view/PanelViewScene.tsx | 36 +++++ src/web-ui/src/app/scenes/registry.ts | 15 ++ .../src/app/scenes/settings/SettingsNav.scss | 24 ---- .../infrastructure/theme/core/ThemeService.ts | 25 ++++ .../theme/presets/slate-theme.ts | 136 +++++++++--------- src/web-ui/src/locales/en-US/common.json | 3 +- src/web-ui/src/locales/en-US/components.json | 1 + src/web-ui/src/locales/zh-CN/common.json | 3 +- src/web-ui/src/locales/zh-CN/components.json | 1 + 26 files changed, 269 insertions(+), 138 deletions(-) create mode 100644 src/web-ui/src/app/scenes/panel-view/PanelViewScene.scss create mode 100644 src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx diff --git a/BitFun-Installer/src/theme/installerThemesData.ts b/BitFun-Installer/src/theme/installerThemesData.ts index 4fe60dda..65f7e4f6 100644 --- a/BitFun-Installer/src/theme/installerThemesData.ts +++ b/BitFun-Installer/src/theme/installerThemesData.ts @@ -140,10 +140,10 @@ export const THEMES: InstallerTheme[] = [ type: 'dark', colors: { background: { primary: '#1a1c1e', secondary: '#1a1c1e', tertiary: '#1a1c1e', quaternary: '#32363a', elevated: '#1a1c1e', workbench: '#1a1c1e', flowchat: '#1a1c1e', tooltip: 'rgba(42, 45, 48, 0.96)' }, - text: { primary: '#e4e6e8', secondary: '#b8bbc0', muted: '#8a8d92', disabled: '#5a5d62' }, - accent: { '50': 'rgba(107, 155, 213, 0.04)', '100': 'rgba(107, 155, 213, 0.08)', '200': 'rgba(107, 155, 213, 0.15)', '300': 'rgba(107, 155, 213, 0.25)', '400': 'rgba(107, 155, 213, 0.4)', '500': '#6b9bd5', '600': '#5a8bc4', '700': 'rgba(90, 139, 196, 0.8)', '800': 'rgba(90, 139, 196, 0.9)' }, - purple: { '50': 'rgba(165, 180, 252, 0.04)', '100': 'rgba(165, 180, 252, 0.08)', '200': 'rgba(165, 180, 252, 0.15)', '300': 'rgba(165, 180, 252, 0.25)', '400': 'rgba(165, 180, 252, 0.4)', '500': '#a5b4fc', '600': '#8b9adb', '700': 'rgba(139, 154, 219, 0.8)', '800': 'rgba(139, 154, 219, 0.9)' }, - semantic: { success: '#7fb899', warning: '#d4a574', error: '#c9878d', info: '#6b9bd5', highlight: '#d4d6d8', highlightBg: 'rgba(212, 214, 216, 0.12)' }, + text: { primary: '#eef0f3', secondary: '#c8ccd2', muted: '#9ea4ab', disabled: '#65696f' }, + accent: { '50': 'rgba(122, 176, 238, 0.04)', '100': 'rgba(122, 176, 238, 0.08)', '200': 'rgba(122, 176, 238, 0.15)', '300': 'rgba(122, 176, 238, 0.25)', '400': 'rgba(122, 176, 238, 0.4)', '500': '#7ab0ee', '600': '#689ad8', '700': 'rgba(104, 154, 216, 0.8)', '800': 'rgba(104, 154, 216, 0.9)' }, + purple: { '50': 'rgba(184, 198, 255, 0.04)', '100': 'rgba(184, 198, 255, 0.08)', '200': 'rgba(184, 198, 255, 0.15)', '300': 'rgba(184, 198, 255, 0.25)', '400': 'rgba(184, 198, 255, 0.4)', '500': '#b8c4ff', '600': '#9dacf5', '700': 'rgba(157, 172, 245, 0.8)', '800': 'rgba(157, 172, 245, 0.9)' }, + semantic: { success: '#7fb899', warning: '#d4a574', error: '#c9878d', info: '#7ab0ee', highlight: '#e2e4e7', highlightBg: 'rgba(212, 214, 216, 0.12)' }, border: { subtle: 'rgba(255, 255, 255, 0.12)', base: 'rgba(255, 255, 255, 0.18)', medium: 'rgba(255, 255, 255, 0.24)', strong: 'rgba(255, 255, 255, 0.32)', prominent: 'rgba(255, 255, 255, 0.45)' }, element: { subtle: 'rgba(255, 255, 255, 0.06)', soft: 'rgba(255, 255, 255, 0.10)', base: 'rgba(255, 255, 255, 0.13)', medium: 'rgba(255, 255, 255, 0.17)', strong: 'rgba(255, 255, 255, 0.21)', elevated: 'rgba(255, 255, 255, 0.25)' }, }, diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index bdb8f01a..774d6087 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -549,7 +549,7 @@ $_section-header-height: 24px; padding: 0 $size-gap-2; margin: 0 $size-gap-2; border-radius: 4px; - opacity: 0.72; + opacity: 0.92; transition: background $motion-fast $easing-standard, opacity $motion-fast $easing-standard; @@ -561,7 +561,7 @@ $_section-header-height: 24px; background: var(--element-bg-soft); .bitfun-nav-panel__section-label { - color: var(--color-text-secondary); + color: var(--color-text-primary); } } @@ -582,7 +582,7 @@ $_section-header-height: 24px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; - color: var(--color-text-secondary); + color: var(--color-text-primary); flex: 1; white-space: nowrap; overflow: hidden; @@ -627,7 +627,7 @@ $_section-header-height: 24px; border: none; border-radius: 10px; background: transparent; - color: var(--color-text-secondary); + color: var(--color-text-primary); cursor: pointer; transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard, @@ -664,13 +664,6 @@ $_section-header-height: 24px; font-size: 13px; font-weight: 500; line-height: 1.1; - opacity: 0.72; - transition: opacity $motion-fast $easing-standard; - - .bitfun-nav-panel__assistant-entry:hover &, - .bitfun-nav-panel__assistant-entry.is-active & { - opacity: 1; - } } &__miniapp-entry-wrap { @@ -687,7 +680,7 @@ $_section-header-height: 24px; border: none; border-radius: 10px; background: transparent; - color: var(--color-text-secondary); + color: var(--color-text-primary); cursor: pointer; text-align: left; transition: background $motion-fast $easing-standard, @@ -750,13 +743,6 @@ $_section-header-height: 24px; font-size: 13px; font-weight: 500; line-height: 1.1; - opacity: 0.72; - transition: opacity $motion-fast $easing-standard; - - .bitfun-nav-panel__miniapp-entry:hover &, - .bitfun-nav-panel__miniapp-entry.is-active & { - opacity: 1; - } } &__miniapp-entry-apps { @@ -798,7 +784,7 @@ $_section-header-height: 24px; &--more { background: color-mix(in srgb, var(--element-bg-medium) 92%, transparent); - color: var(--color-text-secondary); + color: var(--color-text-primary); font-size: 10px; font-weight: 700; letter-spacing: 0.01em; @@ -1125,21 +1111,21 @@ $_section-header-height: 24px; border: none; border-radius: 4px; background: transparent; - color: var(--color-text-muted); + color: var(--color-text-primary); cursor: pointer; text-align: left; font-size: $font-size-sm; font-weight: 400; width: 100%; position: relative; - opacity: 0.4; + opacity: 1; transition: color $motion-fast $easing-standard, background $motion-fast $easing-standard, opacity $motion-fast $easing-standard; &:hover { opacity: 1; - color: var(--color-text-secondary); + color: var(--color-text-primary); background: var(--element-bg-soft); } @@ -1191,12 +1177,12 @@ $_section-header-height: 24px; display: flex; align-items: center; flex-shrink: 0; - opacity: 0.5; + opacity: 0.78; transition: opacity $motion-fast $easing-standard, color $motion-fast $easing-standard; .bitfun-nav-panel__item:hover & { - opacity: 0.75; + opacity: 1; } } @@ -1846,7 +1832,7 @@ $_section-header-height: 24px; border: none; border-radius: $size-radius-base; background: transparent; - color: var(--color-text-secondary); + color: var(--color-text-primary); font-size: 12.5px; font-weight: 500; cursor: pointer; @@ -1965,13 +1951,11 @@ $_section-header-height: 24px; transform-origin: center; transition: background $motion-fast $easing-standard, color $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard, transform $motion-fast $easing-standard; .bitfun-nav-panel__top-action-btn:hover & { background: var(--element-bg-strong); color: var(--color-primary); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 28%, transparent); transform: scale(1.07); } } 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 4cbd5a69..22508acf 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 @@ -63,7 +63,7 @@ border: none; border-radius: 4px; background: transparent; - color: var(--color-text-secondary); + color: var(--color-text-primary); font-size: 13px; font-weight: 400; cursor: pointer; @@ -137,17 +137,17 @@ &__inline-item-icon { flex-shrink: 0; - opacity: 0.6; + opacity: 0.78; display: flex; align-items: center; - color: var(--color-text-muted); + color: color-mix(in srgb, var(--color-text-primary) 72%, var(--color-text-muted)); &.is-code { - color: color-mix(in srgb, var(--color-primary) 70%, var(--color-text-muted)); + color: color-mix(in srgb, var(--color-primary) 70%, var(--color-text-primary)); } &.is-cowork { - color: color-mix(in srgb, var(--color-accent-500) 70%, var(--color-text-muted)); + color: color-mix(in srgb, var(--color-accent-500) 70%, var(--color-text-primary)); } &.is-running { diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss index a6e18210..69b29319 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss @@ -27,7 +27,7 @@ align-items: center; justify-content: space-between; gap: $size-gap-1; - color: var(--color-text-muted); + color: var(--color-text-primary); font-size: 10px; font-weight: 700; letter-spacing: 0.06em; @@ -153,7 +153,7 @@ width: 100%; min-height: 30px; border-radius: 6px; - color: var(--color-text-secondary); + color: var(--color-text-primary); transition: color $motion-fast $easing-standard, background $motion-fast $easing-standard; @@ -296,7 +296,7 @@ &__workspace-item-icon-toggle { opacity: 0; transform: scale(0.7); - color: var(--color-text-secondary); + color: var(--color-text-primary); svg { transition: transform $motion-fast $easing-standard; diff --git a/src/web-ui/src/app/components/SceneBar/SceneBar.scss b/src/web-ui/src/app/components/SceneBar/SceneBar.scss index 29b16226..cc3fdc91 100644 --- a/src/web-ui/src/app/components/SceneBar/SceneBar.scss +++ b/src/web-ui/src/app/components/SceneBar/SceneBar.scss @@ -227,6 +227,10 @@ $_tab-v-margin: 6px; // symmetric top/bottom gap inside SceneBar } } +[data-theme='bitfun-slate'] .bitfun-scene-tab__icon { + opacity: 0.94; +} + @media (prefers-reduced-motion: reduce) { .bitfun-scene-tab { transition: none; diff --git a/src/web-ui/src/app/components/SceneBar/types.ts b/src/web-ui/src/app/components/SceneBar/types.ts index 0a481144..6f8b8b9b 100644 --- a/src/web-ui/src/app/components/SceneBar/types.ts +++ b/src/web-ui/src/app/components/SceneBar/types.ts @@ -21,6 +21,7 @@ export type SceneTabId = | 'assistant' | 'insights' | 'shell' + | 'panel-view' | `miniapp:${string}`; /** Static definition (from registry) for a scene tab type */ diff --git a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx index d39b9795..036903dd 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx @@ -22,12 +22,15 @@ export interface ContentCanvasProps { onInteraction?: (itemId: string, userInput: string) => Promise; /** Before-close callback */ onBeforeClose?: (content: any) => Promise; + /** Disable pop-out and panel-close controls (used in panel-view scene) */ + disablePopOut?: boolean; } export const ContentCanvas: React.FC = ({ workspacePath, mode = 'agent', onInteraction, + disablePopOut = false, }) => { // Store state const { @@ -102,7 +105,7 @@ export const ContentCanvas: React.FC = ({ const renderContent = () => { // Show empty state when primary group has no visible tabs if (!hasPrimaryVisibleTabs) { - return ; + return ; } return ( @@ -115,6 +118,7 @@ export const ContentCanvas: React.FC = ({ onInteraction={onInteraction} onTabCloseWithDirtyCheck={handleCloseWithDirtyCheck} onTabCloseAllWithDirtyCheck={handleCloseAllWithDirtyCheck} + disablePopOut={disablePopOut} /> diff --git a/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorArea.tsx b/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorArea.tsx index 2cf476ad..3904b32d 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorArea.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorArea.tsx @@ -15,6 +15,7 @@ export interface EditorAreaProps { onInteraction?: (itemId: string, userInput: string) => Promise; onTabCloseWithDirtyCheck?: (tabId: string, groupId: EditorGroupId) => Promise; onTabCloseAllWithDirtyCheck?: (groupId: EditorGroupId) => Promise; + disablePopOut?: boolean; } export const EditorArea: React.FC = ({ @@ -23,6 +24,7 @@ export const EditorArea: React.FC = ({ onInteraction, onTabCloseWithDirtyCheck, onTabCloseAllWithDirtyCheck, + disablePopOut = false, }) => { const containerRef = useRef(null); const topRowRef = useRef(null); @@ -142,6 +144,7 @@ export const EditorArea: React.FC = ({ onOpenMissionControl={groupId === 'primary' ? onOpenMissionControl : undefined} onCloseAllTabs={handleCloseAllTabs(groupId)} onInteraction={onInteraction} + disablePopOut={disablePopOut} /> ); diff --git a/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorGroup.tsx b/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorGroup.tsx index 1128542c..b789165c 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorGroup.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/editor-area/EditorGroup.tsx @@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next'; import { TabBar } from '../tab-bar'; import { DropZone } from './DropZone'; import FlexiblePanel from '../../base/FlexiblePanel'; +import { usePanelViewCanvasStore } from '../stores'; +import { useSceneStore } from '../../../../stores/sceneStore'; import type { EditorGroupId, EditorGroupState, @@ -41,6 +43,7 @@ export interface EditorGroupProps { onOpenMissionControl?: () => void; onCloseAllTabs?: () => Promise | void; onInteraction?: (itemId: string, userInput: string) => Promise; + disablePopOut?: boolean; } export const EditorGroup: React.FC = ({ @@ -66,6 +69,7 @@ export const EditorGroup: React.FC = ({ onOpenMissionControl, onCloseAllTabs, onInteraction, + disablePopOut = false, }) => { const { t } = useTranslation('components'); const visibleTabs = useMemo(() => group.tabs.filter(t => !t.isHidden), [group.tabs]); @@ -119,6 +123,13 @@ export const EditorGroup: React.FC = ({ } }, [group.activeTabId, onDirtyStateChange]); + const handleTabPopOut = useCallback((tabId: string) => { + const tab = group.tabs.find(t => t.id === tabId); + if (!tab || !tab.content) return; + usePanelViewCanvasStore.getState().addTab(tab.content as PanelContent, 'active'); + useSceneStore.getState().openScene('panel-view'); + }, [group.tabs]); + const isDragging = draggingTabId !== null; return ( @@ -142,6 +153,7 @@ export const EditorGroup: React.FC = ({ onReorderTab={onReorderTab} onOpenMissionControl={onOpenMissionControl} onCloseAllTabs={onCloseAllTabs} + onTabPopOut={disablePopOut ? undefined : handleTabPopOut} /> create()( })) ); -export type CanvasStoreMode = 'agent' | 'project' | 'git'; +export type CanvasStoreMode = 'agent' | 'project' | 'git' | 'panel-view'; /** * Selects which canvas store instance is used by the current subtree. @@ -1081,10 +1081,12 @@ export const CanvasStoreModeContext = createContext('agent'); export const useAgentCanvasStore = createCanvasStoreHook(); export const useProjectCanvasStore = createCanvasStoreHook(); export const useGitCanvasStore = createCanvasStoreHook(); +export const usePanelViewCanvasStore = createCanvasStoreHook(); const pickStoreByMode = (mode: CanvasStoreMode) => { if (mode === 'project') return useProjectCanvasStore; if (mode === 'git') return useGitCanvasStore; + if (mode === 'panel-view') return usePanelViewCanvasStore; return useAgentCanvasStore; }; diff --git a/src/web-ui/src/app/components/panels/content-canvas/stores/index.ts b/src/web-ui/src/app/components/panels/content-canvas/stores/index.ts index c3b80b8a..28e4d0f5 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/stores/index.ts +++ b/src/web-ui/src/app/components/panels/content-canvas/stores/index.ts @@ -8,6 +8,7 @@ export { useAgentCanvasStore, useProjectCanvasStore, useGitCanvasStore, + usePanelViewCanvasStore, useGroupTabs, useActiveTabId, useLayout, diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss index 52973410..7103721a 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss @@ -136,6 +136,34 @@ margin-left: 2px; } + // Pop out button + &__popout-btn { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + background: none; + border: none; + color: var(--color-text-secondary, rgba(255, 255, 255, 0.6)); + cursor: pointer; + border-radius: 2px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s ease, background-color 0.15s ease; + + &:hover { + background: var(--color-bg-hover, rgba(255, 255, 255, 0.1)); + color: var(--color-text-primary, rgba(255, 255, 255, 0.9)); + } + } + + // Show pop out button on hover + &:hover &__popout-btn { + opacity: 1; + } + // Close button &__close-btn { display: flex; diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx index f69e39d1..df729c65 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx @@ -4,7 +4,7 @@ */ import React, { useCallback, useState } from 'react'; -import { X, Pin, Split } from 'lucide-react'; +import { X, Pin, Split, ExternalLink } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Tooltip } from '@/component-library'; import type { CanvasTab, EditorGroupId, TabState } from '../types'; @@ -30,6 +30,8 @@ export interface TabProps { onDragEnd: () => void; /** Whether being dragged */ isDragging?: boolean; + /** Pop out as independent scene */ + onPopOut?: () => void; } /** @@ -57,6 +59,7 @@ export const Tab: React.FC = ({ onDragStart, onDragEnd, isDragging = false, + onPopOut, }) => { const { t } = useTranslation('components'); const [isHovered, setIsHovered] = useState(false); @@ -93,6 +96,12 @@ export const Tab: React.FC = ({ onPin(); }, [onPin]); + // Handle pop out click + const handlePopOutClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + onPopOut?.(); + }, [onPopOut]); + // Handle drag start const handleDragStart = useCallback((e: React.DragEvent) => { e.dataTransfer.setData('application/json', JSON.stringify({ @@ -166,6 +175,18 @@ export const Tab: React.FC = ({ )} + {/* Pop out button */} + {showCloseButton && onPopOut && ( + + + + )} + {/* Close button */} {showCloseButton && ( diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx index 6fd4aa59..2e5a6686 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/TabBar.tsx @@ -44,6 +44,8 @@ export interface TabBarProps { onOpenMissionControl?: () => void; /** Close all tabs */ onCloseAllTabs?: () => Promise | void; + /** Pop out tab as independent scene */ + onTabPopOut?: (tabId: string) => void; } /** @@ -94,6 +96,7 @@ export const TabBar: React.FC = ({ onReorderTab, onOpenMissionControl, onCloseAllTabs, + onTabPopOut, }) => { const { t } = useTranslation('components'); const [visibleTabsCount, setVisibleTabsCount] = useState(tabs.length); @@ -317,6 +320,7 @@ export const TabBar: React.FC = ({ onDragStart={handleTabDragStart(tab)} onDragEnd={onDragEnd} isDragging={draggingTabId === tab.id} + onPopOut={onTabPopOut ? () => onTabPopOut(tab.id) : undefined} /> ))} diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index db534619..dff4d998 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -32,6 +32,8 @@ const InsightsScene = lazy(() => import('./my-agent/InsightsScene')); const ShellScene = lazy(() => import('./shell/ShellScene')); const WelcomeScene = lazy(() => import('./welcome/WelcomeScene')); const MiniAppScene = lazy(() => import('./miniapps/MiniAppScene')); +const PanelViewScene = lazy(() => import('./panel-view/PanelViewScene')); + interface SceneViewportProps { workspacePath?: string; @@ -119,6 +121,8 @@ function renderScene(id: SceneTabId, workspacePath?: string, isEntering?: boolea return ; case 'shell': return ; + case 'panel-view': + return ; default: if (typeof id === 'string' && id.startsWith('miniapp:')) { return ; diff --git a/src/web-ui/src/app/scenes/panel-view/PanelViewScene.scss b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.scss new file mode 100644 index 00000000..dbb73ccd --- /dev/null +++ b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.scss @@ -0,0 +1,7 @@ +.bitfun-panel-view-scene { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} diff --git a/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx new file mode 100644 index 00000000..c59766fd --- /dev/null +++ b/src/web-ui/src/app/scenes/panel-view/PanelViewScene.tsx @@ -0,0 +1,36 @@ +/** + * PanelViewScene — a standalone scene that hosts a ContentCanvas + * with its own independent store (panel-view mode). + * + * Tabs popped out from the agent scene's right panel are added here, + * allowing them all to be viewed and managed in one place. + */ + +import React, { useCallback } from 'react'; +import { ContentCanvas, CanvasStoreModeContext } from '../../components/panels/content-canvas'; +import './PanelViewScene.scss'; + +interface PanelViewSceneProps { + workspacePath?: string; +} + +const PanelViewScene: React.FC = ({ workspacePath }) => { + const handleInteraction = useCallback(async (_itemId: string, _userInput: string) => { + // no-op + }, []); + + return ( + +
+ +
+
+ ); +}; + +export default PanelViewScene; diff --git a/src/web-ui/src/app/scenes/registry.ts b/src/web-ui/src/app/scenes/registry.ts index aa1d683b..97b8a0c6 100644 --- a/src/web-ui/src/app/scenes/registry.ts +++ b/src/web-ui/src/app/scenes/registry.ts @@ -21,6 +21,7 @@ import { Network, User, BarChart3, + ExternalLink, } from 'lucide-react'; import type { SceneTabDef, SceneTabId } from '../components/SceneBar/types'; @@ -158,12 +159,26 @@ export const SCENE_TAB_REGISTRY: SceneTabDef[] = [ singleton: true, defaultOpen: false, }, + { + id: 'panel-view' as SceneTabId, + label: 'Panel View', + labelKey: 'scenes.panelView', + Icon: ExternalLink, + pinned: false, + fixed: false, + closable: true, + singleton: true, + defaultOpen: false, + }, ]; export function getSceneDef(id: SceneTabId): SceneTabDef | undefined { return SCENE_TAB_REGISTRY.find(d => d.id === id); } +/** Static singleton scene def for the panel-view scene. */ +export const PANEL_VIEW_SCENE_DEF: SceneTabDef = SCENE_TAB_REGISTRY.find(d => d.id === 'panel-view')!; + /** Dynamic scene def for a MiniApp tab (used by SceneBar and useSceneManager). */ export function getMiniAppSceneDef(appId: string, appName?: string): SceneTabDef { const id: SceneTabId = `miniapp:${appId}`; diff --git a/src/web-ui/src/app/scenes/settings/SettingsNav.scss b/src/web-ui/src/app/scenes/settings/SettingsNav.scss index 8fcb22e6..b85d1192 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsNav.scss +++ b/src/web-ui/src/app/scenes/settings/SettingsNav.scss @@ -104,18 +104,6 @@ color: var(--color-text-primary); background: var(--element-bg-soft); font-weight: 500; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 2px; - height: 16px; - background: var(--color-primary); - border-radius: 0 2px 2px 0; - } } &:focus-visible { @@ -233,18 +221,6 @@ color: var(--color-text-primary); background: var(--element-bg-soft); font-weight: 500; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 2px; - height: 16px; - background: var(--color-primary); - border-radius: 0 2px 2px 0; - } } &:focus-visible { diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index b5fe1184..0ac54571 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -21,6 +21,21 @@ import { createLogger } from '@/shared/utils/logger'; const log = createLogger('ThemeService'); +/** Space-separated R G B for `rgba(var(--color-primary-rgb) / α)` in component styles. */ +function accentColorToRgbChannels(accent: string): string | null { + const trimmed = accent.trim(); + const hex6 = /^#([0-9a-f]{6})$/i.exec(trimmed); + if (hex6) { + const n = parseInt(hex6[1], 16); + return `${(n >> 16) & 255} ${(n >> 8) & 255} ${n & 255}`; + } + const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(trimmed); + if (rgb) { + return `${rgb[1]} ${rgb[2]} ${rgb[3]}`; + } + return null; +} + export class ThemeService { private themes: Map = new Map(); @@ -314,6 +329,16 @@ export class ThemeService { Object.entries(colors.accent).forEach(([key, value]) => { root.style.setProperty(`--color-accent-${key}`, value); }); + + const primaryAccent = colors.accent[500]; + const primaryHover = colors.accent[600]; + root.style.setProperty('--color-primary', primaryAccent); + root.style.setProperty('--color-primary-hover', primaryHover); + root.style.setProperty('--color-accent', primaryAccent); + const primaryRgb = accentColorToRgbChannels(primaryAccent); + if (primaryRgb) { + root.style.setProperty('--color-primary-rgb', primaryRgb); + } if (colors.purple) { diff --git a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts index c6342d3f..5f5e41e6 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -9,7 +9,7 @@ export const bitfunSlateTheme: ThemeConfig = { type: 'dark', description: 'Slate gray geometric theme - Deep immersion, high contrast grayscale aesthetics', author: 'BitFun Team', - version: '1.2.3', + version: '1.2.5', colors: { @@ -25,36 +25,36 @@ export const bitfunSlateTheme: ThemeConfig = { }, text: { - primary: '#e4e6e8', - secondary: '#b8bbc0', - muted: '#8a8d92', - disabled: '#5a5d62', + primary: '#eef0f3', + secondary: '#c8ccd2', + muted: '#9ea4ab', + disabled: '#65696f', }, accent: { - 50: 'rgba(107, 155, 213, 0.04)', - 100: 'rgba(107, 155, 213, 0.08)', - 200: 'rgba(107, 155, 213, 0.15)', - 300: 'rgba(107, 155, 213, 0.25)', - 400: 'rgba(107, 155, 213, 0.4)', - 500: '#6b9bd5', - 600: '#5a8bc4', - 700: 'rgba(90, 139, 196, 0.8)', - 800: 'rgba(90, 139, 196, 0.9)', + 50: 'rgba(122, 176, 238, 0.04)', + 100: 'rgba(122, 176, 238, 0.08)', + 200: 'rgba(122, 176, 238, 0.15)', + 300: 'rgba(122, 176, 238, 0.25)', + 400: 'rgba(122, 176, 238, 0.4)', + 500: '#7ab0ee', + 600: '#689ad8', + 700: 'rgba(104, 154, 216, 0.8)', + 800: 'rgba(104, 154, 216, 0.9)', }, purple: { - 50: 'rgba(165, 180, 252, 0.04)', - 100: 'rgba(165, 180, 252, 0.08)', - 200: 'rgba(165, 180, 252, 0.15)', - 300: 'rgba(165, 180, 252, 0.25)', - 400: 'rgba(165, 180, 252, 0.4)', - 500: '#a5b4fc', - 600: '#8b9adb', - 700: 'rgba(139, 154, 219, 0.8)', - 800: 'rgba(139, 154, 219, 0.9)', + 50: 'rgba(184, 198, 255, 0.04)', + 100: 'rgba(184, 198, 255, 0.08)', + 200: 'rgba(184, 198, 255, 0.15)', + 300: 'rgba(184, 198, 255, 0.25)', + 400: 'rgba(184, 198, 255, 0.4)', + 500: '#b8c4ff', + 600: '#9dacf5', + 700: 'rgba(157, 172, 245, 0.8)', + 800: 'rgba(157, 172, 245, 0.9)', }, semantic: { @@ -70,12 +70,12 @@ export const bitfunSlateTheme: ThemeConfig = { errorBg: 'rgba(201, 135, 141, 0.1)', errorBorder: 'rgba(201, 135, 141, 0.3)', - info: '#6b9bd5', - infoBg: 'rgba(107, 155, 213, 0.1)', - infoBorder: 'rgba(107, 155, 213, 0.3)', + info: '#7ab0ee', + infoBg: 'rgba(122, 176, 238, 0.1)', + infoBorder: 'rgba(122, 176, 238, 0.3)', - highlight: '#d4d6d8', + highlight: '#e2e4e7', highlightBg: 'rgba(212, 214, 216, 0.12)', }, @@ -97,8 +97,8 @@ export const bitfunSlateTheme: ThemeConfig = { }, git: { - branch: '#6b9bd5', - branchBg: 'rgba(107, 155, 213, 0.1)', + branch: '#7ab0ee', + branchBg: 'rgba(122, 176, 238, 0.1)', changes: 'rgb(212, 165, 116)', changesBg: 'rgba(212, 165, 116, 0.1)', added: 'rgb(127, 184, 153)', @@ -127,9 +127,9 @@ export const bitfunSlateTheme: ThemeConfig = { }, glow: { - blue: '0 12px 32px rgba(107, 155, 213, 0.22), 0 6px 16px rgba(107, 155, 213, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', - purple: '0 12px 32px rgba(165, 180, 252, 0.22), 0 6px 16px rgba(165, 180, 252, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', - mixed: '0 12px 32px rgba(107, 155, 213, 0.18), 0 6px 16px rgba(165, 180, 252, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', + blue: '0 12px 32px rgba(122, 176, 238, 0.22), 0 6px 16px rgba(122, 176, 238, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', + purple: '0 12px 32px rgba(184, 198, 255, 0.22), 0 6px 16px rgba(184, 198, 255, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', + mixed: '0 12px 32px rgba(122, 176, 238, 0.18), 0 6px 16px rgba(184, 198, 255, 0.15), 0 3px 8px rgba(0, 0, 0, 0.2)', }, blur: { @@ -227,20 +227,20 @@ export const bitfunSlateTheme: ThemeConfig = { windowControls: { minimize: { - dot: 'rgba(107, 155, 213, 0.5)', - dotShadow: '0 0 4px rgba(107, 155, 213, 0.25)', - hoverBg: 'rgba(107, 155, 213, 0.15)', - hoverColor: '#6b9bd5', - hoverBorder: 'rgba(107, 155, 213, 0.25)', - hoverShadow: '0 2px 8px rgba(107, 155, 213, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', + dot: 'rgba(122, 176, 238, 0.5)', + dotShadow: '0 0 4px rgba(122, 176, 238, 0.25)', + hoverBg: 'rgba(122, 176, 238, 0.15)', + hoverColor: '#7ab0ee', + hoverBorder: 'rgba(122, 176, 238, 0.25)', + hoverShadow: '0 2px 8px rgba(122, 176, 238, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', }, maximize: { - dot: 'rgba(107, 155, 213, 0.5)', - dotShadow: '0 0 4px rgba(107, 155, 213, 0.25)', - hoverBg: 'rgba(107, 155, 213, 0.15)', - hoverColor: '#6b9bd5', - hoverBorder: 'rgba(107, 155, 213, 0.25)', - hoverShadow: '0 2px 8px rgba(107, 155, 213, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', + dot: 'rgba(122, 176, 238, 0.5)', + dotShadow: '0 0 4px rgba(122, 176, 238, 0.25)', + hoverBg: 'rgba(122, 176, 238, 0.15)', + hoverColor: '#7ab0ee', + hoverBorder: 'rgba(122, 176, 238, 0.25)', + hoverShadow: '0 2px 8px rgba(122, 176, 238, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', }, close: { dot: 'rgba(201, 135, 141, 0.5)', @@ -251,9 +251,9 @@ export const bitfunSlateTheme: ThemeConfig = { hoverShadow: '0 2px 8px rgba(201, 135, 141, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', }, common: { - defaultColor: 'rgba(212, 214, 216, 0.85)', - defaultDot: 'rgba(168, 171, 176, 0.25)', - disabledDot: 'rgba(168, 171, 176, 0.12)', + defaultColor: 'rgba(232, 234, 236, 0.92)', + defaultDot: 'rgba(198, 202, 208, 0.48)', + disabledDot: 'rgba(168, 171, 176, 0.2)', flowGradient: 'linear-gradient(90deg, transparent, rgba(168, 171, 176, 0.06), rgba(168, 171, 176, 0.10), rgba(168, 171, 176, 0.06), transparent)', }, }, @@ -262,20 +262,20 @@ export const bitfunSlateTheme: ThemeConfig = { default: { background: 'rgba(168, 171, 176, 0.12)', - color: '#a8abb0', + color: '#b8bcc2', border: 'transparent', shadow: 'none', }, hover: { background: 'rgba(168, 171, 176, 0.18)', - color: '#c4c6c9', + color: '#d6d8dc', border: 'transparent', shadow: 'none', transform: 'none', }, active: { background: 'rgba(168, 171, 176, 0.15)', - color: '#c4c6c9', + color: '#d6d8dc', border: 'transparent', shadow: 'none', transform: 'none', @@ -284,21 +284,21 @@ export const bitfunSlateTheme: ThemeConfig = { primary: { default: { - background: 'rgba(107, 155, 213, 0.22)', - color: '#6b9bd5', + background: 'rgba(122, 176, 238, 0.22)', + color: '#7ab0ee', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(107, 155, 213, 0.32)', - color: '#8bb0e0', + background: 'rgba(122, 176, 238, 0.32)', + color: '#9cc4f4', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(107, 155, 213, 0.26)', - color: '#8bb0e0', + background: 'rgba(122, 176, 238, 0.26)', + color: '#9cc4f4', border: 'transparent', shadow: 'none', transform: 'none', @@ -309,20 +309,20 @@ export const bitfunSlateTheme: ThemeConfig = { ghost: { default: { background: 'transparent', - color: '#a8abb0', + color: '#b8bcc2', border: 'transparent', shadow: 'none', }, hover: { background: 'rgba(168, 171, 176, 0.14)', - color: '#c4c6c9', + color: '#d6d8dc', border: 'transparent', shadow: 'none', transform: 'none', }, active: { background: 'rgba(168, 171, 176, 0.11)', - color: '#c4c6c9', + color: '#d6d8dc', border: 'transparent', shadow: 'none', transform: 'none', @@ -336,26 +336,26 @@ export const bitfunSlateTheme: ThemeConfig = { base: 'vs-dark', inherit: true, rules: [ - { token: 'comment', foreground: '8a8d92', fontStyle: 'italic' }, - { token: 'keyword', foreground: '6b9bd5' }, + { token: 'comment', foreground: '9ca2a9', fontStyle: 'italic' }, + { token: 'keyword', foreground: '7ab0ee' }, { token: 'string', foreground: '8fc8a9' }, { token: 'number', foreground: 'b5c4fc' }, { token: 'type', foreground: 'e4b584' }, { token: 'class', foreground: 'e4b584' }, - { token: 'function', foreground: '8bb0e0' }, - { token: 'variable', foreground: 'b8bbc0' }, + { token: 'function', foreground: '9cc4f4' }, + { token: 'variable', foreground: 'c4c8ce' }, { token: 'constant', foreground: 'b5c4fc' }, - { token: 'operator', foreground: '6b9bd5' }, + { token: 'operator', foreground: '7ab0ee' }, { token: 'tag', foreground: 'e4b584' }, - { token: 'attribute.name', foreground: 'b8bbc0' }, + { token: 'attribute.name', foreground: 'c4c8ce' }, { token: 'attribute.value', foreground: '8fc8a9' }, ], colors: { background: '#1a1c1e', - foreground: '#e4e6e8', + foreground: '#eef0f3', lineHighlight: '#22252a', - selection: 'rgba(107, 155, 213, 0.3)', - cursor: '#6b9bd5', + selection: 'rgba(122, 176, 238, 0.3)', + cursor: '#7ab0ee', }, }, }; diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index f5d12ee4..c1f6d4f2 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -792,7 +792,8 @@ "mermaidEditor": "Mermaid", "insights": "Insights", "assistant": "Assistant", - "shell": "Shell" + "shell": "Shell", + "panelView": "Panel View" }, "loading": { "scenes": "Loading scene" diff --git a/src/web-ui/src/locales/en-US/components.json b/src/web-ui/src/locales/en-US/components.json index d5315777..79c011bd 100644 --- a/src/web-ui/src/locales/en-US/components.json +++ b/src/web-ui/src/locales/en-US/components.json @@ -330,6 +330,7 @@ }, "tabs": { "close": "Close Tab", + "popOut": "Pop out as scene", "closeOthers": "Close Other Tabs", "closeAll": "Close All Tabs", "closeRight": "Close Tabs to the Right", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index f9615dd4..e3173b8e 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -792,7 +792,8 @@ "mermaidEditor": "Mermaid 图表", "insights": "洞察", "assistant": "助理", - "shell": "Shell" + "shell": "Shell", + "panelView": "面板视图" }, "loading": { "scenes": "正在加载场景" diff --git a/src/web-ui/src/locales/zh-CN/components.json b/src/web-ui/src/locales/zh-CN/components.json index cf288633..8d04f78a 100644 --- a/src/web-ui/src/locales/zh-CN/components.json +++ b/src/web-ui/src/locales/zh-CN/components.json @@ -330,6 +330,7 @@ }, "tabs": { "close": "关闭标签", + "popOut": "弹出为独立场景", "closeOthers": "关闭其他标签", "closeAll": "关闭所有标签", "closeRight": "关闭右侧标签",