Files
OSIT-AE-App-Svelte/src/routes/journals/ae_comp__journal_entry_obj_li.svelte
Scott Idem cc6f73ca04 style(journals): standardize Skeleton v4 preset-* classes across all journal components
- Replace all Skeleton v2 variant-* classes with v4 preset-* equivalents
  - variant-filled-* → preset-filled-*
  - variant-soft-* / variant-ghost-* → preset-tonal-*
  - variant-outline-* → preset-outlined-*
  - variant-form-material removed from inputs/selects/textareas
  - input-bordered removed

- Fix dark mode: journal entry content hover (dark:hover:bg-blue-950)
- Fix dark mode: journal obj view section/description bg and text colors
- Fix modal headers: add dismissable=false + explicit X close button (all 3 journals modals)
- Fix DaisyUI wrappers removed from modal_journal_entry_append
- app.css: add global select padding-inline to fix text-against-border issue
2026-03-06 19:15:51 -05:00

764 lines
37 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
log_lvl?: number;
lq__journal_obj: any;
lq__journal_entry_obj_li: any;
show_found_header?: boolean;
}
let {
log_lvl = $bindable(0),
lq__journal_obj,
lq__journal_entry_obj_li,
show_found_header = true
}: Props = $props();
// *** Import Svelte specific
import { goto } from '$app/navigation';
import {
CalendarClock,
Check,
CodeXml,
Copy,
Eye,
EyeOff,
Files,
Fingerprint,
Flag,
Group,
ListPlus,
Lock,
NotebookPen,
NotebookText,
NotepadTextDashed,
RemoveFormatting,
Shapes,
Siren,
Tags,
TypeOutline,
X,
LoaderCircle,
BookOpenText
} 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_slct,
journals_trig,
journals_loc
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
let tmp_entry_obj: key_val = $state({});
// 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
let show_append_modal = $state(false);
$effect(() => {
// Sync local boolean with store ID presence
show_append_modal =
!!$journals_sess.show__modal_append__journal_entry_id;
});
function handle_modal_close() {
$journals_sess.show__modal_append__journal_entry_id = null;
show_append_modal = false;
}
function handle_modal_update() {
handle_modal_close();
}
// Derived list of visible items (Standardized Search Pattern 2026-01-27)
// Ensures count matches exactly what is rendered to the user
let visible_journal_entry_obj_li = $derived(
(() => {
// Subscribe to the observable
const list = $lq__journal_entry_obj_li;
// Return null to signify 'loading' vs [] for 'empty'
if (list === undefined || list === null) return null;
if (!Array.isArray(list)) return [];
const filtered = list.filter((item: any) => {
if (!item) return false;
const is_hidden = item.hide === true || item.hide === 1;
const is_disabled = item.enable === false || item.enable === 0;
// Standard Visibility: Filter out hidden/disabled if not in Edit Mode
if (!$ae_loc.edit_mode) {
return !is_hidden && !is_disabled;
}
// Edit Mode Gating:
// - To see Hidden: Must have Trusted Access or higher
if (is_hidden && !$ae_loc.trusted_access) return false;
// - To see Disabled: Must have Administrator Access or higher
if (is_disabled && !$ae_loc.administrator_access) return false;
return true;
});
if (log_lvl)
console.log(
`visible_journal_entry_obj_li: Input=${list.length}, Output=${filtered.length}`
);
return filtered;
})()
);
</script>
<section
class="journal_list flex flex-col gap-1 md:gap-2 items-center justify-center w-full"
>
{#if visible_journal_entry_obj_li === null}
<!-- Loading state -->
<div class="flex flex-col items-center justify-center p-10 opacity-50">
<LoaderCircle size="2em" class="animate-spin mb-2" />
<p>Loading visible entries...</p>
</div>
{:else if visible_journal_entry_obj_li.length > 0}
{#if show_found_header}
<div class="w-full max-w-(--breakpoint-lg) mb-2">
<h2 class="h4 flex items-center gap-2 px-2">
<span class="text-sm text-gray-500 font-normal">
Found:
</span>
<span
class="badge preset-tonal-success font-bold text-lg px-3 py-1"
>
{visible_journal_entry_obj_li.length}<span
class="text-gray-400 dark:text-gray-600"
>&times;</span
>
</span>
</h2>
</div>
{/if}
{#each visible_journal_entry_obj_li as journals_journal_entry_obj, index (journals_journal_entry_obj.journal_entry_id)}
<div
class="
container journal journal_entry_obj
border
px-2 py-1 space-y-1
w-full max-w-(--breakpoint-lg)
flex flex-col items-center justify-center
bg-white text-gray-900
dark:bg-gray-800 dark:text-gray-200
rounded-lg
hover:bg-gray-100 hover:dark:bg-gray-700
hover:border-gray-300
transition-all duration-500 ease-out
"
class:dim={!journals_journal_entry_obj.enable}
class:bg-warning-100={!journals_journal_entry_obj?.enable}
>
<header
class="ae_header flex flex-row gap-2 items-center justify-between w-full"
>
<span class="flex flex-row flex-wrap gap-1">
<span class="journal_entry__name *:hover:inline-block">
{#if journals_journal_entry_obj.alert}
<Siren
size="1.25em"
class="mx-1 inline-block text-red-500"
/>
{/if}
{#if journals_journal_entry_obj.priority}
<Flag
size="1.25em"
class="mx-1 inline-block text-yellow-500"
/>
{/if}
{#if journals_journal_entry_obj.group}
<Group
size="1.25em"
class="mx-1 inline-block text-green-500"
/>
<span class="text-xs text-gray-500 hidden"
>Group:</span
>
<span
class="font-semibold text-sm text-gray-500 hidden md:inline"
>
{journals_journal_entry_obj.group}
</span>
{/if}
</span>
<h3
class:dim={journals_journal_entry_obj.hide}
class="journal__name h4"
>
{#if journals_journal_entry_obj.template}
<NotepadTextDashed
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{@html journals_journal_entry_obj.name ??
'-- no name --'}
{:else if journals_journal_entry_obj.name}
<NotebookText
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{@html journals_journal_entry_obj.name}
{:else}
<CalendarClock
class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-50/60"
/>
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.created_on,
'datetime_iso_12_no_seconds'
)}
{/if}
</h3>
<span
class="flex flex-row flex-wrap gap-1 items-center justify-center"
>
{#if !journals_journal_entry_obj.private}
<!-- Button to copy the Markdown version -->
<button
type="button"
onclick={() => {
let tmp_entry_obj =
journals_journal_entry_obj;
navigator.clipboard
.writeText(tmp_entry_obj.content)
.then(() => {
alert(
'Markdown content copied to clipboard!'
);
})
.catch((error) => {
console.error(
'Failed to copy content:',
error
);
alert(
'Failed to copy content.'
);
});
}}
class:hidden={$lq__journal_obj?.cfg_json
?.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"
title="Copy the markdown content"
>
<RemoveFormatting size="1.25em" />
<span class="hidden">
Copy Plaintext Markdown
</span>
</button>
<!-- Button to copy the rendered to HTML version -->
<button
type="button"
onclick={() => {
let htmlContent =
journals_journal_entry_obj.content_md_html ||
'';
navigator.clipboard
.writeText(htmlContent)
.then(() => {
alert(
'Rendered HTML content copied to clipboard!'
);
})
.catch((error) => {
console.error(
'Failed to copy HTML content:',
error
);
alert(
'Failed to copy HTML content.'
);
});
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json
?.hide_copy_html}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Copy the rendered HTML content"
>
<CodeXml size="1.25em" />
<span class="hidden">
Copy HTML Markup
</span>
</button>
<!-- Button to copy the rich text (rendered HTML) version -->
<button
type="button"
onclick={async () => {
const element = document.getElementById(
`rendered_journal_entry_content_${journals_journal_entry_obj.journal_entry_id}`
);
if (!element) {
console.error(
'Element not found: rendered_journal_entry_content'
);
alert(
'Failed to copy rich content.'
);
return;
}
try {
const htmlContent =
element.innerHTML;
await navigator.clipboard.write([
new ClipboardItem({
'text/html': new Blob(
[htmlContent],
{
type: 'text/html'
}
)
})
]);
alert(
'Rendered rich content copied to clipboard!'
);
} catch (error) {
console.error(
'Failed to copy rich content:',
error
);
alert(
'Failed to copy rich content.'
);
}
}}
class:hidden={journals_journal_entry_obj.template ||
$lq__journal_obj?.cfg_json
?.hide_copy_rich}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline lg:text-xs"
title="Copy the rich text (rendered HTML) content"
>
<TypeOutline size="1.25em" />
<span class="hidden">Copy Rich Text</span>
</button>
<!-- Clone entry -->
<button
type="button"
onclick={() => {
let data_kv = {
code: journals_journal_entry_obj.code,
category_code:
journals_journal_entry_obj.category_code,
name: journals_journal_entry_obj.name,
short_name:
journals_journal_entry_obj.short_name,
content:
journals_journal_entry_obj.content,
description:
journals_journal_entry_obj.description,
tags: journals_journal_entry_obj.tags
};
journals_func
.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id:
journals_journal_entry_obj.journal_id,
data_kv: data_kv,
log_lvl: log_lvl
})
.then((result) => {
if (
result?.journal_id &&
result?.journal_entry_id
) {
alert(
'Journal entry cloned successfully!'
);
goto(
`/journals/${result.journal_id}/entry/${result.journal_entry_id}`
);
}
})
.catch((error) => {
console.error(
'Error cloning journal entry:',
error
);
alert(
'Failed to clone journal entry.'
);
});
}}
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"
title="Clone this journal entry"
>
<Copy size="1.25em" />
<span class="hidden md:inline">Clone</span>
</button>
{:else}
<Lock
size="1.25em"
class="mx-1 inline-block text-red-400 dark:text-red-600"
/>
<span class="text-xs text-gray-500 hidden"
>Private</span
>
<!-- Button to copy the Markdown version -->
<button
type="button"
onclick={() => {
let tmp_entry_obj =
journals_journal_entry_obj;
navigator.clipboard
.writeText(
tmp_entry_obj.content_encrypted
)
.then(() => {
alert(
'Encrypted content copied to clipboard!'
);
})
.catch((error) => {
console.error(
'Failed to copy content:',
error
);
alert(
'Failed to copy content.'
);
});
}}
class:hidden={$lq__journal_obj?.cfg_json
?.hide_copy_encrypted}
class="btn btn-sm p-1 preset-tonal-surface hover:preset-filled-secondary-500 *:hover:inline text-xs lg:text-sm"
title="Copy the encrypted content"
>
<Fingerprint size="1.25em" />
<span class="hidden"> Copy Encrypted </span>
</button>
{/if}
</span>
</span>
<div
class="flex flex-row flex-wrap gap-2 items-center justify-end"
>
<!-- Linked file count -->
<div
class="ae_linked_file_count flex flex-row flex-wrap gap-0.5 items-center justify-start"
class:hidden={!journals_journal_entry_obj?.data_json
?.hosted_file_kv}
>
<Files class="mx-1 inline-block" />
<span class="text-xs text-gray-500 hidden md:inline"
>Linked files:</span
>
<span class="font-semibold text-sm text-gray-500">
{journals_journal_entry_obj?.data_json
?.hosted_file_kv
? Object.keys(
journals_journal_entry_obj?.data_json
?.hosted_file_kv
).length
: 0}×
</span>
</div>
<!-- Tags for journal entry. Comma delimited list. -->
{#if journals_journal_entry_obj.tags && journals_journal_entry_obj.tags.length}
<div
class="tags flex flex-row flex-wrap gap-0.5 items-center justify-start p-1"
>
<Tags class="mx-1 inline-block" />
<span
class="text-xs text-gray-500 hidden md:inline"
>Tags:</span
>
{#each journals_journal_entry_obj.tags.split(',') as tag (tag)}
<span
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-tertiary border border-tertiary-500 transition py-1 px-2"
title={`Tag: ${tag.trim()}`}
>
{tag.trim()}
</span>
{/each}
</div>
{/if}
<!-- Category code for journal entry -->
{#if journals_journal_entry_obj.category_code}
<button
type="button"
onclick={() => {
if (
$journals_loc.entry
.qry__category_code ==
journals_journal_entry_obj.category_code
) {
$journals_loc.entry.qry__category_code =
null;
} else {
$journals_loc.entry.qry__category_code =
journals_journal_entry_obj.category_code;
}
if (
$journals_loc.entry.search_version ===
undefined
)
$journals_loc.entry.search_version = 0;
$journals_loc.entry.search_version++;
}}
class:bg-green-100={$journals_loc.entry
.qry__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"
title={`Filter by category: ${journals_journal_entry_obj.category_code}`}
>
<Shapes class="mx-1 inline-block" />
{journals_journal_entry_obj.category_code ??
'-- no category --'}
</button>
{/if}
<a
href="/journals/{journals_journal_entry_obj?.journal_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"
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')}
Journal ID: ${journals_journal_entry_obj?.journal_id}
`}
>
<NotebookPen class="mx-1 inline-block" />
<span class="hidden md:inline"> View </span>
</a>
<!-- Button to show a modal that will allow for a quick append to Journal Entry option. -->
<button
type="button"
onclick={() => {
$journals_sess.show__modal_append__journal_entry_id =
journals_journal_entry_obj?.id;
tmp_entry_obj = JSON.parse(
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"
title={$lq__journal_obj?.cfg_json?.entry_add_text ==
'append'
? 'Append to Journal Entry'
: 'Prepend to Journal Entry'}
>
<ListPlus />
</button>
</div>
</header>
{#if journals_journal_entry_obj.content}
<div
class:hidden={journals_journal_entry_obj.hide ||
(journals_journal_entry_obj.private &&
$journals_slct.journal_obj?.cfg_json
.hide_private) ||
(journals_journal_entry_obj.personal &&
$journals_slct.journal_obj?.cfg_json
.hide_personal) ||
(journals_journal_entry_obj.professional &&
$journals_slct.journal_obj?.cfg_json
.hide_professional)}
class="journal__content
w-full p-1
bg-slate-100 text-gray-900
dark:bg-slate-900 dark:text-gray-100
shadow-lg rounded-lg
border border-gray-200 dark:border-gray-700
text-wrap text-sm font-mono whitespace-pre-wrap
transition-all
delay-1000 hover:delay-1000 active:delay-100
duration-1000 hover:duration-200 active:duration-200
ease-in-out
active:z-10
hover:bg-blue-100 dark:hover:bg-blue-950 hover:border-blue-500 dark:hover:border-blue-500
overflow-scroll
{$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_click_max_height
? `${$journals_slct.journal_obj.cfg_json.entry_li_click_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}
</div>
<article
class="prose hidden"
id="rendered_journal_entry_content_{journals_journal_entry_obj.journal_entry_id}"
>
{@html journals_journal_entry_obj?.content_md_html}
</article>
{/if}
<section
class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone}
class="ae_section journal_entry__entry"
>
<div
class="ae_group"
class:hidden={!journals_journal_entry_obj?.original_datetime &&
!journals_journal_entry_obj?.original_timezone}
>
<span class="ae_label text-sm">Original date/time:</span
>
{#if journals_journal_entry_obj.original_datetime}
<span
class="ae_value ae_prop prop_original_datetime font-semibold"
>{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.original_datetime,
'datetime_12_long'
)}</span
>
{/if}
{#if journals_journal_entry_obj.original_timezone}
<span class="ae_label text-sm">Timezone:</span>
<span class="ae_value font-semibold"
>{journals_journal_entry_obj.original_timezone}</span
>
{/if}
</div>
</section>
<section
class="ae_meta mt-2 flex flex-col sm:flex-row gap-2 items-center justify-center text-xs text-gray-500"
>
<span
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}
class="flex flex-row gap-1 items-center justify-center"
>
<span class="journal_entry__created_on">
Created:
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.created_on,
'datetime_12_long'
)}
</span>
<span
class:hidden={!journals_journal_entry_obj.updated_on}
class="journal_entry__updated_on"
>
Last update:
{ae_util.iso_datetime_formatter(
journals_journal_entry_obj.updated_on,
'datetime_12_long'
)}
</span>
</span>
<!-- Set/unset hide (boolean) -->
<button
type="button"
onclick={() => {
let data_kv = {
hide: journals_journal_entry_obj?.hide
? false
: true
};
journals_func
.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id:
journals_journal_entry_obj.journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
})
.then(() => {})
.catch((error) => {
console.error(
'Error updating journal entry:',
error
);
alert('Failed to update journal entry.');
});
}}
class:hidden={!$ae_loc.edit_mode}
class="btn btn-sm preset-tonal-surface hover:preset-filled-warning-500 transition py-1 px-2"
title={`Set entry as ${journals_journal_entry_obj.hide ? 'visible' : 'hidden'}`}
>
{#if journals_journal_entry_obj.hide}
<EyeOff
strokeWidth="1"
color="hsla( 0, 100%, 50%, .5)"
class="inline-block"
/>
<span class="hidden md:inline">Hidden</span>
{:else}
<Eye
strokeWidth="2.5"
color="hsla( 120, 100%, 25%, .5)"
class="inline-block"
/>
<span class="hidden lg:inline">Visible</span>
{/if}
</button>
</section>
</div>
{/each}
<!-- Modal for quick append to Journal Entry -->
{#if $journals_sess.show__modal_append__journal_entry_id}
<AeCompModalJournalEntryAppend
bind:open={show_append_modal}
journal_entry={tmp_entry_obj}
journal_config={$lq__journal_obj?.cfg_json}
on_close={handle_modal_close}
on_update={handle_modal_update}
{log_lvl}
/>
{/if}
{:else}
<div
class="flex flex-col items-center justify-center p-10 opacity-50 text-center"
>
<BookOpenText size="3em" class="mb-2 opacity-20 mx-auto" />
<p>
No Journal Entry available to show. Please check the query
filters or create a new Entry.
</p>
</div>
{/if}
</section>