按钮合并

This commit is contained in:
lilan 2026-04-24 16:10:28 +08:00
parent bdd11126e6
commit 9f5056f554
6 changed files with 401 additions and 116 deletions

View File

@ -1,2 +1,15 @@
# uitocode
# 产品功能
2026.4.23
1、设计师视角需要增加一个原型库的按钮
2、5月将流程跑通
2026.4.21
1、从getdesign.md中选取风格毛玻璃浅色等已完成、确定meta-light
2、提供切换产品、设计视角的能力
3、精简功能标签整个页面或某个选中元素优先级1、设计规范已元素的形式呈现
4、可将工具栏分别收起至左、右两个图片按钮;左侧为根据角色变化的工具,右侧为通用工具(已完成)
5、可扩展功能全屏预览、颜色/字体等的设置(要直观,可参考浏览器自带的拾色器)
6、线框图 -》设计规范 -》 设计稿
7、语音图标放到AI对话框的底部上传文件的图标旁边 -》 语音转文字(已完成)
8、opencode的开源代码

View File

@ -221,6 +221,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
const renderContent = () => {
switch (renderMode) {
case 'iframe':
console.log('iframe', file.content);
if (file.fileType === 'svg') {
const svgHtml = `
<!DOCTYPE html>
@ -250,6 +251,13 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
max-width: 100%;
height: auto;
}
.clickable {
cursor: pointer;
}
.clickable.highlighted {
outline: 2px solid #47A5FA;
outline-offset: 4px;
}
</style>
</head>
<body>
@ -266,6 +274,11 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
}
});
});
document.querySelectorAll('.clickable').forEach(function(element) {
element.addEventListener('click', function() {
this.classList.toggle('highlighted');
});
});
</script>
</body>
</html>
@ -444,6 +457,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
);
case 'html':
console.log('html',file.content);
if (file.fileType === 'svg') {
return (
<div
@ -464,6 +478,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
case 'placeholder':
default:
console.log('placeholder default',file.content);
const placeholderIcon = file.fileType === 'svg' ? '🎨' : '🌐';
const placeholderHint = file.fileType === 'svg' ? t('designFrame.placeholder.svg') : t('designFrame.placeholder.html');

View File

@ -1,6 +1,6 @@
.side-bar {
position: absolute;
top: 100px;
top: 20px;
left: 20px;
width: 48px;
background-color: var(--color-white);
@ -473,3 +473,171 @@
transform: scale(0.9);
opacity: 0.5;
}
.toolbar-panel {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-5);
background-color: var(--color-white);
border-top: 1px solid var(--color-border-subtle);
min-width: 280px;
}
.toolbar-section {
display: flex;
align-items: center;
}
.control-group {
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
.toolbar-btn {
padding: var(--space-3) var(--space-5);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
}
.toolbar-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
transform: scale(1.05);
}
.toolbar-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.toolbar-btn.active {
background-color: var(--color-meta-blue);
border-color: var(--color-meta-blue);
color: var(--color-white);
}
.toolbar-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.zoom-display {
padding: 0 var(--space-3);
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
opacity: 0.8;
min-width: 50px;
text-align: center;
letter-spacing: var(--letter-spacing-normal);
}
.zoom-value {
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.layout-toggle {
display: flex;
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-pill);
overflow: hidden;
}
.toggle-btn {
padding: var(--space-3) var(--space-7);
border: none;
background-color: transparent;
color: var(--color-text-primary);
opacity: 0.8;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
}
.toggle-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
opacity: 1;
}
.toggle-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.toggle-btn.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
.viewport-selector {
display: flex;
gap: var(--space-3);
}
.viewport-btn {
padding: var(--space-3) var(--space-5);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
opacity: 0.8;
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
letter-spacing: var(--letter-spacing-tighter);
}
.viewport-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
opacity: 1;
border-color: var(--color-meta-blue);
}
.viewport-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.viewport-btn.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.viewport-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@ -1,16 +1,59 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import './SideBar.css'
import { ArrowIcon, NoteIcon, CopyIcon, PaletteIcon, PushPinIcon, PlusIcon } from './Icons'
import {
ArrowIcon,
NoteIcon,
CopyIcon,
PaletteIcon,
PushPinIcon,
PlusIcon,
HomeIcon,
RefreshIcon,
GlobeIcon,
MobileIcon,
TabletIcon,
DesktopIcon,
TreeIcon,
LinkIcon
} from './Icons'
import ContextModal from './ContextModal'
import { ViewportMode, LayoutMode } from '../types/canvas.types'
interface SideBarProps {
selectedFrames: string[]
onSelectNote: (frameName: string, note: string) => void
onCreateContext: (title: string, description: string) => void
currentZoom?: number
onZoomChange?: (zoom: number) => void
onResetZoom?: () => void
onResetPositions?: () => void
layoutMode?: LayoutMode
onLayoutModeChange?: (mode: LayoutMode) => void
showConnections?: boolean
onToggleConnections?: () => void
useGlobalViewport?: boolean
onToggleGlobalViewport?: () => void
globalViewportMode?: ViewportMode
onViewportModeChange?: (mode: ViewportMode) => void
}
const SideBar: React.FC<SideBarProps> = ({ selectedFrames, onSelectNote, onCreateContext }) => {
const SideBar: React.FC<SideBarProps> = ({
selectedFrames,
onSelectNote,
onCreateContext,
currentZoom = 1,
onResetZoom,
onResetPositions,
layoutMode = 'grid',
onLayoutModeChange,
showConnections = true,
onToggleConnections,
useGlobalViewport = true,
onToggleGlobalViewport,
globalViewportMode = 'tablet',
onViewportModeChange
}) => {
const { t } = useTranslation()
const [activeItem, setActiveItem] = useState('select')
const [showNoteModal, setShowNoteModal] = useState(false)
@ -19,12 +62,26 @@ const SideBar: React.FC<SideBarProps> = ({ selectedFrames, onSelectNote, onCreat
const [noteModalPosition, setNoteModalPosition] = useState({ x: 0, y: 0 })
const [showResourceModal, setShowResourceModal] = useState(false)
const [showContextModal, setShowContextModal] = useState(false)
const [currentViewportMode, setCurrentViewportMode] = useState<ViewportMode>(globalViewportMode)
// 当globalViewportMode变化时更新currentViewportMode
useEffect(() => {
setCurrentViewportMode(globalViewportMode)
}, [globalViewportMode])
const menuItems = [
{ id: 'select', icon: <ArrowIcon />, label: t('sideBar.menu.select') },
{ id: 'note', icon: <NoteIcon />, label: t('sideBar.menu.note') },
{ id: 'copy', icon: <CopyIcon />, label: t('sideBar.menu.copy') },
// { id: 'copy', icon: <CopyIcon />, label: t('sideBar.menu.copy') },
{ id: 'customize', icon: <PaletteIcon />, label: t('sideBar.menu.customize') },
// Canvas controls
{ id: 'resetZoom', icon: <HomeIcon />, label: t('canvasArea.toolbar.resetZoom') },
{ id: 'resetPositions', icon: <RefreshIcon />, label: t('canvasArea.toolbar.resetPositions') },
{ id: 'layoutGrid', icon: <ArrowIcon />, label: t('canvasArea.toolbar.gridLayout') },
{ id: 'layoutHierarchy', icon: <TreeIcon />, label: t('canvasArea.toolbar.hierarchyLayout') },
{ id: 'toggleConnections', icon: <LinkIcon />, label: t('canvasArea.toolbar.toggleConnections') },
{ id: 'toggleGlobalViewport', icon: <GlobeIcon />, label: t('canvasArea.toolbar.toggleGlobalViewport') },
{ id: 'viewportToggle', icon: currentViewportMode === 'mobile' ? <MobileIcon /> : currentViewportMode === 'tablet' ? <TabletIcon /> : <DesktopIcon />, label: t(`canvasArea.toolbar.${currentViewportMode}View`) },
// { id: 'fix', icon: <PushPinIcon />, label: t('sideBar.menu.fix') },
// { id: 'add', icon: <PlusIcon />, label: t('sideBar.menu.add') },
]
@ -43,20 +100,18 @@ const SideBar: React.FC<SideBarProps> = ({ selectedFrames, onSelectNote, onCreat
setShowNoSelectionMessage(false)
setShowNoteModal(true)
// 计算弹窗位置跟随选中的DesignFrame
const selectedFrame = document.querySelector(`[data-frame-name="${selectedFrames[0]}"]`)
if (selectedFrame) {
const rect = selectedFrame.getBoundingClientRect()
setNoteModalPosition({
x: rect.left + rect.width / 2 - 200, // 居中
y: rect.top + rect.height / 2 - 150 // 居中
x: rect.left + rect.width / 2 - 200,
y: rect.top + rect.height / 2 - 150
})
}
} else {
setShowNoSelectionMessage(true)
setShowNoteModal(false)
// 3秒后自动隐藏提示
setTimeout(() => {
setShowNoSelectionMessage(false)
}, 3000)
@ -65,6 +120,23 @@ const SideBar: React.FC<SideBarProps> = ({ selectedFrames, onSelectNote, onCreat
setShowResourceModal(true)
} else if (item.id === 'fix') {
setShowContextModal(true)
} else if (item.id === 'resetZoom') {
onResetZoom?.()
} else if (item.id === 'resetPositions') {
onResetPositions?.()
} else if (item.id === 'layoutGrid') {
onLayoutModeChange?.('grid')
} else if (item.id === 'layoutHierarchy') {
onLayoutModeChange?.('hierarchy')
} else if (item.id === 'toggleConnections') {
onToggleConnections?.()
} else if (item.id === 'toggleGlobalViewport') {
onToggleGlobalViewport?.()
} else if (item.id === 'viewportToggle') {
// 循环切换viewport模式
const nextMode = currentViewportMode === 'mobile' ? 'tablet' : currentViewportMode === 'tablet' ? 'desktop' : 'mobile'
setCurrentViewportMode(nextMode)
onViewportModeChange?.(nextMode)
}
}}
title={item.label}

View File

@ -112,19 +112,46 @@ interface CanvasAreaProps {
onFrameSelect?: (frames: string[]) => void
designFiles?: DesignFile[]
setDesignFiles?: (files: DesignFile[]) => void
currentZoom?: number
onZoomChange?: (zoom: number) => void
onResetZoom?: () => void
onResetPositions?: () => void
layoutMode?: LayoutMode
onLayoutModeChange?: (mode: LayoutMode) => void
showConnections?: boolean
onToggleConnections?: () => void
useGlobalViewport?: boolean
onToggleGlobalViewport?: () => void
globalViewportMode?: ViewportMode
onViewportModeChange?: (mode: ViewportMode) => void
}
const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: externalDesignFiles }) => {
const CanvasArea: React.FC<CanvasAreaProps> = ({
onFrameSelect,
designFiles: externalDesignFiles,
currentZoom: externalZoom,
onZoomChange,
onResetZoom,
onResetPositions,
layoutMode: externalLayoutMode,
onLayoutModeChange,
showConnections: externalShowConnections,
onToggleConnections,
useGlobalViewport: externalUseGlobalViewport,
onToggleGlobalViewport,
globalViewportMode: externalViewportMode,
onViewportModeChange
}) => {
const { t } = useTranslation()
const [designFiles, setDesignFiles] = useState<DesignFile[]>([])
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [currentZoom, setCurrentZoom] = useState(1)
const [currentZoom, setCurrentZoom] = useState(externalZoom ?? 1)
const [currentConfig, setCurrentConfig] = useState<CanvasConfig>(CANVAS_CONFIG)
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('tablet')
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>(externalViewportMode ?? 'tablet')
const [frameViewports, setFrameViewports] = useState<FrameViewportState>({})
const [useGlobalViewport, setUseGlobalViewport] = useState(true)
const [useGlobalViewport, setUseGlobalViewport] = useState(externalUseGlobalViewport ?? true)
const [customPositions, setCustomPositions] = useState<FramePositionState>({})
const [dragState, setDragState] = useState<DragState>({
isDragging: false,
@ -133,12 +160,45 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
currentPosition: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
})
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid')
const [layoutMode, setLayoutMode] = useState<LayoutMode>(externalLayoutMode ?? 'grid')
const [hierarchyTree, setHierarchyTree] = useState<HierarchyTree | null>(null)
const [showConnections, setShowConnections] = useState(true)
const [showConnections, setShowConnections] = useState(externalShowConnections ?? true)
const transformRef = useRef<ReactZoomPanPinchRef>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (externalZoom !== undefined) {
setCurrentZoom(externalZoom)
if (externalZoom === 1 && transformRef.current) {
transformRef.current.resetTransform()
}
}
}, [externalZoom])
useEffect(() => {
if (externalLayoutMode !== undefined) {
setLayoutMode(externalLayoutMode)
}
}, [externalLayoutMode])
useEffect(() => {
if (externalShowConnections !== undefined) {
setShowConnections(externalShowConnections)
}
}, [externalShowConnections])
useEffect(() => {
if (externalUseGlobalViewport !== undefined) {
setUseGlobalViewport(externalUseGlobalViewport)
}
}, [externalUseGlobalViewport])
useEffect(() => {
if (externalViewportMode !== undefined) {
setGlobalViewportMode(externalViewportMode)
}
}, [externalViewportMode])
// 性能优化:根据缩放级别切换渲染模式
const getOptimalRenderMode = useCallback((zoom: number): 'placeholder' | 'iframe' => {
return zoom < 0.3 ? 'placeholder' : 'iframe'
@ -176,6 +236,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
const handleGlobalViewportChange = useCallback((viewport: ViewportMode) => {
setGlobalViewportMode(viewport)
onViewportModeChange?.(viewport)
if (useGlobalViewport) {
const newFrameViewports: FrameViewportState = {}
designFiles.forEach(f => {
@ -216,6 +277,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
const toggleGlobalViewport = useCallback(() => {
const newUseGlobal = !useGlobalViewport
setUseGlobalViewport(newUseGlobal)
onToggleGlobalViewport?.()
if (newUseGlobal) {
const newFrameViewports: FrameViewportState = {}
@ -224,7 +286,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
})
setFrameViewports(newFrameViewports)
}
}, [useGlobalViewport, designFiles, globalViewportMode])
}, [useGlobalViewport, designFiles, globalViewportMode, onToggleGlobalViewport])
// 响应式配置更新
useEffect(() => {
@ -378,7 +440,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
if (transformRef.current) {
transformRef.current.resetTransform()
}
}, [])
onResetZoom?.()
}, [onResetZoom])
const handleTransformChange = useCallback((ref: any) => {
const state = ref.state
@ -389,7 +452,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
}
setCurrentZoom(state.scale)
}, [])
onZoomChange?.(state.scale)
}, [onZoomChange])
// 获取帧位置(自定义、层级或默认网格位置)
const getFramePosition = useCallback((fileName: string, index: number): GridPosition => {
@ -479,7 +543,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
// 重置位置到网格
const handleResetPositions = useCallback(() => {
setCustomPositions({})
}, [])
onResetPositions?.()
}, [onResetPositions])
// 根据当前帧位置更新连接线位置
const updateConnectionPositions = useCallback((connections: ConnectionLine[], files: DesignFile[]): ConnectionLine[] => {
@ -594,98 +659,6 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
return (
<div className="canvas-area" ref={canvasContainerRef}>
<div className="canvas-toolbar">
<div className="toolbar-section">
<div className="control-group">
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomOut} title={t('canvasArea.toolbar.zoomOut')}>
<ZoomOutIcon />
</button> */}
<div className="zoom-display">
<span className="zoom-value">{Math.round(currentZoom * 100)}%</span>
</div>
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomIn} title={t('canvasArea.toolbar.zoomIn')}>
<ZoomInIcon />
</button> */}
<div className="toolbar-divider"></div>
<button className="toolbar-btn" onClick={handleResetZoom} title={t('canvasArea.toolbar.resetZoom')}>
<HomeIcon />
</button>
<button className="toolbar-btn" onClick={handleResetPositions} title={t('canvasArea.toolbar.resetPositions')}>
<RefreshIcon />
</button>
</div>
</div>
<div className="toolbar-section">
<div className="control-group">
<div className="layout-toggle">
<button
className={`toggle-btn ${layoutMode === 'grid' ? 'active' : ''}`}
onClick={() => setLayoutMode('grid')}
title={t('canvasArea.toolbar.gridLayout')}
>
<span>{t('canvasArea.toolbar.grid')}</span>
</button>
<button
className={`toggle-btn ${layoutMode === 'hierarchy' ? 'active' : ''}`}
onClick={() => setLayoutMode('hierarchy')}
title={t('canvasArea.toolbar.hierarchyLayout')}
disabled={!hierarchyTree || hierarchyTree.nodes.size === 0}
>
<TreeIcon />
</button>
</div>
{layoutMode === 'hierarchy' && (
<button
className={`toolbar-btn connection-btn ${showConnections ? 'active' : ''}`}
onClick={() => setShowConnections(!showConnections)}
title={t('canvasArea.toolbar.toggleConnections')}
>
<LinkIcon />
</button>
)}
</div>
</div>
<div className="toolbar-section">
<div className="control-group">
<button
className={`toolbar-btn viewport-mode-btn ${useGlobalViewport ? 'active' : ''}`}
onClick={toggleGlobalViewport}
title={t('canvasArea.toolbar.toggleGlobalViewport')}
>
<GlobeIcon />
</button>
<div className="viewport-selector">
<button
className={`viewport-btn ${globalViewportMode === 'mobile' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('mobile')}
title={t('canvasArea.toolbar.mobileView')}
disabled={!useGlobalViewport}
>
<MobileIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'tablet' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('tablet')}
title={t('canvasArea.toolbar.tabletView')}
disabled={!useGlobalViewport}
>
<TabletIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'desktop' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('desktop')}
title={t('canvasArea.toolbar.desktopView')}
disabled={!useGlobalViewport}
>
<DesktopIcon />
</button>
</div>
</div>
</div>
</div>
<TransformWrapper
ref={transformRef}
initialScale={1}

View File

@ -1,8 +1,9 @@
import { useState } from 'react'
import { useState, useCallback } from 'react'
import CanvasArea from './CanvasArea'
import TopBar from '../../components/TopBar'
import SideBar from '../../components/SideBar'
import AIChatPanel from '../../components/AIChatPanel'
import { ViewportMode, LayoutMode } from '../../types/canvas.types'
import '../../App.css'
// 模拟设计文件数据
@ -11,7 +12,7 @@ const MOCK_DESIGN_FILES = [
name: 'text_1.html',
path: '/designs/text_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World</h1><p>This is a test design file.</p></div>',
content: '<div className="clickable" style="padding: 20px; font-family: Arial, sans-serif;"><h1 className="clickable">Hello World</h1><p className="clickable">This is a test design file.</p></div>',
fileType: 'html',
relationships: {
parent: undefined,
@ -58,6 +59,11 @@ const MainApp: React.FC = () => {
const [isChatVisible, setIsChatVisible] = useState(true)
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
const [designFiles, setDesignFiles] = useState<any[]>(MOCK_DESIGN_FILES)
const [currentZoom, setCurrentZoom] = useState(1)
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid')
const [showConnections, setShowConnections] = useState(true)
const [useGlobalViewport, setUseGlobalViewport] = useState(true)
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('tablet')
const toggleChat = () => {
setIsChatVisible(!isChatVisible)
@ -93,11 +99,49 @@ const MainApp: React.FC = () => {
setDesignFiles([...designFiles, newFrame])
}
const handleResetZoom = useCallback(() => {
setCurrentZoom(1)
}, [])
const handleResetPositions = useCallback(() => {
}, [])
return (
<div className="app-container">
<CanvasArea onFrameSelect={handleFrameSelect} designFiles={designFiles} setDesignFiles={setDesignFiles} />
<CanvasArea
onFrameSelect={handleFrameSelect}
designFiles={designFiles}
setDesignFiles={setDesignFiles}
currentZoom={currentZoom}
onZoomChange={setCurrentZoom}
onResetZoom={handleResetZoom}
onResetPositions={handleResetPositions}
layoutMode={layoutMode}
onLayoutModeChange={setLayoutMode}
showConnections={showConnections}
onToggleConnections={() => setShowConnections(!showConnections)}
useGlobalViewport={useGlobalViewport}
onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)}
globalViewportMode={globalViewportMode}
onViewportModeChange={setGlobalViewportMode}
/>
<TopBar onToggleChat={toggleChat} isChatVisible={isChatVisible} />
<SideBar selectedFrames={selectedFrames} onSelectNote={handleSelectNote} onCreateContext={handleCreateContext} />
<SideBar
selectedFrames={selectedFrames}
onSelectNote={handleSelectNote}
onCreateContext={handleCreateContext}
currentZoom={currentZoom}
onResetZoom={handleResetZoom}
onResetPositions={handleResetPositions}
layoutMode={layoutMode}
onLayoutModeChange={setLayoutMode}
showConnections={showConnections}
onToggleConnections={() => setShowConnections(!showConnections)}
useGlobalViewport={useGlobalViewport}
onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)}
globalViewportMode={globalViewportMode}
onViewportModeChange={setGlobalViewportMode}
/>
{isChatVisible && <AIChatPanel />}
</div>
)