First working prototype
This commit is contained in:
124
src/components/Timeline.svelte
Normal file
124
src/components/Timeline.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user