feat: focus item detail page with inline note + checklist

Each standalone focus item now auto-creates a linked note and checklist.
Clicking a focus item opens a detail page with side-by-side note editor
(left) and checklist (right) with drag-to-reorder. Save & Return writes
the note and goes back to the focus list. Added focus_id FK to notes and
lists tables, made domain optional when creating from focus context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 20:02:27 +00:00
parent a2183af6e2
commit 6abef336c4
9 changed files with 365 additions and 17 deletions

View File

@@ -52,7 +52,7 @@
</td>
<td style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
<td style="padding:1px 1px;vertical-align:middle;">{% if item.task_id %}<span class="priority-dot priority-{{ item.priority }}"></span>{% elif item.list_item_id %}<span style="color:var(--muted);font-size:0.85rem;">&#9776;</span>{% else %}<span style="color:var(--muted);font-size:0.85rem;">&#9679;</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.task_title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.task_title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% else %}<a href="/focus/{{ item.id }}/edit" class="focus-title">{{ item.title }}</a>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.task_title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.task_title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% else %}<a href="/focus/{{ item.id }}" class="focus-title">{{ item.title }}</a>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.area_name %}<span class="row-tag">{{ item.area_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.project_name %}<span class="row-tag" style="background:var(--accent-soft);color:var(--accent)">{{ item.project_name }}</span>{% elif item.list_item_id and item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;text-align:right;color:var(--muted);font-size:0.78rem;">{{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }}</td>

173
templates/focus_detail.html Normal file
View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% block content %}
<div class="breadcrumb">
<a href="/focus">Focus</a>
<span class="sep">/</span>
<span>{{ item.title }}</span>
</div>
<div class="detail-header">
<div class="flex items-center justify-between">
<h1 class="detail-title" style="{{ 'text-decoration:line-through;opacity:0.6;' if item.completed }}">{{ item.title }}</h1>
<div class="flex gap-2">
<a href="/focus/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
<button class="btn {{ 'btn-secondary' if item.completed else 'btn-primary' }} btn-sm">
{{ 'Reopen' if item.completed else 'Complete' }}
</button>
</form>
<form action="/focus/{{ item.id }}/remove" method="post" data-confirm="Delete this focus item?" style="display:inline">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</div>
</div>
<div class="detail-meta mt-2">
{% if item.completed %}<span class="status-badge status-done">completed</span>{% endif %}
{% if domain %}<span class="row-domain-tag" style="background: {{ domain.color or '#4F6EF7' }}22; color: {{ domain.color or '#4F6EF7' }}">{{ domain.name }}</span>{% endif %}
{% if project %}<span class="row-tag">{{ project.name }}</span>{% endif %}
</div>
</div>
<!-- Convert row -->
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:16px;padding:8px 0;">
<span style="font-size:0.78rem;color:var(--muted);font-weight:600;">Convert to:</span>
<form action="/focus/{{ item.id }}/convert-to-task" method="post" data-confirm="Convert to task?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs">Task</button>
</form>
<form action="/focus/{{ item.id }}/convert-to-note" method="post" data-confirm="Convert to note?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs">Note</button>
</form>
<form action="/focus/{{ item.id }}/convert-to-link" method="post" data-confirm="Convert to link?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs">Link</button>
</form>
<form action="/focus/{{ item.id }}/convert-to-list-item" method="post" data-confirm="Add to selected list?" style="display:inline-flex;gap:4px;align-items:end;">
<select name="list_id" class="form-select" style="min-width:140px;height:28px;font-size:12px;" required>
<option value="">Select list...</option>
{% for l in all_lists %}
<option value="{{ l.id }}">{{ l.name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-ghost btn-xs">List Item</button>
</form>
</div>
<!-- Side-by-side: Note (left) + List (right) -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">
<!-- Note panel -->
<div class="card" style="padding:0;">
<div style="padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:600;font-size:0.82rem;color:var(--text);">Notes</span>
</div>
<form action="/focus/{{ item.id }}/save-note" method="post">
<input type="hidden" name="note_id" value="{{ note.id }}">
<textarea name="body" style="width:100%;min-height:320px;padding:10px 12px;border:none;background:transparent;color:var(--text);font-family:var(--font-mono);font-size:0.85rem;resize:vertical;outline:none;box-sizing:border-box;" placeholder="Start typing notes...">{{ note.body if note.body else '' }}</textarea>
<div style="padding:8px 12px;border-top:1px solid var(--border);display:flex;gap:8px;">
<button type="submit" class="btn btn-primary btn-sm">Save &amp; Return</button>
<a href="/focus" class="btn btn-ghost btn-sm">Back</a>
</div>
</form>
</div>
<!-- List panel -->
<div class="card" style="padding:0;">
<div style="padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:600;font-size:0.82rem;color:var(--text);">Checklist</span>
<span style="font-size:0.72rem;color:var(--muted);">{{ list_items|length }} item{{ 's' if list_items|length != 1 }}</span>
</div>
<!-- Add item -->
<form action="/focus/{{ item.id }}/list-item/add" method="post" style="display:flex;gap:6px;padding:8px 12px;border-bottom:1px solid var(--border);">
<input type="hidden" name="list_id" value="{{ list_id }}">
<input type="text" name="content" class="form-input" placeholder="Add item..." required style="flex:1;padding:5px 8px;font-size:0.82rem;">
<button type="submit" class="btn btn-primary btn-sm">+</button>
</form>
<!-- Items -->
<div id="focus-checklist" style="max-height:340px;overflow-y:auto;">
{% for li in list_items %}
<div class="list-row focus-cl-row" draggable="true" data-id="{{ li.id }}" style="padding:4px 8px;min-height:0;border-bottom:1px solid var(--border);{{ 'opacity:0.5;' if li.completed }}cursor:grab;">
<span class="reorder-grip" style="margin-right:4px;color:var(--muted);font-size:0.7rem;cursor:grab;">&#x2630;</span>
<form action="/focus/{{ item.id }}/list-item/{{ li.id }}/toggle" method="post" style="display:inline">
<div class="row-check" style="margin-right:6px;">
<input type="checkbox" id="li-{{ li.id }}" {{ 'checked' if li.completed }} onchange="this.form.submit()">
<label for="li-{{ li.id }}"></label>
</div>
</form>
<span style="flex:1;font-size:0.82rem;{{ 'text-decoration:line-through;' if li.completed }}">{{ li.content }}</span>
<form action="/focus/{{ item.id }}/list-item/{{ li.id }}/delete" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" style="color:var(--red);padding:0 4px;" title="Remove">&times;</button>
</form>
</div>
{% else %}
<div style="padding:16px 12px;color:var(--muted);font-size:0.82rem;text-align:center;">No items yet</div>
{% endfor %}
</div>
<form id="cl-reorder-form" action="/focus/{{ item.id }}/list-item/reorder-all" method="post" style="display:none;">
<input type="hidden" name="item_ids" id="cl-reorder-ids">
</form>
</div>
</div>
<script>
(function() {
var container = document.getElementById('focus-checklist');
if (!container) return;
var dragRow = null;
container.querySelectorAll('.focus-cl-row').forEach(function(row) {
row.addEventListener('dragstart', function(e) {
dragRow = row;
row.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', row.dataset.id);
});
row.addEventListener('dragend', function() {
row.classList.remove('dragging');
container.querySelectorAll('.focus-cl-row.drag-over').forEach(function(el) {
el.classList.remove('drag-over');
});
if (dragRow) {
var allIds = [];
container.querySelectorAll('.focus-cl-row').forEach(function(r) {
allIds.push(r.dataset.id);
});
document.getElementById('cl-reorder-ids').value = allIds.join(',');
document.getElementById('cl-reorder-form').submit();
}
dragRow = null;
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
e.dataTransfer.dropEffect = 'move';
container.querySelectorAll('.focus-cl-row.drag-over').forEach(function(el) {
el.classList.remove('drag-over');
});
row.classList.add('drag-over');
});
row.addEventListener('dragleave', function() {
row.classList.remove('drag-over');
});
row.addEventListener('drop', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
row.classList.remove('drag-over');
var rows = Array.from(container.querySelectorAll('.focus-cl-row'));
var dragIdx = rows.indexOf(dragRow);
var targetIdx = rows.indexOf(row);
if (dragIdx < targetIdx) {
container.insertBefore(dragRow, row.nextSibling);
} else {
container.insertBefore(dragRow, row);
}
});
});
})();
</script>
{% endblock %}

View File

@@ -3,7 +3,9 @@
<div class="breadcrumb">
<a href="/focus">Focus</a>
<span class="sep">/</span>
<span>Edit Item</span>
<a href="/focus/{{ item.id }}">{{ item.title or 'Item' }}</a>
<span class="sep">/</span>
<span>Edit</span>
</div>
<div class="page-header">
@@ -45,7 +47,7 @@
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="/focus" class="btn btn-secondary">Cancel</a>
<a href="/focus/{{ item.id }}" class="btn btn-secondary">Cancel</a>
</div>
</div>
</form>

View File

@@ -14,9 +14,9 @@
</div>
<div class="form-group">
<label class="form-label">Domain *</label>
<select name="domain_id" class="form-select" required>
<option value="">Select domain...</option>
<label class="form-label">Domain{{ ' *' if not (prefill_focus_id is defined and prefill_focus_id) }}</label>
<select name="domain_id" class="form-select" {{ 'required' if not (prefill_focus_id is defined and prefill_focus_id) }}>
<option value="">-- None --</option>
{% for d in domains %}
<option value="{{ d.id }}"
{{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>
@@ -75,9 +75,10 @@
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
{% if prefill_focus_id is defined and prefill_focus_id %}<input type="hidden" name="focus_id" value="{{ prefill_focus_id }}">{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create List' }}</button>
<a href="{{ '/tasks/' ~ prefill_task_id ~ '?tab=lists' if prefill_task_id is defined and prefill_task_id else ('/lists/' ~ item.id if item else '/lists') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ '/focus/' ~ prefill_focus_id ~ '?tab=lists' if prefill_focus_id is defined and prefill_focus_id else ('/tasks/' ~ prefill_task_id ~ '?tab=lists' if prefill_task_id is defined and prefill_task_id else ('/lists/' ~ item.id if item else '/lists')) }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -6,8 +6,8 @@
<form method="post" action="{{ '/notes/' ~ item.id ~ '/edit' if item else '/notes/create' }}">
<div class="form-grid">
<div class="form-group full-width"><label class="form-label">Title *</label><input type="text" name="title" class="form-input" required value="{{ item.title if item else '' }}"></div>
<div class="form-group"><label class="form-label">Domain *</label>
<select name="domain_id" class="form-select" required>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
<div class="form-group"><label class="form-label">Domain{{ ' *' if not (prefill_focus_id is defined and prefill_focus_id) }}</label>
<select name="domain_id" class="form-select" {{ 'required' if not (prefill_focus_id is defined and prefill_focus_id) }}><option value="">-- None --</option>{% for d in domains %}<option value="{{ d.id }}" {{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>{{ d.name }}</option>{% endfor %}</select></div>
<div class="form-group"><label class="form-label">Project</label>
<select name="project_id" class="form-select"><option value="">-- None --</option>{% for p in projects %}<option value="{{ p.id }}" {{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>{{ p.name }}</option>{% endfor %}</select></div>
<div class="form-group full-width"><label class="form-label">Content</label><textarea name="body" class="form-textarea" rows="15" style="font-family:var(--font-mono);font-size:0.88rem">{{ item.body if item and item.body else '' }}</textarea></div>
@@ -15,7 +15,8 @@
<input type="hidden" name="content_format" value="rich">
{% if prefill_task_id is defined and prefill_task_id %}<input type="hidden" name="task_id" value="{{ prefill_task_id }}">{% endif %}
{% if prefill_meeting_id is defined and prefill_meeting_id %}<input type="hidden" name="meeting_id" value="{{ prefill_meeting_id }}">{% endif %}
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="{{ '/notes/' ~ item.id if item else '/notes' }}" class="btn btn-secondary">Cancel</a></div>
{% if prefill_focus_id is defined and prefill_focus_id %}<input type="hidden" name="focus_id" value="{{ prefill_focus_id }}">{% endif %}
<div class="form-actions"><button type="submit" class="btn btn-primary">{{ 'Save' if item else 'Create' }}</button><a href="{{ '/focus/' ~ prefill_focus_id ~ '?tab=notes' if prefill_focus_id is defined and prefill_focus_id else ('/notes/' ~ item.id if item else '/notes') }}" class="btn btn-secondary">Cancel</a></div>
</div>
</form></div>
{% endblock %}