Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 1 addition & 34 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const isValidWorkspaceBlocks = (blocks: any): blocks is Block[] => {
};

export default function App() {
const { isDarkMode, toggleDarkMode } = useDarkMode();
const [blocks, setBlocks] = useState<Block[]>(initialDemoBlocks);
const [selectedBlockId, setSelectedBlockId] = useState<string | null>('demo-1');
const [activeParentId, setActiveParentId] = useState<string | null>(null);
Expand Down Expand Up @@ -122,40 +123,6 @@ export default function App() {
} catch {}
}, []);

// Dark mode state
const [isDarkMode, setIsDarkMode] = useState(() => {
let initialDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
try {
const saved = localStorage.getItem('flowforge_dark_mode');
if (saved) initialDark = saved === 'true';
} catch {
// Ignore storage errors
}

// Apply class pre-paint
if (initialDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
return initialDark;
});

useEffect(() => {
// Preserve ongoing theme synchronization after mount
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
try {
localStorage.setItem('flowforge_dark_mode', String(isDarkMode));
} catch {
// Ignore storage errors
}
}, [isDarkMode]);

const toggleDarkMode = () => setIsDarkMode((prev) => !prev);

// Function to push a toast
const showToast = (message: string, type: 'success' | 'info' | 'error' = 'success') => {
Expand Down
19 changes: 11 additions & 8 deletions src/components/CenterCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export default function CenterCanvas({
<button
onClick={toggleDarkMode}
title={isDarkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
aria-label={isDarkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
className="px-2 py-1.5 border border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500 text-gray-600 dark:text-slate-300 hover:text-gray-800 dark:hover:text-slate-100 hover:bg-gray-50 dark:hover:bg-slate-700 text-xs font-bold rounded-lg transition-colors flex items-center gap-1.5 cursor-pointer mr-2"
>
{isDarkMode ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
Expand Down Expand Up @@ -810,23 +811,25 @@ export default function CenterCanvas({
</div>

{/* Float Zoom and Pan Control HUD Panel */}
<div className="zoom-controls absolute bottom-6 right-6 flex items-center gap-2 bg-white px-3 py-2 rounded-xl shadow-lg border border-gray-150 z-20 select-none">
<div className="zoom-controls absolute bottom-6 right-6 flex items-center gap-2 bg-white dark:bg-gray-900 px-3 py-2 rounded-xl shadow-lg border border-gray-150 z-20 select-none">
<button
onClick={() => setScale(prev => Math.max(0.25, parseFloat((prev - 0.1).toFixed(2))))}
disabled={scale <= 0.25}
title="Zoom Out"
className="w-8 h-8 rounded-lg flex items-center justify-center border border-gray-200 hover:border-gray-350 text-gray-500 hover:text-gray-800 hover:bg-gray-50 transition-colors disabled:opacity-40 disabled:pointer-events-none cursor-pointer"
aria-label="Zoom Out"
className="w-8 h-8 rounded-lg flex items-center justify-center border border-gray-200 dark:border-gray-700 hover:border-gray-350 text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors disabled:opacity-40 disabled:pointer-events-none cursor-pointer"
>
<ZoomOut className="w-4 h-4" />
</button>
<span className="text-xs font-mono font-bold text-gray-600 min-w-[48px] text-center">
<span className="text-xs font-mono font-bold text-gray-600 dark:text-gray-400 min-w-[48px] text-center">
{Math.round(scale * 100)}%
</span>
<button
onClick={() => setScale(prev => Math.min(3, parseFloat((prev + 0.1).toFixed(2))))}
disabled={scale >= 3}
title="Zoom In"
className="w-8 h-8 rounded-lg flex items-center justify-center border border-gray-200 hover:border-gray-350 text-gray-500 hover:text-gray-800 hover:bg-gray-50 transition-colors disabled:opacity-40 disabled:pointer-events-none cursor-pointer"
aria-label="Zoom In"
className="w-8 h-8 rounded-lg flex items-center justify-center border border-gray-200 dark:border-gray-700 hover:border-gray-350 text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors disabled:opacity-40 disabled:pointer-events-none cursor-pointer"
>
<ZoomIn className="w-4 h-4" />
</button>
Expand All @@ -849,16 +852,16 @@ export default function CenterCanvas({
{/* Confirmation Dialog Modal */}
{showConfirmModal && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-xs flex items-center justify-center z-50 animate-fade-in">
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 max-w-sm w-full mx-4 transform transition-all scale-100">
<h3 className="text-sm font-bold text-gray-900 mb-2">New Flowchart Confirmation</h3>
<p className="text-xs text-gray-500 leading-relaxed mb-6">
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-800 p-6 max-w-sm w-full mx-4 transform transition-all scale-100">
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100 mb-2">New Flowchart Confirmation</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 dark:text-gray-500 leading-relaxed mb-6">
Start a new flowchart? Current work will be lost.
</p>
<div className="flex items-center justify-end gap-3">
<button
id="btn-confirm-cancel"
onClick={() => setShowConfirmModal(false)}
className="px-3.5 py-2 border border-gray-200 hover:border-gray-300 hover:bg-gray-50 text-xs font-semibold text-gray-600 hover:text-gray-900 rounded-lg transition-all cursor-pointer"
className="px-3.5 py-2 border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-800 text-xs font-semibold text-gray-600 dark:text-gray-400 dark:text-gray-500 hover:text-gray-900 dark:text-gray-100 rounded-lg transition-all cursor-pointer"
>
Cancel
</button>
Expand Down
9 changes: 6 additions & 3 deletions src/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default function LeftSidebar({
type="button"
onClick={onCancelActiveParent}
title="Cancel chain connection"
aria-label="Cancel chain connection"
className="p-1 hover:bg-indigo-100 rounded-md text-indigo-500 hover:text-indigo-700 transition-colors cursor-pointer"
>
<X className="w-3.5 h-3.5" />
Expand All @@ -138,7 +139,7 @@ export default function LeftSidebar({

<form onSubmit={handleSubmit} className="space-y-4">
<div>
<h2 id="left-sidebar-add-title" className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-3">Add Block</h2>
<h2 id="left-sidebar-add-title" className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 mb-3">Add Block</h2>
<div className="grid grid-cols-2 gap-2">
{shapeCards.map((card) => {
const isSelected = selectedType === card.type;
Expand Down Expand Up @@ -251,21 +252,23 @@ export default function LeftSidebar({
<div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button
title="Edit Block"
aria-label="Edit Block"
onClick={(e) => {
e.stopPropagation();
onSelectBlock(block.id);
}}
className="p-1 text-gray-400 hover:text-indigo-600 rounded hover:bg-gray-100 transition-colors cursor-pointer"
className="p-1 text-gray-400 dark:text-gray-500 hover:text-indigo-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 dark:bg-gray-800 transition-colors cursor-pointer"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
<button
title="Delete Block"
aria-label="Delete Block"
onClick={(e) => {
e.stopPropagation();
onDeleteBlock(block.id);
}}
className="p-1 text-gray-400 hover:text-red-600 rounded hover:bg-red-50 transition-colors cursor-pointer"
className="p-1 text-gray-400 dark:text-gray-500 hover:text-red-600 rounded hover:bg-red-50 transition-colors cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ function ToastItem({ toast, onClose }: { toast: ToastConfig; onClose: (id: strin
</div>
<button
onClick={() => onClose(toast.id)}
className="text-gray-400 hover:text-gray-600 transition-colors cursor-pointer shrink-0"
aria-label="Close Toast"
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 dark:text-gray-400 dark:text-gray-500 transition-colors cursor-pointer shrink-0"
>
<X className="w-4 h-4" />
</button>
Expand Down
5 changes: 5 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ body {
overflow: hidden;
}

.dark body {
background-color: #0f172a; /* slate-900 */
color: #f1f5f9; /* slate-100 */
}

/* Custom scrollbars */
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
Expand Down
56 changes: 56 additions & 0 deletions src/utils/useDarkMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useState, useEffect } from 'react';

export function useDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(() => {
try {
const stored = localStorage.getItem('flowforge_theme');
if (stored === 'dark') return true;
if (stored === 'light') return false;

// Fallback to system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
} catch {
return false;
}
});

useEffect(() => {
const root = window.document.documentElement;
if (isDarkMode) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [isDarkMode]);

useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
try {
const stored = localStorage.getItem('flowforge_theme');
if (!stored) {
setIsDarkMode(e.matches);
}
} catch {
// ignore
}
};

mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

const toggleDarkMode = () => {
setIsDarkMode((prev) => {
const next = !prev;
try {
localStorage.setItem('flowforge_theme', next ? 'dark' : 'light');
} catch {
// ignore
}
return next;
});
};

return { isDarkMode, toggleDarkMode };
}
Loading