diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d4241a2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Supported Versions + +The table below details which versions of FlowCraft are currently supported with security updates: + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | +| < 1.0.0 | :x: | + +## Reporting a Vulnerability + +The FlowCraft team takes security and user privacy seriously. If you discover a security vulnerability or potential exploit, we appreciate your help in disclosing it to us responsibly. + +### Responsible Disclosure Guidelines + +**Please do NOT create a public GitHub issue, pull request, or discussion for security vulnerabilities.** + +Instead, report vulnerabilities privately through one of the following channels: + +1. **GitHub Private Vulnerability Reporting:** Use the "Report a vulnerability" button under the **Security** tab of the repository. +2. **Email Contact:** Send a private email to the project maintainers at `security@flowcraft.dev`. + +### What to Include in Your Report + +To help us evaluate and address the issue quickly, please provide: + +- A clear summary of the vulnerability and its potential impact. +- Detailed step-by-step instructions, screenshots, or a minimal Proof-of-Concept (PoC) script to reproduce the issue. +- The browser environment, OS, and version of FlowCraft where the vulnerability was observed. +- Any proposed remediation or code fix recommendations (if available). + +### Response Timeline + +- **Acknowledgment:** We aim to acknowledge receipt of your vulnerability report within **48 hours**. +- **Triage & Fix:** We will keep you informed as we investigate the vulnerability, work on a patch, and verify the resolution. +- **Public Disclosure:** Once a fix is released, we will coordinate public disclosure and credit security researchers who report issues in good faith. + +Thank you for contributing to the security and safety of the open-source community! diff --git a/package-lock.json b/package-lock.json index cfa3ad2..284166f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1593,6 +1594,7 @@ "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1847,6 +1849,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3423,6 +3426,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3449,6 +3453,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -3583,6 +3588,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3592,6 +3598,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4579,6 +4586,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/App.tsx b/src/App.tsx index 5c4f0d9..59f82ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import CenterCanvas from './components/CenterCanvas'; import RightSidebar from './components/RightSidebar'; import Toast from './components/Toast'; import { Block, ToastConfig, LayoutDirection } from './types'; +import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; // Default blueprint layout tracking const initialDemoBlocks: Block[] = [ @@ -88,6 +89,7 @@ export default function App() { const [layoutDirection, setLayoutDirection] = useState('vertical'); const [currentWorkspace, setCurrentWorkspace] = useState('Form-Flow Sandbox'); const [workspaces, setWorkspaces] = useState(['Form-Flow Sandbox']); + const [showShortcutsHelp, setShowShortcutsHelp] = useState(false); useEffect(() => { try { @@ -251,6 +253,37 @@ export default function App() { showToast(`Block "${block.label}" removed`, 'info'); }; + // Duplicate block (Ctrl+D / Cmd+D) + const handleDuplicateBlock = (id: string) => { + const original = blocks.find((b) => b.id === id); + if (!original) return; + + const newId = `block-${Math.random().toString(36).substring(2, 9)}`; + const duplicatedBlock: Block = { + ...original, + id: newId, + label: `${original.label} (Copy)`, + }; + + setBlocks((prev) => { + const idx = prev.findIndex((b) => b.id === id); + if (idx === -1) return [...prev, duplicatedBlock]; + + const updated = [...prev]; + if (original.type !== 'decision') { + duplicatedBlock.targetId = original.targetId; + updated[idx] = { ...original, targetId: newId }; + } + + updated.splice(idx + 1, 0, duplicatedBlock); + return updated; + }); + + setSelectedBlockId(newId); + setActiveParentId(newId); + showToast(`Duplicated "${original.label}"`, 'success'); + }; + // Select and chain next process block const handleSelectAndContinue = (parentBlock: Block) => { setActiveParentId(parentBlock.id); @@ -284,6 +317,17 @@ export default function App() { } }; + useKeyboardShortcuts({ + blocks, + selectedBlockId, + currentWorkspace, + onSelectBlock: setSelectedBlockId, + onDeleteBlock: handleDeleteBlock, + onDuplicateBlock: handleDuplicateBlock, + onSaveWorkspace: handleSaveWorkspace, + onToggleShortcutsHelp: () => setShowShortcutsHelp((prev) => !prev), + }); + const handleLoadWorkspace = (name: string) => { try { const data = localStorage.getItem('flowforge_workspaces'); @@ -475,6 +519,8 @@ export default function App() { toggleDarkMode={toggleDarkMode} layoutDirection={layoutDirection} onLayoutDirectionChange={setLayoutDirection} + showShortcutsHelp={showShortcutsHelp} + onToggleShortcutsHelp={() => setShowShortcutsHelp((prev) => !prev)} /> {/* RIGHT SIDEBAR PROPERTIES */} diff --git a/src/components/CenterCanvas.tsx b/src/components/CenterCanvas.tsx index 7652c80..e25a3c4 100644 --- a/src/components/CenterCanvas.tsx +++ b/src/components/CenterCanvas.tsx @@ -22,7 +22,10 @@ import { Sun, Trash2, Download, - Upload + Upload, + Keyboard, + X, + ChevronDown } from 'lucide-react'; import { toPng } from 'html-to-image'; import { jsPDF } from 'jspdf'; @@ -49,6 +52,8 @@ interface CenterCanvasProps { currentWorkspace: string; layoutDirection: LayoutDirection; onLayoutDirectionChange: (direction: LayoutDirection) => void; + showShortcutsHelp?: boolean; + onToggleShortcutsHelp?: () => void; } export default function CenterCanvas({ @@ -70,6 +75,8 @@ export default function CenterCanvas({ currentWorkspace, layoutDirection, onLayoutDirectionChange, + showShortcutsHelp = false, + onToggleShortcutsHelp, }: CenterCanvasProps) { const canvasRef = useRef(null); const containerRef = useRef(null); @@ -83,8 +90,20 @@ export default function CenterCanvas({ const [pan, setPan] = useState({ x: 0, y: 0 }); const [isPanning, setIsPanning] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false); + const [showExportMenu, setShowExportMenu] = useState(false); + const exportMenuRef = useRef(null); const panStart = useRef({ x: 0, y: 0 }); + React.useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (exportMenuRef.current && !exportMenuRef.current.contains(e.target as Node)) { + setShowExportMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + // Mouse pan event handlers const handleMouseDown = (e: React.MouseEvent) => { // Only drag with left mouse button click @@ -145,6 +164,9 @@ export default function CenterCanvas({ }; }, []); + const minWidth = 1200; + const minHeight = 800; + // Determine bounds of the layout to ensure the canvas scroll area fits all nodes comfortably // Set minimum height and width larger than standard viewports to guarantee spacious scroll bounds let minLayoutX = 0; @@ -383,19 +405,19 @@ export default function CenterCanvas({ return (
{/* Top Toolbar */} -
-
+
+
Workspace / - STABLE + STABLE
-
+
+ + -
@@ -878,6 +923,60 @@ export default function CenterCanvas({
)} + + {/* Keyboard Shortcuts Help Modal */} + {showShortcutsHelp && ( +
+
+
+
+
+ +
+
+

Keyboard Shortcuts

+

Power-user efficiency cheatsheet

+
+
+ +
+ +
+ {[ + { key: 'Delete / Backspace', desc: 'Delete selected block' }, + { key: 'Ctrl + A / Cmd + A', desc: 'Select top / root block' }, + { key: 'Ctrl + D / Cmd + D', desc: 'Duplicate selected block' }, + { key: 'Ctrl + S / Cmd + S', desc: 'Save workspace state' }, + { key: 'Escape', desc: 'Deselect block & clear focus' }, + { key: 'Tab / Shift + Tab', desc: 'Cycle selection forward / backward' }, + { key: 'Arrow Keys', desc: 'Navigate connected diagram paths' }, + { key: 'Shift + ?', desc: 'Toggle shortcuts help dialog' }, + ].map((s) => ( +
+ {s.desc} + + {s.key} + +
+ ))} +
+ +
+ +
+
+
+ )} ); } diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index c5d9e91..df23aa3 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -30,7 +30,7 @@ export default function RightSidebar({ if (!selectedBlock) { return ( -