danylokhodus commited on
Commit
f104d74
·
1 Parent(s): dcaa853
src/context/AchievementNotificationContext.jsx CHANGED
@@ -131,7 +131,7 @@ const resetDownstreamNodesBackground = (startNodeId, nodes, edges) => {
131
  ...node,
132
  data: {
133
  ...node.data,
134
- status: 'Pending'
135
  }
136
  };
137
  }
@@ -307,7 +307,7 @@ export const AchievementNotificationProvider = ({ children }) => {
307
  ...n,
308
  data: {
309
  ...n.data,
310
- status: 'Pending',
311
  lastResetTime: new Date().toISOString()
312
  }
313
  };
@@ -315,7 +315,7 @@ export const AchievementNotificationProvider = ({ children }) => {
315
  return n;
316
  });
317
  updatedNodes = resetDownstreamNodesBackground(nodeId, updatedNodes, edges);
318
- knownHabitStatuses.current.set(nodeId, 'Pending');
319
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
320
  }
321
  }
@@ -339,7 +339,7 @@ export const AchievementNotificationProvider = ({ children }) => {
339
  ...n,
340
  data: {
341
  ...n.data,
342
- status: 'Pending',
343
  lastResetTime: new Date().toISOString()
344
  }
345
  };
@@ -350,17 +350,17 @@ export const AchievementNotificationProvider = ({ children }) => {
350
  // 2. Reset downstream nodes
351
  updatedNodes = resetDownstreamNodesBackground(nodeId, updatedNodes, edges);
352
 
353
- knownHabitStatuses.current.set(nodeId, 'Pending');
354
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
355
  } else {
356
  knownHabitStatuses.current.set(nodeId, 'Done');
357
  }
358
- } else if (currentStatus === 'Pending') {
359
  if (prevStatus === 'Done') {
360
- console.log(`[HabitTrackerBackground] Habit transitioned to Pending externally: "${node.data.label}"`);
361
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
362
  }
363
- knownHabitStatuses.current.set(nodeId, 'Pending');
364
  }
365
  }
366
  }
 
131
  ...node,
132
  data: {
133
  ...node.data,
134
+ status: 'Todo'
135
  }
136
  };
137
  }
 
307
  ...n,
308
  data: {
309
  ...n.data,
310
+ status: 'Todo',
311
  lastResetTime: new Date().toISOString()
312
  }
313
  };
 
315
  return n;
316
  });
317
  updatedNodes = resetDownstreamNodesBackground(nodeId, updatedNodes, edges);
318
+ knownHabitStatuses.current.set(nodeId, 'Todo');
319
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
320
  }
321
  }
 
339
  ...n,
340
  data: {
341
  ...n.data,
342
+ status: 'Todo',
343
  lastResetTime: new Date().toISOString()
344
  }
345
  };
 
350
  // 2. Reset downstream nodes
351
  updatedNodes = resetDownstreamNodesBackground(nodeId, updatedNodes, edges);
352
 
353
+ knownHabitStatuses.current.set(nodeId, 'Todo');
354
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
355
  } else {
356
  knownHabitStatuses.current.set(nodeId, 'Done');
357
  }
358
+ } else if (currentStatus === 'Pending' || currentStatus === 'Todo') {
359
  if (prevStatus === 'Done') {
360
+ console.log(`[HabitTrackerBackground] Habit transitioned to Todo/Pending externally: "${node.data.label}"`);
361
  newlyRechargedHabits.push(node.data?.label || t('canvas.habit'));
362
  }
363
+ knownHabitStatuses.current.set(nodeId, currentStatus);
364
  }
365
  }
366
  }
src/features/canvas/components/nodes/HabitNode.jsx CHANGED
@@ -26,7 +26,7 @@ const HabitNode = ({ id, data, selected }) => {
26
 
27
  if (data.updateNodeData) {
28
  data.updateNodeData(id, {
29
- status: 'Pending',
30
  lastResetTime: new Date().toISOString()
31
  });
32
  } else {
@@ -37,7 +37,7 @@ const HabitNode = ({ id, data, selected }) => {
37
  ...node,
38
  data: {
39
  ...node.data,
40
- status: 'Pending',
41
  lastResetTime: new Date().toISOString()
42
  }
43
  };
@@ -50,7 +50,7 @@ const HabitNode = ({ id, data, selected }) => {
50
 
51
  const toggleDone = (e) => {
52
  e.stopPropagation();
53
- const newStatus = data.status === 'Done' ? 'Pending' : 'Done';
54
  if (data.updateNodeData) {
55
  data.updateNodeData(id, { status: newStatus });
56
  } else {
@@ -245,7 +245,7 @@ const HabitNode = ({ id, data, selected }) => {
245
  ? 'bg-green-500/10 text-success border-success/30'
246
  : 'bg-amber-500/10 text-[#f59e0b] border-[#f59e0b]/30'
247
  }`}>
248
- {isDone ? t('canvas.habit_status_done') : t('canvas.habit_status_pending')}
249
  </span>
250
  </div>
251
 
 
26
 
27
  if (data.updateNodeData) {
28
  data.updateNodeData(id, {
29
+ status: 'Todo',
30
  lastResetTime: new Date().toISOString()
31
  });
32
  } else {
 
37
  ...node,
38
  data: {
39
  ...node.data,
40
+ status: 'Todo',
41
  lastResetTime: new Date().toISOString()
42
  }
43
  };
 
50
 
51
  const toggleDone = (e) => {
52
  e.stopPropagation();
53
+ const newStatus = data.status === 'Done' ? 'Todo' : 'Done';
54
  if (data.updateNodeData) {
55
  data.updateNodeData(id, { status: newStatus });
56
  } else {
 
245
  ? 'bg-green-500/10 text-success border-success/30'
246
  : 'bg-amber-500/10 text-[#f59e0b] border-[#f59e0b]/30'
247
  }`}>
248
+ {isDone ? t('canvas.habit_status_done', 'Done') : t('canvas.habit_status_todo', 'Todo')}
249
  </span>
250
  </div>
251
 
src/features/canvas/components/nodes/SketchNode.jsx CHANGED
@@ -61,7 +61,7 @@ const SketchNode = ({ id, data, selected }) => {
61
  return;
62
  }
63
 
64
- const newStatus = data.status === 'Done' ? 'Pending' : 'Done';
65
  const newProgress = newStatus === 'Done' ? 100 : 0;
66
 
67
  if (data.updateNodeData) {
@@ -97,12 +97,12 @@ const SketchNode = ({ id, data, selected }) => {
97
  setShowWarning(false);
98
  }, 3000);
99
 
100
- // Cap progress at 99% and status to Pending
101
- updateProgress(99, 'Pending');
102
  return;
103
  }
104
 
105
- const newStatus = val === 100 ? 'Done' : 'Pending';
106
  updateProgress(val, newStatus);
107
  };
108
 
@@ -127,7 +127,10 @@ const SketchNode = ({ id, data, selected }) => {
127
  switch (status) {
128
  case 'Done': return 'bg-green-500/10 text-success border-success/30';
129
  case 'Blocked': return 'bg-red-500/10 text-error border-error/30';
130
- case 'Pending': return 'bg-blue-500/10 text-secondary border-secondary/30';
 
 
 
131
  default: return 'bg-surface-container-lowest text-primary border-primary';
132
  }
133
  };
 
61
  return;
62
  }
63
 
64
+ const newStatus = data.status === 'Done' ? 'Todo' : 'Done';
65
  const newProgress = newStatus === 'Done' ? 100 : 0;
66
 
67
  if (data.updateNodeData) {
 
97
  setShowWarning(false);
98
  }, 3000);
99
 
100
+ // Cap progress at 99% and status to In Progress
101
+ updateProgress(99, 'In Progress');
102
  return;
103
  }
104
 
105
+ const newStatus = val === 100 ? 'Done' : (val === 0 ? 'Todo' : 'In Progress');
106
  updateProgress(val, newStatus);
107
  };
108
 
 
127
  switch (status) {
128
  case 'Done': return 'bg-green-500/10 text-success border-success/30';
129
  case 'Blocked': return 'bg-red-500/10 text-error border-error/30';
130
+ case 'In Progress': return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30';
131
+ case 'Review': return 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30';
132
+ case 'Todo':
133
+ case 'Pending': return 'bg-secondary/15 text-primary/70 border-primary/20';
134
  default: return 'bg-surface-container-lowest text-primary border-primary';
135
  }
136
  };
