Improve list item CSS

This commit is contained in:
digimint 2025-11-22 04:36:54 -06:00
parent dbdb824269
commit 35b53653a1
Signed by: digimint
GPG key ID: 8DF1C6FD85ABF748
7 changed files with 306 additions and 84 deletions

View file

@ -14,6 +14,6 @@ class Task(db.Model):
due: Mapped[datetime] = mapped_column(DateTime(timezone=False)) due: Mapped[datetime] = mapped_column(DateTime(timezone=False))
created: Mapped[datetime] = mapped_column(DateTime(timezone=False), default=now) created: Mapped[datetime] = mapped_column(DateTime(timezone=False), default=now)
complete: Mapped[bool] = mapped_column(Boolean, default=False) complete: Mapped[bool] = mapped_column(Boolean, default=False)
completed: Mapped[int] = mapped_column(DateTime(timezone=False), nullable=True) completed: Mapped[datetime|None] = mapped_column(DateTime(timezone=False), nullable=True)
namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE')) namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE'))
owner: Mapped[int] = mapped_column(Integer, ForeignKey('user.id', ondelete='CASCADE')) owner: Mapped[int] = mapped_column(Integer, ForeignKey('user.id', ondelete='CASCADE'))

View file

@ -1,5 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from typing import Self from typing import Self
import humanize import humanize
@ -34,6 +34,8 @@ class TaskForUser:
due_rel: str due_rel: str
created: datetime created: datetime
complete: bool complete: bool
completed: datetime|None
overdue: bool
namespace_id: int namespace_id: int
can_edit: bool can_edit: bool
@ -41,6 +43,33 @@ class TaskForUser:
can_complete: bool can_complete: bool
can_uncomplete: bool can_uncomplete: bool
@property
def delay_style(self) -> str:
''' Returns an appropriate 'animation-delay' property for
recently-completed tasks; otherwise an empty string.
Intended for use in jinja templating
'''
if self.just_complete and self.completed is not None:
delta = now() - self.completed
return f'animation-delay: {-1*delta.seconds}s;'
else:
return ''
@property
def just_complete(self) -> bool:
''' Returns whether the task was recently finished (within 5 minutes).
Used for jinja templates.
'''
return (
self.complete
and (self.completed is not None)
and (
(now() - self.completed)
< timedelta(minutes=5)
)
)
@classmethod @classmethod
def from_task( def from_task(
cls, cls,
@ -71,6 +100,12 @@ class TaskForUser:
_due_str(tsk.due), _due_str(tsk.due),
tsk.created, tsk.created,
tsk.complete, tsk.complete,
(
tsk.completed.replace(tzinfo=timezone.utc)
if tsk.completed is not None
else None
),
tsk.due.replace(tzinfo=timezone.utc) < now(),
tsk.namespace, tsk.namespace,
NPT.EDIT_ALL_TASKS in perms, NPT.EDIT_ALL_TASKS in perms,
NPT.DELETE_ALL_TASKS in perms, NPT.DELETE_ALL_TASKS in perms,

View file

@ -1,31 +1,160 @@
@keyframes task-reward-fade-even {
0% {
background-color : var(--block-grey-bg);
color : var(--block-grey-text);
fill : var(--block-grey-text);
stroke : var(--block-grey-text);
}
0.5% {
background-color : var(--block-good-bg);
color : var(--block-good-text);
fill : var(--block-good-text);
stroke : var(--block-good-text);
}
80% {
background-color : var(--block-good-bg);
color : var(--block-good-text);
fill : var(--block-good-text);
stroke : var(--block-good-text);
}
100% {
background-color : var(--block-grey-bg);
color : var(--block-grey-text);
fill : var(--block-grey-text);
stroke : var(--block-grey-text);
}
}
@keyframes task-reward-fade-odd {
0% {
background-color : var(--block-grey-2-bg);
color : var(--block-grey-2-text);
fill : var(--block-grey-2-text);
stroke : var(--block-grey-2-text);
}
0.5% {
background-color : var(--block-good-2-bg);
color : var(--block-good-2-text);
fill : var(--block-good-2-text);
stroke : var(--block-good-2-text);
}
80% {
background-color : var(--block-good-2-bg);
color : var(--block-good-2-text);
fill : var(--block-good-2-text);
stroke : var(--block-good-2-text);
}
100% {
background-color : var(--block-grey-2-bg);
color : var(--block-grey-2-text);
fill : var(--block-grey-2-text);
stroke : var(--block-grey-2-text);
}
}
.list{ .list{
width: 100%; width: 100%;
--table-row-even-bg : var(--block-2-bg);
--table-row-even-text : var(--block-2-text);
--table-row-even-border-width: var(--block-2-border-width);
--table-row-even-border-color: var(--block-2-border-color);
--table-row-odd-bg : var(--block-3-bg);
--table-row-odd-text : var(--block-3-text);
--table-row-odd-border-width: var(--block-3-border-width);
--table-row-odd-border-color: var(--block-3-border-color);
tr {
--table-row-bg : var(--table-row-even-bg);
--table-row-text : var(--table-row-even-text);
--table-row-border-width : var(--table-row-even-border-width);
--table-row-border-color : var(--table-row-even-border-color);
background-color: var(--table-row-bg);
color: var(--table-row-text);
border: var(--table-row-border-width) solid var(--table-row-border-color);
transition: background-color 2s;
transition: color 2s;
transition: border 2s;
transition: all 2s;
.icon svg {
fill: var(--table-row-text);
stroke: var(--table-row-text);
transition: fill 2s;
transition: stroke 2s;
} }
table.list{ &:nth-child(odd) {
border-collapse: collapse; --table-row-bg : var(--table-row-odd-bg);
--table-row-text : var(--table-row-odd-text);
--table-row-border-width : var(--table-row-odd-border-width);
--table-row-border-color : var(--table-row-odd-border-color);
} }
.list tr { &.overdue {
background-color: var(--table-row-bg-1); --table-row-even-bg : var(--block-bad-bg);
color: var(--on-table-row); --table-row-even-text : var(--block-bad-text);
--table-row-even-border-width: var(--block-bad-border-width);
--table-row-even-border-color: var(--block-bad-border-color);
--table-row-odd-bg : var(--block-bad-2-bg);
--table-row-odd-text : var(--block-bad-2-text);
--table-row-odd-border-width: var(--block-bad-2-border-width);
--table-row-odd-border-color: var(--block-bad-2-border-color);
} }
.list tr:nth-child(odd) { &.complete {
background-color: var(--table-row-bg-2); --table-row-even-bg : var(--block-grey-bg);
color: var(--on-table-row); --table-row-even-text : var(--block-grey-text);
--table-row-even-border-width : var(--block-grey-border-width);
--table-row-even-border-color : var(--block-grey-border-color);
--table-row-odd-bg : var(--block-grey-2-bg);
--table-row-odd-text : var(--block-grey-2-text);
--table-row-odd-border-width : var(--block-grey-2-border-width);
--table-row-odd-border-color : var(--block-grey-2-border-color);
&.just-complete{
animation-name: task-reward-fade-even;
animation-duration: 300s;
.checkbox .icon svg {
animation-name: task-reward-fade-even;
animation-duration: 300s;
} }
.list .task-header-row { &:nth-child(odd) {
animation-name: task-reward-fade-odd;
.checkbox .icon svg {
animation-name: task-reward-fade-odd;
}
}
}
}
}
.task-table-row {
&.complete {
font-style: italic;
text-decoration: line-through;
}
}
.task-header-row {
border-bottom: 8px solid var(--bg); border-bottom: 8px solid var(--bg);
} }
.list td, .list th{ td, th{
border-right: 4px solid var(--bg); border-right: 4px solid var(--bg);
border-left: 4px solid var(--bg); border-left: 4px solid var(--bg);
border-top: var(--table-row-border-width) solid var(--table-row-border-color);
border-bottom: var(--table-row-border-width) solid var(--table-row-border-color);
} }
.list td { td {
padding-left: 1rem; padding-left: 1rem;
&.nopad { &.nopad {
@ -37,77 +166,82 @@
} }
} }
.list p { p {
margin: 0; margin: 0;
} }
.list .detail-view { .detail-view {
display: none; display: none;
border-bottom: 4px solid var(--bg); border-bottom: 4px solid var(--bg);
border-top: 1px solid var(--bg); border-top: 1px solid var(--bg);
}
.list .detail-view-elem { &.shown {
padding: 1rem;
}
.list .detail-view.shown {
display: table-row; display: table-row;
} }
}
.list .detail-view-elem .small-details { .detail-view-elem {
padding: 1rem;
.small-details {
font-size: small; font-size: small;
font-style: italic; font-style: italic;
} }
.list .detail-view-elem hr { hr {
color: var(--on-table-row); color: var(--table-row-text);
} }
.list .detail-view-elem .detail-view-header h1 { .detail-view-header {
display: flex;
h1 {
margin: 0 1rem; margin: 0 1rem;
font-size: larger; font-size: larger;
} }
.list .detail-view-elem .detail-view-header { &::before {
display: flex;
}
.list .detail-view-elem .detail-view-header::before{
content: ""; content: "";
flex: 1 1; flex: 1 1;
background: repeating-linear-gradient( background: repeating-linear-gradient(
60deg, 60deg,
var(--on-table-row) 0, var(--table-row-text) 0,
var(--on-table-row) 0.25rem, var(--table-row-text) 0.25rem,
transparent 0.35rem, transparent 0.35rem,
transparent 1.0rem, transparent 1.0rem,
var(--on-table-row) 1.1rem var(--table-row-text) 1.1rem
); );
width: 100%; width: 100%;
max-width: 2rem; max-width: 2rem;
} }
.list .detail-view-elem .detail-view-header::after{ &::after{
content: ""; content: "";
flex: 1 1; flex: 1 1;
background: repeating-linear-gradient( background: repeating-linear-gradient(
60deg, 60deg,
var(--on-table-row) 0, var(--table-row-text) 0,
var(--on-table-row) 0.25rem, var(--table-row-text) 0.25rem,
transparent 0.35rem, transparent 0.35rem,
transparent 1.0rem, transparent 1.0rem,
var(--on-table-row) 1.1rem var(--table-row-text) 1.1rem
); );
width: 100%; width: 100%;
} }
}
.list .detail-view-elem .main-description { .main-description {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
}
}
table.list{
border-collapse: collapse;
}

View file

@ -15,6 +15,52 @@
--body-font: sans-serif; --body-font: sans-serif;
--footer-font: sans-serif; --footer-font: sans-serif;
--block-2-bg: #331826;
--block-2-border-color: var(--block-2-bg);
--block-2-text: #ffc4d1;
--block-2-border-width: 0;
--block-3-bg: #462837;
--block-3-border-color: var(--block-3-bg);
--block-3-text: #ffc4d1;
--block-3-border-width: 0;
--block-neutral-border-width : 0;
--block-neutral-2-border-width : 0;
--block-neutral-bg : #182633;
--block-neutral-border-color : var(--block-neutral-bg);
--block-neutral-text : #c4ecff;
--block-neutral-2-bg : #283e46;
--block-neutral-2-border-color : var(--block-neutral-2-bg);
--block-neutral-2-text : #c4e0ff;
--block-grey-border-width : 0;
--block-grey-2-border-width : 0;
--block-grey-bg : #262726;
--block-grey-border-color : var(--block-grey-bg);
--block-grey-text : #b6b6b6;
--block-grey-2-bg : #484948;
--block-grey-2-border-color : var(--block-grey-2-bg);
--block-grey-2-text : #a8a8a8;
--block-good-border-width : 0;
--block-good-2-border-width : 0;
--block-good-bg : #003f1c;
--block-good-border-color : var(--block-good-bg);
--block-good-text : #c4ffce;
--block-good-2-bg : #005333;
--block-good-2-border-color : var(--block-good-2-bg);
--block-good-2-text : #c4ffce;
--block-bad-border-width : 1px;
--block-bad-2-border-width : 1px;
--block-bad-bg : #360a0a;
--block-bad-border-color : #fd1c42;
--block-bad-text : #ff798f;
--block-bad-2-bg : #4b0d0d;
--block-bad-2-border-color : #fd1c42;
--block-bad-2-text : #ffa7b5;
--table-row-bg-1: #331826; --table-row-bg-1: #331826;
--table-row-bg-2: #462837; --table-row-bg-2: #462837;
--on-table-row: #ffc4d1; --on-table-row: #ffc4d1;

View file

@ -153,6 +153,10 @@ function update_reltimes(){
).forEach((el) => { ).forEach((el) => {
const time = new Date(Number(el.dataset.timestamp)*1000) const time = new Date(Number(el.dataset.timestamp)*1000)
const delta = time - Date.now() const delta = time - Date.now()
const tr = el.parentElement.parentElement.parentElement
if(delta < 0 && !tr.classList.contains('overdue')){
tr.classList.add('overdue')
}
el.textContent = sophontize(delta, time) el.textContent = sophontize(delta, time)
}) })
} }

View file

@ -11,7 +11,7 @@
{% endmacro %} {% endmacro %}
{% macro inline_task(task, list_id=0) %} {% macro inline_task(task, list_id=0) %}
<tr class="task-table-row" id="{{ tlist_cid('row', task.id, list_id) }}"> <tr class="task-table-row{{' overdue' if task.overdue else ''}}{{' complete' if task.complete else ''}}{{' just-complete' if task.just_complete else ''}}" id="{{ tlist_cid('row', task.id, list_id) }}" style="{{task.delay_style}}">
<td class="checkbox" id="{{ tlist_cid('check', task.id, list_id) }}"> <td class="checkbox" id="{{ tlist_cid('check', task.id, list_id) }}">
{% if task.complete %} {% if task.complete %}
{% if task.can_uncomplete %} {% if task.can_uncomplete %}
@ -46,7 +46,7 @@
</td> </td>
</tr> </tr>
<tr></tr> <!-- placeholder for CSS styling --> <tr></tr> <!-- placeholder for CSS styling -->
<tr class="detail-view tl-{{ list_id }}" id="{{ tlist_cid('task-detail', task.id, list_id) }}"> <tr class="detail-view tl-{{ list_id }}{{' overdue' if task.overdue else ''}}{{' complete' if task.complete else ''}}{{' just-complete' if task.just_complete else ''}}" id="{{ tlist_cid('task-detail', task.id, list_id) }}" style="{{task.delay_style}}">
<td class="detail-view-elem" colspan=3> <td class="detail-view-elem" colspan=3>
<div class="detail-view-header"> <div class="detail-view-header">
<h1>Task Details: {{ task.name }}</h1> <h1>Task Details: {{ task.name }}</h1>

View file

@ -15,6 +15,7 @@ from taskflower.form.task import task_edit_form_for_task, task_form_for_user
from taskflower.sanitize.task import TaskForUser from taskflower.sanitize.task import TaskForUser
from taskflower.types.either import Either, Left, Right, reduce_either from taskflower.types.either import Either, Left, Right, reduce_either
from taskflower.types.option import Option from taskflower.types.option import Option
from taskflower.util.time import now
from taskflower.web.errors import ( from taskflower.web.errors import (
ResponseErrorNotFound, ResponseErrorNotFound,
response_from_exception response_from_exception
@ -111,6 +112,7 @@ def complete(id: int):
def _do_complete(tsk: Task) -> Either[Exception, Task]: def _do_complete(tsk: Task) -> Either[Exception, Task]:
try: try:
tsk.complete = True tsk.complete = True
tsk.completed = now()
db.session.commit() db.session.commit()
return Right(tsk) return Right(tsk)
except Exception as e: except Exception as e:
@ -166,6 +168,7 @@ def uncomplete(id: int):
def _do_uncomplete(tsk: Task) -> Either[Exception, Task]: def _do_uncomplete(tsk: Task) -> Either[Exception, Task]:
try: try:
tsk.complete = False tsk.complete = False
tsk.completed = None
db.session.commit() db.session.commit()
return Right(tsk) return Right(tsk)
except Exception as e: except Exception as e: