style(journals): stabilize and standardize all configuration interfaces

- Implemented 'untrack' safeguards in all modal effect blocks to prevent reactivity loops.
- Standardized Module, Journal, and Entry configuration with unified tabbed UI and Aether Orange theme.
- Fixed critical 'journal is undefined' and ReferenceErrors in configuration modals.
- Restored missing button toggles and visibility options in journal-level settings.
- Documented Dexie liveQuery '$' prefix mandate in GEMINI.md and project documentation.
- Eliminated legacy JournalEntry_SettingsMenu in favor of ModalJournalEntryConfig.
This commit is contained in:
Scott Idem
2026-01-14 17:50:48 -05:00
parent 8ae23cdcd9
commit cdf318e744
8 changed files with 238 additions and 316 deletions

View File

@@ -11,10 +11,11 @@ This project is the frontend UI/UX for the Aether (AE) system, built with Svelte
## 🏗️ Architecture & Standards
### Svelte v5 (Runes)
- **State Management:** Use `$state` and `$derived`. Props are passed via `$props()` and can be `$bindable`.
- **Event Handling:** Use lowercase `onevent` attributes (e.g., `onclick`). Handle `preventDefault` inside functions.
- **Dexie Integration:** `liveQuery` results are observables and require the `$` prefix (e.g., `$lq__obj`) in templates. Subscribe in `onMount` to avoid SSR issues.
### Reactivity & Stores
- **Svelte 5 Runes:** Use `$state`, `$derived`, and `$effect` for component-level reactivity.
- **Dexie LiveQuery:** Results from `liveQuery` are observables. You **MUST** use the `$` prefix (e.g., `$lq__obj`) in templates and logic to subscribe to the live data. Failure to do so will result in accessing the observable object instead of its data.
- **Persistence:** Use `svelte-persisted-store` for data that must survive page refreshes (e.g., `ae_loc`, `journals_loc`).
- **Initialization:** Always initialize reactive state (`$state`) outside of `$props()` destructuring. Use `untrack` inside `$effect` when synchronizing props to local state to prevent infinite loops.
### ID Convention (Triple-ID Pattern)
- **Standard:** Use `id`, `[obj_type]_id`, and `[obj_type]_id_random` consistently.

View File

@@ -31,6 +31,7 @@ This is a list of tasks to be completed before the next event/show/conference.
- [ ] **Phase 2: UI/UX Excellence**
- [x] Implement "Quick Add" for high-velocity entry.
- [x] Add rapid append/prepend functionality to existing entries.
- [x] Standardize Module, Journal, and Entry configuration modals (Aether Orange pattern).
- [ ] Ensure full cross-platform responsiveness (Mobile -> Workstation).
- [ ] **Phase 3: Content Portability**
- [x] Implement Structured Markdown import (with metadata).

View File

