feat(editor): person form-first layout with structured fields and collapsible notes

When note type is 'person', replace the main TipTap editor with a contact card form
(Relationship, Birthday, Email, Phone, Organization, Address). The TipTap editor moves
to a collapsible 'Notes' section below the form, auto-expanded when existing body content
is present. Person fields are removed from the sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 13:27:22 -04:00
parent 317d8f25d0
commit 64c19932a7
+110 -16
View File
@@ -38,6 +38,7 @@ const dirty = ref(false);
const saving = ref(false); const saving = ref(false);
const showPreview = ref(false); const showPreview = ref(false);
const sidebarOpen = ref(true); const sidebarOpen = ref(true);
const notesExpanded = ref(false);
const editorRef = ref<InstanceType<typeof TiptapEditor> | null>(null); const editorRef = ref<InstanceType<typeof TiptapEditor> | null>(null);
const titleRef = ref<HTMLInputElement | null>(null); const titleRef = ref<HTMLInputElement | null>(null);
const tiptapEditor = computed<Editor | null>(() => { const tiptapEditor = computed<Editor | null>(() => {
@@ -228,6 +229,7 @@ onMounted(async () => {
milestoneId.value = store.currentNote.milestone_id ?? null; milestoneId.value = store.currentNote.milestone_id ?? null;
noteType.value = (store.currentNote.note_type as NoteType) || "note"; noteType.value = (store.currentNote.note_type as NoteType) || "note";
Object.assign(entityMeta, store.currentNote.metadata || {}); Object.assign(entityMeta, store.currentNote.metadata || {});
notesExpanded.value = !!(store.currentNote.body || '').trim();
savedTitle = title.value; savedTitle = title.value;
savedBody = body.value; savedBody = body.value;
savedTags = [...tags.value]; savedTags = [...tags.value];
@@ -382,6 +384,53 @@ onUnmounted(() => assist.clearSelection());
<!-- Main column --> <!-- Main column -->
<div class="note-main" @keydown.ctrl.e.prevent="tiptapEditor?.commands.focus()"> <div class="note-main" @keydown.ctrl.e.prevent="tiptapEditor?.commands.focus()">
<!-- Person form -->
<template v-if="noteType === 'person'">
<div class="entity-form">
<div class="ef-field">
<label class="ef-label">Relationship</label>
<input class="ef-input" v-model="entityMeta.relationship" placeholder="e.g. Friend, Colleague, Family" @input="markDirty" />
</div>
<div class="ef-field">
<label class="ef-label">Birthday</label>
<input class="ef-input" type="date" v-model="entityMeta.birthday" @input="markDirty" />
</div>
<div class="ef-field">
<label class="ef-label">Email</label>
<input class="ef-input" type="email" v-model="entityMeta.email" placeholder="email@example.com" @input="markDirty" />
</div>
<div class="ef-field">
<label class="ef-label">Phone</label>
<input class="ef-input" type="tel" v-model="entityMeta.phone" placeholder="+1 555 000 0000" @input="markDirty" />
</div>
<div class="ef-field">
<label class="ef-label">Organization</label>
<input class="ef-input" v-model="entityMeta.organization" placeholder="Company or organization" @input="markDirty" />
</div>
<div class="ef-field">
<label class="ef-label">Address</label>
<input class="ef-input" v-model="entityMeta.address" placeholder="Street, City, State" @input="markDirty" />
</div>
</div>
<div class="notes-section">
<button class="notes-toggle" @click="notesExpanded = !notesExpanded">
{{ notesExpanded ? '' : '' }} Notes
</button>
<div v-if="notesExpanded" class="notes-editor-wrap">
<MarkdownToolbar v-show="!showPreview" :editor="tiptapEditor" />
<TiptapEditor
ref="editorRef"
:modelValue="body"
placeholder="Additional notes, wikilinks, context..."
@update:modelValue="onBodyUpdate"
@escape="titleRef?.focus()"
/>
</div>
</div>
</template>
<!-- Generic note editor -->
<template v-else>
<div class="body-tabs-row"> <div class="body-tabs-row">
<div class="editor-tabs"> <div class="editor-tabs">
<button :class="['tab', { active: !showPreview }]" @click="showPreview = false">Write</button> <button :class="['tab', { active: !showPreview }]" @click="showPreview = false">Write</button>
@@ -424,6 +473,7 @@ onUnmounted(() => assist.clearSelection());
<!-- Error from assist --> <!-- Error from assist -->
<div v-if="assist.error.value" class="assist-error">{{ assist.error.value }}</div> <div v-if="assist.error.value" class="assist-error">{{ assist.error.value }}</div>
</template>
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->
@@ -488,22 +538,6 @@ onUnmounted(() => assist.clearSelection());
</select> </select>
</div> </div>
<!-- Person metadata -->
<template v-if="noteType === 'person'">
<div class="sb-field">
<label class="sb-label">Relationship</label>
<input class="sb-input" v-model="entityMeta.relationship" placeholder="e.g. Friend, Colleague" @input="markDirty" />
</div>
<div class="sb-field">
<label class="sb-label">Email</label>
<input class="sb-input" v-model="entityMeta.email" type="email" placeholder="email@example.com" @input="markDirty" />
</div>
<div class="sb-field">
<label class="sb-label">Phone</label>
<input class="sb-input" v-model="entityMeta.phone" type="tel" placeholder="+1 555 000 0000" @input="markDirty" />
</div>
</template>
<!-- Place metadata --> <!-- Place metadata -->
<template v-if="noteType === 'place'"> <template v-if="noteType === 'place'">
<div class="sb-field"> <div class="sb-field">
@@ -831,6 +865,66 @@ onUnmounted(() => assist.clearSelection());
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
/* ── Entity form (Person / Place) ───────────────────────── */
.entity-form {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.ef-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.ef-label {
font-family: 'Fraunces', Georgia, serif;
font-style: italic;
font-size: 0.78rem;
color: var(--color-primary);
}
.ef-input {
padding: 8px 12px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-surface);
color: var(--color-text);
font-size: 0.9rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.ef-input:focus {
border-color: var(--color-primary);
box-shadow: var(--focus-ring);
}
.ef-input::placeholder {
color: var(--color-text-muted);
}
/* ── Notes section (collapsible TipTap) ─────────────────── */
.notes-section {
margin-top: 16px;
border-top: 1px solid var(--color-border);
padding-top: 12px;
}
.notes-toggle {
background: none;
border: none;
color: var(--color-primary);
font-family: 'Fraunces', Georgia, serif;
font-style: italic;
font-size: 0.85rem;
cursor: pointer;
padding: 4px 0;
}
.notes-toggle:hover {
color: var(--color-text);
}
.notes-editor-wrap {
margin-top: 8px;
}
/* Narrow screen: sidebar collapses */ /* Narrow screen: sidebar collapses */
@media (max-width: 720px) { @media (max-width: 720px) {
.note-body { flex-direction: column; overflow-y: auto; overflow-x: hidden; } .note-body { flex-direction: column; overflow-y: auto; overflow-x: hidden; }