src/features/canvas/hooks/useCanvasLogic.js CHANGED
@@ -312,9 +312,52 @@ export const useCanvasLogic = (projectId, user) => {
312
  setNodes((nds) => {
313
  return nds.map((n) => {
314
  if (n.id === nodeId) {
315
- const updated = { ...n, data: { ...n.data, ...newData } };
316
- if (newData.isPinned !== undefined) {
317
- updated.draggable = !newData.isPinned;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
  return updated;
320
  }
@@ -578,6 +621,8 @@ export const useCanvasLogic = (projectId, user) => {
578
  let height = n.height;
579
  let nodeData = n.data || '';
580
  let assignees = [];
 
 
581
 
582
  if (n.type === 'condition') {
583
  if (n.decision && n.decision.startsWith('{')) {
@@ -610,6 +655,8 @@ export const useCanvasLogic = (projectId, user) => {
610
  lastResetTime = parsed.lastResetTime;
611
  createdAt = parsed.createdAt || createdAt;
612
  description = parsed.description || description;
 
 
613
  decision = undefined;
614
  } catch (e) {
615
  console.error('Failed to parse habit decision JSON', e);
@@ -627,6 +674,8 @@ export const useCanvasLogic = (projectId, user) => {
627
  isPinned = parsed.isPinned !== undefined ? !!parsed.isPinned : isPinned;
628
  width = parsed.width || width;
629
  height = parsed.height || height;
 
 
630
  if (parsed.assignees && Array.isArray(parsed.assignees)) {
631
  assignees = parsed.assignees;
632
  } else if (parsed.assignee) {
@@ -640,7 +689,8 @@ export const useCanvasLogic = (projectId, user) => {
640
 
641
  const dataObj = {
642
  label: n.label,
643
- status: n.state === 1 ? 'Done' : 'Pending',
 
644
  decision,
645
  options,
646
  selectedOptions,
@@ -797,7 +847,10 @@ export const useCanvasLogic = (projectId, user) => {
797
  resetRule: n.data.resetRule || { type: 'daily', time: '00:00' },
798
  lastResetTime: n.data.lastResetTime,
799
  createdAt: n.data.createdAt || new Date().toISOString(),
800
- description: n.data.description || ''
 
 
 
801
  })
802
  : n.type === 'sketch'
803
  ? JSON.stringify({
@@ -807,7 +860,9 @@ export const useCanvasLogic = (projectId, user) => {
807
  decision: n.data.decision || 'bottom',
808
  assignees: n.data.assignees || [],
809
  color: n.data.color,
810
- isPinned: !!n.data.isPinned
 
 
811
  })
812
  : n.type === 'comment'
813
  ? JSON.stringify({
@@ -1194,8 +1249,9 @@ export const useCanvasLogic = (projectId, user) => {
1194
  const id = crypto.randomUUID();
1195
  const nodeData = {
1196
  label: '',
1197
- status: 'Pending',
1198
  description: '',
 
1199
  ...initialData
1200
  };
1201
 
@@ -1486,7 +1542,8 @@ export const useCanvasLogic = (projectId, user) => {
1486
  position: { x: n.posX, y: n.posY },
1487
  data: {
1488
  label: n.label,
1489
- status: n.state === 1 ? 'Done' : 'Pending',
 
1490
  decision,
1491
  options,
1492
  selectedOptions,
@@ -1575,7 +1632,7 @@ export const useCanvasLogic = (projectId, user) => {
1575
  ...node,
1576
  data: {
1577
  ...node.data,
1578
- status: 'Pending'
1579
  }
1580
  };
1581
  }
@@ -1624,7 +1681,7 @@ export const useCanvasLogic = (projectId, user) => {
1624
  ...n,
1625
  data: {
1626
  ...n.data,
1627
- status: 'Pending',
1628
  lastResetTime: new Date().toISOString()
1629
  }
1630
  }
 
312
  setNodes((nds) => {
313
  return nds.map((n) => {
314
  if (n.id === nodeId) {
315
+ // Automatic status and progress coupling
316
+ let coupledData = { ...newData };
317
+ const currentStatus = n.data?.status || 'Todo';
318
+ const currentProgress = n.data?.progress !== undefined ? n.data.progress : 0;
319
+
320
+ if (coupledData.status !== undefined || coupledData.progress !== undefined) {
321
+ const nextStatus = coupledData.status !== undefined ? coupledData.status : currentStatus;
322
+ const nextProgress = coupledData.progress !== undefined ? Number(coupledData.progress) : currentProgress;
323
+
324
+ if (coupledData.status !== undefined && coupledData.progress === undefined) {
325
+ // Status changed, progress was not
326
+ if (nextStatus === 'Done') {
327
+ coupledData.progress = 100;
328
+ } else if (nextStatus === 'Todo' || nextStatus === 'Pending') {
329
+ coupledData.progress = 0;
330
+ } else if (nextStatus === 'In Progress' && (currentProgress === 0 || currentProgress === 100)) {
331
+ coupledData.progress = 50;
332
+ } else if (nextStatus === 'Review' && (currentProgress === 0 || currentProgress === 100)) {
333
+ coupledData.progress = 90;
334
+ }
335
+ } else if (coupledData.progress !== undefined && coupledData.status === undefined) {
336
+ // Progress changed, status was not
337
+ if (nextProgress === 100) {
338
+ coupledData.status = 'Done';
339
+ } else if (nextProgress === 0) {
340
+ coupledData.status = 'Todo';
341
+ } else if (currentStatus === 'Done' || currentStatus === 'Todo' || currentStatus === 'Pending') {
342
+ coupledData.status = 'In Progress';
343
+ }
344
+ } else if (coupledData.progress !== undefined && coupledData.status !== undefined) {
345
+ // Both changed, align them
346
+ if (coupledData.status === 'Done') {
347
+ coupledData.progress = 100;
348
+ } else if (coupledData.status === 'Todo' || coupledData.status === 'Pending') {
349
+ coupledData.progress = 0;
350
+ } else if (coupledData.progress === 100) {
351
+ coupledData.status = 'Done';
352
+ } else if (coupledData.progress === 0) {
353
+ coupledData.status = 'Todo';
354
+ }
355
+ }
356
+ }
357
+
358
+ const updated = { ...n, data: { ...n.data, ...coupledData } };
359
+ if (coupledData.isPinned !== undefined) {
360
+ updated.draggable = !coupledData.isPinned;
361
  }
362
  return updated;
363
  }
 
621
  let height = n.height;
622
  let nodeData = n.data || '';
623
  let assignees = [];
624
+ let tags = [];
625
+ let status = n.state === 1 ? 'Done' : 'Todo';
626
 
627
  if (n.type === 'condition') {
628
  if (n.decision && n.decision.startsWith('{')) {
 
655
  lastResetTime = parsed.lastResetTime;
656
  createdAt = parsed.createdAt || createdAt;
657
  description = parsed.description || description;
658
+ tags = parsed.tags || [];
659
+ status = parsed.status || (n.state === 1 ? 'Done' : 'Todo');
660
  decision = undefined;
661
  } catch (e) {
662
  console.error('Failed to parse habit decision JSON', e);
 
674
  isPinned = parsed.isPinned !== undefined ? !!parsed.isPinned : isPinned;
675
  width = parsed.width || width;
676
  height = parsed.height || height;
677
+ tags = parsed.tags || [];
678
+ status = parsed.status || (n.state === 1 ? 'Done' : 'Todo');
679
  if (parsed.assignees && Array.isArray(parsed.assignees)) {
680
  assignees = parsed.assignees;
681
  } else if (parsed.assignee) {
 
689
 
690
  const dataObj = {
691
  label: n.label,
692
+ status: status,
693
+ tags: tags,
694
  decision,
695
  options,
696
  selectedOptions,
 
847
  resetRule: n.data.resetRule || { type: 'daily', time: '00:00' },
848
  lastResetTime: n.data.lastResetTime,
849
  createdAt: n.data.createdAt || new Date().toISOString(),
850
+ description: n.data.description || '',
851
+ tags: n.data.tags || [],
852
+ status: n.data.status || 'Todo',
853
+ progress: n.data.progress || 0
854
  })
855
  : n.type === 'sketch'
856
  ? JSON.stringify({
 
860
  decision: n.data.decision || 'bottom',
861
  assignees: n.data.assignees || [],
862
  color: n.data.color,
863
+ isPinned: !!n.data.isPinned,
864
+ tags: n.data.tags || [],
865
+ status: n.data.status || 'Todo'
866
  })
867
  : n.type === 'comment'
868
  ? JSON.stringify({
 
1249
  const id = crypto.randomUUID();
1250
  const nodeData = {
1251
  label: '',
1252
+ status: 'Todo',
1253
  description: '',
1254
+ tags: [],
1255
  ...initialData
1256
  };
1257
 
 
1542
  position: { x: n.posX, y: n.posY },
1543
  data: {
1544
  label: n.label,
1545
+ status: n.state === 1 ? 'Done' : 'Todo',
1546
+ tags: [],
1547
  decision,
1548
  options,
1549
  selectedOptions,
 
1632
  ...node,
1633
  data: {
1634
  ...node.data,
1635
+ status: 'Todo'
1636
  }
1637
  };
1638
  }
 
1681
  ...n,
1682
  data: {
1683
  ...n.data,
1684
+ status: 'Todo',
1685
  lastResetTime: new Date().toISOString()
1686
  }
1687
  }
src/pages/CanvasPage.jsx CHANGED
@@ -83,7 +83,7 @@ const getTransformForBoundsCustom = (bounds, width, height, minZoom, maxZoom, pa
83
  import Sidebar from '../components/Sidebar';
84
  import { useAuth } from '../context/AuthContext';
85
  import { useTheme } from '../context/ThemeContext';
86
- import { Maximize2, Play, Settings, Save, ChevronLeft, ChevronDown, ChevronUp, Loader2, Trash2, Target, GitFork, Image, X, Plus, Timer, Milestone, Share2, User, Copy, Scissors, Clipboard, Check, StickyNote, Pin, Type } from 'lucide-react';
87
 
88
  // Feature components
89
  import SketchNode from '../features/canvas/components/nodes/SketchNode';
@@ -515,6 +515,10 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
515
  const [description, setDescription] = useState(node.data.description || '');
516
  const [showProgress, setShowProgress] = useState(!!node.data.showProgress);
517
  const [progress, setProgress] = useState(node.data.progress || 0);
 
 
 
 
518
  const [assignees, setAssignees] = useState(node.data.assignees || []);
519
  const [isAssigneesExpanded, setIsAssigneesExpanded] = useState(false);
520
  const [color, setColor] = useState(node.data.color || (node.type === 'comment' ? 'yellow' : 'default'));
@@ -530,13 +534,40 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
530
  white: { light: '#f5f5f4', dark: '#1c1917', borderLight: '#d6d3d1', borderDark: '#44403c' }
531
  };
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  const handleSave = () => {
534
  let finalProgress = Number(progress);
535
- let status = finalProgress === 100 ? 'Done' : 'Pending';
536
 
537
  if (node.type === 'sketch' && finalProgress === 100 && node.data.subgoals && node.data.subgoals.done < node.data.subgoals.total) {
538
  finalProgress = 99;
539
- status = 'Pending';
540
  alert(t('canvas.subgoal_warning', 'Виконайте всі підцілі перед завершенням головної цілі!'));
541
  }
542
 
@@ -545,9 +576,10 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
545
  description,
546
  showProgress: node.type === 'comment' ? false : showProgress,
547
  progress: node.type === 'comment' ? 0 : finalProgress,
548
- status: node.type === 'comment' ? 'Pending' : status,
549
  assignees,
550
- color
 
551
  });
552
  };
553
 
@@ -630,7 +662,33 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
630
  </div>
631
  </div>
632
 
633
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
  {/* Assignees Input (Collapsible custom visual list) */}
636
  {node.type !== 'comment' && (
@@ -725,6 +783,117 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
725
  </div>
726
  )}
727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  {/* Show Progress Toggle */}
729
  {node.type !== 'comment' && (
730
  <div className="flex items-center justify-between bg-secondary/5 border-2 border-primary rounded-xl p-4">
@@ -762,7 +931,7 @@ const SketchSettingsModal = ({ node, onClose, onSave, collaborators = [] }) => {
762
  min="0"
763
  max="100"
764
  value={progress}
765
- onChange={(e) => setProgress(Number(e.target.value))}
766
  className="w-full h-2 rounded-lg appearance-none cursor-pointer focus:outline-none transition-all accent-emerald-500"
767
  style={{
768
  background: `linear-gradient(to right, #10b981 ${progress}%, var(--color-border, #e2e8f0) ${progress}%)`
@@ -1228,6 +1397,505 @@ const ShareModal = ({ projectId, onClose }) => {
1228
  );
1229
  };
1230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1231
  const CanvasInner = () => {
1232
  const { t } = useTranslation();
1233
  const { id } = useParams();
@@ -1285,6 +1953,13 @@ const CanvasInner = () => {
1285
  updateProjectMetadata
1286
  } = useCanvasLogic(id, user);
1287
 
 
 
 
 
 
 
 
1288
  const lastCursorSendRef = useRef(0);
1289
  const broadcastRef = useRef(broadcastMessage);
1290
 
@@ -1837,12 +2512,12 @@ const CanvasInner = () => {
1837
  <div className="flex h-screen bg-background overflow-hidden" onMouseUp={handleMouseUp}>
1838
  <Sidebar />
1839
 
1840
- <main className="flex-grow flex flex-col relative">
1841
  <div className="relative h-24 bg-surface-container-lowest border-b-4 border-primary px-8 flex items-center justify-between z-30 shadow-sm">
1842
  <div className="flex items-center gap-6">
1843
  <button
1844
  onClick={() => navigate('/dashboard')}
1845
- className="p-2 hover:bg-surface-variant rounded-xl transition-colors"
1846
  >
1847
  <ChevronLeft size={28} />
1848
  </button>
@@ -1873,6 +2548,34 @@ const CanvasInner = () => {
1873
  {projectData?.description || t('canvas.strategic_goal_map')}
1874
  </p>
1875
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1876
  </div>
1877
 
1878
  <div className="flex items-center gap-4">
@@ -1931,434 +2634,459 @@ const CanvasInner = () => {
1931
  </div>
1932
  </div>
1933
 
1934
- {permission !== 'Read' && (
1935
- <CanvasToolbar
1936
- handleAIRequest={handleAIRequest}
1937
- isAIActive={isAIActive}
1938
- user={user}
1939
- addNewNodeAt={addNewNodeAt}
1940
- handleCopy={handleCopy}
1941
- handleCut={handleCut}
1942
- handlePaste={() => handlePaste(mousePosRef.current)}
1943
- exportToPng={exportToPng}
1944
- onDeleteSelected={() => {
1945
- const selectedNodeIds = nodes.filter(n => n.selected).map(n => n.id);
1946
- const selectedEdgeIds = edges.filter(e => e.selected).map(e => e.id);
1947
- requestDeletion(selectedNodeIds, selectedEdgeIds);
1948
- }}
1949
- hasSelectedNodes={nodes.some(n => n.selected)}
1950
- hasClipboardContent={hasClipboardContent}
1951
- undo={undo}
1952
- redo={redo}
1953
- canUndo={canUndo}
1954
- canRedo={canRedo}
1955
- drawingMode={drawingMode}
1956
- setDrawingMode={setDrawingMode}
1957
- />
1958
- )}
1959
-
1960
- {/* React Flow Canvas */}
1961
  <div
1962
- ref={reactFlowWrapper}
1963
- className="flex-grow graph-paper-bg relative"
1964
- onMouseDown={drawingMode ? undefined : handleMouseDown}
1965
- onPointerDown={onPointerDown}
1966
- onPointerMove={onPointerMove}
1967
- onPointerUp={onPointerUp}
1968
  >
1969
- <ReactFlow
1970
- nodes={nodes}
1971
- edges={edges}
1972
- onNodesChange={onNodesChange}
1973
- onEdgesChange={onEdgesChange}
1974
- onConnect={permission === 'Read' ? undefined : onConnect}
1975
- onEdgesDelete={permission === 'Read' ? undefined : (eds) => requestDeletion([], eds.map(e => e.id))}
1976
- onEdgeClick={permission === 'Read' ? undefined : (_, edge) => requestDeletion([], [edge.id])}
1977
- onNodesDelete={permission === 'Read' ? undefined : (nds) => requestDeletion(nds.map(n => n.id), [])}
1978
- onNodeDragStart={permission === 'Read' ? undefined : () => takeHistorySnapshot()}
1979
- onNodeContextMenu={handleNodeContextMenu}
1980
- onPaneClick={() => setContextMenu(null)}
1981
- onPaneContextMenu={handlePaneContextMenu}
1982
- onDrop={onDrop}
1983
- onDragOver={onDragOver}
1984
- onConnectStart={(event, params) => {
1985
- connectionStartRef.current = params;
1986
- }}
1987
- onConnectEnd={onConnectEnd}
1988
- nodesDraggable={!drawingMode && permission !== 'Read'}
1989
- nodesConnectable={!drawingMode && permission !== 'Read'}
1990
- panOnDrag={!drawingMode}
1991
- selectionOnDrag={!drawingMode}
1992
- nodeTypes={nodeTypes}
1993
- edgeTypes={edgeTypes}
1994
- connectionLineType={ConnectionLineType.SmoothStep}
1995
- fitView
1996
- className="sketch-flow"
1997
- >
1998
- <Background variant="dots" gap={20} size={1} color={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)'} />
1999
- <Controls className="!bottom-8 !left-28 !overflow-hidden" />
2000
- <MiniMap
2001
- className="!bottom-8 !right-8 !overflow-hidden"
2002
- nodeStrokeColor={isDarkMode ? '#e2e3e4' : '#31363F'}
2003
- nodeColor={isDarkMode ? '#222326' : '#EEEEEE'}
2004
- maskColor={isDarkMode ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.6)'}
2005
- />
2006
- {/* Collaborative cursors of active participants */}
2007
- <CollaborativeCursors cursors={cursors} />
2008
- </ReactFlow>
2009
-
2010
- {/* Active Drawing Overlay */}
2011
- {activeStroke && (
2012
- <svg
2013
- className="absolute inset-0 pointer-events-none z-[1000]"
2014
- style={{ width: '100%', height: '100%' }}
2015
- >
2016
- <path
2017
- d={activeStroke.path}
2018
- fill="none"
2019
- stroke={isDarkMode ? '#ffffff' : '#31363F'}
2020
- strokeWidth={3}
2021
- strokeLinecap="round"
2022
- strokeLinejoin="round"
2023
- />
2024
- </svg>
2025
- )}
2026
-
2027
- <AIHintOverlay
2028
- isAIActive={isAIActive}
2029
- setIsAIActive={setIsAIActive}
2030
- aiLoading={aiLoading}
2031
- submitAICanvasPrompt={submitAICanvasPrompt}
2032
- />
2033
- <PaywallModal showPaywall={showPaywall} setShowPaywall={setShowPaywall} />
2034
-
2035
- {editingNode && (
2036
- <ConditionSettingsModal
2037
- node={editingNode}
2038
- onClose={() => setEditingConditionNodeId(null)}
2039
- onSave={handleSaveConditionConfig}
2040
  />
2041
  )}
2042
 
2043
- {editingHabitNode && (
2044
- <HabitSettingsModal
2045
- node={editingHabitNode}
2046
- onClose={() => setEditingHabitNodeId(null)}
2047
- onSave={handleSaveHabitConfig}
2048
- />
2049
- )}
2050
-
2051
- {editingSketchNode && (
2052
- <SketchSettingsModal
2053
- node={editingSketchNode}
2054
- onClose={() => setEditingSketchNodeId(null)}
2055
- onSave={handleSaveSketchConfig}
2056
- collaborators={collaborators}
2057
- />
2058
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2059
 
2060
- {isEditingMetadata && (
2061
- <ProjectMetadataModal
2062
- title={projectTitle}
2063
- description={projectData?.description || ''}
2064
- onClose={() => setIsEditingMetadata(false)}
2065
- onSave={(newTitle, newDesc) => {
2066
- updateProjectMetadata(newTitle, newDesc);
2067
- setIsEditingMetadata(false);
2068
- }}
2069
- />
2070
- )}
 
 
 
 
2071
 
2072
- {isShareModalOpen && (
2073
- <ShareModal
2074
- projectId={id}
2075
- onClose={() => {
2076
- setIsShareModalOpen(false);
2077
- fetchCollaborators();
2078
- }}
2079
  />
2080
- )}
2081
 
2082
- {contextMenu && contextMenu.type === 'node' && (
2083
- <div
2084
- style={{ top: contextMenu.y, left: contextMenu.x }}
2085
- className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[135px] rounded-lg transition-all flex flex-col gap-0.5"
2086
- onClick={(e) => e.stopPropagation()}
2087
- >
2088
- <button
2089
- onClick={(e) => {
2090
- e.stopPropagation();
2091
- // Ensure node is selected for copy if it isn't already
2092
- const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2093
- if (targetNode) {
2094
- const isSelected = targetNode.selected;
2095
- if (!isSelected) {
2096
- setNodes(nds => nds.map(n => n.id === contextMenu.nodeId ? { ...n, selected: true } : n));
2097
- }
2098
- // Copy node(s)
2099
- const selectedNodes = nodes.filter(n => n.selected || n.id === contextMenu.nodeId);
2100
- const selectedEdges = edges.filter(edge =>
2101
- selectedNodes.some(n => n.id === edge.source) &&
2102
- selectedNodes.some(n => n.id === edge.target)
2103
- );
2104
- const clipboardPayload = {
2105
- nodes: selectedNodes.map(n => ({ ...n, originalPosition: { ...n.position } })),
2106
- edges: selectedEdges
2107
- };
2108
- window.__flowClipboard = clipboardPayload;
2109
- triggerClipboardUpdate();
2110
- try {
2111
- navigator.clipboard.writeText('FLOWWEB_NODES_DATA:' + JSON.stringify(clipboardPayload)).catch(() => {});
2112
- } catch (err) {}
2113
- }
2114
- setContextMenu(null);
2115
- }}
2116
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2117
  >
2118
- <Copy size={14} className="text-primary" />
2119
- <span>{t('canvas.context_menu.copy', 'Копіювати')}</span>
2120
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2121
 
2122
- <button
2123
- onClick={(e) => {
2124
- e.stopPropagation();
2125
- // Cut node(s)
2126
- const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2127
- if (targetNode) {
2128
- const selectedNodes = nodes.filter(n => n.selected || n.id === contextMenu.nodeId);
2129
- const selectedEdges = edges.filter(edge =>
2130
- selectedNodes.some(n => n.id === edge.source) &&
2131
- selectedNodes.some(n => n.id === edge.target)
2132
- );
2133
- const clipboardPayload = {
2134
- nodes: selectedNodes.map(n => ({ ...n, originalPosition: { ...n.position } })),
2135
- edges: selectedEdges
2136
- };
2137
- window.__flowClipboard = clipboardPayload;
2138
- triggerClipboardUpdate();
2139
- try {
2140
- navigator.clipboard.writeText('FLOWWEB_NODES_DATA:' + JSON.stringify(clipboardPayload)).catch(() => {});
2141
- } catch (err) {}
2142
- // Delete selected nodes
2143
- selectedNodes.forEach(n => deleteNode(n.id));
2144
- }
2145
- setContextMenu(null);
2146
- }}
2147
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2148
  >
2149
- <Scissors size={14} className="text-primary" />
2150
- <span>{t('canvas.context_menu.cut', 'Вирізати')}</span>
2151
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2152
 
2153
- <button
2154
- onClick={(e) => {
2155
- e.stopPropagation();
2156
- const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2157
- if (targetNode) {
2158
- const newPinnedState = !targetNode.data?.isPinned;
2159
- updateNodeData(contextMenu.nodeId, { isPinned: newPinnedState });
2160
- }
2161
- setContextMenu(null);
2162
- }}
2163
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2164
- >
2165
- <Pin
2166
- size={14}
2167
- className={`text-primary transition-transform ${
2168
- nodes.find(n => n.id === contextMenu.nodeId)?.data?.isPinned
2169
- ? 'rotate-45 text-amber-500 fill-amber-500'
2170
- : ''
2171
- }`}
2172
- />
2173
- <span>
2174
- {nodes.find(n => n.id === contextMenu.nodeId)?.data?.isPinned
2175
- ? t('canvas.context_menu.unpin', 'Відкріпити')
2176
- : t('canvas.context_menu.pin', 'Закріпити')}
2177
- </span>
2178
- </button>
2179
 
2180
- <div className="border-t border-primary/10 my-0.5"></div>
 
 
 
 
 
 
 
 
 
 
 
 
2181
 
2182
- <button
2183
- onClick={(e) => {
2184
- e.stopPropagation();
2185
- requestDeletion([contextMenu.nodeId], []);
2186
- setContextMenu(null);
2187
- }}
2188
- className="w-full text-left px-2.5 py-1.5 hover:bg-red-500/20 text-red-600 hover:text-red-700 font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2189
  >
2190
- <Trash2 size={14} />
2191
- <span>{t('canvas.context_menu.delete')}</span>
2192
- </button>
2193
- </div>
2194
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2195
 
2196
- {contextMenu && contextMenu.type === 'pane' && (
2197
- <div
2198
- style={{ top: contextMenu.y, left: contextMenu.x }}
2199
- className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[170px] rounded-lg transition-all flex flex-col gap-0.5"
2200
- onClick={(e) => e.stopPropagation()}
2201
- >
2202
- {permission !== 'Read' && (
2203
- <>
2204
- <div className="px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-on-surface-variant/40 border-b border-primary/10">
2205
- {t('canvas.context_menu.create_node')}
2206
- </div>
2207
- <button
2208
- onClick={(e) => {
2209
- e.stopPropagation();
2210
- addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'sketch');
2211
- setContextMenu(null);
2212
- }}
2213
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2214
- >
2215
- <Target size={15} className="text-primary" />
2216
- <span>{t('canvas.context_menu.goal_node')}</span>
2217
- </button>
2218
- <button
2219
- onClick={(e) => {
2220
- e.stopPropagation();
2221
- addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'comment');
2222
- setContextMenu(null);
2223
- }}
2224
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2225
- >
2226
- <StickyNote size={15} className="text-secondary" />
2227
- <span>Стікер</span>
2228
- </button>
2229
- <button
2230
- onClick={(e) => {
2231
- e.stopPropagation();
2232
- addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'condition');
2233
- setContextMenu(null);
2234
- }}
2235
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2236
- >
2237
- <GitFork size={15} className="text-primary" />
2238
- <span>{t('canvas.context_menu.condition_node')}</span>
2239
- </button>
2240
- <button
2241
- onClick={(e) => {
2242
- e.stopPropagation();
2243
- addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'habit');
2244
- setContextMenu(null);
2245
- }}
2246
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2247
- >
2248
- <Timer size={15} className="text-secondary" />
2249
- <span>{t('canvas.context_menu.habit_node')}</span>
2250
- </button>
2251
 
2252
- {window.__flowClipboard && window.__flowClipboard.nodes && window.__flowClipboard.nodes.length > 0 && (
2253
- <>
2254
- <div className="border-t border-primary/10 my-0.5"></div>
2255
- <button
2256
- onClick={(e) => {
2257
- e.stopPropagation();
2258
- handlePaste({ x: contextMenu.clientX, y: contextMenu.clientY });
2259
- setContextMenu(null);
2260
- }}
2261
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2262
- >
2263
- <Clipboard size={15} className="text-primary" />
2264
- <span>{t('canvas.context_menu.paste', 'Вставити')}</span>
2265
- </button>
2266
- </>
2267
- )}
2268
 
2269
- <div className="border-t border-primary/10 my-0.5"></div>
2270
- </>
2271
- )}
 
 
 
 
2272
 
2273
- <button
2274
- onClick={(e) => {
2275
- e.stopPropagation();
2276
- exportToPng();
2277
- setContextMenu(null);
2278
- }}
2279
- className="w-full text-left px-2.5 py-1.5 hover:bg-green-50 text-green-600 hover:text-green-700 font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2280
- >
2281
- <Image size={15} className="text-green-600" />
2282
- <span>{t('canvas.context_menu.export_png')}</span>
2283
- </button>
2284
- </div>
2285
- )}
2286
 
2287
- {contextMenu && contextMenu.type === 'connect-pane' && (
2288
- <div
2289
- style={{ top: contextMenu.y, left: contextMenu.x }}
2290
- className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[170px] rounded-lg transition-all flex flex-col gap-0.5"
2291
- onClick={(e) => e.stopPropagation()}
2292
- >
2293
- <div className="px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-secondary border-b border-primary/10">
2294
- Зв'язати з новою:
2295
- </div>
2296
- <button
2297
- onClick={(e) => {
2298
- e.stopPropagation();
2299
- const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'sketch');
2300
- if (contextMenu.connectionSource && newNodeId) {
2301
- const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2302
- const isSource = handleType === 'source';
2303
- onConnect({
2304
- source: isSource ? nodeId : newNodeId,
2305
- target: isSource ? newNodeId : nodeId,
2306
- sourceHandle: isSource ? handleId : 'main-source',
2307
- targetHandle: isSource ? 'main-target' : handleId
2308
- });
2309
- }
2310
- setContextMenu(null);
2311
- }}
2312
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2313
- >
2314
- <Target size={15} className="text-primary" />
2315
- <span>{t('canvas.context_menu.goal_node')}</span>
2316
- </button>
2317
- <button
2318
- onClick={(e) => {
2319
- e.stopPropagation();
2320
- const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'condition');
2321
- if (contextMenu.connectionSource && newNodeId) {
2322
- const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2323
- const isSource = handleType === 'source';
2324
- onConnect({
2325
- source: isSource ? nodeId : newNodeId,
2326
- target: isSource ? newNodeId : nodeId,
2327
- sourceHandle: isSource ? handleId : null,
2328
- targetHandle: isSource ? null : handleId
2329
- });
2330
- }
2331
- setContextMenu(null);
2332
- }}
2333
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2334
- >
2335
- <GitFork size={15} className="text-primary" />
2336
- <span>{t('canvas.context_menu.condition_node')}</span>
2337
- </button>
2338
- <button
2339
- onClick={(e) => {
2340
- e.stopPropagation();
2341
- const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'habit');
2342
- if (contextMenu.connectionSource && newNodeId) {
2343
- const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2344
- const isSource = handleType === 'source';
2345
- onConnect({
2346
- source: isSource ? nodeId : newNodeId,
2347
- target: isSource ? newNodeId : nodeId,
2348
- sourceHandle: isSource ? handleId : null,
2349
- targetHandle: isSource ? null : handleId
2350
- });
2351
- }
2352
- setContextMenu(null);
2353
- }}
2354
- className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2355
- >
2356
- <Timer size={15} className="text-secondary" />
2357
- <span>{t('canvas.context_menu.habit_node')}</span>
2358
- </button>
2359
- </div>
2360
- )}
2361
- </div>
2362
  </main>
2363
  </div>
2364
  );
 
83
  import Sidebar from '../components/Sidebar';
84
  import { useAuth } from '../context/AuthContext';
85
  import { useTheme } from '../context/ThemeContext';
86
+ import { Maximize2, Play, Settings, Save, ChevronLeft, ChevronDown, ChevronUp, Loader2, Trash2, Target, GitFork, Image, X, Plus, Timer, Milestone, Share2, User, Copy, Scissors, Clipboard, Check, StickyNote, Pin, Type, Tag, Search, ListTodo, Map } from 'lucide-react';
87
 
88
  // Feature components
89
  import SketchNode from '../features/canvas/components/nodes/SketchNode';
 
515
  const [description, setDescription] = useState(node.data.description || '');
516
  const [showProgress, setShowProgress] = useState(!!node.data.showProgress);
517
  const [progress, setProgress] = useState(node.data.progress || 0);
518
+ const [status, setStatus] = useState(node.data.status || 'Todo');
519
+ const [tags, setTags] = useState(node.data.tags || []);
520
+ const [customTagInput, setCustomTagInput] = useState('');
521
+ const [isTagsExpanded, setIsTagsExpanded] = useState(false);
522
  const [assignees, setAssignees] = useState(node.data.assignees || []);
523
  const [isAssigneesExpanded, setIsAssigneesExpanded] = useState(false);
524
  const [color, setColor] = useState(node.data.color || (node.type === 'comment' ? 'yellow' : 'default'));
 
534
  white: { light: '#f5f5f4', dark: '#1c1917', borderLight: '#d6d3d1', borderDark: '#44403c' }
535
  };
536
 
537
+ const handleStatusChange = (newStatus) => {
538
+ setStatus(newStatus);
539
+ if (newStatus === 'Done') {
540
+ setProgress(100);
541
+ } else if (newStatus === 'Todo') {
542
+ setProgress(0);
543
+ } else {
544
+ if (progress === 100 || progress === 0) {
545
+ setProgress(newStatus === 'Review' ? 90 : 10);
546
+ }
547
+ }
548
+ };
549
+
550
+ const handleProgressChange = (newVal) => {
551
+ const numericVal = Number(newVal);
552
+ setProgress(numericVal);
553
+ if (numericVal === 100) {
554
+ setStatus('Done');
555
+ } else if (numericVal === 0) {
556
+ setStatus('Todo');
557
+ } else {
558
+ if (status === 'Done' || status === 'Todo') {
559
+ setStatus('In Progress');
560
+ }
561
+ }
562
+ };
563
+
564
  const handleSave = () => {
565
  let finalProgress = Number(progress);
566
+ let finalStatus = status;
567
 
568
  if (node.type === 'sketch' && finalProgress === 100 && node.data.subgoals && node.data.subgoals.done < node.data.subgoals.total) {
569
  finalProgress = 99;
570
+ finalStatus = 'In Progress';
571
  alert(t('canvas.subgoal_warning', 'Виконайте всі підцілі перед завершенням головної цілі!'));
572
  }
573
 
 
576
  description,
577
  showProgress: node.type === 'comment' ? false : showProgress,
578
  progress: node.type === 'comment' ? 0 : finalProgress,
579
+ status: node.type === 'comment' ? 'Pending' : finalStatus,
580
  assignees,
581
+ color,
582
+ tags
583
  });
584
  };
585
 
 
662
  </div>
663
  </div>
664
 
665
+ {/* Goal Status Selector */}
666
+ {node.type !== 'comment' && (
667
+ <div>
668
+ <label className="block text-[11px] font-bold uppercase tracking-widest text-primary/60 mb-2">
669
+ {t('canvas.goal_status_label', 'Статус цілі')}
670
+ </label>
671
+ <div className="grid grid-cols-4 gap-1.5 bg-secondary/5 border-2 border-primary rounded-xl p-1.5">
672
+ {['Todo', 'In Progress', 'Review', 'Done'].map((st) => {
673
+ const isSelected = status === st;
674
+ let btnClass = isSelected
675
+ ? 'bg-primary text-on-primary shadow-[2px_2px_0px_0px_var(--color-primary)]'
676
+ : 'text-primary/70 hover:bg-secondary/10 hover:text-primary';
677
+
678
+ return (
679
+ <button
680
+ key={st}
681
+ type="button"
682
+ onClick={() => handleStatusChange(st)}
683
+ className={`py-2 text-[10px] font-extrabold uppercase rounded-lg border border-transparent transition-all cursor-pointer ${btnClass}`}
684
+ >
685
+ {st === 'In Progress' ? 'Progress' : st}
686
+ </button>
687
+ );
688
+ })}
689
+ </div>
690
+ </div>
691
+ )}
692
 
693
  {/* Assignees Input (Collapsible custom visual list) */}
694
  {node.type !== 'comment' && (
 
783
  </div>
784
  )}
785
 
786
+ {/* Goal Tags Editor */}
787
+ {node.type !== 'comment' && (
788
+ <div className="border-2 border-primary rounded-2xl p-4 bg-secondary/5 space-y-2">
789
+ <div
790
+ onClick={() => setIsTagsExpanded(!isTagsExpanded)}
791
+ className="flex items-center justify-between cursor-pointer select-none"
792
+ >
793
+ <label className="block text-[11px] font-bold uppercase tracking-widest text-primary/60 cursor-pointer">
794
+ {t('canvas.tags_label', 'Теги цілі')}
795
+ </label>
796
+ <div className="flex items-center gap-2">
797
+ {!isTagsExpanded && tags.length > 0 && (
798
+ <div className="flex flex-wrap gap-1 items-center max-w-[150px] justify-end">
799
+ {tags.slice(0, 2).map((tg, i) => (
800
+ <span key={i} className="text-[8px] font-extrabold px-1.5 py-0.5 rounded border border-primary/20 bg-secondary/15 text-primary">
801
+ {tg}
802
+ </span>
803
+ ))}
804
+ {tags.length > 2 && <span className="text-[8px] font-bold">+{tags.length - 2}</span>}
805
+ </div>
806
+ )}
807
+ {isTagsExpanded ? <ChevronUp size={16} className="text-primary" /> : <ChevronDown size={16} className="text-primary" />}
808
+ </div>
809
+ </div>
810
+
811
+ {isTagsExpanded && (
812
+ <div className="space-y-3 pt-2 animate-fade-in">
813
+ {/* Current Tags */}
814
+ {tags.length > 0 ? (
815
+ <div className="flex flex-wrap gap-1.5">
816
+ {tags.map((tg, idx) => (
817
+ <span
818
+ key={idx}
819
+ className="flex items-center gap-1.5 text-[10px] font-extrabold px-2.5 py-0.5 rounded-lg border-2 border-primary bg-surface-container-lowest text-primary shadow-[1px_1px_0px_0px_var(--color-primary)]"
820
+ >
821
+ <span>{tg}</span>
822
+ <button
823
+ type="button"
824
+ onClick={() => setTags(tags.filter(t => t !== tg))}
825
+ className="hover:text-red-500 font-extrabold ml-1 cursor-pointer"
826
+ >
827
+ &times;
828
+ </button>
829
+ </span>
830
+ ))}
831
+ </div>
832
+ ) : (
833
+ <p className="text-[10px] text-primary/40 italic">{t('canvas.no_tags', 'Немає тегів')}</p>
834
+ )}
835
+
836
+ {/* Quick default tags selector */}
837
+ <div className="space-y-1">
838
+ <span className="block text-[8px] font-bold uppercase tracking-wider text-primary/50">{t('canvas.quick_add_tags', 'Швидкі теги')}</span>
839
+ <div className="flex flex-wrap gap-1 max-h-[70px] overflow-y-auto pr-1">
840
+ {['Todo', 'In Progress', 'Done', 'Feature', 'Bug', 'Refactor', 'High', 'Medium', 'Low'].map(tg => {
841
+ const isAdded = tags.includes(tg);
842
+ return (
843
+ <button
844
+ key={tg}
845
+ type="button"
846
+ disabled={isAdded}
847
+ onClick={() => setTags([...tags, tg])}
848
+ className={`text-[9px] font-extrabold px-2 py-1 rounded-lg border-2 border-primary/20 transition-all ${
849
+ isAdded
850
+ ? 'bg-secondary/20 text-primary/40 cursor-not-allowed border-transparent'
851
+ : 'bg-surface-container-lowest text-primary border-primary/40 hover:border-primary cursor-pointer'
852
+ }`}
853
+ >
854
+ +{tg}
855
+ </button>
856
+ );
857
+ })}
858
+ </div>
859
+ </div>
860
+
861
+ {/* Add Custom Tag */}
862
+ <div className="flex gap-2">
863
+ <input
864
+ type="text"
865
+ value={customTagInput}
866
+ onChange={(e) => setCustomTagInput(e.target.value)}
867
+ placeholder={t('canvas.custom_tag_placeholder', 'Свій тег')}
868
+ className="flex-grow px-3 py-1.5 bg-secondary/10 border-2 border-primary rounded-xl text-xs font-bold text-primary focus:outline-none focus:bg-surface-container-lowest transition-all"
869
+ onKeyDown={(e) => {
870
+ if (e.key === 'Enter') {
871
+ e.preventDefault();
872
+ if (customTagInput.trim() && !tags.includes(customTagInput.trim())) {
873
+ setTags([...tags, customTagInput.trim()]);
874
+ setCustomTagInput('');
875
+ }
876
+ }
877
+ }}
878
+ />
879
+ <button
880
+ type="button"
881
+ onClick={() => {
882
+ if (customTagInput.trim() && !tags.includes(customTagInput.trim())) {
883
+ setTags([...tags, customTagInput.trim()]);
884
+ setCustomTagInput('');
885
+ }
886
+ }}
887
+ className="px-3 py-1.5 bg-primary text-on-primary font-bold text-xs rounded-xl border-2 border-primary cursor-pointer hover:scale-105 transition-all shadow-[2px_2px_0px_0px_var(--color-primary)] active:translate-y-0.5"
888
+ >
889
+ +
890
+ </button>
891
+ </div>
892
+ </div>
893
+ )}
894
+ </div>
895
+ )}
896
+
897
  {/* Show Progress Toggle */}
898
  {node.type !== 'comment' && (
899
  <div className="flex items-center justify-between bg-secondary/5 border-2 border-primary rounded-xl p-4">
 
931
  min="0"
932
  max="100"
933
  value={progress}
934
+ onChange={(e) => handleProgressChange(e.target.value)}
935
  className="w-full h-2 rounded-lg appearance-none cursor-pointer focus:outline-none transition-all accent-emerald-500"
936
  style={{
937
  background: `linear-gradient(to right, #10b981 ${progress}%, var(--color-border, #e2e8f0) ${progress}%)`
 
1397
  );
1398
  };
1399
 
1400
+ const TaskListView = ({
1401
+ nodes, edges, updateNodeData, collaborators = [], permission, t, isDarkMode,
1402
+ searchQuery, setSearchQuery, filterActive, setFilterActive, filterCollaborator,
1403
+ setFilterCollaborator, filterTag, setFilterTag, expandedNodes, setExpandedNodes,
1404
+ setEditingSketchNodeId, setEditingHabitNodeId
1405
+ }) => {
1406
+ const [addingTagNodeId, setAddingTagNodeId] = useState(null);
1407
+ const [inlineTagText, setInlineTagText] = useState('');
1408
+
1409
+ // 1. Identify subgoals & build dependency structures
1410
+ const subgoalEdges = useMemo(() => edges.filter(e => e.sourceHandle?.startsWith('subgoals-source')), [edges]);
1411
+
1412
+ const subtaskTargetIds = useMemo(() => new Set(subgoalEdges.map(e => e.target)), [subgoalEdges]);
1413
+
1414
+ const getSubtasks = useCallback((parentId) => {
1415
+ return subgoalEdges
1416
+ .filter(e => e.source === parentId)
1417
+ .map(e => nodes.find(n => n.id === e.target))
1418
+ .filter(Boolean)
1419
+ .filter(n => n.type === 'sketch' || n.type === 'habit');
1420
+ }, [subgoalEdges, nodes]);
1421
+
1422
+ // 2. Extract tags & filter matching
1423
+ const allUniqueTags = useMemo(() => {
1424
+ const defaultTags = ['Todo', 'In Progress', 'Done', 'Feature', 'Bug', 'Refactor', 'High', 'Medium', 'Low'];
1425
+ const set = new Set(defaultTags);
1426
+ nodes.forEach(n => {
1427
+ n.data?.tags?.forEach(tg => {
1428
+ if (tg && typeof tg === 'string') {
1429
+ set.add(tg.trim());
1430
+ }
1431
+ });
1432
+ });
1433
+ return Array.from(set);
1434
+ }, [nodes]);
1435
+
1436
+ const matchesFilters = useCallback((node) => {
1437
+ // 1. Active filter: те к которым мы подошли и еще не выполнили
1438
+ if (filterActive && !(node.data?.isReached && node.data?.status !== 'Done')) {
1439
+ return false;
1440
+ }
1441
+
1442
+ // 2. Collaborator filter: чисто задания конкретного участника
1443
+ if (filterCollaborator !== 'all') {
1444
+ const assignees = node.data?.assignees || [];
1445
+ const isAssigned = assignees.some(a => (a.sharedWithUserId || 'owner') === filterCollaborator);
1446
+ if (!isAssigned) return false;
1447
+ }
1448
+
1449
+ // 3. Tag filter
1450
+ if (filterTag !== 'all') {
1451
+ if (!node.data?.tags?.includes(filterTag)) {
1452
+ return false;
1453
+ }
1454
+ }
1455
+
1456
+ // 4. Search query
1457
+ if (searchQuery.trim() !== '') {
1458
+ const q = searchQuery.toLowerCase();
1459
+ const l = (node.data?.label || '').toLowerCase();
1460
+ const d = (node.data?.description || '').toLowerCase();
1461
+ const tgs = (node.data?.tags || []).map(tg => tg.toLowerCase());
1462
+ if (!l.includes(q) && !d.includes(q) && !tgs.some(tg => tg.includes(q))) {
1463
+ return false;
1464
+ }
1465
+ }
1466
+
1467
+ return true;
1468
+ }, [filterActive, filterCollaborator, filterTag, searchQuery]);
1469
+
1470
+ const nodeOrDescendantMatches = useCallback((node) => {
1471
+ if (matchesFilters(node)) return true;
1472
+ return getSubtasks(node.id).some(s => nodeOrDescendantMatches(s));
1473
+ }, [matchesFilters, getSubtasks]);
1474
+
1475
+ const orderedRootTasks = useMemo(() => {
1476
+ const rootTasks = nodes.filter(n => (n.type === 'sketch' || n.type === 'habit') && !subtaskTargetIds.has(n.id));
1477
+ const normalEdges = edges.filter(e => !e.sourceHandle?.startsWith('subgoals-source'));
1478
+
1479
+ // Build adj list and inDegrees for standard edges (sequence flow)
1480
+ const adj = {};
1481
+ const inDegree = {};
1482
+ rootTasks.forEach(t => {
1483
+ adj[t.id] = [];
1484
+ inDegree[t.id] = 0;
1485
+ });
1486
+
1487
+ normalEdges.forEach(e => {
1488
+ if (adj[e.source] && adj[e.target] !== undefined) {
1489
+ adj[e.source].push(e.target);
1490
+ inDegree[e.target]++;
1491
+ }
1492
+ });
1493
+
1494
+ const queue = rootTasks.filter(t => inDegree[t.id] === 0).map(t => t.id);
1495
+ const orderedIds = [];
1496
+
1497
+ while (queue.length > 0) {
1498
+ queue.sort((a, b) => {
1499
+ const nodeA = nodes.find(n => n.id === a);
1500
+ const nodeB = nodes.find(n => n.id === b);
1501
+ return (nodeA?.position?.y || 0) - (nodeB?.position?.y || 0);
1502
+ });
1503
+
1504
+ const curr = queue.shift();
1505
+ orderedIds.push(curr);
1506
+
1507
+ adj[curr]?.forEach(neigh => {
1508
+ inDegree[neigh]--;
1509
+ if (inDegree[neigh] === 0) {
1510
+ queue.push(neigh);
1511
+ }
1512
+ });
1513
+ }
1514
+
1515
+ rootTasks.forEach(t => {
1516
+ if (!orderedIds.includes(t.id)) {
1517
+ orderedIds.push(t.id);
1518
+ }
1519
+ });
1520
+
1521
+ return orderedIds
1522
+ .map(id => nodes.find(n => n.id === id))
1523
+ .filter(Boolean)
1524
+ .filter(nodeOrDescendantMatches);
1525
+ }, [nodes, edges, subtaskTargetIds, nodeOrDescendantMatches]);
1526
+
1527
+ const toggleExpanded = (nodeId) => {
1528
+ const newExpanded = new Set(expandedNodes);
1529
+ if (newExpanded.has(nodeId)) {
1530
+ newExpanded.delete(nodeId);
1531
+ } else {
1532
+ newExpanded.add(nodeId);
1533
+ }
1534
+ setExpandedNodes(newExpanded);
1535
+ };
1536
+
1537
+ const handleToggleStatus = (nodeId, currentStatus) => {
1538
+ if (permission === 'Read') return;
1539
+ const isDone = currentStatus === 'Done';
1540
+ const newStatus = isDone ? 'Todo' : 'Done';
1541
+ const newProgress = isDone ? 0 : 100;
1542
+
1543
+ updateNodeData(nodeId, {
1544
+ status: newStatus,
1545
+ progress: newProgress
1546
+ });
1547
+ };
1548
+
1549
+ const handleDropdownStatusChange = (nodeId, newStatus) => {
1550
+ if (permission === 'Read') return;
1551
+ let newProgress = 0;
1552
+ if (newStatus === 'Done') {
1553
+ newProgress = 100;
1554
+ } else if (newStatus === 'Todo') {
1555
+ newProgress = 0;
1556
+ } else if (newStatus === 'Review') {
1557
+ newProgress = 90;
1558
+ } else {
1559
+ newProgress = 50;
1560
+ }
1561
+
1562
+ updateNodeData(nodeId, {
1563
+ status: newStatus,
1564
+ progress: newProgress
1565
+ });
1566
+ };
1567
+
1568
+ const renderTaskCard = (node, depth = 0) => {
1569
+ const subtasks = getSubtasks(node.id).filter(nodeOrDescendantMatches);
1570
+ const isExpanded = expandedNodes.has(node.id);
1571
+ const hasSubtasks = subtasks.length > 0;
1572
+
1573
+ const isDone = node.data?.status === 'Done';
1574
+ const status = node.data?.status || 'Todo';
1575
+ const progress = node.data?.progress || 0;
1576
+
1577
+ const pastelColor = node.data?.color || 'default';
1578
+ const bgColors = {
1579
+ default: 'bg-surface-container-lowest border-primary',
1580
+ yellow: 'bg-yellow-50 dark:bg-yellow-950/20 border-yellow-400 dark:border-yellow-600',
1581
+ blue: 'bg-blue-50 dark:bg-blue-950/20 border-blue-400 dark:border-blue-600',
1582
+ green: 'bg-emerald-50 dark:bg-emerald-950/20 border-emerald-400 dark:border-emerald-600',
1583
+ pink: 'bg-pink-50 dark:bg-pink-950/20 border-pink-400 dark:border-pink-600',
1584
+ purple: 'bg-purple-50 dark:bg-purple-950/20 border-purple-400 dark:border-purple-600',
1585
+ orange: 'bg-orange-50 dark:bg-orange-950/20 border-orange-400 dark:border-orange-600',
1586
+ white: 'bg-stone-50 dark:bg-stone-900/20 border-stone-300 dark:border-stone-700'
1587
+ };
1588
+ const cardBg = bgColors[pastelColor] || bgColors.default;
1589
+
1590
+ return (
1591
+ <div key={node.id} className="space-y-3">
1592
+ <div
1593
+ className={`p-4 border-2 rounded-2xl flex flex-col md:flex-row md:items-center justify-between gap-4 transition-all shadow-[3px_3px_0px_0px_var(--color-primary)] hover:shadow-[5px_5px_0px_0px_var(--color-primary)] ${cardBg} ${
1594
+ !node.data?.isReached ? 'opacity-65' : ''
1595
+ }`}
1596
+ >
1597
+ <div className="flex items-start gap-3 flex-1 min-w-0">
1598
+ <button
1599
+ disabled={permission === 'Read'}
1600
+ onClick={() => handleToggleStatus(node.id, status)}
1601
+ className={`mt-1 flex-shrink-0 w-5.5 h-5.5 rounded-lg border-2 border-primary transition-all flex items-center justify-center cursor-pointer ${
1602
+ isDone
1603
+ ? 'bg-success border-success text-black'
1604
+ : 'bg-surface-container-lowest hover:bg-secondary/15'
1605
+ }`}
1606
+ >
1607
+ {isDone && <Check size={14} strokeWidth={4} className="text-black" />}
1608
+ </button>
1609
+
1610
+ {hasSubtasks ? (
1611
+ <button
1612
+ onClick={() => toggleExpanded(node.id)}
1613
+ className="mt-1 flex-shrink-0 p-0.5 border-2 border-primary rounded-md bg-surface-container-lowest hover:bg-secondary/10 text-primary transition-all cursor-pointer shadow-[1px_1px_0px_0px_var(--color-primary)]"
1614
+ >
1615
+ {isExpanded ? <ChevronUp size={12} strokeWidth={3} /> : <ChevronDown size={12} strokeWidth={3} />}
1616
+ </button>
1617
+ ) : (
1618
+ <div className="w-5.5" />
1619
+ )}
1620
+
1621
+ <div className="min-w-0 flex-1">
1622
+ <div className="flex flex-wrap items-center gap-2">
1623
+ <span className={`font-display-md text-base text-primary leading-snug break-words ${isDone ? 'line-through opacity-50' : ''}`}>
1624
+ {node.data?.label || (node.type === 'habit' ? t('canvas.new_habit', 'Нова звичка') : t('canvas.goal_node', 'Ціль'))}
1625
+ </span>
1626
+
1627
+ <span className="text-[8px] font-extrabold uppercase px-1.5 py-0.5 rounded border border-primary/20 bg-secondary/10 text-primary/70 tracking-wider shadow-[0.5px_0.5px_0px_0px_rgba(0,0,0,0.05)]">
1628
+ {node.type === 'habit' ? 'Звичка' : 'Ціль'}
1629
+ </span>
1630
+
1631
+ {!node.data?.isReached && (
1632
+ <span className="text-[8px] font-extrabold uppercase px-1.5 py-0.5 rounded border border-amber-500 bg-amber-500/10 text-amber-500 tracking-wider">
1633
+ Заблоковано
1634
+ </span>
1635
+ )}
1636
+ </div>
1637
+
1638
+ {node.data?.description && (
1639
+ <p className={`text-xs text-primary/70 font-bold uppercase tracking-wide mt-1 leading-normal max-w-2xl break-words ${isDone ? 'opacity-40' : ''}`}>
1640
+ {node.data.description}
1641
+ </p>
1642
+ )}
1643
+
1644
+ <div className="flex flex-wrap items-center gap-1.5 mt-2.5">
1645
+ {(node.data?.tags || []).map((tg, idx) => (
1646
+ <span
1647
+ key={idx}
1648
+ className="flex items-center gap-1 text-[9px] font-extrabold px-1.5 py-0.5 rounded border border-primary bg-secondary/15 text-primary shadow-[1.5px_1.5px_0px_0px_rgba(0,0,0,0.05)]"
1649
+ >
1650
+ <span>{tg}</span>
1651
+ {permission !== 'Read' && (
1652
+ <button
1653
+ onClick={() => {
1654
+ const currentTags = node.data?.tags || [];
1655
+ updateNodeData(node.id, { tags: currentTags.filter(t => t !== tg) });
1656
+ }}
1657
+ className="hover:text-red-500 font-extrabold ml-0.5 text-[8px] cursor-pointer"
1658
+ >
1659
+ &times;
1660
+ </button>
1661
+ )}
1662
+ </span>
1663
+ ))}
1664
+
1665
+ {addingTagNodeId === node.id ? (
1666
+ <div className="flex items-center gap-1.5 animate-fade-in">
1667
+ <input
1668
+ type="text"
1669
+ autoFocus
1670
+ value={inlineTagText}
1671
+ onChange={(e) => setInlineTagText(e.target.value)}
1672
+ placeholder="Новий тег..."
1673
+ onKeyDown={(e) => {
1674
+ if (e.key === 'Enter') {
1675
+ e.preventDefault();
1676
+ if (inlineTagText.trim()) {
1677
+ const currentTags = node.data?.tags || [];
1678
+ if (!currentTags.includes(inlineTagText.trim())) {
1679
+ updateNodeData(node.id, { tags: [...currentTags, inlineTagText.trim()] });
1680
+ }
1681
+ }
1682
+ setAddingTagNodeId(null);
1683
+ setInlineTagText('');
1684
+ } else if (e.key === 'Escape') {
1685
+ setAddingTagNodeId(null);
1686
+ setInlineTagText('');
1687
+ }
1688
+ }}
1689
+ className="px-2 py-0.5 border border-primary rounded bg-surface-container-lowest text-[10px] font-bold text-primary focus:outline-none w-20"
1690
+ />
1691
+ <button
1692
+ onClick={() => {
1693
+ if (inlineTagText.trim()) {
1694
+ const currentTags = node.data?.tags || [];
1695
+ if (!currentTags.includes(inlineTagText.trim())) {
1696
+ updateNodeData(node.id, { tags: [...currentTags, inlineTagText.trim()] });
1697
+ }
1698
+ }
1699
+ setAddingTagNodeId(null);
1700
+ setInlineTagText('');
1701
+ }}
1702
+ className="text-[10px] font-extrabold text-green-600 hover:text-green-700 cursor-pointer"
1703
+ >
1704
+
1705
+ </button>
1706
+ <button
1707
+ onClick={() => {
1708
+ setAddingTagNodeId(null);
1709
+ setInlineTagText('');
1710
+ }}
1711
+ className="text-[10px] font-extrabold text-red-500 hover:text-red-600 cursor-pointer"
1712
+ >
1713
+ ×
1714
+ </button>
1715
+ </div>
1716
+ ) : (
1717
+ permission !== 'Read' && (
1718
+ <button
1719
+ onClick={() => {
1720
+ setAddingTagNodeId(node.id);
1721
+ setInlineTagText('');
1722
+ }}
1723
+ className="text-[9px] font-extrabold px-1.5 py-0.5 rounded border-2 border-dashed border-primary/40 hover:border-primary text-primary bg-secondary/5 hover:bg-secondary/10 transition-colors flex items-center gap-0.5 cursor-pointer shadow-[1px_1px_0px_0px_rgba(0,0,0,0.05)]"
1724
+ >
1725
+ <Plus size={8} />
1726
+ <span>Тег</span>
1727
+ </button>
1728
+ )
1729
+ )}
1730
+ </div>
1731
+ </div>
1732
+ </div>
1733
+
1734
+ <div className="flex items-center gap-4 flex-wrap shrink-0 justify-end">
1735
+ {node.type === 'sketch' && node.data?.showProgress && (
1736
+ <div className="flex flex-col gap-1 w-24">
1737
+ <div className="flex items-center justify-between text-[9px] font-extrabold text-primary/60">
1738
+ <span>Прогрес</span>
1739
+ <span>{progress}%</span>
1740
+ </div>
1741
+ <div className="w-full bg-secondary/20 h-1.5 rounded-full overflow-hidden border border-primary/10">
1742
+ <div className="bg-emerald-500 h-full transition-all" style={{ width: `${progress}%` }} />
1743
+ </div>
1744
+ </div>
1745
+ )}
1746
+
1747
+ {node.data?.assignees && node.data.assignees.length > 0 && (
1748
+ <div className="flex items-center -space-x-1 flex-row-reverse">
1749
+ {node.data.assignees.slice(0, 3).reverse().map((a, i) => (
1750
+ <div
1751
+ key={a.sharedWithUserId || a.email || i}
1752
+ title={a.displayName || a.email}
1753
+ className="w-6.5 h-6.5 rounded-full border border-primary bg-secondary/15 text-primary flex items-center justify-center text-[9px] font-extrabold shadow-[1px_1px_0px_0px_var(--color-primary)]"
1754
+ >
1755
+ {getAssigneeInitials(a.displayName, a.email)}
1756
+ </div>
1757
+ ))}
1758
+ {node.data.assignees.length > 3 && (
1759
+ <span className="text-[9px] font-extrabold text-primary mr-1">+{node.data.assignees.length - 3}</span>
1760
+ )}
1761
+ </div>
1762
+ )}
1763
+
1764
+ {node.type !== 'comment' && (
1765
+ <select
1766
+ disabled={permission === 'Read'}
1767
+ value={status}
1768
+ onChange={(e) => handleDropdownStatusChange(node.id, e.target.value)}
1769
+ className={`px-2.5 py-1.5 rounded-xl border-2 border-primary font-bold text-xs uppercase cursor-pointer transition-all shadow-[2px_2px_0px_0px_var(--color-primary)] focus:outline-none focus:translate-y-0.5 focus:shadow-[1px_1px_0px_0px_var(--color-primary)] ${
1770
+ status === 'Done'
1771
+ ? 'bg-success/20 text-success'
1772
+ : status === 'Review'
1773
+ ? 'bg-blue-500/20 text-blue-600 dark:text-blue-400'
1774
+ : status === 'In Progress'
1775
+ ? 'bg-amber-500/20 text-amber-600 dark:text-amber-400'
1776
+ : 'bg-secondary/10 text-primary/70'
1777
+ }`}
1778
+ >
1779
+ <option value="Todo" className="bg-surface-container-lowest text-primary">Todo</option>
1780
+ <option value="In Progress" className="bg-surface-container-lowest text-primary">In Progress</option>
1781
+ <option value="Review" className="bg-surface-container-lowest text-primary">Review</option>
1782
+ <option value="Done" className="bg-surface-container-lowest text-primary">Done</option>
1783
+ </select>
1784
+ )}
1785
+
1786
+ {permission !== 'Read' && (
1787
+ <button
1788
+ onClick={() => {
1789
+ if (node.type === 'habit') {
1790
+ setEditingHabitNodeId(node.id);
1791
+ } else {
1792
+ setEditingSketchNodeId(node.id);
1793
+ }
1794
+ }}
1795
+ className="p-2 border-2 border-primary rounded-xl bg-surface-container-lowest hover:bg-secondary/10 text-primary transition-all cursor-pointer shadow-[2px_2px_0px_0px_var(--color-primary)] active:translate-y-0.5 active:shadow-[1px_1px_0px_0px_var(--color-primary)]"
1796
+ title="Налаштування ноди"
1797
+ >
1798
+ <Settings size={14} />
1799
+ </button>
1800
+ )}
1801
+ </div>
1802
+ </div>
1803
+
1804
+ {hasSubtasks && isExpanded && (
1805
+ <div className="pl-6 border-l-2 border-dashed border-primary/20 ml-4 space-y-4 pt-1 pb-2">
1806
+ {subtasks.map(sub => renderTaskCard(sub, depth + 1))}
1807
+ </div>
1808
+ )}
1809
+ </div>
1810
+ );
1811
+ };
1812
+
1813
+ return (
1814
+ <div className="flex-grow flex flex-col bg-background overflow-y-auto p-8 graph-paper-bg sketch-flow">
1815
+ <div className="bg-surface-container-lowest border-4 border-primary rounded-3xl p-6 shadow-[5px_5px_0px_0px_var(--color-primary)] mb-8 flex flex-col gap-4 max-w-5xl w-full mx-auto">
1816
+ <div className="flex items-center gap-3">
1817
+ <ListTodo size={24} className="text-primary animate-pulse" />
1818
+ <h3 className="text-xl font-display-lg text-primary">Список завдань та прогресу</h3>
1819
+ </div>
1820
+
1821
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-center">
1822
+ <div className="lg:col-span-6 relative">
1823
+ <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 text-primary/40" size={16} />
1824
+ <input
1825
+ type="text"
1826
+ value={searchQuery}
1827
+ onChange={(e) => setSearchQuery(e.target.value)}
1828
+ placeholder="Шукати за назвою, описом або тегами..."
1829
+ className="w-full pl-10 pr-4 py-2.5 bg-secondary/10 border-2 border-primary rounded-xl font-bold text-xs text-primary focus:outline-none focus:bg-surface-container-lowest transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,0.05)]"
1830
+ />
1831
+ </div>
1832
+
1833
+ <div className="lg:col-span-2">
1834
+ <button
1835
+ onClick={() => setFilterActive(!filterActive)}
1836
+ className={`w-full py-2.5 px-3 rounded-xl border-2 font-bold text-xs uppercase transition-all cursor-pointer text-center flex items-center justify-center gap-1.5 shadow-[2px_2px_0px_0px_var(--color-primary)] active:translate-y-0.5 active:shadow-[1px_1px_0px_0px_var(--color-primary)] ${
1837
+ filterActive
1838
+ ? 'bg-primary text-white border-primary'
1839
+ : 'bg-surface-container-lowest text-primary border-primary hover:bg-secondary/5'
1840
+ }`}
1841
+ >
1842
+ <Target size={14} />
1843
+ <span>Лише активні</span>
1844
+ </button>
1845
+ </div>
1846
+
1847
+ <div className="lg:col-span-2">
1848
+ <select
1849
+ value={filterCollaborator}
1850
+ onChange={(e) => setFilterCollaborator(e.target.value)}
1851
+ className="w-full py-2.5 px-3 rounded-xl border-2 border-primary font-bold text-xs text-primary bg-surface-container-lowest cursor-pointer shadow-[2px_2px_0px_0px_var(--color-primary)] focus:outline-none"
1852
+ >
1853
+ <option value="all">Усі виконавці</option>
1854
+ <option value="owner">Власник</option>
1855
+ {collaborators.map(c => (
1856
+ <option key={c.sharedWithUserId || c.email} value={c.sharedWithUserId || 'owner'}>
1857
+ {c.displayName || c.email}
1858
+ </option>
1859
+ ))}
1860
+ </select>
1861
+ </div>
1862
+
1863
+ <div className="lg:col-span-2">
1864
+ <select
1865
+ value={filterTag}
1866
+ onChange={(e) => setFilterTag(e.target.value)}
1867
+ className="w-full py-2.5 px-3 rounded-xl border-2 border-primary font-bold text-xs text-primary bg-surface-container-lowest cursor-pointer shadow-[2px_2px_0px_0px_var(--color-primary)] focus:outline-none"
1868
+ >
1869
+ <option value="all">Усі теги</option>
1870
+ {allUniqueTags.map(tg => (
1871
+ <option key={tg} value={tg}>
1872
+ {tg}
1873
+ </option>
1874
+ ))}
1875
+ </select>
1876
+ </div>
1877
+ </div>
1878
+ </div>
1879
+
1880
+ <div className="flex-1 space-y-4 max-w-5xl w-full mx-auto pb-12">
1881
+ {orderedRootTasks.length === 0 ? (
1882
+ <div className="bg-surface-container-lowest border-4 border-primary rounded-3xl p-12 text-center shadow-[6px_6px_0px_0px_var(--color-primary)]">
1883
+ <div className="flex justify-center mb-4">
1884
+ <StickyNote size={48} className="text-primary/30 animate-bounce" />
1885
+ </div>
1886
+ <h4 className="text-lg font-display-md text-primary mb-2">Немає завдань</h4>
1887
+ <p className="text-xs font-bold uppercase tracking-widest text-primary/50 max-w-md mx-auto leading-relaxed">
1888
+ Створіть цілі або звички на холсті або змініть налаштування фільтрації у верхній панелі!
1889
+ </p>
1890
+ </div>
1891
+ ) : (
1892
+ orderedRootTasks.map(node => renderTaskCard(node, 0))
1893
+ )}
1894
+ </div>
1895
+ </div>
1896
+ );
1897
+ };
1898
+
1899
  const CanvasInner = () => {
1900
  const { t } = useTranslation();
1901
  const { id } = useParams();
 
1953
  updateProjectMetadata
1954
  } = useCanvasLogic(id, user);
1955
 
1956
+ const [activeTab, setActiveTab] = useState('canvas'); // 'canvas' | 'list'
1957
+ const [searchQuery, setSearchQuery] = useState('');
1958
+ const [filterActive, setFilterActive] = useState(false);
1959
+ const [filterCollaborator, setFilterCollaborator] = useState('all');
1960
+ const [filterTag, setFilterTag] = useState('all');
1961
+ const [expandedNodes, setExpandedNodes] = useState(new Set());
1962
+
1963
  const lastCursorSendRef = useRef(0);
1964
  const broadcastRef = useRef(broadcastMessage);
1965
 
 
2512
  <div className="flex h-screen bg-background overflow-hidden" onMouseUp={handleMouseUp}>
2513
  <Sidebar />
2514
 
2515
+ <main className="flex-grow flex flex-col relative text-primary">
2516
  <div className="relative h-24 bg-surface-container-lowest border-b-4 border-primary px-8 flex items-center justify-between z-30 shadow-sm">
2517
  <div className="flex items-center gap-6">
2518
  <button
2519
  onClick={() => navigate('/dashboard')}
2520
+ className="p-2 hover:bg-surface-variant rounded-xl transition-colors text-primary"
2521
  >
2522
  <ChevronLeft size={28} />
2523
  </button>
 
2548
  {projectData?.description || t('canvas.strategic_goal_map')}
2549
  </p>
2550
  </div>
2551
+
2552
+ {/* Styled Notebook Tabs Segmented Control */}
2553
+ <div className="flex items-center bg-secondary/5 border-2 border-primary rounded-xl p-1 shadow-[2px_2px_0px_0px_var(--color-primary)] ml-4">
2554
+ <button
2555
+ type="button"
2556
+ onClick={() => setActiveTab('canvas')}
2557
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-xs uppercase tracking-wider transition-all cursor-pointer select-none ${
2558
+ activeTab === 'canvas'
2559
+ ? 'bg-primary text-white shadow-[1px_1px_0px_0px_var(--color-primary)]'
2560
+ : 'text-primary hover:bg-secondary/10'
2561
+ }`}
2562
+ >
2563
+ <Map size={16} />
2564
+ <span>{t('canvas.tabs.canvas', 'Карта')}</span>
2565
+ </button>
2566
+ <button
2567
+ type="button"
2568
+ onClick={() => setActiveTab('list')}
2569
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-xs uppercase tracking-wider transition-all cursor-pointer select-none ${
2570
+ activeTab === 'list'
2571
+ ? 'bg-primary text-white shadow-[1px_1px_0px_0px_var(--color-primary)]'
2572
+ : 'text-primary hover:bg-secondary/10'
2573
+ }`}
2574
+ >
2575
+ <ListTodo size={16} />
2576
+ <span>{t('canvas.tabs.list', 'Завдання')}</span>
2577
+ </button>
2578
+ </div>
2579
  </div>
2580
 
2581
  <div className="flex items-center gap-4">
 
2634
  </div>
2635
  </div>
2636
 
2637
+ {/* React Flow Canvas Wrapper with visibility toggled via display style */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2638
  <div
2639
+ className="flex-grow flex flex-col relative"
2640
+ style={{ display: activeTab === 'canvas' ? 'flex' : 'none' }}
 
 
 
 
2641
  >
2642
+ {permission !== 'Read' && (
2643
+ <CanvasToolbar
2644
+ handleAIRequest={handleAIRequest}
2645
+ isAIActive={isAIActive}
2646
+ user={user}
2647
+ addNewNodeAt={addNewNodeAt}
2648
+ handleCopy={handleCopy}
2649
+ handleCut={handleCut}
2650
+ handlePaste={() => handlePaste(mousePosRef.current)}
2651
+ exportToPng={exportToPng}
2652
+ onDeleteSelected={() => {
2653
+ const selectedNodeIds = nodes.filter(n => n.selected).map(n => n.id);
2654
+ const selectedEdgeIds = edges.filter(e => e.selected).map(e => e.id);
2655
+ requestDeletion(selectedNodeIds, selectedEdgeIds);
2656
+ }}
2657
+ hasSelectedNodes={nodes.some(n => n.selected)}
2658
+ hasClipboardContent={hasClipboardContent}
2659
+ undo={undo}
2660
+ redo={redo}
2661
+ canUndo={canUndo}
2662
+ canRedo={canRedo}
2663
+ drawingMode={drawingMode}
2664
+ setDrawingMode={setDrawingMode}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2665
  />
2666
  )}
2667
 
2668
+ <div
2669
+ ref={reactFlowWrapper}
2670
+ className="flex-grow graph-paper-bg relative"
2671
+ onMouseDown={drawingMode ? undefined : handleMouseDown}
2672
+ onPointerDown={onPointerDown}
2673
+ onPointerMove={onPointerMove}
2674
+ onPointerUp={onPointerUp}
2675
+ >
2676
+ <ReactFlow
2677
+ nodes={nodes}
2678
+ edges={edges}
2679
+ onNodesChange={onNodesChange}
2680
+ onEdgesChange={onEdgesChange}
2681
+ onConnect={permission === 'Read' ? undefined : onConnect}
2682
+ onEdgesDelete={permission === 'Read' ? undefined : (eds) => requestDeletion([], eds.map(e => e.id))}
2683
+ onEdgeClick={permission === 'Read' ? undefined : (_, edge) => requestDeletion([], [edge.id])}
2684
+ onNodesDelete={permission === 'Read' ? undefined : (nds) => requestDeletion(nds.map(n => n.id), [])}
2685
+ onNodeDragStart={permission === 'Read' ? undefined : () => takeHistorySnapshot()}
2686
+ onNodeContextMenu={handleNodeContextMenu}
2687
+ onPaneClick={() => setContextMenu(null)}
2688
+ onPaneContextMenu={handlePaneContextMenu}
2689
+ onDrop={onDrop}
2690
+ onDragOver={onDragOver}
2691
+ onConnectStart={(event, params) => {
2692
+ connectionStartRef.current = params;
2693
+ }}
2694
+ onConnectEnd={onConnectEnd}
2695
+ nodesDraggable={!drawingMode && permission !== 'Read'}
2696
+ nodesConnectable={!drawingMode && permission !== 'Read'}
2697
+ panOnDrag={!drawingMode}
2698
+ selectionOnDrag={!drawingMode}
2699
+ nodeTypes={nodeTypes}
2700
+ edgeTypes={edgeTypes}
2701
+ connectionLineType={ConnectionLineType.SmoothStep}
2702
+ fitView
2703
+ className="sketch-flow"
2704
+ >
2705
+ <Background variant="dots" gap={20} size={1} color={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)'} />
2706
+ <Controls className="!bottom-8 !left-28 !overflow-hidden" />
2707
+ <MiniMap
2708
+ className="!bottom-8 !right-8 !overflow-hidden"
2709
+ nodeStrokeColor={isDarkMode ? '#e2e3e4' : '#31363F'}
2710
+ nodeColor={isDarkMode ? '#222326' : '#EEEEEE'}
2711
+ maskColor={isDarkMode ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.6)'}
2712
+ />
2713
+ <CollaborativeCursors cursors={cursors} />
2714
+ </ReactFlow>
2715
 
2716
+ {activeStroke && (
2717
+ <svg
2718
+ className="absolute inset-0 pointer-events-none z-[1000]"
2719
+ style={{ width: '100%', height: '100%' }}
2720
+ >
2721
+ <path
2722
+ d={activeStroke.path}
2723
+ fill="none"
2724
+ stroke={isDarkMode ? '#ffffff' : '#31363F'}
2725
+ strokeWidth={3}
2726
+ strokeLinecap="round"
2727
+ strokeLinejoin="round"
2728
+ />
2729
+ </svg>
2730
+ )}
2731
 
2732
+ <AIHintOverlay
2733
+ isAIActive={isAIActive}
2734
+ setIsAIActive={setIsAIActive}
2735
+ aiLoading={aiLoading}
2736
+ submitAICanvasPrompt={submitAICanvasPrompt}
 
 
2737
  />
2738
+ <PaywallModal showPaywall={showPaywall} setShowPaywall={setShowPaywall} />
2739
 
2740
+ {contextMenu && contextMenu.type === 'node' && (
2741
+ <div
2742
+ style={{ top: contextMenu.y, left: contextMenu.x }}
2743
+ className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[135px] rounded-lg transition-all flex flex-col gap-0.5"
2744
+ onClick={(e) => e.stopPropagation()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2745
  >
2746
+ <button
2747
+ onClick={(e) => {
2748
+ e.stopPropagation();
2749
+ const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2750
+ if (targetNode) {
2751
+ const isSelected = targetNode.selected;
2752
+ if (!isSelected) {
2753
+ setNodes(nds => nds.map(n => n.id === contextMenu.nodeId ? { ...n, selected: true } : n));
2754
+ }
2755
+ const selectedNodes = nodes.filter(n => n.selected || n.id === contextMenu.nodeId);
2756
+ const selectedEdges = edges.filter(edge =>
2757
+ selectedNodes.some(n => n.id === edge.source) &&
2758
+ selectedNodes.some(n => n.id === edge.target)
2759
+ );
2760
+ const clipboardPayload = {
2761
+ nodes: selectedNodes.map(n => ({ ...n, originalPosition: { ...n.position } })),
2762
+ edges: selectedEdges
2763
+ };
2764
+ window.__flowClipboard = clipboardPayload;
2765
+ triggerClipboardUpdate();
2766
+ try {
2767
+ navigator.clipboard.writeText('FLOWWEB_NODES_DATA:' + JSON.stringify(clipboardPayload)).catch(() => {});
2768
+ } catch (err) {}
2769
+ }
2770
+ setContextMenu(null);
2771
+ }}
2772
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2773
+ >
2774
+ <Copy size={14} className="text-primary" />
2775
+ <span>{t('canvas.context_menu.copy', 'Копіювати')}</span>
2776
+ </button>
2777
+
2778
+ <button
2779
+ onClick={(e) => {
2780
+ e.stopPropagation();
2781
+ const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2782
+ if (targetNode) {
2783
+ const selectedNodes = nodes.filter(n => n.selected || n.id === contextMenu.nodeId);
2784
+ const selectedEdges = edges.filter(edge =>
2785
+ selectedNodes.some(n => n.id === edge.source) &&
2786
+ selectedNodes.some(n => n.id === edge.target)
2787
+ );
2788
+ const clipboardPayload = {
2789
+ nodes: selectedNodes.map(n => ({ ...n, originalPosition: { ...n.position } })),
2790
+ edges: selectedEdges
2791
+ };
2792
+ window.__flowClipboard = clipboardPayload;
2793
+ triggerClipboardUpdate();
2794
+ try {
2795
+ navigator.clipboard.writeText('FLOWWEB_NODES_DATA:' + JSON.stringify(clipboardPayload)).catch(() => {});
2796
+ } catch (err) {}
2797
+ selectedNodes.forEach(n => deleteNode(n.id));
2798
+ }
2799
+ setContextMenu(null);
2800
+ }}
2801
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2802
+ >
2803
+ <Scissors size={14} className="text-primary" />
2804
+ <span>{t('canvas.context_menu.cut', 'Вирізати')}</span>
2805
+ </button>
2806
+
2807
+ <button
2808
+ onClick={(e) => {
2809
+ e.stopPropagation();
2810
+ const targetNode = nodes.find(n => n.id === contextMenu.nodeId);
2811
+ if (targetNode) {
2812
+ const newPinnedState = !targetNode.data?.isPinned;
2813
+ updateNodeData(contextMenu.nodeId, { isPinned: newPinnedState });
2814
+ }
2815
+ setContextMenu(null);
2816
+ }}
2817
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2818
+ >
2819
+ <Pin
2820
+ size={14}
2821
+ className={`text-primary transition-transform ${
2822
+ nodes.find(n => n.id === contextMenu.nodeId)?.data?.isPinned
2823
+ ? 'rotate-45 text-amber-500 fill-amber-500'
2824
+ : ''
2825
+ }`}
2826
+ />
2827
+ <span>
2828
+ {nodes.find(n => n.id === contextMenu.nodeId)?.data?.isPinned
2829
+ ? t('canvas.context_menu.unpin', 'Відкріпити')
2830
+ : t('canvas.context_menu.pin', 'Закріпити')}
2831
+ </span>
2832
+ </button>
2833
+
2834
+ <div className="border-t border-primary/10 my-0.5"></div>
2835
+
2836
+ <button
2837
+ onClick={(e) => {
2838
+ e.stopPropagation();
2839
+ requestDeletion([contextMenu.nodeId], []);
2840
+ setContextMenu(null);
2841
+ }}
2842
+ className="w-full text-left px-2.5 py-1.5 hover:bg-red-500/20 text-red-600 hover:text-red-700 font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2843
+ >
2844
+ <Trash2 size={14} />
2845
+ <span>{t('canvas.context_menu.delete')}</span>
2846
+ </button>
2847
+ </div>
2848
+ )}
2849
 
2850
+ {contextMenu && contextMenu.type === 'pane' && (
2851
+ <div
2852
+ style={{ top: contextMenu.y, left: contextMenu.x }}
2853
+ className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[170px] rounded-lg transition-all flex flex-col gap-0.5"
2854
+ onClick={(e) => e.stopPropagation()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2855
  >
2856
+ {permission !== 'Read' && (
2857
+ <>
2858
+ <div className="px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-on-surface-variant/40 border-b border-primary/10">
2859
+ {t('canvas.context_menu.create_node')}
2860
+ </div>
2861
+ <button
2862
+ onClick={(e) => {
2863
+ e.stopPropagation();
2864
+ addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'sketch');
2865
+ setContextMenu(null);
2866
+ }}
2867
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2868
+ >
2869
+ <Target size={15} className="text-primary" />
2870
+ <span>{t('canvas.context_menu.goal_node')}</span>
2871
+ </button>
2872
+ <button
2873
+ onClick={(e) => {
2874
+ e.stopPropagation();
2875
+ addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'comment');
2876
+ setContextMenu(null);
2877
+ }}
2878
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2879
+ >
2880
+ <StickyNote size={15} className="text-secondary" />
2881
+ <span>Стікер</span>
2882
+ </button>
2883
+ <button
2884
+ onClick={(e) => {
2885
+ e.stopPropagation();
2886
+ addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'condition');
2887
+ setContextMenu(null);
2888
+ }}
2889
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2890
+ >
2891
+ <GitFork size={15} className="text-primary" />
2892
+ <span>{t('canvas.context_menu.condition_node')}</span>
2893
+ </button>
2894
+ <button
2895
+ onClick={(e) => {
2896
+ e.stopPropagation();
2897
+ addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'habit');
2898
+ setContextMenu(null);
2899
+ }}
2900
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2901
+ >
2902
+ <Timer size={15} className="text-secondary" />
2903
+ <span>{t('canvas.context_menu.habit_node')}</span>
2904
+ </button>
2905
+
2906
+ {window.__flowClipboard && window.__flowClipboard.nodes && window.__flowClipboard.nodes.length > 0 && (
2907
+ <>
2908
+ <div className="border-t border-primary/10 my-0.5"></div>
2909
+ <button
2910
+ onClick={(e) => {
2911
+ e.stopPropagation();
2912
+ handlePaste({ x: contextMenu.clientX, y: contextMenu.clientY });
2913
+ setContextMenu(null);
2914
+ }}
2915
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2916
+ >
2917
+ <Clipboard size={15} className="text-primary" />
2918
+ <span>{t('canvas.context_menu.paste', 'Вставити')}</span>
2919
+ </button>
2920
+ </>
2921
+ )}
2922
 
2923
+ <div className="border-t border-primary/10 my-0.5"></div>
2924
+ </>
2925
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2926
 
2927
+ <button
2928
+ onClick={(e) => {
2929
+ e.stopPropagation();
2930
+ exportToPng();
2931
+ setContextMenu(null);
2932
+ }}
2933
+ className="w-full text-left px-2.5 py-1.5 hover:bg-green-50 text-green-600 hover:text-green-700 font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2934
+ >
2935
+ <Image size={15} className="text-green-600" />
2936
+ <span>{t('canvas.context_menu.export_png')}</span>
2937
+ </button>
2938
+ </div>
2939
+ )}
2940
 
2941
+ {contextMenu && contextMenu.type === 'connect-pane' && (
2942
+ <div
2943
+ style={{ top: contextMenu.y, left: contextMenu.x }}
2944
+ className="absolute bg-surface-container-lowest border-2 border-primary shadow-[2px_2px_0px_0px_var(--color-primary)] py-1.5 px-1 z-50 min-w-[170px] rounded-lg transition-all flex flex-col gap-0.5"
2945
+ onClick={(e) => e.stopPropagation()}
 
 
2946
  >
2947
+ <div className="px-2.5 py-1 text-[9px] font-bold uppercase tracking-widest text-secondary border-b border-primary/10">
2948
+ Зв'язати з новою:
2949
+ </div>
2950
+ <button
2951
+ onClick={(e) => {
2952
+ e.stopPropagation();
2953
+ const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'sketch');
2954
+ if (contextMenu.connectionSource && newNodeId) {
2955
+ const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2956
+ const isSource = handleType === 'source';
2957
+ onConnect({
2958
+ source: isSource ? nodeId : newNodeId,
2959
+ target: isSource ? newNodeId : nodeId,
2960
+ sourceHandle: isSource ? handleId : 'main-source',
2961
+ targetHandle: isSource ? 'main-target' : handleId
2962
+ });
2963
+ }
2964
+ setContextMenu(null);
2965
+ }}
2966
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2967
+ >
2968
+ <Target size={15} className="text-primary" />
2969
+ <span>{t('canvas.context_menu.goal_node')}</span>
2970
+ </button>
2971
+ <button
2972
+ onClick={(e) => {
2973
+ e.stopPropagation();
2974
+ const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'condition');
2975
+ if (contextMenu.connectionSource && newNodeId) {
2976
+ const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2977
+ const isSource = handleType === 'source';
2978
+ onConnect({
2979
+ source: isSource ? nodeId : newNodeId,
2980
+ target: isSource ? newNodeId : nodeId,
2981
+ sourceHandle: isSource ? handleId : null,
2982
+ targetHandle: isSource ? null : handleId
2983
+ });
2984
+ }
2985
+ setContextMenu(null);
2986
+ }}
2987
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
2988
+ >
2989
+ <GitFork size={15} className="text-primary" />
2990
+ <span>{t('canvas.context_menu.condition_node')}</span>
2991
+ </button>
2992
+ <button
2993
+ onClick={(e) => {
2994
+ e.stopPropagation();
2995
+ const newNodeId = addNewNodeAt(contextMenu.clientX, contextMenu.clientY, 'habit');
2996
+ if (contextMenu.connectionSource && newNodeId) {
2997
+ const { nodeId, handleId, handleType } = contextMenu.connectionSource;
2998
+ const isSource = handleType === 'source';
2999
+ onConnect({
3000
+ source: isSource ? nodeId : newNodeId,
3001
+ target: isSource ? newNodeId : nodeId,
3002
+ sourceHandle: isSource ? handleId : null,
3003
+ targetHandle: isSource ? null : handleId
3004
+ });
3005
+ }
3006
+ setContextMenu(null);
3007
+ }}
3008
+ className="w-full text-left px-2.5 py-1.5 hover:bg-surface-variant text-primary font-bold rounded-md transition-colors flex items-center gap-2 text-xs"
3009
+ >
3010
+ <Timer size={15} className="text-secondary" />
3011
+ <span>{t('canvas.context_menu.habit_node')}</span>
3012
+ </button>
3013
+ </div>
3014
+ )}
3015
+ </div>
3016
+ </div>
3017
 
3018
+ {/* Task List Tab content */}
3019
+ {activeTab === 'list' && (
3020
+ <TaskListView
3021
+ nodes={nodes}
3022
+ edges={edges}
3023
+ updateNodeData={updateNodeData}
3024
+ collaborators={collaborators}
3025
+ permission={permission}
3026
+ t={t}
3027
+ isDarkMode={isDarkMode}
3028
+ searchQuery={searchQuery}
3029
+ setSearchQuery={setSearchQuery}
3030
+ filterActive={filterActive}
3031
+ setFilterActive={setFilterActive}
3032
+ filterCollaborator={filterCollaborator}
3033
+ setFilterCollaborator={setFilterCollaborator}
3034
+ filterTag={filterTag}
3035
+ setFilterTag={setFilterTag}
3036
+ expandedNodes={expandedNodes}
3037
+ setExpandedNodes={setExpandedNodes}
3038
+ setEditingSketchNodeId={setEditingSketchNodeId}
3039
+ setEditingHabitNodeId={setEditingHabitNodeId}
3040
+ />
3041
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3042
 
3043
+ {/* Globally rendered settings modals accessible from both views */}
3044
+ {editingNode && (
3045
+ <ConditionSettingsModal
3046
+ node={editingNode}
3047
+ onClose={() => setEditingConditionNodeId(null)}
3048
+ onSave={handleSaveConditionConfig}
3049
+ />
3050
+ )}
 
 
 
 
 
 
 
 
3051
 
3052
+ {editingHabitNode && (
3053
+ <HabitSettingsModal
3054
+ node={editingHabitNode}
3055
+ onClose={() => setEditingHabitNodeId(null)}
3056
+ onSave={handleSaveHabitConfig}
3057
+ />
3058
+ )}
3059
 
3060
+ {editingSketchNode && (
3061
+ <SketchSettingsModal
3062
+ node={editingSketchNode}
3063
+ onClose={() => setEditingSketchNodeId(null)}
3064
+ onSave={handleSaveSketchConfig}
3065
+ collaborators={collaborators}
3066
+ />
3067
+ )}
 
 
 
 
 
3068
 
3069
+ {isEditingMetadata && (
3070
+ <ProjectMetadataModal
3071
+ title={projectTitle}
3072
+ description={projectData?.description || ''}
3073
+ onClose={() => setIsEditingMetadata(false)}
3074
+ onSave={(newTitle, newDesc) => {
3075
+ updateProjectMetadata(newTitle, newDesc);
3076
+ setIsEditingMetadata(false);
3077
+ }}
3078
+ />
3079
+ )}
3080
+
3081
+ {isShareModalOpen && (
3082
+ <ShareModal
3083
+ projectId={id}
3084
+ onClose={() => {
3085
+ setIsShareModalOpen(false);
3086
+ fetchCollaborators();
3087
+ }}
3088
+ />
3089
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3090
  </main>
3091
  </div>
3092
  );