ファイルをアップロードできるページ(IndexedDB)

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ファイルマネージャー</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
    <link rel="icon" type="image/svg+xml" href="favicon.svg">
</head>

<body>
    <div class="app-layout">
        <!-- Sidebar -->
        <div class="sidebar">
            <div class="sidebar-header">
                <div class="header-title">
                    <svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
                    </svg>
                    <span>Pages</span>
                </div>
                <div class="header-actions">
                    <button id="bulkDeletePagesBtn" class="icon-btn hidden" title="選択したページを削除"
                        style="color: var(--danger-color);">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20"
                            height="20">
                            <polyline points="3 6 5 6 21 6"></polyline>
                            <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
                            </path>
                        </svg>
                    </button>
                    <button id="toggleViewBtn" class="icon-btn" title="表示切り替え">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20"
                            height="20">
                            <line x1="8" y1="6" x2="21" y2="6"></line>
                            <line x1="8" y1="12" x2="21" y2="12"></line>
                            <line x1="8" y1="18" x2="21" y2="18"></line>
                            <line x1="3" y1="6" x2="3.01" y2="6"></line>
                            <line x1="3" y1="12" x2="3.01" y2="12"></line>
                            <line x1="3" y1="18" x2="3.01" y2="18"></line>
                        </svg>
                    </button>
                </div>
            </div>
            <div class="create-page-btn-container">
                <button class="btn-primary" id="newPageBtn">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
                        <line x1="12" y1="5" x2="12" y2="19"></line>
                        <line x1="5" y1="12" x2="19" y2="12"></line>
                    </svg>
                    新規ページ作成
                </button>
            </div>
            <div class="page-list" id="pageList">
                <!-- Pages will be loaded here -->
            </div>
            <div class="resizer" id="sidebarResizer"></div>
        </div>

        <!-- Main Content -->
        <main class="main-content">
            <div id="pageContent" class="page-content hidden">
                <header class="page-header">
                    <input type="text" id="pageTitle" class="page-title-input" placeholder="ページタイトルを入力...">
                    <button id="exportBtn" class="export-btn" title="エクスポート">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18"
                            height="18">
                            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                            <polyline points="7 10 12 15 17 10"></polyline>
                            <line x1="12" y1="15" x2="12" y2="3"></line>
                        </svg>
                        エクスポート
                    </button>
                </header>

                <div class="editor-area">
                    <textarea id="pageText" class="page-textarea" placeholder="ここにテキストを入力..."></textarea>
                </div>

                <div class="files-area">
                    <div class="area-header">
                        <h3>添付ファイル・URL・テキスト</h3>
                        <button id="bulkDeleteBtn" class="bulk-delete-btn hidden" title="選択した項目を削除">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16"
                                height="16" style="flex-shrink: 0;">
                                <polyline points="3 6 5 6 21 6"></polyline>
                                <path
                                    d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
                                </path>
                            </svg>
                            <span id="selectedCount" style="margin-left: 4px;">0</span>
                        </button>
                    </div>

                    <div class="compact-drop-zone" id="dropZone">
                        <div class="drop-row">
                            <span class="drop-label-text">ファイル</span>
                            <label for="fileInput" class="select-file-btn">ファイルを選択する</label>
                            <span class="drop-hint-text">(ドラッグアンドドロップでもファイルを添付できます。)</span>
                        </div>
                        <div class="drop-row url-row">
                            <span class="drop-label-text">URL</span>
                            <input type="text" id="urlInput" class="url-input" placeholder="https://example.com">
                            <button id="addUrlBtn" class="add-url-btn">追加</button>
                        </div>
                        <div class="drop-row text-row">
                            <span class="drop-label-text">テキスト</span>
                            <input type="text" id="textInput" class="text-input" placeholder="メモやテキストを入力">
                            <button id="addTextBtn" class="add-text-btn">追加</button>
                        </div>
                        <input type="file" id="fileInput" multiple hidden>
                    </div>

                    <div class="files-list-container">
                        <table class="files-table">
                            <tbody id="fileList">
                                <!-- Files will be inserted here -->
                            </tbody>
                        </table>
                        <div id="emptyFilesState" class="empty-state">
                            <p>添付ファイルはありません</p>
                        </div>
                    </div>
                </div>
            </div>

            <div id="noPageSelected" class="no-page-selected">
                <div class="empty-message">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="12" y1="18" x2="12" y2="12"></line>
                        <line x1="9" y1="15" x2="15" y2="15"></line>
                    </svg>
                    <h3>ページを選択または作成してください</h3>
                </div>
            </div>
        </main>
    </div>
    <script src="db.js"></script>
    <script src="ui.js"></script>
    <script src="app.js"></script>
</body>

</html>

 app.js

// Main Application
console.log('app.js loaded');

document.addEventListener('DOMContentLoaded', () => {
    console.log('DOMContentLoaded fired');
    initApp();
});

async function initApp() {
    console.log('initApp called');
    try {
        await initDB();
        console.log('Database initialized successfully');

        await loadPages();
        console.log('Pages loaded');

        setupEventListeners();
        console.log('Event listeners set up');

        initSidebarResizer();
        console.log('Sidebar resizer initialized');

        const pages = await getAllPages();

        // Try to restore last active page from localStorage
        const savedPageId = localStorage.getItem('activePageId');
        if (savedPageId) {
            const pageExists = pages.find(p => p.id === parseInt(savedPageId));
            if (pageExists) {
                selectPage(parseInt(savedPageId));
                console.log('Restored last active page:', savedPageId);
            } else {
                // Saved page doesn't exist anymore, select first page or create new
                if (pages.length > 0) {
                    selectPage(pages[0].id);
                } else {
                    await createNewPage('無題のページ');
                }
            }
        } else if (pages.length > 0) {
            selectPage(pages[0].id);
        } else {
            const allPages = await getAllPages();
            if (allPages.length === 0) {
                await createNewPage('無題のページ');
            }
        }

        console.log('App initialization complete');
    } catch (error) {
        console.error('Initialization failed:', error);
        alert('データベースの初期化に失敗しました。');
    }
}

 db.js

// Database configuration
const DB_NAME = 'FileManagerDB';
const DB_VERSION = 6; // Version 6: Added text items support
let db = null;