@@ -71,8 +71,9 @@ This document outlines the modernization of the Journals module UI in the Svelte
### Phase 4: Polish & Security (ACTIVE)
- [x] Implement Auto-Save toggle and visual status indicators.
- [x] Extract decryption workflow to non-reactive helper.
- [ ] Solidify E2EE passcode system for Journals and Entries.
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
- [x] **RESOLVED:** Decryption workflow stability (Fixed via dependency isolation).
- [ ] Solidify E2EE passcode system for Journals and Entries.
- [ ] Audit encryption flow for Quick Added and Imported entries.
- [ ] Integrate Outbound Email sharing.
@@ -80,14 +81,25 @@ This document outlines the modernization of the Journals module UI in the Svelte
## 🧠 Lessons Learned: Solving the Svelte 5 Reactivity Hang
During the implementation of the Privacy/Decryption toggle, we encountered a critical browser hang caused by an infinite reactivity loop. Here is how we resolved it and the patterns we should follow in the future:
During the implementation of the Privacy/Decryption toggle and the new Configuration Modals, we encountered critical browser hangs caused by infinite reactivity loops. Here is how we resolved them:
### 1. Rigorous Dependency Isolation (`untrack`)
Svelte 5 runes (`$effect`, `$derived`) automatically track **every** reactive variable read inside them.
* **The Problem:** An effect would read `save_status` or `tmp_entry_obj.content` to decide if it should sync, but the act of syncing would update those same variables, re-triggering the effect.
* **The Fix:** Wrap any "check-only" state or store reads in `untrack(() => ... )`. This allows the effect to use the value without becoming a dependency of it.
* **The Fix:** Wrap any "check-only" state or store reads in `untrack(() => ... )`. This allows the effect to use the value without becoming a dependency of it. This is **CRITICAL** when initializing local state from props inside an effect.
### 2. Manual Deep Copying vs. Proxies
### 2. Standardized Modal UI ("Aether Orange")
We have established a unified design language for configuration interfaces:
* **Header/Footer:** Use `bg-orange-100 dark:bg-orange-900` with consistent borders.
* **Tabs:** Center-aligned tabbed navigation using Skeleton UI `btn` and `variant-filled-primary` / `variant-soft-surface`.
* **Icons:** Every tab and primary action should have a Lucide icon for better scannability.
* **Explicit Persistence:** All configuration modals must follow an "Edit working copy -> Save Changes" pattern to prevent accidental store/API churn.
### 3. Dexie LiveQuery Subscriptions
* **The Problem:** Accessing `liveQuery` observables directly in templates results in `[object Object]` or `undefined` property errors.
* **The Mandate:** ALWAYS use the `$` prefix (e.g. `$lq__obj`) when passing or using data from a Dexie `liveQuery`.
### 4. Manual Deep Copying vs. Proxies
Svelte 5 state is backed by Proxies.
* **The Problem:** Using `JSON.parse(JSON.stringify(proxy))` can sometimes trigger unexpected behavior or loops when used inside a reactive context.
* **The Fix:** Implement a manual `deep_copy` helper or selective property assignment when syncing "Original" vs "Temporary" state. This ensures `orig_entry_obj` is a plain JS object, making the `has_unsaved_changes` check stable.

View File

