]> Eric's Git Repo - listv4.git/commitdiff
Toolbar and list updates
authorEric Wertz <ericwertz@Erics-MacBook-Pro.local>
Mon, 2 Jun 2025 01:58:40 +0000 (21:58 -0400)
committerEric Wertz <ericwertz@Erics-MacBook-Pro.local>
Mon, 2 Jun 2025 01:58:40 +0000 (21:58 -0400)
backend/ListController.cpp
frontend/app/AppContext.tsx
frontend/app_routes.tsx
frontend/components/Icons.tsx
frontend/components/ListContext.tsx
frontend/components/ListItem.tsx
frontend/components/Toolbar.tsx
frontend/styles.css
frontend/views/List.tsx

index 943db4aaf6acc77deefdb896059ee0e34013c402..e68a0da87aa09f806ea8a18dc2bcc725094aab3a 100644 (file)
@@ -256,6 +256,7 @@ CHTTPResponse* CListController::fetch(CHTTPRequest& request) {
                 i.rowid, i.uuid, i.created_at, i.updated_at, i.type, i.json,
                 0 as level,
                 COALESCE(json_extract(i.json, '$.order'), 0) as item_order,
+                printf('%010d', COALESCE(json_extract(i.json, '$.order'), 0)) as sort_path,
                 json_patch(
                     json_object(
                         '_uuid', i.uuid,
@@ -279,6 +280,7 @@ CHTTPResponse* CListController::fetch(CHTTPRequest& request) {
                 child.type, child.json,
                 parent.level + 1,
                 COALESCE(json_extract(child.json, '$.order'), 0) as item_order,
+                parent.sort_path || '.' || printf('%010d', COALESCE(json_extract(child.json, '$.order'), 0)) as sort_path,
                 json_patch(
                     json_object(
                         '_uuid', child.uuid,
@@ -302,29 +304,26 @@ CHTTPResponse* CListController::fetch(CHTTPRequest& request) {
                 t1.level,
                 t1.created_at,
                 t1.item_order,
+                t1.sort_path,
                 json_set(
                     t1.item_json,
                     '$._children',
                     COALESCE(
                         (SELECT json_group_array(json(t2.item_json))
                          FROM item_tree t2 
-                         WHERE EXISTS (
-                             SELECT 1 FROM itemslink il 
-                             WHERE il.parent_id = t1.rowid 
-                             AND il.child_id = t2.rowid 
-                             AND il.type = 'listitem_hierarchy'
-                         )
+                         JOIN itemslink il ON t2.rowid = il.child_id 
+                         WHERE il.parent_id = t1.rowid 
+                         AND il.type = 'listitem_hierarchy'
                          ORDER BY t2.item_order ASC),
                         json('[]')
                     )
                 ) as final_json
             FROM item_tree t1
-            ORDER BY t1.item_order ASC
         )
         SELECT json_group_array(json(final_json)) as result
         FROM hierarchy 
         WHERE level = 0 
-        ORDER BY item_order ASC
+        ORDER BY sort_path ASC
     )";
     
     CDBQuery query = db->query(sql);
index 72f8f838b5d45a3ed30909d1d01e4b20e7702d68..5b56b159d223ad8a589eaa27f7f4a1962a6af111 100644 (file)
@@ -24,6 +24,7 @@ interface AppContextType {
     setUser: (user: User) => void;
     checkAuth: () => void;
     fetchData: (url: string) => Promise<any>;
+    logout: () => void;
 }
 
 
@@ -113,6 +114,26 @@ export class AppContextProvider extends Component<{ children: any }> {
             .finally(() => {
                 this.state.setIsLoading(false);
             });
+        },
+        logout: () => {
+        // Add a cache-busting query parameter
+        const cacheBuster = `?cb=${new Date().getTime()}`;
+        fetch( `auth/logout${cacheBuster}`, { credentials: 'include' } )
+            .then(response => {
+                if (response.ok) {
+                    console.log('Logout successful, redirecting...');
+                    window.location.href = '/';
+                } else {
+                    console.error('Logout failed:', response);
+                    // Optionally, still redirect or show an error
+                    window.location.href = '/'; // Or handle error more gracefully
+                }
+            })
+            .catch(error => {
+                console.error('Error during logout fetch:', error);
+                // Optionally, still redirect or show an error
+                window.location.href = '/'; // Or handle error more gracefully
+            });
         }
     };
 
index da4432c2b3bc1b10a0e299ef053382a7df136d5d..70380e0fd3b7b17688e5dede1d62575dec32ecbc 100644 (file)
@@ -13,7 +13,7 @@ interface AppRoute {
 export const AppRoutes = [
     {
         path: '/',
-        view: Home,
+        view: List,
         auth_required: true,
     },
     {
index 480b678a9530f4b2109e5fa2fa39d49e6bdc3cb0..7c0a295f0855be92b8996cd597aa82f52b5bed2a 100644 (file)
@@ -127,4 +127,82 @@ export const SettingsIcon = ({ size = 24, color = 'currentColor', className = ''
     >
         <path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
     </svg>
+);
+
+export const IndentIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M3 21h18v-2H3v2zM3 8v8l4-4-4-4zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z" />
+    </svg>
+);
+
+export const OutdentIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M3 21h18v-2H3v2zM7 8v8l-4-4 4-4zm4 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z" />
+    </svg>
+);
+
+export const ImportantIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
+    </svg>
+);
+
+export const CheckIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
+    </svg>
+);
+
+export const MoveUpIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" />
+    </svg>
+);
+
+export const MoveDownIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => (
+    <svg 
+        viewBox="0 0 24 24" 
+        width={size} 
+        height={size} 
+        fill={color}
+        class={className}
+        style={{ verticalAlign: 'middle' }}
+    >
+        <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z" />
+    </svg>
 ); 
\ No newline at end of file
index 79a65eb49f95e9cacf9f4b165aeefca97103763d..acbfba85270f906d5085e02a519aea437bef0010 100644 (file)
-import { h } from 'preact';
+import { h, createContext } from 'preact';
 import { useContext, useEffect, useState } from 'preact/hooks';
 import { AppContext } from 'app/AppContext';
 import { ListItem, ItemProps } from 'components/ListItem';
 import { setCursorPosition, getCursorPosition } from '../util/util';
 
-export const ListContext = () => {
+interface ListContextValue {
+    focusedItem: ItemProps | null;
+    canPromote: boolean;
+    canDemote: boolean;
+    canMoveUp: boolean;
+    canMoveDown: boolean;
+    promoteItem: () => void;
+    demoteItem: () => void;
+    toggleImportantItem: () => void;
+    toggleCompletedItem: () => void;
+    moveUpItem: () => void;
+    moveDownItem: () => void;
+    items: ItemProps[];
+    handleItemChange: (item: ItemProps) => void;
+    handleMoveFocus: (currentItem: ItemProps, direction: 'up' | 'down', cursorPosition?: number) => void;
+    handlePromote: (currentItem: ItemProps) => void;
+    handleDemote: (currentItem: ItemProps) => void;
+    handleToggleImportant: (currentItem: ItemProps) => void;
+    handleToggleCompleted: (currentItem: ItemProps) => void;
+    handleCreateItem: (currentItem: ItemProps, beforeContent: string, afterContent: string) => void;
+    handleDeleteItem: (currentItem: ItemProps, remainingContent: string) => void;
+    handleItemFocus: (item: ItemProps) => void;
+    handleMoveUp: (currentItem: ItemProps) => void;
+    handleMoveDown: (currentItem: ItemProps) => void;
+}
+
+export const ListContextProvider = createContext<ListContextValue>({
+    focusedItem: null,
+    canPromote: false,
+    canDemote: false,
+    canMoveUp: false,
+    canMoveDown: false,
+    promoteItem: () => {},
+    demoteItem: () => {},
+    toggleImportantItem: () => {},
+    toggleCompletedItem: () => {},
+    moveUpItem: () => {},
+    moveDownItem: () => {},
+    items: [],
+    handleItemChange: () => {},
+    handleMoveFocus: () => {},
+    handlePromote: () => {},
+    handleDemote: () => {},
+    handleToggleImportant: () => {},
+    handleToggleCompleted: () => {},
+    handleCreateItem: () => {},
+    handleDeleteItem: () => {},
+    handleItemFocus: () => {},
+    handleMoveUp: () => {},
+    handleMoveDown: () => {},
+});
+
+// Hook to manage list state and operations
+export const useListState = () => {
     const [items, setItems] = useState<ItemProps[]>([]);
     const [focusedItemUuid, setFocusedItemUuid] = useState<string | null>(null);
     const [pendingFocusRestore, setPendingFocusRestore] = useState<{ uuid: string, cursorPosition: number } | null>(null);
     const [saveTimeout, setSaveTimeout] = useState<number | null>(null);
     const { fetchData } = useContext(AppContext);
 
-    // Helper function to ensure all items have proper order values
+    // Core tree utilities
+    const findItemInTree = (items: ItemProps[], uuid: string): ItemProps | null => {
+        for (const item of items) {
+            if (item._uuid === uuid) return item;
+            const found = findItemInTree(item._children, uuid);
+            if (found) return found;
+        }
+        return null;
+    };
+
+    const updateItemInTree = (items: ItemProps[], targetUuid: string, updater: (item: ItemProps) => ItemProps): ItemProps[] => {
+        return items.map(item => {
+            if (item._uuid === targetUuid) return updater(item);
+            return { ...item, _children: updateItemInTree(item._children, targetUuid, updater) };
+        });
+    };
+
+    // Reorder items and update order properties
+    const reorderItems = (items: ItemProps[]): ItemProps[] => {
+        return items.map((item, index) => ({
+            ...item,
+            order: index,
+            __is_dirty: true,
+            _children: reorderItems(item._children)
+        }));
+    };
+
+    // Find siblings and context for operations
+    const findItemContext = (items: ItemProps[], targetUuid: string): {
+        siblings: ItemProps[];
+        index: number;
+        parent: ItemProps | null;
+        actualSiblings: ItemProps[];
+    } | null => {
+        // Check root level
+        const rootIndex = items.findIndex(item => item._uuid === targetUuid);
+        if (rootIndex !== -1) {
+            return {
+                siblings: items.filter(item => !item.__is_deleted),
+                index: items.filter((item, i) => i < rootIndex && !item.__is_deleted).length,
+                parent: null,
+                actualSiblings: items
+            };
+        }
+
+        // Check nested levels
+        for (const item of items) {
+            const childIndex = item._children.findIndex(child => child._uuid === targetUuid);
+            if (childIndex !== -1) {
+                const visibleChildren = item._children.filter(child => !child.__is_deleted);
+                const visibleIndex = visibleChildren.findIndex(child => child._uuid === targetUuid);
+                return {
+                    siblings: visibleChildren,
+                    index: visibleIndex,
+                    parent: item,
+                    actualSiblings: item._children
+                };
+            }
+            const result = findItemContext(item._children, targetUuid);
+            if (result) return result;
+        }
+        return null;
+    };
+
+    const focusedItem = focusedItemUuid ? findItemInTree(items, focusedItemUuid) : null;
+
+    // Capability checks
+    const canPromote = focusedItem ? !!findItemContext(items, focusedItem._uuid)?.parent : false;
+    const canDemote = focusedItem ? (findItemContext(items, focusedItem._uuid)?.index ?? 0) > 0 : false;
+    const canMoveUp = focusedItem ? (findItemContext(items, focusedItem._uuid)?.index ?? 0) > 0 : false;
+    const canMoveDown = (() => {
+        if (!focusedItem) return false;
+        const context = findItemContext(items, focusedItem._uuid);
+        return context ? context.index < context.siblings.length - 1 : false;
+    })();
+
+    // Toolbar actions
+    const promoteItem = () => focusedItem && handlePromote(focusedItem);
+    const demoteItem = () => focusedItem && handleDemote(focusedItem);
+    const toggleImportantItem = () => focusedItem && handleToggleImportant(focusedItem);
+    const toggleCompletedItem = () => focusedItem && handleToggleCompleted(focusedItem);
+    const moveUpItem = () => focusedItem && handleMoveUp(focusedItem);
+    const moveDownItem = () => focusedItem && handleMoveDown(focusedItem);
+
+    const handleItemFocus = (item: ItemProps) => {
+        if (item._uuid !== focusedItemUuid) {
+            setFocusedItemUuid(item._uuid);
+        }
+    };
+
+    // Schedule focus restoration with cursor position
+    const scheduleRestore = (uuid: string, cursorPosition: number) => {
+        setPendingFocusRestore({ uuid, cursorPosition });
+    };
+
+    // Get cursor position from active element
+    const getCurrentCursorPosition = (itemUuid: string): number => {
+        const activeElement = document.activeElement as HTMLElement;
+        return (activeElement?.getAttribute('data-uuid') === itemUuid) ? getCursorPosition(activeElement) : 0;
+    };
+
+    // Initialize items with proper order values
     const ensureItemOrders = (items: ItemProps[]): ItemProps[] => {
         return items.map((item, index) => ({
             ...item,
@@ -23,45 +177,38 @@ export const ListContext = () => {
     const fetchItems = async () => {
         try {
             const response = await fetchData('/list/fetch') as { data: ItemProps[] };
-            if (response?.data && response.data.length > 0) {
-                // Ensure all items have proper order values
+            if (response?.data?.length > 0) {
                 const itemsWithOrders = ensureItemOrders(response.data);
                 setItems(itemsWithOrders);
-                if (itemsWithOrders.length > 0) {
-                    setFocusedItemUuid(itemsWithOrders[0]._uuid);
-                }
-            }
-            else {
-                //Set default empty item
-                setItems([{
+                setFocusedItemUuid(itemsWithOrders[0]._uuid);
+            } else {
+                // Create default empty item
+                const defaultItem: ItemProps = {
                     _uuid: crypto.randomUUID(),
                     _type: 'item',
                     _created_at: new Date().toISOString(),
                     _updated_at: new Date().toISOString(),
                     _children: [],
-
                     content: '',
                     level: 0,
                     important: false,
                     completed_at: null,
                     order: 0
-                }]);
+                };
+                setItems([defaultItem]);
+                setFocusedItemUuid(defaultItem._uuid);
             }
         } catch (error) {
             console.error('Failed to fetch items:', error);
         }
     };
 
-    // Helper function to flatten the tree structure into an array with parentUuid references
-    // Only includes items that are dirty (need to be saved)
+    // Auto-save functionality
     const flattenItemsForSave = (items: ItemProps[], parentUuid?: string): any[] => {
         const result: any[] = [];
-        
         for (const item of items) {
-            // Only include dirty items
             if (item.__is_dirty) {
-                // Convert item to backend format
-                const flatItem = {
+                result.push({
                     uuid: item._uuid,
                     content: item.content,
                     level: item.level,
@@ -71,81 +218,42 @@ export const ListContext = () => {
                     parentUuid: parentUuid || '',
                     isNew: item.__is_new || false,
                     isDeleted: item.__is_deleted || false
-                };
-                
-                result.push(flatItem);
+                });
             }
-            
-            // Always recursively check children, regardless of parent dirty status
-            if (item._children && item._children.length > 0) {
+            if (item._children?.length > 0) {
                 result.push(...flattenItemsForSave(item._children, item._uuid));
             }
         }
-        
         return result;
     };
 
-    // Helper function to clear dirty flags for saved items
     const clearDirtyFlags = (items: ItemProps[], savedUuids: Set<string>): ItemProps[] => {
-        return items.filter(item => {
-            // Remove items that were saved as deleted
-            if (item.__is_deleted && savedUuids.has(item._uuid)) {
-                return false;
-            }
-            return true;
-        }).map(item => {
-            const updatedItem = { ...item };
-            
-            // Clear dirty flags if this item was saved
-            if (savedUuids.has(item._uuid)) {
-                updatedItem.__is_dirty = false;
-                updatedItem.__is_new = false;
-            }
-            
-            // Recursively process children
-            if (item._children && item._children.length > 0) {
-                updatedItem._children = clearDirtyFlags(item._children, savedUuids);
-            }
-            
-            return updatedItem;
-        });
+        return items.filter(item => !(item.__is_deleted && savedUuids.has(item._uuid)))
+            .map(item => ({
+                ...item,
+                __is_dirty: savedUuids.has(item._uuid) ? false : item.__is_dirty,
+                __is_new: savedUuids.has(item._uuid) ? false : item.__is_new,
+                _children: clearDirtyFlags(item._children, savedUuids)
+            }));
     };
 
-    // Save function with debouncing
     const scheduleSave = () => {
-        // Clear existing timeout
-        if (saveTimeout) {
-            clearTimeout(saveTimeout);
-        }
+        if (saveTimeout) clearTimeout(saveTimeout);
         
-        // Set new timeout for 1 second
         const newTimeout = setTimeout(async () => {
             try {
                 const flatItems = flattenItemsForSave(items);
-                
-                // Skip save if no dirty items
-                if (flatItems.length === 0) {
-                    console.log('No dirty items to save');
-                    return;
-                }
+                if (flatItems.length === 0) return;
                 
                 const response = await fetch('/list/save', {
                     method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json',
-                    },
+                    headers: { 'Content-Type': 'application/json' },
                     credentials: 'include',
                     body: JSON.stringify(flatItems)
                 });
                 
-                if (!response.ok) {
-                    throw new Error(`Save failed: ${response.status} ${response.statusText}`);
-                }
+                if (!response.ok) throw new Error(`Save failed: ${response.status}`);
                 
-                const result = await response.json();
-                console.log('List saved successfully:', result);
-
-                // Clear dirty flags for saved items
                 const savedUuids = new Set(flatItems.map(item => item.uuid));
                 setItems(prevItems => clearDirtyFlags(prevItems, savedUuids));
             } catch (error) {
@@ -156,12 +264,13 @@ export const ListContext = () => {
         setSaveTimeout(newTimeout);
     };
 
+    // Basic operations
     const handleItemChange = (item: ItemProps) => {
         setItems(prevItems => updateItemInTree(prevItems, item._uuid, () => item));
     };
 
     const handleToggleImportant = (currentItem: ItemProps) => {
-        setItems(prevItems => updateItemInTree(prevItems, currentItem._uuid, (item) => ({
+        setItems(prevItems => updateItemInTree(prevItems, currentItem._uuid, item => ({
             ...item,
             important: !item.important,
             __is_dirty: true
@@ -170,32 +279,22 @@ export const ListContext = () => {
 
     const handleToggleCompleted = (currentItem: ItemProps) => {
         const newCompletedAt = currentItem.completed_at ? null : new Date().toISOString();
-        
-        // Recursive function to update completed status for item and all children
-        const updateCompletedRecursive = (item: ItemProps): ItemProps => ({
+        const updateCompleted = (item: ItemProps): ItemProps => ({
             ...item,
             completed_at: newCompletedAt,
             __is_dirty: true,
-            _children: item._children.map(updateCompletedRecursive)
+            _children: item._children.map(updateCompleted)
         });
-
-        setItems(prevItems => updateItemInTree(prevItems, currentItem._uuid, updateCompletedRecursive));
+        setItems(prevItems => updateItemInTree(prevItems, currentItem._uuid, updateCompleted));
     };
 
     const handleCreateItem = (currentItem: ItemProps, beforeContent: string, afterContent: string) => {
-        // Generate a new UUID for the new item
         const newUuid = crypto.randomUUID();
-        const now = new Date().toISOString();
-        
-        // Calculate the order for the new item (next order after current item)
-        const newOrder = currentItem.order + 1;
-        
-        // Create the new item
         const newItem: ItemProps = {
             _uuid: newUuid,
             _type: 'item',
-            _created_at: now,
-            _updated_at: now,
+            _created_at: new Date().toISOString(),
+            _updated_at: new Date().toISOString(),
             _children: [],
             __is_new: true,
             __is_dirty: true,
@@ -203,31 +302,26 @@ export const ListContext = () => {
             level: currentItem.level,
             important: false,
             completed_at: null,
-            order: newOrder
+            order: currentItem.order + 1
         };
 
-        // Update the current item with the before content
-        const updatedCurrentItem = { ...currentItem, content: beforeContent, __is_dirty: true };
-
         setItems(prevItems => {
-            // First update the current item
-            const itemsWithUpdatedCurrent = updateItemInTree(prevItems, currentItem._uuid, () => updatedCurrentItem);
-            
-            // Then insert the new item after the current item and reorder siblings
-            const insertAfterItemWithReorder = (items: ItemProps[], targetUuid: string, itemToInsert: ItemProps): ItemProps[] => {
+            // Update current item
+            const updatedItems = updateItemInTree(prevItems, currentItem._uuid, item => ({
+                ...item,
+                content: beforeContent,
+                __is_dirty: true
+            }));
+
+            // Insert new item and reorder
+            const insertItem = (items: ItemProps[], targetUuid: string): ItemProps[] => {
                 for (let i = 0; i < items.length; i++) {
                     if (items[i]._uuid === targetUuid) {
-                        // Insert the new item after current item
-                        const newItems = [...items.slice(0, i + 1), itemToInsert, ...items.slice(i + 1)];
-                        // Reorder all items after the insertion point
-                        return newItems.map((item, index) => ({
-                            ...item,
-                            order: index,
-                            __is_dirty: index > i ? true : item.__is_dirty
-                        }));
+                        const newItems = [...items.slice(0, i + 1), newItem, ...items.slice(i + 1)];
+                        return reorderItems(newItems);
                     }
                     if (items[i]._children.length > 0) {
-                        const result = insertAfterItemWithReorder(items[i]._children, targetUuid, itemToInsert);
+                        const result = insertItem(items[i]._children, targetUuid);
                         if (result !== items[i]._children) {
                             return items.map((item, index) => 
                                 index === i ? { ...item, _children: result } : item
@@ -238,214 +332,125 @@ export const ListContext = () => {
                 return items;
             };
 
-            const newItems = insertAfterItemWithReorder(itemsWithUpdatedCurrent, currentItem._uuid, newItem);
-            
-            return newItems;
+            return insertItem(updatedItems, currentItem._uuid);
         });
 
-        // Schedule focus on the new item
+        // Focus new item
         setTimeout(() => {
-            const newElement = document.querySelector(`[data-uuid="${newUuid}"]`) as HTMLElement;
-            if (newElement) {
-                newElement.focus();
-                setCursorPosition(newElement, 0);
+            const element = document.querySelector(`[data-uuid="${newUuid}"]`) as HTMLElement;
+            if (element) {
+                element.focus();
+                setCursorPosition(element, 0);
             }
         }, 0);
     };
 
     const handleDeleteItem = (currentItem: ItemProps, remainingContent: string) => {
-        // Find the previous logical item in the tree
         const findPreviousItem = (items: ItemProps[], targetUuid: string): ItemProps | null => {
             let previousItem: ItemProps | null = null;
-            
             const traverse = (itemList: ItemProps[]): ItemProps | null => {
                 for (const item of itemList) {
-                    // Skip deleted items when finding previous item
-                    if (item.__is_deleted) {
-                        continue;
-                    }
-                    if (item._uuid === targetUuid) {
-                        return previousItem;
-                    }
+                    if (item.__is_deleted) continue;
+                    if (item._uuid === targetUuid) return previousItem;
                     previousItem = item;
-                    
-                    // Check children
                     const result = traverse(item._children);
                     if (result) return result;
                 }
                 return null;
             };
-            
             return traverse(items);
         };
 
         setItems(prevItems => {
             const previousItem = findPreviousItem(prevItems, currentItem._uuid);
-            
-            if (!previousItem) {
-                // Can't delete if there's no previous item
-                return prevItems;
-            }
+            if (!previousItem) return prevItems;
 
-            // Update the previous item with the remaining content
-            const updatedPreviousItem = { 
-                ...previousItem, 
-                content: previousItem.content + remainingContent,
+            // Update previous item and mark current as deleted
+            let result = updateItemInTree(prevItems, previousItem._uuid, item => ({
+                ...item,
+                content: item.content + remainingContent,
                 __is_dirty: true
-            };
+            }));
 
-            // Mark the current item as deleted instead of removing it
-            const itemsWithDeletedItem = updateItemInTree(prevItems, currentItem._uuid, (item) => ({
+            result = updateItemInTree(result, currentItem._uuid, item => ({
                 ...item,
                 __is_deleted: true,
                 __is_dirty: true
             }));
 
-            // Update the previous item
-            const finalItems = updateItemInTree(itemsWithDeletedItem, previousItem._uuid, () => updatedPreviousItem);
-
-            // Schedule focus on the previous item
+            // Focus previous item
             setTimeout(() => {
-                const prevElement = document.querySelector(`[data-uuid="${previousItem._uuid}"]`) as HTMLElement;
-                if (prevElement) {
-                    prevElement.focus();
-                    setCursorPosition(prevElement, previousItem.content.length);
+                const element = document.querySelector(`[data-uuid="${previousItem._uuid}"]`) as HTMLElement;
+                if (element) {
+                    element.focus();
+                    setCursorPosition(element, previousItem.content.length);
                 }
             }, 0);
 
-            return finalItems;
+            return result;
         });
     };
 
-    const handleMoveFocus = (currentItem: ItemProps, direction: 'up' | 'down') => {
-        const currentIndex = items.findIndex(item => item._uuid === currentItem._uuid);
-        if (currentIndex === -1) return;
-
-        let nextIndex;
-        if (direction === 'down') {
-            nextIndex = Math.min(items.length - 1, currentIndex + 1);
-        } else {
-            nextIndex = Math.max(0, currentIndex - 1);
-        }
-
-        if (items[nextIndex]) {
-            setFocusedItemUuid(items[nextIndex]._uuid);
-        }
-    };
-
-    // Helper function to find an item in the nested structure
-    const findItemInTree = (items: ItemProps[], uuid: string): ItemProps | null => {
-        for (const item of items) {
-            if (item._uuid === uuid) {
-                return item;
-            }
-            const found = findItemInTree(item._children, uuid);
-            if (found) {
-                return found;
+    const handleMoveFocus = (currentItem: ItemProps, direction: 'up' | 'down', cursorPosition?: number) => {
+        const flattenVisible = (items: ItemProps[]): ItemProps[] => {
+            const result: ItemProps[] = [];
+            for (const item of items) {
+                if (!item.__is_deleted) {
+                    result.push(item);
+                    result.push(...flattenVisible(item._children));
+                }
             }
-        }
-        return null;
-    };
+            return result;
+        };
 
-    // Helper function to update an item in the nested structure
-    const updateItemInTree = (items: ItemProps[], targetUuid: string, updater: (item: ItemProps) => ItemProps): ItemProps[] => {
-        return items.map(item => {
-            if (item._uuid === targetUuid) {
-                return updater(item);
-            }
-            return {
-                ...item,
-                _children: updateItemInTree(item._children, targetUuid, updater)
-            };
-        });
-    };
+        const flatItems = flattenVisible(items);
+        const currentIndex = flatItems.findIndex(item => item._uuid === currentItem._uuid);
+        if (currentIndex === -1) return;
 
-    // Helper function to remove an item from the nested structure
-    const removeItemFromTree = (items: ItemProps[], targetUuid: string): { items: ItemProps[], removedItem: ItemProps | null } => {
-        for (let i = 0; i < items.length; i++) {
-            if (items[i]._uuid === targetUuid) {
-                const removedItem = items[i];
-                return {
-                    items: [...items.slice(0, i), ...items.slice(i + 1)],
-                    removedItem
-                };
-            }
-            const result = removeItemFromTree(items[i]._children, targetUuid);
-            if (result.removedItem) {
-                return {
-                    items: items.map((item, index) => 
-                        index === i ? { ...item, _children: result.items } : item
-                    ),
-                    removedItem: result.removedItem
-                };
-            }
+        const nextIndex = direction === 'down' 
+            ? Math.min(flatItems.length - 1, currentIndex + 1)
+            : Math.max(0, currentIndex - 1);
+
+        if (flatItems[nextIndex]?._uuid !== currentItem._uuid) {
+            setFocusedItemUuid(flatItems[nextIndex]._uuid);
+            setTimeout(() => {
+                const element = document.querySelector(`[data-uuid="${flatItems[nextIndex]._uuid}"]`) as HTMLElement;
+                if (element) {
+                    element.focus();
+                    setCursorPosition(element, cursorPosition || 0);
+                }
+            }, 0);
         }
-        return { items, removedItem: null };
     };
 
+    // Promote/Demote operations
     const handlePromote = (currentItem: ItemProps) => {
-        // Store cursor position before the operation
-        const activeElement = document.activeElement as HTMLElement;
-        let cursorPosition = 0;
-        if (activeElement && activeElement.getAttribute('data-uuid') === currentItem._uuid) {
-            cursorPosition = getCursorPosition(activeElement);
-        }
-
-        // Promote (outdent) - move item up one level
+        const cursorPosition = getCurrentCursorPosition(currentItem._uuid);
+        
         setItems(prevItems => {
-            // Find the parent of the current item
-            const findParent = (items: ItemProps[], targetUuid: string): ItemProps | null => {
-                for (const item of items) {
-                    if (item._children.some(child => child._uuid === targetUuid)) {
-                        return item;
-                    }
-                    const found = findParent(item._children, targetUuid);
-                    if (found) return found;
-                }
-                return null;
-            };
-
-            const parent = findParent(prevItems, currentItem._uuid);
-            if (!parent) {
-                // Already at root level, can't promote further
-                return prevItems;
-            }
+            const context = findItemContext(prevItems, currentItem._uuid);
+            if (!context?.parent) return prevItems;
 
-            // Remove item from its current parent and reorder remaining siblings
-            const { items: itemsAfterRemoval, removedItem } = removeItemFromTree(prevItems, currentItem._uuid);
-            if (!removedItem) return prevItems;
-
-            // Reorder the children of the former parent
-            const itemsWithReorderedSiblings = updateItemInTree(itemsAfterRemoval, parent._uuid, (parentItem) => ({
-                ...parentItem,
-                _children: parentItem._children.map((child, index) => ({
-                    ...child,
-                    order: index,
-                    __is_dirty: true
-                })),
+            // Remove item from current location
+            let result = updateItemInTree(prevItems, context.parent._uuid, parent => ({
+                ...parent,
+                _children: reorderItems(parent._children.filter(child => child._uuid !== currentItem._uuid)),
                 __is_dirty: true
             }));
 
-            // Update the item's level
-            const promotedItem = { ...removedItem, level: Math.max(0, removedItem.level - 1) };
-
-            // Find where to insert the promoted item (after its former parent) and reorder siblings
-            const insertAfterParentWithReorder = (items: ItemProps[], parentUuid: string, itemToInsert: ItemProps): ItemProps[] => {
+            // Insert after parent with updated level
+            const promotedItem = { ...currentItem, level: Math.max(0, currentItem.level - 1), __is_dirty: true };
+            const insertAfterParent = (items: ItemProps[], parentUuid: string): ItemProps[] => {
                 for (let i = 0; i < items.length; i++) {
                     if (items[i]._uuid === parentUuid) {
-                        const newItems = [...items.slice(0, i + 1), itemToInsert, ...items.slice(i + 1)];
-                        // Reorder all items after the insertion point
-                        return newItems.map((item, index) => ({
-                            ...item,
-                            order: index,
-                            __is_dirty: index > i ? true : item.__is_dirty
-                        }));
+                        const newItems = [...items.slice(0, i + 1), promotedItem, ...items.slice(i + 1)];
+                        return reorderItems(newItems);
                     }
                     if (items[i]._children.length > 0) {
-                        const result = insertAfterParentWithReorder(items[i]._children, parentUuid, itemToInsert);
-                        if (result !== items[i]._children) {
+                        const children = insertAfterParent(items[i]._children, parentUuid);
+                        if (children !== items[i]._children) {
                             return items.map((item, index) => 
-                                index === i ? { ...item, _children: result } : item
+                                index === i ? { ...item, _children: children } : item
                             );
                         }
                     }
@@ -453,127 +458,114 @@ export const ListContext = () => {
                 return items;
             };
 
-            const newItems = insertAfterParentWithReorder(itemsWithReorderedSiblings, parent._uuid, promotedItem);
-            
-            // Schedule focus restoration
-            setPendingFocusRestore({ uuid: currentItem._uuid, cursorPosition });
-            
-            return newItems;
+            result = insertAfterParent(result, context.parent._uuid);
+            scheduleRestore(currentItem._uuid, cursorPosition);
+            return result;
         });
     };
 
     const handleDemote = (currentItem: ItemProps) => {
-        // Store cursor position before the operation
-        const activeElement = document.activeElement as HTMLElement;
-        let cursorPosition = 0;
-        if (activeElement && activeElement.getAttribute('data-uuid') === currentItem._uuid) {
-            cursorPosition = getCursorPosition(activeElement);
-        }
-
-        // Demote (indent) - move item down one level by making it a child of the previous sibling
+        const cursorPosition = getCurrentCursorPosition(currentItem._uuid);
+        
         setItems(prevItems => {
-            // Find the current item and its siblings
-            const findSiblingsAndIndex = (items: ItemProps[], targetUuid: string): { siblings: ItemProps[], index: number, parent: ItemProps | null } | null => {
-                // Check root level
-                const rootIndex = items.findIndex(item => item._uuid === targetUuid);
-                if (rootIndex !== -1) {
-                    return { siblings: items, index: rootIndex, parent: null };
-                }
-
-                // Check nested levels
-                for (const item of items) {
-                    const childIndex = item._children.findIndex(child => child._uuid === targetUuid);
-                    if (childIndex !== -1) {
-                        return { siblings: item._children, index: childIndex, parent: item };
-                    }
-                    const result = findSiblingsAndIndex(item._children, targetUuid);
-                    if (result) return result;
-                }
-                return null;
-            };
+            const context = findItemContext(prevItems, currentItem._uuid);
+            if (!context || context.index === 0) return prevItems;
 
-            const siblingInfo = findSiblingsAndIndex(prevItems, currentItem._uuid);
-            if (!siblingInfo || siblingInfo.index === 0) {
-                // Can't demote if it's the first item or not found
-                return prevItems;
-            }
-
-            const { siblings, index, parent } = siblingInfo;
-            const previousSibling = siblings[index - 1];
-
-            // Remove item from current position
-            const { items: itemsAfterRemoval, removedItem } = removeItemFromTree(prevItems, currentItem._uuid);
-            if (!removedItem) return prevItems;
-
-            // Reorder remaining siblings where the item was removed
-            let itemsWithReorderedSiblings = itemsAfterRemoval;
-            if (parent) {
-                itemsWithReorderedSiblings = updateItemInTree(itemsAfterRemoval, parent._uuid, (parentItem) => ({
-                    ...parentItem,
-                    _children: parentItem._children.map((child, childIndex) => ({
-                        ...child,
-                        order: childIndex,
-                        __is_dirty: true
-                    })),
-                    __is_dirty: true
-                }));
-            } else {
-                // Root level reordering
-                itemsWithReorderedSiblings = itemsAfterRemoval.map((item, itemIndex) => ({
-                    ...item,
-                    order: itemIndex,
+            const previousSibling = context.siblings[context.index - 1];
+            
+            // Remove from current location and reorder
+            let result = context.parent 
+                ? updateItemInTree(prevItems, context.parent._uuid, parent => ({
+                    ...parent,
+                    _children: reorderItems(parent._children.filter(child => child._uuid !== currentItem._uuid)),
                     __is_dirty: true
-                }));
-            }
+                }))
+                : reorderItems(prevItems.filter(item => item._uuid !== currentItem._uuid));
 
-            // Update the item's level and order (will be last child)
+            // Add as child of previous sibling
             const demotedItem = { 
-                ...removedItem, 
-                level: removedItem.level + 1,
-                order: -1, // Will be set correctly when adding to children
+                ...currentItem, 
+                level: currentItem.level + 1,
                 __is_dirty: true
             };
 
-            // Add item as a child of the previous sibling
-            const newItems = updateItemInTree(itemsWithReorderedSiblings, previousSibling._uuid, (sibling) => {
-                const newChildren = [...sibling._children, demotedItem];
-                return {
-                    ...sibling,
-                    _children: newChildren.map((child, childIndex) => ({
-                        ...child,
-                        order: childIndex,
-                        __is_dirty: true
-                    })),
-                    __is_dirty: true
-                };
-            });
-            
-            // Schedule focus restoration
-            setPendingFocusRestore({ uuid: currentItem._uuid, cursorPosition });
-            
-            return newItems;
+            result = updateItemInTree(result, previousSibling._uuid, sibling => ({
+                ...sibling,
+                _children: reorderItems([...sibling._children, demotedItem]),
+                __is_dirty: true
+            }));
+
+            scheduleRestore(currentItem._uuid, cursorPosition);
+            return result;
+        });
+    };
+
+    // Move operations (swap with adjacent sibling)
+    const handleMoveUp = (currentItem: ItemProps) => {
+        const cursorPosition = getCurrentCursorPosition(currentItem._uuid);
+        
+        setItems(prevItems => {
+            const context = findItemContext(prevItems, currentItem._uuid);
+            if (!context || context.index === 0) return prevItems;
+
+            const { actualSiblings, parent } = context;
+            const currentActualIndex = actualSiblings.findIndex(item => item._uuid === currentItem._uuid);
+            const previousActualIndex = actualSiblings.findIndex(item => item._uuid === context.siblings[context.index - 1]._uuid);
+
+            const swapItems = (items: ItemProps[]): ItemProps[] => {
+                const newItems = [...items];
+                [newItems[currentActualIndex], newItems[previousActualIndex]] = 
+                [newItems[previousActualIndex], newItems[currentActualIndex]];
+                return reorderItems(newItems);
+            };
+
+            const result = parent 
+                ? updateItemInTree(prevItems, parent._uuid, p => ({ ...p, _children: swapItems(p._children), __is_dirty: true }))
+                : swapItems(prevItems);
+
+            scheduleRestore(currentItem._uuid, cursorPosition);
+            return result;
+        });
+    };
+
+    const handleMoveDown = (currentItem: ItemProps) => {
+        const cursorPosition = getCurrentCursorPosition(currentItem._uuid);
+        
+        setItems(prevItems => {
+            const context = findItemContext(prevItems, currentItem._uuid);
+            if (!context || context.index === context.siblings.length - 1) return prevItems;
+
+            const { actualSiblings, parent } = context;
+            const currentActualIndex = actualSiblings.findIndex(item => item._uuid === currentItem._uuid);
+            const nextActualIndex = actualSiblings.findIndex(item => item._uuid === context.siblings[context.index + 1]._uuid);
+
+            const swapItems = (items: ItemProps[]): ItemProps[] => {
+                const newItems = [...items];
+                [newItems[currentActualIndex], newItems[nextActualIndex]] = 
+                [newItems[nextActualIndex], newItems[currentActualIndex]];
+                return reorderItems(newItems);
+            };
+
+            const result = parent 
+                ? updateItemInTree(prevItems, parent._uuid, p => ({ ...p, _children: swapItems(p._children), __is_dirty: true }))
+                : swapItems(prevItems);
+
+            scheduleRestore(currentItem._uuid, cursorPosition);
+            return result;
         });
     };
 
-    // Effect to handle focus restoration after promote/demote
+    // Effects
     useEffect(() => {
         if (pendingFocusRestore) {
             const { uuid, cursorPosition } = pendingFocusRestore;
-            // Find the element by uuid and restore focus
-            const findAndFocusElement = () => {
-                const elements = document.querySelectorAll('[data-uuid]');
-                for (const element of elements) {
-                    if (element.getAttribute('data-uuid') === uuid) {
-                        const contentEditable = element as HTMLElement;
-                        contentEditable.focus();
-                        setCursorPosition(contentEditable, cursorPosition);
-                        break;
-                    }
+            setTimeout(() => {
+                const element = document.querySelector(`[data-uuid="${uuid}"]`) as HTMLElement;
+                if (element) {
+                    element.focus();
+                    setCursorPosition(element, cursorPosition);
                 }
-            };
-            
-            // Use setTimeout to ensure the DOM has been updated
-            setTimeout(findAndFocusElement, 0);
+            }, 0);
             setPendingFocusRestore(null);
         }
     }, [pendingFocusRestore, items]);
@@ -581,54 +573,25 @@ export const ListContext = () => {
     useEffect(() => {
         fetchItems();
     }, []);
-
+    
     useEffect(() => {
-        // Cleanup timeout on unmount
         return () => {
-            if (saveTimeout) {
-                clearTimeout(saveTimeout);
-            }
+            if (saveTimeout) clearTimeout(saveTimeout);
         };
     }, [saveTimeout]);
-
-    // Effect to auto-save when dirty items are detected
+    
     useEffect(() => {
-        // Check if there are any dirty items in the tree
         const hasDirtyItems = (items: ItemProps[]): boolean => {
-            for (const item of items) {
-                if (item.__is_dirty) {
-                    return true;
-                }
-                if (item._children && item._children.length > 0 && hasDirtyItems(item._children)) {
-                    return true;
-                }
-            }
-            return false;
+            return items.some(item => item.__is_dirty || (item._children?.length > 0 && hasDirtyItems(item._children)));
         };
-
-        if (hasDirtyItems(items)) {
-            scheduleSave();
-        }
+        if (hasDirtyItems(items)) scheduleSave();
     }, [items]);
 
-    return (
-        <ul>
-            {items.filter(item => !item.__is_deleted).map(item => (
-                <ListItem 
-                    key={item._uuid} 
-                    item={item} 
-                    onChange={handleItemChange} 
-                    moveFocus={handleMoveFocus}
-                    promote={handlePromote}
-                    demote={handleDemote}
-                    toggleImportant={handleToggleImportant}
-                    toggleCompleted={handleToggleCompleted}
-                    createItem={handleCreateItem}
-                    deleteItem={handleDeleteItem}
-                />
-            ))}
-        </ul>
-    );
-};
-
-export default ListContext;
\ No newline at end of file
+    return {
+        items, setItems, focusedItem, canPromote, canDemote, canMoveUp, canMoveDown,
+        promoteItem, demoteItem, toggleImportantItem, toggleCompletedItem, moveUpItem, moveDownItem,
+        handleItemFocus, handleItemChange, handleMoveFocus, handlePromote, handleDemote,
+        handleToggleImportant, handleToggleCompleted, handleCreateItem, handleDeleteItem,
+        handleMoveUp, handleMoveDown,
+    };
+};
\ No newline at end of file
index 97c7ae137f12eb36918f54168af3284787741d4c..46852a2b32c3cf65ac7e968fa481b2fae758eeec 100644 (file)
@@ -31,13 +31,16 @@ interface ListItemProps {
     toggleCompleted: (currentItem: ItemProps) => void;
     createItem: (currentItem: ItemProps, beforeContent: string, afterContent: string) => void;
     deleteItem: (currentItem: ItemProps, remainingContent: string) => void;
+    onFocus?: (item: ItemProps) => void;
+    moveUp: (currentItem: ItemProps) => void;
+    moveDown: (currentItem: ItemProps) => void;
 };
 
 interface ListItemRef {
     focus: (focusLastChild: boolean, cursorPosition: number) => void;
 }
 
-export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange, moveFocus, promote, demote, toggleImportant, toggleCompleted, createItem, deleteItem }: ListItemProps, ref: Ref<ListItemRef>) => {
+export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange, moveFocus, promote, demote, toggleImportant, toggleCompleted, createItem, deleteItem, onFocus, moveUp, moveDown }: ListItemProps, ref: Ref<ListItemRef>) => {
     const localRef = useRef<HTMLDivElement>(null);
     const childRefs = useRef<(ListItemRef | null)[]>([]);
     const [targetFocusInfo, setTargetFocusInfo] = useState<{ index: number, timestamp: number, focusLastChild: boolean, cursorPosition?: number } | null>(null);
@@ -88,32 +91,9 @@ export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange
         }
     };
 
-    const handleMoveFocus = (currentItem: ItemProps, direction: 'up' | 'down', cursorPosition?: number) => {
-        const visibleChildren = item._children.filter(child => !child.__is_deleted);
-        const currentIndex = visibleChildren.findIndex((child: ItemProps) => child._uuid === currentItem._uuid);
-
-        let nextIndex: number | undefined;
-        if (direction === 'down') {
-            if (currentIndex === visibleChildren.length - 1) {
-                moveFocus(item, 'down', cursorPosition || 0);
-            } else {
-                nextIndex = currentIndex + 1;
-            }
-        } else {
-            if (currentIndex === 0) {
-                if( localRef.current ) {
-                    localRef.current.focus();
-                    setCursorPosition(localRef.current, cursorPosition || 0);
-                }
-            } else {
-                nextIndex = currentIndex - 1;
-            }
-        }
-        if (nextIndex !== undefined && visibleChildren[nextIndex]) {
-            // Find the actual index in the full children array for the target focus info
-            const targetChild = visibleChildren[nextIndex];
-            const actualIndex = item._children.findIndex(child => child._uuid === targetChild._uuid);
-            setTargetFocusInfo({ index: actualIndex, timestamp: Date.now(), focusLastChild: direction === 'up', cursorPosition: cursorPosition });
+    const handleFocus = (event: FocusEvent) => {
+        if (onFocus) {
+            onFocus(item);
         }
     };
 
@@ -157,17 +137,17 @@ export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange
                 
                 deleteItem(item, remainingContent);
             }
+        } else if (event.key === 'ArrowUp' && event.ctrlKey) {
+            // Ctrl+Up - move item up in order
+            event.preventDefault();
+            moveUp(item);
+        } else if (event.key === 'ArrowDown' && event.ctrlKey) {
+            // Ctrl+Down - move item down in order
+            event.preventDefault();
+            moveDown(item);
         } else if (event.key === 'ArrowDown') {
             event.preventDefault();
-            const visibleChildren = item._children.filter(child => !child.__is_deleted);
-            if( visibleChildren.length > 0 ) {
-                // Find the actual index of the first visible child
-                const firstVisibleChild = visibleChildren[0];
-                const actualIndex = item._children.findIndex(child => child._uuid === firstVisibleChild._uuid);
-                setTargetFocusInfo({ index: actualIndex, timestamp: Date.now(), focusLastChild: false, cursorPosition: getCursorPosition(localRef.current) });
-            } else {
-                moveFocus(item, 'down', getCursorPosition(localRef.current));
-            }
+            moveFocus(item, 'down', getCursorPosition(localRef.current));
         } else if (event.key === 'ArrowUp') {
             event.preventDefault();
             moveFocus(item, 'up', getCursorPosition(localRef.current));
@@ -196,6 +176,7 @@ export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange
                 ref={localRef}
                 contentEditable={true}
                 onInput={handleInput}
+                onFocus={handleFocus}
                 onKeyDown={handleKeyDown}
                 data-uuid={item._uuid}
                 className={`list-item-level-${Math.min(item.level, 9)} ${item.important ? 'list-item-important' : ''} ${item.completed_at ? 'list-item-completed' : ''}`}
@@ -207,13 +188,16 @@ export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange
                             key={child._uuid}
                             item={child}
                             onChange={onChange}
-                            moveFocus={handleMoveFocus}
+                            moveFocus={moveFocus}
                             promote={promote}
                             demote={demote}
                             toggleImportant={toggleImportant}
                             toggleCompleted={toggleCompleted}
                             createItem={createItem}
                             deleteItem={deleteItem}
+                            onFocus={onFocus}
+                            moveUp={moveUp}
+                            moveDown={moveDown}
                             ref={(el: ListItemRef | null) => {
                                 if (childRefs.current && index < childRefs.current.length) {
                                      childRefs.current[index] = el;
index 8d2764b3249be3758df04ada785de828fb4a9ddf..6151803a06e873fe6d13a8d85c983d77fab9963c 100644 (file)
@@ -1,33 +1,69 @@
-import { h } from 'preact';
-import { LogoutIcon, MenuIcon, PersonIcon } from './Icons';
+import { h, JSX } from 'preact';
+import { useContext } from 'preact/hooks';
+import { AppContext } from 'app/AppContext';
+import LoadingSpinner from './LoadingSpinner';
+import { MenuIcon, CheckIcon } from './Icons';
 
-export const Toolbar = () => {
-    const logout = () => {
-        // Add a cache-busting query parameter
-        const cacheBuster = `?cb=${new Date().getTime()}`;
-        fetch( `auth/logout${cacheBuster}`, { credentials: 'include' } )
-            .then(response => {
-                if (response.ok) {
-                    console.log('Logout successful, redirecting...');
-                    window.location.href = '/';
-                } else {
-                    console.error('Logout failed:', response);
-                    // Optionally, still redirect or show an error
-                    window.location.href = '/'; // Or handle error more gracefully
-                }
-            })
-            .catch(error => {
-                console.error('Error during logout fetch:', error);
-                // Optionally, still redirect or show an error
-                window.location.href = '/'; // Or handle error more gracefully
-            });
+export interface ToolBarButton {
+    icon: JSX.Element;
+    onClick: () => void;
+    title: string;
+    disabled?: boolean;
+    active?: boolean;
+    separatorAfter?: boolean;
+}
+
+interface ToolbarProps {
+    buttons: ToolBarButton[];
+    onMenuClick?: () => void;
+}
+
+export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => {
+    const appContext = useContext(AppContext);
+
+    const handleMenuClick = () => {
+        if (onMenuClick) {
+            onMenuClick();
+        } else {
+            // Default menu behavior - could open a drawer, show options, etc.
+            console.log('Menu clicked');
+        }
     };
+
     return (
         <div class="toolbar">
-            <h1>Toolbar</h1>
-            <button><MenuIcon size={24} /></button>
-            <button><PersonIcon size={24} /></button>
-            <button onClick={logout}><LogoutIcon size={24} /></button>
+            <div class="toolbar-left">
+                {buttons.map((button, index) => (
+                    <div key={index} class="toolbar-button-container">
+                        <button 
+                            onClick={button.onClick}
+                            title={button.title}
+                            disabled={button.disabled}
+                            class={`toolbar-button ${button.active ? 'active' : ''} ${button.disabled ? 'disabled' : ''}`}
+                        >
+                            {button.icon}
+                        </button>
+                        {button.separatorAfter && (
+                            <div class="toolbar-separator" />
+                        )}
+                    </div>
+                ))}
+            </div>
+            
+            <div class="toolbar-right">
+                {appContext.isLoading ? (
+                    <LoadingSpinner />
+                ) : (
+                    <CheckIcon size={20} />
+                )}
+                <button 
+                    onClick={handleMenuClick}
+                    title="Menu"
+                    class="toolbar-button menu-button"
+                >
+                    <MenuIcon size={20} />
+                </button>
+            </div>
         </div>
     );
 };
index 5ccf96256c24b54e7695d1b5c7025cef31ae668f..9346a029784ed615ae433c4e2fb748b07388b4b9 100644 (file)
@@ -239,4 +239,95 @@ li > div[contenteditable] {
 .list-item-level-6 { margin-left: 144px; }
 .list-item-level-7 { margin-left: 168px; }
 .list-item-level-8 { margin-left: 192px; }
-.list-item-level-9 { margin-left: 216px; }
\ No newline at end of file
+.list-item-level-9 { margin-left: 216px; }
+
+.toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 16px;
+    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+    border-bottom: 1px solid #dee2e6;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1000;
+    height: 52px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.toolbar-left {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.toolbar-right {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.toolbar-button-container {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.toolbar-separator {
+    width: 1px;
+    height: 24px;
+    background: #dee2e6;
+    margin: 0 8px;
+}
+
+.toolbar-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    border: 1px solid transparent;
+    border-radius: 6px;
+    background: transparent;
+    color: #495057;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    padding: 0;
+    margin: 0;
+}
+
+.toolbar-button:hover:not(.disabled) {
+    background: rgba(0, 116, 255, 0.1);
+    border-color: rgba(0, 116, 255, 0.2);
+    color: #0074ff;
+}
+
+.toolbar-button:active:not(.disabled) {
+    transform: translateY(1px);
+    background: rgba(0, 116, 255, 0.15);
+}
+
+.toolbar-button.active {
+    background: #0074ff;
+    color: white;
+    border-color: #0056b3;
+}
+
+.toolbar-button.active:hover {
+    background: #0056b3;
+    color: white;
+}
+
+.toolbar-button.disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+    color: #6c757d;
+}
+
+.toolbar-button.disabled:hover {
+    background: transparent;
+    border-color: transparent;
+    color: #6c757d;
+}
\ No newline at end of file
index 9df8c0a5598380ee516beb2fabeef45181ad951c..b7704d24caec2908068f5ba542e22e342e2e00f2 100644 (file)
 import { h } from 'preact';
-import { ListContext } from 'components/ListContext';
+import { useContext } from 'preact/hooks';
+import { ListContextProvider, useListState } from 'components/ListContext';
+import { ListItem } from 'components/ListItem';
+import { Toolbar, ToolBarButton } from 'components/Toolbar';
+import { IndentIcon, OutdentIcon, ImportantIcon, CheckIcon, MoveUpIcon, MoveDownIcon } from 'components/Icons';
+
+const ListWithToolbar = () => {
+    const listContext = useContext(ListContextProvider);
+
+    const buttons: ToolBarButton[] = [
+        {
+            icon: <MoveUpIcon size={20} />,
+            onClick: listContext.moveUpItem,
+            title: 'Move Up (Ctrl+Up)',
+            disabled: !listContext.canMoveUp,
+        },
+        {
+            icon: <MoveDownIcon size={20} />,
+            onClick: listContext.moveDownItem,
+            title: 'Move Down (Ctrl+Down)',
+            disabled: !listContext.canMoveDown,
+            separatorAfter: true,
+        },
+        {
+            icon: <OutdentIcon size={20} />,
+            onClick: listContext.promoteItem,
+            title: 'Outdent (Shift+Tab)',
+            disabled: !listContext.canPromote,
+        },
+        {
+            icon: <IndentIcon size={20} />,
+            onClick: listContext.demoteItem,
+            title: 'Indent (Tab)',
+            disabled: !listContext.canDemote,
+            separatorAfter: true,
+        },
+        {
+            icon: <ImportantIcon size={20} />,
+            onClick: listContext.toggleImportantItem,
+            title: 'Toggle Important (Ctrl+B)',
+            disabled: !listContext.focusedItem,
+            active: listContext.focusedItem?.important || false,
+        },
+        {
+            icon: <CheckIcon size={20} />,
+            onClick: listContext.toggleCompletedItem,
+            title: 'Toggle Completed (Ctrl+S)',
+            disabled: !listContext.focusedItem,
+            active: !!listContext.focusedItem?.completed_at,
+        },
+    ];
+    
+    return (
+        <div>
+            <Toolbar buttons={buttons} />
+            <div style={{ marginTop: '72px' }}>
+                <ul>
+                    {listContext.items.filter(item => !item.__is_deleted).map(item => (
+                        <ListItem 
+                            key={item._uuid} 
+                            item={item} 
+                            onChange={listContext.handleItemChange} 
+                            moveFocus={listContext.handleMoveFocus}
+                            promote={listContext.handlePromote}
+                            demote={listContext.handleDemote}
+                            toggleImportant={listContext.handleToggleImportant}
+                            toggleCompleted={listContext.handleToggleCompleted}
+                            createItem={listContext.handleCreateItem}
+                            deleteItem={listContext.handleDeleteItem}
+                            onFocus={listContext.handleItemFocus}
+                            moveUp={listContext.handleMoveUp}
+                            moveDown={listContext.handleMoveDown}
+                        />
+                    ))}
+                </ul>
+            </div>
+        </div>
+    );
+};
 
 export const List = () => {
-    // ListContext will now fetch its own data.
-    // No initialItems are passed.
-    return <ListContext />;
+    const listState = useListState();
+    
+    return (
+        <ListContextProvider.Provider value={{
+            focusedItem: listState.focusedItem,
+            canPromote: listState.canPromote,
+            canDemote: listState.canDemote,
+            canMoveUp: listState.canMoveUp,
+            canMoveDown: listState.canMoveDown,
+            promoteItem: listState.promoteItem,
+            demoteItem: listState.demoteItem,
+            toggleImportantItem: listState.toggleImportantItem,
+            toggleCompletedItem: listState.toggleCompletedItem,
+            moveUpItem: listState.moveUpItem,
+            moveDownItem: listState.moveDownItem,
+            items: listState.items,
+            handleItemChange: listState.handleItemChange,
+            handleMoveFocus: listState.handleMoveFocus,
+            handlePromote: listState.handlePromote,
+            handleDemote: listState.handleDemote,
+            handleToggleImportant: listState.handleToggleImportant,
+            handleToggleCompleted: listState.handleToggleCompleted,
+            handleCreateItem: listState.handleCreateItem,
+            handleDeleteItem: listState.handleDeleteItem,
+            handleItemFocus: listState.handleItemFocus,
+            handleMoveUp: listState.handleMoveUp,
+            handleMoveDown: listState.handleMoveDown,
+        }}>
+            <ListWithToolbar />
+        </ListContextProvider.Provider>
+    );
 };
 
 export default List;
\ No newline at end of file