// Initialize Database
function initDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onerror = (event) => reject(event.target.error);

        request.onsuccess = (event) => {
            db = event.target.result;
            console.log('Database initialized');
            resolve(db);
        };

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            const txn = event.target.transaction;

            // 1. Ensure 'files' store exists
            if (!db.objectStoreNames.contains('files')) {
                const fileStore = db.createObjectStore('files', { keyPath: 'id', autoIncrement: true });
                fileStore.createIndex('pageId', 'pageId', { unique: false });
            } else {
                const fileStore = txn.objectStore('files');
                if (!fileStore.indexNames.contains('pageId')) {
                    fileStore.createIndex('pageId', 'pageId', { unique: false });
                }
            }

            // 2. Ensure 'pages' store exists
            if (!db.objectStoreNames.contains('pages')) {
                const pageStore = db.createObjectStore('pages', { keyPath: 'id', autoIncrement: true });
                pageStore.createIndex('updatedAt', 'updatedAt', { unique: false });
            }

            // 3. Ensure 'urls' store exists (Version 5)
            if (!db.objectStoreNames.contains('urls')) {
                const urlStore = db.createObjectStore('urls', { keyPath: 'id', autoIncrement: true });
                urlStore.createIndex('pageId', 'pageId', { unique: false });
            }

            // 3.5. Ensure 'texts' store exists (Version 6)
            if (!db.objectStoreNames.contains('texts')) {
                const textStore = db.createObjectStore('texts', { keyPath: 'id', autoIncrement: true });
                textStore.createIndex('pageId', 'pageId', { unique: false });
            }

            // 4. Data Migration (Only if upgrading from V1 where files existed but pages didn't)
            if (event.oldVersion > 0 && event.oldVersion < 2) {
                const fileStore = txn.objectStore('files');

                fileStore.openCursor().onsuccess = (e) => {
                    const cursor = e.target.result;
                    if (cursor) {
                        const updateData = cursor.value;
                        updateData.pageId = 1;
                        cursor.update(updateData);
                        cursor.continue();
                    }
                };

                const pageStore = txn.objectStore('pages');
                pageStore.add({ content: '', title: '移行されたファイル', updatedAt: new Date().getTime() });
            }

            // 5. Data Migration for V3 -> V4 (Add 'order' field to existing files)
            if (event.oldVersion > 0 && event.oldVersion < 4) {
                const fileStore = txn.objectStore('files');
                fileStore.openCursor().onsuccess = (e) => {
                    const cursor = e.target.result;
                    if (cursor) {
                        const file = cursor.value;
                        if (file.order === undefined) {
                            file.order = file.timestamp || 0;
                            cursor.update(file);
                        }
                        cursor.continue();
                    }
                };
            }
        };
    });
}

// Page Operations
function getAllPages() {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pages'], 'readonly');
        const store = transaction.objectStore('pages');
        const pages = [];
        const request = store.openCursor(null, 'prev');

        request.onsuccess = (e) => {
            const cursor = e.target.result;
            if (cursor) {
                pages.push(cursor.value);
                cursor.continue();
            } else {
                resolve(pages);
            }
        };
        request.onerror = (e) => reject(e.target.error);
    });
}

function getPage(id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pages'], 'readonly');
        const store = transaction.objectStore('pages');
        const request = store.get(id);
        request.onsuccess = () => resolve(request.result);
        request.onerror = (e) => reject(e.target.error);
    });
}

function addPage(title) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pages'], 'readwrite');
        const store = transaction.objectStore('pages');
        const request = store.add({
            title: title,
            content: '',
            updatedAt: new Date().getTime(),
            textUpdatedAt: new Date().getTime() // Initialize with creation time
        });
        request.onsuccess = () => resolve(request.result);
        request.onerror = (e) => reject(e.target.error);
    });
}

function updatePageInDB(id, data) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pages'], 'readwrite');
        const store = transaction.objectStore('pages');
        const getReq = store.get(id);

        getReq.onsuccess = () => {
            const page = getReq.result;
            if (!page) {
                reject('Page not found');
                return;
            }
            const changes = { ...data };
            changes.updatedAt = new Date().getTime();
            
            // If content is being updated, also update textUpdatedAt
            if (changes.content !== undefined) {
                changes.textUpdatedAt = new Date().getTime();
            }

            const updatedPage = { ...page, ...changes };
            const putReq = store.put(updatedPage);
            putReq.onsuccess = () => resolve();
            putReq.onerror = (e) => reject(e.target.error);
        };
        getReq.onerror = (e) => reject(e.target.error);
    });
}

function deletePageFromDB(id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['pages', 'files'], 'readwrite');
        const pageStore = transaction.objectStore('pages');
        const fileStore = transaction.objectStore('files');
        const fileIndex = fileStore.index('pageId');

        pageStore.delete(id);

        const fileReq = fileIndex.getAllKeys(id);
        fileReq.onsuccess = () => {
            const keys = fileReq.result;
            keys.forEach(key => fileStore.delete(key));
        };

        transaction.oncomplete = () => resolve();
        transaction.onerror = (e) => reject(e.target.error);
    });
}

// File Operations
function saveFileToDB(file, pageId) {
    return new Promise(async (resolve, reject) => {
        if (!pageId) {
            reject('No active page selected');
            return;
        }

        try {
            // Get current max order
            const currentFiles = await getFilesByPage(pageId);
            const maxOrder = currentFiles.length > 0
                ? Math.max(...currentFiles.map(f => f.order || 0))
                : 0;

            const transaction = db.transaction(['files'], 'readwrite');
            const store = transaction.objectStore('files');

            const fileData = {
                name: file.name,
                type: file.type,
                size: file.size,
                data: file,
                timestamp: new Date().getTime(),
                pageId: pageId,
                order: maxOrder + 100 // Increment by 100 for easier reordering
            };

            const request = store.add(fileData);
            request.onsuccess = () => resolve(request.result);
            request.onerror = (e) => reject(e.target.error);
        } catch (error) {
            reject(error);
        }
    });
}

function getFilesByPage(pageId) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['files'], 'readonly');
        const store = transaction.objectStore('files');
        const index = store.index('pageId');
        const files = [];

        const request = index.openCursor(IDBKeyRange.only(pageId));

        request.onsuccess = (e) => {
            const cursor = e.target.result;
            if (cursor) {
                files.push(cursor.value);
                cursor.continue();
            } else {
                // Sort by order field, fallback to timestamp
                files.sort((a, b) => {
                    const orderA = a.order !== undefined ? a.order : (a.timestamp || 0);
                    const orderB = b.order !== undefined ? b.order : (b.timestamp || 0);
                    return orderA - orderB;
                });
                resolve(files);
            }
        };
        request.onerror = (e) => reject(e.target.error);
    });
}

function deleteFileFromDB(id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['files'], 'readwrite');
        const store = transaction.objectStore('files');
        const request = store.delete(id);
        request.onsuccess = () => resolve();
        request.onerror = (e) => reject(e.target.error);
    });
}

function updateFileOrderInDB(filesToUpdate) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['files'], 'readwrite');
        const store = transaction.objectStore('files');

        filesToUpdate.forEach(item => {
            const getReq = store.get(item.id);
            getReq.onsuccess = () => {
                const file = getReq.result;
                if (file) {
                    file.order = item.order;
                    store.put(file);
                }
            };
        });

        transaction.oncomplete = () => resolve();
        transaction.onerror = (e) => reject(e.target.error);
    });
}

