openpencil/src/components/editor/editor-layout.tsx
Kayshen Xu ffbd2be112 V0.1.1 (#17)
* chore(electron): update mac build scripts for improved artifact handling

- Modified the `electron:build:mac-arm64` script to rename the generated YAML file for better clarity.
- Adjusted the `electron:build:mac-both` script to run builds sequentially without file renaming logic, ensuring consistent output.

* chore(electron): enable notarization for macOS builds and update build workflow secrets

- Added notarization support in `electron-builder.yml` for enhanced security.
- Updated GitHub Actions workflow to include necessary Apple credentials for notarization.

* chore(electron): add additional secrets for macOS notarization in build workflow

- Included CSC_LINK and CSC_KEY_PASSWORD in the GitHub Actions workflow to support code signing for macOS builds.

* feat(types): add ImageFitMode type and objectFit to ImageNode

Support fill/fit/crop/tile image scaling modes, matching Figma's
image fill behavior. Default is 'fill' (cover) for backward compat.

* feat(canvas): render images per fill mode with native crop

Add computeImageTransform helper supporting fill/fit/crop/tile modes.
Fill/crop uses FabricImage native cropX/cropY instead of clipPath to
avoid conflict with parent frame clipping. Tile mode creates a Rect
with Pattern fill. Detect mode changes via __needsRecreation flag for
object recreation when switching between tile and non-tile modes.

* feat(panels): add image fit mode dropdown to property panel

New ImageSection component with Fill/Fit/Crop/Tile dropdown for
image nodes. Wired into PropertyPanel between icon and appearance
sections.

* feat(figma): preserve image scale mode from Figma import

Map Figma imageScaleMode (FIT/FILL/TILE) to objectFit property on
imported ImageNodes so fill mode is preserved across import.

* fix(canvas): fix zoom-to-fit bounds inflated by clipped children

computeDocBounds was recursing into frame children, inflating the
bounding box beyond visible frame bounds. Now only recurses into
groups. Also use double-RAF in Figma import for reliable timing.

* feat(figma): implement Figma clipboard paste functionality

- Added a new hook, useFigmaPaste, to handle pasting Figma clipboard data into the canvas.
- Integrated clipboard data extraction and processing to convert Figma nodes into PenNodes.
- Enhanced keyboard shortcuts to attempt reading Figma data from the system clipboard as a fallback.
- Introduced utility functions for decoding and processing Figma clipboard HTML data.
- Updated editor layout to utilize the new Figma paste functionality.

* feat(figma): implement Figma clipboard support for pasting nodes

- Added a new hook, `useFigmaPaste`, to handle Figma clipboard data extraction and processing.
- Integrated Figma clipboard support into the editor layout and keyboard shortcuts for seamless pasting.
- Updated README to reflect changes in file format from `.pen` to `.op`.
- Refactored AI service methods to route to appropriate provider SDK based on the `provider` field, enhancing flexibility in AI interactions.

* fix(figma): preserve imported node order and disable openpencil auto layout

Prevent imported/generated nodes from being prepended in auto-layout containers, which could reverse visual order during progressive insertion.

Hide the unfinished OpenPencil auto-layout path from the import dialog to avoid selecting a mode that is not ready yet.

* fix(ai): enforce explicit provider and model routing

Pass selected provider and model through design generation, orchestration, sub-agent, and validation flows.

Disable provider/model fallback and remove model retry-without-selection behavior so requests fail fast instead of silently routing to Claude.

* chore(package): bump version to 0.1.1

---------

Co-authored-by: Fini <fini.yang@gmail.com>
2026-03-02 22:26:09 +08:00

172 lines
6.2 KiB
TypeScript

import { lazy, Suspense, useState, useCallback, useEffect } from 'react'
import { TooltipProvider } from '@/components/ui/tooltip'
import TopBar from './top-bar'
import Toolbar from './toolbar'
import StatusBar from './status-bar'
import LayerPanel from '@/components/panels/layer-panel'
import PropertyPanel from '@/components/panels/property-panel'
import AIChatPanel, { AIChatMinimizedBar } from '@/components/panels/ai-chat-panel'
import CodePanel from '@/components/panels/code-panel'
import VariablesPanel from '@/components/panels/variables-panel'
import ComponentBrowserPanel from '@/components/panels/component-browser-panel'
import ExportDialog from '@/components/shared/export-dialog'
import SaveDialog from '@/components/shared/save-dialog'
import AgentSettingsDialog from '@/components/shared/agent-settings-dialog'
import FigmaImportDialog from '@/components/shared/figma-import-dialog'
import UpdateReadyBanner from './update-ready-banner'
import { useAIStore } from '@/stores/ai-store'
import { useCanvasStore } from '@/stores/canvas-store'
import { useDocumentStore } from '@/stores/document-store'
import { useAgentSettingsStore } from '@/stores/agent-settings-store'
import { useUIKitStore } from '@/stores/uikit-store'
import { useElectronMenu } from '@/hooks/use-electron-menu'
import { useFigmaPaste } from '@/hooks/use-figma-paste'
const FabricCanvas = lazy(() => import('@/canvas/fabric-canvas'))
export default function EditorLayout() {
const toggleMinimize = useAIStore((s) => s.toggleMinimize)
const hasSelection = useCanvasStore((s) => s.selection.activeId !== null)
const layerPanelOpen = useCanvasStore((s) => s.layerPanelOpen)
const variablesPanelOpen = useCanvasStore((s) => s.variablesPanelOpen)
const figmaImportOpen = useCanvasStore((s) => s.figmaImportDialogOpen)
const closeFigmaImport = useCallback(() => {
useCanvasStore.getState().setFigmaImportDialogOpen(false)
}, [])
const browserOpen = useUIKitStore((s) => s.browserOpen)
const saveDialogOpen = useDocumentStore((s) => s.saveDialogOpen)
const closeSaveDialog = useCallback(() => {
useDocumentStore.getState().setSaveDialogOpen(false)
}, [])
const [codePanelOpen, setCodePanelOpen] = useState(false)
const [exportOpen, setExportOpen] = useState(false)
const toggleCodePanel = useCallback(() => {
setCodePanelOpen((prev) => !prev)
}, [])
const closeExport = useCallback(() => {
setExportOpen(false)
}, [])
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const isMod = e.metaKey || e.ctrlKey
// Cmd+J: toggle AI panel minimize
if (isMod && e.key === 'j') {
e.preventDefault()
toggleMinimize()
return
}
// Cmd+Shift+C: toggle code panel
if (isMod && e.shiftKey && e.key.toLowerCase() === 'c') {
e.preventDefault()
toggleCodePanel()
return
}
// Cmd+Shift+E: open export
if (isMod && e.shiftKey && e.key.toLowerCase() === 'e') {
e.preventDefault()
setExportOpen((prev) => !prev)
return
}
// Cmd+Shift+V: toggle variables panel
if (isMod && e.shiftKey && e.key.toLowerCase() === 'v') {
e.preventDefault()
useCanvasStore.getState().toggleVariablesPanel()
return
}
// Cmd+Shift+K: toggle UIKit browser
if (isMod && e.shiftKey && e.key.toLowerCase() === 'k') {
e.preventDefault()
useUIKitStore.getState().toggleBrowser()
return
}
// Cmd+Shift+F: open Figma import
if (isMod && e.shiftKey && e.key.toLowerCase() === 'f') {
e.preventDefault()
useCanvasStore.getState().setFigmaImportDialogOpen(true)
return
}
// Cmd+,: open agent settings
if (isMod && e.key === ',') {
e.preventDefault()
useAgentSettingsStore.getState().setDialogOpen(true)
return
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [toggleMinimize, toggleCodePanel])
// Handle Electron native menu actions
useElectronMenu()
// Handle Figma clipboard paste
useFigmaPaste()
// Hydrate persisted settings
useEffect(() => {
useAgentSettingsStore.getState().hydrate()
useUIKitStore.getState().hydrate()
}, [])
return (
<TooltipProvider delayDuration={300}>
<div className="h-screen flex flex-col bg-background">
<UpdateReadyBanner />
<TopBar />
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 flex overflow-hidden">
{layerPanelOpen && <LayerPanel />}
<div className="flex-1 flex flex-col min-w-0 relative">
<Suspense
fallback={
<div className="flex-1 flex items-center justify-center bg-muted text-muted-foreground text-sm">
Loading canvas...
</div>
}
>
<FabricCanvas />
</Suspense>
<Toolbar />
{/* Floating variables panel — anchored to the right of the toolbar */}
{variablesPanelOpen && <VariablesPanel />}
{/* Floating UIKit browser panel */}
{browserOpen && <ComponentBrowserPanel />}
{/* Bottom bar: minimized AI (left) + zoom controls (right) */}
<div className="absolute bottom-2 left-2 right-2 z-10 flex items-center justify-between pointer-events-none">
<div className="pointer-events-auto">
<AIChatMinimizedBar />
</div>
<div className="pointer-events-auto">
<StatusBar />
</div>
</div>
{/* Expanded AI panel (floating, draggable) */}
<AIChatPanel />
</div>
{hasSelection && <PropertyPanel />}
</div>
{codePanelOpen && <CodePanel onClose={() => setCodePanelOpen(false)} />}
</div>
<ExportDialog open={exportOpen} onClose={closeExport} />
<SaveDialog open={saveDialogOpen} onClose={closeSaveDialog} />
<AgentSettingsDialog />
<FigmaImportDialog open={figmaImportOpen} onClose={closeFigmaImport} />
</div>
</TooltipProvider>
)
}