* 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>
172 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|