Loading editor/html/css/editor.css +129 −0 Original line number Diff line number Diff line Loading @@ -472,6 +472,135 @@ dialog label { color: var(--bg-primary); } /* Settings Dialog */ .settings-dialog { width: 700px; max-width: 90vw; max-height: 80vh; padding: 0; } .settings-dialog .dialog-actions { padding: 0.8rem 1.2rem; border-top: 1px solid var(--border); } .set-modal { display: flex; min-height: 350px; } .set-sidebar { width: 180px; min-width: 180px; background: var(--bg-primary); border-right: 1px solid var(--border); padding: 1rem 0; } .set-sidebar h3 { padding: 0 1rem 0.8rem; font-size: 0.95rem; color: var(--accent); border-bottom: 1px solid var(--border); margin-bottom: 0.5rem; } .set-nav { list-style: none; padding: 0; margin: 0; } .set-nav-item { padding: 0.5rem 1rem; cursor: pointer; color: var(--text-secondary); font-size: 0.85rem; transition: background 0.15s, color 0.15s; } .set-nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); } .set-nav-item.active { background: var(--bg-secondary); color: var(--accent); border-left: 3px solid var(--accent); } .set-content { flex: 1; padding: 1.2rem; overflow-y: auto; } .set-section h4 { margin-bottom: 1rem; color: var(--text-primary); font-size: 1rem; } .set-field { margin-bottom: 1rem; display: flex; align-items: center; gap: 0.8rem; } .set-field label { min-width: 120px; color: var(--text-secondary); font-size: 0.85rem; } .set-field select, .set-field input { flex: 1; padding: 0.45rem 0.6rem; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 0.9rem; outline: none; } .set-field select:focus, .set-field input:focus { border-color: var(--accent); } .set-save-btn { padding: 0.4rem 1rem; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 0.85rem; } .set-save-btn:hover { background: var(--accent-hover); } .set-status { font-size: 0.8rem; color: var(--text-muted); } .set-status.success { color: var(--success, #a6e3a1); } .set-status.error { color: var(--danger); } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; Loading editor/html/index.html +32 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ <span id="doc-name" class="doc-name">Untitled</span> </div> <div class="toolbar-group" style="margin-left:auto"> <button id="btn-settings" data-i18n-title="Settings">⚙ <span data-i18n="Settings">Einstellungen</span></button> <button id="btn-logout" data-i18n-title="Logout">🚪 <span data-i18n="Logout">Abmelden</span></button> </div> </header> Loading Loading @@ -109,6 +110,37 @@ </div> </dialog> <dialog id="settings-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="Settings">Einstellungen</h3> <ul class="set-nav"> <li class="set-nav-item active" data-section="language" data-i18n="Language">Sprache</li> </ul> </div> <div class="set-content" id="settings-content"> <!-- Language section (default) --> <div id="settings-language" class="set-section"> <h4 data-i18n="Language">Sprache</h4> <div class="set-field"> <label data-i18n="Language">Language / Sprache</label> <select id="settings-lang-select"> <option value="de">Deutsch</option> <option value="en">English</option> </select> </div> <div class="set-field"> <button id="btn-settings-lang-save" class="set-save-btn" data-i18n="save">Speichern</button> <span id="settings-lang-status" class="set-status"></span> </div> </div> </div> </div> <div class="dialog-actions"> <button id="btn-settings-close" data-i18n="Close">Schließen</button> </div> </dialog> <script src="js/i18n.js"></script> <script src="js/api.js"></script> <script src="js/tree.js"></script> Loading editor/html/js/api.js +10 −0 Original line number Diff line number Diff line Loading @@ -153,6 +153,16 @@ var EditorApi = (function() { documentId: documentId, revisionId: revisionId }); }, getSettingsLanguage: function() { return request('GET', '/api/settings/language'); }, setSettingsLanguage: function(language) { return request('POST', '/api/settings/language', { language: language }); } }; })(); editor/html/js/editor.js +64 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ bindToolbar(); bindTreeActions(); bindDialogs(); bindSettingsDialog(); Preview.clear(); DocumentTree.setOnSelect(function(uuid) { Loading Loading @@ -139,6 +140,10 @@ window.location.href = '/login.html'; }); }); document.getElementById('btn-settings').addEventListener('click', function() { openSettings(); }); } // --- Tree action bindings --- Loading Loading @@ -397,6 +402,65 @@ } } // --- Settings --- function openSettings() { EditorApi.getSettingsLanguage().then(function(resp) { var sel = document.getElementById('settings-lang-select'); sel.value = resp.language || 'en'; document.getElementById('settings-lang-status').textContent = ''; document.getElementById('settings-dialog').showModal(); }).catch(function(err) { alert(I18n.t('I18N_ACTION_FAILED') + ': ' + (err.error || '')); }); } function bindSettingsDialog() { document.getElementById('btn-settings-close').addEventListener('click', function() { document.getElementById('settings-dialog').close(); }); document.getElementById('btn-settings-lang-save').addEventListener('click', function() { var lang = document.getElementById('settings-lang-select').value; var status = document.getElementById('settings-lang-status'); status.textContent = I18n.t('I18N_SAVING', 'Saving...'); EditorApi.setSettingsLanguage(lang).then(function() { status.textContent = I18n.t('I18N_SAVED', 'Saved'); status.className = 'set-status success'; // Clear lang cookie so DB setting takes effect document.cookie = 'lang=;path=/;max-age=0'; // Reload translations and re-translate page I18n.load(function() { I18n.translatePage(); // Clear status after re-translate setTimeout(function() { status.textContent = ''; status.className = 'set-status'; }, 2000); }); }).catch(function(err) { status.textContent = I18n.t('I18N_SAVE_FAILED', 'Save failed'); status.className = 'set-status error'; }); }); // Nav section switching var navItems = document.querySelectorAll('.set-nav-item'); for (var i = 0; i < navItems.length; i++) { navItems[i].addEventListener('click', function() { for (var j = 0; j < navItems.length; j++) navItems[j].classList.remove('active'); this.classList.add('active'); var sections = document.querySelectorAll('.set-section'); for (var k = 0; k < sections.length; k++) sections[k].style.display = 'none'; var target = document.getElementById('settings-' + this.getAttribute('data-section')); if (target) target.style.display = 'block'; }); } } // --- Keyboard shortcuts --- document.addEventListener('keydown', function(e) { Loading editor/html/login.html +56 −5 Original line number Diff line number Diff line Loading @@ -87,10 +87,34 @@ font-size: 0.85rem; display: none; } .lang-switcher { position: absolute; top: 1rem; right: 1rem; } .lang-switcher select { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem 0.6rem; font-size: 0.85rem; cursor: pointer; outline: none; } .lang-switcher select:focus { border-color: var(--accent); } </style> </head> <body> <div class="login-container"> <div class="lang-switcher"> <select id="lang-select"> <option value="de">Deutsch</option> <option value="en">English</option> </select> </div> <div class="login-box"> <h1>blogi editor</h1> <p class="subtitle" data-i18n="Login">Sign in with your authdb account</p> Loading @@ -113,7 +137,34 @@ <script src="js/i18n.js"></script> <script> (function() { I18n.load(function() { I18n.translatePage(); }); var langSelect = document.getElementById('lang-select'); // Read current lang cookie function getLangCookie() { var m = document.cookie.match(/(?:^|;\s*)lang=([^;]*)/); return m ? m[1] : ''; } function setLangCookie(val) { document.cookie = 'lang=' + val + ';path=/;max-age=31536000;SameSite=Strict'; } // Set initial selection from cookie var cur = getLangCookie(); if (cur) langSelect.value = cur; I18n.load(function() { I18n.translatePage(); // Sync select to actual loaded language langSelect.value = I18n.getLang(); }); langSelect.addEventListener('change', function() { setLangCookie(this.value); // Reload i18n and re-translate I18n.load(function() { I18n.translatePage(); }); }); var form = document.getElementById('login-form'); var errBox = document.getElementById('login-error'); Loading @@ -123,7 +174,7 @@ e.preventDefault(); errBox.style.display = 'none'; btn.disabled = true; btn.textContent = 'Signing in...'; btn.textContent = I18n.t('Loading...', 'Signing in...'); var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/login', true); Loading @@ -131,7 +182,7 @@ xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; btn.disabled = false; btn.textContent = 'Sign in'; btn.textContent = I18n.t('Submit', 'Sign in'); if (xhr.status === 200) { window.location.href = '/'; Loading @@ -147,8 +198,8 @@ }; xhr.onerror = function() { btn.disabled = false; btn.textContent = 'Sign in'; errBox.textContent = 'Network error'; btn.textContent = I18n.t('Submit', 'Sign in'); errBox.textContent = I18n.t('Error:', 'Network error'); errBox.style.display = 'block'; }; xhr.send(JSON.stringify({ Loading Loading
editor/html/css/editor.css +129 −0 Original line number Diff line number Diff line Loading @@ -472,6 +472,135 @@ dialog label { color: var(--bg-primary); } /* Settings Dialog */ .settings-dialog { width: 700px; max-width: 90vw; max-height: 80vh; padding: 0; } .settings-dialog .dialog-actions { padding: 0.8rem 1.2rem; border-top: 1px solid var(--border); } .set-modal { display: flex; min-height: 350px; } .set-sidebar { width: 180px; min-width: 180px; background: var(--bg-primary); border-right: 1px solid var(--border); padding: 1rem 0; } .set-sidebar h3 { padding: 0 1rem 0.8rem; font-size: 0.95rem; color: var(--accent); border-bottom: 1px solid var(--border); margin-bottom: 0.5rem; } .set-nav { list-style: none; padding: 0; margin: 0; } .set-nav-item { padding: 0.5rem 1rem; cursor: pointer; color: var(--text-secondary); font-size: 0.85rem; transition: background 0.15s, color 0.15s; } .set-nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); } .set-nav-item.active { background: var(--bg-secondary); color: var(--accent); border-left: 3px solid var(--accent); } .set-content { flex: 1; padding: 1.2rem; overflow-y: auto; } .set-section h4 { margin-bottom: 1rem; color: var(--text-primary); font-size: 1rem; } .set-field { margin-bottom: 1rem; display: flex; align-items: center; gap: 0.8rem; } .set-field label { min-width: 120px; color: var(--text-secondary); font-size: 0.85rem; } .set-field select, .set-field input { flex: 1; padding: 0.45rem 0.6rem; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 0.9rem; outline: none; } .set-field select:focus, .set-field input:focus { border-color: var(--accent); } .set-save-btn { padding: 0.4rem 1rem; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 0.85rem; } .set-save-btn:hover { background: var(--accent-hover); } .set-status { font-size: 0.8rem; color: var(--text-muted); } .set-status.success { color: var(--success, #a6e3a1); } .set-status.error { color: var(--danger); } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; Loading
editor/html/index.html +32 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ <span id="doc-name" class="doc-name">Untitled</span> </div> <div class="toolbar-group" style="margin-left:auto"> <button id="btn-settings" data-i18n-title="Settings">⚙ <span data-i18n="Settings">Einstellungen</span></button> <button id="btn-logout" data-i18n-title="Logout">🚪 <span data-i18n="Logout">Abmelden</span></button> </div> </header> Loading Loading @@ -109,6 +110,37 @@ </div> </dialog> <dialog id="settings-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="Settings">Einstellungen</h3> <ul class="set-nav"> <li class="set-nav-item active" data-section="language" data-i18n="Language">Sprache</li> </ul> </div> <div class="set-content" id="settings-content"> <!-- Language section (default) --> <div id="settings-language" class="set-section"> <h4 data-i18n="Language">Sprache</h4> <div class="set-field"> <label data-i18n="Language">Language / Sprache</label> <select id="settings-lang-select"> <option value="de">Deutsch</option> <option value="en">English</option> </select> </div> <div class="set-field"> <button id="btn-settings-lang-save" class="set-save-btn" data-i18n="save">Speichern</button> <span id="settings-lang-status" class="set-status"></span> </div> </div> </div> </div> <div class="dialog-actions"> <button id="btn-settings-close" data-i18n="Close">Schließen</button> </div> </dialog> <script src="js/i18n.js"></script> <script src="js/api.js"></script> <script src="js/tree.js"></script> Loading
editor/html/js/api.js +10 −0 Original line number Diff line number Diff line Loading @@ -153,6 +153,16 @@ var EditorApi = (function() { documentId: documentId, revisionId: revisionId }); }, getSettingsLanguage: function() { return request('GET', '/api/settings/language'); }, setSettingsLanguage: function(language) { return request('POST', '/api/settings/language', { language: language }); } }; })();
editor/html/js/editor.js +64 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ bindToolbar(); bindTreeActions(); bindDialogs(); bindSettingsDialog(); Preview.clear(); DocumentTree.setOnSelect(function(uuid) { Loading Loading @@ -139,6 +140,10 @@ window.location.href = '/login.html'; }); }); document.getElementById('btn-settings').addEventListener('click', function() { openSettings(); }); } // --- Tree action bindings --- Loading Loading @@ -397,6 +402,65 @@ } } // --- Settings --- function openSettings() { EditorApi.getSettingsLanguage().then(function(resp) { var sel = document.getElementById('settings-lang-select'); sel.value = resp.language || 'en'; document.getElementById('settings-lang-status').textContent = ''; document.getElementById('settings-dialog').showModal(); }).catch(function(err) { alert(I18n.t('I18N_ACTION_FAILED') + ': ' + (err.error || '')); }); } function bindSettingsDialog() { document.getElementById('btn-settings-close').addEventListener('click', function() { document.getElementById('settings-dialog').close(); }); document.getElementById('btn-settings-lang-save').addEventListener('click', function() { var lang = document.getElementById('settings-lang-select').value; var status = document.getElementById('settings-lang-status'); status.textContent = I18n.t('I18N_SAVING', 'Saving...'); EditorApi.setSettingsLanguage(lang).then(function() { status.textContent = I18n.t('I18N_SAVED', 'Saved'); status.className = 'set-status success'; // Clear lang cookie so DB setting takes effect document.cookie = 'lang=;path=/;max-age=0'; // Reload translations and re-translate page I18n.load(function() { I18n.translatePage(); // Clear status after re-translate setTimeout(function() { status.textContent = ''; status.className = 'set-status'; }, 2000); }); }).catch(function(err) { status.textContent = I18n.t('I18N_SAVE_FAILED', 'Save failed'); status.className = 'set-status error'; }); }); // Nav section switching var navItems = document.querySelectorAll('.set-nav-item'); for (var i = 0; i < navItems.length; i++) { navItems[i].addEventListener('click', function() { for (var j = 0; j < navItems.length; j++) navItems[j].classList.remove('active'); this.classList.add('active'); var sections = document.querySelectorAll('.set-section'); for (var k = 0; k < sections.length; k++) sections[k].style.display = 'none'; var target = document.getElementById('settings-' + this.getAttribute('data-section')); if (target) target.style.display = 'block'; }); } } // --- Keyboard shortcuts --- document.addEventListener('keydown', function(e) { Loading
editor/html/login.html +56 −5 Original line number Diff line number Diff line Loading @@ -87,10 +87,34 @@ font-size: 0.85rem; display: none; } .lang-switcher { position: absolute; top: 1rem; right: 1rem; } .lang-switcher select { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem 0.6rem; font-size: 0.85rem; cursor: pointer; outline: none; } .lang-switcher select:focus { border-color: var(--accent); } </style> </head> <body> <div class="login-container"> <div class="lang-switcher"> <select id="lang-select"> <option value="de">Deutsch</option> <option value="en">English</option> </select> </div> <div class="login-box"> <h1>blogi editor</h1> <p class="subtitle" data-i18n="Login">Sign in with your authdb account</p> Loading @@ -113,7 +137,34 @@ <script src="js/i18n.js"></script> <script> (function() { I18n.load(function() { I18n.translatePage(); }); var langSelect = document.getElementById('lang-select'); // Read current lang cookie function getLangCookie() { var m = document.cookie.match(/(?:^|;\s*)lang=([^;]*)/); return m ? m[1] : ''; } function setLangCookie(val) { document.cookie = 'lang=' + val + ';path=/;max-age=31536000;SameSite=Strict'; } // Set initial selection from cookie var cur = getLangCookie(); if (cur) langSelect.value = cur; I18n.load(function() { I18n.translatePage(); // Sync select to actual loaded language langSelect.value = I18n.getLang(); }); langSelect.addEventListener('change', function() { setLangCookie(this.value); // Reload i18n and re-translate I18n.load(function() { I18n.translatePage(); }); }); var form = document.getElementById('login-form'); var errBox = document.getElementById('login-error'); Loading @@ -123,7 +174,7 @@ e.preventDefault(); errBox.style.display = 'none'; btn.disabled = true; btn.textContent = 'Signing in...'; btn.textContent = I18n.t('Loading...', 'Signing in...'); var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/login', true); Loading @@ -131,7 +182,7 @@ xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; btn.disabled = false; btn.textContent = 'Sign in'; btn.textContent = I18n.t('Submit', 'Sign in'); if (xhr.status === 200) { window.location.href = '/'; Loading @@ -147,8 +198,8 @@ }; xhr.onerror = function() { btn.disabled = false; btn.textContent = 'Sign in'; errBox.textContent = 'Network error'; btn.textContent = I18n.t('Submit', 'Sign in'); errBox.textContent = I18n.t('Error:', 'Network error'); errBox.style.display = 'block'; }; xhr.send(JSON.stringify({ Loading