@@ -1,227 +0,0 @@
<script lang="ts">
/**
* JournalEntry_SettingsMenu.svelte
* Extracted 2026-01-08 to modularize the Journal Entry Header.
* Manages Category, Flags, Copy/Clone, History, and Sort Order.
*/
import {
Plus, Minus, ArrowDown10, Flag, FlagOff,
Eye, EyeOff, ShieldCheck, ShieldMinus,
Clock, X, Trash2, Settings, Shapes,
Copy, RemoveFormatting, CodeXml, TypeOutline,
History, Pencil, PenLine, FileX, SquareLibrary,
ArrowUpToLine, ArrowDownToLine, FileDown
} from '@lucide/svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { journals_slct, journals_loc, journals_sess } from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { goto } from '$app/navigation';
import AE_ObjectFlags from '$lib/ae_elements/AE_ObjectFlags.svelte';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
interface Props {
entry: ae_JournalEntry;
journal: ae_Journal;
journals_li: ae_Journal[];
tmp_entry_obj: any; // Bindable
onSave: () => void;
onDecryptHistory: () => void;
onChangeJournal: () => void;
onAppend?: () => void;
onPrepend?: () => void;
onShowExport?: () => void;
log_lvl?: number;
}
let {
entry,
journal,
journals_li = [],
tmp_entry_obj = $bindable(),
onSave,
onDecryptHistory,
onChangeJournal,
onAppend,
onPrepend,
onShowExport,
log_lvl = 0
}: Props = $props();
async function delete_entry() {
if (confirm(`Are you sure you want to delete this journal entry?`)) {
let delete_method: "delete" | "disable" | "hide" = "disable";
if ($ae_loc.administrator_access && $ae_loc.edit_mode) {
delete_method = 'delete';
}
try {
await journals_func.delete_ae_obj_id__journal_entry({
api_cfg: $ae_api,
journal_entry_id: entry.journal_entry_id,
method: delete_method,
log_lvl
});
alert('Journal entry deleted successfully!');
goto(`/journals/${$journals_slct.journal_id}`);
} catch (error) {
console.error('Error deleting journal entry:', error);
alert('Failed to delete journal entry.');
}
}
}
function copyToClipboard(text: string, msg: string) {
navigator.clipboard.writeText(text)
.then(() => alert(msg))
.catch(err => console.error('Copy failed:', err));
}
async function copyRichText() {
const element = document.getElementById(`rendered_journal_entry_content_${entry.journal_entry_id}`);
if (!element) return;
try {
await navigator.clipboard.write([
new ClipboardItem({ 'text/html': new Blob([element.innerHTML], { type: 'text/html' }) })
]);
alert('Rich text copied!');
} catch (err) {
console.error('Rich copy failed:', err);
}
}
</script>
<div class="space-y-4 min-w-[280px]">
<!-- Append/Prepend Actions -->
<div class="grid grid-cols-2 gap-2">
<button class="btn btn-sm variant-soft-secondary" onclick={onPrepend} title="Prepend to Note">
<ArrowUpToLine size="1.2em" class="mr-1"/> Prepend
</button>
<button class="btn btn-sm variant-soft-secondary" onclick={onAppend} title="Append to Note">
<ArrowDownToLine size="1.2em" class="mr-1"/> Append
</button>
</div>
<!-- Export Action -->
<button
class="btn btn-sm variant-soft-secondary w-full"
onclick={() => {
console.log('JournalEntry_SettingsMenu: Export Entry clicked');
if (onShowExport) {
onShowExport();
} else {
console.warn('JournalEntry_SettingsMenu: onShowExport prop is undefined');
}
}}
title="Export this Entry"
>
<FileDown size="1.2em" class="mr-2"/> Export Entry
</button>
<!-- Category selection -->
<div class="flex items-center gap-2">
<Shapes size="1.1em" class="text-surface-500" />
<select
class="select select-sm preset-tonal-primary grow"
bind:value={tmp_entry_obj.category_code}
onchange={onSave}
>
<option value="">Select Category</option>
<!-- @ts-ignore -->
{#each journal?.cfg_json?.category_li ?? [] as category}
<option value={category.code}>{category.name}</option>
{/each}
</select>
</div>
<!-- Flags (Standardized component) -->
<AE_ObjectFlags
bind:obj={tmp_entry_obj}
onToggle={onSave}
hideAlert={journal?.cfg_json?.hide_btn_alert}
hidePrivate={journal?.cfg_json?.hide_btn_private}
hidePublic={journal?.cfg_json?.hide_btn_public}
hidePersonal={journal?.cfg_json?.hide_btn_personal}
hideProfessional={journal?.cfg_json?.hide_btn_professional}
hideTemplate={journal?.cfg_json?.hide_btn_template}
/>
<!-- Copy Actions -->
<div class="grid grid-cols-3 gap-1">
<button class="btn btn-sm variant-soft-surface" onclick={() => copyToClipboard(tmp_entry_obj.content, 'Markdown copied!')} title="Copy Markdown">
<RemoveFormatting size="1.2em" />
</button>
<button class="btn btn-sm variant-soft-surface" onclick={() => copyToClipboard(entry.content_md_html || '', 'HTML copied!')} title="Copy HTML">
<CodeXml size="1.2em" />
</button>
<button class="btn btn-sm variant-soft-surface" onclick={copyRichText} title="Copy Rich Text">
<TypeOutline size="1.2em" />
</button>
</div>
<div class="divider !my-1"></div>
<!-- History & Sort Section -->
<div class="flex flex-col gap-2">
<!-- History Toggle -->
{#if entry.history || entry.history_encrypted}
<button
class="btn btn-sm w-full { $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'history' ? 'variant-filled-secondary' : 'variant-soft-secondary'}"
onclick={onDecryptHistory}
>
<History size="1.1em" class="mr-2" /> Review History
</button>
{/if}
<!-- Cleaned Sort Controls -->
<div class="flex items-center justify-between p-2 bg-surface-500/10 rounded-lg border border-surface-500/20">
<span class="text-xs font-bold uppercase tracking-wider opacity-60">Sort Order</span>
<div class="flex items-center gap-3">
<button
class="btn-icon btn-icon-sm variant-filled-surface border border-surface-500/50"
onclick={() => { tmp_entry_obj.sort = (entry.sort ?? 0) + 1; onSave(); }}
>
<Plus size="1em" />
</button>
<span class="font-mono font-bold text-sm w-6 text-center">{tmp_entry_obj.sort ?? entry.sort ?? 0}</span>
<button
class="btn-icon btn-icon-sm variant-filled-surface border border-surface-500/50"
onclick={() => { tmp_entry_obj.sort = Math.max(0, (entry.sort ?? 0) - 1); onSave(); }}
>
<Minus size="1em" />
</button>
</div>
</div>
</div>
<!-- Archive Control -->
<div class="flex items-center gap-2 p-1 bg-surface-500/5 rounded border border-surface-500/10">
<Clock size="1.1em" class="opacity-50 ml-1" />
<input type="datetime-local" bind:value={tmp_entry_obj.archive_on} onchange={onSave} class="input input-sm border-none bg-transparent text-xs grow" />
{#if entry.archive_on}
<button onclick={() => { tmp_entry_obj.archive_on = null; onSave(); }} class="btn-icon btn-icon-sm text-error-500"><X size="1em"/></button>
{/if}
</div>
<!-- Move to Journal -->
{#if $ae_loc.edit_mode && journals_li.length}
<div class="p-2 border border-surface-500/20 rounded-lg space-y-1">
<span class="text-[10px] uppercase font-bold opacity-50 flex items-center gap-1">
<SquareLibrary size="1em" /> Move to Journal
</span>
<select
class="select select-sm w-full text-xs"
bind:value={tmp_entry_obj.journal_id}
onchange={() => { if(confirm('Change parent journal?')) onChangeJournal(); }}
>
<option value="">Select Journal</option>
{#each journals_li as j}
<option value={j.journal_id}>{j.name}</option>
{/each}
</select>
</div>
{/if}
<!-- Deletion -->
<button class="btn btn-sm variant-filled-error w-full mt-2" onclick={delete_entry}>
<Trash2 size="1.1em" class="mr-2" /> Delete Entry
</button>
</div>

View File

@@ -357,7 +357,7 @@
</script>
<section class="ae_view flex flex-col gap-2 w-full h-full p-1" bind:clientHeight={$ae_loc.iframe_height_modal_body}>
{#if $lq__journal_entry_obj}
{#if $lq__journal_entry_obj && $lq__journal_obj}
<JournalEntry_Header
entry={$lq__journal_entry_obj}
journal={$lq__journal_obj}

View File

@@ -2,17 +2,14 @@
/**
* ae_comp__journal_obj_id_edit.svelte
* Standardized Journal-level configuration.
* Restored missing visibility and button toggles.
*/
import {
CalendarClock,
Check,
Settings,
X,
Database,
CodeXml,
ShieldCheck,
BookOpenText,
MousePointerClick,
Trash2,
Fingerprint,
LockKeyhole,
@@ -20,13 +17,28 @@
Minus,
Palette,
MonitorPlay,
Layout
Layout,
Eye,
EyeOff,
Siren,
MessageSquareWarning,
Copy,
FilePlus,
Globe,
BookHeart,
BriefcaseBusiness,
Target,
Expand,
BetweenVerticalEnd,
BetweenVerticalStart,
X,
Settings
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { goto } from '$app/navigation';
import { untrack } from 'svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import {
ae_loc,
ae_api
@@ -57,8 +69,15 @@
// Deep copy on mount or when lq changes to ensure we have a working copy
$effect(() => {
if (show && lq__journal_obj && !tmp__journal_obj.journal_id) {
tmp__journal_obj = JSON.parse(JSON.stringify(lq__journal_obj));
if (show && lq__journal_obj) {
const source_id = lq__journal_obj.journal_id;
untrack(() => {
if (!tmp__journal_obj.journal_id || tmp__journal_obj.journal_id !== source_id) {
tmp__journal_obj = JSON.parse(JSON.stringify(lq__journal_obj));
// Ensure cfg_json exists
if (!tmp__journal_obj.cfg_json) tmp__journal_obj.cfg_json = {};
}
});
}
});
@@ -69,26 +88,10 @@
}
try {
const data_kv = {
name: tmp__journal_obj.name,
description: tmp__journal_obj.description ?? '',
type_code: tmp__journal_obj.type_code,
passcode: tmp__journal_obj.passcode,
private_passcode: tmp__journal_obj.private_passcode,
passcode_timeout: tmp__journal_obj.passcode_timeout,
auth_key: tmp__journal_obj.auth_key,
cfg_json: tmp__journal_obj.cfg_json,
group: tmp__journal_obj.group,
sort: tmp__journal_obj.sort,
hide: tmp__journal_obj.hide,
enable: tmp__journal_obj.enable,
priority: tmp__journal_obj.priority
};
await journals_func.update_ae_obj__journal({
api_cfg: $ae_api,
journal_id: lq__journal_obj?.journal_id,
data_kv: data_kv,
data_kv: tmp__journal_obj,
log_lvl: log_lvl
});
show = false;
@@ -128,11 +131,11 @@
{#snippet header()}
<h3 class="flex items-center gap-2 text-lg font-bold">
<BookOpenText class="text-primary-500" />
<span>Edit Journal: {lq__journal_obj?.name ?? '--'}</span>
<span>Edit Journal: {$lq__journal_obj?.name ?? '--'}</span>
</h3>
{/snippet}
<div class="space-y-6 py-2 h-[70vh] overflow-y-auto px-4">
<div class="space-y-6 py-2 h-[75vh] overflow-y-auto px-4">
<!-- Navigation Tabs -->
<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">
<button class="btn btn-sm transition-all {tab === 'general' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'general')}>
@@ -180,10 +183,10 @@
<!-- Categories -->
<section class="space-y-4 p-2">
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<Settings size="1em" class="text-primary-500" />
<MonitorPlay size="1em" class="text-primary-500" />
Journal Categories
</h2>
<div class="space-y-2 bg-surface-500/5 p-4 rounded-lg">
<div class="space-y-2 bg-surface-500/5 p-4 rounded-lg border border-surface-500/10">
{#each tmp__journal_obj?.cfg_json?.category_li ?? [] as category, i}
<div class="flex gap-2 items-center">
<input type="text" bind:value={category.code} class="input input-sm w-32" placeholder="Code" />
@@ -210,7 +213,7 @@
<LockKeyhole size="1.2em" class="text-primary-500" />
Encryption Passcodes
</h2>
<div class="bg-warning-500/10 border border-warning-500/30 p-4 rounded-lg space-y-4">
<div class="bg-warning-500/10 border border-warning-500/30 p-4 rounded-lg space-y-4 shadow-inner">
<label class="label">
<span class="text-xs font-bold uppercase tracking-wider opacity-70">Primary Passcode (Stored)</span>
<div class="flex gap-2">
@@ -236,25 +239,27 @@
<Fingerprint size="1.2em" class="text-primary-500" />
Status & Lifecycle
</h2>
<div class="grid grid-cols-2 gap-4">
<label class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.enable} class="checkbox" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
<input type="checkbox" bind:checked={tmp__journal_obj.enable} class="checkbox checkbox-primary" />
<span class="font-bold">Enabled</span>
</label>
<label class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.hide} class="checkbox" />
<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">
<input type="checkbox" bind:checked={tmp__journal_obj.hide} class="checkbox checkbox-primary" />
<span class="font-bold">Hidden</span>
</label>
<label class="flex items-center space-x-3 cursor-pointer p-3 rounded-lg bg-surface-500/5 border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.priority} class="checkbox" />
<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">
<input type="checkbox" bind:checked={tmp__journal_obj.priority} class="checkbox checkbox-primary" />
<span class="font-bold">Priority Journal</span>
</label>
</div>
</section>
<button class="btn btn-sm variant-filled-error w-full mt-12" onclick={delete_journal}>
<Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal
</button>
<section class="pt-8">
<button class="btn btn-sm variant-filled-error w-full shadow-lg" onclick={delete_journal}>
<Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal
</button>
</section>
</div>
{:else if tab === 'ui'}
@@ -262,9 +267,9 @@
<section class="space-y-4 p-2">
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<MonitorPlay size="1.2em" class="text-primary-500" />
Editor & Viewer
Default View Modes
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<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">
<label class="label">
<span class="text-sm font-bold opacity-70">Preferred Viewer</span>
<select bind:value={tmp__journal_obj.cfg_json.pref_viewer} class="select variant-form-material">
@@ -280,25 +285,126 @@
<option value="codemirror">CodeMirror (Advanced)</option>
</select>
</label>
<label class="label">
<span class="text-sm font-bold opacity-70">Color Scheme</span>
<select bind:value={tmp__journal_obj.cfg_json.color_scheme} class="select variant-form-material">
<option value="">Default (Slate)</option>
<option value="blue">Deep Blue</option>
<option value="green">Nature Green</option>
<option value="orange">Aether Orange</option>
<option value="red">Warning Red</option>
<option value="yellow">Caution Yellow</option>
</select>
</label>
<label class="label">
<span class="text-sm font-bold opacity-70">Quick Add Placement</span>
<select bind:value={tmp__journal_obj.cfg_json.entry_add_text} class="select variant-form-material">
<option value="append">Append to End (Default)</option>
<option value="prepend">Prepend to Start</option>
</select>
</label>
</div>
</section>
<!-- Visibility Toggles (Restored) -->
<section class="space-y-4 p-2">
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<Eye size="1.2em" class="text-primary-500" />
Entry Visibility (Global)
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_private} class="checkbox checkbox-sm" />
<span class="text-xs font-bold">Hide Private</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_personal} class="checkbox checkbox-sm" />
<span class="text-xs font-bold">Hide Personal</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_professional} class="checkbox checkbox-sm" />
<span class="text-xs font-bold">Hide Professional</span>
</label>
</div>
</section>
<!-- Button Toggles (Restored) -->
<section class="space-y-4 p-2">
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<Settings size="1.2em" class="text-primary-500" />
Entry Feature Buttons
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_alert} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Alert</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_private} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Private</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_public} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Public</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_personal} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Pers.</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_professional} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Prof.</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_btn_template} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Templ.</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_copy_plain_md} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Copy MD</span>
</label>
<label class="flex items-center gap-2 p-2 bg-surface-500/5 rounded border border-surface-500/10">
<input type="checkbox" bind:checked={tmp__journal_obj.cfg_json.hide_clone} class="checkbox checkbox-sm" />
<span class="text-[10px] uppercase font-bold">Hide Clone</span>
</label>
</div>
</section>
<section class="space-y-4 p-2">
<h2 class="text-lg font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<Palette size="1.2em" class="text-primary-500" />
Visual Theme
<Layout size="1.2em" class="text-primary-500" />
List Interactions
</h2>
<label class="label">
<span class="text-sm font-bold opacity-70">Color Scheme</span>
<select bind:value={tmp__journal_obj.cfg_json.color_scheme} class="select variant-form-material">
<option value="">Default (Slate)</option>
<option value="blue">Deep Blue</option>
<option value="green">Nature Green</option>
<option value="orange">Aether Orange</option>
<option value="red">Warning Red</option>
<option value="yellow">Caution Yellow</option>
</select>
</label>
<div class="bg-surface-500/5 p-4 rounded-lg space-y-4">
<label class="label">
<span class="text-xs font-bold opacity-70">Expansion Trigger</span>
<select bind:value={tmp__journal_obj.cfg_json.expand_li_content} class="select select-sm variant-form-material">
<option value="click">Click to Expand</option>
<option value="hover">Hover to Expand</option>
</select>
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label class="label">
<span class="text-xs font-bold opacity-70">List Max Height</span>
<select bind:value={tmp__journal_obj.cfg_json.entry_li_max_height} class="select select-sm variant-form-material">
<option value="">Default</option>
<option value="max-h-16">Small (16)</option>
<option value="max-h-32">Medium (32)</option>
<option value="max-h-64">Large (64)</option>
<option value="max-h-full">Full</option>
</select>
</label>
<label class="label">
<span class="text-xs font-bold opacity-70">Active Max Height</span>
<select bind:value={tmp__journal_obj.cfg_json.entry_li_click_max_height} class="select select-sm variant-form-material">
<option value="">Default</option>
<option value="active:max-h-64">Large (64)</option>
<option value="active:max-h-96">X-Large (96)</option>
<option value="active:max-h-full">Full</option>
</select>
</label>
</div>
</div>
</section>
</div>

