# Specialized Note Type Editors — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the generic note editor with type-specialized form-first views for Person, Place, and List, and fix tab navigation so focus flows logically from title through content fields, skipping the formatting toolbar.

**Architecture:** `NoteEditorView.vue` gains type-conditional template sections. When `noteType` is `person` or `place`, the main editor area renders a structured form with the TipTap editor in a secondary "Notes" section. When `noteType` is `list`, a dedicated list builder replaces TipTap as the primary interface. `MarkdownToolbar.vue` gets `tabindex="-1"` on buttons. Backend `_note_to_item` gains new person/place fields.

**Tech Stack:** Vue 3 Composition API, TipTap editor, TypeScript, scoped CSS.

---

## File Map

| Action | Path |
|--------|------|
| Modify | `frontend/src/views/NoteEditorView.vue` |
| Modify | `frontend/src/components/MarkdownToolbar.vue` |
| Modify | `frontend/src/views/KnowledgeView.vue` |
| Modify | `src/fabledassistant/services/knowledge.py` |

---

### Task 1: Tab navigation fix — toolbar tabindex + auto-focus

**Files:**
- Modify: `frontend/src/components/MarkdownToolbar.vue`
- Modify: `frontend/src/views/NoteEditorView.vue`

**Context:** The MarkdownToolbar renders buttons via `v-for` in a single `<button>` element. Adding `tabindex="-1"` removes them from tab order while keeping them clickable. The NoteEditorView already has a `titleRef` — auto-focus on mount needs to call `.focus()` on it. The title placeholder should vary by note type.

- [ ] **Step 1: Add tabindex="-1" to toolbar buttons**

In `frontend/src/components/MarkdownToolbar.vue`, find:

```html
        <button
          v-for="btn in group"
          :key="btn.id"
          :class="['md-btn', { active: btn.isActive() }]"
          :title="btn.title"
          type="button"
          @mousedown.prevent="btn.command()"
        >
```

Replace with:

```html
        <button
          v-for="btn in group"
          :key="btn.id"
          :class="['md-btn', { active: btn.isActive() }]"
          :title="btn.title"
          type="button"
          tabindex="-1"
          @mousedown.prevent="btn.command()"
        >
```

- [ ] **Step 2: Add auto-focus on mount and type-dependent placeholder**

In `frontend/src/views/NoteEditorView.vue`, find the title input:

```html
      <input
        ref="titleRef"
        v-model="title"
        type="text"
        placeholder="Title"
        class="title-input"
```

Replace with:

```html
      <input
        ref="titleRef"
        v-model="title"
        type="text"
        :placeholder="titlePlaceholder"
        class="title-input"
```

Add the computed property in the `<script setup>` section, after the `isEditing` computed:

```ts
const titlePlaceholder = computed(() => {
  switch (noteType.value) {
    case 'person': return 'Name';
    case 'place': return 'Place name';
    case 'list': return 'List title';
    default: return 'Title';
  }
});
```

- [ ] **Step 3: Auto-focus title on mount**

In the `onMounted` callback, after all the data loading logic (after the draft restore try/catch block), add:

```ts
  await nextTick();
  titleRef.value?.focus();
```

- [ ] **Step 4: Verify TypeScript compiles**

```bash
cd frontend && npx tsc --noEmit
```

- [ ] **Step 5: Commit**

```bash
git add frontend/src/components/MarkdownToolbar.vue frontend/src/views/NoteEditorView.vue
git commit -m "feat(editor): skip toolbar in tab order; auto-focus title; type-dependent placeholders"
```

---

### Task 2: Person editor — form-first layout

**Files:**
- Modify: `frontend/src/views/NoteEditorView.vue`

**Context:** When `noteType === 'person'`, the main content area should render a contact card form instead of the TipTap-first editor. The person metadata fields (currently in the sidebar) move to the main area, and new fields (birthday, organization, address) are added. The TipTap editor becomes a collapsible "Notes" section below. The sidebar keeps project/tags/type/etc but loses the person-specific fields.

- [ ] **Step 1: Add the person form template**

In the template, find the `<!-- ── Main column ──` section. The current structure is:

```html
      <div class="note-main" @keydown.ctrl.e.prevent="tiptapEditor?.commands.focus()">
        <div class="body-tabs-row">
          ...
        </div>
        <!-- Streaming/Review/Normal editor templates -->
      </div>
```

Wrap the existing main column content in a `v-if="noteType === 'note'"` (and also show it for any type not person/place/list), and add a person form block. Replace the opening of the main column content:

Find the `<div class="note-main"` line and the content inside it up to `</div>` that closes `.note-main`. Wrap all existing content inside:

```html
      <div class="note-main">
        <!-- ── 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 (existing) ───────────────────── -->
        <template v-else-if="noteType === 'note'">
          <!-- ... existing TipTap-first editor content stays here ... -->
        </template>
      </div>
```

IMPORTANT: Do NOT duplicate the existing editor content. Wrap the existing content in `<template v-else-if="noteType === 'note'">` and place the person form as a sibling `<template>` above it. The place and list forms will be added in subsequent tasks.

- [ ] **Step 2: Add `notesExpanded` ref**

In the `<script setup>`, after the `sidebarOpen` ref, add:

```ts
const notesExpanded = ref(false);
```

Also initialize it based on whether the note has body content, in the onMounted data-loading section. After `Object.assign(entityMeta, store.currentNote.metadata || {});` add:

```ts
      notesExpanded.value = !!(store.currentNote.body || '').trim();
```

- [ ] **Step 3: Remove person fields from sidebar**

In the sidebar template, find:

```html
          <!-- 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>
```

Delete this entire block.

- [ ] **Step 4: Add entity form CSS**

Add to the `<style scoped>` block:

```css
/* ── 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;
}
```

- [ ] **Step 5: Verify TypeScript compiles**

```bash
cd frontend && npx tsc --noEmit
```

- [ ] **Step 6: Commit**

```bash
git add frontend/src/views/NoteEditorView.vue
git commit -m "feat(editor): person form-first layout with structured fields and collapsible notes"
```

---

### Task 3: Place editor + List builder

**Files:**
- Modify: `frontend/src/views/NoteEditorView.vue`

**Context:** Place uses the same entity form pattern as Person with different fields. List uses a dedicated checklist builder with Enter-to-add and Backspace-to-delete behavior. Both are additional `<template>` branches in the main column.

- [ ] **Step 1: Add place form template**

In the `note-main` div, after the person `</template>` and before the generic note `<template v-else-if="noteType === 'note'">`, add:

```html
        <!-- ── Place form ───────────────────────────────────────── -->
        <template v-else-if="noteType === 'place'">
          <div class="entity-form">
            <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 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">Hours</label>
              <input class="ef-input" v-model="entityMeta.hours" placeholder="e.g. Mon–Fri 9am–5pm" @input="markDirty" />
            </div>
            <div class="ef-field">
              <label class="ef-label">Website</label>
              <input class="ef-input" type="url" v-model="entityMeta.website" placeholder="https://..." @input="markDirty" />
            </div>
            <div class="ef-field">
              <label class="ef-label">Category</label>
              <input class="ef-input" v-model="entityMeta.category" placeholder="e.g. Restaurant, Office, Doctor" @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>
```

- [ ] **Step 2: Remove place fields from sidebar**

Find and delete:

```html
          <!-- Place metadata -->
          <template v-if="noteType === 'place'">
            <div class="sb-field">
              <label class="sb-label">Address</label>
              <input class="sb-input" v-model="entityMeta.address" placeholder="Street, City" @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>
            <div class="sb-field">
              <label class="sb-label">Hours</label>
              <input class="sb-input" v-model="entityMeta.hours" placeholder="e.g. Mon–Fri 9–5" @input="markDirty" />
            </div>
          </template>
```

- [ ] **Step 3: Add list item types and state**

In the `<script setup>`, after the `notesExpanded` ref, add:

```ts
// ── List builder ─────────────────────────────────────────────────────────────
interface ListItem {
  text: string;
  checked: boolean;
}
const listItems = ref<ListItem[]>([]);
const listItemRefs = ref<(HTMLInputElement | null)[]>([]);

function parseListFromBody(bodyText: string): { items: ListItem[]; extra: string } {
  const lines = bodyText.split('\n');
  const items: ListItem[] = [];
  const extraLines: string[] = [];
  let pastList = false;
  for (const line of lines) {
    const stripped = line.trimStart();
    if (!pastList && (stripped.startsWith('- [ ] ') || stripped.startsWith('- [x] ') || stripped.startsWith('- [X] '))) {
      items.push({ text: stripped.slice(6), checked: !stripped.startsWith('- [ ] ') });
    } else if (!pastList && stripped === '' && items.length > 0) {
      pastList = true;
    } else {
      pastList = true;
      extraLines.push(line);
    }
  }
  return { items, extra: extraLines.join('\n').trim() };
}

function serializeListToBody(): string {
  const listPart = listItems.value
    .map(item => `- [${item.checked ? 'x' : ' '}] ${item.text}`)
    .join('\n');
  const extraPart = body.value.trim();
  return extraPart ? `${listPart}\n\n${extraPart}` : listPart;
}

function addListItem(afterIndex?: number) {
  const idx = afterIndex !== undefined ? afterIndex + 1 : listItems.value.length;
  listItems.value.splice(idx, 0, { text: '', checked: false });
  markDirty();
  nextTick(() => {
    listItemRefs.value[idx]?.focus();
  });
}

function removeListItem(index: number) {
  if (listItems.value.length <= 1) return;
  listItems.value.splice(index, 1);
  markDirty();
  nextTick(() => {
    const focusIdx = Math.max(0, index - 1);
    listItemRefs.value[focusIdx]?.focus();
  });
}

function onListItemKeydown(e: KeyboardEvent, index: number) {
  if (e.key === 'Enter') {
    e.preventDefault();
    addListItem(index);
  } else if (e.key === 'Backspace' && listItems.value[index].text === '') {
    e.preventDefault();
    removeListItem(index);
  }
}

function onListItemInput(index: number) {
  markDirty();
}

function toggleListItemCheck(index: number) {
  listItems.value[index].checked = !listItems.value[index].checked;
  markDirty();
}
```

