mirror of
https://github.com/abort-retry-ignore/joplock.git
synced 2026-10-19 02:09:26 +00:00
font choices, flushSave robustness, nav toggle fix
Some checks failed
Build and push Joplock image / build-and-push (push) Has been cancelled
Some checks failed
Build and push Joplock image / build-and-push (push) Has been cancelled
- Add rounded + humanist font families with expanded fallback stacks - Refactor flushSave() from minified one-liner: watchdog timeout, mutex for encrypted autosave coordination, proper cleanup on all paths - Fix openNavFolderAndFirstNote so already-expanded folders can collapse - Service worker cache v46 -> v47
This commit is contained in:
parent
44a71039d7
commit
965e8f9ff4
6 changed files with 132 additions and 21 deletions
|
|
@ -217,7 +217,7 @@ const normalizeSettings = settings => ({
|
|||
autoLogout: !!Number(settings.autoLogout) || settings.autoLogout === true || settings.autoLogout === '1',
|
||||
autoLogoutMinutes: normalizeInteger(settings.autoLogoutMinutes, defaultSettings.autoLogoutMinutes, 1, 480),
|
||||
theme: validThemes.includes(settings.theme) ? settings.theme : defaultSettings.theme,
|
||||
noteFontFamily: (settings.noteFontFamily === 'mono' || settings.noteFontFamily === 'serif') ? settings.noteFontFamily : defaultSettings.noteFontFamily,
|
||||
noteFontFamily: (['mono', 'serif', 'rounded', 'humanist'].includes(settings.noteFontFamily)) ? settings.noteFontFamily : defaultSettings.noteFontFamily,
|
||||
newlineBehavior: (settings.newlineBehavior === 'linebreak' || settings.newlineBehavior === 'invert') ? settings.newlineBehavior : defaultSettings.newlineBehavior,
|
||||
liveSearch: !!Number(settings.liveSearch) || settings.liveSearch === true || settings.liveSearch === '1',
|
||||
highlightActiveLine: settings.highlightActiveLine !== false && settings.highlightActiveLine !== '0' && settings.highlightActiveLine !== 0,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ const ASSET_VERSION = '20260706cm6-markdown2';
|
|||
|
||||
const noteFontFamilyCSS = (settings) => {
|
||||
const f = settings.noteFontFamily || 'sans';
|
||||
return f === 'mono' ? "'Cascadia Mono',monospace" : f === 'serif' ? "Georgia,'Times New Roman',serif" : '';
|
||||
if (f === 'mono') return "'Cascadia Mono','SF Mono',Consolas,'Liberation Mono',Menlo,monospace";
|
||||
if (f === 'serif') return "Georgia,'Times New Roman',Times,serif";
|
||||
if (f === 'rounded') return "'SF Pro Rounded',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif";
|
||||
if (f === 'humanist') return "Optima,Candara,'Segoe UI',Calibri,sans-serif";
|
||||
return "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue',sans-serif";
|
||||
};
|
||||
|
||||
const formatBytes = value => {
|
||||
|
|
@ -56,7 +60,7 @@ const layoutPage = (options = {}) => {
|
|||
<link rel="stylesheet" href="/styles.css?v=${ASSET_VERSION}" />
|
||||
<title>Joplock</title>
|
||||
</head>
|
||||
<body class="theme-dark-grey${noteFontFamilyCSS(settings)==="'Cascadia Mono',monospace"?' note-body-monospace':''}" style="--font-family-note:${noteFontFamilyCSS(settings)};--font-size-note:${escapeHtml(settings.noteFontSize || 15)}px;--font-size-note-mobile:${escapeHtml(settings.mobileNoteFontSize || ((settings.noteFontSize || 15) + 2))}px;--font-size-code:${escapeHtml(settings.codeFontSize || 12)}px;--font-size-markdown:${escapeHtml(settings.markdownFontSize || 14)}px;">
|
||||
<body class="theme-dark-grey${noteFontFamilyCSS(settings)==="'Cascadia Mono','SF Mono',Consolas,'Liberation Mono',Menlo,monospace"?' note-body-monospace':''}" style="--font-family-note:${noteFontFamilyCSS(settings)};--font-size-note:${escapeHtml(settings.noteFontSize || 15)}px;--font-size-note-mobile:${escapeHtml(settings.mobileNoteFontSize || ((settings.noteFontSize || 15) + 2))}px;--font-size-code:${escapeHtml(settings.codeFontSize || 12)}px;--font-size-markdown:${escapeHtml(settings.markdownFontSize || 14)}px;">
|
||||
<script>
|
||||
(function(){
|
||||
var keys=['joplock-theme','joplock-nav-collapsed','joplock-nav-folders'];
|
||||
|
|
@ -108,7 +112,7 @@ const layoutPage = (options = {}) => {
|
|||
<script src="/app.js?v=${ASSET_VERSION}" defer></script>
|
||||
<title>Joplock</title>
|
||||
</head>
|
||||
<body class="app-shell theme-${escapeHtml(settings.theme || 'matrix')}${noteFontFamilyCSS(settings)==="'Cascadia Mono',monospace"?' note-body-monospace':''}${settings.uiMode === 'mobile' ? ' force-mobile' : ''}${settings.uiMode === 'desktop' ? ' force-desktop' : ''}" style="--font-family-note:${noteFontFamilyCSS(settings)};--font-size-note:${escapeHtml(settings.noteFontSize || 15)}px;--font-size-note-mobile:${escapeHtml(settings.mobileNoteFontSize || ((settings.noteFontSize || 15) + 2))}px;--font-size-code:${escapeHtml(settings.codeFontSize || 12)}px;--font-size-markdown:${escapeHtml(settings.markdownFontSize || 14)}px;" data-newline-behavior="${escapeHtml(settings.newlineBehavior || 'linebreak')}">
|
||||
<body class="app-shell theme-${escapeHtml(settings.theme || 'matrix')}${noteFontFamilyCSS(settings)==="'Cascadia Mono','SF Mono',Consolas,'Liberation Mono',Menlo,monospace"?' note-body-monospace':''}${settings.uiMode === 'mobile' ? ' force-mobile' : ''}${settings.uiMode === 'desktop' ? ' force-desktop' : ''}" style="--font-family-note:${noteFontFamilyCSS(settings)};--font-size-note:${escapeHtml(settings.noteFontSize || 15)}px;--font-size-note-mobile:${escapeHtml(settings.mobileNoteFontSize || ((settings.noteFontSize || 15) + 2))}px;--font-size-code:${escapeHtml(settings.codeFontSize || 12)}px;--font-size-markdown:${escapeHtml(settings.markdownFontSize || 14)}px;" data-newline-behavior="${escapeHtml(settings.newlineBehavior || 'linebreak')}">
|
||||
<div id="note-loading-overlay" aria-hidden="true">
|
||||
<div class="note-loading-ring"></div>
|
||||
<div class="note-loading-label">Loading note…</div>
|
||||
|
|
|
|||
|
|
@ -207,8 +207,10 @@ const settingsPage = (options = {}) => {
|
|||
<label class="settings-field">
|
||||
<span>Note font</span>
|
||||
<select id="settings-note-font-family" class="login-input" onchange="saveSetting('noteFontFamily',this.value)">
|
||||
<option value="sans"${!settings.noteFontFamily||settings.noteFontFamily==='sans'?' selected':''}>System (sans-serif)</option>
|
||||
<option value="serif"${settings.noteFontFamily==='serif'?' selected':''}>Serif</option>
|
||||
<option value="sans"${!settings.noteFontFamily||settings.noteFontFamily==='sans'?' selected':''}>System Sans</option>
|
||||
<option value="serif"${settings.noteFontFamily==='serif'?' selected':''}>System Serif</option>
|
||||
<option value="rounded"${settings.noteFontFamily==='rounded'?' selected':''}>Rounded</option>
|
||||
<option value="humanist"${settings.noteFontFamily==='humanist'?' selected':''}>Humanist</option>
|
||||
<option value="mono"${settings.noteFontFamily==='mono'?' selected':''}>Monospace (Cascadia Mono)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -599,7 +601,7 @@ const settingsPage = (options = {}) => {
|
|||
function dlog(){if(DEBUG){try{console.log.apply(console,['[joplock]'].concat([].slice.call(arguments)))}catch(_){}}}
|
||||
dlog('settings script loaded');
|
||||
function bindRange(inputId,valueId,cssVar){var input=document.getElementById(inputId);var value=document.getElementById(valueId);if(!input||!value)return;document.body.style.setProperty(cssVar,input.value+'px');value.textContent=input.value+'px';input.addEventListener('input',function(){document.body.style.setProperty(cssVar,this.value+'px');value.textContent=this.value+'px'})}
|
||||
function bindFont(){var input=document.getElementById('settings-note-font-family');if(!input)return;function apply(v){document.body.style.setProperty('--font-family-note',v==='mono'?"'Cascadia Mono',monospace":v==='serif'?"Georgia,'Times New Roman',serif":'');document.body.classList.toggle('note-body-monospace',v==='mono')}apply(input.value);input.addEventListener('change',function(){apply(this.value)})}
|
||||
function bindFont(){var input=document.getElementById('settings-note-font-family');if(!input)return;function apply(v){var stacks={mono:"'Cascadia Mono','SF Mono',Consolas,'Liberation Mono',Menlo,monospace",serif:"Georgia,'Times New Roman',Times,serif",rounded:"'SF Pro Rounded',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif",humanist:"Optima,Candara,'Segoe UI',Calibri,sans-serif",sans:"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue',sans-serif"};document.body.style.setProperty('--font-family-note',stacks[v]||'');document.body.classList.toggle('note-body-monospace',v==='mono')}apply(input.value);input.addEventListener('change',function(){apply(this.value)})}
|
||||
bindRange('settings-note-font','settings-note-font-value','--font-size-note');
|
||||
bindRange('settings-markdown-font','settings-markdown-font-value','--font-size-markdown');
|
||||
bindRange('settings-mobile-note-font','settings-mobile-note-font-value','--font-size-note-mobile');
|
||||
|
|
|
|||
129
public/app.js
129
public/app.js
|
|
@ -348,24 +348,41 @@ function openNavFolderAndFirstNote(id){
|
|||
if(isMobileShellMode())return;
|
||||
var el=document.querySelector('.nav-folder[data-folder-id="'+id.replace(/"/g,'\\"')+'"]');
|
||||
if(!el)return;
|
||||
var alreadyOpen=!el.classList.contains('collapsed');
|
||||
toggleNavFolder(id,true);
|
||||
// After notes are loaded, click the first one
|
||||
function clickFirst(notesDiv){
|
||||
var first=notesDiv.querySelector('.notelist-item');
|
||||
if(first)first.click();
|
||||
if(!el.classList.contains('collapsed')){
|
||||
// Folder is already expanded — this must be a real toggle (collapse),
|
||||
// not another forced-open. Without this branch, toggleNavFolder(id,true)
|
||||
// below always forces collapsed=false, so an open folder like "Examples"
|
||||
// could never be closed again via its title text (only the tiny chevron
|
||||
// button, which calls toggleNavFolder with no force arg, worked).
|
||||
toggleNavFolder(id,false);
|
||||
return;
|
||||
}
|
||||
var notesDiv=el.querySelector('.nav-folder-notes[data-folder-id]');
|
||||
// Capture BEFORE toggling: toggleNavFolder() sets data-loaded="1" itself
|
||||
// (synchronously, before its htmx fetch) whenever it lazy-loads notes, so
|
||||
// checking data-loaded only after the call below can't distinguish
|
||||
// "already loaded from a previous expand" from "just started loading now".
|
||||
var wasLoaded=!!(notesDiv&¬esDiv.getAttribute('data-loaded'));
|
||||
toggleNavFolder(id,true);
|
||||
// After notes are loaded, click the first one
|
||||
function clickFirst(){
|
||||
var first=notesDiv&¬esDiv.querySelector('.notelist-item');
|
||||
if(first)first.click();
|
||||
}
|
||||
if(!notesDiv)return;
|
||||
if(alreadyOpen&¬esDiv.getAttribute('data-loaded')){
|
||||
// Notes already present — click immediately
|
||||
clickFirst(notesDiv);
|
||||
if(wasLoaded){
|
||||
// Notes were already loaded from an earlier expand (e.g. this folder was
|
||||
// last collapsed via the chevron button rather than reloaded) —
|
||||
// toggleNavFolder() won't issue an htmx fetch, so no htmx:afterSettle
|
||||
// will ever fire for this notesDiv. Click immediately instead.
|
||||
clickFirst();
|
||||
} else {
|
||||
// Wait for htmx swap into this notesDiv to settle, then click first note
|
||||
// First expand: wait for the lazy-load htmx swap into this notesDiv to
|
||||
// settle, then click first note.
|
||||
function onSettle(e){
|
||||
if(e.detail&&e.detail.target===notesDiv){
|
||||
document.body.removeEventListener('htmx:afterSettle',onSettle);
|
||||
clickFirst(notesDiv);
|
||||
clickFirst();
|
||||
}
|
||||
}
|
||||
document.body.addEventListener('htmx:afterSettle',onSettle);
|
||||
|
|
@ -3368,7 +3385,75 @@ document.addEventListener('visibilitychange',function(){if(document.hidden){_log
|
|||
window.addEventListener('load',function(){if(isMobileShellMode())return;initNavPanel();initEditorPanel()});
|
||||
window.addEventListener('resize',applyMobileTitleMode);
|
||||
document.addEventListener('keydown',function(e){var mac=navigator.platform&&navigator.platform.indexOf('Mac')!==-1;var mod=mac?e.metaKey:e.ctrlKey;if(mod&&e.shiftKey&&e.key.toLowerCase()==='z'){e.preventDefault();undoSnapshot()}});
|
||||
function flushSave(callback){var form=activeEditorForm();if(!form){_log('flushSave skip (no form)');if(callback)callback(true);return}if(_saveTimer){clearTimeout(_saveTimer);_saveTimer=null}if(_saveTitleTimer){clearTimeout(_saveTitleTimer);_saveTitleTimer=null}if(_pvSyncTimer){clearTimeout(_pvSyncTimer);_pvSyncTimer=null;_syncPVInFlight=true;syncPV();_syncPVInFlight=false}else{var pv=getPV();if(pv&&_previewDirty)syncPV();else if(!pv&&_editorMode!=='markdown'&&_editorMode!=='md')tinyMCESyncToTA()}syncTitleToHidden({silent:true});var h=formHash(form);if(h===_savedHash){_log('flushSave skip (hash unchanged)',h);if(callback)callback(true);return}setSaveState('<span class="autosave-saving">Saving...</span>','Saving...');var restoreReq=function(){};buildFlushRequest(form).then(function(req){if(!req){if(callback)callback(true);return}restoreReq=req.restore||restoreReq;_log('flushSave',req.url);return fetch(req.url,{method:'PUT',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:req.body}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(function(html){restoreReq();_log('flushSave ok',html.slice(0,80));snapshotHash();window._mobileNewNoteId=null;setSaveState('<span class="autosave-ok">Saved</span>','Saved');if(callback)callback(true)})}).catch(function(err){restoreReq();_log('flushSave error',err);showOffline();if(callback)callback(false)})}
|
||||
function flushSave(callback){
|
||||
var form=activeEditorForm();
|
||||
if(!form){_log('flushSave skip (no form)');if(callback)callback(true);return}
|
||||
if(_saveTimer){clearTimeout(_saveTimer);_saveTimer=null}
|
||||
if(_saveTitleTimer){clearTimeout(_saveTitleTimer);_saveTitleTimer=null}
|
||||
if(_pvSyncTimer){clearTimeout(_pvSyncTimer);_pvSyncTimer=null;_syncPVInFlight=true;syncPV();_syncPVInFlight=false}else{var pv=getPV();if(pv&&_previewDirty)syncPV();else if(!pv&&_editorMode!=='markdown'&&_editorMode!=='md')tinyMCESyncToTA()}
|
||||
syncTitleToHidden({silent:true});
|
||||
var run=function(){
|
||||
var h=formHash(form);
|
||||
if(h===_savedHash){_log('flushSave skip (hash unchanged)',h);if(callback)callback(true);return}
|
||||
setSaveState('<span class="autosave-saving">Saving...</span>','Saving...');
|
||||
var restoreReq=function(){};
|
||||
var settled=false;
|
||||
var p;
|
||||
// finish(): the one place that clears the watchdog, releases the
|
||||
// _flushSaveInFlight mutex, and invokes callback exactly once — used
|
||||
// on success, on error, AND on watchdog timeout. Without this, a
|
||||
// fetch() that never settles (dropped connection, server hang) would
|
||||
// (a) leave #note-body's name="body" stripped forever (formHash()
|
||||
// then silently ignores all future body edits — note stuck "Edited",
|
||||
// nothing ever saves again) and (b) never invoke callback, which
|
||||
// permanently freezes the nav-intercept click handler below (clicking
|
||||
// another note in the list would do nothing).
|
||||
var finish=function(ok){
|
||||
if(settled)return;
|
||||
settled=true;
|
||||
clearTimeout(watchdog);
|
||||
if(_flushSaveInFlight===p)_flushSaveInFlight=null;
|
||||
if(callback)callback(ok);
|
||||
};
|
||||
var watchdog=setTimeout(function(){
|
||||
_log('flushSave watchdog fired, forcing restore');
|
||||
restoreReq();
|
||||
showOffline();
|
||||
finish(false);
|
||||
},20000);
|
||||
p=buildFlushRequest(form).then(function(req){
|
||||
if(settled)return;
|
||||
if(!req){finish(true);return}
|
||||
restoreReq=req.restore||restoreReq;
|
||||
_log('flushSave',req.url);
|
||||
return fetch(req.url,{method:'PUT',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:req.body}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.text()}).then(function(html){
|
||||
if(settled)return;
|
||||
restoreReq();
|
||||
_log('flushSave ok',html.slice(0,80));
|
||||
snapshotHash();
|
||||
window._mobileNewNoteId=null;
|
||||
setSaveState('<span class="autosave-ok">Saved</span>','Saved');
|
||||
finish(true);
|
||||
});
|
||||
}).catch(function(err){
|
||||
if(settled)return;
|
||||
restoreReq();
|
||||
_log('flushSave error',err);
|
||||
showOffline();
|
||||
finish(false);
|
||||
});
|
||||
_flushSaveInFlight=p;
|
||||
};
|
||||
// If an encrypted autosave already owns the textarea's temporary
|
||||
// body-swap (_setOneShotEncryptedBody), wait for it instead of racing a
|
||||
// second swap on the same element — see _encryptedAutosaveInFlight.
|
||||
if(_encryptedAutosaveInFlight){
|
||||
_log('flushSave waiting for in-flight encrypted autosave');
|
||||
_encryptedAutosaveInFlight.then(run,run);
|
||||
}else{
|
||||
run();
|
||||
}
|
||||
}
|
||||
function shouldInterceptNavigationClick(target){var navTarget=target&&target.closest?target.closest('.notelist-item,.sidebar-item,.nav-folder-row,[hx-get],[hx-post],[hx-delete]'):null;if(!navTarget)return null;if(navTarget.closest&&navTarget.closest('#note-editor-form'))return null;if(navTarget.closest&&navTarget.closest('#folder-context-menu,#folder-modal,#link-modal,#history-modal,#code-modal,#new-folder-modal,#vault-modal,#empty-trash-modal'))return null;return navTarget}
|
||||
document.addEventListener('click',function(e){var navTarget=shouldInterceptNavigationClick(e.target);if(!navTarget)return;var form=activeEditorForm();var status=queryActiveEditor('#autosave-status');var dirty=status&&status.querySelector('.autosave-edited');if(!form||!dirty)return;_log('navigation click intercepted, flushing save',navTarget.className||navTarget.id||navTarget.tagName);e.preventDefault();e.stopImmediatePropagation();flushSave(function(saved){if(saved){_log('flushSave done, re-clicking navigation target');navTarget.click()}})},true);
|
||||
window.joplockLiveSearch=_cfg.liveSearch||false;
|
||||
|
|
@ -4388,6 +4473,18 @@ function _setOneShotEncryptedBody(form,ciphertext){
|
|||
};
|
||||
}
|
||||
|
||||
// Mutex guarding #note-body's temporary name-swap (_setOneShotEncryptedBody):
|
||||
// the debounced autosave (_triggerEncryptedSave) and the forced flush
|
||||
// (flushSave/buildFlushRequest) both rename-then-restore the SAME textarea
|
||||
// while encrypting. If both ran concurrently, one's restore() could be
|
||||
// skipped/duplicated and name="body" could be left stripped permanently —
|
||||
// formHash() only hashes named elements, so body edits would then be
|
||||
// invisible to it forever ("hash unchanged" on every keystroke, note stuck
|
||||
// on "Edited", edit silently never saved). These flags let each side wait
|
||||
// for the other instead of racing it on the same element.
|
||||
var _encryptedAutosaveInFlight=null; // Promise<boolean> while _triggerEncryptedSave owns the swap
|
||||
var _flushSaveInFlight=null; // Promise<boolean> while flushSave owns the swap
|
||||
|
||||
function _triggerEncryptedSave(form,ciphertext){
|
||||
if(!form)return;
|
||||
// Cancel any pending plaintext autosave (e.g. from the change event that
|
||||
|
|
@ -4399,6 +4496,8 @@ function _triggerEncryptedSave(form,ciphertext){
|
|||
var restore=_setOneShotEncryptedBody(form,ciphertext);
|
||||
var done=false;
|
||||
var cleanupTimer=null;
|
||||
var resolveInFlight=function(){};
|
||||
_encryptedAutosaveInFlight=new Promise(function(res){resolveInFlight=res});
|
||||
var cleanup=function(success){
|
||||
if(done)return;
|
||||
done=true;
|
||||
|
|
@ -4410,6 +4509,8 @@ function _triggerEncryptedSave(form,ciphertext){
|
|||
// Re-snapshot the form hash after the textarea regains its name attr,
|
||||
// so a subsequent debounced scheduleSave compares apples to apples.
|
||||
if(success)snapshotHash();
|
||||
_encryptedAutosaveInFlight=null;
|
||||
resolveInFlight(success);
|
||||
};
|
||||
var onDone=function(e){
|
||||
if(e.target!==form)return;
|
||||
|
|
@ -4462,6 +4563,10 @@ scheduleSave=function(){
|
|||
_saveTimer=null;
|
||||
if(_syncPVInFlight||_pvSyncTimer){scheduleSave();return}
|
||||
if(_anyModalOpen()){scheduleSave();return}
|
||||
// A forced flushSave() (e.g. navigating to another note) already owns
|
||||
// the textarea's temporary name-swap — don't race it with a second
|
||||
// encrypt+swap on the same element. Re-check once it's done.
|
||||
if(_flushSaveInFlight){_log('encrypted scheduleSave deferred, flush in flight');scheduleSave();return}
|
||||
if(!form)return;
|
||||
var h=formHash(form);
|
||||
if(h===_savedHash){_log('encrypted scheduleSave skip, hash unchanged',h);return}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const CACHE_NAME = 'joplock-shell-v46-20260708greytheme3';
|
||||
const CACHE_NAME = 'joplock-shell-v47-20260708fontchoices';
|
||||
const STATIC_ASSETS = ['/styles.css', '/htmx.min.js', '/turndown.min.js', '/codemirror.min.js', '/hljs.min.js', '/manifest.webmanifest', '/icon.svg', '/icon-192.png', '/icon-512.png', '/maskable-icon-192.png', '/maskable-icon-512.png', '/apple-touch-icon.png', '/fonts/CascadiaMono-Regular.woff2', '/fonts/CascadiaMono-Bold.woff2'];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@ body.mobile-shell-active {
|
|||
|
||||
body.note-body-monospace,
|
||||
body.note-body-monospace .editor-preview {
|
||||
font-family: 'Cascadia Mono', monospace;
|
||||
font-family: 'Cascadia Mono','SF Mono',Consolas,'Liberation Mono',Menlo,monospace;
|
||||
}
|
||||
.editor-preview {
|
||||
font-family: var(--font-family-note, inherit);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue