First working prototype

This commit is contained in:
2026-09-06 13:50:58 +02:00
parent ecf08f8307
commit f61b9025eb
44 changed files with 5118 additions and 905 deletions

View File

@@ -0,0 +1,124 @@
<script>
import { fmtDuration, fmtTime, roundTs } from '../lib/time.js';
let { entries = [], rounding = 15, now = Date.now(), onselect = null } = $props();
const ordered = $derived([...entries].sort((a, b) => a.seq - b.seq));
/**
* Durations are measured between the *displayed* boundaries, so a row can never
* read "10:45–11:45" next to "1h02". The running entry is the exception: its end
* is not shown as a time, so it keeps a live, unrounded count.
*/
function duration(entry) {
if (entry.endTs == null) return Math.max(0, now - entry.startTs);
return Math.max(
0,
roundTs(entry.endTs, rounding) - roundTs(entry.startTs, rounding),
);
}
</script>
{#if ordered.length}
<ul class="timeline">
{#each ordered as entry (entry.id)}
<li>
<!-- The whole row is the target: correcting a mis-tapped task should not
require hunting for a small pencil. -->
<button
type="button"
class:running={entry.endTs == null}
onclick={() => onselect?.(entry)}
disabled={!onselect}
>
<span class="time tnum">
{fmtTime(entry.startTs, rounding)}–{entry.endTs == null
? '…'
: fmtTime(entry.endTs, rounding)}
</span>
<span class="dur tnum">{fmtDuration(duration(entry))}</span>
<span class="what">
<strong>{entry.task}</strong>
{#if entry.client}<span class="dim">{entry.client}</span>{/if}
</span>
{#if onselect}<span class="edit" aria-hidden="true">✎</span>{/if}
</button>
</li>
{/each}
</ul>
{/if}
<style>
.timeline {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
li + li {
border-top: 2px solid var(--line);
}
/* Times and totals on one line, the task name on its own full-width line
below: at phone width a shared row squeezes long names into mid-word
breaks. */
li button {
width: 100%;
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-areas:
'time dur edit'
'what what edit';
align-items: center;
column-gap: 10px;
row-gap: 2px;
padding: 12px 4px;
border: 0;
border-radius: 0;
text-align: left;
font-weight: 400;
min-height: var(--tap);
}
li button[disabled] {
opacity: 1;
}
li button.running {
font-weight: 800;
}
.time {
grid-area: time;
font-size: 0.95rem;
font-weight: 700;
}
.dur {
grid-area: dur;
justify-self: end;
font-weight: 700;
}
.edit {
grid-area: edit;
padding-left: 6px;
color: var(--fg-dim);
}
.what {
grid-area: what;
display: flex;
flex-direction: column;
/* Let the column shrink inside the grid so long names wrap rather than
overflow — a truncated task name is useless at a glance. */
min-width: 0;
overflow-wrap: break-word;
}
.what span {
font-size: 0.85rem;
}
</style>