Refactor Journals UI and enhance CodeMirror editor

- Added formatting toolbar to journal entry editor with support for bold, italic, headers, and lists.
- Standardized iconography to Lucide across Journals module, removing legacy FontAwesome.
- Improved responsiveness and dark mode compatibility in layout and list views.
- Refactored CodeMirror component to support external control via editorView binding.
- Hardened security by removing unnecessary @html tags in journal names.
This commit is contained in:
Scott Idem
2026-01-08 14:39:26 -05:00
parent 480094305c
commit 01ced00f78
6 changed files with 122 additions and 22 deletions

View File

@@ -0,0 +1,60 @@
import type { EditorView } from '@codemirror/view';
/**
* Wraps the current selection in CodeMirror with the given prefix and suffix.
*/
export function wrapSelection(view: EditorView, prefix: string, suffix: string = prefix) {
if (!view) return;
const { state, dispatch } = view;
const changes = state.changeByRange((range) => {
const selectedText = state.doc.sliceString(range.from, range.to);
return {
changes: [
{ from: range.from, insert: prefix },
{ from: range.to, insert: suffix }
],
range: {
from: range.from + prefix.length,
to: range.to + prefix.length
}
};
});
dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input' }));
view.focus();
}
/**
* Inserts a prefix at the start of each line in the selection (e.g., for lists or blockquotes).
*/
export function toggleLinePrefix(view: EditorView, prefix: string) {
if (!view) return;
const { state, dispatch } = view;
const changes = state.changeByRange((range) => {
const lines = [];
for (let pos = range.from; pos <= range.to; ) {
const line = state.doc.lineAt(pos);
lines.push(line);
pos = line.to + 1;
}
const isAlreadyPrefixed = lines.every(l => l.text.startsWith(prefix));
const lineChanges = lines.map(l => {
if (isAlreadyPrefixed) {
return { from: l.from, to: l.from + prefix.length, insert: '' };
} else {
return { from: l.from, insert: prefix };
}
});
return {
changes: lineChanges,
range: {
from: range.from + (isAlreadyPrefixed ? -prefix.length : prefix.length),
to: range.to + (isAlreadyPrefixed ? -prefix.length * lines.length : prefix.length * lines.length)
}
};
});
dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input' }));
view.focus();
}

View File

@@ -29,6 +29,7 @@
let { let {
content = 'test test test test', content = 'test test test test',
new_content = $bindable(''), new_content = $bindable(''),
editorView = $bindable(), // Exposed for external control
theme_mode = 'light', theme_mode = 'light',
extensions = [], extensions = [],
editable = true, editable = true,

View File

@@ -7,7 +7,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { House, RefreshCw, Satellite } from '@lucide/svelte'; import { ArrowDownUp, House, RefreshCw, Satellite } from '@lucide/svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
@@ -271,10 +271,6 @@
`Scroll to top button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`, `Scroll to top button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
scroll_container() scroll_container()
); );
// document.getElementById('ae_idaa')?.scrollTo(0, 0);
// document.documentElement?.scrollTo(0, 0);
// document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
// document.body.scrollTop = 0; // For Safari
} }
}); });
@@ -284,11 +280,11 @@
behavior: 'smooth' behavior: 'smooth'
}); });
window.parent.postMessage({ scroll_to: 0 }, '*'); // This should be window.parent.postMessage({ scroll_to: 0 }, '*');
}} }}
title="Scroll to top" title="Scroll to top"
> >
<span class="fas fa-arrow-up"></span> <ArrowDownUp class="rotate-180" />
Scroll to Top Scroll to Top
</button> </button>
@@ -345,10 +341,6 @@
`Scroll to bottom button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`, `Scroll to bottom button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
scroll_container() scroll_container()
); );
// document.getElementById('ae_idaa')?.scrollTo(0, 0);
// document.documentElement?.scrollTo(0, 0);
// document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
// document.body.scrollTop = 0; // For Safari
} }
}); });
@@ -358,13 +350,12 @@
behavior: 'smooth' behavior: 'smooth'
}); });
window.parent.postMessage({ scroll_to: yScroll }, '*'); // This should be window.parent.postMessage({ scroll_to: yScroll }, '*');
}} }}
title="Scroll to bottom" title="Scroll to bottom"
> >
<span class="fas fa-arrow-down"></span> <ArrowDownUp />
Scroll to Bottom Scroll to Bottom
<!-- yTop={yTop} yScroll={yScroll} yHeight={yHeight} yScroll={yScroll} scrollTop={scroll_container().scrollTop} total={scroll_container().scrollTop + yHeight} -->
</button> </button>
</div> </div>

View File