- [ ] **Step 4: Initialize list items on mount**

In the onMounted data-loading section, after `notesExpanded.value = !!(store.currentNote.body || '').trim();`, add:

```ts
      if (noteType.value === 'list') {
        const parsed = parseListFromBody(body.value);
        listItems.value = parsed.items.length > 0 ? parsed.items : [{ text: '', checked: false }];
        body.value = parsed.extra;
        notesExpanded.value = !!parsed.extra;
      }
```

And in the new-note branch (the `else` block after loading), after `noteType.value = qt as NoteType;`, add:

```ts
    if (noteType.value === 'list') {
      listItems.value = [{ text: '', checked: false }];
    }
```

- [ ] **Step 5: Update save to serialize list**

In the `save` function, find where the body is prepared for the API call. Before the `apiPost` or `apiPatch` call that sends the note data, add list serialization. Find the save function's data construction. Add before the API call:

```ts
  const finalBody = noteType.value === 'list' ? serializeListToBody() : body.value;
```

Then use `finalBody` instead of `body.value` in the API payload. Find all occurrences of `body: body.value` in the save function and replace with `body: finalBody`.

- [ ] **Step 6: Add list builder template**

In the `note-main` div, after the place `</template>` and before the generic note `<template v-else-if="noteType === 'note'">`, add:

```html
        <!-- ── List builder ─────────────────────────────────────── -->
        <template v-else-if="noteType === 'list'">
          <div class="list-builder">
            <div
              v-for="(item, idx) in listItems"
              :key="idx"
              class="lb-item"
            >
              <input
                type="checkbox"
                :checked="item.checked"
                @change="toggleListItemCheck(idx)"
                class="lb-check"
                tabindex="-1"
              />
              <input
                :ref="(el) => { listItemRefs[idx] = el as HTMLInputElement | null }"
                v-model="item.text"
                class="lb-text"
                placeholder="List item..."
                @keydown="onListItemKeydown($event, idx)"
                @input="onListItemInput(idx)"
              />
              <button class="lb-delete" tabindex="-1" @click="removeListItem(idx)" title="Remove item">&times;</button>
            </div>
            <button class="lb-add" @click="addListItem()">+ Add item</button>
          </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, context..."
                @update:modelValue="onBodyUpdate"
                @escape="titleRef?.focus()"
              />
            </div>
          </div>
        </template>
```

- [ ] **Step 7: Add list builder CSS**

Add to the `<style scoped>` block:

```css
/* ── List builder ───────────────────────────────────────── */
.list-builder {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 0;
}
.lb-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.lb-check {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  accent-color: var(--color-primary);
  cursor: pointer;
}
.lb-text {
  flex: 1;
  padding: 7px 10px;
  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;
}
.lb-text:focus {
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}
.lb-text::placeholder {
  color: var(--color-text-muted);
}
.lb-delete {
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 1.1rem;
  cursor: pointer;
  padding: 0 4px;
  line-height: 1;
  opacity: 0;
  transition: opacity 0.12s, color 0.12s;
}
.lb-item:hover .lb-delete,
.lb-text:focus ~ .lb-delete {
  opacity: 1;
}
.lb-delete:hover {
  color: var(--color-danger);
}
.lb-add {
  background: none;
  border: 1px dashed var(--color-border);
  border-radius: 8px;
  padding: 7px 12px;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  cursor: pointer;
  margin-top: 4px;
  transition: border-color 0.15s, color 0.15s;
}
.lb-add:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}
```

