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:
@@ -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,48 +384,96 @@ 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()">
|
||||||
<div class="body-tabs-row">
|
<!-- ── Person form ──────────────────────────────────────── -->
|
||||||
<div class="editor-tabs">
|
<template v-if="noteType === 'person'">
|
||||||
<button :class="['tab', { active: !showPreview }]" @click="showPreview = false">Write</button>
|
<div class="entity-form">
|
||||||
<button :class="['tab', { active: showPreview }]" @click="showPreview = true">Preview</button>
|
<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>
|
||||||
<MarkdownToolbar v-show="!showPreview && assist.state.value === 'idle'" :editor="tiptapEditor" />
|
<div class="notes-section">
|
||||||
</div>
|
<button class="notes-toggle" @click="notesExpanded = !notesExpanded">
|
||||||
|
{{ notesExpanded ? '▾' : '▸' }} Notes
|
||||||
<!-- Streaming preview -->
|
</button>
|
||||||
<template v-if="assist.state.value === 'streaming'">
|
<div v-if="notesExpanded" class="notes-editor-wrap">
|
||||||
<div class="stream-label">Generating...</div>
|
<MarkdownToolbar v-show="!showPreview" :editor="tiptapEditor" />
|
||||||
<div class="stream-preview prose" v-html="renderMarkdown(assist.streamingText.value)" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Review: full-document diff -->
|
|
||||||
<template v-else-if="assist.state.value === 'review'">
|
|
||||||
<DiffView :diff="assist.diff.value" class="main-diff" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Normal editor -->
|
|
||||||
<template v-else>
|
|
||||||
<Transition name="tab-fade" mode="out-in">
|
|
||||||
<div v-if="!showPreview" class="body-editor-wrap">
|
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
ref="editorRef"
|
ref="editorRef"
|
||||||
:modelValue="body"
|
:modelValue="body"
|
||||||
placeholder="Write your note in Markdown..."
|
placeholder="Additional notes, wikilinks, context..."
|
||||||
@update:modelValue="onBodyUpdate"
|
@update:modelValue="onBodyUpdate"
|
||||||
@selectionChange="onSelectionChange"
|
|
||||||
@escape="titleRef?.focus()"
|
@escape="titleRef?.focus()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
v-else
|
|
||||||
class="preview-pane prose"
|
|
||||||
v-html="renderedPreview"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Error from assist -->
|
<!-- ── Generic note editor ──────────────────────────────── -->
|
||||||
<div v-if="assist.error.value" class="assist-error">{{ assist.error.value }}</div>
|
<template v-else>
|
||||||
|
<div class="body-tabs-row">
|
||||||
|
<div class="editor-tabs">
|
||||||
|
<button :class="['tab', { active: !showPreview }]" @click="showPreview = false">Write</button>
|
||||||
|
<button :class="['tab', { active: showPreview }]" @click="showPreview = true">Preview</button>
|
||||||
|
</div>
|
||||||
|
<MarkdownToolbar v-show="!showPreview && assist.state.value === 'idle'" :editor="tiptapEditor" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Streaming preview -->
|
||||||
|
<template v-if="assist.state.value === 'streaming'">
|
||||||
|
<div class="stream-label">Generating...</div>
|
||||||
|
<div class="stream-preview prose" v-html="renderMarkdown(assist.streamingText.value)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Review: full-document diff -->
|
||||||
|
<template v-else-if="assist.state.value === 'review'">
|
||||||
|
<DiffView :diff="assist.diff.value" class="main-diff" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Normal editor -->
|
||||||
|
<template v-else>
|
||||||
|
<Transition name="tab-fade" mode="out-in">
|
||||||
|
<div v-if="!showPreview" class="body-editor-wrap">
|
||||||
|
<TiptapEditor
|
||||||
|
ref="editorRef"
|
||||||
|
:modelValue="body"
|
||||||
|
placeholder="Write your note in Markdown..."
|
||||||
|
@update:modelValue="onBodyUpdate"
|
||||||
|
@selectionChange="onSelectionChange"
|
||||||
|
@escape="titleRef?.focus()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="preview-pane prose"
|
||||||
|
v-html="renderedPreview"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Error from assist -->
|
||||||
|
<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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user