Prettier for Journals

This commit is contained in:
Scott Idem
2026-03-24 12:25:22 -04:00
parent e1338b1a72
commit b74c6d0e9c
25 changed files with 3983 additions and 4238 deletions

View File

@@ -1,67 +1,76 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').LayoutProps} */ /** @type {import('./$types').LayoutProps} */
let log_lvl = $state(0); let log_lvl = $state(0);
// *** Import Svelte specific // *** Import Svelte specific
import { untrack } from 'svelte'; import { untrack } from 'svelte';
// import { browser } from '$app/environment'; // import { browser } from '$app/environment';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { ArrowDownUp, ArrowRight, House, RefreshCw, Satellite } from '@lucide/svelte'; import {
ArrowDownUp,
ArrowRight,
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';
import { import {
journals_loc, journals_loc,
journals_slct, journals_slct,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import Element_data_store from '$lib/elements/element_data_store.svelte'; import Element_data_store from '$lib/elements/element_data_store.svelte';
import Help_tech from '$lib/app_components/e_app_help_tech.svelte'; import Help_tech from '$lib/app_components/e_app_help_tech.svelte';
// *** Setup Svelte properties // *** Setup Svelte properties
interface Props { interface Props {
data: any; data: any;
children: any; children: any;
} }
let { data, children }: Props = $props(); let { data, children }: Props = $props();
// Use effects for store initializations to prevent render-phase updates // Use effects for store initializations to prevent render-phase updates
$effect(() => { $effect(() => {
untrack(() => { untrack(() => {
if ($slct.account_id !== data.account_id) { if ($slct.account_id !== data.account_id) {
$slct.account_id = data.account_id; $slct.account_id = data.account_id;
} }
}); });
}); });
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
$effect(() => { $effect(() => {
if (ae_acct) { if (ae_acct) {
untrack(() => { untrack(() => {
if ($journals_slct.journal_id !== ae_acct.slct.journal_id) { if ($journals_slct.journal_id !== ae_acct.slct.journal_id) {
$journals_slct.journal_id = ae_acct.slct.journal_id; $journals_slct.journal_id = ae_acct.slct.journal_id;
} }
if (JSON.stringify($journals_slct.journal_obj_li) !== JSON.stringify(ae_acct.slct.journal_obj_li)) { if (
JSON.stringify($journals_slct.journal_obj_li) !==
JSON.stringify(ae_acct.slct.journal_obj_li)
) {
$journals_slct.journal_obj_li = ae_acct.slct.journal_obj_li; $journals_slct.journal_obj_li = ae_acct.slct.journal_obj_li;
} }
}); });
} }
}); });
let nav_y_height = $state(0); let nav_y_height = $state(0);
let box: any = $state(null); let box: any = $state(null);
let xLeft = $state(0); let xLeft = $state(0);
let xScroll = $state(0); let xScroll = $state(0);
let xWidth = $state(0); let xWidth = $state(0);
let yTop = $state(0); let yTop = $state(0);
let yScroll = $state(0); let yScroll = $state(0);
let yHeight = $state(0); let yHeight = $state(0);
function handle_scroll() { function handle_scroll() {
// console.log(`handle_scroll() called`); // console.log(`handle_scroll() called`);
if (box) { if (box) {
xLeft = box.scrollLeft; xLeft = box.scrollLeft;
@@ -72,15 +81,15 @@
yScroll = box.scrollHeight; yScroll = box.scrollHeight;
// console.log(`handle_scroll() called: ${yTop}`); // console.log(`handle_scroll() called: ${yTop}`);
} }
} }
function scroll_container() { function scroll_container() {
return ( return (
document.getElementById('ae_main_content') || document.getElementById('ae_main_content') ||
document.documentElement || document.documentElement ||
document.body document.body
); );
} }
</script> </script>
<svelte:head> <svelte:head>
@@ -97,15 +106,14 @@
class:iframe={$ae_loc?.iframe} class:iframe={$ae_loc?.iframe}
class=" class="
ae_journals ae_journals
h-full max-h-full max-w-7xl m-auto flex h-full
max-h-full
max-w-7xl flex-col gap-1
overflow-auto overflow-auto
flex flex-col gap-1
m-auto
bg-gray-50 dark:bg-gray-900 bg-gray-50 text-gray-800
text-gray-800 dark:text-gray-200 dark:bg-gray-900 dark:text-gray-200
" ">
>
<!-- class:hidden={yTop > 200} --> <!-- class:hidden={yTop > 200} -->
<nav <nav
bind:clientHeight={nav_y_height} bind:clientHeight={nav_y_height}
@@ -113,36 +121,33 @@
class:opacity-0={yTop > 250} class:opacity-0={yTop > 250}
class=" class="
submenu submenu
z-20 absolute
hover:opacity-100 top-0
absolute top-0 left-0 right-0 right-0 left-0 z-20 m-auto
w-full max-w-7xl flex min-h-12
min-h-12 w-full
p-1 px-2 pb-2 m-auto max-w-7xl flex-row flex-wrap items-center
flex flex-row flex-wrap justify-around gap-1 rounded-b-lg
items-center justify-around sm:justify-between border-b-2 bg-gray-200 p-1
gap-1 px-2
border-b-2 rounded-b-lg pb-2 transition-all
bg-gray-200 dark:bg-gray-800 duration-1000 hover:opacity-100
transition-all duration-1000 sm:justify-between dark:bg-gray-800
" ">
>
<span class="justify-self-start"> <span class="justify-self-start">
<!-- Be sure to explain what &AElig; (Aether) means in the title text or similar! --> <!-- Be sure to explain what &AElig; (Aether) means in the title text or similar! -->
<Satellite <Satellite
size="1.5em" size="1.5em"
class="mx-1 inline-block text-gray-500" class="mx-1 inline-block text-gray-500" />
/>
<abbr title="Aether - Journals Module"> Æ Journals </abbr> <abbr title="Aether - Journals Module"> Æ Journals </abbr>
</span> </span>
<a <a
href="/" href="/"
class="btn btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-success-500" class="btn btn-sm preset-tonal-surface border-surface-500 hover:preset-filled-success-500 border">
>
<House /> <House />
<span class="hidden md:inline"> Home </span> <span class="hidden md:inline"> Home </span>
</a> </a>
@@ -213,9 +218,8 @@
// window.location.reload(true); // true only works with Firefox // window.location.reload(true); // true only works with Firefox
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.'); // alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
}} }}
class="btn btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-warning-500" class="btn btn-sm preset-tonal-surface border-surface-500 hover:preset-filled-warning-500 border"
title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared." title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared.">
>
<!-- <span class="fas fa-eraser mx-1"></span> --> <!-- <span class="fas fa-eraser mx-1"></span> -->
<!-- <span class="fas fa-sync mx-1"></span> --> <!-- <span class="fas fa-sync mx-1"></span> -->
<RefreshCw /> <RefreshCw />
@@ -230,8 +234,7 @@
show_btn_class="btn-info" show_btn_class="btn-info"
additional_kv={{ additional_kv={{
test: true test: true
}} }}></Help_tech>
></Help_tech>
</nav> </nav>
<!-- Add overflow-auto to section element to have the main nav sort of sticky at top --> <!-- Add overflow-auto to section element to have the main nav sort of sticky at top -->
@@ -244,23 +247,21 @@
class=" class="
main_content main_content
grow grow
px-1 md:px-2 px-1 pb-48
pb-48 md:px-2
" ">
>
{@render children?.()} {@render children?.()}
</section> </section>
<div <div
class:hidden={yTop < 500} class:hidden={yTop < 500}
class=" class="
z-20 fixed
hover:opacity-100 right-1
fixed bottom-48 right-1 bottom-48 z-20 flex
flex flex-col gap-1 items-end justify-end flex-col items-end justify-end gap-1 hover:opacity-100
" ">
>
<!-- Scroll to top button --> <!-- Scroll to top button -->
<button <button
type="button" type="button"
@@ -293,8 +294,7 @@
window.parent.postMessage({ scroll_to: 0 }, '*'); window.parent.postMessage({ scroll_to: 0 }, '*');
}} }}
title="Scroll to top" title="Scroll to top">
>
<ArrowDownUp class="rotate-180" /> <ArrowDownUp class="rotate-180" />
Scroll to Top Scroll to Top
</button> </button>
@@ -321,8 +321,7 @@
window.parent.postMessage({ scroll_to: xScroll }, '*'); window.parent.postMessage({ scroll_to: xScroll }, '*');
}} }}
title="Scroll to right" title="Scroll to right">
>
<ArrowRight size="1em" class="inline" /> <ArrowRight size="1em" class="inline" />
<!-- Scroll to Right <!-- Scroll to Right
xLeft={xLeft} xScroll={xScroll} xWidth={xWidth} xScroll={xScroll} scrollLeft={scroll_container().scrollLeft} xLeft={xLeft} xScroll={xScroll} xWidth={xWidth} xScroll={xScroll} scrollLeft={scroll_container().scrollLeft}
@@ -360,8 +359,7 @@
window.parent.postMessage({ scroll_to: yScroll }, '*'); window.parent.postMessage({ scroll_to: yScroll }, '*');
}} }}
title="Scroll to bottom" title="Scroll to bottom">
>
<ArrowDownUp /> <ArrowDownUp />
Scroll to Bottom Scroll to Bottom
</button> </button>
@@ -373,38 +371,35 @@
class:opacity-0={yTop > 250} class:opacity-0={yTop > 250}
class=" class="
footer footer
z-20 absolute
hover:opacity-100 right-0
absolute bottom-0 left-0 right-0 bottom-0 left-0 z-20 m-auto
w-full max-w-7xl flex w-full
p-1 m-auto max-w-7xl flex-row
flex flex-row flex-wrap flex-wrap items-center justify-between
items-center justify-between gap-1 rounded-t-lg
sm:flex-row md:items-center md:justify-between border-t-2 border-gray-200 bg-gray-200
gap-1 p-1
border-t-2 border-gray-200 dark:border-gray-600 text-xs transition-all duration-1000
rounded-t-lg hover:text-base
bg-gray-200 dark:bg-gray-800 hover:opacity-100 sm:flex-row
text-xs hover:text-base md:items-center md:justify-between
transition-all duration-1000 dark:border-gray-600 dark:bg-gray-800
" "
class:ae_debug={$ae_loc?.debug} class:ae_debug={$ae_loc?.debug}>
>
<Element_data_store <Element_data_store
ds_code="hub__site__appshell_footer" ds_code="hub__site__appshell_footer"
ds_type="html" ds_type="html"
class_li="grow flex flex-row justify-between" class_li="grow flex flex-row justify-between" />
/>
</footer> </footer>
</div> </div>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You are not logged in as a user. You must be signed in to access the You are not logged in as a user. You must be signed in to access the
journals module. journals module.

View File

@@ -1,53 +1,60 @@
<script lang="ts"> <script lang="ts">
/** /**
* src/routes/journals/+page.svelte * src/routes/journals/+page.svelte
* Modernized Journals Index View * Modernized Journals Index View
* Focus: Simplicity for regular users, power tools for edit mode. * Focus: Simplicity for regular users, power tools for edit mode.
*/ */
// import { onMount } from 'svelte'; // import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Icons // *** Icons
import { BookPlus, FileUp, LoaderCircle, Sparkles, SquareLibrary, Wrench } from '@lucide/svelte'; import {
// *** Libraries & Stores BookPlus,
import { liveQuery } from 'dexie'; FileUp,
import { Modal } from 'flowbite-svelte'; LoaderCircle,
import { db_core } from '$lib/ae_core/db_core'; Sparkles,
import { db_journals } from '$lib/ae_journals/db_journals'; SquareLibrary,
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; Wrench
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores'; } from '@lucide/svelte';
import { // *** Libraries & Stores
import { liveQuery } from 'dexie';
import { Modal } from 'flowbite-svelte';
import { db_core } from '$lib/ae_core/db_core';
import { db_journals } from '$lib/ae_journals/db_journals';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
// *** Components // *** Components
import AE_Comp_Modal_Journal_Config from './ae_comp__modal_journal_config.svelte'; import AE_Comp_Modal_Journal_Config from './ae_comp__modal_journal_config.svelte';
import Journal_obj_li from './ae_comp__journal_obj_li.svelte'; import Journal_obj_li from './ae_comp__journal_obj_li.svelte';
import AE_Comp_Journal_Entry_Quick_Add from './ae_comp__journal_entry_quick_add.svelte'; import AE_Comp_Journal_Entry_Quick_Add from './ae_comp__journal_entry_quick_add.svelte';
import AE_Comp_Modal_Journal_Import from './ae_comp__modal_journal_import.svelte'; import AE_Comp_Modal_Journal_Import from './ae_comp__modal_journal_import.svelte';
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
// *** State // *** State
let show_import_modal = $state(false); let show_import_modal = $state(false);
let log_lvl = 0; let log_lvl = 0;
// *** LiveQueries // *** LiveQueries
let lq__account = $derived( let lq__account = $derived(
liveQuery(async () => { liveQuery(async () => {
if (!$slct.account_id) return null; if (!$slct.account_id) return null;
return await db_core.account.get($slct.account_id); return await db_core.account.get($slct.account_id);
}) })
); );
let lq__journal_obj_li = $derived( let lq__journal_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
return await db_journals.journal return await db_journals.journal
.where('person_id') .where('person_id')
@@ -55,9 +62,9 @@
.reverse() .reverse()
.sortBy('tmp_sort_3'); .sortBy('tmp_sort_3');
}) })
); );
async function create_journal() { async function create_journal() {
if (!confirm('Create a new journal?')) return; if (!confirm('Create a new journal?')) return;
const name = $journals_sess.journal.new_journal_name; const name = $journals_sess.journal.new_journal_name;
@@ -87,17 +94,15 @@
} else { } else {
alert('Please provide a name and type.'); alert('Please provide a name and type.');
} }
} }
</script> </script>
<div <div
class="page_container flex flex-col gap-4 sm:gap-6 md:gap-8 items-center w-full min-h-screen p-3 sm:p-4 md:p-8" class="page_container flex min-h-screen w-full flex-col items-center gap-4 p-3 sm:gap-6 sm:p-4 md:gap-8 md:p-8">
>
<!-- Header Section --> <!-- Header Section -->
<header class="text-center space-y-2 max-w-3xl"> <header class="max-w-3xl space-y-2 text-center">
<h1 <h1
class="text-3xl sm:text-4xl md:text-5xl font-black tracking-tight text-surface-900 dark:text-surface-100" class="text-surface-900 dark:text-surface-100 text-3xl font-black tracking-tight sm:text-4xl md:text-5xl">
>
<SquareLibrary size="1em" class="text-primary-500 inline-block" /> <SquareLibrary size="1em" class="text-primary-500 inline-block" />
Journals Journals
</h1> </h1>
@@ -105,7 +110,9 @@
{#if $ae_loc.person.given_name || $ae_loc.person.family_name} {#if $ae_loc.person.given_name || $ae_loc.person.family_name}
<p class="text-surface-600 dark:text-surface-400 font-medium"> <p class="text-surface-600 dark:text-surface-400 font-medium">
<span class="text-primary-500 font-semibold"> <span class="text-primary-500 font-semibold">
{[$ae_loc.person.given_name, $ae_loc.person.family_name].filter(Boolean).join(' ')} {[$ae_loc.person.given_name, $ae_loc.person.family_name]
.filter(Boolean)
.join(' ')}
</span> </span>
</p> </p>
{/if} {/if}
@@ -113,19 +120,17 @@
<!-- Quick Add Integrated Section --> <!-- Quick Add Integrated Section -->
<section class="w-full max-w-2xl"> <section class="w-full max-w-2xl">
<div class="relative group"> <div class="group relative">
<!-- Glow ring: slightly brighter in dark mode where colors need more presence --> <!-- Glow ring: slightly brighter in dark mode where colors need more presence -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 dark:opacity-40 group-hover:opacity-60 dark:group-hover:opacity-70 transition duration-1000 group-hover:duration-200" class="from-primary-500 to-secondary-500 absolute -inset-1 rounded-2xl bg-linear-to-r opacity-25 blur transition duration-1000 group-hover:opacity-60 group-hover:duration-200 dark:opacity-40 dark:group-hover:opacity-70">
></div> </div>
<AE_Comp_Journal_Entry_Quick_Add <AE_Comp_Journal_Entry_Quick_Add
journals_li={$lq__journal_obj_li} journals_li={$lq__journal_obj_li}
class="relative shadow-2xl rounded-xl overflow-hidden border border-surface-500/10 bg-surface-50 dark:bg-surface-900" class="border-surface-500/10 bg-surface-50 dark:bg-surface-900 relative overflow-hidden rounded-xl border shadow-2xl" />
/>
</div> </div>
<div <div
class="mt-2 flex items-center justify-center gap-2 text-xs opacity-50 font-bold uppercase tracking-widest" class="mt-2 flex items-center justify-center gap-2 text-xs font-bold tracking-widest uppercase opacity-50">
>
<Sparkles size="1em" /> <Sparkles size="1em" />
<span>Fast Input Mode Active</span> <span>Fast Input Mode Active</span>
</div> </div>
@@ -134,33 +139,29 @@
<!-- Administrative Action Bar (Edit Mode Only) --> <!-- Administrative Action Bar (Edit Mode Only) -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<nav <nav
class="flex flex-row flex-wrap gap-3 items-center justify-center w-full py-4 border-y border-surface-500/10" class="border-surface-500/10 flex w-full flex-row flex-wrap items-center justify-center gap-3 border-y py-4">
>
<button <button
type="button" type="button"
class="btn preset-tonal-secondary shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-secondary shadow-lg transition-transform hover:scale-105"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = true)} ($journals_sess.show__modal_new__journal_obj = true)}>
>
<BookPlus size="1.2em" class="mr-2" /> <BookPlus size="1.2em" class="mr-2" />
<span>New Journal</span> <span>New Journal</span>
</button> </button>
<button <button
type="button" type="button"
class="btn preset-tonal-surface shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-surface shadow-lg transition-transform hover:scale-105"
onclick={() => (show_import_modal = true)} onclick={() => (show_import_modal = true)}>
>
<FileUp size="1.2em" class="mr-2" /> <FileUp size="1.2em" class="mr-2" />
<span>Import</span> <span>Import</span>
</button> </button>
<button <button
type="button" type="button"
class="btn preset-tonal-surface shadow-lg hover:scale-105 transition-transform" class="btn preset-tonal-surface shadow-lg transition-transform hover:scale-105"
onclick={() => onclick={() =>
($journals_sess.show__modal__journals_config = true)} ($journals_sess.show__modal__journals_config = true)}>
>
<Wrench size="1.2em" class="mr-2" /> <Wrench size="1.2em" class="mr-2" />
<span>Config</span> <span>Config</span>
</button> </button>
@@ -168,11 +169,10 @@
{/if} {/if}
<!-- Main List Section --> <!-- Main List Section -->
<main class="w-full flex justify-center"> <main class="flex w-full justify-center">
{#if $lq__journal_obj_li === undefined} {#if $lq__journal_obj_li === undefined}
<div <div
class="flex flex-col items-center justify-center p-20 gap-4 opacity-50" class="flex flex-col items-center justify-center gap-4 p-20 opacity-50">
>
<LoaderCircle size="3em" class="animate-spin" /> <LoaderCircle size="3em" class="animate-spin" />
<p class="text-xl font-bold">Accessing Brain...</p> <p class="text-xl font-bold">Accessing Brain...</p>
</div> </div>
@@ -180,11 +180,10 @@
<Journal_obj_li {lq__journal_obj_li} /> <Journal_obj_li {lq__journal_obj_li} />
{:else} {:else}
<div <div
class="max-w-md text-center p-12 bg-surface-500/5 rounded-3xl border-2 border-dashed border-surface-500/20" class="bg-surface-500/5 border-surface-500/20 max-w-md rounded-3xl border-2 border-dashed p-12 text-center">
>
<SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" /> <SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" />
<h3 class="text-2xl font-bold mb-2">No Journals Found</h3> <h3 class="mb-2 text-2xl font-bold">No Journals Found</h3>
<p class="opacity-60 mb-6"> <p class="mb-6 opacity-60">
You haven't created any journals yet. Start by creating one You haven't created any journals yet. Start by creating one
to begin your documentation journey. to begin your documentation journey.
</p> </p>
@@ -192,8 +191,7 @@
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = true)} ($journals_sess.show__modal_new__journal_obj = true)}>
>
Create Your First Journal Create Your First Journal
</button> </button>
</div> </div>
@@ -208,32 +206,27 @@
bind:open={$journals_sess.show__modal_new__journal_obj} bind:open={$journals_sess.show__modal_new__journal_obj}
autoclose={false} autoclose={false}
size="md" size="md"
class="bg-white dark:bg-surface-900 shadow-2xl rounded-2xl" class="dark:bg-surface-900 rounded-2xl bg-white shadow-2xl">
> <div class="space-y-4 p-2">
<div class="p-2 space-y-4">
<div class="space-y-1"> <div class="space-y-1">
<!-- svelte-ignore a11y_label_has_associated_control --> <!-- svelte-ignore a11y_label_has_associated_control -->
<label class="label text-sm font-bold opacity-75" <label class="label text-sm font-bold opacity-75"
>Journal Name</label >Journal Name</label>
>
<input <input
type="text" type="text"
placeholder="e.g. My Daily Logs" placeholder="e.g. My Daily Logs"
bind:value={$journals_sess.journal.new_journal_name} bind:value={$journals_sess.journal.new_journal_name}
class="input" class="input" />
/>
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<!-- svelte-ignore a11y_label_has_associated_control --> <!-- svelte-ignore a11y_label_has_associated_control -->
<label class="label text-sm font-bold opacity-75" <label class="label text-sm font-bold opacity-75"
>Type Code</label >Type Code</label>
>
<input <input
type="text" type="text"
placeholder="e.g. diary, log, notebook" placeholder="e.g. diary, log, notebook"
bind:value={$journals_sess.journal.new_journal_type_code} bind:value={$journals_sess.journal.new_journal_type_code}
class="input" class="input" />
/>
</div> </div>
<div class="flex justify-end gap-2 pt-4"> <div class="flex justify-end gap-2 pt-4">
<button <button
@@ -241,13 +234,11 @@
class="btn preset-tonal-surface" class="btn preset-tonal-surface"
onclick={() => onclick={() =>
($journals_sess.show__modal_new__journal_obj = false)} ($journals_sess.show__modal_new__journal_obj = false)}
>Cancel</button >Cancel</button>
>
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold" class="btn preset-filled-primary font-bold"
onclick={create_journal}>Create Journal</button onclick={create_journal}>Create Journal</button>
>
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -255,12 +246,10 @@
{#if $journals_sess.show__modal__journals_config} {#if $journals_sess.show__modal__journals_config}
<AE_Comp_Modal_Journal_Config <AE_Comp_Modal_Journal_Config
show={$journals_sess.show__modal__journals_config} show={$journals_sess.show__modal__journals_config} />
/>
{/if} {/if}
<AE_Comp_Modal_Journal_Import <AE_Comp_Modal_Journal_Import
bind:open={show_import_modal} bind:open={show_import_modal}
on_close={() => (show_import_modal = false)} on_close={() => (show_import_modal = false)}
on_import_complete={() => {}} on_import_complete={() => {}} />
/>

View File

@@ -1,41 +1,41 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').LayoutProps} */ /** @type {import('./$types').LayoutProps} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
let { data, children } = $props(); let { data, children } = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { FilePlus, Notebook, SquareLibrary, X } from '@lucide/svelte'; import { FilePlus, Notebook, SquareLibrary, X } from '@lucide/svelte';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct journals_slct
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import type { ae_JournalEntry } from '$lib/types/ae_types'; import type { ae_JournalEntry } from '$lib/types/ae_types';
import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte'; import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store) // NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration. // to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log(`ae_acct = `, ae_acct); console.log(`ae_acct = `, ae_acct);
} }
}); });
let show_menu__all_journals: boolean = $state(false); let show_menu__all_journals: boolean = $state(false);
let lq__journal_obj = $derived( let lq__journal_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal.get( let results = await db_journals.journal.get(
$journals_slct?.journal_id ?? '' $journals_slct?.journal_id ?? ''
@@ -53,9 +53,9 @@
return results; return results;
}) })
); );
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}` `lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`
@@ -77,7 +77,7 @@
} }
} }
} }
}); });
</script> </script>
<!-- Svelte layout for a Journal ID page and children --> <!-- Svelte layout for a Journal ID page and children -->
@@ -85,29 +85,27 @@
class=" class="
ae_journals__journal ae_journals__journal
mx-auto mx-auto
flex flex-col grow gap-1 flex max-h-max min-h-full max-w-max
items-center
min-h-full
max-h-max
min-w-full min-w-full
max-w-max grow
flex-col
items-center
gap-1
space-y-2 space-y-2
" ">
>
<div <div
class=" class="
flex flex-row flex-wrap relative flex w-full
flex-row
flex-wrap items-center
justify-between
gap-1 gap-1
items-center justify-between
border-gray-400
border-b border-b
py-2 border-gray-400
w-full py-2 transition-all
hover:bg-slate-100 hover:dark:bg-slate-700
relative transition-all hover:bg-slate-100 hover:dark:bg-slate-700
" ">
>
<!-- If middle click then open the all journals page in a new tab. Otherwise show/hide the menu. --> <!-- If middle click then open the all journals page in a new tab. Otherwise show/hide the menu. -->
<button <button
type="button" type="button"
@@ -134,8 +132,7 @@
transition-all transition-all
" "
title={`View all journals menu: "${$ae_loc?.user?.name}" title={`View all journals menu: "${$ae_loc?.user?.name}"
Middle-click to open in new tab`} Middle-click to open in new tab`}>
>
<!-- <BookHeart /> --> <!-- <BookHeart /> -->
<!-- <Library /> --> <!-- <Library /> -->
{#if show_menu__all_journals} {#if show_menu__all_journals}
@@ -152,16 +149,15 @@ Middle-click to open in new tab`}
<div <div
class=" class="
absolute top-12 left-0 absolute top-12 left-0
p-4 z-50 w-80 z-50 w-80 max-w-fit
space-y-0.5
bg-white dark:bg-gray-800
border border-gray-500
shadow-xl rounded-lg
min-w-72 min-w-72
max-w-fit space-y-0.5 rounded-lg
border border-gray-500
bg-white p-4
shadow-xl
dark:bg-gray-800
" "
class:hidden={!show_menu__all_journals} class:hidden={!show_menu__all_journals}>
>
<a <a
href="/journals" href="/journals"
class=" class="
@@ -172,8 +168,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="View all journals for this account: {$ae_loc.account_name}" title="View all journals for this account: {$ae_loc.account_name}">
>
<!-- <BookHeart /> --> <!-- <BookHeart /> -->
<!-- <Library /> --> <!-- <Library /> -->
<SquareLibrary class="text-blue-500" /> <SquareLibrary class="text-blue-500" />
@@ -195,12 +190,11 @@ Middle-click to open in new tab`}
}} }}
class=" class="
form-select form-select
border-neutral-400-600
w-full w-full
border p-1
text-sm text-sm
border border-neutral-400-600 ">
p-1
"
>
<option value="" disabled selected> <option value="" disabled selected>
{Object.keys($journals_loc.entry_view_history_kv) {Object.keys($journals_loc.entry_view_history_kv)
.length}&times; Recent Entries... .length}&times; Recent Entries...
@@ -228,8 +222,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="View all journal entries for this journal: {$lq__journal_obj?.name}" title="View all journal entries for this journal: {$lq__journal_obj?.name}">
>
<Notebook /> <Notebook />
<!-- <Bookmark /> --> <!-- <Bookmark /> -->
<!-- <BookHeart class="m-1" /> --> <!-- <BookHeart class="m-1" /> -->
@@ -315,8 +308,7 @@ Middle-click to open in new tab`}
hover:preset-filled-tertiary-300-700 hover:preset-filled-tertiary-300-700
transition-all transition-all
" "
title="Create a new journal entry for this journal: {$lq__journal_obj?.name}" title="Create a new journal entry for this journal: {$lq__journal_obj?.name}">
>
<FilePlus /> <FilePlus />
<!-- <span class="fas fa-plus m-1"></span> --> <!-- <span class="fas fa-plus m-1"></span> -->
<span class="hidden sm:inline"> New Entry </span> <span class="hidden sm:inline"> New Entry </span>

View File