- [ ] **Step 8: Handle the generic note template wrapper**

Make sure the existing TipTap-first editor content is wrapped in `<template v-else>` (not `v-else-if="noteType === 'note'"`) so it serves as the default for any unrecognized type.

The final structure in `.note-main` should be:

```
<template v-if="noteType === 'person'"> ... </template>
<template v-else-if="noteType === 'place'"> ... </template>
<template v-else-if="noteType === 'list'"> ... </template>
<template v-else> ... existing TipTap editor ... </template>
```

- [ ] **Step 9: Verify TypeScript compiles**

```bash
cd frontend && npx tsc --noEmit
```

- [ ] **Step 10: Commit**

```bash
git add frontend/src/views/NoteEditorView.vue
git commit -m "feat(editor): place form-first layout and list builder with Enter-to-add"
```

---

### Task 4: Backend — new person/place fields in knowledge cards

**Files:**
- Modify: `src/fabledassistant/services/knowledge.py`
- Modify: `frontend/src/views/KnowledgeView.vue`

**Context:** The knowledge card display should show the new fields (birthday, organization for person; website, category for place). The backend `_note_to_item` needs to include them. The frontend card rendering needs to display the useful ones.

- [ ] **Step 1: Update `_note_to_item` for person**

In `src/fabledassistant/services/knowledge.py`, find:

```python
    if note.entity_type == "person":
        item["relationship"] = meta.get("relationship", "")
        item["email"] = meta.get("email", "")
        item["phone"] = meta.get("phone", "")
```

Replace with:

```python
    if note.entity_type == "person":
        item["relationship"] = meta.get("relationship", "")
        item["email"] = meta.get("email", "")
        item["phone"] = meta.get("phone", "")
        item["birthday"] = meta.get("birthday", "")
        item["organization"] = meta.get("organization", "")
        item["address"] = meta.get("address", "")
```

- [ ] **Step 2: Update `_note_to_item` for place**

Find:

```python
    elif note.entity_type == "place":
        item["address"] = meta.get("address", "")
        item["phone"] = meta.get("phone", "")
        item["hours"] = meta.get("hours", "")
```

Replace with:

```python
    elif note.entity_type == "place":
        item["address"] = meta.get("address", "")
        item["phone"] = meta.get("phone", "")
        item["hours"] = meta.get("hours", "")
        item["website"] = meta.get("website", "")
        item["category"] = meta.get("category", "")
```

- [ ] **Step 3: Update KnowledgeItem interface**

In `frontend/src/views/KnowledgeView.vue`, find the `KnowledgeItem` interface and add the new fields:

After `phone?: string;` add:
```ts
  birthday?: string;
  organization?: string;
```

After `hours?: string;` add:
```ts
  website?: string;
  category?: string;
```

- [ ] **Step 4: Update person card display**

In the template, find the person card specifics:

```html
              <div v-if="item.note_type === 'person'" class="k-card-meta">
                <span v-if="item.relationship" class="meta-chip">{{ item.relationship }}</span>
                <span v-if="item.phone" class="meta-muted">{{ item.phone }}</span>
              </div>
```

Replace with:

```html
              <div v-if="item.note_type === 'person'" class="k-card-meta">
                <span v-if="item.relationship" class="meta-chip">{{ item.relationship }}</span>
                <span v-if="item.organization" class="meta-muted">{{ item.organization }}</span>
                <span v-if="item.phone" class="meta-muted">{{ item.phone }}</span>
              </div>
```

- [ ] **Step 5: Update place card display**

Find:

```html
              <div v-else-if="item.note_type === 'place'" class="k-card-meta">
                <span v-if="item.address" class="meta-muted">{{ item.address }}</span>
                <span v-if="item.hours" class="meta-muted">{{ item.hours }}</span>
              </div>
```

Replace with:

```html
              <div v-else-if="item.note_type === 'place'" class="k-card-meta">
                <span v-if="item.category" class="meta-chip">{{ item.category }}</span>
                <span v-if="item.address" class="meta-muted">{{ item.address }}</span>
                <span v-if="item.hours" class="meta-muted">{{ item.hours }}</span>
              </div>
```

- [ ] **Step 6: Verify TypeScript compiles and backend syntax**

```bash
cd frontend && npx tsc --noEmit
python -c "import ast; ast.parse(open('src/fabledassistant/services/knowledge.py').read()); print('OK')"
```

- [ ] **Step 7: Commit**

```bash
git add src/fabledassistant/services/knowledge.py frontend/src/views/KnowledgeView.vue
git commit -m "feat(knowledge): show organization/birthday for person cards, category for place cards"
```
