- Focus page: turn sequence number into persistent editable priority (focus_priority column) - Focus detail: add links section (add existing, create new, unlink) via focus_links junction table - Focus detail: add copy and inline edit for checklist items - Task detail lists tab: add existing list assignment and unlink actions - Lists page: add drag-and-drop reorder support - Links/bookmarks pages: remove artificial URL truncation, use CSS ellipsis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
13 KiB
HTML
259 lines
13 KiB
HTML
{% 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 & 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;">☰</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 class="cl-display-{{ li.id }}" style="flex:1;font-size:0.82rem;{{ 'text-decoration:line-through;' if li.completed }}">{{ li.content }}</span>
|
|
<form class="cl-edit-{{ li.id }}" action="/focus/{{ item.id }}/list-item/{{ li.id }}/edit" method="post" style="display:none;flex:1;gap:4px;align-items:center;">
|
|
<input type="text" name="content" value="{{ li.content }}" class="form-input" style="flex:1;padding:2px 6px;font-size:0.82rem;" required>
|
|
<button type="submit" class="btn btn-primary btn-xs">Save</button>
|
|
<button type="button" class="btn btn-ghost btn-xs cl-edit-cancel" data-id="{{ li.id }}">Cancel</button>
|
|
</form>
|
|
<button type="button" class="btn btn-ghost btn-xs cl-copy-btn" data-text="{{ li.content|e }}" title="Copy" style="color:var(--muted);padding:0 3px;">📋</button>
|
|
<button type="button" class="btn btn-ghost btn-xs cl-edit-btn" data-id="{{ li.id }}" title="Edit" style="color:var(--muted);padding:0 3px;">✎</button>
|
|
<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">×</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>
|
|
|
|
<!-- Links -->
|
|
<div class="card mt-4">
|
|
<div class="card-header">
|
|
<h2 class="card-title">Links</h2>
|
|
</div>
|
|
<form action="/focus/{{ item.id }}/links/add" method="post" class="flex gap-2 items-end" style="padding: 12px; border-bottom: 1px solid var(--border);">
|
|
<div class="form-group" style="flex:2; margin:0;">
|
|
<label class="form-label">Link</label>
|
|
<select name="link_id" class="form-select" required>
|
|
<option value="">Select link...</option>
|
|
{% for l in all_links %}
|
|
<option value="{{ l.id }}">{{ l.label }} — {{ l.url[:40] }}{% if l.url|length > 40 %}...{% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group" style="flex:1; margin:0;">
|
|
<label class="form-label">Role</label>
|
|
<input type="text" name="role" class="form-input" list="link-roles" placeholder="e.g. reference, docs...">
|
|
<datalist id="link-roles">
|
|
<option value="website">
|
|
<option value="documentation">
|
|
<option value="reference">
|
|
<option value="repository">
|
|
<option value="social">
|
|
<option value="resource">
|
|
</datalist>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-sm">Add</button>
|
|
<a href="/links/create?focus_id={{ item.id }}" class="btn btn-ghost btn-sm">+ New Link</a>
|
|
</form>
|
|
{% for l in linked_links %}
|
|
<div class="list-row">
|
|
<span class="row-title"><a href="{{ l.url }}" target="_blank">{{ l.label }}</a></span>
|
|
<span class="row-meta" style="min-width:0;overflow:hidden;text-overflow:ellipsis;">{{ l.url }}</span>
|
|
{% if l.role %}<span class="row-tag">{{ l.role }}</span>{% endif %}
|
|
<div class="row-actions">
|
|
<a href="/links/{{ l.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
|
|
<form action="/focus/{{ item.id }}/links/{{ l.id }}/remove" method="post" style="display:inline">
|
|
<button class="btn btn-ghost btn-xs">Unlink</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-sm text-muted" style="padding: 12px;">No links</div>
|
|
{% endfor %}
|
|
</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);
|
|
}
|
|
});
|
|
});
|
|
})();
|
|
|
|
// Copy list item text
|
|
document.querySelectorAll('.cl-copy-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var text = btn.getAttribute('data-text');
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
var orig = btn.innerHTML;
|
|
btn.innerHTML = '✓';
|
|
btn.style.color = 'var(--green)';
|
|
setTimeout(function() { btn.innerHTML = orig; btn.style.color = 'var(--muted)'; }, 1200);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Edit list item inline
|
|
document.querySelectorAll('.cl-edit-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var id = btn.dataset.id;
|
|
document.querySelector('.cl-display-' + id).style.display = 'none';
|
|
var form = document.querySelector('.cl-edit-' + id);
|
|
form.style.display = 'flex';
|
|
form.querySelector('input').focus();
|
|
});
|
|
});
|
|
document.querySelectorAll('.cl-edit-cancel').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var id = btn.dataset.id;
|
|
document.querySelector('.cl-display-' + id).style.display = '';
|
|
document.querySelector('.cl-edit-' + id).style.display = 'none';
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|