Implement generic AE_ObjectFlags and AE_MetadataFooter components

- Created AE_ObjectFlags.svelte for standardized flag management across all modules.
- Created AE_MetadataFooter.svelte for unified creation/update/original timestamp display.
- Modularized Journal Entry view by replacing manual flag and footer logic with generic elements.
- Improved code reusability and reduced main component complexity.
This commit is contained in:
Scott Idem
2026-01-08 18:08:47 -05:00
parent efe8677ab6
commit c884eed52c
3 changed files with 210 additions and 192 deletions

View File

@@ -0,0 +1,65 @@
<script lang="ts">
/**
* AE_MetadataFooter.svelte
* GENERIC Aether Metadata Display
* Reusable across all modules to standardize created/updated/original info.
*/
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc } from '$lib/stores/ae_stores';
import { Clock, CalendarClock, Globe } from '@lucide/svelte';
interface Props {
obj: any;
showOriginal?: boolean;
containerClass?: string;
}
let {
obj,
showOriginal = true,
containerClass = "ae_meta flex flex-col gap-2 p-4 border-t border-surface-500/10 text-xs text-surface-500 w-full"
}: Props = $props();
</script>
<footer class={containerClass}>
<!-- Original Date/Time Info (if applicable) -->
{#if showOriginal && (obj?.original_datetime || obj?.original_timezone)}
<div class="flex flex-row flex-wrap gap-x-4 gap-y-1 items-center bg-surface-500/5 p-2 rounded">
<span class="flex items-center gap-1">
<Globe size="1.1em" class="opacity-70" />
<span class="font-bold uppercase tracking-tighter">Original:</span>
{obj?.original_datetime ? ae_util.iso_datetime_formatter(obj.original_datetime, 'datetime_iso_12_no_seconds') : '--'}
</span>
{#if obj?.original_timezone}
<span class="flex items-center gap-1">
<span class="font-bold uppercase tracking-tighter">TZ:</span>
{obj.original_timezone}
</span>
{/if}
</div>
{/if}
<!-- System Timestamps -->
<div class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1">
<div class="flex flex-wrap gap-4 justify-center sm:justify-start">
<span class="flex items-center gap-1" title="Creation date">
<CalendarClock size="1.1em" class="opacity-70 text-primary-500" />
<span class="font-semibold uppercase tracking-tighter">Created:</span>
{ae_util.iso_datetime_formatter(obj?.created_on, 'datetime_iso_12_no_seconds')}
</span>
{#if obj?.updated_on}
<span class="flex items-center gap-1" title="Last update">
<Clock size="1.1em" class="opacity-70 text-secondary-500" />
<span class="font-semibold uppercase tracking-tighter">Updated:</span>
{ae_util.iso_datetime_formatter(obj.updated_on, 'datetime_iso_12_no_seconds')}
</span>
{/if}
</div>
{#if obj?.journal_entry_type || obj?.type}
<span class="badge variant-soft-surface text-[10px] uppercase font-bold tracking-widest">
Type: {obj?.journal_entry_type || obj?.type}
</span>
{/if}
</div>
</footer>

View File

@@ -0,0 +1,131 @@
<script lang="ts">
/**
* AE_ObjectFlags.svelte
* GENERIC Aether Object Flags & Visibility Toggles
* Manages: alert, private, public, personal, professional, template
*/
import {
Siren, MessageSquareWarning, Fingerprint,
Globe, BookHeart, BriefcaseBusiness, NotepadTextDashed,
Settings
} from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
interface Props {
// The object containing the flags (bindable)
obj: any;
// Visibility configuration (optional overrides)
showLabels?: boolean;
hideAlert?: boolean;
hidePrivate?: boolean;
hidePublic?: boolean;
hidePersonal?: boolean;
hideProfessional?: boolean;
hideTemplate?: boolean;
// Callbacks
onToggle?: (prop: string, newValue: boolean) => void;
// Styling
containerClass?: string;
}
let {
obj = $bindable(),
showLabels = true,
hideAlert = false,
hidePrivate = false,
hidePublic = false,
hidePersonal = false,
hideProfessional = false,
hideTemplate = false,
onToggle,
containerClass = "flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10"
}: Props = $props();
function handle_toggle(prop: string) {
obj[prop] = !obj[prop];
if (onToggle) onToggle(prop, obj[prop]);
}
</script>
<div class={containerClass}>
{#if showLabels}
<span class="text-xs text-surface-500 flex items-center gap-1 uppercase font-bold tracking-wider mr-2">
<Settings size="1.1em" /> Flags:
</span>
{/if}
<!-- Alert Status -->
{#if !hideAlert}
<button
type="button"
onclick={() => handle_toggle('alert')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Alert Status"
>
<Siren size="1.2em" class={obj?.alert ? 'text-error-500' : 'opacity-40'} />
</button>
{/if}
<!-- Private / E2EE -->
{#if !hidePrivate}
<button
type="button"
onclick={() => handle_toggle('private')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Private/Encrypted"
>
<Fingerprint size="1.2em" class={obj?.private ? 'text-success-500' : 'opacity-40'} />
</button>
{/if}
<!-- Public Visibility -->
{#if !hidePublic}
<button
type="button"
onclick={() => handle_toggle('public')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Public Visibility"
>
<Globe size="1.2em" class={obj?.public ? 'text-success-500' : 'opacity-40'} />
</button>
{/if}
<!-- Personal Scope -->
{#if !hidePersonal}
<button
type="button"
onclick={() => handle_toggle('personal')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Personal Scope"
>
<BookHeart size="1.2em" class={obj?.personal ? 'text-success-500' : 'opacity-40'} />
</button>
{/if}
<!-- Professional Scope -->
{#if !hideProfessional}
<button
type="button"
onclick={() => handle_toggle('professional')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Professional Scope"
>
<BriefcaseBusiness size="1.2em" class={obj?.professional ? 'text-success-500' : 'opacity-40'} />
</button>
{/if}
<!-- Template Status -->
{#if !hideTemplate && $ae_loc.edit_mode}
<button
type="button"
onclick={() => handle_toggle('template')}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle Template Mode"
>
<NotepadTextDashed size="1.2em" class={obj?.template ? 'text-success-500' : 'opacity-40'} />
</button>
{/if}
</div>

View File

@@ -91,6 +91,9 @@
import Comp_journal_entry_file_li from './ae_comp__journal_entry_obj_file_li.svelte';
import Comp_hosted_files_download_button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
import AE_AITools from '$lib/ae_elements/AE_AITools.svelte';
import AE_ObjectFlags from '$lib/ae_elements/AE_ObjectFlags.svelte';
import AE_MetadataFooter from '$lib/ae_elements/AE_MetadataFooter.svelte';
import JournalEntry_Metadata from './JournalEntry_Metadata.svelte';
// *** Configuration
let llm_api_token =
@@ -1020,173 +1023,16 @@
<!-- </div> -->
<!-- Journal Entry Options: alert, private, public, personal, professional, template -->
<div
class="
flex flex-row flex-wrap gap-0.5 items-center justify-evenly
"
>
<span class="text-sm text-gray-500">
<Settings strokeWidth="1" class="mx-1 inline-block" />
Flags:
</span>
<!-- Entry alert status -->
<!-- class:hidden={$lq__journal_obj?.cfg_json?.hide_btn_alert} -->
<button
type="button"
onclick={() => {
tmp_entry_obj.alert = !$lq__journal_entry_obj?.alert;
update_journal_entry();
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle alert status of this journal entry"
>
{#if $lq__journal_entry_obj?.alert}
<Siren strokeWidth="2.5" color="red" />
{:else}
<MessageSquareWarning strokeWidth="1" color="gray" />
{/if}
</button>
<!-- Entry marked as private -->
<!-- ($lq__journal_obj?.cfg_json?.hide_btn_private) || -->
<button
type="button"
class:hidden={tmp_entry_obj?.private && !$ae_loc.edit_mode}
onclick={() => {
// Allow for toggle between private (encrypted) and not,
// and if private, allow for decryption.
if (
!$lq__journal_entry_obj?.private &&
tmp_entry_obj?.content
) {
// Handle converting to private (encrypted) content
if (
confirm(
'Are you sure you want to encrypt and resave this journal entry?'
)
) {
// Setting private to true will cause the update_journal_entry() function to encrypt the content.
tmp_entry_obj.private = true;
update_journal_entry();
} else {
return false;
}
} else if (
$lq__journal_entry_obj?.private &&
tmp_entry_obj?.content_encrypted
) {
// Handle converting to not private (decrypted) content
if (
confirm(
'Are you sure you want to decrypt and resave this journal entry unencrypted?'
)
) {
// tmp_entry_obj.private = !$lq__journal_entry_obj?.private;
// update_journal_entry();
// Setting private to false will cause the update_journal_entry() function to decrypt the content. This will also copy it to tmp_entry_obj.content.
tmp_entry_obj.private = false;
// tmp_entry_obj.
// handle_decrypt_content();
update_journal_entry();
} else {
return false;
}
}
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle private visibility of this journal entry"
>
{#if $lq__journal_entry_obj?.private}
<Fingerprint strokeWidth="2.5" color="green" />
<!-- Private (Encrypted) -->
{:else}
<Fingerprint strokeWidth="1" color="gray" />
<!-- Not Private -->
{/if}
</button>
<!-- Entry allowed to be public -->
<!-- class:hidden={$lq__journal_obj.cfg_json?.hide_btn_public} -->
<button
type="button"
onclick={() => {
if (tmp_entry_obj) {
tmp_entry_obj.public = !$lq__journal_entry_obj?.public;
update_journal_entry();
}
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle public visibility of this journal entry"
>
{#if $lq__journal_entry_obj?.public}
<Globe strokeWidth="2.5" color="green" />
{:else}
<Globe strokeWidth="1" color="gray" />
{/if}
</button>
<!-- Entry marked as personal -->
<!-- class:hidden={$lq__journal_obj.cfg_json?.hide_btn_personal} -->
<button
type="button"
onclick={() => {
if (tmp_entry_obj) {
tmp_entry_obj.personal = !$lq__journal_entry_obj?.personal;
update_journal_entry();
}
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle personal visibility of this journal entry"
>
{#if $lq__journal_entry_obj?.personal}
<BookHeart strokeWidth="2.5" color="green" />
{:else}
<BookHeart strokeWidth="1" color="gray" />
{/if}
</button>
<!-- Entry marked as professional -->
<!-- class:hidden={$lq__journal_obj.cfg_json?.hide_btn_professional} -->
<button
type="button"
onclick={() => {
tmp_entry_obj.professional =
!$lq__journal_entry_obj?.professional;
update_journal_entry();
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle professional visibility of this journal entry"
>
{#if $lq__journal_entry_obj?.professional}
<BriefcaseBusiness strokeWidth="2.5" color="green" />
{:else}
<BriefcaseBusiness strokeWidth="1" color="gray" />
{/if}
</button>
<!-- Toggle if entry should be used as a template entry (cloneable). Only visible in edit_mode. -->
<button
type="button"
class:hidden={$lq__journal_obj?.cfg_json?.hide_btn_template ||
!$ae_loc.edit_mode}
onclick={() => {
if (tmp_entry_obj) {
tmp_entry_obj.template = !$lq__journal_entry_obj?.template;
update_journal_entry();
}
}}
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
title="Toggle if used as journal entry template"
>
{#if $lq__journal_entry_obj?.template}
<NotepadTextDashed strokeWidth="2.5" color="green" />
{:else}
<NotepadTextDashed strokeWidth="1" color="gray" />
{/if}
</button>
</div>
<AE_ObjectFlags
bind:obj={tmp_entry_obj}
onToggle={() => update_journal_entry()}
hideAlert={$lq__journal_obj?.cfg_json?.hide_btn_alert}
hidePrivate={$lq__journal_obj?.cfg_json?.hide_btn_private}
hidePublic={$lq__journal_obj?.cfg_json?.hide_btn_public}
hidePersonal={$lq__journal_obj?.cfg_json?.hide_btn_personal}
hideProfessional={$lq__journal_obj?.cfg_json?.hide_btn_professional}
hideTemplate={$lq__journal_obj?.cfg_json?.hide_btn_template}
/>
<!-- Copy and Clone Buttons -->
<div>
@@ -2232,31 +2078,7 @@
{/if}
</section>
<section class="ae_meta flex flex-row flex-wrap gap-1 items-center justify-between w-full">
<span class="flex flex-row items-center justify-center text-sm text-gray-500">
{#if !$ae_loc.edit_mode}
<span class="">
{ae_util.iso_datetime_formatter(
$lq__journal_entry_obj?.created_on,
'datetime_iso_12_no_seconds'
)}
{$lq__journal_entry_obj?.updated_on
? ` | Last updated: ${ae_util.iso_datetime_formatter($lq__journal_entry_obj?.updated_on, 'datetime_iso_12_no_seconds')}`
: ''}
</span>
{:else}
<span class="">
{ae_util.iso_datetime_formatter(
$lq__journal_entry_obj?.created_on,
'datetime_iso_tz'
)}
{$lq__journal_entry_obj?.updated_on
? ` | Last updated: ${ae_util.iso_datetime_formatter($lq__journal_entry_obj?.updated_on, 'datetime_iso_tz')}`
: ''}
</span>
{/if}
</span>
</section>
<AE_MetadataFooter obj={tmp_entry_obj} />
{:else}