// URL Operations
function saveURLToDB(url, title, pageId) {
    return new Promise(async (resolve, reject) => {
        if (!pageId) {
            reject('No active page selected');
            return;
        }

        try {
            // Get current max order from both files and URLs
            const currentItems = await getItemsByPage(pageId);
            const maxOrder = currentItems.length > 0
                ? Math.max(...currentItems.map(item => item.order || 0))
                : 0;

            const transaction = db.transaction(['urls'], 'readwrite');
            const store = transaction.objectStore('urls');

            const urlData = {
                url: url,
                title: title || url,
                pageId: pageId,
                timestamp: new Date().getTime(),
                order: maxOrder + 100 // Increment by 100 for easier reordering
            };

            const request = store.add(urlData);
            request.onsuccess = () => resolve(request.result);
            request.onerror = (e) => reject(e.target.error);
        } catch (error) {
            reject(error);
        }
    });
}

function getURLsByPage(pageId) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['urls'], 'readonly');
        const store = transaction.objectStore('urls');
        const index = store.index('pageId');
        const urls = [];

        const request = index.openCursor(IDBKeyRange.only(pageId));

        request.onsuccess = (e) => {
            const cursor = e.target.result;
            if (cursor) {
                urls.push(cursor.value);
                cursor.continue();
            } else {
                // Sort by order field, fallback to timestamp
                urls.sort((a, b) => {
                    const orderA = a.order !== undefined ? a.order : (a.timestamp || 0);
                    const orderB = b.order !== undefined ? b.order : (b.timestamp || 0);
                    return orderA - orderB;
                });
                resolve(urls);
            }
        };
        request.onerror = (e) => reject(e.target.error);
    });
}

function deleteURLFromDB(id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['urls'], 'readwrite');
        const store = transaction.objectStore('urls');
        const request = store.delete(id);
        request.onsuccess = () => resolve();
        request.onerror = (e) => reject(e.target.error);
    });
}

function updateURLOrderInDB(urlsToUpdate) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['urls'], 'readwrite');
        const store = transaction.objectStore('urls');

        urlsToUpdate.forEach(item => {
            const getReq = store.get(item.id);
            getReq.onsuccess = () => {
                const url = getReq.result;
                if (url) {
                    url.order = item.order;
                    store.put(url);
                }
            };
        });

        transaction.oncomplete = () => resolve();
        transaction.onerror = (e) => reject(e.target.error);
    });
}

// Unified function to get both files and URLs
function getItemsByPage(pageId) {
    return new Promise(async (resolve, reject) => {
        try {
            const files = await getFilesByPage(pageId);
            const urls = await getURLsByPage(pageId);
            const texts = await getTextsByPage(pageId);

            // Add type identifier to each item
            const filesWithType = files.map(f => ({ ...f, itemType: 'file' }));
            const urlsWithType = urls.map(u => ({ ...u, itemType: 'url' }));
            const textsWithType = texts.map(t => ({ ...t, itemType: 'text' }));

            // Merge and sort by order
            const allItems = [...filesWithType, ...urlsWithType, ...textsWithType];
            allItems.sort((a, b) => {
                const orderA = a.order !== undefined ? a.order : (a.timestamp || 0);
                const orderB = b.order !== undefined ? b.order : (b.timestamp || 0);
                return orderA - orderB;
            });

            resolve(allItems);
        } catch (error) {
            reject(error);
        }
    });
}

// Text Operations
function saveTextToDB(content, pageId) {
    return new Promise(async (resolve, reject) => {
        if (!pageId) {
            reject('No active page selected');
            return;
        }

        try {
            // Get current max order from all items
            const currentItems = await getItemsByPage(pageId);
            const maxOrder = currentItems.length > 0
                ? Math.max(...currentItems.map(item => item.order || 0))
                : 0;

            const transaction = db.transaction(['texts'], 'readwrite');
            const store = transaction.objectStore('texts');

            const textData = {
                content: content,
                pageId: pageId,
                timestamp: new Date().getTime(),
                order: maxOrder + 100 // Increment by 100 for easier reordering
            };

            const request = store.add(textData);
            request.onsuccess = () => resolve(request.result);
            request.onerror = (e) => reject(e.target.error);
        } catch (error) {
            reject(error);
        }
    });
}

function getTextsByPage(pageId) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['texts'], 'readonly');
        const store = transaction.objectStore('texts');
        const index = store.index('pageId');
        const texts = [];

        const request = index.openCursor(IDBKeyRange.only(pageId));

        request.onsuccess = (e) => {
            const cursor = e.target.result;
            if (cursor) {
                texts.push(cursor.value);
                cursor.continue();
            } else {
                // Sort by order field, fallback to timestamp
                texts.sort((a, b) => {
                    const orderA = a.order !== undefined ? a.order : (a.timestamp || 0);
                    const orderB = b.order !== undefined ? b.order : (b.timestamp || 0);
                    return orderA - orderB;
                });
                resolve(texts);
            }
        };
        request.onerror = (e) => reject(e.target.error);
    });
}

function deleteTextFromDB(id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['texts'], 'readwrite');
        const store = transaction.objectStore('texts');
        const request = store.delete(id);
        request.onsuccess = () => resolve();
        request.onerror = (e) => reject(e.target.error);
    });
}

function updateTextOrderInDB(textsToUpdate) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['texts'], 'readwrite');
        const store = transaction.objectStore('texts');

        textsToUpdate.forEach(item => {
            const getReq = store.get(item.id);
            getReq.onsuccess = () => {
                const text = getReq.result;
                if (text) {
                    text.order = item.order;
                    store.put(text);
                }
            };
        });

        transaction.oncomplete = () => resolve();
        transaction.onerror = (e) => reject(e.target.error);
    });
}



 ui.js

// UI State
let activePageId = null;
let saveTimeout = null;

