- add and expand feature stores for entries, fragments, todos, lists, settings - move CRUD logic into store helpers and simplify component state handling - update SidePanel + button to create section-specific items - switch fragment UX to view-first with edit/create modes - add and update docs for store-based state management - remove deprecated account route
433 lines
11 KiB
Svelte
433 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { goto } from "$app/navigation";
|
|
import AppModal from "$lib/components/AppModal.svelte";
|
|
import Navbar from "$lib/components/Navbar.svelte";
|
|
import {
|
|
addFragmentType,
|
|
addSettingsTag,
|
|
removeFragmentType,
|
|
removeSettingsTag,
|
|
settingsFragmentTypes,
|
|
settingsTags,
|
|
updateFragmentType,
|
|
updateSettingsTag
|
|
} from "$lib/stores/settings";
|
|
|
|
const activeSection = "settings";
|
|
|
|
let modalOpen = false;
|
|
let modalTitle = "";
|
|
let modalMessage = "";
|
|
let modalConfirmText = "OK";
|
|
let modalCancelText = "Cancel";
|
|
let modalShowCancel = false;
|
|
let modalTone: "default" | "danger" = "default";
|
|
let modalAction: "logout-confirm" | "logout-info" | null = null;
|
|
let newTag = "";
|
|
let newFragmentType = "";
|
|
let editingTagIndex: number | null = null;
|
|
let editingTagValue = "";
|
|
let editingFragmentTypeIndex: number | null = null;
|
|
let editingFragmentTypeValue = "";
|
|
|
|
function showModal(options: {
|
|
action: "logout-confirm" | "logout-info";
|
|
title: string;
|
|
message: string;
|
|
confirmText?: string;
|
|
cancelText?: string;
|
|
showCancel?: boolean;
|
|
tone?: "default" | "danger";
|
|
}) {
|
|
modalAction = options.action;
|
|
modalTitle = options.title;
|
|
modalMessage = options.message;
|
|
modalConfirmText = options.confirmText ?? "OK";
|
|
modalCancelText = options.cancelText ?? "Cancel";
|
|
modalShowCancel = options.showCancel ?? false;
|
|
modalTone = options.tone ?? "default";
|
|
modalOpen = true;
|
|
}
|
|
|
|
function closeModal() {
|
|
modalOpen = false;
|
|
modalAction = null;
|
|
}
|
|
|
|
function handleModalConfirm() {
|
|
if (modalAction === "logout-confirm") {
|
|
showModal({
|
|
action: "logout-info",
|
|
title: "Logout Requested",
|
|
message: "You have been logged out.",
|
|
confirmText: "Close"
|
|
});
|
|
return;
|
|
}
|
|
|
|
closeModal();
|
|
}
|
|
|
|
function handleSelect(id: string) {
|
|
if (id === "logout") {
|
|
showModal({
|
|
action: "logout-confirm",
|
|
title: "Confirm Logout",
|
|
message: "Are you sure you want to log out?",
|
|
confirmText: "Log Out",
|
|
cancelText: "Cancel",
|
|
showCancel: true,
|
|
tone: "danger"
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (id === "settings") {
|
|
return;
|
|
}
|
|
|
|
if (id === "account") {
|
|
goto("/account");
|
|
return;
|
|
}
|
|
|
|
goto("/");
|
|
}
|
|
|
|
function addTag() {
|
|
if (addSettingsTag(newTag)) {
|
|
newTag = "";
|
|
}
|
|
}
|
|
|
|
function startEditTag(index: number, tag: string) {
|
|
editingTagIndex = index;
|
|
editingTagValue = tag;
|
|
}
|
|
|
|
function saveEditTag() {
|
|
if (editingTagIndex === null) return;
|
|
if (updateSettingsTag(editingTagIndex, editingTagValue)) {
|
|
editingTagIndex = null;
|
|
editingTagValue = "";
|
|
}
|
|
}
|
|
|
|
function cancelEditTag() {
|
|
editingTagIndex = null;
|
|
editingTagValue = "";
|
|
}
|
|
|
|
function removeTag(index: number) {
|
|
if (!removeSettingsTag(index)) return;
|
|
if (editingTagIndex === index) {
|
|
cancelEditTag();
|
|
}
|
|
}
|
|
|
|
function addFragmentTypeLocal() {
|
|
if (addFragmentType(newFragmentType)) {
|
|
newFragmentType = "";
|
|
}
|
|
}
|
|
|
|
function startEditFragmentType(index: number, fragmentType: string) {
|
|
editingFragmentTypeIndex = index;
|
|
editingFragmentTypeValue = fragmentType;
|
|
}
|
|
|
|
function saveEditFragmentType() {
|
|
if (editingFragmentTypeIndex === null) return;
|
|
if (updateFragmentType(editingFragmentTypeIndex, editingFragmentTypeValue)) {
|
|
editingFragmentTypeIndex = null;
|
|
editingFragmentTypeValue = "";
|
|
}
|
|
}
|
|
|
|
function cancelEditFragmentType() {
|
|
editingFragmentTypeIndex = null;
|
|
editingFragmentTypeValue = "";
|
|
}
|
|
|
|
function removeFragmentTypeLocal(index: number) {
|
|
if (!removeFragmentType(index)) return;
|
|
if (editingFragmentTypeIndex === index) {
|
|
cancelEditFragmentType();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="app-shell panel-closed">
|
|
<Navbar {activeSection} onSelect={handleSelect} />
|
|
|
|
<main class="route-view">
|
|
<header class="route-header">
|
|
<h1>Settings</h1>
|
|
<p>Configure app behavior and interface options.</p>
|
|
</header>
|
|
|
|
<section class="route-card">
|
|
<label class="toggle-row">
|
|
<input type="checkbox" checked />
|
|
<span>Launch to last opened entry</span>
|
|
</label>
|
|
|
|
<label class="toggle-row">
|
|
<input type="checkbox" />
|
|
<span>Enable compact editor mode</span>
|
|
</label>
|
|
|
|
<label>
|
|
Default startup view
|
|
<select>
|
|
<option>Entries</option>
|
|
<option>Calendar</option>
|
|
<option>Fragments</option>
|
|
</select>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="route-card">
|
|
<h2>Tags</h2>
|
|
<p class="section-copy">Add and manage tags used for notes and entries.</p>
|
|
|
|
<div class="create-row">
|
|
<input
|
|
type="text"
|
|
placeholder="Add tag (example: Research)"
|
|
bind:value={newTag}
|
|
on:keydown={(event) => event.key === "Enter" && addTag()}
|
|
/>
|
|
<button type="button" class="secondary-btn" on:click={addTag}>Add</button>
|
|
</div>
|
|
|
|
<ul class="item-list">
|
|
{#each $settingsTags as tag, index}
|
|
<li class="item-row">
|
|
{#if editingTagIndex === index}
|
|
<input
|
|
type="text"
|
|
bind:value={editingTagValue}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") saveEditTag();
|
|
if (event.key === "Escape") cancelEditTag();
|
|
}}
|
|
/>
|
|
<div class="row-actions">
|
|
<button type="button" class="secondary-btn" on:click={saveEditTag}>Save</button>
|
|
<button type="button" class="ghost-btn" on:click={cancelEditTag}>Cancel</button>
|
|
</div>
|
|
{:else}
|
|
<span>{tag}</span>
|
|
<div class="row-actions">
|
|
<button type="button" class="ghost-btn" on:click={() => startEditTag(index, tag)}>Edit</button>
|
|
<button type="button" class="danger-btn" on:click={() => removeTag(index)}>Remove</button>
|
|
</div>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="route-card">
|
|
<h2>Fragment Types</h2>
|
|
<p class="section-copy">Configure custom fragment types for the Fragments section.</p>
|
|
|
|
<div class="create-row">
|
|
<input
|
|
type="text"
|
|
placeholder="Add fragment type (example: Observation)"
|
|
bind:value={newFragmentType}
|
|
on:keydown={(event) => event.key === "Enter" && addFragmentTypeLocal()}
|
|
/>
|
|
<button type="button" class="secondary-btn" on:click={addFragmentTypeLocal}>Add</button>
|
|
</div>
|
|
|
|
<ul class="item-list">
|
|
{#each $settingsFragmentTypes as type, index}
|
|
<li class="item-row">
|
|
{#if editingFragmentTypeIndex === index}
|
|
<input
|
|
type="text"
|
|
bind:value={editingFragmentTypeValue}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") saveEditFragmentType();
|
|
if (event.key === "Escape") cancelEditFragmentType();
|
|
}}
|
|
/>
|
|
<div class="row-actions">
|
|
<button type="button" class="secondary-btn" on:click={saveEditFragmentType}>Save</button>
|
|
<button type="button" class="ghost-btn" on:click={cancelEditFragmentType}>Cancel</button>
|
|
</div>
|
|
{:else}
|
|
<span>{type}</span>
|
|
<div class="row-actions">
|
|
<button type="button" class="ghost-btn" on:click={() => startEditFragmentType(index, type)}>Edit</button>
|
|
<button type="button" class="danger-btn" on:click={() => removeFragmentTypeLocal(index)}>Remove</button>
|
|
</div>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<AppModal
|
|
open={modalOpen}
|
|
title={modalTitle}
|
|
message={modalMessage}
|
|
confirmText={modalConfirmText}
|
|
cancelText={modalCancelText}
|
|
showCancel={modalShowCancel}
|
|
tone={modalTone}
|
|
onConfirm={handleModalConfirm}
|
|
onCancel={closeModal}
|
|
/>
|
|
|
|
<style>
|
|
.route-view {
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
background: linear-gradient(180deg, var(--surface-2) 0%, var(--bg-editor) 100%);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.route-header h1 {
|
|
font-size: 1.1rem;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.route-header p {
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.route-card {
|
|
border: 1px solid var(--border-soft);
|
|
background: var(--surface-1);
|
|
border-radius: 10px;
|
|
padding: 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
max-width: 640px;
|
|
}
|
|
|
|
.toggle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--text-muted);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.route-card label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
color: var(--text-muted);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.route-card select {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 7px;
|
|
background: var(--bg-app);
|
|
color: var(--text-primary);
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.route-card h2 {
|
|
font-size: 0.95rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.section-copy {
|
|
font-size: 0.82rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.create-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.create-row input {
|
|
flex: 1;
|
|
}
|
|
|
|
.item-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.item-row {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
font-size: 0.84rem;
|
|
color: var(--text-primary);
|
|
background: var(--bg-app);
|
|
}
|
|
|
|
.item-row input,
|
|
.route-card input {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 7px;
|
|
background: var(--bg-app);
|
|
color: var(--text-primary);
|
|
padding: 6px 9px;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.row-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.secondary-btn,
|
|
.ghost-btn,
|
|
.danger-btn {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 7px;
|
|
padding: 6px 10px;
|
|
font-size: 0.78rem;
|
|
cursor: pointer;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.secondary-btn {
|
|
background: var(--surface-3);
|
|
border-color: var(--border-strong);
|
|
}
|
|
|
|
.ghost-btn {
|
|
background: var(--surface-1);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.danger-btn {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.secondary-btn:hover,
|
|
.ghost-btn:hover,
|
|
.danger-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
</style>
|
|
|
|
|