View File

@@ -80,16 +80,16 @@
<!-- Navigation Tabs -->
<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">
<button class="btn btn-sm transition-all {tab === 'actions' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'actions')}>
Quick Actions
<Clock size="1.1em" class="mr-1" /> Quick Actions
</button>
<button class="btn btn-sm transition-all {tab === 'meta' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'meta')}>
Metadata
<Shapes size="1.1em" class="mr-1" /> Metadata
</button>
<button class="btn btn-sm transition-all {tab === 'security' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'security')}>
Security
<ShieldCheck size="1.1em" class="mr-1" /> Security
</button>
<button class="btn btn-sm transition-all {tab === 'json' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => (tab = 'json')}>
JSON
<CodeXml size="1.1em" class="mr-1" /> JSON
</button>
</div>

View File

@@ -1,21 +1,26 @@
<script lang="ts">
/**
* modal_journals_config.svelte
* Standardized Global/Module configuration for Journals.
* Standardized Module-level settings for Journals.
* Fixed Svelte 5 state placement and reactivity loops.
*/
import {
CalendarClock,
Check,
Settings,
X,
Database,
CodeXml,
Check,
ShieldCheck,
BookOpenText,
MousePointerClick
CodeXml,
Layout,
Palette,
MonitorPlay,
X,
CalendarClock,
MousePointerClick,
Database
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { untrack } from 'svelte';
// *** Import Aether specific variables and functions
import {
ae_loc,
ae_api
@@ -32,11 +37,31 @@
}
let {
log_lvl = $bindable(0),
log_lvl = 0,
show = $bindable(false)
}: Props = $props();
// Internal State
let tab: 'form' | 'local_json' | 'session_json' = $state('form');
let tmp_config: any = $state({
entry: {}
});
$effect(() => {
if (show) {
untrack(() => {
const fresh_config = JSON.parse(JSON.stringify($journals_loc));
// Ensure the structure is preserved
if (!fresh_config.entry) fresh_config.entry = {};
tmp_config = fresh_config;
});
}
});
function handle_save() {
journals_loc.set(tmp_config);
show = false;
}
</script>
<Modal
@@ -45,8 +70,8 @@
placement="top-center"
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"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg"
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"
>
{#snippet header()}
<h3 class="flex items-center gap-2 text-lg font-bold">
@@ -57,7 +82,7 @@
<div class="space-y-6 py-2 h-[60vh] overflow-y-auto px-4">
<!-- Navigation Tabs -->
<div class="flex justify-center gap-1 mb-4 p-1 bg-surface-500/10 rounded-lg max-w-fit mx-auto">
<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">
<button
class="btn btn-sm transition-all {tab === 'form' ? 'variant-filled-primary' : 'variant-soft-surface'}"
onclick={() => (tab = 'form')}
@@ -89,7 +114,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
<label class="label">
<span class="text-sm font-bold opacity-70">DateTime Format</span>
<select bind:value={$journals_loc.datetime_format} class="select select-sm variant-form-material">
<select bind:value={tmp_config.datetime_format} class="select select-sm variant-form-material">
<option value="datetime_12_short">MMM D, YY hh:mm A</option>
<option value="datetime_12_long">MMMM D, YYYY hh:mm A</option>
<option value="datetime_short">MMM D, YY HH:mm</option>
@@ -100,7 +125,7 @@
</label>
<label class="label">
<span class="text-sm font-bold opacity-70">Time-Only Format</span>
<select bind:value={$journals_loc.time_format} class="select select-sm variant-form-material">
<select bind:value={tmp_config.time_format} class="select select-sm variant-form-material">
<option value="time_12_short">12-hour short (3:30 PM)</option>
<option value="time_12_long">12-hour long (3:30:45 PM)</option>
<option value="time_short">24-hour short (15:30)</option>
@@ -118,14 +143,14 @@
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
<label class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10">
<input type="checkbox" bind:checked={$journals_loc.entry.auto_save} class="checkbox" />
<input type="checkbox" bind:checked={tmp_config.entry.auto_save} class="checkbox" />
<div class="space-y-0.5">
<span class="font-bold">Enable Auto-Save</span>
<p class="text-xs opacity-60">Automatically sync changes while editing</p>
</div>
</label>
<label class="flex items-center space-x-3 cursor-pointer p-2 rounded-lg bg-surface-500/5 border border-surface-500/10">
<input type="checkbox" bind:checked={$journals_loc.show_id_random} class="checkbox" />
<input type="checkbox" bind:checked={tmp_config.show_id_random} class="checkbox" />
<div class="space-y-0.5">
<span class="font-bold">Show Technical IDs</span>
<p class="text-xs opacity-60">Display UUIDs in metadata footers</p>
@@ -156,7 +181,7 @@
<E_app_codemirror_v5
editable={false}
readonly={true}
content={JSON.stringify($journals_loc, null, 2)}
content={JSON.stringify(tmp_config, null, 2)}
theme_mode={$ae_loc.theme_mode}
class="rounded-lg border border-surface-500/30"
/>
@@ -175,9 +200,13 @@
</div>
{#snippet footer()}
<button class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]" onclick={() => (show = false)}>
<Check size="1.2em" class="mr-2" />
Done
</button>
<div class="flex gap-4">
<button class="btn variant-ghost-surface font-bold min-w-[100px]" onclick={() => (show = false)}>
<X size="1.2em" class="mr-2" /> Cancel
</button>
<button class="btn variant-filled-primary font-bold shadow-lg min-w-[120px]" onclick={handle_save}>
<Check size="1.2em" class="mr-2" /> Save Changes
</button>
</div>
{/snippet}
</Modal>