Compare commits

...

6 Commits

Author SHA1 Message Date
f88c6e5fd4 fix: notes preview text wrapping and right margin
Add word-break/overflow-wrap to .detail-body so long text wraps
properly, and add right margin inside the preview card for balanced
spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:27:49 +00:00
c42fe9dc13 fix: consistent row styling for capture and focus items
- Remove card-style spacing (margin, border-radius, background) from capture-item and focus-item
- Use border-bottom pattern matching list-row for uniform density
- Wrap capture and focus items in .card containers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:57:40 +00:00
34b232de5f feat: consistent compact density across all list views
- Reduce capture-item, files table, date group labels to 6px 12px padding
- Set font-size 0.80rem on capture-text, file table cells

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:53:41 +00:00
497436a0a3 feat: universal reorder grip handles and compact UI density
- Add generic move_in_order() to BaseRepository for reorder support
- Add reusable reorder_arrows.html partial with grip dot handles
- Add reorder routes to all 9 list routers (tasks, notes, links, contacts, meetings, decisions, appointments, lists, focus)
- Compact row padding (6px 12px, gap 8px) on .list-row and .focus-item
- Reduce font size to 0.80rem on row titles, sidebar nav, domain tree

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:44:30 +00:00
75b055299a feat: file search, type/tag filters, and pagination
Add tsvector full-text search, type filter (image/document/text/
spreadsheet/archive), tag filter dropdown, and pagination (50/page).
Replace folder button bar with compact dropdown. All filters combine
and carry through sort and pagination links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:05:10 +00:00
41b974c804 feat: sortable file table and dropdown folder picker
Replace flat folder button bar with compact dropdown select.
Add sortable columns (path, name, date) to file list table.
Restore soft-deleted files on sync when file still on disk.
Serve text file previews with white background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:49:16 +00:00
27 changed files with 570 additions and 85 deletions

View File

@@ -248,3 +248,72 @@ class BaseRepository:
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"), text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": (i + 1) * 10, "id": str(id)} {"order": (i + 1) * 10, "id": str(id)}
) )
async def swap_sort_order(self, id_a: str, id_b: str) -> None:
"""Swap sort_order between two rows."""
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE id IN (:a, :b)"),
{"a": str(id_a), "b": str(id_b)},
)
rows = {str(r._mapping["id"]): r._mapping["sort_order"] for r in result}
if len(rows) != 2:
return
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": rows[str(id_b)], "id": str(id_a)},
)
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": rows[str(id_a)], "id": str(id_b)},
)
async def move_in_order(self, item_id: str, direction: str, filters: dict | None = None) -> None:
"""Move an item up or down within its sort group.
Handles lazy initialization (all sort_order=0) and swaps with neighbor.
filters: optional dict to scope the group (e.g. {"list_id": some_id}).
"""
where_clauses = ["is_deleted = false"]
params: dict[str, Any] = {}
if filters:
for i, (key, value) in enumerate(filters.items()):
if value is None:
where_clauses.append(f"{key} IS NULL")
else:
param_name = f"mf_{i}"
where_clauses.append(f"{key} = :{param_name}")
params[param_name] = value
where_sql = " AND ".join(where_clauses)
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
params,
)
items = [dict(r._mapping) for r in result]
if len(items) < 2:
return
# Lazy init: if all sort_order are 0, assign incremental values
if all(r["sort_order"] == 0 for r in items):
for i, r in enumerate(items):
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": (i + 1) * 10, "id": str(r["id"])},
)
# Re-fetch
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
params,
)
items = [dict(r._mapping) for r in result]
ids = [str(r["id"]) for r in items]
if item_id not in ids:
return
idx = ids.index(item_id)
if direction == "up" and idx > 0:
await self.swap_sort_order(ids[idx], ids[idx - 1])
elif direction == "down" and idx < len(ids) - 1:
await self.swap_sort_order(ids[idx], ids[idx + 1])

View File