// Utility function
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Setup Event Listeners
function setupEventListeners() {
    console.log('Setting up event listeners...');

    // Sidebar
    document.getElementById('newPageBtn').addEventListener('click', () => createNewPage());

    // Toggle View State
    const toggleViewBtn = document.getElementById('toggleViewBtn');
    const pageList = document.getElementById('pageList');

    const savedMode = localStorage.getItem('pageListMode');
    if (savedMode === 'detail') {
        pageList.classList.add('mode-detail');
        if (toggleViewBtn) toggleViewBtn.classList.add('active');
    }

    if (toggleViewBtn) {
        toggleViewBtn.addEventListener('click', () => {
            pageList.classList.toggle('mode-detail');
            const isDetail = pageList.classList.contains('mode-detail');
            localStorage.setItem('pageListMode', isDetail ? 'detail' : 'list');
        });
    }

    // Main Content
    document.getElementById('pageTitle').addEventListener('input', (e) => {
        const newTitle = e.target.value;
        document.title = newTitle || '無題のページ';
        debouncedSave({ title: newTitle });
    });

    document.getElementById('pageText').addEventListener('input', (e) => {
        debouncedSave({ content: e.target.value });
    });

    // document.getElementById('deletePageBtn').addEventListener('click', deleteCurrentPage);

    // Drop Zone
    const dropZone = document.getElementById('dropZone');
    const fileInput = document.getElementById('fileInput');

    dropZone.addEventListener('dragover', (e) => {
        e.preventDefault();
        dropZone.classList.add('drag-over');
    });
    dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
    dropZone.addEventListener('drop', handleDrop);

    fileInput.addEventListener('change', handleFileSelect);

    // URL Input
    const urlInput = document.getElementById('urlInput');
    const addUrlBtn = document.getElementById('addUrlBtn');

    addUrlBtn.addEventListener('click', handleURLAdd);
    urlInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            handleURLAdd();
        }
    });

    // Text Input
    const textInput = document.getElementById('textInput');
    const addTextBtn = document.getElementById('addTextBtn');

    addTextBtn.addEventListener('click', handleTextAdd);
    textInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            handleTextAdd();
        }
    });

    // Export button
    document.getElementById('exportBtn').addEventListener('click', handleExport);

    // File list delegation
    document.getElementById('fileList').addEventListener('click', handleFileAction);

    // Bulk Delete Listener
    const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
    if (bulkDeleteBtn && !bulkDeleteBtn.hasAttribute('data-has-listener')) {
        bulkDeleteBtn.addEventListener('click', async () => {
            const checkboxes = document.querySelectorAll('.page-checkbox:checked');
            if (checkboxes.length === 0) return;

            if (!confirm(`${checkboxes.length}件のページを削除しますか?`)) return;

            const idsToDelete = Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));

            try {
                for (const id of idsToDelete) {
                    await deletePageFromDB(id);
                }

                if (activePageId && idsToDelete.includes(activePageId)) {
                    activePageId = null;
                    document.getElementById('pageTitle').value = '';
                    document.getElementById('pageText').value = '';
                    document.getElementById('fileList').innerHTML = '';
                    document.getElementById('pageContent').classList.add('hidden');
                    document.getElementById('noPageSelected').classList.remove('hidden');
                }
                await loadPages();
            } catch (e) {
                console.error(e);
                alert('削除エラーが発生しました');
            }
        });
        bulkDeleteBtn.setAttribute('data-has-listener', 'true');
    }
}