@@ -8,7 +8,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { BookPlus, FolderPlus, Library, SquareLibrary, Wrench } from '@lucide/svelte'; import { BookPlus, FolderPlus, Library, Loader, SquareLibrary, Wrench } from '@lucide/svelte';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
@@ -243,7 +243,7 @@
{#await ae_acct.slct.journal_obj_li} {#await ae_acct.slct.journal_obj_li}
<div class="flex flex-col items-center justify-center p-8"> <div class="flex flex-col items-center justify-center p-8">
<span class="fas fa-spinner fa-spin text-4xl text-primary-500 mb-4"></span> <Loader size="2em" class="animate-spin text-primary-500 mb-4" />
<span class="text-lg text-gray-600 dark:text-gray-400">Loading journals...</span> <span class="text-lg text-gray-600 dark:text-gray-400">Loading journals...</span>
</div> </div>
{:then} {:then}

View File

@@ -9,6 +9,7 @@
ArrowDown01, ArrowDown01,
ArrowDown10, ArrowDown10,
ArrowDownUp, ArrowDownUp,
Bold,
BookHeart, BookHeart,
Bot, Bot,
BotMessageSquare, BotMessageSquare,
@@ -31,6 +32,10 @@
Group, Group,
Hash, Hash,
History, History,
Italic,
Link,
List,
ListOrdered,
Loader, Loader,
LockKeyhole, LockKeyhole,
LockKeyholeOpen, LockKeyholeOpen,
@@ -43,6 +48,7 @@
Pencil, Pencil,
PenLine, PenLine,
Plus, Plus,
Quote,
RemoveFormatting, RemoveFormatting,
Save, Save,
Search, Search,
@@ -54,6 +60,7 @@
Siren, Siren,
Skull, Skull,
SquareLibrary, SquareLibrary,
Strikethrough,
Tags, Tags,
Trash2, Trash2,
TypeOutline, TypeOutline,
@@ -64,6 +71,8 @@
import OpenAI from 'openai'; import OpenAI from 'openai';
// import { Configuration, OpenAIApi } from 'openai'; // import { Configuration, OpenAIApi } from 'openai';
import { wrapSelection, toggleLinePrefix } from '$lib/ae_journals/ae_journals_editor_helpers';
let llm_api_token = let llm_api_token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVhYjI2MzdlLThiMjktNGM2Zi05MzVhLWFkYjU1MDkwMGU5MCJ9.4y5AStXZJAVnWRlgG3lVV0-xKIfMzqdNRuInGwT0ThQ'; 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVhYjI2MzdlLThiMjktNGM2Zi05MzVhLWFkYjU1MDkwMGU5MCJ9.4y5AStXZJAVnWRlgG3lVV0-xKIfMzqdNRuInGwT0ThQ';
@@ -128,6 +137,8 @@
lq__journal_entry_obj lq__journal_entry_obj
}: Props = $props(); }: Props = $props();
let editorView: any = $state();
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`ae_comp__journal_entry_obj_id_view.svelte`, `ae_comp__journal_entry_obj_id_view.svelte`,
@@ -2651,19 +2662,56 @@ tabindex={$ae_loc.edit_mode ? 0 : -1} -->
<!-- disabled={tmp_entry_obj?.private && !$journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]} --> <!-- disabled={tmp_entry_obj?.private && !$journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]} -->
{#if $lq__journal_obj?.cfg_json?.pref_editor == 'codemirror'} {#if $lq__journal_obj?.cfg_json?.pref_editor == 'codemirror'}
<!-- bg-slate-100 text-gray-900 <!-- Toolbar for CodeMirror -->
dark:bg-slate-900 dark:text-gray-100 <div class="flex flex-row flex-wrap gap-1 p-1 bg-surface-100-900 border-x border-t border-orange-300 dark:border-orange-700 rounded-t-lg w-full max-w-6xl">
border border-orange-200 dark:border-orange-700 <button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bold" onclick={() => wrapSelection(editorView, '**')}>
hover:border-orange-500 dark:hover:border-orange-500 --> <Bold size="1.25em" />
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Italic" onclick={() => wrapSelection(editorView, '*')}>
<Italic size="1.25em" />
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Strikethrough" onclick={() => wrapSelection(editorView, '~~')}>
<Strikethrough size="1.25em" />
</button>
<span class="border-r border-surface-500 mx-1"></span>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 1" onclick={() => toggleLinePrefix(editorView, '# ')}>
<span class="font-bold">H1</span>
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 2" onclick={() => toggleLinePrefix(editorView, '## ')}>
<span class="font-bold">H2</span>
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 3" onclick={() => toggleLinePrefix(editorView, '### ')}>
<span class="font-bold">H3</span>
</button>
<span class="border-r border-surface-500 mx-1"></span>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bullet List" onclick={() => toggleLinePrefix(editorView, '- ')}>
<List size="1.25em" />
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Ordered List" onclick={() => toggleLinePrefix(editorView, '1. ')}>
<ListOrdered size="1.25em" />
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Blockquote" onclick={() => toggleLinePrefix(editorView, '> ')}>
<Quote size="1.25em" />
</button>
<span class="border-r border-surface-500 mx-1"></span>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Link" onclick={() => wrapSelection(editorView, '[', '](https://)')}>
<Link size="1.25em" />
</button>
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Code" onclick={() => wrapSelection(editorView, '`')}>
<CodeXml size="1.25em" />
</button>
</div>
<E_app_codemirror_v5 <E_app_codemirror_v5
content={tmp_entry_obj?.content ?? ''} content={tmp_entry_obj?.content ?? ''}
bind:new_content={tmp_entry_obj.content} bind:new_content={tmp_entry_obj.content}
bind:editorView={editorView}
bind:theme_mode={$ae_loc.theme_mode} bind:theme_mode={$ae_loc.theme_mode}
placeholder="Write using Markdown here..." placeholder="Write using Markdown here..."
class=" class="
p-2 p-2
preset-outlined-warning-300-700 preset-outlined-warning-300-700
shadow-lg rounded-lg shadow-lg rounded-b-lg rounded-t-none
bg-gray-100 text-gray-950 bg-gray-100 text-gray-950
dark:bg-gray-800 dark:text-gray-100 dark:bg-gray-800 dark:text-gray-100
" "

View File

@@ -60,7 +60,7 @@
> >
<h3 class="journal__name h3"> <h3 class="journal__name h3">
<BookType class="m-1 inline-block" /> <BookType class="m-1 inline-block" />
<span class="journal__name">{@html journals_journal_obj.name}</span> <span class="journal__name">{journals_journal_obj.name}</span>
</h3> </h3>
<!-- Show a label if the type code is set --> <!-- Show a label if the type code is set -->