@@ -300,3 +300,15 @@ async def delete_appointment(
repo = BaseRepository("appointments", db) repo = BaseRepository("appointments", db)
await repo.soft_delete(appointment_id) await repo.soft_delete(appointment_id)
return RedirectResponse(url="/appointments", status_code=303) return RedirectResponse(url="/appointments", status_code=303)
@router.post("/reorder")
async def reorder_appointment(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("appointments", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/appointments"), status_code=303)

View File

@@ -124,3 +124,15 @@ async def delete_contact(contact_id: str, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("contacts", db) repo = BaseRepository("contacts", db)
await repo.soft_delete(contact_id) await repo.soft_delete(contact_id)
return RedirectResponse(url="/contacts", status_code=303) return RedirectResponse(url="/contacts", status_code=303)
@router.post("/reorder")
async def reorder_contact(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("contacts", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/contacts"), status_code=303)

View File

@@ -216,3 +216,15 @@ async def delete_decision(decision_id: str, request: Request, db: AsyncSession =
repo = BaseRepository("decisions", db) repo = BaseRepository("decisions", db)
await repo.soft_delete(decision_id) await repo.soft_delete(decision_id)
return RedirectResponse(url="/decisions", status_code=303) return RedirectResponse(url="/decisions", status_code=303)
@router.post("/reorder")
async def reorder_decision(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("decisions", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/decisions"), status_code=303)

View File

@@ -91,9 +91,11 @@ async def sync_files(db: AsyncSession):
)) ))
db_records = [dict(r._mapping) for r in result] db_records = [dict(r._mapping) for r in result]
# Build lookup sets # Build lookup maps
all_db_paths = {r["storage_path"] for r in db_records} all_db_paths = {r["storage_path"] for r in db_records}
active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]} active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]}
deleted_on_disk = {r["storage_path"]: r for r in db_records
if r["is_deleted"] and r["storage_path"] in disk_files}
# New on disk, not in DB at all → create record # New on disk, not in DB at all → create record
new_files = disk_files - all_db_paths new_files = disk_files - all_db_paths
@@ -116,6 +118,12 @@ async def sync_files(db: AsyncSession):
}) })
added += 1 added += 1
# Soft-deleted in DB but still on disk → restore
for rel_path, record in deleted_on_disk.items():
repo = BaseRepository("files", db)
await repo.restore(record["id"])
added += 1
# Active in DB but missing from disk → soft-delete # Active in DB but missing from disk → soft-delete
missing_files = active_db_paths - disk_files missing_files = active_db_paths - disk_files
for record in db_records: for record in db_records:
@@ -127,10 +135,36 @@ async def sync_files(db: AsyncSession):
return {"added": added, "removed": removed} return {"added": added, "removed": removed}
SORT_OPTIONS = {
"path": "storage_path ASC",
"path_desc": "storage_path DESC",
"name": "original_filename ASC",
"name_desc": "original_filename DESC",
"date": "created_at DESC",
"date_asc": "created_at ASC",
}
# Map type filter values to mime prefixes/patterns
TYPE_FILTERS = {
"image": "image/%",
"document": "application/pdf",
"text": "text/%",
"spreadsheet": "application/vnd.%sheet%",
"archive": "application/%zip%",
}
PER_PAGE = 50
@router.get("/") @router.get("/")
async def list_files( async def list_files(
request: Request, request: Request,
folder: Optional[str] = None, folder: Optional[str] = None,
sort: Optional[str] = None,
q: Optional[str] = None,
file_type: Optional[str] = None,
tag: Optional[str] = None,
page: int = 1,
context_type: Optional[str] = None, context_type: Optional[str] = None,
context_id: Optional[str] = None, context_id: Optional[str] = None,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
@@ -142,38 +176,88 @@ async def list_files(
folders = get_folders() folders = get_folders()
order_by = SORT_OPTIONS.get(sort, "storage_path ASC")
# Normalize folder param from form: " " (space) = root, "" = all, None = all
if folder is not None:
if folder.strip() == "" and folder != "":
# Space-only value means root folder
folder = ""
elif folder.strip() == "":
# Empty string means "all folders" (no filter)
folder = None
# Build dynamic WHERE clauses
where_clauses = ["f.is_deleted = false"]
params = {}
if context_type and context_id: if context_type and context_id:
# Files attached to a specific entity where_clauses.append("fm.context_type = :ct AND fm.context_id = :cid")
result = await db.execute(text(""" params["ct"] = context_type
params["cid"] = context_id
if folder is not None:
if folder == "":
where_clauses.append("f.storage_path NOT LIKE '%/%'")
else:
where_clauses.append("f.storage_path LIKE :prefix")
params["prefix"] = folder + "/%"
if q and q.strip():
search_terms = q.strip().split()
tsquery = " & ".join(f"{t}:*" for t in search_terms)
where_clauses.append("f.search_vector @@ to_tsquery('english', :tsquery)")
params["tsquery"] = tsquery
if file_type and file_type in TYPE_FILTERS:
where_clauses.append("f.mime_type LIKE :mime_pattern")
params["mime_pattern"] = TYPE_FILTERS[file_type]
if tag and tag.strip():
where_clauses.append(":tag = ANY(f.tags)")
params["tag"] = tag.strip()
where_sql = " AND ".join(where_clauses)
# Count total for pagination
if context_type and context_id:
count_sql = f"""
SELECT COUNT(*) FROM files f
JOIN file_mappings fm ON fm.file_id = f.id
WHERE {where_sql}
"""
else:
count_sql = f"SELECT COUNT(*) FROM files f WHERE {where_sql}"
total = (await db.execute(text(count_sql), params)).scalar()
# Paginate
if page < 1:
page = 1
offset = (page - 1) * PER_PAGE
total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
# Query
if context_type and context_id:
query_sql = f"""
SELECT f.*, fm.context_type, fm.context_id SELECT f.*, fm.context_type, fm.context_id
FROM files f FROM files f
JOIN file_mappings fm ON fm.file_id = f.id JOIN file_mappings fm ON fm.file_id = f.id
WHERE f.is_deleted = false AND fm.context_type = :ct AND fm.context_id = :cid WHERE {where_sql}
ORDER BY f.created_at DESC ORDER BY {order_by}
"""), {"ct": context_type, "cid": context_id}) LIMIT :lim OFFSET :off
elif folder is not None: """
if folder == "":
# Root folder: files with no directory separator in storage_path
result = await db.execute(text("""
SELECT * FROM files
WHERE is_deleted = false AND storage_path NOT LIKE '%/%'
ORDER BY created_at DESC
"""))
else:
# Specific folder: storage_path starts with folder/
result = await db.execute(text("""
SELECT * FROM files
WHERE is_deleted = false AND storage_path LIKE :prefix
ORDER BY created_at DESC
"""), {"prefix": folder + "/%"})
else: else:
# All files query_sql = f"""
result = await db.execute(text(""" SELECT f.* FROM files f
SELECT * FROM files WHERE {where_sql}
WHERE is_deleted = false ORDER BY {order_by}
ORDER BY created_at DESC LIMIT :lim OFFSET :off
""")) """
params["lim"] = PER_PAGE
params["off"] = offset
result = await db.execute(text(query_sql), params)
items = [dict(r._mapping) for r in result] items = [dict(r._mapping) for r in result]
# Add derived folder field for display # Add derived folder field for display
@@ -181,9 +265,23 @@ async def list_files(
dirname = os.path.dirname(item["storage_path"]) dirname = os.path.dirname(item["storage_path"])
item["folder"] = dirname if dirname else "/" item["folder"] = dirname if dirname else "/"
# Get all unique tags for the tag filter dropdown
tag_result = await db.execute(text(
"SELECT DISTINCT unnest(tags) AS tag FROM files WHERE is_deleted = false AND tags IS NOT NULL ORDER BY tag"
))
all_tags = [r._mapping["tag"] for r in tag_result]
return templates.TemplateResponse("files.html", { return templates.TemplateResponse("files.html", {
"request": request, "sidebar": sidebar, "items": items, "request": request, "sidebar": sidebar, "items": items,
"folders": folders, "current_folder": folder, "folders": folders, "current_folder": folder,
"current_sort": sort or "path",
"current_q": q or "",
"current_type": file_type or "",
"current_tag": tag or "",
"current_page": page,
"total_pages": total_pages,
"total_files": total,
"all_tags": all_tags,
"sync_result": sync_result, "sync_result": sync_result,
"context_type": context_type or "", "context_type": context_type or "",
"context_id": context_id or "", "context_id": context_id or "",

View File

@@ -121,6 +121,20 @@ async def add_to_focus(
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/reorder")
async def reorder_focus(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
focus_date: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("daily_focus", db)
parsed_date = date.fromisoformat(focus_date)
await repo.move_in_order(item_id, direction, filters={"focus_date": parsed_date})
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/{focus_id}/toggle") @router.post("/{focus_id}/toggle")
async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("daily_focus", db) repo = BaseRepository("daily_focus", db)

View File

@@ -154,3 +154,15 @@ async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends
repo = BaseRepository("links", db) repo = BaseRepository("links", db)
await repo.soft_delete(link_id) await repo.soft_delete(link_id)
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303) return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)
@router.post("/reorder")
async def reorder_link(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("links", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)

View File

@@ -332,3 +332,35 @@ async def remove_contact(
"DELETE FROM contact_lists WHERE contact_id = :cid AND list_id = :lid" "DELETE FROM contact_lists WHERE contact_id = :cid AND list_id = :lid"
), {"cid": contact_id, "lid": list_id}) ), {"cid": contact_id, "lid": list_id})
return RedirectResponse(url=f"/lists/{list_id}", status_code=303) return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
@router.post("/reorder")
async def reorder_list(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("lists", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/lists"), status_code=303)
@router.post("/{list_id}/items/reorder")
async def reorder_list_item(
list_id: str,
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
parent_id: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("list_items", db)
filters = {"list_id": list_id}
if parent_id:
filters["parent_item_id"] = parent_id
else:
# Top-level items only (no parent)
filters["parent_item_id"] = None
await repo.move_in_order(item_id, direction, filters=filters)
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)

View File

@@ -404,3 +404,15 @@ async def remove_contact(
"DELETE FROM contact_meetings WHERE contact_id = :cid AND meeting_id = :mid" "DELETE FROM contact_meetings WHERE contact_id = :cid AND meeting_id = :mid"
), {"cid": contact_id, "mid": meeting_id}) ), {"cid": contact_id, "mid": meeting_id})
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=contacts", status_code=303) return RedirectResponse(url=f"/meetings/{meeting_id}?tab=contacts", status_code=303)
@router.post("/reorder")
async def reorder_meeting(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("meetings", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/meetings"), status_code=303)

View File

@@ -193,3 +193,15 @@ async def delete_note(note_id: str, request: Request, db: AsyncSession = Depends
await repo.soft_delete(note_id) await repo.soft_delete(note_id)
referer = request.headers.get("referer", "/notes") referer = request.headers.get("referer", "/notes")
return RedirectResponse(url=referer, status_code=303) return RedirectResponse(url=referer, status_code=303)
@router.post("/reorder")
async def reorder_note(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("notes", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/notes"), status_code=303)

View File

@@ -481,3 +481,15 @@ async def remove_contact(
"DELETE FROM contact_tasks WHERE contact_id = :cid AND task_id = :tid" "DELETE FROM contact_tasks WHERE contact_id = :cid AND task_id = :tid"
), {"cid": contact_id, "tid": task_id}) ), {"cid": contact_id, "tid": task_id})
return RedirectResponse(url=f"/tasks/{task_id}?tab=contacts", status_code=303) return RedirectResponse(url=f"/tasks/{task_id}?tab=contacts", status_code=303)
@router.post("/reorder")
async def reorder_task(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("tasks", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/tasks"), status_code=303)

View File

@@ -152,11 +152,11 @@ a:hover { color: var(--accent-hover); }
.nav-item { .nav-item {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.80rem;
gap: 8px; gap: 8px;
padding: 7px 10px; padding: 7px 10px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.92rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all var(--transition); transition: all var(--transition);
@@ -201,7 +201,7 @@ a:hover { color: var(--accent-hover); }
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 6px 10px; padding: 6px 10px;
font-size: 0.85rem; font-size: 0.80rem;
font-weight: 600; font-weight: 600;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
@@ -233,7 +233,7 @@ a:hover { color: var(--accent-hover); }
.project-link { .project-link {
display: block; display: block;
padding: 4px 10px 4px 18px; padding: 4px 10px 4px 18px;
font-size: 0.85rem; font-size: 0.80rem;
color: var(--text-secondary); color: var(--text-secondary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
white-space: nowrap; white-space: nowrap;
@@ -401,8 +401,8 @@ a:hover { color: var(--accent-hover); }
.list-row { .list-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 10px 12px; padding: 6px 12px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
transition: background var(--transition); transition: background var(--transition);
} }
@@ -466,6 +466,7 @@ a:hover { color: var(--accent-hover); }
.row-title { .row-title {
flex: 1; flex: 1;
font-size: 0.80rem;
font-weight: 500; font-weight: 500;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -735,6 +736,9 @@ a:hover { color: var(--accent-hover); }
.detail-body { .detail-body {
line-height: 1.75; line-height: 1.75;
color: var(--text); color: var(--text);
overflow-wrap: break-word;
word-break: break-word;
min-width: 0;
} }
.detail-body p { margin-bottom: 1em; } .detail-body p { margin-bottom: 1em; }
@@ -792,17 +796,15 @@ a:hover { color: var(--accent-hover); }
.capture-item { .capture-item {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 8px;
padding: 12px; padding: 6px 12px;
background: var(--surface); border-bottom: 1px solid var(--border);
border: 1px solid var(--border); transition: background var(--transition);
border-radius: var(--radius);
margin-bottom: 8px;
} }
.capture-text { .capture-text {
flex: 1; flex: 1;
font-size: 0.92rem; font-size: 0.80rem;
} }
.capture-actions { .capture-actions {
@@ -815,20 +817,17 @@ a:hover { color: var(--accent-hover); }
.focus-item { .focus-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 8px;
padding: 14px 16px; padding: 6px 12px;
background: var(--surface); border-bottom: 1px solid var(--border);
border: 1px solid var(--border); transition: background var(--transition);
border-radius: var(--radius);
margin-bottom: 6px;
transition: all var(--transition);
} }
.focus-item:hover { border-color: var(--accent); } .focus-item:hover { background: var(--surface2); }
.focus-item.completed { opacity: 0.6; } .focus-item.completed { opacity: 0.6; }
.focus-item.completed .focus-title { text-decoration: line-through; } .focus-item.completed .focus-title { text-decoration: line-through; }
.focus-title { flex: 1; font-weight: 500; } .focus-title { flex: 1; font-size: 0.80rem; font-weight: 500; }
.focus-meta { font-size: 0.78rem; color: var(--muted); } .focus-meta { font-size: 0.78rem; color: var(--muted); }
/* ---- Alerts ---- */ /* ---- Alerts ---- */
@@ -1108,6 +1107,35 @@ a:hover { color: var(--accent-hover); }
transition: width 0.3s; transition: width 0.3s;
} }
/* ---- Reorder Grip Handle ---- */
.reorder-grip {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0;
margin-right: 4px;
opacity: 0.3;
transition: opacity 0.15s;
flex-shrink: 0;
}
.reorder-grip:hover { opacity: 0.9; }
.reorder-grip form { display: block; margin: 0; padding: 0; line-height: 0; }
.grip-btn {
display: block;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 10px;
width: 12px;
height: 10px;
padding: 0;
line-height: 10px;
transition: color 0.15s;
text-align: center;
}
.grip-btn:hover { color: var(--accent); }
/* ---- Scrollbar ---- */ /* ---- Scrollbar ---- */
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-track { background: transparent; }

View File

@@ -22,7 +22,7 @@
{% set appt_date = appt.start_at.strftime('%A, %B %-d, %Y') if appt.start_at else 'No Date' %} {% set appt_date = appt.start_at.strftime('%A, %B %-d, %Y') if appt.start_at else 'No Date' %}
{% if appt_date != current_date.value %} {% if appt_date != current_date.value %}
{% if not loop.first %}</div>{% endif %} {% if not loop.first %}</div>{% endif %}
<div class="date-group-label" style="padding: 12px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}"> <div class="date-group-label" style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
{{ appt_date }} {{ appt_date }}
</div> </div>
<div> <div>
@@ -30,6 +30,9 @@
{% endif %} {% endif %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/appointments/reorder", item_id=appt.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<div style="flex-shrink: 0; min-width: 60px;"> <div style="flex-shrink: 0; min-width: 60px;">
{% if appt.all_day %} {% if appt.all_day %}
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span> <span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>

View File

@@ -53,11 +53,12 @@
</div> </div>
{% if items %} {% if items %}
<div class="card">
{% for item in items %} {% for item in items %}
{# Batch header #} {# Batch header #}
{% if item._batch_first and item.import_batch_id %} {% if item._batch_first and item.import_batch_id %}
<div class="flex items-center justify-between mb-2 mt-3" style="padding:6px 12px;background:var(--surface2);border-radius:var(--radius-sm)"> <div class="flex items-center justify-between" style="padding:6px 12px;background:var(--surface2);">
<span class="text-xs text-muted">Batch &middot; {{ batches[item.import_batch_id|string] }} items</span> <span class="text-xs text-muted">Batch &middot; {{ batches[item.import_batch_id|string] }} items</span>
<form action="/capture/batch/{{ item.import_batch_id }}/undo" method="post" style="display:inline" data-confirm="Delete all items in this batch?"> <form action="/capture/batch/{{ item.import_batch_id }}/undo" method="post" style="display:inline" data-confirm="Delete all items in this batch?">
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Undo batch</button> <button class="btn btn-ghost btn-xs" style="color:var(--red)">Undo batch</button>
@@ -99,6 +100,7 @@
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
<div class="empty-state-icon">&#128229;</div> <div class="empty-state-icon">&#128229;</div>

View File

@@ -8,6 +8,9 @@
<div class="card"> <div class="card">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/contacts/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span> <span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span>
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %} {% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %} {% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}

View File

@@ -25,6 +25,9 @@
<div class="card mt-3"> <div class="card mt-3">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/decisions/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span> <span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span>
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span> <span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span> <span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Files<span class="page-count">{{ items|length }}</span></h1> <h1 class="page-title">Files<span class="page-count">{{ total_files }}</span></h1>
<div class="flex gap-2"> <div class="flex gap-2">
<form action="/files/sync" method="post" style="display:inline"> <form action="/files/sync" method="post" style="display:inline">
<button type="submit" class="btn btn-secondary">Sync Files</button> <button type="submit" class="btn btn-secondary">Sync Files</button>
@@ -16,47 +16,140 @@
</div> </div>
{% endif %} {% endif %}
{% if folders %} <form class="filters-bar" method="get" action="/files" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 16px;">
<div class="filter-bar" style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px;"> <input type="text" name="q" value="{{ current_q }}" class="form-input" placeholder="Search files..." style="max-width: 220px; padding: 6px 10px; font-size: 0.85rem;">
<a href="/files" class="btn btn-xs {{ 'btn-primary' if current_folder is none else 'btn-ghost' }}">All</a>
<a href="/files?folder=" class="btn btn-xs {{ 'btn-primary' if current_folder is not none and current_folder == '' else 'btn-ghost' }}">/</a> <select name="folder" class="form-input" style="max-width: 200px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
{% for f in folders %} <option value="" {{ 'selected' if current_folder is none }}>All folders</option>
<a href="/files?folder={{ f }}" class="btn btn-xs {{ 'btn-primary' if current_folder == f else 'btn-ghost' }}">{{ f }}</a> <option value=" " {{ 'selected' if current_folder is not none and current_folder == '' }}>/ (root)</option>
{% for f in folders %}
<option value="{{ f }}" {{ 'selected' if current_folder == f }}>{% if '/' in f %}&nbsp;&nbsp;{{ f.split('/')[-1] }}{% else %}{{ f }}{% endif %}</option>
{% endfor %}
</select>
<select name="file_type" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
<option value="">All types</option>
<option value="image" {{ 'selected' if current_type == 'image' }}>Images</option>
<option value="document" {{ 'selected' if current_type == 'document' }}>Documents</option>
<option value="text" {{ 'selected' if current_type == 'text' }}>Text</option>
<option value="spreadsheet" {{ 'selected' if current_type == 'spreadsheet' }}>Spreadsheets</option>
<option value="archive" {{ 'selected' if current_type == 'archive' }}>Archives</option>
</select>
{% if all_tags %}
<select name="tag" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
<option value="">All tags</option>
{% for t in all_tags %}
<option value="{{ t }}" {{ 'selected' if current_tag == t }}>{{ t }}</option>
{% endfor %}
</select>
{% endif %}
{% if current_q or current_type or current_tag or current_folder is not none %}
<input type="hidden" name="sort" value="{{ current_sort }}">
<button type="submit" class="btn btn-primary btn-xs">Search</button>
<a href="/files" class="btn btn-ghost btn-xs">Clear</a>
{% else %}
<button type="submit" class="btn btn-primary btn-xs">Search</button>
{% endif %}
</form>
{% set qp = [] %}
{% if current_folder is not none %}{% if current_folder == '' %}{{ qp.append('folder= ') or '' }}{% else %}{{ qp.append('folder=' ~ current_folder) or '' }}{% endif %}{% endif %}
{% if current_q %}{{ qp.append('q=' ~ current_q) or '' }}{% endif %}
{% if current_type %}{{ qp.append('file_type=' ~ current_type) or '' }}{% endif %}
{% if current_tag %}{{ qp.append('tag=' ~ current_tag) or '' }}{% endif %}
{% set filter_qs = qp | join('&') %}
{% set sort_base = '/files?' ~ (filter_qs ~ '&' if filter_qs else '') %}
{% if items %}
<div class="card" style="overflow-x: auto;">
<table class="data-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid var(--border); text-align: left;">
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'path_desc' if current_sort == 'path' else 'path' }}" style="color: var(--muted); text-decoration: none;">
Path {{ '▲' if current_sort == 'path' else ('▼' if current_sort == 'path_desc' else '') }}
</a>
</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'name_desc' if current_sort == 'name' else 'name' }}" style="color: var(--muted); text-decoration: none;">
Name {{ '▲' if current_sort == 'name' else ('▼' if current_sort == 'name_desc' else '') }}
</a>
</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Type</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Size</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'date_asc' if current_sort == 'date' else 'date' }}" style="color: var(--muted); text-decoration: none;">
Date {{ '▼' if current_sort == 'date' else ('▲' if current_sort == 'date_asc' else '') }}
</a>
</th>
<th style="padding: 6px 12px;"></th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem;">{{ item.folder }}</td>
<td style="padding: 6px 12px; font-size: 0.80rem;">
<a href="/files/{{ item.id }}/preview" style="color: var(--accent);">{{ item.original_filename }}</a>
</td>
<td style="padding: 6px 12px;">
{% if item.mime_type %}<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>{% endif %}
</td>
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
{% if item.size_bytes %}{{ "%.1f"|format(item.size_bytes / 1024) }} KB{% endif %}
</td>
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
{{ item.created_at.strftime('%Y-%m-%d') if item.created_at else '' }}
</td>
<td style="padding: 6px 12px; text-align: right; white-space: nowrap;">
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
{% set page_base = sort_base ~ 'sort=' ~ current_sort ~ '&' %}
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 16px;">
{% if current_page > 1 %}
<a href="{{ page_base }}page={{ current_page - 1 }}" class="btn btn-ghost btn-xs">Prev</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == current_page %}
<span class="btn btn-primary btn-xs" style="pointer-events: none;">{{ p }}</span>
{% elif p <= 3 or p >= total_pages - 2 or (p >= current_page - 1 and p <= current_page + 1) %}
<a href="{{ page_base }}page={{ p }}" class="btn btn-ghost btn-xs">{{ p }}</a>
{% elif p == 4 and current_page > 5 %}
<span style="color: var(--muted);">...</span>
{% elif p == total_pages - 3 and current_page < total_pages - 4 %}
<span style="color: var(--muted);">...</span>
{% endif %}
{% endfor %} {% endfor %}
{% if current_page < total_pages %}
<a href="{{ page_base }}page={{ current_page + 1 }}" class="btn btn-ghost btn-xs">Next</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if items %}
<div class="card">
{% for item in items %}
<div class="list-row">
<span class="row-title">
<a href="/files/{{ item.id }}/preview">{{ item.original_filename }}</a>
</span>
<span class="row-meta" style="color: var(--muted); font-size: 0.8rem;">{{ item.folder }}</span>
{% if item.mime_type %}
<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>
{% endif %}
{% if item.size_bytes %}
<span class="row-meta">{{ "%.1f"|format(item.size_bytes / 1024) }} KB</span>
{% endif %}
{% if item.description %}
<span class="row-meta">{{ item.description[:50] }}</span>
{% endif %}
<div class="row-actions">
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
<div class="empty-state-icon">&#128193;</div> <div class="empty-state-icon">&#128193;</div>
{% if current_q or current_type or current_tag %}
<div class="empty-state-text">No files match your filters</div>
<a href="/files" class="btn btn-secondary">Clear Filters</a>
{% else %}
<div class="empty-state-text">No files{{ ' in this folder' if current_folder is not none else ' uploaded yet' }}</div> <div class="empty-state-text">No files{{ ' in this folder' if current_folder is not none else ' uploaded yet' }}</div>
<a href="/files/upload" class="btn btn-primary">Upload First File</a> <a href="/files/upload" class="btn btn-primary">Upload First File</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -14,8 +14,12 @@
<!-- Focus items --> <!-- Focus items -->
{% if items %} {% if items %}
<div class="card">
{% for item in items %} {% for item in items %}
<div class="focus-item {{ 'completed' if item.completed }}"> <div class="focus-item {{ 'completed' if item.completed }}">
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline"> <form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
<div class="row-check"> <div class="row-check">
<input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()"> <input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()">
@@ -32,6 +36,7 @@
</form> </form>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div> <div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
{% endif %} {% endif %}

View File

@@ -8,6 +8,9 @@
<div class="card"> <div class="card">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/links/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %} {% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
<span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span> <span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span>
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %} {% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}

View File

@@ -46,6 +46,9 @@
<div class="card mt-2"> <div class="card mt-2">
{% for li in list_items %} {% for li in list_items %}
<div class="list-row {{ 'completed' if li.completed }}"> <div class="list-row {{ 'completed' if li.completed }}">
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=li.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.list_type == 'checklist' %} {% if item.list_type == 'checklist' %}
<div class="row-check"> <div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline"> <form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
@@ -68,6 +71,9 @@
<!-- Child items --> <!-- Child items -->
{% for child in child_map.get(li.id|string, []) %} {% for child in child_map.get(li.id|string, []) %}
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;"> <div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=child.id, extra_fields={"parent_id": li.id|string} %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.list_type == 'checklist' %} {% if item.list_type == 'checklist' %}
<div class="row-check"> <div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline"> <form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">

View File

@@ -25,6 +25,9 @@
<div class="card mt-3"> <div class="card mt-3">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/lists/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span> <span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
<span class="row-meta"> <span class="row-meta">
{{ item.completed_count }}/{{ item.item_count }} items {{ item.completed_count }}/{{ item.item_count }} items

View File

@@ -18,6 +18,9 @@
<div class="card mt-3"> <div class="card mt-3">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/meetings/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span> <span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span>
<span class="row-meta">{{ item.meeting_date }}</span> <span class="row-meta">{{ item.meeting_date }}</span>
{% if item.location %} {% if item.location %}

View File

@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
{% if item.body %} {% if item.body %}
<div class="card"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body)">{{ item.body }}</div></div> <div class="card" style="overflow:hidden"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body);margin-right:20px">{{ item.body }}</div></div>
{% else %} {% else %}
<div class="card"><div class="text-muted" style="padding:20px">No content yet. <a href="/notes/{{ item.id }}/edit">Start writing</a></div></div> <div class="card"><div class="text-muted" style="padding:20px">No content yet. <a href="/notes/{{ item.id }}/edit">Start writing</a></div></div>
{% endif %} {% endif %}

View File

@@ -8,6 +8,9 @@
<div class="card"> <div class="card">
{% for item in items %} {% for item in items %}
<div class="list-row"> <div class="list-row">
{% with reorder_url="/notes/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %} {% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
<span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span> <span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span>
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %} {% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}

View File

@@ -0,0 +1,30 @@
{#
Reorder grip handle. Include with:
{% with reorder_url="/focus/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
Required context vars:
reorder_url - POST endpoint for the reorder action
item_id - ID of the current item
Optional:
extra_fields - dict of extra hidden fields (e.g. {"focus_date": "2026-03-03"})
#}
<span class="reorder-grip">
<form action="{{ reorder_url }}" method="post">
<input type="hidden" name="item_id" value="{{ item_id }}">
<input type="hidden" name="direction" value="up">
{% if extra_fields %}{% for k, v in extra_fields.items() %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}{% endif %}
<button type="submit" class="grip-btn" title="Move up"></button>
</form>
<form action="{{ reorder_url }}" method="post">
<input type="hidden" name="item_id" value="{{ item_id }}">
<input type="hidden" name="direction" value="down">
{% if extra_fields %}{% for k, v in extra_fields.items() %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}{% endif %}
<button type="submit" class="grip-btn" title="Move down"></button>
</form>
</span>

View File

@@ -53,6 +53,9 @@
<div class="card"> <div class="card">
{% for item in items %} {% for item in items %}
<div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}"> <div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}">
{% with reorder_url="/tasks/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<div class="row-check"> <div class="row-check">
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline"> <form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }} <input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}

View File

@@ -67,7 +67,7 @@
{% for entry in entries %} {% for entry in entries %}
{% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %} {% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %}
{% if entry_date != current_date.value %} {% if entry_date != current_date.value %}
<div style="padding: 10px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}"> <div style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
{{ entry_date }} {{ entry_date }}
</div> </div>
{% set current_date.value = entry_date %} {% set current_date.value = entry_date %}