@@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').PageData} */ /** @type {import('./$types').PageData} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
// *** Import other supporting libraries // *** Import other supporting libraries
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
@@ -25,63 +25,61 @@
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_prom, journals_prom,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompJournalObjIdView from './../ae_comp__journal_obj_id_view.svelte'; import AeCompJournalObjIdView from './../ae_comp__journal_obj_id_view.svelte';
import Journal_entry_obj_li_wrapper from './../ae_comp__journal_entry_obj_li_wrapper.svelte'; import Journal_entry_obj_li_wrapper from './../ae_comp__journal_entry_obj_li_wrapper.svelte';
import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte'; import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte';
import AeCompModalJournalImport from '../ae_comp__modal_journal_import.svelte'; import AeCompModalJournalImport from '../ae_comp__modal_journal_import.svelte';
// Variables // Variables
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
let show_export_modal = $state(false); let show_export_modal = $state(false);
let show_import_modal = $state(false); let show_import_modal = $state(false);
let search_id_li: Array<string> = $state([]); let search_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null; let search_debounce_timer: any = null;
let last_search_id = 0; let last_search_id = 0;
let last_executed_key = ''; // Search Guard Key let last_executed_key = ''; // Search Guard Key
function handle_import_complete() { function handle_import_complete() {
// Trigger a refresh of the journal entry list // Trigger a refresh of the journal entry list
if ($journals_loc.entry.search_version === undefined) if ($journals_loc.entry.search_version === undefined)
$journals_loc.entry.search_version = 0; $journals_loc.entry.search_version = 0;
$journals_loc.entry.search_version++; $journals_loc.entry.search_version++;
} }
$effect(() => { $effect(() => {
if (!ae_acct) return; if (!ae_acct) return;
untrack(() => { untrack(() => {
$journals_slct.journal_id = ae_acct.slct.journal_id; $journals_slct.journal_id = ae_acct.slct.journal_id;
$journals_slct.journal_entry_id = null; $journals_slct.journal_entry_id = null;
}); });
}); });
let lq__journal_obj = $derived( let lq__journal_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
return await db_journals.journal.get( return await db_journals.journal.get($journals_slct?.journal_id ?? '');
$journals_slct?.journal_id ?? ''
);
}) })
); );
// Stable LiveQuery Pattern (Aether UI V3) // Stable LiveQuery Pattern (Aether UI V3)
// Re-wrapped in $derived to ensure the observable instance remains stable // Re-wrapped in $derived to ensure the observable instance remains stable
// unless the underlying dependencies (ids, search context) change. // unless the underlying dependencies (ids, search context) change.
// Important: keep the `liveQuery` closure free of transient reactive // Important: keep the `liveQuery` closure free of transient reactive
// references — capture stable values (ids, search keys) so the observable // references — capture stable values (ids, search keys) so the observable
// isn't recreated unnecessarily on every render. Use `search_id_li` or // isn't recreated unnecessarily on every render. Use `search_id_li` or
// other plain arrays/values as explicit dependencies. // other plain arrays/values as explicit dependencies.
let lq__journal_entry_obj_li = $derived( let lq__journal_entry_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
const ids = search_id_li; const ids = search_id_li;
const journal_id = $lq__journal_obj?.journal_id; const journal_id = $lq__journal_obj?.journal_id;
@@ -111,11 +109,11 @@
return []; return [];
}) })
); );
// Standardized Reactive Search Pattern (Aether UI V3) // Standardized Reactive Search Pattern (Aether UI V3)
// 1. Isolate dependencies into a stable derived object // 1. Isolate dependencies into a stable derived object
let search_params = $derived({ let search_params = $derived({
v: $journals_loc.entry.search_version, v: $journals_loc.entry.search_version,
str: ($journals_loc.entry.qry__search_text ?? '').toLowerCase().trim(), str: ($journals_loc.entry.qry__search_text ?? '').toLowerCase().trim(),
cat: $journals_loc.entry.qry__category_code, cat: $journals_loc.entry.qry__category_code,
@@ -125,10 +123,10 @@
journal_id: $journals_slct.journal_id, journal_id: $journals_slct.journal_id,
person_id: $ae_loc.person_id, person_id: $ae_loc.person_id,
remote_first: $journals_loc.entry.qry__remote_first remote_first: $journals_loc.entry.qry__remote_first
}); });
// 2. Controlled effect for triggering searches // 2. Controlled effect for triggering searches
$effect(() => { $effect(() => {
// Establishes reactive dependency on search_params // Establishes reactive dependency on search_params
const params = search_params; const params = search_params;
@@ -143,9 +141,9 @@
return () => { return () => {
if (search_debounce_timer) clearTimeout(search_debounce_timer); if (search_debounce_timer) clearTimeout(search_debounce_timer);
}; };
}); });
async function handle_search_refresh(params: any) { async function handle_search_refresh(params: any) {
// 1. Guard: Check if criteria actually changed // 1. Guard: Check if criteria actually changed
const qry_key = JSON.stringify(params); const qry_key = JSON.stringify(params);
if (qry_key === last_executed_key) return; if (qry_key === last_executed_key) return;
@@ -183,9 +181,7 @@
return false; return false;
if (qry_str) { if (qry_str) {
const name = (entry.name ?? '').toLowerCase(); const name = (entry.name ?? '').toLowerCase();
const content = ( const content = (entry.content ?? '').toLowerCase();
entry.content ?? ''
).toLowerCase();
return ( return (
name.includes(qry_str) || name.includes(qry_str) ||
content.includes(qry_str) content.includes(qry_str)
@@ -280,16 +276,16 @@
}); });
} }
} }
} }
if (browser) { if (browser) {
window.parent.postMessage( window.parent.postMessage(
{ journal_id: $journals_slct?.journal_id ?? null }, { journal_id: $journals_slct?.journal_id ?? null },
'*' '*'
); );
} }
import { LoaderCircle } from '@lucide/svelte'; import { LoaderCircle } from '@lucide/svelte';
</script> </script>
<svelte:head> <svelte:head>
@@ -300,9 +296,8 @@
{#if $lq__journal_obj === undefined} {#if $lq__journal_obj === undefined}
<div <div
class="flex flex-col items-center justify-center p-20 opacity-50 text-center" class="flex flex-col items-center justify-center p-20 text-center opacity-50">
> <LoaderCircle size="3em" class="mx-auto mb-4 animate-spin" />
<LoaderCircle size="3em" class="animate-spin mb-4 mx-auto" />
<p class="text-xl">Loading Journal...</p> <p class="text-xl">Loading Journal...</p>
</div> </div>
{:else if $ae_loc.person_id == $lq__journal_obj?.person_id} {:else if $ae_loc.person_id == $lq__journal_obj?.person_id}
@@ -310,32 +305,27 @@
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
on_show_export={() => (show_export_modal = true)} on_show_export={() => (show_export_modal = true)}
on_show_import={() => (show_import_modal = true)} on_show_import={() => (show_import_modal = true)} />
/>
<Journal_entry_obj_li_wrapper <Journal_entry_obj_li_wrapper
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
show_found_header={false} show_found_header={false}
{log_lvl} {log_lvl} />
/>
<AeCompModalJournalExport <AeCompModalJournalExport
bind:open={show_export_modal} bind:open={show_export_modal}
entries={$lq__journal_entry_obj_li ?? []} entries={$lq__journal_entry_obj_li ?? []}
journal={$lq__journal_obj} journal={$lq__journal_obj}
on_close={() => (show_export_modal = false)} on_close={() => (show_export_modal = false)} />
/>
<AeCompModalJournalImport <AeCompModalJournalImport
bind:open={show_import_modal} bind:open={show_import_modal}
on_close={() => (show_import_modal = false)} on_close={() => (show_import_modal = false)}
on_import_complete={handle_import_complete} on_import_complete={handle_import_complete} />
/>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You must be logged in as the owner to view this Journal. You must be logged in as the owner to view this Journal.
</p> </p>

View File

@@ -1,60 +1,55 @@
<script lang="ts"> <script lang="ts">
/** @type {import('./$types').PageData} */ /** @type {import('./$types').PageData} */
let log_lvl: number = $state(0); let log_lvl: number = $state(0);
// *** Import Svelte specific // *** Import Svelte specific
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
// *** Import other supporting libraries // *** Import other supporting libraries
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
// import type { key_val } from '$lib/ae_stores'; // import type { key_val } from '$lib/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
// import { core_func } from '$lib/ae_core/ae_core_functions'; // import { core_func } from '$lib/ae_core/ae_core_functions';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import { import { ae_loc, ae_sess, ae_api, ae_trig } from '$lib/stores/ae_stores';
ae_loc, import {
ae_sess,
ae_api,
ae_trig,
} from '$lib/stores/ae_stores';
import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_prom, journals_prom,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import Journal_entry_view from './../../../ae_comp__journal_entry_obj_id_view.svelte'; import Journal_entry_view from './../../../ae_comp__journal_entry_obj_id_view.svelte';
// import Element_data_store from '$lib/elements/element_data_store.svelte'; // import Element_data_store from '$lib/elements/element_data_store.svelte';
import AeCompModalJournalExport from '../../../ae_comp__modal_journal_export.svelte'; import AeCompModalJournalExport from '../../../ae_comp__modal_journal_export.svelte';
interface Props { interface Props {
data: any; data: any;
} }
let { data }: Props = $props(); let { data }: Props = $props();
// let ae_promises: key_val = {}; // let ae_promises: key_val = {};
// let ae_tmp: key_val = {}; // let ae_tmp: key_val = {};
// let ae_triggers: key_val = {}; // let ae_triggers: key_val = {};
// Variables // Variables
// *** Quickly pull out data from parent(s) // *** Quickly pull out data from parent(s)
let ae_acct = $derived(data[data.account_id]); let ae_acct = $derived(data[data.account_id]);
let show_export_modal = $state(false); let show_export_modal = $state(false);
$effect(() => { $effect(() => {
if (!ae_acct) return; if (!ae_acct) return;
if (log_lvl) console.log(`ae_acct = `, ae_acct); if (log_lvl) console.log(`ae_acct = `, ae_acct);
untrack(() => { untrack(() => {
$journals_slct.journal_id = ae_acct.slct.journal_id; $journals_slct.journal_id = ae_acct.slct.journal_id;
}); });
}); });
let lq__journal_obj = $derived( let lq__journal_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal.get( let results = await db_journals.journal.get(
$journals_slct?.journal_id ?? '' $journals_slct?.journal_id ?? ''
@@ -72,9 +67,9 @@
return results; return results;
}) })
); );
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`lq__journal_obj: journal_id = ${$journals_slct?.journal_id}` `lq__journal_obj: journal_id = ${$journals_slct?.journal_id}`
@@ -96,9 +91,9 @@
} }
} }
} }
}); });
let lq__journal_obj_li = $derived( let lq__journal_obj_li = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal let results = await db_journals.journal
.where('person_id') .where('person_id')
@@ -117,9 +112,9 @@
return results; return results;
}) })
); );
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log(`lq__journal_obj_li: person_id = ${$ae_loc.person_id}`); console.log(`lq__journal_obj_li: person_id = ${$ae_loc.person_id}`);
console.log(`lq__journal_obj_li: results = `, lq__journal_obj_li); console.log(`lq__journal_obj_li: results = `, lq__journal_obj_li);
@@ -140,19 +135,19 @@
} }
} }
} }
}); });
// For some reason data.params.journal_entry_id (or whatever param) is not being passed to this page when loaded by a link from another page. This seems to be a bug with Svelte or SvelteKit. Hopefully fixed in a future version 5? 2024-11-06 // For some reason data.params.journal_entry_id (or whatever param) is not being passed to this page when loaded by a link from another page. This seems to be a bug with Svelte or SvelteKit. Hopefully fixed in a future version 5? 2024-11-06
// NOTE: This must remain reactive (in an effect) so it updates on same-route navigation. // NOTE: This must remain reactive (in an effect) so it updates on same-route navigation.
$effect(() => { $effect(() => {
if (!ae_acct) return; if (!ae_acct) return;
untrack(() => { untrack(() => {
$journals_slct.journal_entry_id = ae_acct.slct.journal_entry_id; $journals_slct.journal_entry_id = ae_acct.slct.journal_entry_id;
// $journals_slct.journal_entry_obj = ae_acct.slct.journal_entry_obj; // $journals_slct.journal_entry_obj = ae_acct.slct.journal_entry_obj;
}); });
}); });
let lq__journal_entry_obj = $derived( let lq__journal_entry_obj = $derived(
liveQuery(async () => { liveQuery(async () => {
let results = await db_journals.journal_entry.get( let results = await db_journals.journal_entry.get(
$journals_slct.journal_entry_id ?? '' $journals_slct.journal_entry_id ?? ''
@@ -170,17 +165,14 @@
return results; return results;
}) })
); );
$effect(() => { $effect(() => {
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`lq__journal_entry_obj: journal_entry_id = ${$journals_slct?.journal_entry_id}` `lq__journal_entry_obj: journal_entry_id = ${$journals_slct?.journal_entry_id}`
); );
console.log( console.log(`lq__journal_entry_obj: results = `, lq__journal_entry_obj);
`lq__journal_entry_obj: results = `,
lq__journal_entry_obj
);
if ($journals_slct.journal_entry_obj && lq__journal_entry_obj) { if ($journals_slct.journal_entry_obj && lq__journal_entry_obj) {
if ( if (
JSON.stringify($journals_slct.journal_entry_obj) !== JSON.stringify($journals_slct.journal_entry_obj) !==
@@ -197,9 +189,9 @@
} }
} }
} }
}); });
$effect(() => { $effect(() => {
if (browser && $lq__journal_entry_obj?.journal_entry_id) { if (browser && $lq__journal_entry_obj?.journal_entry_id) {
// Start with the current KV or convert the LI to a KV if needed // Start with the current KV or convert the LI to a KV if needed
let history_kv = { let history_kv = {
@@ -258,7 +250,7 @@
// log_lvl = 1; // log_lvl = 1;
} }
}); });
</script> </script>
<!-- <svelte:head> <!-- <svelte:head>
@@ -274,22 +266,20 @@
class=" class="
ae_journals__journal_entry ae_journals__journal_entry
mx-auto mx-auto
flex flex-col grow gap-1 flex max-h-max min-h-full max-w-max
items-center
min-h-full
max-h-max
min-w-full min-w-full
max-w-max grow
flex-col
items-center
gap-1
space-y-2 space-y-2
" ">
>
<!-- {#if $lq__journal_entry_obj} --> <!-- {#if $lq__journal_entry_obj} -->
<Journal_entry_view <Journal_entry_view
{lq__journal_obj} {lq__journal_obj}
{lq__journal_obj_li} {lq__journal_obj_li}
{lq__journal_entry_obj} {lq__journal_entry_obj}
on_show_export={() => (show_export_modal = true)} on_show_export={() => (show_export_modal = true)} />
/>
<!-- {/if} --> <!-- {/if} -->
</section> </section>
@@ -297,12 +287,10 @@
bind:open={show_export_modal} bind:open={show_export_modal}
entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []} entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []}
journal={$lq__journal_obj} journal={$lq__journal_obj}
on_close={() => (show_export_modal = false)} on_close={() => (show_export_modal = false)} />
/>
{:else} {:else}
<section <section
class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center" class="main_content flex grow flex-col items-center gap-1 px-1 pb-28 md:px-2">
>
<p class="text-center"> <p class="text-center">
You must be logged in as the owner to view this Journal Entry. You must be logged in as the owner to view this Journal Entry.
</p> </p>

View File

@@ -1,24 +1,19 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_ai_tools.svelte * ae_comp__journal_entry_ai_tools.svelte
* Journal-specific wrapper for the generic AE_AITools. * Journal-specific wrapper for the generic AE_AITools.
* Handles layout/positioning and specific save behavior for journal entries. * Handles layout/positioning and specific save behavior for journal entries.
*/ */
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte'; import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
interface Props { interface Props {
content: string; content: string;
summary: string; // Bindable summary: string; // Bindable
on_save: () => void; on_save: () => void;
log_lvl?: number; log_lvl?: number;
} }
let { let { content, summary = $bindable(), on_save, log_lvl = 0 }: Props = $props();
content,
summary = $bindable(),
on_save,
log_lvl = 0
}: Props = $props();
</script> </script>
<div class="journal-entry-ai-tools absolute top-2 right-2 z-10"> <div class="journal-entry-ai-tools absolute top-2 right-2 z-10">
@@ -29,6 +24,5 @@
summary = newSummary; summary = newSummary;
on_save(); on_save();
}} }}
{log_lvl} {log_lvl} />
/>
</div> </div>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_editor.svelte * ae_comp__journal_entry_editor.svelte
* Extracted 2026-01-08 to modularize the massive Journal Entry view. * Extracted 2026-01-08 to modularize the massive Journal Entry view.
* Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes. * Handles: CodeMirror vs Plain vs Rendered HTML for both View and Edit modes.
*/ */
import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte'; import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
journal: ae_Journal; journal: ae_Journal;
tmp_entry_obj: any; // Bindable tmp_entry_obj: any; // Bindable
@@ -22,9 +22,9 @@
updated_idb: boolean; updated_idb: boolean;
on_save: () => void; on_save: () => void;
on_force_reset?: () => void; on_force_reset?: () => void;
} }
let { let {
entry, entry,
journal, journal,
tmp_entry_obj = $bindable(), tmp_entry_obj = $bindable(),
@@ -33,19 +33,18 @@
updated_idb, updated_idb,
on_save, on_save,
on_force_reset on_force_reset
}: Props = $props(); }: Props = $props();
const is_editing = $derived( const is_editing = $derived(
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current' $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'
); );
</script> </script>
<div <div
class="journal-entry-editor-wrapper grow w-full flex flex-col items-center" class="journal-entry-editor-wrapper flex w-full grow flex-col items-center">
>
{#if !is_editing} {#if !is_editing}
<!-- VIEW MODE --> <!-- VIEW MODE -->
<div class="w-full max-w-6xl p-4 prose dark:prose-invert"> <div class="prose dark:prose-invert w-full max-w-6xl p-4">
{@html tmp_entry_obj?.content_md_html || ''} {@html tmp_entry_obj?.content_md_html || ''}
</div> </div>
{:else} {:else}
@@ -53,10 +52,9 @@
{#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted} {#if !tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted}
<!-- Decryption Required Message --> <!-- Decryption Required Message -->
<div <div
class="w-full max-w-6xl p-4 bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 rounded-lg border border-error-500 flex flex-col gap-4" class="bg-error-100 dark:bg-error-900/30 text-error-900 dark:text-error-100 border-error-500 flex w-full max-w-6xl flex-col gap-4 rounded-lg border p-4">
>
<div class="space-y-2"> <div class="space-y-2">
<div class="font-bold flex items-center gap-2"> <div class="flex items-center gap-2 font-bold">
<LockKeyhole size="1.25em" /> <LockKeyhole size="1.25em" />
Decryption Required Decryption Required
</div> </div>
@@ -65,16 +63,15 @@
</p> </p>
{#if tmp_entry_obj?.content === false} {#if tmp_entry_obj?.content === false}
<p <p
class="text-xs font-bold text-error-500 uppercase tracking-widest" class="text-error-500 text-xs font-bold tracking-widest uppercase">
>
Decryption failed. Incorrect passcode. Decryption failed. Incorrect passcode.
</p> </p>
{/if} {/if}
</div> </div>
{#if $ae_loc.edit_mode && on_force_reset} {#if $ae_loc.edit_mode && on_force_reset}
<div class="pt-4 border-t border-error-500/20"> <div class="border-error-500/20 border-t pt-4">
<p class="text-xs mb-2 opacity-70 italic"> <p class="mb-2 text-xs italic opacity-70">
Passcode lost? You can force a reset to plain text, Passcode lost? You can force a reset to plain text,
but all currently encrypted data will be permanently but all currently encrypted data will be permanently
deleted. deleted.
@@ -82,8 +79,7 @@
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold" class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold"
onclick={on_force_reset} onclick={on_force_reset}>
>
<RefreshCcw size="1.1em" class="mr-2" /> Force Reset to <RefreshCcw size="1.1em" class="mr-2" /> Force Reset to
Plain Text Plain Text
</button> </button>
@@ -98,14 +94,12 @@
bind:editor_view bind:editor_view
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
placeholder="Write using Markdown..." placeholder="Write using Markdown..."
class_li="p-2 preset-outlined-warning-300-700 shadow-lg rounded-lg w-full max-w-6xl bg-surface-50 dark:bg-surface-800" class_li="p-2 preset-outlined-warning-300-700 shadow-lg rounded-lg w-full max-w-6xl bg-surface-50 dark:bg-surface-800" />
/>
{:else} {:else}
<textarea <textarea
bind:value={tmp_entry_obj.content} bind:value={tmp_entry_obj.content}
class="textarea grow w-full max-w-6xl p-4 font-mono shadow-lg rounded-lg border-orange-500/30 h-[500px] whitespace-pre-wrap break-words" class="textarea h-[500px] w-full max-w-6xl grow rounded-lg border-orange-500/30 p-4 font-mono break-words whitespace-pre-wrap shadow-lg"
placeholder="Edit content..." placeholder="Edit content..."></textarea>
></textarea>
{/if} {/if}
<!-- Floating Save Button --> <!-- Floating Save Button -->
@@ -113,9 +107,8 @@
type="button" type="button"
onclick={on_save} onclick={on_save}
disabled={!has_changed} disabled={!has_changed}
class="btn btn-sm md:btn-md lg:btn-lg fixed top-72 right-6 min-w-32 preset-filled-success shadow-xl z-20 transition-all" class="btn btn-sm md:btn-md lg:btn-lg preset-filled-success fixed top-72 right-6 z-20 min-w-32 shadow-xl transition-all"
class:hidden={!has_changed} class:hidden={!has_changed}>
>
<Save size="1.2em" class="mr-2" /> Save <Save size="1.2em" class="mr-2" /> Save
</button> </button>
@@ -124,16 +117,14 @@
type="button" type="button"
onclick={on_save} onclick={on_save}
disabled={!has_changed} disabled={!has_changed}
class="btn preset-tonal-warning hover:preset-filled-warning-500 w-full max-w-96 mt-4" class="btn preset-tonal-warning hover:preset-filled-warning-500 mt-4 w-full max-w-96"
class:invisible={!has_changed} class:invisible={!has_changed}>
>
<Save size="1.2em" class="mr-2" /> Save Changes <Save size="1.2em" class="mr-2" /> Save Changes
</button> </button>
{#if updated_idb} {#if updated_idb}
<p <p
class="text-xs text-error-500 mt-2 font-bold animate-pulse uppercase tracking-widest" class="text-error-500 mt-2 animate-pulse text-xs font-bold tracking-widest uppercase">
>
IDB object updated since last load! IDB object updated since last load!
</p> </p>
{/if} {/if}

View File

@@ -1,18 +1,31 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_header.svelte * ae_comp__journal_entry_header.svelte
* Standardized Journal Entry Header. * Standardized Journal Entry Header.
* Manages name, sync status, and triggers the modular config. * Manages name, sync status, and triggers the modular config.
*/ */
import { ChevronLeft, CircleCheck, CircleX, Eye, Fingerprint, LoaderCircle, LockKeyhole, LockKeyholeOpen, Pencil, RefreshCw, Save, Settings } from '@lucide/svelte'; import {
import { ae_util } from '$lib/ae_utils/ae_utils'; ChevronLeft,
import { CircleCheck,
CircleX,
Eye,
Fingerprint,
LoaderCircle,
LockKeyhole,
LockKeyholeOpen,
Pencil,
RefreshCw,
Save,
Settings
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
journal: ae_Journal; journal: ae_Journal;
journals_li?: ae_Journal[]; journals_li?: ae_Journal[];
@@ -23,9 +36,9 @@
on_decrypt: () => void; on_decrypt: () => void;
on_show_config: () => void; on_show_config: () => void;
log_lvl?: number; log_lvl?: number;
} }
let { let {
entry, entry,
journal, journal,
tmp_entry_obj = $bindable(), tmp_entry_obj = $bindable(),
@@ -35,14 +48,13 @@
on_decrypt, on_decrypt,
on_show_config, on_show_config,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
const is_decrypted = $derived( const is_decrypted = $derived(
$journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted === $journals_sess?.journal_kv[journal?.id]?.journal_passcode_decrypted === true
true );
);
function toggle_edit_mode() { function toggle_edit_mode() {
const isEditing = const isEditing =
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'; $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current';
if (isEditing) { if (isEditing) {
@@ -51,25 +63,23 @@
} else { } else {
$journals_loc.entry.edit_kv[entry.journal_entry_id] = 'current'; $journals_loc.entry.edit_kv[entry.journal_entry_id] = 'current';
} }
} }
</script> </script>
<header <header
class="flex flex-col md:flex-row items-center justify-between gap-4 p-3 class="flex w-full flex-col items-center justify-between gap-4 rounded-xl
bg-gray-50 dark:bg-gray-800 border border-gray-200
border border-gray-200 dark:border-gray-700 bg-gray-50 p-3 shadow-sm
rounded-xl shadow-sm w-full" md:flex-row dark:border-gray-700 dark:bg-gray-800">
> <div class="flex w-full items-center gap-3 md:w-auto">
<div class="flex items-center gap-3 w-full md:w-auto">
<a <a
href="/journals/{journal.journal_id}" href="/journals/{journal.journal_id}"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
title="Back to Journal" title="Back to Journal">
>
<ChevronLeft size="1.2em" /> <ChevronLeft size="1.2em" />
</a> </a>
<div class="flex items-center gap-2 grow"> <div class="flex grow items-center gap-2">
<button <button
type="button" type="button"
onclick={toggle_edit_mode} onclick={toggle_edit_mode}
@@ -77,12 +87,10 @@
$journals_loc.entry.edit_kv[entry.journal_entry_id] === $journals_loc.entry.edit_kv[entry.journal_entry_id] ===
'current' 'current'
? 'preset-filled-success' ? 'preset-filled-success'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}">
>
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'} {#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
{#if has_changed}<Save size="1.2em" />{:else}<Eye {#if has_changed}<Save size="1.2em" />{:else}<Eye
size="1.2em" size="1.2em" />{/if}
/>{/if}
{:else} {:else}
<Pencil size="1.2em" /> <Pencil size="1.2em" />
{/if} {/if}
@@ -92,12 +100,11 @@
<input <input
type="text" type="text"
bind:value={tmp_entry_obj.name} bind:value={tmp_entry_obj.name}
class="input input-sm font-bold text-lg grow md:min-w-[300px] border-none bg-transparent focus:ring-2 focus:ring-primary-500" class="input input-sm focus:ring-primary-500 grow border-none bg-transparent text-lg font-bold focus:ring-2 md:min-w-[300px]"
placeholder="Entry Title..." placeholder="Entry Title..."
onchange={on_save} onchange={on_save} />
/>
{:else} {:else}
<h2 class="text-base md:text-lg font-bold truncate max-w-md"> <h2 class="max-w-md truncate text-base font-bold md:text-lg">
{entry.name || {entry.name ||
ae_util.iso_datetime_formatter( ae_util.iso_datetime_formatter(
entry.created_on, entry.created_on,
@@ -108,12 +115,11 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-2 w-full md:w-auto justify-end"> <div class="flex w-full items-center justify-end gap-2 md:w-auto">
<!-- Auto-Save indicator --> <!-- Auto-Save indicator -->
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'} {#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
<div <div
class="flex items-center gap-1 px-2 border-r border-surface-500/20 mr-1" class="border-surface-500/20 mr-1 flex items-center gap-1 border-r px-2">
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm {$journals_loc.entry.auto_save class="btn-icon btn-icon-sm {$journals_loc.entry.auto_save
@@ -122,19 +128,16 @@
onclick={() => onclick={() =>
($journals_loc.entry.auto_save = ($journals_loc.entry.auto_save =
!$journals_loc.entry.auto_save)} !$journals_loc.entry.auto_save)}
title="Toggle Auto Save" title="Toggle Auto Save">
>
<RefreshCw size="1em" /> <RefreshCw size="1em" />
</button> </button>
{#if $journals_loc.entry.auto_save} {#if $journals_loc.entry.auto_save}
{#if save_status === 'saving'}<LoaderCircle {#if save_status === 'saving'}<LoaderCircle
size="1em" size="1em"
class="animate-spin text-primary-500" class="text-primary-500 animate-spin" />
/>
{:else if save_status === 'saved'}<CircleCheck {:else if save_status === 'saved'}<CircleCheck
size="1em" size="1em"
class="text-success-500" class="text-success-500" />
/>
{:else}<CircleX size="1em" class="opacity-30" />{/if} {:else}<CircleX size="1em" class="opacity-30" />{/if}
{/if} {/if}
</div> </div>
@@ -145,25 +148,22 @@
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm transition-all {is_decrypted class="btn-icon btn-icon-sm transition-all {is_decrypted
? 'preset-filled-success shadow-lg shadow-success-500/20' ? 'preset-filled-success shadow-success-500/20 shadow-lg'
: 'preset-tonal-warning'}" : 'preset-tonal-warning'}"
onclick={on_decrypt} onclick={on_decrypt}
title={is_decrypted ? 'Lock Content' : 'Decrypt Content'} title={is_decrypted ? 'Lock Content' : 'Decrypt Content'}>
>
{#if is_decrypted}<LockKeyholeOpen {#if is_decrypted}<LockKeyholeOpen
size="1.2em" size="1.2em" />{:else}<LockKeyhole size="1.2em" />{/if}
/>{:else}<LockKeyhole size="1.2em" />{/if}
</button> </button>
{/if} {/if}
<div class="w-[1px] h-6 bg-surface-500/20 mx-1"></div> <div class="bg-surface-500/20 mx-1 h-6 w-[1px]"></div>
<!-- Unified Config Button --> <!-- Unified Config Button -->
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-primary font-bold" class="btn btn-sm preset-tonal-primary font-bold"
onclick={on_show_config} onclick={on_show_config}>
>
<Settings size="1.1em" class="mr-2" /> Config <Settings size="1.1em" class="mr-2" /> Config
</button> </button>
@@ -172,8 +172,7 @@
<button <button
type="button" type="button"
class="btn btn-sm preset-filled-primary" class="btn btn-sm preset-filled-primary"
onclick={on_save} onclick={on_save}>
>
<Save size="1.1em" class="mr-2" /> Save <Save size="1.1em" class="mr-2" /> Save
</button> </button>
{/if} {/if}

View File

@@ -1,25 +1,24 @@
<script lang="ts"> <script lang="ts">
/** /**
* JournalEntry_Metadata.svelte * JournalEntry_Metadata.svelte
* Extracted 2026-01-08 to modularize the massive Journal Entry God Component. * Extracted 2026-01-08 to modularize the massive Journal Entry God Component.
* Displays creation, update, and original datetime/timezone information. * Displays creation, update, and original datetime/timezone information.
*/ */
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
import type { ae_JournalEntry } from '$lib/types/ae_types'; import type { ae_JournalEntry } from '$lib/types/ae_types';
interface Props { interface Props {
entry: ae_JournalEntry; entry: ae_JournalEntry;
} }
let { entry }: Props = $props(); let { entry }: Props = $props();
</script> </script>
<section class="journal-metadata w-full space-y-4"> <section class="journal-metadata w-full space-y-4">
<!-- System Timestamps --> <!-- System Timestamps -->
<div <div
class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1 text-xs text-surface-500" class="text-surface-500 flex flex-col items-center justify-between gap-2 px-1 text-xs sm:flex-row">
>
<div class="flex gap-4"> <div class="flex gap-4">
<span title="Creation date"> <span title="Creation date">
<span class="font-semibold">Created:</span> <span class="font-semibold">Created:</span>

View File

@@ -1,18 +1,26 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_entry_obj_file_li.svelte * ae_comp__journal_entry_obj_file_li.svelte
* Manages and displays file attachments for Journal Entries. * Manages and displays file attachments for Journal Entries.
* Ported/Refactored 2026-01-26 to include View mode and strictly snake_case. * Ported/Refactored 2026-01-26 to include View mode and strictly snake_case.
*/ */
// *** Import Lucide Icons // *** Import Lucide Icons
import { Download, ExternalLink, FileUp, LoaderCircle, Paperclip, RefreshCw, Trash2 } from '@lucide/svelte'; import {
// *** Import Aether specific variables and functions Download,
import type { key_val } from '$lib/stores/ae_stores'; ExternalLink,
import { ae_util } from '$lib/ae_utils/ae_utils'; FileUp,
import { core_func } from '$lib/ae_core/ae_core_functions'; LoaderCircle,
import { api } from '$lib/api/api'; Paperclip,
import { RefreshCw,
Trash2
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { core_func } from '$lib/ae_core/ae_core_functions';
import { api } from '$lib/api/api';
import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
@@ -20,49 +28,49 @@
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_trig, journals_trig,
journals_prom journals_prom
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import Comp_hosted_files_upload from '$lib/ae_core/ae_comp__hosted_files_upload.svelte'; import Comp_hosted_files_upload from '$lib/ae_core/ae_comp__hosted_files_upload.svelte';
import Element_manage_hosted_file_li_wrap from '$lib/elements/element_manage_hosted_file_li_all.svelte'; import Element_manage_hosted_file_li_wrap from '$lib/elements/element_manage_hosted_file_li_all.svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte'; import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
link_to_type: string; link_to_type: string;
link_to_id: string; link_to_id: string;
lq__journal_entry_obj: any; lq__journal_entry_obj: any;
} }
let { let {
log_lvl = 0, log_lvl = 0,
link_to_type, link_to_type,
link_to_id, link_to_id,
lq__journal_entry_obj lq__journal_entry_obj
}: Props = $props(); }: Props = $props();
// *** State // *** State
let ae_promises: Record<string, any> = $state({}); let ae_promises: Record<string, any> = $state({});
let upload_complete: boolean = $state(false); let upload_complete: boolean = $state(false);
// Selection state for "Select Existing" mode // Selection state for "Select Existing" mode
let slct_hosted_file_kv: key_val = $state({}); let slct_hosted_file_kv: key_val = $state({});
let slct_hosted_file_id: string | null = $state(null); let slct_hosted_file_id: string | null = $state(null);
let slct_hosted_file_obj: any = $state(null); let slct_hosted_file_obj: any = $state(null);
// Selection state for "Upload" mode // Selection state for "Upload" mode
let slct_hosted_file_id_li: string[] = $state([]); let slct_hosted_file_id_li: string[] = $state([]);
let slct_hosted_file_obj_li: any[] = $state([]); let slct_hosted_file_obj_li: any[] = $state([]);
// Derived: Unified file list from both legacy data_json and new linked_li_json // Derived: Unified file list from both legacy data_json and new linked_li_json
let unified_file_li = $derived.by(() => { let unified_file_li = $derived.by(() => {
const entry = $lq__journal_entry_obj; const entry = $lq__journal_entry_obj;
if (!entry) return []; if (!entry) return [];
@@ -80,9 +88,8 @@
if ( if (
!files.find( !files.find(
(f) => (f) =>
(f.hosted_file_id || (f.hosted_file_id || f.id || f.hosted_file_id) ===
f.id || id
f.hosted_file_id) === id
) )
) { ) {
files.push({ ...obj, id: id }); files.push({ ...obj, id: id });
@@ -92,9 +99,9 @@
} }
return files; return files;
}); });
async function update_journal_entry(updated_files: any[]) { async function update_journal_entry(updated_files: any[]) {
let data_kv: key_val = { let data_kv: key_val = {
linked_li_json: JSON.stringify(updated_files), linked_li_json: JSON.stringify(updated_files),
// Maintain data_json.hosted_file_kv for backward compatibility // Maintain data_json.hosted_file_kv for backward compatibility
@@ -119,17 +126,13 @@
} catch (error) { } catch (error) {
console.error('Error updating journal entry files:', error); console.error('Error updating journal entry files:', error);
} }
} }
// *** Effects for File Management // *** Effects for File Management
// Handle "Select Existing" completion // Handle "Select Existing" completion
$effect(() => { $effect(() => {
if ( if ($lq__journal_entry_obj && slct_hosted_file_id && slct_hosted_file_obj) {
$lq__journal_entry_obj &&
slct_hosted_file_id &&
slct_hosted_file_obj
) {
const new_file = { const new_file = {
...slct_hosted_file_obj, ...slct_hosted_file_obj,
id: slct_hosted_file_id id: slct_hosted_file_id
@@ -141,10 +144,10 @@
update_journal_entry(updated_li); update_journal_entry(updated_li);
} }
}); });
// Handle "Upload" completion // Handle "Upload" completion
$effect(() => { $effect(() => {
if ( if (
$lq__journal_entry_obj && $lq__journal_entry_obj &&
upload_complete && upload_complete &&
@@ -161,9 +164,9 @@
update_journal_entry(updated_li); update_journal_entry(updated_li);
} }
}); });
async function handle_remove_file(file_id: string) { async function handle_remove_file(file_id: string) {
if (!confirm('Are you sure you want to remove this file attachment?')) if (!confirm('Are you sure you want to remove this file attachment?'))
return; return;
@@ -185,21 +188,19 @@
} }
await update_journal_entry(updated_li); await update_journal_entry(updated_li);
} }
</script> </script>
<section class="ae_section journal_entry_files w-full space-y-4 my-2"> <section class="ae_section journal_entry_files my-2 w-full space-y-4">
<!-- Header --> <!-- Header -->
<div <div
class="flex items-center justify-between border-b border-surface-500/20 pb-2" class="border-surface-500/20 flex items-center justify-between border-b pb-2">
>
<h3 class="h3 flex items-center gap-2 text-lg font-bold"> <h3 class="h3 flex items-center gap-2 text-lg font-bold">
<Paperclip size="1.1em" /> <Paperclip size="1.1em" />
Attachments Attachments
{#if unified_file_li.length} {#if unified_file_li.length}
<span class="badge preset-tonal-surface text-xs" <span class="badge preset-tonal-surface text-xs"
>{unified_file_li.length}</span >{unified_file_li.length}</span>
>
{/if} {/if}
</h3> </h3>
@@ -212,8 +213,7 @@
$ae_sess.files.add_to_use_files_method === 'upload' $ae_sess.files.add_to_use_files_method === 'upload'
? 'select' ? 'select'
: 'upload'; : 'upload';
}} }}>
>
<RefreshCw size="1em" class="mr-2" /> <RefreshCw size="1em" class="mr-2" />
{$ae_sess.files.add_to_use_files_method === 'select' {$ae_sess.files.add_to_use_files_method === 'select'
? 'Switch to Upload' ? 'Switch to Upload'
@@ -224,14 +224,13 @@
<!-- File Grid --> <!-- File Grid -->
{#if unified_file_li.length} {#if unified_file_li.length}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{#each unified_file_li as file (file.hosted_file_id ?? file.id)} {#each unified_file_li as file (file.hosted_file_id ?? file.id)}
{@const file_id = {@const file_id =
file.hosted_file_id || file.id || file.hosted_file_id} file.hosted_file_id || file.id || file.hosted_file_id}
<div <div
class="flex items-center justify-between p-3 rounded-xl bg-surface-50-950 border border-surface-500/10 group hover:border-primary-500 transition-all shadow-sm" class="bg-surface-50-950 border-surface-500/10 group hover:border-primary-500 flex items-center justify-between rounded-xl border p-3 shadow-sm transition-all">
> <div class="flex grow items-center gap-3 overflow-hidden">
<div class="flex items-center gap-3 overflow-hidden grow">
<AE_Comp_Hosted_Files_Download_Button <AE_Comp_Hosted_Files_Download_Button
hosted_file_id={file_id} hosted_file_id={file_id}
hosted_file_obj={file} hosted_file_obj={file}
@@ -241,18 +240,16 @@
show_divider={true} show_divider={true}
max_filename={25} max_filename={25}
show_direct_download={$ae_loc.trusted_access && show_direct_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode} $ae_loc.edit_mode} />
/>
</div> </div>
<div class="flex items-center gap-1 ml-2"> <div class="ml-2 flex items-center gap-1">
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error" class="btn btn-sm preset-tonal-error"
onclick={() => handle_remove_file(file_id)} onclick={() => handle_remove_file(file_id)}
title="Remove attachment" title="Remove attachment">
>
<Trash2 size="1.1em" /> <Trash2 size="1.1em" />
</button> </button>
{/if} {/if}
@@ -262,8 +259,7 @@
</div> </div>
{:else if !$ae_loc.edit_mode} {:else if !$ae_loc.edit_mode}
<p <p
class="text-sm text-surface-500 italic p-4 text-center bg-surface-500/5 rounded-xl" class="text-surface-500 bg-surface-500/5 rounded-xl p-4 text-center text-sm italic">
>
No files attached to this entry. No files attached to this entry.
</p> </p>
{/if} {/if}
@@ -271,8 +267,7 @@
<!-- Edit/Management Tools --> <!-- Edit/Management Tools -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div <div
class="mt-4 p-4 rounded-2xl bg-surface-500/5 border-2 border-dashed border-surface-500/20" class="bg-surface-500/5 border-surface-500/20 mt-4 rounded-2xl border-2 border-dashed p-4">
>
{#if $ae_sess.files.add_to_use_files_method === 'upload'} {#if $ae_sess.files.add_to_use_files_method === 'upload'}
<Comp_hosted_files_upload <Comp_hosted_files_upload
{link_to_type} {link_to_type}
@@ -282,13 +277,11 @@
bind:hosted_file_obj_li={slct_hosted_file_obj_li} bind:hosted_file_obj_li={slct_hosted_file_obj_li}
bind:hosted_file_obj_kv={slct_hosted_file_kv} bind:hosted_file_obj_kv={slct_hosted_file_kv}
accept="*/*" accept="*/*"
class_li="!max-w-none" class_li="!max-w-none">
>
{#snippet label()} {#snippet label()}
<div class="flex flex-col items-center gap-2 py-2"> <div class="flex flex-col items-center gap-2 py-2">
<div <div
class="p-3 rounded-full bg-primary-500/10 text-primary-500" class="bg-primary-500/10 text-primary-500 rounded-full p-3">
>
<FileUp size="1.8em" /> <FileUp size="1.8em" />
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -302,7 +295,7 @@
</Comp_hosted_files_upload> </Comp_hosted_files_upload>
{:else} {:else}
<div class="space-y-2"> <div class="space-y-2">
<p class="text-sm font-bold opacity-70 ml-1"> <p class="ml-1 text-sm font-bold opacity-70">
Select from existing hosted files: Select from existing hosted files:
</p> </p>
<Element_manage_hosted_file_li_wrap <Element_manage_hosted_file_li_wrap
@@ -313,8 +306,7 @@
class_li={''} class_li={''}
bind:slct_hosted_file_kv bind:slct_hosted_file_kv
bind:slct_hosted_file_id bind:slct_hosted_file_id
bind:slct_hosted_file_obj bind:slct_hosted_file_obj />
/>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -1,69 +1,69 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
/** /**
* ae_comp__journal_entry_obj_id_view.svelte * ae_comp__journal_entry_obj_id_view.svelte
* Reference Implementation for Journal Entry View/Edit * Reference Implementation for Journal Entry View/Edit
* Corrected for V3 API Strictness & Robust Decryption * Corrected for V3 API Strictness & Robust Decryption
* Uses strictly snake_case. * Uses strictly snake_case.
*/ */
// *** Import Svelte core // *** Import Svelte core
// import { goto } from '$app/navigation'; // import { goto } from '$app/navigation';
// *** Import secondary libraries // *** Import secondary libraries
import { marked } from 'marked'; import { marked } from 'marked';
// *** Import Aether components and helpers // *** Import Aether components and helpers
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct journals_slct
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { decrypt_journal_entry } from '$lib/ae_journals/ae_journals_decryption'; import { decrypt_journal_entry } from '$lib/ae_journals/ae_journals_decryption';
import AE_Comp_Journal_Entry_Editor from './ae_comp__journal_entry_editor.svelte'; import AE_Comp_Journal_Entry_Editor from './ae_comp__journal_entry_editor.svelte';
import AE_Comp_Journal_Entry_Header from './ae_comp__journal_entry_header.svelte'; import AE_Comp_Journal_Entry_Header from './ae_comp__journal_entry_header.svelte';
import AE_Comp_Journal_Entry_Metadata from './ae_comp__journal_entry_metadata.svelte'; import AE_Comp_Journal_Entry_Metadata from './ae_comp__journal_entry_metadata.svelte';
import AE_Comp_Journal_Entry_AiTools from './ae_comp__journal_entry_ai_tools.svelte'; import AE_Comp_Journal_Entry_AiTools from './ae_comp__journal_entry_ai_tools.svelte';
import AE_Comp_Journal_Entry_ObjFileLi from './ae_comp__journal_entry_obj_file_li.svelte'; import AE_Comp_Journal_Entry_ObjFileLi from './ae_comp__journal_entry_obj_file_li.svelte';
import AE_Comp_Modal_Journal_Entry_Append from './ae_comp__modal_journal_entry_append.svelte'; import AE_Comp_Modal_Journal_Entry_Append from './ae_comp__modal_journal_entry_append.svelte';
import AE_Comp_Modal_Journal_Entry_Config from './ae_comp__modal_journal_entry_config.svelte'; import AE_Comp_Modal_Journal_Entry_Config from './ae_comp__modal_journal_entry_config.svelte';
// Icons // Icons
import { CircleAlert, CircleX, LoaderCircle } from '@lucide/svelte'; import { CircleAlert, CircleX, LoaderCircle } from '@lucide/svelte';
// *** Props // *** Props
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_obj_li: any; lq__journal_obj_li: any;
lq__journal_entry_obj: any; lq__journal_entry_obj: any;
on_show_export?: () => void; on_show_export?: () => void;
} }
let { let {
log_lvl = 0, log_lvl = 0,
lq__journal_obj, lq__journal_obj,
lq__journal_obj_li, lq__journal_obj_li,
lq__journal_entry_obj, lq__journal_entry_obj,
on_show_export on_show_export
}: Props = $props(); }: Props = $props();
// *** State // *** State
let editor_view: any = $state(); let editor_view: any = $state();
let orig_entry_obj: key_val | null = $state(null); let orig_entry_obj: key_val | null = $state(null);
let tmp_entry_obj: key_val = $state({}); let tmp_entry_obj: key_val = $state({});
let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved'); let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
let decryption_error: string | null = $state(null); let decryption_error: string | null = $state(null);
let auto_save_timer: ReturnType<typeof setTimeout>; let auto_save_timer: ReturnType<typeof setTimeout>;
let is_processing = $state(false); let is_processing = $state(false);
let show_config_modal = $state(false); let show_config_modal = $state(false);
// *** Helpers // *** Helpers
function deep_copy(obj: any) { function deep_copy(obj: any) {
if (!obj) return null; if (!obj) return null;
try { try {
const copy: key_val = {}; const copy: key_val = {};
@@ -84,23 +84,21 @@
console.error('deep_copy failed:', e); console.error('deep_copy failed:', e);
return { ...obj }; return { ...obj };
} }
} }
// *** Derived // *** Derived
let has_unsaved_changes = $derived.by(() => { let has_unsaved_changes = $derived.by(() => {
if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false; if (!tmp_entry_obj || !orig_entry_obj || is_processing) return false;
const normalize = (val: any) => const normalize = (val: any) =>
val === null || val === undefined ? '' : String(val).trim(); val === null || val === undefined ? '' : String(val).trim();
const content_changed = const content_changed =
normalize(tmp_entry_obj.content) !== normalize(tmp_entry_obj.content) !== normalize(orig_entry_obj.content);
normalize(orig_entry_obj.content);
const name_changed = const name_changed =
normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name); normalize(tmp_entry_obj.name) !== normalize(orig_entry_obj.name);
const private_changed = const private_changed =
(tmp_entry_obj.private ?? false) !== (tmp_entry_obj.private ?? false) !== (orig_entry_obj.private ?? false);
(orig_entry_obj.private ?? false);
if (content_changed || name_changed || private_changed) return true; if (content_changed || name_changed || private_changed) return true;
@@ -115,19 +113,18 @@
]; ];
for (const field of other_fields) { for (const field of other_fields) {
if ( if (
normalize(tmp_entry_obj[field]) !== normalize(tmp_entry_obj[field]) !== normalize(orig_entry_obj[field])
normalize(orig_entry_obj[field])
) )
return true; return true;
} }
return false; return false;
}); });
// *** Effects // *** Effects
// 1. Initial Load & Background Sync // 1. Initial Load & Background Sync
$effect(() => { $effect(() => {
const entry = $lq__journal_entry_obj; // Track only entry const entry = $lq__journal_entry_obj; // Track only entry
untrack(() => { untrack(() => {
@@ -135,15 +132,10 @@
if (!entry || !(entry.updated_on || entry.created_on)) return; if (!entry || !(entry.updated_on || entry.created_on)) return;
const session_kv = $journals_sess?.journal_kv[journal?.id]; const session_kv = $journals_sess?.journal_kv[journal?.id];
const is_decrypted = const is_decrypted = session_kv?.journal_passcode_decrypted === true;
session_kv?.journal_passcode_decrypted === true;
// Only sync if saved and not currently processing/editing // Only sync if saved and not currently processing/editing
if ( if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
save_status === 'saved' &&
!has_unsaved_changes &&
!is_processing
) {
// Prevent overwrite of recovered text if we are in a decrypted session // Prevent overwrite of recovered text if we are in a decrypted session
if (is_decrypted && tmp_entry_obj.content && !entry.content) { if (is_decrypted && tmp_entry_obj.content && !entry.content) {
return; return;
@@ -158,28 +150,23 @@
} }
} }
}); });
}); });
// 2. Auto-Save Debounce // 2. Auto-Save Debounce
$effect(() => { $effect(() => {
// Track core properties // Track core properties
const _content = tmp_entry_obj.content; const _content = tmp_entry_obj.content;
const _private = tmp_entry_obj.private; const _private = tmp_entry_obj.private;
// Isolate logic from secondary dependencies // Isolate logic from secondary dependencies
const should_save = untrack( const should_save = untrack(
() => () => has_unsaved_changes && !is_processing && save_status !== 'saving'
has_unsaved_changes &&
!is_processing &&
save_status !== 'saving'
); );
if (should_save) { if (should_save) {
if (save_status !== 'saving') save_status = 'unsaved'; if (save_status !== 'saving') save_status = 'unsaved';
const auto_save_enabled = untrack( const auto_save_enabled = untrack(() => $journals_loc.entry.auto_save);
() => $journals_loc.entry.auto_save
);
if (auto_save_enabled) { if (auto_save_enabled) {
clearTimeout(auto_save_timer); clearTimeout(auto_save_timer);
auto_save_timer = setTimeout(() => { auto_save_timer = setTimeout(() => {
@@ -198,10 +185,10 @@
} else if (save_status === 'unsaved' && !has_unsaved_changes) { } else if (save_status === 'unsaved' && !has_unsaved_changes) {
save_status = 'saved'; save_status = 'saved';
} }
}); });
// 3. Auto-Decryption Workflow // 3. Auto-Decryption Workflow
$effect(() => { $effect(() => {
const journal = $lq__journal_obj; const journal = $lq__journal_obj;
const entry = $lq__journal_entry_obj; const entry = $lq__journal_entry_obj;
if (!journal?.id || !entry) return; if (!journal?.id || !entry) return;
@@ -220,11 +207,11 @@
) { ) {
untrack(() => run_decryption_workflow()); untrack(() => run_decryption_workflow());
} }
}); });
// *** Actions // *** Actions
async function run_decryption_workflow() { async function run_decryption_workflow() {
const journal = $lq__journal_obj; const journal = $lq__journal_obj;
if (!journal?.id || is_processing) return; if (!journal?.id || is_processing) return;
@@ -280,14 +267,10 @@
return s; return s;
}); });
is_processing = false; is_processing = false;
} }
async function update_journal_entry(fields_kv?: key_val) { async function update_journal_entry(fields_kv?: key_val) {
if ( if (!$ae_loc.trusted_access || save_status === 'saving' || is_processing)
!$ae_loc.trusted_access ||
save_status === 'saving' ||
is_processing
)
return; return;
is_processing = true; is_processing = true;
@@ -356,9 +339,9 @@
} finally { } finally {
is_processing = false; is_processing = false;
} }
} }
async function handle_content_decryption() { async function handle_content_decryption() {
const journal = $lq__journal_obj; const journal = $lq__journal_obj;
const entry = $lq__journal_entry_obj; const entry = $lq__journal_entry_obj;
if (!journal?.id || !entry || is_processing) return; if (!journal?.id || !entry || is_processing) return;
@@ -379,14 +362,14 @@
}); });
is_processing = false; is_processing = false;
} }
} }
function handle_marked(text: string) { function handle_marked(text: string) {
if (!text) return ''; if (!text) return '';
return marked.parse(text.replace(/^[]/, '')); return marked.parse(text.replace(/^[]/, ''));
} }
async function handle_force_reset() { async function handle_force_reset() {
if ( if (
!confirm( !confirm(
'WARNING: This will permanently DELETE the encrypted content and history for this entry and reset it to plain text. This cannot be undone. Proceed?' 'WARNING: This will permanently DELETE the encrypted content and history for this entry and reset it to plain text. This cannot be undone. Proceed?'
@@ -413,27 +396,26 @@
await update_journal_entry(); await update_journal_entry();
is_processing = false; is_processing = false;
} }
let show_append_modal = $state(false); let show_append_modal = $state(false);
let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto'); let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
</script> </script>
<div class="relative group/entry-view w-full"> <div class="group/entry-view relative w-full">
<!-- Subtle glow — same technique as the rest of the Journals module --> <!-- Subtle glow — same technique as the rest of the Journals module -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500/50 to-secondary-500/50 class="from-primary-500/50 to-secondary-500/50 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-5 dark:opacity-10 rounded-2xl bg-linear-to-r opacity-5 blur
group-hover/entry-view:opacity-15 dark:group-hover/entry-view:opacity-20 transition duration-700
transition duration-700 pointer-events-none" group-hover/entry-view:opacity-15 dark:opacity-10 dark:group-hover/entry-view:opacity-20">
></div> </div>
<section <section
class="ae_view relative flex flex-col gap-2 w-full h-full p-2 class="ae_view relative flex h-full w-full flex-col gap-2 rounded-xl
bg-white dark:bg-gray-900 border border-gray-200
border border-gray-200 dark:border-gray-700 bg-white p-2 shadow-xl
rounded-xl shadow-xl" dark:border-gray-700 dark:bg-gray-900"
bind:clientHeight={$ae_loc.iframe_height_modal_body} bind:clientHeight={$ae_loc.iframe_height_modal_body}>
>
{#if $lq__journal_entry_obj && $lq__journal_obj} {#if $lq__journal_entry_obj && $lq__journal_obj}
<AE_Comp_Journal_Entry_Header <AE_Comp_Journal_Entry_Header
entry={$lq__journal_entry_obj} entry={$lq__journal_entry_obj}
@@ -445,24 +427,20 @@
on_decrypt={handle_content_decryption} on_decrypt={handle_content_decryption}
on_show_config={() => (show_config_modal = true)} on_show_config={() => (show_config_modal = true)}
{save_status} {save_status}
{log_lvl} {log_lvl} />
/>
{#if decryption_error} {#if decryption_error}
<div <div
class="w-full p-4 bg-error-500/20 border-2 border-error-500 rounded-lg flex items-center justify-between shadow-2xl z-50 animate-bounce" class="bg-error-500/20 border-error-500 z-50 flex w-full animate-bounce items-center justify-between rounded-lg border-2 p-4 shadow-2xl">
>
<div <div
class="flex items-center gap-4 text-error-700 dark:text-error-300 font-bold" class="text-error-700 dark:text-error-300 flex items-center gap-4 font-bold">
>
<CircleAlert size="2.5em" /> <CircleAlert size="2.5em" />
<span class="text-xl">{decryption_error}</span> <span class="text-xl">{decryption_error}</span>
</div> </div>
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold" class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 font-bold"
onclick={() => (decryption_error = null)} onclick={() => (decryption_error = null)}>
>
<CircleX size="1.2em" class="mr-2" /> Dismiss <CircleX size="1.2em" class="mr-2" /> Dismiss
</button> </button>
</div> </div>
@@ -470,19 +448,20 @@
<!-- ring-2 inset indicates "edit mode" in both light and dark without a background swap --> <!-- ring-2 inset indicates "edit mode" in both light and dark without a background swap -->
<section <section
class="grow relative p-1 rounded-lg shadow-md overflow-hidden class="relative grow overflow-hidden rounded-lg border border-gray-200
bg-gray-50 dark:bg-gray-800 bg-gray-50 p-1
border border-gray-200 dark:border-gray-700 shadow-md dark:border-gray-700 dark:bg-gray-800
{$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current' {$journals_loc.entry.edit_kv[
? 'ring-2 ring-inset ring-primary-500/40' : ''}" $lq__journal_entry_obj?.journal_entry_id
> ] == 'current'
? 'ring-primary-500/40 ring-2 ring-inset'
: ''}">
<div class="absolute top-2 right-2 z-10"> <div class="absolute top-2 right-2 z-10">
<AE_Comp_Journal_Entry_AiTools <AE_Comp_Journal_Entry_AiTools
content={tmp_entry_obj.content} content={tmp_entry_obj.content}
bind:summary={tmp_entry_obj.summary} bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()} on_save={() => update_journal_entry()}
{log_lvl} {log_lvl} />
/>
</div> </div>
<AE_Comp_Journal_Entry_Editor <AE_Comp_Journal_Entry_Editor
@@ -493,16 +472,14 @@
has_changed={has_unsaved_changes} has_changed={has_unsaved_changes}
updated_idb={false} updated_idb={false}
on_save={() => update_journal_entry()} on_save={() => update_journal_entry()}
on_force_reset={handle_force_reset} on_force_reset={handle_force_reset} />
/>
</section> </section>
<AE_Comp_Journal_Entry_ObjFileLi <AE_Comp_Journal_Entry_ObjFileLi
{log_lvl} {log_lvl}
link_to_type="journal_entry" link_to_type="journal_entry"
link_to_id={$lq__journal_entry_obj.journal_entry_id} link_to_id={$lq__journal_entry_obj.journal_entry_id}
{lq__journal_entry_obj} {lq__journal_entry_obj} />
/>
<AE_Comp_Journal_Entry_Metadata entry={tmp_entry_obj as any} /> <AE_Comp_Journal_Entry_Metadata entry={tmp_entry_obj as any} />
@@ -515,8 +492,7 @@
on_update={() => { on_update={() => {
show_append_modal = false; show_append_modal = false;
}} }}
{log_lvl} {log_lvl} />
/>
<AE_Comp_Modal_Journal_Entry_Config <AE_Comp_Modal_Journal_Entry_Config
bind:show={show_config_modal} bind:show={show_config_modal}
@@ -534,15 +510,13 @@
modal_mode = 'prepend'; modal_mode = 'prepend';
show_append_modal = true; show_append_modal = true;
}} }}
{log_lvl} {log_lvl} />
/>
{:else} {:else}
<div <div
class="p-20 text-center opacity-50 flex flex-col items-center gap-4" class="flex flex-col items-center gap-4 p-20 text-center opacity-50">
>
<LoaderCircle class="animate-spin" size="4em" /> <LoaderCircle class="animate-spin" size="4em" />
<span class="text-2xl font-bold">Loading Journal Entry...</span> <span class="text-2xl font-bold">Loading Journal Entry...</span>
</div> </div>
{/if} {/if}
</section> </section>
</div> </div>

View File

@@ -1,64 +1,81 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_entry_obj_li: any; lq__journal_entry_obj_li: any;
show_found_header?: boolean; show_found_header?: boolean;
} }
let { let {
log_lvl = $bindable(0), log_lvl = $bindable(0),
lq__journal_obj, lq__journal_obj,
lq__journal_entry_obj_li, lq__journal_entry_obj_li,
show_found_header = true show_found_header = true
}: Props = $props(); }: Props = $props();
// *** Import Svelte specific // *** Import Svelte specific
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { BookOpenText, CalendarClock, Check, CodeXml, Copy, Eye, EyeOff, Files, Fingerprint, Flag, Group, ListPlus, LoaderCircle, Lock, NotebookPen, NotebookText, NotepadTextDashed, RemoveFormatting, Shapes, Siren, Tags, TypeOutline, X } from '@lucide/svelte'; import {
// *** Import Aether specific variables and functions BookOpenText,
import type { key_val } from '$lib/stores/ae_stores'; CalendarClock,
import { ae_util } from '$lib/ae_utils/ae_utils'; Check,
import { CodeXml,
ae_loc, Copy,
ae_sess, Eye,
ae_api, EyeOff,
ae_trig, Files,
slct Fingerprint,
} from '$lib/stores/ae_stores'; Flag,
import { Group,
ListPlus,
LoaderCircle,
Lock,
NotebookPen,
NotebookText,
NotepadTextDashed,
RemoveFormatting,
Shapes,
Siren,
Tags,
TypeOutline,
X
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, ae_trig, slct } from '$lib/stores/ae_stores';
import {
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_trig, journals_trig,
journals_loc journals_loc
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte'; import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
let tmp_entry_obj: key_val = $state({}); let tmp_entry_obj: key_val = $state({});
// Derived state for modal visibility // Derived state for modal visibility
// We cast to boolean for the prop, but we need to handle the close event to clear the store ID // We cast to boolean for the prop, but we need to handle the close event to clear the store ID
let show_append_modal = $state(false); let show_append_modal = $state(false);
$effect(() => { $effect(() => {
// Sync local boolean with store ID presence // Sync local boolean with store ID presence
show_append_modal = show_append_modal = !!$journals_sess.show__modal_append__journal_entry_id;
!!$journals_sess.show__modal_append__journal_entry_id; });
});
function handle_modal_close() { function handle_modal_close() {
$journals_sess.show__modal_append__journal_entry_id = null; $journals_sess.show__modal_append__journal_entry_id = null;
show_append_modal = false; show_append_modal = false;
} }
function handle_modal_update() { function handle_modal_update() {
handle_modal_close(); handle_modal_close();
} }
// Derived list of visible items (Standardized Search Pattern 2026-01-27) // Derived list of visible items (Standardized Search Pattern 2026-01-27)
// Ensures count matches exactly what is rendered to the user // Ensures count matches exactly what is rendered to the user
let visible_journal_entry_obj_li = $derived( let visible_journal_entry_obj_li = $derived(
(() => { (() => {
// Subscribe to the observable // Subscribe to the observable
const list = $lq__journal_entry_obj_li; const list = $lq__journal_entry_obj_li;
@@ -95,40 +112,38 @@
return filtered; return filtered;
})() })()
); );
</script> </script>
<div class="relative group/entries w-full"> <div class="group/entries relative w-full">
<!-- Subtle glow behind the entry list — same technique as the other journal sections --> <!-- Subtle glow behind the entry list — same technique as the other journal sections -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500/50 to-secondary-500/50 class="from-primary-500/50 to-secondary-500/50 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-5 dark:opacity-10 rounded-2xl bg-linear-to-r opacity-5 blur
group-hover/entries:opacity-15 dark:group-hover/entries:opacity-20 transition duration-700
transition duration-700 pointer-events-none" group-hover/entries:opacity-15 dark:opacity-10 dark:group-hover/entries:opacity-20">
></div> </div>
<section <section
class="journal_list relative flex flex-col gap-1 md:gap-2 items-center justify-center w-full" class="journal_list relative flex w-full flex-col items-center justify-center gap-1 md:gap-2">
>
{#if visible_journal_entry_obj_li === null} {#if visible_journal_entry_obj_li === null}
<!-- Loading state --> <!-- Loading state -->
<div class="flex flex-col items-center justify-center p-10 opacity-50"> <div
<LoaderCircle size="2em" class="animate-spin mb-2" /> class="flex flex-col items-center justify-center p-10 opacity-50">
<LoaderCircle size="2em" class="mb-2 animate-spin" />
<p>Loading visible entries...</p> <p>Loading visible entries...</p>
</div> </div>
{:else if visible_journal_entry_obj_li.length > 0} {:else if visible_journal_entry_obj_li.length > 0}
{#if show_found_header} {#if show_found_header}
<div class="w-full max-w-(--breakpoint-lg) mb-2"> <div class="mb-2 w-full max-w-(--breakpoint-lg)">
<h2 class="h4 flex items-center gap-2 px-2"> <h2 class="h4 flex items-center gap-2 px-2">
<span class="text-sm text-gray-500 font-normal"> <span class="text-sm font-normal text-gray-500">
Found: Found:
</span> </span>
<span <span
class="badge preset-tonal-success font-bold text-lg px-3 py-1" class="badge preset-tonal-success px-3 py-1 text-lg font-bold">
>
{visible_journal_entry_obj_li.length}<span {visible_journal_entry_obj_li.length}<span
class="text-gray-400 dark:text-gray-600" class="text-gray-400 dark:text-gray-600"
>&times;</span >&times;</span>
>
</span> </span>
</h2> </h2>
</div> </div>
@@ -137,52 +152,46 @@
{#each visible_journal_entry_obj_li as journals_journal_entry_obj, index (journals_journal_entry_obj.journal_entry_id)} {#each visible_journal_entry_obj_li as journals_journal_entry_obj, index (journals_journal_entry_obj.journal_entry_id)}
<div <div
class=" class="
container journal journal_entry_obj group/entry journal journal_entry_obj group/entry border-l-primary-500/40
border border-gray-200 dark:border-gray-700 hover:border-l-primary-500 container flex
border-l-4 border-l-primary-500/40
px-2 py-1 space-y-1
w-full max-w-(--breakpoint-lg) w-full max-w-(--breakpoint-lg)
flex flex-col items-center justify-center flex-col items-center justify-center
bg-white text-gray-900 space-y-1 rounded-lg
dark:bg-gray-800 dark:text-gray-200 border border-l-4 border-gray-200 bg-white
rounded-lg px-2 py-1
hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-900 transition-all
hover:border-l-primary-500 hover:shadow-md duration-200
transition-all duration-200 ease-out ease-out hover:bg-gray-50
hover:shadow-md dark:border-gray-700
dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700
" "
class:dim={!journals_journal_entry_obj.enable} class:dim={!journals_journal_entry_obj.enable}
class:bg-warning-100={!journals_journal_entry_obj?.enable} class:bg-warning-100={!journals_journal_entry_obj?.enable}>
>
<header <header
class="ae_header flex flex-row gap-2 items-center justify-between w-full" class="ae_header flex w-full flex-row items-center justify-between gap-2">
>
<span class="flex flex-row flex-wrap gap-1"> <span class="flex flex-row flex-wrap gap-1">
<span class="journal_entry__name *:hover:inline-block"> <span
class="journal_entry__name *:hover:inline-block">
{#if journals_journal_entry_obj.alert} {#if journals_journal_entry_obj.alert}
<Siren <Siren
size="1.25em" size="1.25em"
class="mx-1 inline-block text-red-500" class="mx-1 inline-block text-red-500" />
/>
{/if} {/if}
{#if journals_journal_entry_obj.priority} {#if journals_journal_entry_obj.priority}
<Flag <Flag
size="1.25em" size="1.25em"
class="mx-1 inline-block text-yellow-500" class="mx-1 inline-block text-yellow-500" />
/>
{/if} {/if}
{#if journals_journal_entry_obj.group} {#if journals_journal_entry_obj.group}
<Group <Group
size="1.25em" size="1.25em"
class="mx-1 inline-block text-green-500" class="mx-1 inline-block text-green-500" />
/> <span class="hidden text-xs text-gray-500"
<span class="text-xs text-gray-500 hidden" >Group:</span>
>Group:</span
>
<span <span
class="font-semibold text-sm text-gray-500 hidden md:inline" class="hidden text-sm font-semibold text-gray-500 md:inline">
>
{journals_journal_entry_obj.group} {journals_journal_entry_obj.group}
</span> </span>
{/if} {/if}
@@ -190,24 +199,20 @@
<h3 <h3
class:dim={journals_journal_entry_obj.hide} class:dim={journals_journal_entry_obj.hide}
class="journal__name h4" class="journal__name h4">
>
{#if journals_journal_entry_obj.template} {#if journals_journal_entry_obj.template}
<NotepadTextDashed <NotepadTextDashed
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" />
/>
{@html journals_journal_entry_obj.name ?? {@html journals_journal_entry_obj.name ??
'-- no name --'} '-- no name --'}
{:else if journals_journal_entry_obj.name} {:else if journals_journal_entry_obj.name}
<NotebookText <NotebookText
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" />
/>
{@html journals_journal_entry_obj.name} {@html journals_journal_entry_obj.name}
{:else} {:else}
<CalendarClock <CalendarClock
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60" />
/>
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
journals_journal_entry_obj.created_on, journals_journal_entry_obj.created_on,
'datetime_iso_12_no_seconds' 'datetime_iso_12_no_seconds'
@@ -216,8 +221,7 @@
</h3> </h3>
<span <span
class="flex flex-row flex-wrap gap-1 items-center justify-center" class="flex flex-row flex-wrap items-center justify-center gap-1">
>
{#if !journals_journal_entry_obj.private} {#if !journals_journal_entry_obj.private}
<!-- Button to copy the Markdown version --> <!-- Button to copy the Markdown version -->
<button <button
@@ -227,7 +231,9 @@
journals_journal_entry_obj; journals_journal_entry_obj;
navigator.clipboard navigator.clipboard
.writeText(tmp_entry_obj.content) .writeText(
tmp_entry_obj.content
)
.then(() => { .then(() => {
alert( alert(
'Markdown content copied to clipboard!' 'Markdown content copied to clipboard!'
@@ -245,9 +251,8 @@
}} }}
class:hidden={$lq__journal_obj?.cfg_json class:hidden={$lq__journal_obj?.cfg_json
?.hide_copy_plain_md} ?.hide_copy_plain_md}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm" class="btn btn-sm preset-tonal-surface hover:preset-filled-secondary-500 p-1 text-xs *:hover:inline lg:text-sm"
title="Copy the markdown content" title="Copy the markdown content">
>
<RemoveFormatting size="1.25em" /> <RemoveFormatting size="1.25em" />
<span class="hidden"> <span class="hidden">
Copy Plaintext Markdown Copy Plaintext Markdown
@@ -282,9 +287,8 @@
class:hidden={journals_journal_entry_obj.template || class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json $lq__journal_obj?.cfg_json
?.hide_copy_html} ?.hide_copy_html}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs" class="btn btn-sm preset-tonal-surface hover:preset-filled-secondary-500 p-1 *:hover:inline lg:text-xs"
title="Copy the rendered HTML content" title="Copy the rendered HTML content">
>
<CodeXml size="1.25em" /> <CodeXml size="1.25em" />
<span class="hidden"> <span class="hidden">
Copy HTML Markup Copy HTML Markup
@@ -295,7 +299,8 @@
<button <button
type="button" type="button"
onclick={async () => { onclick={async () => {
const element = document.getElementById( const element =
document.getElementById(
`rendered_journal_entry_content_${journals_journal_entry_obj.journal_entry_id}` `rendered_journal_entry_content_${journals_journal_entry_obj.journal_entry_id}`
); );
if (!element) { if (!element) {
@@ -311,16 +316,21 @@
try { try {
const htmlContent = const htmlContent =
element.innerHTML; element.innerHTML;
await navigator.clipboard.write([ await navigator.clipboard.write(
[
new ClipboardItem({ new ClipboardItem({
'text/html': new Blob( 'text/html':
[htmlContent], new Blob(
[
htmlContent
],
{ {
type: 'text/html' type: 'text/html'
} }
) )
}) })
]); ]
);
alert( alert(
'Rendered rich content copied to clipboard!' 'Rendered rich content copied to clipboard!'
@@ -338,11 +348,11 @@
class:hidden={journals_journal_entry_obj.template || class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json $lq__journal_obj?.cfg_json
?.hide_copy_rich} ?.hide_copy_rich}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs" class="btn btn-sm preset-tonal-surface hover:preset-filled-secondary-500 p-1 *:hover:inline lg:text-xs"
title="Copy the rich text (rendered HTML) content" title="Copy the rich text (rendered HTML) content">
>
<TypeOutline size="1.25em" /> <TypeOutline size="1.25em" />
<span class="hidden">Copy Rich Text</span> <span class="hidden"
>Copy Rich Text</span>
</button> </button>
<!-- Clone entry --> <!-- Clone entry -->
@@ -395,20 +405,18 @@
}); });
}} }}
class:hidden={!journals_journal_entry_obj.template} class:hidden={!journals_journal_entry_obj.template}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs" class="btn btn-sm preset-tonal-surface hover:preset-filled-secondary-500 p-1 *:hover:inline lg:text-xs"
title="Clone this journal entry" title="Clone this journal entry">
>
<Copy size="1.25em" /> <Copy size="1.25em" />
<span class="hidden md:inline">Clone</span> <span class="hidden md:inline"
>Clone</span>
</button> </button>
{:else} {:else}
<Lock <Lock
size="1.25em" size="1.25em"
class="mx-1 inline-block text-red-400 dark:text-red-600" class="mx-1 inline-block text-red-400 dark:text-red-600" />
/> <span class="hidden text-xs text-gray-500"
<span class="text-xs text-gray-500 hidden" >Private</span>
>Private</span
>
<!-- Button to copy the Markdown version --> <!-- Button to copy the Markdown version -->
<button <button
@@ -438,35 +446,35 @@
}} }}
class:hidden={$lq__journal_obj?.cfg_json class:hidden={$lq__journal_obj?.cfg_json
?.hide_copy_encrypted} ?.hide_copy_encrypted}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm" class="btn btn-sm preset-tonal-surface hover:preset-filled-secondary-500 p-1 text-xs *:hover:inline lg:text-sm"
title="Copy the encrypted content" title="Copy the encrypted content">
>
<Fingerprint size="1.25em" /> <Fingerprint size="1.25em" />
<span class="hidden"> Copy Encrypted </span> <span class="hidden">
Copy Encrypted
</span>
</button> </button>
{/if} {/if}
</span> </span>
</span> </span>
<div <div
class="flex flex-row flex-wrap gap-2 items-center justify-end" class="flex flex-row flex-wrap items-center justify-end gap-2">
>
<!-- Linked file count --> <!-- Linked file count -->
<div <div
class="ae_linked_file_count flex flex-row flex-wrap gap-0.5 items-center justify-start" class="ae_linked_file_count flex flex-row flex-wrap items-center justify-start gap-0.5"
class:hidden={!journals_journal_entry_obj?.data_json class:hidden={!journals_journal_entry_obj
?.hosted_file_kv} ?.data_json?.hosted_file_kv}>
>
<Files class="mx-1 inline-block" /> <Files class="mx-1 inline-block" />
<span class="text-xs text-gray-500 hidden md:inline" <span
>Linked files:</span class="hidden text-xs text-gray-500 md:inline"
> >Linked files:</span>
<span class="font-semibold text-sm text-gray-500"> <span
class="text-sm font-semibold text-gray-500">
{journals_journal_entry_obj?.data_json {journals_journal_entry_obj?.data_json
?.hosted_file_kv ?.hosted_file_kv
? Object.keys( ? Object.keys(
journals_journal_entry_obj?.data_json journals_journal_entry_obj
?.hosted_file_kv ?.data_json?.hosted_file_kv
).length ).length
: 0}× : 0}×
</span> </span>
@@ -475,19 +483,16 @@
<!-- Tags for journal entry. Comma delimited list. --> <!-- Tags for journal entry. Comma delimited list. -->
{#if journals_journal_entry_obj.tags && journals_journal_entry_obj.tags.length} {#if journals_journal_entry_obj.tags && journals_journal_entry_obj.tags.length}
<div <div
class="tags flex flex-row flex-wrap gap-0.5 items-center justify-start p-1" class="tags flex flex-row flex-wrap items-center justify-start gap-0.5 p-1">
>
<Tags class="mx-1 inline-block" /> <Tags class="mx-1 inline-block" />
<span <span
class="text-xs text-gray-500 hidden md:inline" class="hidden text-xs text-gray-500 md:inline"
>Tags:</span >Tags:</span>
>
{#each journals_journal_entry_obj.tags.split(',') as tag (tag)} {#each journals_journal_entry_obj.tags.split(',') as tag (tag)}
<span <span
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-tertiary border border-tertiary-500 transition py-1 px-2" class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-tertiary border-tertiary-500 border px-2 py-1 transition"
title={`Tag: ${tag.trim()}`} title={`Tag: ${tag.trim()}`}>
>
{tag.trim()} {tag.trim()}
</span> </span>
{/each} {/each}
@@ -511,8 +516,8 @@
journals_journal_entry_obj.category_code; journals_journal_entry_obj.category_code;
} }
if ( if (
$journals_loc.entry.search_version === $journals_loc.entry
undefined .search_version === undefined
) )
$journals_loc.entry.search_version = 0; $journals_loc.entry.search_version = 0;
$journals_loc.entry.search_version++; $journals_loc.entry.search_version++;
@@ -520,9 +525,8 @@
class:bg-green-100={$journals_loc.entry class:bg-green-100={$journals_loc.entry
.qry__category_code == .qry__category_code ==
journals_journal_entry_obj.category_code} journals_journal_entry_obj.category_code}
class="btn btn-sm preset-outlined-secondary hover:preset-filled-secondary-500 transition py-1 px-2" class="btn btn-sm preset-outlined-secondary hover:preset-filled-secondary-500 px-2 py-1 transition"
title={`Filter by category: ${journals_journal_entry_obj.category_code}`} title={`Filter by category: ${journals_journal_entry_obj.category_code}`}>
>
<Shapes class="mx-1 inline-block" /> <Shapes class="mx-1 inline-block" />
{journals_journal_entry_obj.category_code ?? {journals_journal_entry_obj.category_code ??
'-- no category --'} '-- no category --'}
@@ -532,12 +536,11 @@
<a <a
href="/journals/{journals_journal_entry_obj?.journal_id ?? href="/journals/{journals_journal_entry_obj?.journal_id ??
$lq__journal_obj?.journal_id}/entry/{journals_journal_entry_obj?.journal_entry_id}" $lq__journal_obj?.journal_id}/entry/{journals_journal_entry_obj?.journal_entry_id}"
class="btn preset-tonal-primary border border-primary-500 hover:preset-filled-primary-500 transition" class="btn preset-tonal-primary border-primary-500 hover:preset-filled-primary-500 border transition"
title={`View ID: ${journals_journal_entry_obj?.id} title={`View ID: ${journals_journal_entry_obj?.id}
${journals_journal_entry_obj?.name ?? ae_util.iso_datetime_formatter(journals_journal_entry_obj.created_on, 'datetime_iso_12_no_seconds')} ${journals_journal_entry_obj?.name ?? ae_util.iso_datetime_formatter(journals_journal_entry_obj.created_on, 'datetime_iso_12_no_seconds')}
Journal ID: ${journals_journal_entry_obj?.journal_id} Journal ID: ${journals_journal_entry_obj?.journal_id}
`} `}>
>
<NotebookPen class="mx-1 inline-block" /> <NotebookPen class="mx-1 inline-block" />
<span class="hidden md:inline"> View </span> <span class="hidden md:inline"> View </span>
</a> </a>
@@ -549,15 +552,16 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
$journals_sess.show__modal_append__journal_entry_id = $journals_sess.show__modal_append__journal_entry_id =
journals_journal_entry_obj?.id; journals_journal_entry_obj?.id;
tmp_entry_obj = JSON.parse( tmp_entry_obj = JSON.parse(
JSON.stringify(journals_journal_entry_obj) JSON.stringify(
journals_journal_entry_obj
)
); );
}} }}
class="btn btn-icon btn-sm preset-tonal-surface border border-surface-500 hover:preset-filled-secondary-500 transition" class="btn btn-icon btn-sm preset-tonal-surface border-surface-500 hover:preset-filled-secondary-500 border transition"
title={$lq__journal_obj?.cfg_json?.entry_add_text == title={$lq__journal_obj?.cfg_json
'append' ?.entry_add_text == 'append'
? 'Append to Journal Entry' ? 'Append to Journal Entry'
: 'Prepend to Journal Entry'} : 'Prepend to Journal Entry'}>
>
<ListPlus /> <ListPlus />
</button> </button>
</div> </div>
@@ -576,19 +580,19 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
$journals_slct.journal_obj?.cfg_json $journals_slct.journal_obj?.cfg_json
.hide_professional)} .hide_professional)}
class="journal__content class="journal__content
w-full p-1 w-full overflow-scroll
bg-gray-100 text-gray-900 rounded-lg border
dark:bg-gray-900 dark:text-gray-100 border-gray-200 bg-gray-100
shadow-lg rounded-lg p-1 font-mono
border border-gray-200 dark:border-gray-700 text-sm text-wrap whitespace-pre-wrap
text-wrap text-sm font-mono whitespace-pre-wrap text-gray-900 shadow-lg transition-all delay-1000
transition-all duration-1000
delay-1000 hover:delay-1000 active:delay-100 ease-in-out hover:border-blue-500 hover:bg-blue-100
duration-1000 hover:duration-200 active:duration-200 hover:delay-1000 hover:duration-200 active:z-10
ease-in-out active:delay-100
active:z-10 active:duration-200
hover:bg-blue-100 dark:hover:bg-blue-950 hover:border-blue-500 dark:hover:border-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-blue-500
overflow-scroll dark:hover:bg-blue-950
{$journals_slct.journal_obj.cfg_json.entry_li_max_height {$journals_slct.journal_obj.cfg_json.entry_li_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_max_height}` ? `${$journals_slct.journal_obj.cfg_json.entry_li_max_height}`
: ''} : ''}
@@ -602,15 +606,13 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
.entry_li_hover_max_height .entry_li_hover_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height}` ? `${$journals_slct.journal_obj.cfg_json.entry_li_hover_max_height}`
: ''} : ''}
" ">
>
{@html journals_journal_entry_obj.content} {@html journals_journal_entry_obj.content}
</div> </div>
<article <article
class="prose hidden" class="prose hidden"
id="rendered_journal_entry_content_{journals_journal_entry_obj.journal_entry_id}" id="rendered_journal_entry_content_{journals_journal_entry_obj.journal_entry_id}">
>
{@html journals_journal_entry_obj?.content_md_html} {@html journals_journal_entry_obj?.content_md_html}
</article> </article>
{/if} {/if}
@@ -618,41 +620,35 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
<section <section
class:hidden={!journals_journal_entry_obj?.original_datetime && class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone} !journals_journal_entry_obj?.original_timezone}
class="ae_section journal_entry__entry" class="ae_section journal_entry__entry">
>
<div <div
class="ae_group" class="ae_group"
class:hidden={!journals_journal_entry_obj?.original_datetime && class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone} !journals_journal_entry_obj?.original_timezone}>
> <span class="ae_label text-sm"
<span class="ae_label text-sm">Original date/time:</span >Original date/time:</span>
>
{#if journals_journal_entry_obj.original_datetime} {#if journals_journal_entry_obj.original_datetime}
<span <span
class="ae_value ae_prop prop_original_datetime font-semibold" class="ae_value ae_prop prop_original_datetime font-semibold"
>{ae_util.iso_datetime_formatter( >{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.original_datetime, journals_journal_entry_obj.original_datetime,
'datetime_12_long' 'datetime_12_long'
)}</span )}</span>
>
{/if} {/if}
{#if journals_journal_entry_obj.original_timezone} {#if journals_journal_entry_obj.original_timezone}
<span class="ae_label text-sm">Timezone:</span> <span class="ae_label text-sm">Timezone:</span>
<span class="ae_value font-semibold" <span class="ae_value font-semibold"
>{journals_journal_entry_obj.original_timezone}</span >{journals_journal_entry_obj.original_timezone}</span>
>
{/if} {/if}
</div> </div>
</section> </section>
<section <section
class="ae_meta mt-2 flex flex-col sm:flex-row gap-2 items-center justify-center text-xs text-gray-500" class="ae_meta mt-2 flex flex-col items-center justify-center gap-2 text-xs text-gray-500 sm:flex-row">
>
<span <span
class:hidden={!$ae_loc.trusted_access || class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode} !$ae_loc.edit_mode}
class="flex flex-row gap-1 items-center justify-center" class="flex flex-row items-center justify-center gap-1">
>
<span class="journal_entry__created_on"> <span class="journal_entry__created_on">
Created: Created:
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
@@ -662,8 +658,7 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
</span> </span>
<span <span
class:hidden={!journals_journal_entry_obj.updated_on} class:hidden={!journals_journal_entry_obj.updated_on}
class="journal_entry__updated_on" class="journal_entry__updated_on">
>
Last update: Last update:
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
journals_journal_entry_obj.updated_on, journals_journal_entry_obj.updated_on,
@@ -695,24 +690,23 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
'Error updating journal entry:', 'Error updating journal entry:',
error error
); );
alert('Failed to update journal entry.'); alert(
'Failed to update journal entry.'
);
}); });
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn btn-sm preset-tonal-surface hover:preset-filled-warning-500 transition py-1 px-2" class="btn btn-sm preset-tonal-surface hover:preset-filled-warning-500 px-2 py-1 transition"
title={`Set entry as ${journals_journal_entry_obj.hide ? 'visible' : 'hidden'}`} title={`Set entry as ${journals_journal_entry_obj.hide ? 'visible' : 'hidden'}`}>
>
{#if journals_journal_entry_obj.hide} {#if journals_journal_entry_obj.hide}
<EyeOff <EyeOff
strokeWidth="1" strokeWidth="1"
class="inline-block text-error-500/60" class="text-error-500/60 inline-block" />
/>
<span class="hidden md:inline">Hidden</span> <span class="hidden md:inline">Hidden</span>
{:else} {:else}
<Eye <Eye
strokeWidth="2.5" strokeWidth="2.5"
class="inline-block text-success-700 dark:text-success-400" class="text-success-700 dark:text-success-400 inline-block" />
/>
<span class="hidden lg:inline">Visible</span> <span class="hidden lg:inline">Visible</span>
{/if} {/if}
</button> </button>
@@ -728,19 +722,17 @@ Journal ID: ${journals_journal_entry_obj?.journal_id}
journal_config={$lq__journal_obj?.cfg_json} journal_config={$lq__journal_obj?.cfg_json}
on_close={handle_modal_close} on_close={handle_modal_close}
on_update={handle_modal_update} on_update={handle_modal_update}
{log_lvl} {log_lvl} />
/>
{/if} {/if}
{:else} {:else}
<div <div
class="flex flex-col items-center justify-center p-10 opacity-50 text-center" class="flex flex-col items-center justify-center p-10 text-center opacity-50">
> <BookOpenText size="3em" class="mx-auto mb-2 opacity-20" />
<BookOpenText size="3em" class="mb-2 opacity-20 mx-auto" />
<p> <p>
No Journal Entry available to show. Please check the query No Journal Entry available to show. Please check the query
filters or create a new Entry. filters or create a new Entry.
</p> </p>
</div> </div>
{/if} {/if}
</section> </section>
</div> </div>

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
container_class_li?: string | Array<string>; container_class_li?: string | Array<string>;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_entry_obj_li: any; lq__journal_entry_obj_li: any;
show_found_header?: boolean; show_found_header?: boolean;
log_lvl?: number; log_lvl?: number;
} }
let { let {
container_class_li = '', container_class_li = '',
lq__journal_obj, lq__journal_obj,
lq__journal_entry_obj_li, lq__journal_entry_obj_li,
show_found_header = true, show_found_header = true,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
// *** Import other supporting libraries // *** Import other supporting libraries
import { LoaderCircle } from '@lucide/svelte'; import { LoaderCircle } from '@lucide/svelte';
// *** Import Aether specific components // *** Import Aether specific components
import Journal_entry_obj_li from './ae_comp__journal_entry_obj_li.svelte'; import Journal_entry_obj_li from './ae_comp__journal_entry_obj_li.svelte';
</script> </script>
{#if $lq__journal_entry_obj_li} {#if $lq__journal_entry_obj_li}
@@ -26,11 +26,10 @@
{lq__journal_obj} {lq__journal_obj}
{lq__journal_entry_obj_li} {lq__journal_entry_obj_li}
{show_found_header} {show_found_header}
{log_lvl} {log_lvl} />
/>
{:else} {:else}
<div class="flex flex-col items-center justify-center p-10 opacity-50"> <div class="flex flex-col items-center justify-center p-10 opacity-50">
<LoaderCircle size="2em" class="animate-spin mb-2" /> <LoaderCircle size="2em" class="mb-2 animate-spin" />
<p>Loading entries...</p> <p>Loading entries...</p>
</div> </div>
{/if} {/if}

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
} }
let { log_lvl = $bindable(0), lq__journal_obj }: Props = $props(); let { log_lvl = $bindable(0), lq__journal_obj }: Props = $props();
import { import {
ArrowDown01, ArrowDown01,
ArrowDown10, ArrowDown10,
ArrowDownUp, ArrowDownUp,
@@ -48,9 +48,9 @@
Trash2, Trash2,
TypeOutline, TypeOutline,
X X
} from '@lucide/svelte'; } from '@lucide/svelte';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
@@ -58,35 +58,34 @@
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_prom, journals_prom,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
// *** Functions and Logic // *** Functions and Logic
function handle_search_trigger() { function handle_search_trigger() {
if ($journals_loc.entry.search_version === undefined) { if ($journals_loc.entry.search_version === undefined) {
$journals_loc.entry.search_version = 0; $journals_loc.entry.search_version = 0;
} }
$journals_loc.entry.search_version++; $journals_loc.entry.search_version++;
} }
function prevent_default<T extends Event>(fn: (event: T) => void) { function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) { return function (event: T) {
event.preventDefault(); event.preventDefault();
fn(event); fn(event);
}; };
} }
</script> </script>
<div <div
class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2" class="ae_group filters_and_search flex flex-row flex-wrap items-center justify-center gap-2">
>
<!-- Search input form --> <!-- Search input form -->
<span class="flex flex-row flex-wrap items-center justify-center gap-1"> <span class="flex flex-row flex-wrap items-center justify-center gap-1">
<form <form
@@ -94,9 +93,8 @@
handle_search_trigger(); handle_search_trigger();
})} })}
autocomplete="off" autocomplete="off"
class="search_form flex flex-row flex-wrap gap-1 items-center justify-center" class="search_form flex flex-row flex-wrap items-center justify-center gap-1">
> <span class="hidden text-sm text-gray-500 lg:inline">
<span class="text-sm text-gray-500 hidden lg:inline">
Search: Search:
</span> </span>
<input <input
@@ -111,18 +109,16 @@
autocomplete="off" autocomplete="off"
class=" class="
input input-sm input input-sm
w-44 md:w-52 w-44 text-sm
text-sm md:w-52
" "
class:bg-red-200={$journals_sess.entry_li == null} class:bg-red-200={$journals_sess.entry_li == null}
class:dark:bg-red-800={$journals_sess.entry_li == null} class:dark:bg-red-800={$journals_sess.entry_li == null} />
/>
<button <button
type="submit" type="submit"
class="btn btn-sm preset-filled-primary transition" class="btn btn-sm preset-filled-primary transition"
title="Perform detailed search" title="Perform detailed search">
>
<Library size="1.25em" /> <Library size="1.25em" />
</button> </button>
@@ -142,12 +138,10 @@
hover:preset-filled-surface-500 hover:preset-filled-surface-500
transition-all transition-all
" "
title="Clear search query text" title="Clear search query text">
>
<RemoveFormatting <RemoveFormatting
size="1.25em" size="1.25em"
class="text-neutral-800/60 dark:text-neutral-50/60" class="text-neutral-800/60 dark:text-neutral-50/60" />
/>
<span class="hidden md:inline"> Clear </span> <span class="hidden md:inline"> Clear </span>
</button> </button>
</form> </form>
@@ -155,15 +149,14 @@
<!-- Give list of categories to base the new entry on --> <!-- Give list of categories to base the new entry on -->
<span class="flex flex-row items-center gap-2"> <span class="flex flex-row items-center gap-2">
<span class="text-sm text-gray-500 hidden md:inline"> Category: </span> <span class="hidden text-sm text-gray-500 md:inline"> Category: </span>
<select <select
class="select select-sm" class="select select-sm"
bind:value={$journals_loc.entry.qry__category_code} bind:value={$journals_loc.entry.qry__category_code}
onchange={(event) => { onchange={(event) => {
handle_search_trigger(); handle_search_trigger();
}} }}
title="Filter by category" title="Filter by category">
>
<option value="">All Categories</option> <option value="">All Categories</option>
{#each $lq__journal_obj?.cfg_json?.category_li as category (category.code)} {#each $lq__journal_obj?.cfg_json?.category_li as category (category.code)}
<option value={category.code}>{category.name}</option> <option value={category.code}>{category.name}</option>
@@ -173,8 +166,7 @@
<!-- Search Control Toggles --> <!-- Search Control Toggles -->
<span <span
class="flex flex-row flex-wrap items-center gap-2 border-l border-surface-300-700 pl-2" class="border-surface-300-700 flex flex-row flex-wrap items-center gap-2 border-l pl-2">
>
<!-- Global Search hidden until backend supports person_id in entries 2026-01-27 --> <!-- Global Search hidden until backend supports person_id in entries 2026-01-27 -->
<!-- <!--
<label <label
@@ -193,9 +185,8 @@
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<label <label
class="flex items-center gap-1 cursor-pointer" class="flex cursor-pointer items-center gap-1"
title="When enabled, search results are fetched directly from the server first." title="When enabled, search results are fetched directly from the server first.">
>
<span class="text-xs font-semibold text-gray-500"> <span class="text-xs font-semibold text-gray-500">
Remote First? Remote First?
</span> </span>
@@ -203,8 +194,7 @@
type="checkbox" type="checkbox"
bind:checked={$journals_loc.entry.qry__remote_first} bind:checked={$journals_loc.entry.qry__remote_first}
onchange={handle_search_trigger} onchange={handle_search_trigger}
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
</label> </label>
{/if} {/if}
</span> </span>

View File

@@ -1,34 +1,34 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import { import {
journals_slct, journals_slct,
journals_loc, journals_loc,
journals_trig journals_trig
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { BookType } from '@lucide/svelte'; import { BookType } from '@lucide/svelte';
// Props // Props
let { let {
class: className = '', class: className = '',
placeholder = 'Type your quick note... (First line = Title)', placeholder = 'Type your quick note... (First line = Title)',
journals_li = [] // Optional list of journals to select from journals_li = [] // Optional list of journals to select from
} = $props(); } = $props();
// State // State
let note_content = $state(''); let note_content = $state('');
let is_submitting = $state(false); let is_submitting = $state(false);
// Derived / Local target // Derived / Local target
// We prefer the persisted 'qry__journal_id' if we are on the main landing page // We prefer the persisted 'qry__journal_id' if we are on the main landing page
let selected_journal_id = $state($journals_loc.entry.qry__journal_id); let selected_journal_id = $state($journals_loc.entry.qry__journal_id);
// If a journal is explicitly selected via slct (e.g. we are in a journal view), use that // If a journal is explicitly selected via slct (e.g. we are in a journal view), use that
let target_journal_id = $derived( let target_journal_id = $derived(
$journals_slct.journal_id || selected_journal_id $journals_slct.journal_id || selected_journal_id
); );
async function handle_submit() { async function handle_submit() {
if (!note_content.trim()) return; if (!note_content.trim()) return;
if (!target_journal_id) { if (!target_journal_id) {
alert('Please select a target journal first.'); alert('Please select a target journal first.');
@@ -74,25 +74,24 @@
} }
is_submitting = false; is_submitting = false;
} }
function handle_keydown(e: KeyboardEvent) { function handle_keydown(e: KeyboardEvent) {
if (e.ctrlKey && e.key === 'Enter') { if (e.ctrlKey && e.key === 'Enter') {
handle_submit(); handle_submit();
} }
} }
function handle_journal_change(e: Event) { function handle_journal_change(e: Event) {
const val = (e.target as HTMLSelectElement).value; const val = (e.target as HTMLSelectElement).value;
selected_journal_id = val; selected_journal_id = val;
$journals_loc.entry.qry__journal_id = val; // Persist choice $journals_loc.entry.qry__journal_id = val; // Persist choice
} }
</script> </script>
<div class="card p-4 space-y-4 preset-tonal-surface {className}"> <div class="card preset-tonal-surface space-y-4 p-4 {className}">
<header <header
class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2" class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
>
<h3 class="h3 flex items-center gap-2"> <h3 class="h3 flex items-center gap-2">
<BookType size="1.2em" class="text-primary-500" /> <BookType size="1.2em" class="text-primary-500" />
Quick Add Quick Add
@@ -103,11 +102,9 @@
<select <select
class="select select-sm font-bold" class="select select-sm font-bold"
value={target_journal_id} value={target_journal_id}
onchange={handle_journal_change} onchange={handle_journal_change}>
>
<option value="" disabled selected={!target_journal_id} <option value="" disabled selected={!target_journal_id}
>Select Target Journal...</option >Select Target Journal...</option>
>
{#each journals_li as journal (journal.id)} {#each journals_li as journal (journal.id)}
<option value={journal.id}>{journal.name}</option> <option value={journal.id}>{journal.name}</option>
{/each} {/each}
@@ -124,22 +121,19 @@
bind:value={note_content} bind:value={note_content}
{placeholder} {placeholder}
onkeydown={handle_keydown} onkeydown={handle_keydown}
disabled={is_submitting} disabled={is_submitting}></textarea>
></textarea>
<div class="flex justify-between items-center"> <div class="flex items-center justify-between">
<span <span
class="text-[10px] opacity-50 font-mono uppercase tracking-tighter hidden sm:block" class="hidden font-mono text-[10px] tracking-tighter uppercase opacity-50 sm:block">
>
Press Ctrl + Enter to save Press Ctrl + Enter to save
</span> </span>
<div class="flex justify-end space-x-2 grow sm:grow-0"> <div class="flex grow justify-end space-x-2 sm:grow-0">
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-surface" class="btn btn-sm preset-tonal-surface"
onclick={() => (note_content = '')} onclick={() => (note_content = '')}
disabled={is_submitting || note_content.length === 0} disabled={is_submitting || note_content.length === 0}>
>
Clear Clear
</button> </button>
<button <button
@@ -148,8 +142,7 @@
onclick={handle_submit} onclick={handle_submit}
disabled={is_submitting || disabled={is_submitting ||
!target_journal_id || !target_journal_id ||
note_content.length === 0} note_content.length === 0}>
>
{#if is_submitting}Saving...{:else}Add Note{/if} {#if is_submitting}Saving...{:else}Add Note{/if}
</button> </button>
</div> </div>

View File

@@ -1,49 +1,82 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
/** /**
* ae_comp__journal_obj_id_edit.svelte * ae_comp__journal_obj_id_edit.svelte
* Standardized Journal-level configuration. * Standardized Journal-level configuration.
* Restored missing visibility and button toggles. * Restored missing visibility and button toggles.
*/ */
import { BetweenVerticalEnd, BetweenVerticalStart, BookHeart, BookOpenText, BriefcaseBusiness, CalendarClock, Check, CodeXml, Copy, Database, Expand, Eye, EyeOff, FileDown, FilePlus, FileUp, Fingerprint, Globe, Layout, LockKeyhole, MessageSquareWarning, Minus, MonitorPlay, MousePointerClick, Palette, Plus, Settings, ShieldCheck, Siren, Target, Trash2, X, Zap } from '@lucide/svelte'; import {
import { Modal } from 'flowbite-svelte'; BetweenVerticalEnd,
import { goto } from '$app/navigation'; BetweenVerticalStart,
BookHeart,
BookOpenText,
BriefcaseBusiness,
CalendarClock,
Check,
CodeXml,
Copy,
Database,
Expand,
Eye,
EyeOff,
FileDown,
FilePlus,
FileUp,
Fingerprint,
Globe,
Layout,
LockKeyhole,
MessageSquareWarning,
Minus,
MonitorPlay,
MousePointerClick,
Palette,
Plus,
Settings,
ShieldCheck,
Siren,
Target,
Trash2,
X,
Zap
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { goto } from '$app/navigation';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct journals_slct
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
show?: boolean; show?: boolean;
on_new_entry?: () => void; on_new_entry?: () => void;
on_show_export?: () => void; on_show_export?: () => void;
on_show_import?: () => void; on_show_import?: () => void;
} }
let { let {
log_lvl = $bindable(0), log_lvl = $bindable(0),
lq__journal_obj, lq__journal_obj,
show = $bindable(false), show = $bindable(false),
on_new_entry, on_new_entry,
on_show_export, on_show_export,
on_show_import on_show_import
}: Props = $props(); }: Props = $props();
// *** Internal State // *** Internal State
let tab: 'actions' | 'general' | 'security' | 'ui' | 'json' = let tab: 'actions' | 'general' | 'security' | 'ui' | 'json' = $state('actions');
$state('actions'); let tmp__journal_obj: any = $state({});
let tmp__journal_obj: any = $state({});
// Deep copy on mount or when lq changes to ensure we have a working copy // Deep copy on mount or when lq changes to ensure we have a working copy
$effect(() => { $effect(() => {
if (show && $lq__journal_obj) { if (show && $lq__journal_obj) {
const source_id = $lq__journal_obj.journal_id; const source_id = $lq__journal_obj.journal_id;
untrack(() => { untrack(() => {
@@ -51,18 +84,15 @@
!tmp__journal_obj.journal_id || !tmp__journal_obj.journal_id ||
tmp__journal_obj.journal_id !== source_id tmp__journal_obj.journal_id !== source_id
) { ) {
tmp__journal_obj = JSON.parse( tmp__journal_obj = JSON.parse(JSON.stringify($lq__journal_obj));
JSON.stringify($lq__journal_obj)
);
// Ensure cfg_json exists // Ensure cfg_json exists
if (!tmp__journal_obj.cfg_json) if (!tmp__journal_obj.cfg_json) tmp__journal_obj.cfg_json = {};
tmp__journal_obj.cfg_json = {};
} }
}); });
} }
}); });
async function handle_update_journal(close_modal: boolean = true) { async function handle_update_journal(close_modal: boolean = true) {
if (!tmp__journal_obj.name || !tmp__journal_obj.type_code) { if (!tmp__journal_obj.name || !tmp__journal_obj.type_code) {
alert('Please provide both name and type for the journal.'); alert('Please provide both name and type for the journal.');
return; return;
@@ -112,9 +142,9 @@
console.error('Error updating journal:', error); console.error('Error updating journal:', error);
if (close_modal) alert('Failed to update journal.'); if (close_modal) alert('Failed to update journal.');
} }
} }
async function delete_journal() { async function delete_journal() {
if ( if (
confirm( confirm(
`CRITICAL WARNING: Are you sure you want to delete the journal "${$lq__journal_obj.name}"? This will delete all entries associated with it.` `CRITICAL WARNING: Are you sure you want to delete the journal "${$lq__journal_obj.name}"? This will delete all entries associated with it.`
@@ -133,7 +163,7 @@
alert('Failed to delete journal.'); alert('Failed to delete journal.');
} }
} }
} }
</script> </script>
<Modal <Modal
@@ -142,32 +172,32 @@
dismissable={false} dismissable={false}
placement="top-center" placement="top-center"
size="xl" size="xl"
class="relative flex flex-col mx-auto w-full bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-orange-300 dark:border-orange-700 rounded-lg shadow-xl" class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800" headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800" footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
>
{#snippet header()} {#snippet header()}
<h3 class="flex-1 flex items-center gap-2 text-lg font-bold"> <h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Settings class="text-primary-500" /> <Settings class="text-primary-500" />
<span>Journal Config: {$lq__journal_obj?.name ?? '--'}</span> <span>Journal Config: {$lq__journal_obj?.name ?? '--'}</span>
</h3> </h3>
<button type="button" class="btn-icon btn-icon-sm preset-tonal-surface ml-2" onclick={() => (show = false)}> <button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" /> <X size="1.1em" />
</button> </button>
{/snippet} {/snippet}
<div class="space-y-6 py-2 h-[75vh] overflow-y-auto px-4"> <div class="h-[75vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div <div
class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto sticky top-0 z-10 backdrop-blur-sm" class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
>
<button <button
type="button" type="button"
class="btn btn-sm transition-all {tab === 'actions' class="btn btn-sm transition-all {tab === 'actions'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'actions')} onclick={() => (tab = 'actions')}>
>
<Zap size="1.1em" class="mr-1" /> Quick Actions <Zap size="1.1em" class="mr-1" /> Quick Actions
</button> </button>
<button <button
@@ -175,8 +205,7 @@
class="btn btn-sm transition-all {tab === 'general' class="btn btn-sm transition-all {tab === 'general'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'general')} onclick={() => (tab = 'general')}>
>
<Layout size="1.1em" class="mr-1" /> General <Layout size="1.1em" class="mr-1" /> General
</button> </button>
<button <button
@@ -184,8 +213,7 @@
class="btn btn-sm transition-all {tab === 'security' class="btn btn-sm transition-all {tab === 'security'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'security')} onclick={() => (tab = 'security')}>
>
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security <ShieldCheck size="1.1em" class="mr-1" /> Status & Security
</button> </button>
<button <button
@@ -193,8 +221,7 @@
class="btn btn-sm transition-all {tab === 'ui' class="btn btn-sm transition-all {tab === 'ui'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'ui')} onclick={() => (tab = 'ui')}>
>
<Palette size="1.1em" class="mr-1" /> UI/Visuals <Palette size="1.1em" class="mr-1" /> UI/Visuals
</button> </button>
<button <button
@@ -202,23 +229,21 @@
class="btn btn-sm transition-all {tab === 'json' class="btn btn-sm transition-all {tab === 'json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'json')} onclick={() => (tab = 'json')}>
>
<CodeXml size="1.1em" class="mr-1" /> JSON <CodeXml size="1.1em" class="mr-1" /> JSON
</button> </button>
</div> </div>
{#if tab === 'actions'} {#if tab === 'actions'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<section class="grid grid-cols-1 md:grid-cols-2 gap-4"> <section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<button <button
type="button" type="button"
class="btn preset-tonal-secondary w-full py-4 text-lg font-bold" class="btn preset-tonal-secondary w-full py-4 text-lg font-bold"
onclick={() => { onclick={() => {
show = false; show = false;
on_new_entry?.(); on_new_entry?.();
}} }}>
>
<FilePlus size="1.5em" class="mr-2" /> New Journal Entry <FilePlus size="1.5em" class="mr-2" /> New Journal Entry
</button> </button>
<button <button
@@ -227,8 +252,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_show_export?.(); on_show_export?.();
}} }}>
>
<FileDown size="1.5em" class="mr-2" /> Export Entries <FileDown size="1.5em" class="mr-2" /> Export Entries
</button> </button>
<button <button
@@ -237,31 +261,27 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_show_import?.(); on_show_import?.();
}} }}>
>
<FileUp size="1.5em" class="mr-2" /> Import Entries <FileUp size="1.5em" class="mr-2" /> Import Entries
</button> </button>
</section> </section>
</div> </div>
{:else if tab === 'general'} {:else if tab === 'general'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<!-- Core Meta --> <!-- Core Meta -->
<section class="grid grid-cols-1 gap-4 p-2"> <section class="grid grid-cols-1 gap-4 p-2">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Journal Name</span >Journal Name</span>
>
<input <input
type="text" type="text"
bind:value={tmp__journal_obj.name} bind:value={tmp__journal_obj.name}
class="input" class="input"
placeholder="e.g. Personal Log" placeholder="e.g. Personal Log" />
/>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Description (Markdown)</span >Description (Markdown)</span>
>
<textarea <textarea
bind:value={tmp__journal_obj.description} bind:value={tmp__journal_obj.description}
class="textarea h-32" class="textarea h-32"
@@ -271,29 +291,24 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Type</span >Type</span>
>
<select <select
bind:value={tmp__journal_obj.type_code} bind:value={tmp__journal_obj.type_code}
class="select" class="select">
>
{#each $journals_loc.journal.type_code_li as type (type.code)} {#each $journals_loc.journal.type_code_li as type (type.code)}
<option value={type.code} <option value={type.code}
>{type.name}</option >{type.name}</option>
>
{/each} {/each}
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Journal Group (text)</span >Journal Group (text)</span>
>
<input <input
type="text" type="text"
bind:value={tmp__journal_obj.group} bind:value={tmp__journal_obj.group}
class="input" class="input"
placeholder="Standard" placeholder="Standard" />
/>
</label> </label>
</div> </div>
</section> </section>
@@ -301,28 +316,24 @@
<!-- Categories --> <!-- Categories -->
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<MonitorPlay size="1em" class="text-primary-500" /> <MonitorPlay size="1em" class="text-primary-500" />
Journal Categories Journal Categories
</h2> </h2>
<div <div
class="space-y-2 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 space-y-2 rounded-lg border p-4">
>
{#each tmp__journal_obj?.cfg_json?.category_li ?? [] as category, i (category.code ?? i)} {#each tmp__journal_obj?.cfg_json?.category_li ?? [] as category, i (category.code ?? i)}
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
<input <input
type="text" type="text"
bind:value={category.code} bind:value={category.code}
class="input input-sm w-32" class="input input-sm w-32"
placeholder="Code" placeholder="Code" />
/>
<input <input
type="text" type="text"
bind:value={category.name} bind:value={category.name}
class="input input-sm grow" class="input input-sm grow"
placeholder="Display Name" placeholder="Display Name" />
/>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-error" class="btn-icon btn-icon-sm preset-tonal-error"
@@ -331,15 +342,14 @@
i, i,
1 1
); );
}} }}>
>
<Minus size="1em" /> <Minus size="1em" />
</button> </button>
</div> </div>
{/each} {/each}
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-primary w-full mt-2" class="btn btn-sm preset-tonal-primary mt-2 w-full"
onclick={() => { onclick={() => {
if (!tmp__journal_obj.cfg_json.category_li) if (!tmp__journal_obj.cfg_json.category_li)
tmp__journal_obj.cfg_json.category_li = []; tmp__journal_obj.cfg_json.category_li = [];
@@ -347,81 +357,68 @@
code: '', code: '',
name: '' name: ''
}); });
}} }}>
>
<Plus size="1em" class="mr-1" /> Add Category <Plus size="1em" class="mr-1" /> Add Category
</button> </button>
</div> </div>
</section> </section>
</div> </div>
{:else if tab === 'security'} {:else if tab === 'security'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<!-- Status & Lifecycle --> <!-- Status & Lifecycle -->
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Fingerprint size="1.2em" class="text-primary-500" /> <Fingerprint size="1.2em" class="text-primary-500" />
Status & Lifecycle Status & Lifecycle
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp__journal_obj.enable} bind:checked={tmp__journal_obj.enable}
onchange={() => handle_update_journal(false)} onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Enabled</span> <span class="font-bold">Enabled</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Allow access to this journal</span >Allow access to this journal</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp__journal_obj.hide} bind:checked={tmp__journal_obj.hide}
onchange={() => handle_update_journal(false)} onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Hidden</span> <span class="font-bold">Hidden</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Hide from standard lists</span >Hide from standard lists</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp__journal_obj.priority} bind:checked={tmp__journal_obj.priority}
onchange={() => handle_update_journal(false)} onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Priority Journal</span> <span class="font-bold">Priority Journal</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Star or pin to top</span >Star or pin to top</span>
>
</div> </div>
</label> </label>
<div <div
class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold text-sm">Sort Order</span <span class="text-sm font-bold"
> >Sort Order</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Manual list position</span >Manual list position</span>
>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
@@ -431,12 +428,10 @@
tmp__journal_obj.sort = tmp__journal_obj.sort =
(tmp__journal_obj.sort ?? 0) - 1; (tmp__journal_obj.sort ?? 0) - 1;
handle_update_journal(false); handle_update_journal(false);
}}><Minus size="1em" /></button }}><Minus size="1em" /></button>
>
<span <span
class="font-mono font-bold w-8 text-center text-lg" class="w-8 text-center font-mono text-lg font-bold"
>{tmp__journal_obj.sort ?? 0}</span >{tmp__journal_obj.sort ?? 0}</span>
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
@@ -444,8 +439,7 @@
tmp__journal_obj.sort = tmp__journal_obj.sort =
(tmp__journal_obj.sort ?? 0) + 1; (tmp__journal_obj.sort ?? 0) + 1;
handle_update_journal(false); handle_update_journal(false);
}}><Plus size="1em" /></button }}><Plus size="1em" /></button>
>
</div> </div>
</div> </div>
</div> </div>
@@ -454,34 +448,29 @@
<!-- Encryption Passcodes --> <!-- Encryption Passcodes -->
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<LockKeyhole size="1.2em" class="text-primary-500" /> <LockKeyhole size="1.2em" class="text-primary-500" />
Encryption Passcodes Encryption Passcodes
</h2> </h2>
<div <div
class="bg-warning-500/10 border border-warning-500/30 p-4 rounded-lg space-y-4 shadow-inner" class="bg-warning-500/10 border-warning-500/30 space-y-4 rounded-lg border p-4 shadow-inner">
>
<label class="label"> <label class="label">
<span <span
class="text-xs font-bold uppercase tracking-wider opacity-70" class="text-xs font-bold tracking-wider uppercase opacity-70"
>Primary Passcode (Stored)</span >Primary Passcode (Stored)</span>
>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="password" type="password"
bind:value={tmp__journal_obj.passcode} bind:value={tmp__journal_obj.passcode}
class="input grow" class="input grow"
placeholder="Module-level passcode" placeholder="Module-level passcode" />
/>
<Fingerprint class="opacity-30" /> <Fingerprint class="opacity-30" />
</div> </div>
</label> </label>
<label class="label"> <label class="label">
<span <span
class="text-xs font-bold uppercase tracking-wider opacity-70" class="text-xs font-bold tracking-wider uppercase opacity-70"
>Private Passcode (Double Encryption)</span >Private Passcode (Double Encryption)</span>
>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="password" type="password"
@@ -489,14 +478,12 @@
tmp__journal_obj.private_passcode tmp__journal_obj.private_passcode
} }
class="input grow" class="input grow"
placeholder="User-level secret" placeholder="User-level secret" />
/>
<ShieldCheck class="opacity-30" /> <ShieldCheck class="opacity-30" />
</div> </div>
</label> </label>
<div <div
class="text-[10px] text-warning-700 dark:text-warning-300 italic" class="text-warning-700 dark:text-warning-300 text-[10px] italic">
>
* Note: Passcodes are used locally for E2EE. Primary * Note: Passcodes are used locally for E2EE. Primary
is often stored in the DB, while Private should is often stored in the DB, while Private should
remain known only to you. remain known only to you.
@@ -508,71 +495,58 @@
<button <button
type="button" type="button"
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-full shadow-lg" class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-full shadow-lg"
onclick={delete_journal} onclick={delete_journal}>
>
<Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal <Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal
</button> </button>
</section> </section>
</div> </div>
{:else if tab === 'ui'} {:else if tab === 'ui'}
<div class="space-y-8 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-8 duration-300">
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<MonitorPlay size="1.2em" class="text-primary-500" /> <MonitorPlay size="1.2em" class="text-primary-500" />
Default View Modes Default View Modes
</h2> </h2>
<div <div
class="grid grid-cols-1 md:grid-cols-2 gap-6 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 grid grid-cols-1 gap-6 rounded-lg border p-4 md:grid-cols-2">
>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Preferred Viewer</span >Preferred Viewer</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json.pref_viewer tmp__journal_obj.cfg_json.pref_viewer
} }
class="select" class="select">
>
<option value="rendered" <option value="rendered"
>Rendered HTML (Default)</option >Rendered HTML (Default)</option>
>
<option value="plain">Plain Text</option> <option value="plain">Plain Text</option>
<option value="codemirror" <option value="codemirror"
>CodeMirror (Syntax)</option >CodeMirror (Syntax)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Preferred Editor</span >Preferred Editor</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json.pref_editor tmp__journal_obj.cfg_json.pref_editor
} }
class="select" class="select">
>
<option value="textarea" <option value="textarea"
>Standard Textarea</option >Standard Textarea</option>
>
<option value="codemirror" <option value="codemirror"
>CodeMirror (Advanced)</option >CodeMirror (Advanced)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Color Scheme</span >Color Scheme</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json.color_scheme tmp__journal_obj.cfg_json.color_scheme
} }
class="select" class="select">
>
<option value="">Default (Slate)</option> <option value="">Default (Slate)</option>
<option value="blue">Deep Blue</option> <option value="blue">Deep Blue</option>
<option value="green">Nature Green</option> <option value="green">Nature Green</option>
@@ -583,19 +557,16 @@
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Quick Add Placement</span >Quick Add Placement</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json.entry_add_text tmp__journal_obj.cfg_json.entry_add_text
} }
class="select" class="select">
>
<option value="append" <option value="append"
>Append to End (Default)</option >Append to End (Default)</option>
> <option value="prepend"
<option value="prepend">Prepend to Start</option >Prepend to Start</option>
>
</select> </select>
</label> </label>
</div> </div>
@@ -604,51 +575,42 @@
<!-- Visibility Toggles (Restored) --> <!-- Visibility Toggles (Restored) -->
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Eye size="1.2em" class="text-primary-500" /> <Eye size="1.2em" class="text-primary-500" />
Entry Visibility (Global) Entry Visibility (Global)
</h2> </h2>
<div <div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2" class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
>
<label <label
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_private tmp__journal_obj.cfg_json.hide_private
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span class="text-xs font-bold">Hide Private</span> <span class="text-xs font-bold">Hide Private</span>
</label> </label>
<label <label
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_personal tmp__journal_obj.cfg_json.hide_personal
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span class="text-xs font-bold">Hide Personal</span> <span class="text-xs font-bold">Hide Personal</span>
</label> </label>
<label <label
class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_professional tmp__journal_obj.cfg_json.hide_professional
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/>
<span class="text-xs font-bold" <span class="text-xs font-bold"
>Hide Professional</span >Hide Professional</span>
>
</label> </label>
</div> </div>
</section> </section>
@@ -656,193 +618,157 @@
<!-- Button Toggles (Restored) --> <!-- Button Toggles (Restored) -->
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Settings size="1.2em" class="text-primary-500" /> <Settings size="1.2em" class="text-primary-500" />
Entry Feature Buttons Entry Feature Buttons
</h2> </h2>
<div <div
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2" class="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_btn_alert tmp__journal_obj.cfg_json.hide_btn_alert
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Alert</span>
>Hide Alert</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_btn_private tmp__journal_obj.cfg_json.hide_btn_private
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Private</span>
>Hide Private</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_btn_public tmp__journal_obj.cfg_json.hide_btn_public
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Public</span>
>Hide Public</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_btn_personal tmp__journal_obj.cfg_json.hide_btn_personal
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Pers.</span>
>Hide Pers.</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json tmp__journal_obj.cfg_json
.hide_btn_professional .hide_btn_professional
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Prof.</span>
>Hide Prof.</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_btn_template tmp__journal_obj.cfg_json.hide_btn_template
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Templ.</span>
>Hide Templ.</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_copy_plain_md tmp__journal_obj.cfg_json.hide_copy_plain_md
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Copy MD</span>
>Hide Copy MD</span
>
</label> </label>
<label <label
class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
tmp__journal_obj.cfg_json.hide_clone tmp__journal_obj.cfg_json.hide_clone
} }
class="checkbox checkbox-sm" class="checkbox checkbox-sm" />
/> <span class="text-[10px] font-bold uppercase"
<span class="text-[10px] uppercase font-bold" >Hide Clone</span>
>Hide Clone</span
>
</label> </label>
</div> </div>
</section> </section>
<section class="space-y-4 p-2"> <section class="space-y-4 p-2">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Layout size="1.2em" class="text-primary-500" /> <Layout size="1.2em" class="text-primary-500" />
List Interactions List Interactions
</h2> </h2>
<div class="bg-surface-500/5 p-4 rounded-lg space-y-4"> <div class="bg-surface-500/5 space-y-4 rounded-lg p-4">
<label class="label"> <label class="label">
<span class="text-xs font-bold opacity-70" <span class="text-xs font-bold opacity-70"
>Expansion Trigger</span >Expansion Trigger</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json.expand_li_content tmp__journal_obj.cfg_json.expand_li_content
} }
class="select" class="select">
>
<option value="click">Click to Expand</option> <option value="click">Click to Expand</option>
<option value="hover">Hover to Expand</option> <option value="hover">Hover to Expand</option>
</select> </select>
</label> </label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<label class="label"> <label class="label">
<span class="text-xs font-bold opacity-70" <span class="text-xs font-bold opacity-70"
>List Max Height</span >List Max Height</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json tmp__journal_obj.cfg_json
.entry_li_max_height .entry_li_max_height
} }
class="select select-sm" class="select select-sm">
>
<option value="">Default</option> <option value="">Default</option>
<option value="max-h-16">Small (16)</option> <option value="max-h-16">Small (16)</option>
<option value="max-h-32">Medium (32)</option <option value="max-h-32"
> >Medium (32)</option>
<option value="max-h-64">Large (64)</option> <option value="max-h-64">Large (64)</option>
<option value="max-h-full">Full</option> <option value="max-h-full">Full</option>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-xs font-bold opacity-70" <span class="text-xs font-bold opacity-70"
>Active Max Height</span >Active Max Height</span>
>
<select <select
bind:value={ bind:value={
tmp__journal_obj.cfg_json tmp__journal_obj.cfg_json
.entry_li_click_max_height .entry_li_click_max_height
} }
class="select select-sm" class="select select-sm">
>
<option value="">Default</option> <option value="">Default</option>
<option value="active:max-h-64" <option value="active:max-h-64"
>Large (64)</option >Large (64)</option>
>
<option value="active:max-h-96" <option value="active:max-h-96"
>X-Large (96)</option >X-Large (96)</option>
>
<option value="active:max-h-full" <option value="active:max-h-full"
>Full</option >Full</option>
>
</select> </select>
</label> </label>
</div> </div>
@@ -855,8 +781,7 @@
readonly={true} readonly={true}
content={JSON.stringify(tmp__journal_obj, null, 2)} content={JSON.stringify(tmp__journal_obj, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -865,16 +790,14 @@
<div class="flex gap-4"> <div class="flex gap-4">
<button <button
type="button" type="button"
class="btn preset-tonal-surface font-bold min-w-[100px]" class="btn preset-tonal-surface min-w-[100px] font-bold"
onclick={() => (show = false)} onclick={() => (show = false)}>
>
<X size="1.2em" class="mr-2" /> Cancel <X size="1.2em" class="mr-2" /> Cancel
</button> </button>
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold min-w-[120px]" class="btn preset-filled-primary min-w-[120px] font-bold"
onclick={() => handle_update_journal(true)} onclick={() => handle_update_journal(true)}>
>
<Check size="1.2em" class="mr-2" /> Save Changes <Check size="1.2em" class="mr-2" /> Save Changes
</button> </button>
</div> </div>

View File

@@ -1,12 +1,22 @@
<script lang="ts"> <script lang="ts">
// *** Import Svelte specific // *** Import Svelte specific
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// *** Import other supporting libraries // *** Import other supporting libraries
import { BookOpenText, BookPlus, FileDown, FilePlus, FileUp, LoaderCircle, Menu, Pencil, Settings } from '@lucide/svelte'; import {
// *** Import Aether specific variables and functions BookOpenText,
import { ae_util } from '$lib/ae_utils/ae_utils'; BookPlus,
import { FileDown,
FilePlus,
FileUp,
LoaderCircle,
Menu,
Pencil,
Settings
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
@@ -14,42 +24,42 @@
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess, journals_sess,
journals_slct, journals_slct,
journals_trig, journals_trig,
journals_prom journals_prom
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import Journal_obj_id_edit from './ae_comp__journal_obj_id_edit.svelte'; import Journal_obj_id_edit from './ae_comp__journal_obj_id_edit.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
lq__journal_obj: any; lq__journal_obj: any;
lq__journal_entry_obj_li: any; lq__journal_entry_obj_li: any;
on_show_export?: () => void; on_show_export?: () => void;
on_show_import?: () => void; on_show_import?: () => void;
} }
let { let {
log_lvl = 0, log_lvl = 0,
lq__journal_obj, lq__journal_obj,
lq__journal_entry_obj_li, lq__journal_entry_obj_li,
on_show_export, on_show_export,
on_show_import on_show_import
}: Props = $props(); }: Props = $props();
// let ae_promises: key_val = {}; // let ae_promises: key_val = {};
// let ae_tmp: key_val = {}; // let ae_tmp: key_val = {};
// let ae_trigger: any = null; // let ae_trigger: any = null;
// let ae_triggers: key_val = {}; // let ae_triggers: key_val = {};
let typed_journal_passcode: string = $state(''); let typed_journal_passcode: string = $state('');
let passcode_timer: any = $state(null); let passcode_timer: any = $state(null);
$effect(() => { $effect(() => {
if (typed_journal_passcode?.length > 4) { if (typed_journal_passcode?.length > 4) {
if (!$journals_sess?.journal_kv) { if (!$journals_sess?.journal_kv) {
$journals_sess.journal_kv = {}; $journals_sess.journal_kv = {};
@@ -105,9 +115,9 @@
passcode_timer = null; passcode_timer = null;
}, timeout_ms); }, timeout_ms);
} }
}); });
function verify_journal_passcode() { function verify_journal_passcode() {
if (log_lvl) { if (log_lvl) {
console.log( console.log(
`verify_journal_passcode: typed_journal_passcode = ${typed_journal_passcode} journal private passcode = ${$lq__journal_obj?.private_passcode}` `verify_journal_passcode: typed_journal_passcode = ${typed_journal_passcode} journal private passcode = ${$lq__journal_obj?.private_passcode}`
@@ -127,9 +137,9 @@
typed_journal_passcode = ''; typed_journal_passcode = '';
} else { } else {
} }
} }
async function handle_new_entry() { async function handle_new_entry() {
let data_kv = { let data_kv = {
category_code: null category_code: null
}; };
@@ -146,8 +156,7 @@
}); });
if (results?.journal_entry_id) { if (results?.journal_entry_id) {
$journals_slct.journal_entry_id = $journals_slct.journal_entry_id = results.journal_entry_id;
results.journal_entry_id;
$journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] = $journals_loc.entry.edit_kv[$journals_slct.journal_entry_id] =
'current'; 'current';
goto( goto(
@@ -158,63 +167,56 @@
console.error('Error creating journal entry:', error); console.error('Error creating journal entry:', error);
alert('Failed to create new journal entry.'); alert('Failed to create new journal entry.');
} }
} }
</script> </script>
<div class="relative group/view w-full mx-2 my-1"> <div class="group/view relative mx-2 my-1 w-full">
<!-- Glow ring — mirrors Quick Add and Journal List --> <!-- Glow ring — mirrors Quick Add and Journal List -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 class="from-primary-500 to-secondary-500 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-10 dark:opacity-20 rounded-2xl bg-linear-to-r opacity-10 blur
group-hover/view:opacity-25 dark:group-hover/view:opacity-35 transition duration-700
transition duration-700 pointer-events-none" group-hover/view:opacity-25 dark:opacity-20 dark:group-hover/view:opacity-35">
></div> </div>
<section <section
class="relative rounded-xl p-3 w-full class="relative flex w-full flex-col
flex flex-col gap-2 items-center justify-center items-center justify-center gap-2 rounded-xl border
bg-white dark:bg-gray-900 border-gray-200 bg-white
border border-gray-200 dark:border-gray-700 p-3 text-gray-900 shadow-xl
text-gray-900 dark:text-gray-100 dark:border-gray-700 dark:bg-gray-900
shadow-xl" dark:text-gray-100"
bind:clientHeight={$ae_loc.iframe_height_modal_body} bind:clientHeight={$ae_loc.iframe_height_modal_body}>
>
<header <header
class="ae_header journal__header flex flex-row flex-wrap gap-2 items-center justify-between w-full" class="ae_header journal__header flex w-full flex-row flex-wrap items-center justify-between gap-2">
>
<h2 class="journal__name h3 text-center"> <h2 class="journal__name h3 text-center">
<BookOpenText class="inline-block text-primary-500/80" /> <BookOpenText class="text-primary-500/80 inline-block" />
{@html $lq__journal_obj?.name ?? 'Loading...'} {@html $lq__journal_obj?.name ?? 'Loading...'}
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<span <span
class="badge preset-tonal-success font-bold text-lg px-2 ml-2" class="badge preset-tonal-success ml-2 px-2 text-lg font-bold"
title="Entries matching current filters" title="Entries matching current filters">
>
{$lq__journal_entry_obj_li?.length ?? '0'}<span {$lq__journal_entry_obj_li?.length ?? '0'}<span
class="text-xs opacity-50 ml-0.5">&times;</span class="ml-0.5 text-xs opacity-50">&times;</span>
>
</span> </span>
{/if} {/if}
{#await $journals_prom.load__journal_entry_obj_li} {#await $journals_prom.load__journal_entry_obj_li}
<LoaderCircle <LoaderCircle
size="1em" size="1em"
class="inline-block animate-spin ml-1 text-primary-500" class="text-primary-500 ml-1 inline-block animate-spin" />
/>
{/await} {/await}
</h2> </h2>
<div <div
class="grow flex flex-row flex-wrap gap-2 items-center justify-end" class="flex grow flex-row flex-wrap items-center justify-end gap-2">
>
<!-- Simplified Config Trigger --> <!-- Simplified Config Trigger -->
<button <button
type="button" type="button"
onclick={() => onclick={() =>
($journals_sess.show__modal_edit__journal_obj = true)} ($journals_sess.show__modal_edit__journal_obj = true)}
class="btn preset-tonal-secondary py-1 px-3 shadow-md" class="btn preset-tonal-secondary px-3 py-1 shadow-md"
title="Journal Config & Actions" title="Journal Config & Actions">
>
<Settings size="1.2em" class="mr-2" /> <Settings size="1.2em" class="mr-2" />
<span class="hidden md:inline">Config</span> <span class="hidden md:inline">Config</span>
</button> </button>
@@ -227,8 +229,7 @@
type="text" type="text"
bind:value={typed_journal_passcode} bind:value={typed_journal_passcode}
placeholder="Passcode" placeholder="Passcode"
class="input input-sm w-32" class="input input-sm w-32" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -239,24 +240,23 @@
<div <div
class=" class="
prose prose
space-y-1 word-break
p-2 prose-p:m-0
w-full max-w-(--breakpoint-sm) md:max-w-(--breakpoint-md) prose-p:p-0 prose-h1:underline prose-h1:decoration-double
font-mono
bg-gray-50 text-gray-900
dark:bg-gray-800 dark:text-gray-100
shadow-md rounded-lg
text-sm font-normal text-wrap word-break
prose-p:m-0 prose-p:p-0
prose-h1:underline prose-h1:decoration-double
prose-h2:underline prose-h2:underline
prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-h1:text-2xl prose-h2:text-xl
prose-h1:m-0 prose-h2:m-0 prose-h3:m-0 prose-h4:m-0 prose-h5:m-0 prose-h6:m-0 prose-h3:text-lg prose-h1:m-0
prose-h2:m-0 prose-h3:m-0
prose-h4:m-0 prose-h5:m-0 prose-h6:m-0 prose-li:m-0
prose-li:m-0 prose-li:p-0 prose-li:line-height-none prose-li:p-0 prose-li:line-height-none
" w-full max-w-(--breakpoint-sm)
> space-y-1
rounded-lg bg-gray-50 p-2
font-mono text-sm font-normal text-wrap text-gray-900 shadow-md
md:max-w-(--breakpoint-md) dark:bg-gray-800 dark:text-gray-100
">
{@html $lq__journal_obj.description_md_html} {@html $lq__journal_obj.description_md_html}
</div> </div>
{/if} {/if}
@@ -270,5 +270,4 @@
bind:show={$journals_sess.show__modal_edit__journal_obj} bind:show={$journals_sess.show__modal_edit__journal_obj}
on_new_entry={handle_new_entry} on_new_entry={handle_new_entry}
{on_show_export} {on_show_export}
{on_show_import} {on_show_import} />
/>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__journal_obj_li.svelte * ae_comp__journal_obj_li.svelte
* Modernized Journal List Component * Modernized Journal List Component
* Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop) * Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop)
* Style: Tailwind 4 + Skeleton UI Reference Standard * Style: Tailwind 4 + Skeleton UI Reference Standard
*/ */
import { BookOpenText, BookType, Calendar, Clock, Hash } from '@lucide/svelte'; import { BookOpenText, BookType, Calendar, Clock, Hash } from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores'; import { ae_loc } from '$lib/stores/ae_stores';
interface Props { interface Props {
lq__journal_obj_li: any; lq__journal_obj_li: any;
} }
let { lq__journal_obj_li }: Props = $props(); let { lq__journal_obj_li }: Props = $props();
</script> </script>
<!-- <!--
@@ -22,83 +22,106 @@
Uses plain Tailwind gray scale with explicit dark: variants — no Skeleton paired utilities — Uses plain Tailwind gray scale with explicit dark: variants — no Skeleton paired utilities —
so behavior is predictable regardless of which Skeleton theme is active. so behavior is predictable regardless of which Skeleton theme is active.
--> -->
<div class="w-full max-w-2xl relative group/list"> <div class="group/list relative w-full max-w-2xl">
<!-- Glow ring behind the list, same technique as Quick Add --> <!-- Glow ring behind the list, same technique as Quick Add -->
<div <div
class="absolute -inset-1 bg-linear-to-r from-primary-500 to-secondary-500 class="from-primary-500 to-secondary-500 pointer-events-none absolute -inset-1
rounded-2xl blur opacity-10 dark:opacity-20 rounded-2xl bg-linear-to-r opacity-10 blur
group-hover/list:opacity-25 dark:group-hover/list:opacity-35 transition duration-700
transition duration-700 pointer-events-none" group-hover/list:opacity-25 dark:opacity-20 dark:group-hover/list:opacity-35">
></div> </div>
<section class="journal_list relative w-full space-y-1.5 p-2 sm:p-3 <section
bg-white dark:bg-gray-900 class="journal_list relative w-full space-y-1.5 rounded-2xl border
border border-gray-200 dark:border-gray-700 border-gray-200 bg-white
rounded-2xl shadow-xl"> p-2 shadow-xl sm:p-3
dark:border-gray-700 dark:bg-gray-900">
{#if $lq__journal_obj_li && $lq__journal_obj_li.length} {#if $lq__journal_obj_li && $lq__journal_obj_li.length}
{#each $lq__journal_obj_li as journal (journal.journal_id)} {#each $lq__journal_obj_li as journal (journal.journal_id)}
<a <a
href="/journals/{journal?.journal_id}" href="/journals/{journal?.journal_id}"
class="journal_card group relative class="journal_card group border-l-primary-500/60
flex items-center gap-3 px-4 py-3 hover:border-l-primary-500 relative flex items-center gap-3
bg-gray-50 dark:bg-gray-800 rounded-xl border
border border-gray-200 dark:border-gray-700 border-l-4 border-gray-200 bg-gray-50
border-l-4 border-l-primary-500/60 px-4 py-3
rounded-xl shadow-sm shadow-sm transition-all
hover:shadow-md hover:border-l-primary-500 duration-150 ease-in-out
hover:bg-gray-100 dark:hover:bg-gray-700 hover:bg-gray-100 hover:shadow-md
active:scale-[0.99] active:scale-[0.99]
transition-all duration-150 ease-in-out" dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
class:hidden={(journal?.hide || !journal?.enable) && !$ae_loc.trusted_access} class:hidden={(journal?.hide || !journal?.enable) &&
!$ae_loc.trusted_access}
class:opacity-60={journal.hide} class:opacity-60={journal.hide}
class:!border-l-warning-500={!journal?.enable} class:!border-l-warning-500={!journal?.enable}>
>
<!-- Icon: fixed size, never squashed --> <!-- Icon: fixed size, never squashed -->
<div class="shrink-0 p-2 bg-primary-500/10 rounded-lg text-primary-500 <div
group-hover:bg-primary-500 group-hover:text-white transition-colors"> class="bg-primary-500/10 text-primary-500 group-hover:bg-primary-500 shrink-0 rounded-lg
p-2 transition-colors group-hover:text-white">
<BookType size="1.3em" /> <BookType size="1.3em" />
</div> </div>
<!-- Name + badge: min-w-0 flex-1 lets text shrink and wrap --> <!-- Name + badge: min-w-0 flex-1 lets text shrink and wrap -->
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="text-sm sm:text-base font-bold <div
text-gray-900 dark:text-gray-100 class="text-sm leading-snug font-bold
leading-snug break-words"> break-words text-gray-900
sm:text-base dark:text-gray-100">
{journal.name} {journal.name}
</div> </div>
<div class="flex flex-wrap items-center gap-2 mt-0.5"> <div class="mt-0.5 flex flex-wrap items-center gap-2">
{#if journal.type_code} {#if journal.type_code}
<span class="badge preset-tonal-warning text-[10px] uppercase tracking-wider font-bold"> <span
class="badge preset-tonal-warning text-[10px] font-bold tracking-wider uppercase">
{journal.type_code} {journal.type_code}
</span> </span>
{/if} {/if}
<!-- Description snippet: edit mode only --> <!-- Description snippet: edit mode only -->
{#if journal.description && $ae_loc.edit_mode} {#if journal.description && $ae_loc.edit_mode}
<span class="text-[11px] text-gray-500 dark:text-gray-400 font-mono truncate max-w-[16rem]"> <span
{ae_util.strip_html ? ae_util.strip_html(journal.description).slice(0, 60) : journal.description.slice(0, 60)}&hellip; class="max-w-[16rem] truncate font-mono text-[11px] text-gray-500 dark:text-gray-400">
{ae_util.strip_html
? ae_util
.strip_html(journal.description)
.slice(0, 60)
: journal.description.slice(
0,
60
)}&hellip;
</span> </span>
{/if} {/if}
</div> </div>
</div> </div>
<!-- Stats: right-aligned, compact --> <!-- Stats: right-aligned, compact -->
<div class="shrink-0 flex flex-col items-end gap-0.5 <div
class="flex shrink-0 flex-col items-end gap-0.5
text-xs text-gray-500 dark:text-gray-400"> text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-1" title="Entry Count"> <div
class="flex items-center gap-1"
title="Entry Count">
<Hash size="0.85em" /> <Hash size="0.85em" />
<span class="font-bold tabular-nums">{journal.journal_entry_count ?? 0}&times;</span> <span class="font-bold tabular-nums"
>{journal.journal_entry_count ??
0}&times;</span>
</div> </div>
<div class="flex items-center gap-1" title="Last Updated"> <div
class="flex items-center gap-1"
title="Last Updated">
<Clock size="0.85em" /> <Clock size="0.85em" />
<span class="tabular-nums">{ae_util.iso_datetime_formatter( <span class="tabular-nums"
>{ae_util.iso_datetime_formatter(
journal.updated_on || journal.created_on, journal.updated_on || journal.created_on,
'date_short' 'date_short'
)}</span> )}</span>
</div> </div>
{#if $ae_loc.edit_mode && $ae_loc.administrator_access} {#if $ae_loc.edit_mode && $ae_loc.administrator_access}
<div class="flex items-center gap-1 opacity-40" title="Created"> <div
class="flex items-center gap-1 opacity-40"
title="Created">
<Calendar size="0.85em" /> <Calendar size="0.85em" />
<span class="font-mono text-[10px] tabular-nums">{ae_util.iso_datetime_formatter( <span class="font-mono text-[10px] tabular-nums"
>{ae_util.iso_datetime_formatter(
journal.created_on, journal.created_on,
'date_short' 'date_short'
)}</span> )}</span>
@@ -107,24 +130,30 @@
</div> </div>
<!-- Chevron hint: desktop only --> <!-- Chevron hint: desktop only -->
<BookOpenText size="0.95em" <BookOpenText
class="shrink-0 opacity-0 group-hover:opacity-30 transition-opacity hidden sm:block" /> size="0.95em"
class="hidden shrink-0 opacity-0 transition-opacity group-hover:opacity-30 sm:block" />
<!-- Status overlays: edit mode only --> <!-- Status overlays: edit mode only -->
{#if $ae_loc.edit_mode} {#if $ae_loc.edit_mode}
<div class="absolute -top-1.5 -right-1.5 flex gap-1"> <div class="absolute -top-1.5 -right-1.5 flex gap-1">
{#if journal.hide} {#if journal.hide}
<span class="badge-icon preset-tonal-surface shadow-sm" title="Hidden">🚫</span> <span
class="badge-icon preset-tonal-surface shadow-sm"
title="Hidden">🚫</span>
{/if} {/if}
{#if !journal.enable} {#if !journal.enable}
<span class="badge-icon preset-tonal-warning shadow-sm" title="Disabled">⚠️</span> <span
class="badge-icon preset-tonal-warning shadow-sm"
title="Disabled">⚠️</span>
{/if} {/if}
</div> </div>
{/if} {/if}
</a> </a>
{/each} {/each}
{:else} {:else}
<div class="p-20 text-center text-gray-400 dark:text-gray-500 flex flex-col items-center gap-4"> <div
class="flex flex-col items-center gap-4 p-20 text-center text-gray-400 dark:text-gray-500">
<BookType size="4em" /> <BookType size="4em" />
<p class="text-xl">No journals found in this view.</p> <p class="text-xl">No journals found in this view.</p>
</div> </div>

View File

@@ -1,36 +1,49 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
/** /**
* ae_comp__modal_journal_config.svelte * ae_comp__modal_journal_config.svelte
* Standardized Module-level settings for Journals. * Standardized Module-level settings for Journals.
* Fixed Svelte 5 state placement and reactivity loops. * Fixed Svelte 5 state placement and reactivity loops.
*/ */
import { CalendarClock, Check, CodeXml, Database, Layout, MonitorPlay, MousePointerClick, Palette, Settings, ShieldCheck, Wrench, X } from '@lucide/svelte'; import {
import { Modal } from 'flowbite-svelte'; CalendarClock,
Check,
CodeXml,
Database,
Layout,
MonitorPlay,
MousePointerClick,
Palette,
Settings,
ShieldCheck,
Wrench,
X
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
show?: boolean; show?: boolean;
} }
let { log_lvl = 0, show = $bindable(false) }: Props = $props(); let { log_lvl = 0, show = $bindable(false) }: Props = $props();
// Internal State // Internal State
let tab: 'form' | 'local_json' | 'session_json' = $state('form'); let tab: 'form' | 'local_json' | 'session_json' = $state('form');
let tmp_config: any = $state({ let tmp_config: any = $state({
journal: {}, journal: {},
entry: {} entry: {}
}); });
$effect(() => { $effect(() => {
if (show) { if (show) {
untrack(() => { untrack(() => {
const fresh_config = JSON.parse(JSON.stringify($journals_loc)); const fresh_config = JSON.parse(JSON.stringify($journals_loc));
@@ -39,12 +52,12 @@
tmp_config = fresh_config; tmp_config = fresh_config;
}); });
} }
}); });
function handle_save() { function handle_save() {
journals_loc.set(tmp_config); journals_loc.set(tmp_config);
show = false; show = false;
} }
</script> </script>
<Modal <Modal
@@ -53,32 +66,32 @@
dismissable={false} dismissable={false}
placement="top-center" placement="top-center"
size="xl" size="xl"
class="relative flex flex-col mx-auto w-full bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-orange-300 dark:border-orange-700 rounded-lg shadow-xl" class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800" headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800" footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
>
{#snippet header()} {#snippet header()}
<h3 class="flex-1 flex items-center gap-2 text-lg font-bold"> <h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Wrench class="text-primary-500" /> <Wrench class="text-primary-500" />
<span>&AElig; Journals Module Config</span> <span>&AElig; Journals Module Config</span>
</h3> </h3>
<button type="button" class="btn-icon btn-icon-sm preset-tonal-surface ml-2" onclick={() => (show = false)}> <button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" /> <X size="1.1em" />
</button> </button>
{/snippet} {/snippet}
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4"> <div class="h-[60vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div <div
class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto sticky top-0 z-10 backdrop-blur-sm" class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
>
<button <button
type="button" type="button"
class="btn btn-sm transition-all {tab === 'form' class="btn btn-sm transition-all {tab === 'form'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'form')} onclick={() => (tab = 'form')}>
>
<Settings size="1.1em" class="mr-1" /> Config <Settings size="1.1em" class="mr-1" /> Config
</button> </button>
<button <button
@@ -86,8 +99,7 @@
class="btn btn-sm transition-all {tab === 'local_json' class="btn btn-sm transition-all {tab === 'local_json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'local_json')} onclick={() => (tab = 'local_json')}>
>
<Database size="1.1em" class="mr-1" /> Local JSON <Database size="1.1em" class="mr-1" /> Local JSON
</button> </button>
<button <button
@@ -95,71 +107,55 @@
class="btn btn-sm transition-all {tab === 'session_json' class="btn btn-sm transition-all {tab === 'session_json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'session_json')} onclick={() => (tab = 'session_json')}>
>
<CodeXml size="1.1em" class="mr-1" /> Session JSON <CodeXml size="1.1em" class="mr-1" /> Session JSON
</button> </button>
</div> </div>
{#if tab === 'form'} {#if tab === 'form'}
<div class="space-y-8 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-8 duration-300">
<!-- Date/Time Section --> <!-- Date/Time Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<CalendarClock size="1.2em" class="text-primary-500" /> <CalendarClock size="1.2em" class="text-primary-500" />
Date and Time Display Date and Time Display
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>DateTime Format</span >DateTime Format</span>
>
<select <select
bind:value={tmp_config.datetime_format} bind:value={tmp_config.datetime_format}
class="select select-sm" class="select select-sm">
>
<option value="datetime_12_short" <option value="datetime_12_short"
>MMM D, YY hh:mm A</option >MMM D, YY hh:mm A</option>
>
<option value="datetime_12_long" <option value="datetime_12_long"
>MMMM D, YYYY hh:mm A</option >MMMM D, YYYY hh:mm A</option>
>
<option value="datetime_short" <option value="datetime_short"
>MMM D, YY HH:mm</option >MMM D, YY HH:mm</option>
>
<option value="datetime_long" <option value="datetime_long"
>MMMM D, YYYY HH:mm</option >MMMM D, YYYY HH:mm</option>
>
<option value="datetime_us" <option value="datetime_us"
>US (MM/DD/YYYY hh:mm:ss A)</option >US (MM/DD/YYYY hh:mm:ss A)</option>
>
<option value="datetime_iso" <option value="datetime_iso"
>ISO (YYYY-MM-DD HH:mm:ss)</option >ISO (YYYY-MM-DD HH:mm:ss)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Time-Only Format</span >Time-Only Format</span>
>
<select <select
bind:value={tmp_config.time_format} bind:value={tmp_config.time_format}
class="select select-sm" class="select select-sm">
>
<option value="time_12_short" <option value="time_12_short"
>12-hour short (3:30 PM)</option >12-hour short (3:30 PM)</option>
>
<option value="time_12_long" <option value="time_12_long"
>12-hour long (3:30:45 PM)</option >12-hour long (3:30:45 PM)</option>
>
<option value="time_short" <option value="time_short"
>24-hour short (15:30)</option >24-hour short (15:30)</option>
>
<option value="time_long" <option value="time_long"
>24-hour long (15:30:45)</option >24-hour long (15:30:45)</option>
>
</select> </select>
</label> </label>
</div> </div>
@@ -168,23 +164,19 @@
<!-- UI Section --> <!-- UI Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<MousePointerClick <MousePointerClick
size="1.2em" size="1.2em"
class="text-primary-500" class="text-primary-500" />
/>
User Interface Preferences User Interface Preferences
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label <label
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_config.entry.auto_save} bind:checked={tmp_config.entry.auto_save}
class="checkbox" class="checkbox" />
/>
<div class="space-y-0.5"> <div class="space-y-0.5">
<span class="font-bold">Enable Auto-Save</span> <span class="font-bold">Enable Auto-Save</span>
<p class="text-xs opacity-60"> <p class="text-xs opacity-60">
@@ -193,16 +185,14 @@
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_config.show_id_random} bind:checked={tmp_config.show_id_random}
class="checkbox" class="checkbox" />
/>
<div class="space-y-0.5"> <div class="space-y-0.5">
<span class="font-bold">Show Technical IDs</span <span class="font-bold"
> >Show Technical IDs</span>
<p class="text-xs opacity-60"> <p class="text-xs opacity-60">
Display UUIDs in metadata footers Display UUIDs in metadata footers
</p> </p>
@@ -214,52 +204,42 @@
<!-- Journal Query Filters Section --> <!-- Journal Query Filters Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<Database size="1.2em" class="text-primary-500" /> <Database size="1.2em" class="text-primary-500" />
Journal Query Filters Journal Query Filters
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Enabled Status</span >Enabled Status</span>
>
<select <select
bind:value={tmp_config.journal.qry__enabled} bind:value={tmp_config.journal.qry__enabled}
class="select select-sm" class="select select-sm">
>
<option value="enabled">Enabled Only</option> <option value="enabled">Enabled Only</option>
<option value="not_enabled" <option value="not_enabled"
>Disabled Only</option >Disabled Only</option>
>
<option value="all" <option value="all"
>All (Enabled & Disabled & NULL)</option >All (Enabled & Disabled & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Hidden Status</span >Hidden Status</span>
>
<select <select
bind:value={tmp_config.journal.qry__hidden} bind:value={tmp_config.journal.qry__hidden}
class="select select-sm" class="select select-sm">
>
<option value="not_hidden">Visible Only</option> <option value="not_hidden">Visible Only</option>
<option value="hidden">Hidden Only</option> <option value="hidden">Hidden Only</option>
<option value="all" <option value="all"
>All (Visible & Hidden & NULL)</option >All (Visible & Hidden & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Query Limit</span >Query Limit</span>
>
<select <select
bind:value={tmp_config.journal.qry__limit} bind:value={tmp_config.journal.qry__limit}
class="select select-sm" class="select select-sm">
>
<option value={10}>10</option> <option value={10}>10</option>
<option value={20}>20</option> <option value={20}>20</option>
<option value={50}>50</option> <option value={50}>50</option>
@@ -275,52 +255,42 @@
<!-- Entry Query Filters Section --> <!-- Entry Query Filters Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<Database size="1.2em" class="text-primary-500" /> <Database size="1.2em" class="text-primary-500" />
Entry Query Filters Entry Query Filters
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 p-2"> <div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Enabled Status</span >Enabled Status</span>
>
<select <select
bind:value={tmp_config.entry.qry__enabled} bind:value={tmp_config.entry.qry__enabled}
class="select select-sm" class="select select-sm">
>
<option value="enabled">Enabled Only</option> <option value="enabled">Enabled Only</option>
<option value="not_enabled" <option value="not_enabled"
>Disabled Only</option >Disabled Only</option>
>
<option value="all" <option value="all"
>All (Enabled & Disabled & NULL)</option >All (Enabled & Disabled & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Hidden Status</span >Hidden Status</span>
>
<select <select
bind:value={tmp_config.entry.qry__hidden} bind:value={tmp_config.entry.qry__hidden}
class="select select-sm" class="select select-sm">
>
<option value="not_hidden">Visible Only</option> <option value="not_hidden">Visible Only</option>
<option value="hidden">Hidden Only</option> <option value="hidden">Hidden Only</option>
<option value="all" <option value="all"
>All (Visible & Hidden & NULL)</option >All (Visible & Hidden & NULL)</option>
>
</select> </select>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Query Limit</span >Query Limit</span>
>
<select <select
bind:value={tmp_config.entry.qry__limit} bind:value={tmp_config.entry.qry__limit}
class="select select-sm" class="select select-sm">
>
<option value={10}>10</option> <option value={10}>10</option>
<option value={20}>20</option> <option value={20}>20</option>
<option value={50}>50</option> <option value={50}>50</option>
@@ -336,30 +306,25 @@
<!-- Security Section --> <!-- Security Section -->
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-xl font-bold">
>
<ShieldCheck size="1.2em" class="text-primary-500" /> <ShieldCheck size="1.2em" class="text-primary-500" />
Security & Encryption Security & Encryption
</h2> </h2>
<div <div
class="p-4 bg-orange-500/5 rounded-lg border border-orange-500/20 space-y-4" class="space-y-4 rounded-lg border border-orange-500/20 bg-orange-500/5 p-4">
> <div class="text-sm italic opacity-80">
<div class="text-sm opacity-80 italic">
Global security overrides for the journal module. Global security overrides for the journal module.
</div> </div>
<label <label
class="flex items-center space-x-3 cursor-pointer" class="flex cursor-pointer items-center space-x-3">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
$journals_sess.enable_session_passcode_cache $journals_sess.enable_session_passcode_cache
} }
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<span class="text-sm font-bold" <span class="text-sm font-bold"
>Cache Passcodes in Session</span >Cache Passcodes in Session</span>
>
</label> </label>
</div> </div>
</section> </section>
@@ -370,8 +335,7 @@
readonly={true} readonly={true}
content={JSON.stringify(tmp_config, null, 2)} content={JSON.stringify(tmp_config, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{:else if tab === 'session_json'} {:else if tab === 'session_json'}
<div class="h-full min-h-[400px]"> <div class="h-full min-h-[400px]">
@@ -379,8 +343,7 @@
readonly={true} readonly={true}
content={JSON.stringify($journals_sess, null, 2)} content={JSON.stringify($journals_sess, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -389,16 +352,14 @@
<div class="flex gap-4"> <div class="flex gap-4">
<button <button
type="button" type="button"
class="btn preset-tonal-surface font-bold min-w-[100px]" class="btn preset-tonal-surface min-w-[100px] font-bold"
onclick={() => (show = false)} onclick={() => (show = false)}>
>
<X size="1.2em" class="mr-2" /> Cancel <X size="1.2em" class="mr-2" /> Cancel
</button> </button>
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold shadow-lg min-w-[120px]" class="btn preset-filled-primary min-w-[120px] font-bold shadow-lg"
onclick={handle_save} onclick={handle_save}>
>
<Check size="1.2em" class="mr-2" /> Save Changes <Check size="1.2em" class="mr-2" /> Save Changes
</button> </button>
</div> </div>

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Check, X } from '@lucide/svelte'; import { Check, X } from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
interface Props { interface Props {
open: boolean; open: boolean;
journal_entry: key_val; journal_entry: key_val;
journal_config: key_val; // The cfg_json from the journal object journal_config: key_val; // The cfg_json from the journal object
@@ -14,9 +14,9 @@
on_close: () => void; on_close: () => void;
on_update: () => void; on_update: () => void;
log_lvl?: number; log_lvl?: number;
} }
let { let {
open = $bindable(false), open = $bindable(false),
journal_entry, journal_entry,
journal_config, journal_config,
@@ -24,32 +24,32 @@
on_close, on_close,
on_update, on_update,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
// Local State // Local State
let tmp_entry_obj: key_val = $state({}); let tmp_entry_obj: key_val = $state({});
// Header Options // Header Options
let add_timestamp_header: boolean = $state(true); let add_timestamp_header: boolean = $state(true);
let add_timestamp_header_w_day_of_week: boolean = $state(true); let add_timestamp_header_w_day_of_week: boolean = $state(true);
let add_text_header: string = $state(''); let add_text_header: string = $state('');
let add_text: string = $state(''); let add_text: string = $state('');
// Change detection // Change detection
let has_changes: boolean = $derived( let has_changes: boolean = $derived(
add_text_header.length > 0 || add_text.length > 0 add_text_header.length > 0 || add_text.length > 0
); );
// Initialize tmp object when entry changes or modal opens // Initialize tmp object when entry changes or modal opens
$effect(() => { $effect(() => {
if (open && journal_entry) { if (open && journal_entry) {
tmp_entry_obj = JSON.parse(JSON.stringify(journal_entry)); tmp_entry_obj = JSON.parse(JSON.stringify(journal_entry));
// Reset fields // Reset fields
add_text_header = ''; add_text_header = '';
add_text = ''; add_text = '';
} }
}); });
async function handle_save() { async function handle_save() {
let current_entry_content = tmp_entry_obj?.content || ''; let current_entry_content = tmp_entry_obj?.content || '';
let add_content = ''; let add_content = '';
let new_content = current_entry_content; let new_content = current_entry_content;
@@ -60,9 +60,7 @@
'datetime_iso_12_no_seconds' 'datetime_iso_12_no_seconds'
); );
let day_of_week_str = add_timestamp_header_w_day_of_week let day_of_week_str = add_timestamp_header_w_day_of_week
? ' (' + ? ' (' + ae_util.iso_datetime_formatter(new Date(), 'week_long') + ')'
ae_util.iso_datetime_formatter(new Date(), 'week_long') +
')'
: ''; : '';
if (add_timestamp_header && add_text_header) { if (add_timestamp_header && add_text_header) {
@@ -113,8 +111,7 @@
let data_kv = { content: new_content }; let data_kv = { content: new_content };
try { try {
let update_result = let update_result = await journals_func.update_ae_obj__journal_entry({
await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api, api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj?.journal_entry_id, journal_entry_id: tmp_entry_obj?.journal_entry_id,
data_kv: data_kv, data_kv: data_kv,
@@ -132,7 +129,7 @@
console.error('Error updating journal entry:', error); console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.'); alert('Failed to update journal entry.');
} }
} }
</script> </script>
<Modal <Modal
@@ -145,8 +142,7 @@
autoclose={false} autoclose={false}
placement="top-center" placement="top-center"
size="xl" size="xl"
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative flex flex-col gap-1 mx-auto w-full" class="top-center relative mx-auto flex w-full flex-col gap-1 divide-gray-200 rounded-lg border-gray-200 bg-white text-gray-800 shadow-md dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200">
>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<!-- Checkbox Options --> <!-- Checkbox Options -->
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
@@ -155,8 +151,7 @@
type="checkbox" type="checkbox"
id="append_timestamp_header" id="append_timestamp_header"
bind:checked={add_timestamp_header} bind:checked={add_timestamp_header}
class="checkbox" class="checkbox" />
/>
<span>Use timestamp as Markdown header</span> <span>Use timestamp as Markdown header</span>
</label> </label>
@@ -165,8 +160,7 @@
type="checkbox" type="checkbox"
id="append_timestamp_header_w_day_of_week" id="append_timestamp_header_w_day_of_week"
bind:checked={add_timestamp_header_w_day_of_week} bind:checked={add_timestamp_header_w_day_of_week}
class="checkbox" class="checkbox" />
/>
<span>Include day of week</span> <span>Include day of week</span>
</label> </label>
</div> </div>
@@ -176,8 +170,7 @@
type="text" type="text"
placeholder="Markdown header for content (Optional)" placeholder="Markdown header for content (Optional)"
bind:value={add_text_header} bind:value={add_text_header}
class="input" class="input" />
/>
<!-- Main Content Area --> <!-- Main Content Area -->
<textarea <textarea
@@ -185,26 +178,23 @@
class="textarea min-h-48" class="textarea min-h-48"
placeholder="Content to {mode === 'auto' placeholder="Content to {mode === 'auto'
? journal_config?.entry_add_text ? journal_config?.entry_add_text
: mode}..." : mode}...">
>
</textarea> </textarea>
<div class="flex justify-end gap-2 mt-2"> <div class="mt-2 flex justify-end gap-2">
<button <button
type="button" type="button"
disabled={!has_changes} disabled={!has_changes}
onclick={handle_save} onclick={handle_save}
class="btn preset-filled-primary" class="btn preset-filled-primary"
class:opacity-50={!has_changes} class:opacity-50={!has_changes}>
>
<Check class="mr-1" /> <Check class="mr-1" />
Update Update
</button> </button>
<button <button
type="button" type="button"
onclick={on_close} onclick={on_close}
class="btn preset-tonal-surface" class="btn preset-tonal-surface">
>
<X class="mr-1" /> <X class="mr-1" />
Cancel Cancel
</button> </button>

View File

@@ -1,20 +1,40 @@
<script lang="ts"> <script lang="ts">
/** /**
* ae_comp__modal_journal_entry_config.svelte * ae_comp__modal_journal_entry_config.svelte
* Standardized Journal Entry-level configuration. * Standardized Journal Entry-level configuration.
*/ */
import { ArrowDownToLine, ArrowUpToLine, Check, Clock, CodeXml, Copy, Database, FileDown, Fingerprint, Minus, Plus, RefreshCcw, Settings, Shapes, ShieldCheck, Tag, Trash2, X, Zap } from '@lucide/svelte'; import {
import { Modal } from 'flowbite-svelte'; ArrowDownToLine,
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; ArrowUpToLine,
import { Check,
Clock,
CodeXml,
Copy,
Database,
FileDown,
Fingerprint,
Minus,
Plus,
RefreshCcw,
Settings,
Shapes,
ShieldCheck,
Tag,
Trash2,
X,
Zap
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import {
journals_loc, journals_loc,
journals_sess journals_sess
} from '$lib/ae_journals/ae_journals_stores'; } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte'; import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
show?: boolean; show?: boolean;
entry: any; entry: any;
@@ -25,9 +45,9 @@
on_show_export?: () => void; on_show_export?: () => void;
on_append?: () => void; on_append?: () => void;
on_prepend?: () => void; on_prepend?: () => void;
} }
let { let {
log_lvl = $bindable(0), log_lvl = $bindable(0),
show = $bindable(false), show = $bindable(false),
entry, entry,
@@ -38,14 +58,13 @@
on_show_export, on_show_export,
on_append, on_append,
on_prepend on_prepend
}: Props = $props(); }: Props = $props();
let tab: 'actions' | 'meta' | 'security' | 'json' = $state('actions'); let tab: 'actions' | 'meta' | 'security' | 'json' = $state('actions');
const normalize_date = (val: string | null) => const normalize_date = (val: string | null) => (val ? val.slice(0, 16) : null);
val ? val.slice(0, 16) : null;
async function handle_update_entry() { async function handle_update_entry() {
try { try {
// WHITELISTED BASE TABLE COLUMNS ONLY // WHITELISTED BASE TABLE COLUMNS ONLY
const data_kv = { const data_kv = {
@@ -83,7 +102,7 @@
} catch (error) { } catch (error) {
console.error('Error updating journal entry:', error); console.error('Error updating journal entry:', error);
} }
} }
</script> </script>
<Modal <Modal
@@ -92,32 +111,32 @@
dismissable={false} dismissable={false}
placement="top-center" placement="top-center"
size="lg" size="lg"
class="relative flex flex-col mx-auto w-full bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-orange-300 dark:border-orange-700 rounded-lg shadow-xl" class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800" headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800" footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
>
{#snippet header()} {#snippet header()}
<h3 class="flex-1 flex items-center gap-2 text-lg font-bold"> <h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Settings class="text-primary-500" /> <Settings class="text-primary-500" />
<span>Entry Config: {tmp_entry_obj.name || '--'}</span> <span>Entry Config: {tmp_entry_obj.name || '--'}</span>
</h3> </h3>
<button type="button" class="btn-icon btn-icon-sm preset-tonal-surface ml-2" onclick={() => (show = false)}> <button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" /> <X size="1.1em" />
</button> </button>
{/snippet} {/snippet}
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4"> <div class="h-[60vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div <div
class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto sticky top-0 z-10 backdrop-blur-sm" class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
>
<button <button
type="button" type="button"
class="btn btn-sm transition-all {tab === 'actions' class="btn btn-sm transition-all {tab === 'actions'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'actions')} onclick={() => (tab = 'actions')}>
>
<Zap size="1.1em" class="mr-1" /> Quick Actions <Zap size="1.1em" class="mr-1" /> Quick Actions
</button> </button>
<button <button
@@ -125,8 +144,7 @@
class="btn btn-sm transition-all {tab === 'meta' class="btn btn-sm transition-all {tab === 'meta'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'meta')} onclick={() => (tab = 'meta')}>
>
<Shapes size="1.1em" class="mr-1" /> Metadata <Shapes size="1.1em" class="mr-1" /> Metadata
</button> </button>
<button <button
@@ -134,8 +152,7 @@
class="btn btn-sm transition-all {tab === 'security' class="btn btn-sm transition-all {tab === 'security'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'security')} onclick={() => (tab = 'security')}>
>
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security <ShieldCheck size="1.1em" class="mr-1" /> Status & Security
</button> </button>
<button <button
@@ -143,23 +160,21 @@
class="btn btn-sm transition-all {tab === 'json' class="btn btn-sm transition-all {tab === 'json'
? 'preset-filled-primary' ? 'preset-filled-primary'
: 'preset-tonal-surface'}" : 'preset-tonal-surface'}"
onclick={() => (tab = 'json')} onclick={() => (tab = 'json')}>
>
<CodeXml size="1.1em" class="mr-1" /> JSON <CodeXml size="1.1em" class="mr-1" /> JSON
</button> </button>
</div> </div>
{#if tab === 'actions'} {#if tab === 'actions'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<section class="grid grid-cols-1 md:grid-cols-2 gap-4"> <section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<button <button
type="button" type="button"
class="btn preset-tonal-secondary w-full" class="btn preset-tonal-secondary w-full"
onclick={() => { onclick={() => {
show = false; show = false;
on_prepend?.(); on_prepend?.();
}} }}>
>
<ArrowUpToLine size="1.2em" class="mr-2" /> Prepend Content <ArrowUpToLine size="1.2em" class="mr-2" /> Prepend Content
</button> </button>
<button <button
@@ -168,8 +183,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_append?.(); on_append?.();
}} }}>
>
<ArrowDownToLine size="1.2em" class="mr-2" /> Append Content <ArrowDownToLine size="1.2em" class="mr-2" /> Append Content
</button> </button>
<button <button
@@ -178,8 +192,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_show_export?.(); on_show_export?.();
}} }}>
>
<FileDown size="1.2em" class="mr-2" /> Export Entry <FileDown size="1.2em" class="mr-2" /> Export Entry
</button> </button>
<button <button
@@ -189,13 +202,12 @@
/* Clone logic here */ alert( /* Clone logic here */ alert(
'Clone not yet implemented in modal' 'Clone not yet implemented in modal'
); );
}} }}>
>
<Copy size="1.2em" class="mr-2" /> Clone Entry <Copy size="1.2em" class="mr-2" /> Clone Entry
</button> </button>
</section> </section>
<section class="space-y-4 pt-4 border-t border-surface-500/20"> <section class="border-surface-500/20 space-y-4 border-t pt-4">
<h4 class="text-xs font-bold uppercase opacity-50"> <h4 class="text-xs font-bold uppercase opacity-50">
Quick Category Quick Category
</h4> </h4>
@@ -211,8 +223,7 @@
tmp_entry_obj.category_code = cat.code; tmp_entry_obj.category_code = cat.code;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}>
>
{cat.name} {cat.name}
</button> </button>
{/each} {/each}
@@ -220,7 +231,7 @@
</section> </section>
</div> </div>
{:else if tab === 'meta'} {:else if tab === 'meta'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70">Category</span> <span class="text-sm font-bold opacity-70">Category</span>
<select <select
@@ -229,8 +240,7 @@
onchange={() => { onchange={() => {
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}>
>
<option value="">None</option> <option value="">None</option>
{#each journal?.cfg_json?.category_li ?? [] as cat (cat.code)} {#each journal?.cfg_json?.category_li ?? [] as cat (cat.code)}
<option value={cat.code}>{cat.name}</option> <option value={cat.code}>{cat.name}</option>
@@ -240,9 +250,8 @@
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Tags (Comma separated)</span >Tags (Comma separated)</span>
> <div class="flex items-center gap-2">
<div class="flex gap-2 items-center">
<Tag size="1.2em" class="opacity-30" /> <Tag size="1.2em" class="opacity-30" />
<input <input
type="text" type="text"
@@ -252,16 +261,14 @@
onchange={() => { onchange={() => {
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }} />
/>
</div> </div>
</label> </label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Archive On</span >Archive On</span>
>
<input <input
type="datetime-local" type="datetime-local"
value={normalize_date(tmp_entry_obj.archive_on)} value={normalize_date(tmp_entry_obj.archive_on)}
@@ -271,13 +278,11 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="input" class="input" />
/>
</label> </label>
<label class="label"> <label class="label">
<span class="text-sm font-bold opacity-70" <span class="text-sm font-bold opacity-70"
>Sort Priority</span >Sort Priority</span>
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
type="button" type="button"
@@ -287,11 +292,9 @@
(tmp_entry_obj.sort ?? 0) - 1; (tmp_entry_obj.sort ?? 0) - 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Minus size="1em" /></button }}><Minus size="1em" /></button>
> <span class="w-8 text-center font-mono font-bold"
<span class="font-mono font-bold w-8 text-center" >{tmp_entry_obj.sort ?? 0}</span>
>{tmp_entry_obj.sort ?? 0}</span
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
@@ -300,25 +303,22 @@
(tmp_entry_obj.sort ?? 0) + 1; (tmp_entry_obj.sort ?? 0) + 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Plus size="1em" /></button }}><Plus size="1em" /></button>
>
</div> </div>
</label> </label>
</div> </div>
</div> </div>
{:else if tab === 'security'} {:else if tab === 'security'}
<div class="space-y-6 animate-in fade-in duration-300"> <div class="animate-in fade-in space-y-6 duration-300">
<section class="space-y-4"> <section class="space-y-4">
<h2 <h2
class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2" class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
>
<Fingerprint size="1.2em" class="text-primary-500" /> <Fingerprint size="1.2em" class="text-primary-500" />
Status & Security Status & Security
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.enable} bind:checked={tmp_entry_obj.enable}
@@ -326,18 +326,15 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Enabled</span> <span class="font-bold">Enabled</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Allow access to this entry</span >Allow access to this entry</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.hide} bind:checked={tmp_entry_obj.hide}
@@ -345,18 +342,15 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Hidden</span> <span class="font-bold">Hidden</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Hide from standard lists</span >Hide from standard lists</span>
>
</div> </div>
</label> </label>
<label <label
class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10 transition-colors hover:bg-surface-500/10" class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={tmp_entry_obj.priority} bind:checked={tmp_entry_obj.priority}
@@ -364,24 +358,20 @@
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}} }}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold">Priority Entry</span> <span class="font-bold">Priority Entry</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Star or pin to top</span >Star or pin to top</span>
>
</div> </div>
</label> </label>
<div <div
class="flex items-center justify-between p-3 rounded-lg bg-surface-500/5 border border-surface-500/10" class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold text-sm">Sort Order</span <span class="text-sm font-bold"
> >Sort Order</span>
<span class="text-xs opacity-60" <span class="text-xs opacity-60"
>Manual list position</span >Manual list position</span>
>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
@@ -392,12 +382,10 @@
(tmp_entry_obj.sort ?? 0) - 1; (tmp_entry_obj.sort ?? 0) - 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Minus size="1em" /></button }}><Minus size="1em" /></button>
>
<span <span
class="font-mono font-bold w-8 text-center text-lg" class="w-8 text-center font-mono text-lg font-bold"
>{tmp_entry_obj.sort ?? 0}</span >{tmp_entry_obj.sort ?? 0}</span>
>
<button <button
type="button" type="button"
class="btn-icon btn-icon-sm preset-tonal-surface" class="btn-icon btn-icon-sm preset-tonal-surface"
@@ -406,8 +394,7 @@
(tmp_entry_obj.sort ?? 0) + 1; (tmp_entry_obj.sort ?? 0) + 1;
handle_update_entry(); handle_update_entry();
on_save(); on_save();
}}><Plus size="1em" /></button }}><Plus size="1em" /></button>
>
</div> </div>
</div> </div>
</div> </div>
@@ -429,21 +416,18 @@
hide_personal={journal?.cfg_json?.hide_btn_personal} hide_personal={journal?.cfg_json?.hide_btn_personal}
hide_professional={journal?.cfg_json hide_professional={journal?.cfg_json
?.hide_btn_professional} ?.hide_btn_professional}
hide_template={journal?.cfg_json?.hide_btn_template} hide_template={journal?.cfg_json?.hide_btn_template} />
/>
</section> </section>
{#if tmp_entry_obj.private && !tmp_entry_obj.content && tmp_entry_obj.content_encrypted} {#if tmp_entry_obj.private && !tmp_entry_obj.content && tmp_entry_obj.content_encrypted}
<section class="pt-8 border-t border-error-500/20"> <section class="border-error-500/20 border-t pt-8">
<div <div
class="bg-error-500/10 p-4 rounded-lg border border-error-500/30" class="bg-error-500/10 border-error-500/30 rounded-lg border p-4">
>
<h4 <h4
class="text-error-500 font-bold flex items-center gap-2 mb-2" class="text-error-500 mb-2 flex items-center gap-2 font-bold">
>
<RefreshCcw size="1.2em" /> Disaster Recovery <RefreshCcw size="1.2em" /> Disaster Recovery
</h4> </h4>
<p class="text-xs opacity-70 mb-4 italic"> <p class="mb-4 text-xs italic opacity-70">
If the encryption passcode is lost, the data is If the encryption passcode is lost, the data is
unrecoverable. You can force a reset to plain unrecoverable. You can force a reset to plain
text to reuse this entry ID. text to reuse this entry ID.
@@ -454,8 +438,7 @@
onclick={() => { onclick={() => {
show = false; show = false;
on_force_reset?.(); on_force_reset?.();
}} }}>
>
Force Reset to Plain Text Force Reset to Plain Text
</button> </button>
</div> </div>
@@ -468,8 +451,7 @@
class="btn btn-sm preset-tonal-error w-full" class="btn btn-sm preset-tonal-error w-full"
onclick={() => { onclick={() => {
alert('Delete logic handled in parent component'); alert('Delete logic handled in parent component');
}} }}>
>
<Trash2 size="1.1em" class="mr-2" /> Delete Entry <Trash2 size="1.1em" class="mr-2" /> Delete Entry
</button> </button>
</section> </section>
@@ -480,8 +462,7 @@
readonly={true} readonly={true}
content={JSON.stringify(tmp_entry_obj, null, 2)} content={JSON.stringify(tmp_entry_obj, null, 2)}
theme_mode={$ae_loc.theme_mode} theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" class_li="rounded-lg border border-surface-500/30" />
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -489,9 +470,8 @@
{#snippet footer()} {#snippet footer()}
<button <button
type="button" type="button"
class="btn preset-filled-primary font-bold min-w-[120px]" class="btn preset-filled-primary min-w-[120px] font-bold"
onclick={() => (show = false)} onclick={() => (show = false)}>
>
<Check size="1.2em" class="mr-2" /> <Check size="1.2em" class="mr-2" />
Done Done
</button> </button>

View File

@@ -1,48 +1,54 @@
<script lang="ts"> <script lang="ts">
/** /**
* @file ae_comp__modal_journal_export.svelte * @file ae_comp__modal_journal_export.svelte
* @description Modal component for bulk exporting journal entries. * @description Modal component for bulk exporting journal entries.
* Allows exporting entries using various templates (Markdown, HTML, JSON). * Allows exporting entries using various templates (Markdown, HTML, JSON).
* @author One Sky IT * @author One Sky IT
*/ */
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Code, Copy, Download, FileJson, FileType, Settings2 } from '@lucide/svelte'; import {
import { ae_util } from '$lib/ae_utils/ae_utils'; Code,
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types'; Copy,
import { Download,
FileJson,
FileType,
Settings2
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
import {
EXPORT_TEMPLATES, EXPORT_TEMPLATES,
type ExportTemplate type ExportTemplate
} from '$lib/ae_journals/ae_journals_export_templates'; } from '$lib/ae_journals/ae_journals_export_templates';
interface Props { interface Props {
open: boolean; open: boolean;
entries: ae_JournalEntry[]; entries: ae_JournalEntry[];
journal?: ae_Journal; journal?: ae_Journal;
on_close: () => void; on_close: () => void;
} }
let { let {
open = $bindable(false), open = $bindable(false),
entries = [], entries = [],
journal, journal,
on_close on_close
}: Props = $props(); }: Props = $props();
// State // State
let selected_template_id: string = $state('standard_markdown'); let selected_template_id: string = $state('standard_markdown');
let export_preview: string = $state(''); let export_preview: string = $state('');
let export_count: number = $derived(entries.length); let export_count: number = $derived(entries.length);
const templates = Object.values(EXPORT_TEMPLATES); const templates = Object.values(EXPORT_TEMPLATES);
const selected_template = $derived( const selected_template = $derived(
EXPORT_TEMPLATES[ EXPORT_TEMPLATES[selected_template_id as keyof typeof EXPORT_TEMPLATES] ||
selected_template_id as keyof typeof EXPORT_TEMPLATES EXPORT_TEMPLATES.standard_markdown
] || EXPORT_TEMPLATES.standard_markdown );
);
// Auto-select template based on journal type when modal opens // Auto-select template based on journal type when modal opens
$effect(() => { $effect(() => {
if (open && journal?.type_code) { if (open && journal?.type_code) {
if ( if (
journal.type_code === 'personal_log' && journal.type_code === 'personal_log' &&
@@ -56,24 +62,24 @@
selected_template_id = 'amazon_vine'; selected_template_id = 'amazon_vine';
} }
} }
}); });
// Re-generate preview when entries or template changes // Re-generate preview when entries or template changes
$effect(() => { $effect(() => {
if (open && entries.length > 0) { if (open && entries.length > 0) {
generate_preview(); generate_preview();
} }
}); });
function generate_preview() { function generate_preview() {
if (!selected_template) return; if (!selected_template) return;
export_preview = selected_template.formatter(entries); export_preview = selected_template.formatter(entries);
} }
/** /**
* Downloads the generated content as a file. * Downloads the generated content as a file.
*/ */
function handle_download() { function handle_download() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `journal_export_${timestamp}.${selected_template.extension}`; const filename = `journal_export_${timestamp}.${selected_template.extension}`;
const blob = new Blob([export_preview], { const blob = new Blob([export_preview], {
@@ -88,12 +94,12 @@
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
/** /**
* Copies the generated content to the clipboard. * Copies the generated content to the clipboard.
*/ */
async function handle_copy() { async function handle_copy() {
try { try {
await navigator.clipboard.writeText(export_preview); await navigator.clipboard.writeText(export_preview);
alert('Content copied to clipboard!'); alert('Content copied to clipboard!');
@@ -101,7 +107,7 @@
console.error('Failed to copy:', err); console.error('Failed to copy:', err);
alert('Failed to copy to clipboard.'); alert('Failed to copy to clipboard.');
} }
} }
</script> </script>
<Modal <Modal
@@ -109,12 +115,10 @@
bind:open bind:open
autoclose={false} autoclose={false}
size="xl" size="xl"
class="w-full" class="w-full">
>
<div class="space-y-4"> <div class="space-y-4">
<div <div
class="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 rounded-lg" class="flex items-center justify-between rounded-lg bg-blue-50 p-4 text-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
>
<div> <div>
<strong>Ready to Export:</strong> <strong>Ready to Export:</strong>
{export_count} entries. {export_count} entries.
@@ -122,8 +126,7 @@
{#if journal} {#if journal}
<div class="text-xs opacity-70"> <div class="text-xs opacity-70">
Journal Type: <span class="font-mono" Journal Type: <span class="font-mono"
>{journal.type_code || 'standard'}</span >{journal.type_code || 'standard'}</span>
>
</div> </div>
{/if} {/if}
</div> </div>
@@ -134,19 +137,18 @@
<Settings2 size="1em" /> <Settings2 size="1em" />
<span class="label-text font-bold">Select Export Template</span> <span class="label-text font-bold">Select Export Template</span>
</label> </label>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2"> <div class="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-5">
{#each templates as template (template.id)} {#each templates as template (template.id)}
<button <button
type="button" type="button"
class="btn preset-outlined-surface flex flex-col gap-1 p-2 h-24 items-center justify-center border-2 text-center {selected_template_id === class="btn preset-outlined-surface flex h-24 flex-col items-center justify-center gap-1 border-2 p-2 text-center {selected_template_id ===
template.id template.id
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'opacity-60'}" : 'opacity-60'}"
onclick={() => { onclick={() => {
selected_template_id = template.id; selected_template_id = template.id;
generate_preview(); generate_preview();
}} }}>
>
{#if template.extension === 'json'} {#if template.extension === 'json'}
<FileJson size="1.5em" /> <FileJson size="1.5em" />
{:else if template.extension === 'html'} {:else if template.extension === 'html'}
@@ -154,12 +156,10 @@
{:else} {:else}
<FileType size="1.5em" /> <FileType size="1.5em" />
{/if} {/if}
<span class="text-xs font-bold leading-tight" <span class="text-xs leading-tight font-bold"
>{template.name}</span >{template.name}</span>
>
<span class="text-[10px] opacity-70" <span class="text-[10px] opacity-70"
>.{template.extension}</span >.{template.extension}</span>
>
</button> </button>
{/each} {/each}
</div> </div>
@@ -167,43 +167,38 @@
<!-- Preview Area --> <!-- Preview Area -->
<div class="form-control"> <div class="form-control">
<div class="label flex justify-between items-center"> <div class="label flex items-center justify-between">
<span class="label-text">Preview</span> <span class="label-text">Preview</span>
<span class="text-[10px] opacity-50 uppercase tracking-widest" <span class="text-[10px] tracking-widest uppercase opacity-50"
>{selected_template.name}</span >{selected_template.name}</span>
>
</div> </div>
<textarea <textarea
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900" class="textarea h-64 bg-gray-50 font-mono text-xs dark:bg-gray-900"
readonly readonly
value={export_preview.substring(0, 5000) + value={export_preview.substring(0, 5000) +
(export_preview.length > 5000 (export_preview.length > 5000
? '\n... (truncated for preview)' ? '\n... (truncated for preview)'
: '')} : '')}></textarea>
></textarea>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="modal-action flex justify-between items-center"> <div class="modal-action flex items-center justify-between">
<button <button
type="button" type="button"
class="btn preset-tonal-secondary" class="btn preset-tonal-secondary"
onclick={on_close}>Close</button onclick={on_close}>Close</button>
>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
type="button" type="button"
class="btn preset-tonal-primary" class="btn preset-tonal-primary"
onclick={handle_copy} onclick={handle_copy}>
>
<Copy class="mr-2" size="1.2em" /> Copy to Clipboard <Copy class="mr-2" size="1.2em" /> Copy to Clipboard
</button> </button>
<button <button
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
onclick={handle_download} onclick={handle_download}>
>
<Download class="mr-2" size="1.2em" /> Download File <Download class="mr-2" size="1.2em" /> Download File
</button> </button>
</div> </div>

View File

@@ -1,60 +1,63 @@
<script lang="ts"> <script lang="ts">
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { Check, CircleAlert, FileText, RefreshCw, Upload, X } from '@lucide/svelte'; import {
import { Check,
CircleAlert,
FileText,
RefreshCw,
Upload,
X
} from '@lucide/svelte';
import {
PARSERS, PARSERS,
type AeJournalEntryInput type AeJournalEntryInput
} from '$lib/ae_journals/ae_journals_parsers'; } from '$lib/ae_journals/ae_journals_parsers';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores'; import { ae_api } from '$lib/stores/ae_stores';
import { journals_slct } from '$lib/ae_journals/ae_journals_stores'; import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
interface Props { interface Props {
open: boolean; open: boolean;
on_close: () => void; on_close: () => void;
on_import_complete: () => void; on_import_complete: () => void;
} }
let { let { open = $bindable(false), on_close, on_import_complete }: Props = $props();
open = $bindable(false),
on_close,
on_import_complete
}: Props = $props();
let files: FileList | null = $state(null); let files: FileList | null = $state(null);
let selected_parser: keyof typeof PARSERS = $state('standard'); let selected_parser: keyof typeof PARSERS = $state('standard');
let parsed_entries: AeJournalEntryInput[] = $state([]); let parsed_entries: AeJournalEntryInput[] = $state([]);
let is_parsing = $state(false); let is_parsing = $state(false);
let is_importing = $state(false); let is_importing = $state(false);
let import_log: string[] = $state([]); let import_log: string[] = $state([]);
let is_dragging = $state(false); let is_dragging = $state(false);
// Watch for file selection or parser change to trigger parsing // Watch for file selection or parser change to trigger parsing
$effect(() => { $effect(() => {
if (files && files.length > 0) { if (files && files.length > 0) {
parse_files(); parse_files();
} }
}); });
function handle_drag_enter(e: DragEvent) { function handle_drag_enter(e: DragEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
is_dragging = true; is_dragging = true;
} }
function handle_drag_leave(e: DragEvent) { function handle_drag_leave(e: DragEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
is_dragging = false; is_dragging = false;
} }
function handle_drag_over(e: DragEvent) { function handle_drag_over(e: DragEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
is_dragging = true; is_dragging = true;
} }
function handle_drop(e: DragEvent) { function handle_drop(e: DragEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
is_dragging = false; is_dragging = false;
@@ -62,9 +65,9 @@
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
files = e.dataTransfer.files; files = e.dataTransfer.files;
} }
} }
async function parse_files() { async function parse_files() {
if (!files) return; if (!files) return;
is_parsing = true; is_parsing = true;
parsed_entries = []; parsed_entries = [];
@@ -82,9 +85,9 @@
} }
} }
is_parsing = false; is_parsing = false;
} }
async function handle_import() { async function handle_import() {
if (parsed_entries.length === 0) return; if (parsed_entries.length === 0) return;
is_importing = true; is_importing = true;
import_log = []; import_log = [];
@@ -136,7 +139,7 @@
); );
on_import_complete(); on_import_complete();
open = false; open = false;
} }
</script> </script>
<Modal <Modal
@@ -144,21 +147,18 @@
bind:open bind:open
autoclose={false} autoclose={false}
size="xl" size="xl"
class="w-full" class="w-full">
>
<div class="space-y-4"> <div class="space-y-4">
<!-- Configuration --> <!-- Configuration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div> <div>
<label class="label"> <label class="label">
<span>Parser Strategy</span> <span>Parser Strategy</span>
<select class="select" bind:value={selected_parser}> <select class="select" bind:value={selected_parser}>
<option value="standard" <option value="standard"
>Standard (1 File = 1 Entry)</option >Standard (1 File = 1 Entry)</option>
>
<option value="personal_log" <option value="personal_log"
>Personal Log (Split by Date)</option >Personal Log (Split by Date)</option>
>
<option value="amazon_vine">Amazon Vine Reviews</option> <option value="amazon_vine">Amazon Vine Reviews</option>
</select> </select>
</label> </label>
@@ -173,25 +173,23 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class=" class="
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all duration-200 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2
flex flex-col items-center justify-center gap-2 border-dashed p-8 text-center transition-all duration-200
{is_dragging {is_dragging
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'} : 'border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500'}
" "
ondragenter={handle_drag_enter} ondragenter={handle_drag_enter}
ondragleave={handle_drag_leave} ondragleave={handle_drag_leave}
ondragover={handle_drag_over} ondragover={handle_drag_over}
ondrop={handle_drop} ondrop={handle_drop}
onclick={() => onclick={() =>
document.getElementById('file_import_input')?.click()} document.getElementById('file_import_input')?.click()}>
>
<Upload class="h-10 w-10 text-gray-400" /> <Upload class="h-10 w-10 text-gray-400" />
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<span <span
class="font-semibold text-primary-600 hover:text-primary-500" class="text-primary-600 hover:text-primary-500 font-semibold"
>Click to upload</span >Click to upload</span>
>
or drag and drop or drag and drop
</p> </p>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
@@ -204,17 +202,15 @@
class="hidden" class="hidden"
multiple multiple
accept=".md,.txt" accept=".md,.txt"
onchange={(e) => (files = e.currentTarget.files)} onchange={(e) => (files = e.currentTarget.files)} />
/>
</div> </div>
</div> </div>
</div> </div>
<!-- Preview --> <!-- Preview -->
<div <div
class="border rounded-lg p-2 bg-gray-50 dark:bg-gray-900 max-h-64 overflow-y-auto" class="max-h-64 overflow-y-auto rounded-lg border bg-gray-50 p-2 dark:bg-gray-900">
> <h4 class="mb-2 flex justify-between font-bold">
<h4 class="font-bold mb-2 flex justify-between">
<span>Preview ({parsed_entries.length} entries)</span> <span>Preview ({parsed_entries.length} entries)</span>
{#if is_parsing} {#if is_parsing}
<RefreshCw class="animate-spin" /> <RefreshCw class="animate-spin" />
@@ -222,7 +218,7 @@
</h4> </h4>
{#if parsed_entries.length > 0} {#if parsed_entries.length > 0}
<table class="table table-compact w-full text-xs"> <table class="table-compact table w-full text-xs">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -234,9 +230,8 @@
{#each parsed_entries as entry, i (i)} {#each parsed_entries as entry, i (i)}
<tr> <tr>
<td <td
class="truncate max-w-[200px]" class="max-w-[200px] truncate"
title={entry.name}>{entry.name}</td title={entry.name}>{entry.name}</td>
>
<td>{entry.created_on?.substring(0, 10)}</td> <td>{entry.created_on?.substring(0, 10)}</td>
<td>{entry.tags.join(', ')}</td> <td>{entry.tags.join(', ')}</td>
</tr> </tr>
@@ -244,11 +239,11 @@
</tbody> </tbody>
</table> </table>
{:else if files && files.length > 0 && !is_parsing} {:else if files && files.length > 0 && !is_parsing}
<div class="text-center text-gray-500 py-4"> <div class="py-4 text-center text-gray-500">
No entries found in selected files. No entries found in selected files.
</div> </div>
{:else if !files} {:else if !files}
<div class="text-center text-gray-500 py-4"> <div class="py-4 text-center text-gray-500">
Select files to preview import. Select files to preview import.
</div> </div>
{/if} {/if}
@@ -257,8 +252,7 @@
<!-- Import Log --> <!-- Import Log -->
{#if import_log.length > 0} {#if import_log.length > 0}
<div <div
class="bg-black text-green-400 p-2 rounded text-xs font-mono max-h-32 overflow-y-auto" class="max-h-32 overflow-y-auto rounded bg-black p-2 font-mono text-xs text-green-400">
>
{#each import_log as log, i (i)} {#each import_log as log, i (i)}
<div>{log}</div> <div>{log}</div>
{/each} {/each}
@@ -269,14 +263,12 @@
<button <button
type="button" type="button"
class="btn preset-tonal-secondary" class="btn preset-tonal-secondary"
onclick={on_close}>Cancel</button onclick={on_close}>Cancel</button>
>
<button <button
type="button" type="button"
class="btn preset-filled-primary" class="btn preset-filled-primary"
disabled={parsed_entries.length === 0 || is_importing} disabled={parsed_entries.length === 0 || is_importing}
onclick={handle_import} onclick={handle_import}>
>
{#if is_importing} {#if is_importing}
Importing... Importing...
{:else} {:else}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
// *** Import Svelte specific // *** Import Svelte specific
import { import {
ArrowDown01, ArrowDown01,
ArrowDown10, ArrowDown10,
ArrowDownUp, ArrowDownUp,
@@ -44,12 +44,12 @@
Trash2, Trash2,
TypeOutline, TypeOutline,
X X
} from '@lucide/svelte'; } from '@lucide/svelte';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils'; import { ae_util } from '$lib/ae_utils/ae_utils';
import { import {
ae_snip, ae_snip,
ae_loc, ae_loc,
ae_sess, ae_sess,
@@ -57,12 +57,12 @@
ae_trig, ae_trig,
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions'; import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { journals_slct } from '$lib/ae_journals/ae_journals_stores'; import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
interface Props { interface Props {
log_lvl?: number; log_lvl?: number;
obj_priority: any; obj_priority: any;
obj_sort: any; obj_sort: any;
@@ -76,9 +76,9 @@
lq__journal_entry_obj: any; lq__journal_entry_obj: any;
change_journal_id: any; change_journal_id: any;
lq__journal_obj_li: any; lq__journal_obj_li: any;
} }
let { let {
log_lvl = 0, log_lvl = 0,
obj_priority, obj_priority,
obj_sort, obj_sort,
@@ -92,14 +92,13 @@
lq__journal_entry_obj, lq__journal_entry_obj,
change_journal_id, change_journal_id,
lq__journal_obj_li lq__journal_obj_li
}: Props = $props(); }: Props = $props();
let ae_promises: key_val = $state({}); let ae_promises: key_val = $state({});
</script> </script>
<section <section
class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-between w-full" class="ae_meta flex w-full flex-row flex-wrap items-center justify-between gap-1">
>
<!-- {lq__journal_entry_obj?.priority} <!-- {lq__journal_entry_obj?.priority}
{lq__journal_entry_obj?.sort} {lq__journal_entry_obj?.sort}
{lq__journal_entry_obj?.group} {lq__journal_entry_obj?.group}
@@ -113,9 +112,8 @@
obj_priority = !obj_priority; obj_priority = !obj_priority;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm md:btn-icon-base preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm md:btn-icon-base preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Toggle priority of this journal entry" title="Toggle priority of this journal entry">
>
{#if obj_priority} {#if obj_priority}
<Flag strokeWidth="2.5" color="green" class="inline-block" /> <Flag strokeWidth="2.5" color="green" class="inline-block" />
{:else} {:else}
@@ -127,17 +125,15 @@
<!-- Set sort order (number) --> <!-- Set sort order (number) -->
<span <span
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="flex flex-row flex-wrap items-center justify-center border border-gray-300 rounded-lg" class="flex flex-row flex-wrap items-center justify-center rounded-lg border border-gray-300">
>
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
obj_sort = obj_sort ? obj_sort + 1 : 1; obj_sort = obj_sort ? obj_sort + 1 : 1;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Increment sort order of this journal entry" title="Increment sort order of this journal entry">
>
<Plus strokeWidth="2.5" color="blue" /> <Plus strokeWidth="2.5" color="blue" />
</button> </button>
<span class="mx-1"> <span class="mx-1">
@@ -154,9 +150,8 @@
obj_sort = obj_sort ? obj_sort - 1 : 0; obj_sort = obj_sort ? obj_sort - 1 : 0;
// update_journal_entry(); // update_journal_entry();
}} }}
class="btn-icon btn-icon-sm preset-tonal-tertiary transition hover:preset-filled-tertiary-500" class="btn-icon btn-icon-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 transition"
title="Decrement sort order of this journal entry" title="Decrement sort order of this journal entry">
>
<Minus strokeWidth="2.5" color="blue" /> <Minus strokeWidth="2.5" color="blue" />
</button> </button>
</span> </span>
@@ -171,13 +166,11 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="input input-sm input-bordered w-24" class="input input-sm input-bordered w-24"
title="Set group (for sorting) of this journal entry" title="Set group (for sorting) of this journal entry" />
/>
<!-- Set archive datetime (string) --> <!-- Set archive datetime (string) -->
<span <span
class="flex flex-row flex-wrap items-center justify-center border border-gray-200 rounded-lg" class="flex flex-row flex-wrap items-center justify-center rounded-lg border border-gray-200">
>
<input <input
type="datetime-local" type="datetime-local"
bind:value={obj_archive_on} bind:value={obj_archive_on}
@@ -187,8 +180,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="input input-sm input-bordered w-auto border-none" class="input input-sm input-bordered w-auto border-none"
title="Set archive on datetime for archiving this journal entry" title="Set archive on datetime for archiving this journal entry" />
/>
{#if obj_archive_on} {#if obj_archive_on}
<!-- Button to clear the datetime --> <!-- Button to clear the datetime -->
@@ -200,8 +192,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Clear the archive on datetime for this journal entry" title="Clear the archive on datetime for this journal entry">
>
<X strokeWidth="2.5" color="red" /> <X strokeWidth="2.5" color="red" />
<!-- <span class="hidden">Clear Archive</span> --> <!-- <span class="hidden">Clear Archive</span> -->
</button> </button>
@@ -221,8 +212,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn-icon btn-icon-sm preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Set the archive on datetime for this journal entry" title="Set the archive on datetime for this journal entry">
>
<Clock strokeWidth="2.5" color="blue" /> <Clock strokeWidth="2.5" color="blue" />
<!-- <span class="hidden">Set Archive</span> --> <!-- <span class="hidden">Set Archive</span> -->
</button> </button>
@@ -237,8 +227,7 @@
update_journal_entry(); update_journal_entry();
}} }}
class="btn btn-sm md:btn-md preset-tonal-warning hover:preset-filled-warning-500 transition" class="btn btn-sm md:btn-md preset-tonal-warning hover:preset-filled-warning-500 transition"
title="Toggle visibility of this journal entry" title="Toggle visibility of this journal entry">
>
{#if obj_hide} {#if obj_hide}
<EyeOff strokeWidth="1" color="red" class="inline-block" /> <EyeOff strokeWidth="1" color="red" class="inline-block" />
<span class="hidden md:inline">Hidden</span> <span class="hidden md:inline">Hidden</span>
@@ -257,8 +246,7 @@
}} }}
class:hidden={!$ae_loc.administrator_access || !$ae_loc.edit_mode} class:hidden={!$ae_loc.administrator_access || !$ae_loc.edit_mode}
class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition" class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition"
title="Toggle enable status of this journal entry" title="Toggle enable status of this journal entry">
>
{#if obj_enable} {#if obj_enable}
<ShieldCheck strokeWidth="2.5" color="green" class="inline-block" /> <ShieldCheck strokeWidth="2.5" color="green" class="inline-block" />
<span class="hidden md:inline">Enabled</span> <span class="hidden md:inline">Enabled</span>
@@ -304,8 +292,7 @@
}} }}
class:hidden={!$ae_loc.edit_mode} class:hidden={!$ae_loc.edit_mode}
class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition" class="btn btn-sm md:btn-md preset-tonal-error hover:preset-filled-error-500 transition"
title="Delete this journal entry" title="Delete this journal entry">
>
{#if $ae_loc.administrator_access && $ae_loc.edit_mode} {#if $ae_loc.administrator_access && $ae_loc.edit_mode}
<FileX strokeWidth="2.5" color="red" class="inline-block" /> <FileX strokeWidth="2.5" color="red" class="inline-block" />
<span class="hidden md:inline">Delete</span> <span class="hidden md:inline">Delete</span>
@@ -316,8 +303,7 @@
</button> </button>
<span <span
class="flex flex-row items-center justify-center text-sm text-gray-500" class="flex flex-row items-center justify-center text-sm text-gray-500">
>
{#if !$ae_loc.edit_mode} {#if !$ae_loc.edit_mode}
<span class=""> <span class="">
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
@@ -344,19 +330,18 @@
<!-- Select option list of Journals to choose from. This is used to assign the Journal Entry to a different Journal ID. --> <!-- Select option list of Journals to choose from. This is used to assign the Journal Entry to a different Journal ID. -->
{#if $ae_loc.edit_mode && lq__journal_obj_li?.length} {#if $ae_loc.edit_mode && lq__journal_obj_li?.length}
<div <div
class="flex flex-row flex-wrap gap-2 items-center justify-start border border-gray-200 rounded-lg" class="flex flex-row flex-wrap items-center justify-start gap-2 rounded-lg border border-gray-200">
>
<SquareLibrary size="1em" class="mx-1" /> <SquareLibrary size="1em" class="mx-1" />
<span class="text-sm text-gray-500 hidden sm:inline"> <span class="hidden text-sm text-gray-500 sm:inline">
Journal: Journal:
</span> </span>
<select <select
class="novi_btn btn btn-secondary btn-sm class="novi_btn btn btn-secondary btn-sm
preset-tonal-primary preset-tonal-primary
hover:preset-filled-primary-500 hover:preset-filled-primary-500
transition
text-xs
border-none border-none
text-xs
transition
" "
bind:value={tmp_entry_obj.journal_id} bind:value={tmp_entry_obj.journal_id}
onchange={(event) => { onchange={(event) => {
@@ -372,14 +357,12 @@
change_journal_id(); change_journal_id();
} }
}} }}
title="Select a different journal for this entry" title="Select a different journal for this entry">
>
<option value="">Select Journal</option> <option value="">Select Journal</option>
{#each lq__journal_obj_li as journal (journal.journal_id)} {#each lq__journal_obj_li as journal (journal.journal_id)}
<option <option
value={journal.journal_id} value={journal.journal_id}
title={`Journal: ${journal.name}`} title={`Journal: ${journal.name}`}>
>
{journal.name} {journal.name}
</option> </option>
{/each} {/each}

View File

@@ -30,33 +30,49 @@ describe('Journal Entry Visibility Filtering', () => {
{ id: '1', name: 'Normal Entry', hide: false, enable: true }, { id: '1', name: 'Normal Entry', hide: false, enable: true },
{ id: '2', name: 'Hidden Entry', hide: true, enable: true }, { id: '2', name: 'Hidden Entry', hide: true, enable: true },
{ id: '3', name: 'Disabled Entry', hide: false, enable: false }, { id: '3', name: 'Disabled Entry', hide: false, enable: false },
{ id: '4', name: 'Hidden & Disabled', hide: true, enable: false }, { id: '4', name: 'Hidden & Disabled', hide: true, enable: false }
]; ];
it('should show only normal entries when Edit Mode is OFF (Manager)', () => { it('should show only normal entries when Edit Mode is OFF (Manager)', () => {
const ae_loc = { edit_mode: false, trusted_access: true, administrator_access: true }; const ae_loc = {
edit_mode: false,
trusted_access: true,
administrator_access: true
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(1); expect(result?.length).toBe(1);
expect(result?.[0].id).toBe('1'); expect(result?.[0].id).toBe('1');
}); });
it('should show hidden entries to Trusted users when Edit Mode is ON', () => { it('should show hidden entries to Trusted users when Edit Mode is ON', () => {
const ae_loc = { edit_mode: true, trusted_access: true, administrator_access: false }; const ae_loc = {
edit_mode: true,
trusted_access: true,
administrator_access: false
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
// Should see Normal (1) and Hidden (2). Should NOT see Disabled (3, 4) // Should see Normal (1) and Hidden (2). Should NOT see Disabled (3, 4)
expect(result?.length).toBe(2); expect(result?.length).toBe(2);
expect(result?.map(r => r.id)).toContain('1'); expect(result?.map((r) => r.id)).toContain('1');
expect(result?.map(r => r.id)).toContain('2'); expect(result?.map((r) => r.id)).toContain('2');
}); });
it('should show everything to Administrators when Edit Mode is ON', () => { it('should show everything to Administrators when Edit Mode is ON', () => {
const ae_loc = { edit_mode: true, trusted_access: true, administrator_access: true }; const ae_loc = {
edit_mode: true,
trusted_access: true,
administrator_access: true
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(4); expect(result?.length).toBe(4);
}); });
it('should hide everything sensitive to Public users even if Edit Mode is ON (Safety Check)', () => { it('should hide everything sensitive to Public users even if Edit Mode is ON (Safety Check)', () => {
const ae_loc = { edit_mode: true, trusted_access: false, administrator_access: false }; const ae_loc = {
edit_mode: true,
trusted_access: false,
administrator_access: false
};
const result = filterEntries(mockEntries, ae_loc); const result = filterEntries(mockEntries, ae_loc);
expect(result?.length).toBe(1); expect(result?.length).toBe(1);
expect(result?.[0].id).toBe('1'); expect(result?.[0].id).toBe('1');