// Page UI Logic
async function loadPages() {
    const pages = await getAllPages();
    pages.sort((a, b) => {
        // Priority: textUpdatedAt > updatedAt > 0
        const timeA = a.textUpdatedAt || a.updatedAt || 0;
        const timeB = b.textUpdatedAt || b.updatedAt || 0;
        return timeB - timeA;
    });

    const pageList = document.getElementById('pageList');
    pageList.innerHTML = '';

    pages.forEach(page => {
        const div = document.createElement('div');
        div.className = `page-item ${page.id === activePageId ? 'active' : ''}`;

        // Use textUpdatedAt for display if available, otherwise fallback to updatedAt
        const displayTime = page.textUpdatedAt || page.updatedAt || Date.now();
        const date = new Date(displayTime);
        const dateStr = `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;

        div.innerHTML = `
            <input type="checkbox" class="page-checkbox" data-id="${page.id}">
            <div class="page-info">
                <div class="page-item-title">${escapeHtml(page.title || '無題のページ')}</div>
                <div class="page-item-date">${dateStr}</div>
            </div>
            <button class="sidebar-delete-btn" title="ページを削除">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                    <polyline points="3 6 5 6 21 6"></polyline>
                    <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
                </svg>
            </button>
        `;
        div.dataset.id = page.id;

        div.onclick = (e) => {
            selectPage(page.id);
        };

        const checkbox = div.querySelector('.page-checkbox');
        checkbox.onclick = (e) => {
            e.stopPropagation();
            updateBulkDeleteState();
        };

        const delBtn = div.querySelector('.sidebar-delete-btn');
        delBtn.onclick = (e) => {
            e.stopPropagation();
            deletePage(page.id);
        };

        pageList.appendChild(div);
    });

    updateBulkDeleteState();
}

function updateBulkDeleteState() {
    const checkboxes = document.querySelectorAll('.page-checkbox:checked');
    const bulkBtn = document.getElementById('bulkDeleteBtn');
    if (bulkBtn) {
        if (checkboxes.length > 0) {
            bulkBtn.classList.add('visible');
        } else {
            bulkBtn.classList.remove('visible');
        }
    }
}

async function deletePage(id) {
    if (!confirm('このページと関連ファイルを完全に削除しますか?')) return;
    try {
        await deletePageFromDB(id);
        if (activePageId === id) {
            activePageId = null;
            localStorage.removeItem('activePageId'); // Clear saved page
            document.title = 'ファイルマネージャー';
            document.getElementById('pageTitle').value = '';
            document.getElementById('pageText').value = '';
            document.getElementById('fileList').innerHTML = '';
            document.getElementById('pageContent').classList.add('hidden');
            document.getElementById('noPageSelected').classList.remove('hidden');
        }
        await loadPages();
    } catch (e) {
        console.error(e);
        alert('削除に失敗しました');
    }
}

async function createNewPage(initialTitle = '無題のページ') {
    try {
        const id = await addPage(initialTitle);
        activePageId = id;
        await loadPages();
        await selectPage(id);
    } catch (e) {
        console.error('Error creating page', e);
    }
}

async function selectPage(id) {
    activePageId = id;
    const page = await getPage(id);
    if (!page) return;

    // Save to localStorage for persistence across reloads
    localStorage.setItem('activePageId', id);

    document.title = page.title || '無題のページ';

    document.querySelectorAll('.page-item').forEach(el => {
        el.classList.toggle('active', parseInt(el.dataset.id) === id);
    });

    document.getElementById('pageContent').classList.remove('hidden');
    document.getElementById('noPageSelected').classList.add('hidden');
    document.getElementById('pageTitle').value = page.title;
    document.getElementById('pageText').value = page.content;

    await refreshFilesList();
}

async function deleteCurrentPage() {
    if (!activePageId) return;
    if (!confirm('このページと関連ファイルをすべて削除しますか?')) return;

    await deletePageFromDB(activePageId);
    activePageId = null;
    localStorage.removeItem('activePageId'); // Clear saved page
    document.getElementById('pageContent').classList.add('hidden');
    document.getElementById('noPageSelected').classList.remove('hidden');
    await loadPages();
}

function debouncedSave(data) {
    clearTimeout(saveTimeout);
    saveTimeout = setTimeout(async () => {
        if (activePageId) {
            await updatePageInDB(activePageId, data);
            await loadPages();
        }
    }, 500);
}

// File UI Logic
async function handleDrop(e) {
    e.preventDefault();
    document.getElementById('dropZone').classList.remove('drag-over');
    if (!activePageId) return;

    const files = e.dataTransfer.files;
    if (files.length > 0) await processFiles(files);
}

async function handleFileSelect(e) {
    if (!activePageId) return;
    const files = Array.from(e.target.files);
    if (files.length > 0) await processFiles(files);
    e.target.value = '';
}

async function processFiles(files) {
    for (const file of files) {
        try {
            await saveFileToDB(file, activePageId);
        } catch (err) {
            console.error(`Error saving ${file.name}:`, err);
        }
    }
    await refreshItemsList();
}

// URL handling
async function handleURLAdd() {
    if (!activePageId) return;

    const urlInput = document.getElementById('urlInput');
    const url = urlInput.value.trim();

    if (!url) return;

    // Basic URL validation
    try {
        new URL(url);
    } catch (e) {
        alert('有効なURLを入力してください');
        return;
    }

    try {
        // Fetch page title
        let title = url; // Default to URL
        try {
            const response = await fetch(url, {
                method: 'GET',
                mode: 'cors'
            });
            const html = await response.text();
            const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
            if (titleMatch && titleMatch[1]) {
                title = titleMatch[1].trim();
            }
        } catch (fetchError) {
            // If fetch fails (CORS, network, etc.), use URL as title
            console.log('Could not fetch page title, using URL:', fetchError);
        }

        await saveURLToDB(url, title, activePageId);
        urlInput.value = '';
        await refreshItemsList();
    } catch (err) {
        console.error('Error saving URL:', err);
        alert('URLの保存に失敗しました');
    }
}

// Text handling
async function handleTextAdd() {
    if (!activePageId) return;

    const textInput = document.getElementById('textInput');
    const content = textInput.value.trim();

    if (!content) return;

    try {
        await saveTextToDB(content, activePageId);
        textInput.value = '';
        await refreshItemsList();
    } catch (err) {
        console.error('Error saving text:', err);
        alert('テキストの保存に失敗しました');
    }
}

async function refreshItemsList() {
    if (!activePageId) return;
    const items = await getItemsByPage(activePageId);
    const list = document.getElementById('fileList');
    const empty = document.getElementById('emptyFilesState');

    list.innerHTML = '';

    if (items.length === 0) {
        empty.classList.add('visible');
        return;
    }
    empty.classList.remove('visible');

    items.forEach(item => {
        const row = document.createElement('tr');
        row.className = 'file-row';
        row.draggable = true;
        row.dataset.id = item.id;
        row.dataset.order = item.order;
        row.dataset.type = item.itemType; // 'file', 'url', or 'text'

        // Choose icon based on type
        let iconSvg = '';
        let displayName = '';

        if (item.itemType === 'file') {
            iconSvg = `
                <svg style="width: 16px; height: 16px; color: var(--text-muted);" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                    <polyline points="13 2 13 9 20 9"></polyline>
                </svg>
            `;
            displayName = item.name || '名称未設定';
        } else if (item.itemType === 'url') {
            iconSvg = `
                <svg style="width: 16px; height: 16px; color: var(--primary-color);" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
                    <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
                </svg>
            `;
            displayName = item.title || item.url;
        } else {
            // text
            iconSvg = `
                <svg style="width: 16px; height: 16px; color: #10b981;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <line x1="17" y1="10" x2="3" y2="10"></line>
                    <line x1="21" y1="6" x2="3" y2="6"></line>
                    <line x1="21" y1="14" x2="3" y2="14"></line>
                    <line x1="17" y1="18" x2="3" y2="18"></line>
                </svg>
            `;
            displayName = item.content;
        }

        row.innerHTML = `
            <td class="checkbox-cell">
                <input type="checkbox" class="item-checkbox" data-id="${item.id}" data-type="${item.itemType}">
            </td>
            <td class="drag-handle-cell">
                <div class="drag-handle">
                    <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
                        <circle cx="9" cy="5" r="1"></circle>
                        <circle cx="9" cy="12" r="1"></circle>
                        <circle cx="9" cy="19" r="1"></circle>
                        <circle cx="15" cy="5" r="1"></circle>
                        <circle cx="15" cy="12" r="1"></circle>
                        <circle cx="15" cy="19" r="1"></circle>
                    </svg>
                </div>
            </td>
            <td>
                <div class="file-name-container">
                    ${iconSvg}
                    <span class="file-name-text">${escapeHtml(displayName)}</span>
                </div>
            </td>
            <td class="text-right">
                ${item.itemType === 'file' ? `
                    <button class="action-btn download" data-id="${item.id}" data-type="file" title="ダウンロード">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                            <polyline points="7 10 12 15 17 10"></polyline>
                            <line x1="12" y1="15" x2="12" y2="3"></line>
                        </svg>
                    </button>
                ` : ''}
                <button class="action-btn delete" data-id="${item.id}" data-type="${item.itemType}" title="削除">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                        <polyline points="3 6 5 6 21 6"></polyline>
                        <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
                    </svg>
                </button>
            </td>
        `;

        const nameContainer = row.querySelector('.file-name-container');
        if (item.itemType === 'file') {
            nameContainer.onclick = () => openFile(item);
        } else if (item.itemType === 'url') {
            nameContainer.onclick = () => openURL(item);
        } else {
            nameContainer.onclick = () => openText(item);
        }

        const downloadBtn = row.querySelector('.download');
        if (downloadBtn) {
            downloadBtn.onclick = (e) => {
                e.stopPropagation();
                downloadFile(item);
            };
        }

        // Checkbox handler
        const checkbox = row.querySelector('.item-checkbox');
        checkbox.onclick = (e) => {
            e.stopPropagation();
            updateBulkDeleteItemState();
        };

        // Add drag event listeners
        row.addEventListener('dragstart', handleRowDragStart);
        row.addEventListener('dragover', handleRowDragOver);
        row.addEventListener('drop', handleRowDrop);
        row.addEventListener('dragenter', (e) => e.preventDefault());
        row.addEventListener('dragend', handleRowDragEnd);

        list.appendChild(row);
    });
}

// Keep old function name for backward compatibility
async function refreshFilesList() {
    await refreshItemsList();
}

function handleFileAction(e) {
    const delBtn = e.target.closest('.delete');
    if (delBtn) {
        e.stopPropagation();
        const id = parseInt(delBtn.dataset.id);
        const type = delBtn.dataset.type;
        deleteItem(id, type);
    }
}

async function deleteItem(id, type) {
    if (!confirm(type === 'file' ? 'ファイルを削除しますか?' : type === 'url' ? 'URLを削除しますか?' : 'テキストを削除しますか?')) return;

    try {
        if (type === 'file') {
            await deleteFileFromDB(id);
        } else if (type === 'url') {
            await deleteURLFromDB(id);
        } else {
            await deleteTextFromDB(id);
        }
        await refreshItemsList();
    } catch (err) {
        console.error('Error deleting item:', err);
    }
}

async function deleteFile(id) {
    await deleteItem(id, 'file');
}

function downloadFile(fileData) {
    const url = URL.createObjectURL(fileData.data);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileData.name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

function openFile(fileData) {
    const url = URL.createObjectURL(fileData.data);
    window.open(url, '_blank');
    setTimeout(() => { /* keep alive */ }, 1000);
}

function openURL(urlData) {
    window.open(urlData.url, '_blank');
}

// Sidebar Resizing
function initSidebarResizer() {
    const sidebar = document.querySelector('.sidebar');
    const resizer = document.getElementById('sidebarResizer');

    if (!resizer) return;

    let isResizing = false;

    resizer.addEventListener('mousedown', (e) => {
        isResizing = true;
        sidebar.classList.add('resizing');
        document.body.style.cursor = 'col-resize';
        document.body.style.userSelect = 'none';
    });

    document.addEventListener('mousemove', (e) => {
        if (!isResizing) return;
        const newWidth = e.clientX;
        if (newWidth > 180 && newWidth < 500) {
            sidebar.style.width = `${newWidth}px`;
        }
    });

    document.addEventListener('mouseup', () => {
        if (isResizing) {
            isResizing = false;
            sidebar.classList.remove('resizing');
            document.body.style.cursor = '';
            document.body.style.userSelect = '';
        }
    });
}

// Drag and Drop for File Reordering
let dragSrcRow = null;

function handleRowDragStart(e) {
    dragSrcRow = this;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/html', this.innerHTML);
    this.classList.add('dragging');
}

function handleRowDragOver(e) {
    if (e.preventDefault) {
        e.preventDefault();
    }
    e.dataTransfer.dropEffect = 'move';
    return false;
}

async function handleRowDrop(e) {
    if (e.stopPropagation) {
        e.stopPropagation();
    }

    if (dragSrcRow !== this) {
        const list = document.getElementById('fileList');
        const rows = Array.from(list.children);

        let srcIndex = rows.indexOf(dragSrcRow);
        let targetIndex = rows.indexOf(this);

        // Reorder DOM
        if (srcIndex < targetIndex) {
            this.after(dragSrcRow);
        } else {
            this.before(dragSrcRow);
        }

        // Save new order to database
        await saveNewItemOrder();
    }

    return false;
}

function handleRowDragEnd(e) {
    this.classList.remove('dragging');
}

async function saveNewItemOrder() {
    const list = document.getElementById('fileList');
    const rows = list.querySelectorAll('tr.file-row');
    const fileUpdates = [];
    const urlUpdates = [];
    const textUpdates = [];

    rows.forEach((row, index) => {
        const id = parseInt(row.dataset.id);
        const type = row.dataset.type;
        const newOrder = (index + 1) * 100;

        if (type === 'file') {
            fileUpdates.push({ id: id, order: newOrder });
        } else if (type === 'url') {
            urlUpdates.push({ id: id, order: newOrder });
        } else {
            textUpdates.push({ id: id, order: newOrder });
        }
    });

    if (fileUpdates.length > 0) {
        await updateFileOrderInDB(fileUpdates);
    }
    if (urlUpdates.length > 0) {
        await updateURLOrderInDB(urlUpdates);
    }
    if (textUpdates.length > 0) {
        await updateTextOrderInDB(textUpdates);
    }
}

async function saveNewFileOrder() {
    await saveNewItemOrder();
}

// Text operations
function openText(textData) {
    // Copy text to clipboard
    navigator.clipboard.writeText(textData.content).then(() => {
        alert('テキストをクリップボードにコピーしました');
    }).catch(() => {
        // Fallback: show in alert
        alert(textData.content);
    });
}

// Bulk delete for items
function updateBulkDeleteItemState() {
    const checkboxes = document.querySelectorAll('.item-checkbox:checked');
    // For now, just log - we'll add a visible button later
    console.log(`${checkboxes.length} items selected`);
}

async function handleBulkDeleteItems() {
    const checkboxes = document.querySelectorAll('.item-checkbox:checked');
    if (checkboxes.length === 0) return;

    if (!confirm(`${checkboxes.length}件のアイテムを削除しますか?`)) return;

    try {
        for (const checkbox of checkboxes) {
            const id = parseInt(checkbox.dataset.id);
            const type = checkbox.dataset.type;

            if (type === 'file') {
                await deleteFileFromDB(id);
            } else if (type === 'url') {
                await deleteURLFromDB(id);
            } else {
                await deleteTextFromDB(id);
            }
        }
        await refreshItemsList();
    } catch (err) {
        console.error('Error in bulk delete:', err);
        alert('削除エラーが発生しました');
    }
}

// Export functionality
async function handleExport() {
    if (!activePageId) {
        alert('ページが選択されていません');
        return;
    }

    try {
        const items = await getItemsByPage(activePageId);
        const pageTitle = document.getElementById('pageTitle').value || '無題のページ';

        if (items.length === 0) {
            alert('エクスポートするアイテムがありません');
            return;
        }

        // Separate items by type
        const files = items.filter(item => item.itemType === 'file');
        const urls = items.filter(item => item.itemType === 'url');
        const texts = items.filter(item => item.itemType === 'text');

        // Export files as actual files
        for (const file of files) {
            const url = URL.createObjectURL(file.data);
            const a = document.createElement('a');
            a.href = url;
            a.download = file.name;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            // Small delay between downloads
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        // Export URLs as text file
        if (urls.length > 0) {
            const urlText = urls.map(item => {
                return `${item.title || item.url}\n${item.url}\n`;
            }).join('\n');

            const blob = new Blob([urlText], { type: 'text/plain;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `${pageTitle}_urls.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        // Export text items as text file
        if (texts.length > 0) {
            const textContent = texts.map((item, index) => {
                return `--- テキスト ${index + 1} ---\n${item.content}\n`;
            }).join('\n');

            const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `${pageTitle}_texts.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }

        const summary = [];
        if (files.length > 0) summary.push(`${files.length}個のファイル`);
        if (urls.length > 0) summary.push(`${urls.length}個のURL`);
        if (texts.length > 0) summary.push(`${texts.length}個のテキスト`);

        alert(`エクスポートが完了しました\n${summary.join('、')}`);
    } catch (err) {
        console.error('Error exporting data:', err);
        alert('エクスポートに失敗しました');
    }
}

 style.css

:root {
    --primary-color: #6366f1;
    --primary-hover: #4f46e5;
    --bg-color: #f8fafc;
    --card-bg: #ffffff;
    --text-main: #1e293b;
    --text-muted: #64748b;
    --border-color: #e2e8f0;
    --danger-color: #ef4444;
    --success-color: #10b981;
    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --radius: 12px;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Inter', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-main);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

:root {
    --primary-color: #6366f1;
    --primary-hover: #4f46e5;
    --sidebar-bg: #f1f5f9;
    --sidebar-border: #e2e8f0;
    --bg-color: #ffffff;
    --text-main: #1e293b;
    --text-muted: #64748b;
    --border-color: #e2e8f0;
    --danger-color: #ef4444;
    --accent-bg: #e0e7ff;
    --radius: 8px;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Inter', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-main);
    height: 100vh;
    overflow: hidden;
    /* App-like feel */
}

/* Layout */
.app-layout {
    display: flex;
    height: 100%;
}

/* Sidebar Resizer */
.resizer {
    width: 5px;
    background-color: transparent;
    cursor: col-resize;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 10;
    transition: background-color 0.2s;
}

.resizer:hover,
.sidebar.resizing .resizer {
    background-color: var(--primary-color);
}

/* Sidebar */
.sidebar {
    width: 250px;
    background-color: #f1f5f9;
    border-right: 1px solid var(--border-color);
    display: flex;
    flex-direction: column;
    position: relative;
    /* Needed for resizer positioning */
    min-width: 180px;
    max-width: 500px;
}

/* Sidebar Header Controls */
.sidebar-header {
    padding: 1.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Spacing for controls */
    font-weight: 700;
    color: var(--primary-color);
    font-size: 1.2rem;
}

.header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.icon-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-muted);
    padding: 4px;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
}

.icon-btn:hover {
    background-color: rgba(0, 0, 0, 0.05);
    color: var(--text-main);
}

.header-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.bulk-delete-btn {
    background: #fff;
    border: 1px solid var(--border-color);
    color: var(--text-muted);
    cursor: pointer;
    width: 50px;
    height: 32px;
    border-radius: 50%;
    /* Circular */
    display: none;
    /* Hidden by default */
    align-items: center;
    justify-content: center;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

.bulk-delete-btn.visible {
    display: flex;
    animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

@keyframes popIn {
    from {
        transform: scale(0.5);
        opacity: 0;
    }

    to {
        transform: scale(1);
        opacity: 1;
    }
}

.bulk-delete-btn:hover {
    color: var(--danger-color);
    border-color: #fecaca;
    background-color: #fef2f2;
    transform: translateY(-1px);
    box-shadow: 0 4px 6px -1px rgba(220, 38, 38, 0.1);
}

.bulk-delete-btn:active {
    transform: translateY(0);
}

.logo-icon {
    width: 24px;
    height: 24px;
    color: var(--primary-color);
}

.sidebar-header h2 {
    font-size: 1.125rem;
    font-weight: 600;
}

.create-page-btn-container {
    padding: 0 1rem 1rem;
}

.btn-primary {
    width: 100%;
    background-color: var(--primary-color);
    color: white;
    border: none;
    padding: 0.75rem;
    border-radius: 6px;
    cursor: pointer;
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    transition: background 0.2s;
}

.btn-primary:hover {
    background-color: var(--primary-hover);
}

.new-page-btn svg {
    width: 18px;
    height: 18px;
}

.page-list {
    flex: 1;
    overflow-y: auto;
    padding: 0 0.5rem;
}

.page-item {
    padding: 0.5rem 0.75rem;
    margin-bottom: 0px;
    border-bottom: 1px solid #f1f5f9;
    cursor: pointer;
    color: var(--text-main);
    transition: background 0.1s;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    height: 48px;
    /* Default height */
}

/* Two-line mode styles */
.page-list.mode-detail .page-item {
    height: auto;
    min-height: 60px;
    padding-top: 0.75rem;
    padding-bottom: 0.75rem;
}

.page-item:hover {
    background-color: #fff;
}

.page-item.active {
    background-color: #fff;
    border-left: 4px solid var(--primary-color);
    padding-left: calc(0.75rem - 4px);
    /* maintain spacing */
}

/* Container for text parts to handle overflow */
.page-info {
    display: flex;
    flex-direction: row;
    /* Horizontal layout */
    align-items: center;
    overflow: hidden;
    flex: 1;
    gap: 0.5rem;
}

.page-list.mode-detail .page-info {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
}

.page-item-title {
    font-weight: 500;
    font-size: 0.9rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
    /* Take remaining space */
    width: 100%;
}

.page-item-date {
    font-size: 0.7rem;
    color: var(--text-muted);
    white-space: nowrap;
    flex-shrink: 0;
    /* Don't shrink date */
}

.page-list.mode-detail .page-item-date {
    font-size: 0.75rem;
    color: var(--text-muted);
}

.page-checkbox {
    margin-right: 0.5rem;
    cursor: pointer;
    width: 16px;
    height: 16px;
}

/* Delete button in sidebar (individual) */
.sidebar-delete-btn {
    padding: 4px;
    border-radius: 4px;
    color: var(--text-muted);
    border: none;
    background: transparent;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    /* Hidden by default */
    transition: opacity 0.2s, background-color 0.2s;
    margin-left: 0.25rem;
}

.page-item:hover .sidebar-delete-btn,
.sidebar-delete-btn:focus {
    opacity: 1;
}

.sidebar-delete-btn:hover {
    background-color: #ffe4e6;
    color: var(--danger-color);
}

/* Main Content */
.main-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: #fff;
    position: relative;
}

.no-page-selected {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
}

.no-page-selected.hidden {
    display: none;
}

.empty-message {
    text-align: center;
}

.empty-icon {
    width: 64px;
    height: 64px;
    color: #cbd5e1;
    margin-bottom: 1rem;
}

/* Page Content View */
.page-content {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.page-content.hidden {
    display: none;
}

.page-header {
    padding: 0.75rem 2rem;
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    gap: 1rem;
}

.page-title-input {
    flex: 1;
    font-size: 1.5rem;
    font-weight: 600;
    border: none;
    outline: none;
    padding: 0.5rem;
    background: transparent;
    color: var(--text-main);
}

.page-title-input::placeholder {
    color: #cbd5e1;
}

.delete-page-btn {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    padding: 0.5rem;
    border-radius: 6px;
    transition: all 0.2s;
}

.delete-page-btn:hover {
    background-color: #fee2e2;
    color: var(--danger-color);
}

.delete-page-btn svg {
    width: 20px;
    height: 20px;
}

/* Editor Area */
.editor-area {
    height: 80px;
    /* Fixed smaller height */
    overflow-y: auto;
    display: flex;
    justify-content: center;
    /* Center the editor content */
    background-color: #f8fafc;
    /* Slight bg distinction */
    flex-shrink: 0;
    border-bottom: 1px solid var(--border-color);
}

.export-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.875rem;
    font-weight: 500;
    transition: background-color 0.2s;
    white-space: nowrap;
}

.export-btn:hover {
    background-color: var(--primary-hover);
}

.export-btn svg {
    flex-shrink: 0;
}

.page-textarea {
    width: 100%;
    max-width: 800px;
    /* Restrict width */
    height: 100%;
    border: none;
    resize: none;
    outline: none;
    font-size: 1rem;
    line-height: 1.6;
    color: var(--text-main);
    font-family: inherit;
    background-color: transparent;
    padding: 1rem;
    /* Add some internal padding */
}

/* Files Area */
.files-area {
    flex: 1;
    /* Take remaining space */
    border-top: none;
    display: flex;
    flex-direction: column;
    background-color: #fff;
    min-height: 0;
    /* Important for flex child scrolling */
}

.area-header {
    padding: 0.75rem 2rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border-color);
    background-color: #fff;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.bulk-delete-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.85rem;
    background-color: var(--danger-color);
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.8rem;
    transition: opacity 0.2s, background-color 0.2s;
    font-weight: 500;
    white-space: nowrap;
    flex-shrink: 0;
}

.bulk-delete-btn:hover {
    opacity: 0.9;
}

.bulk-delete-btn.hidden {
    display: none;
}

/* Compact Drop Zone - New Style */
.compact-drop-zone {
    padding: 1rem 2rem;
    border-bottom: 1px solid var(--border-color);
    background-color: #f1f5f9;
    /* Light gray bg for visual separation */
    transition: background 0.2s;
}

.compact-drop-zone.drag-over {
    background-color: var(--accent-bg);
    border-color: var(--primary-color);
}

.drop-row {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.drop-label-text {
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--text-main);
}

.select-file-btn {
    display: inline-block;
    background-color: #fff;
    border: 1px solid #ccc;
    color: var(--text-main);
    padding: 0.25rem 0.75rem;
    font-size: 0.9rem;
    border-radius: 4px;
    /* Slight rounded */
    cursor: pointer;
    font-weight: 500;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
    background: linear-gradient(to bottom, #fff, #f9fafb);
}

.select-file-btn:hover {
    background-color: #f3f4f6;
    border-color: #b0b0b0;
}

.select-file-btn:active {
    background-color: #e5e7eb;
}

.drop-hint-text {
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* URL Input Row */
.url-row {
    margin-top: 0.75rem;
}

.url-input {
    flex: 1;
    padding: 0.4rem 0.75rem;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 0.9rem;
    outline: none;
    transition: border-color 0.2s;
}

.url-input:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}

.add-url-btn {
    background-color: var(--primary-color);
    color: white;
    border: none;
    padding: 0.4rem 1rem;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 500;
    transition: background-color 0.2s;
}

.add-url-btn:hover {
    background-color: var(--primary-hover);
}

.add-url-btn:active {
    transform: translateY(1px);
}

/* Text Input Row */
.text-row {
    margin-top: 0.75rem;
}

.text-input {
    flex: 1;
    padding: 0.4rem 0.75rem;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 0.9rem;
    outline: none;
    transition: border-color 0.2s;
}

.text-input:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}

.add-text-btn {
    background-color: var(--primary-color);
    color: white;
    border: none;
    padding: 0.4rem 1rem;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 500;
    transition: background-color 0.2s;
}

.add-text-btn:hover {
    background-color: var(--primary-hover);
}

.add-text-btn:active {
    transform: translateY(1px);
}



/* Files List */
.files-list-container {
    flex: 1;
    overflow-y: auto;
    padding: 0;
}

.files-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
}

.files-table td {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border-color);
}

.files-table tr:hover {
    background-color: #f1f5f9;
}

.file-name-container {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
}

.file-name-container:hover span {
    color: var(--primary-color);
    text-decoration: underline;
}

.rename-input {
    flex: 1;
    padding: 2px 4px;
    font-size: 0.875rem;
    border: 1px solid var(--primary-color);
    border-radius: 4px;
    outline: none;
    min-width: 0;
}

.rename-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px;
    color: var(--text-muted);
    border-radius: 4px;
    opacity: 0;
    transition: opacity 0.2s, color 0.2s;
    margin-left: 0.5rem;
}

.rename-btn:hover {
    color: var(--primary-color);
    background-color: var(--hover-bg);
}

.file-name-container:hover .rename-btn {
    opacity: 1;
}

.action-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px;
    color: var(--text-muted);
    border-radius: 4px;
}

.action-btn:hover {
    background-color: rgba(0, 0, 0, 0.05);
    color: var(--text-main);
}

.action-btn.delete:hover {
    color: var(--danger-color);
    background-color: #fee2e2;
}

.empty-state {
    padding: 2rem;
    text-align: center;
    color: var(--text-muted);
    display: none;
}

.empty-state.visible {
    display: block;
}

.text-right {
    text-align: right;
}

/* Drag and Drop Styles */
.drag-handle-cell {
    display: none;
}

.file-row {
    transition: background-color 0.2s;
    cursor: grab;
}

.file-row:active {
    cursor: grabbing;
}

.file-row.dragging {
    opacity: 0.5;
    background-color: var(--hover-bg);
}

.file-row:not(.dragging):hover {
    background-color: var(--hover-bg);
}

/* Checkbox cell styles */
.checkbox-cell {
    width: 30px;
    padding: 0.5rem 0.75rem;
    text-align: center;
}

.item-checkbox {
    cursor: pointer;
    width: 16px;
    height: 16px;
    accent-color: var(--primary-color);
}

.drag-handle-cell {
    width: 30px;
    padding: 0.5rem 0.25rem;
    cursor: grab;
}

.drag-handle {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    opacity: 0.5;
    transition: opacity 0.2s;
}

.file-row:hover .drag-handle {
    opacity: 1;
}

.hidden {
    display: none !important;
}

 favicon.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <!-- フォルダが背景を兼ねる -->
  <path d="M0 18h24l6 8h34v38H0z"
        fill="#facc15"/>

  <!-- 輪郭 -->
  <path d="M0 18h24l6 8h34"
        fill="none"
        stroke="#111"
        stroke-width="3"/>
  <rect x="1.5" y="26" width="61" height="36" rx="6"
        fill="none"
        stroke="#111"
        stroke-width="3"/>

  <!-- スロット -->
  <rect x="20" y="42" width="24" height="4" rx="2"
        fill="#111"/>
</svg>