improve quality of life ux

This commit is contained in:
JCEEE
2026-09-14 13:38:52 +01:00
parent da6a6e372c
commit 8bb120e8eb
3 changed files with 175 additions and 21 deletions
+58 -1
View File
@@ -17,7 +17,13 @@
active: string;
onchange: (v: string) => void;
};
weeknav?: { current: string; onPrev: () => void; onNext: () => void };
weeknav?: {
current: string;
onPrev: () => void;
onNext: () => void;
onCurrent?: () => void;
isCurrent?: boolean;
};
sort?: {
options: { label: string; value: string }[];
active: string;
@@ -35,6 +41,15 @@
{@render children?.()}
{#if weeknav?.onCurrent}
<button
class="now-btn header-now"
class:now-hidden={weeknav.isCurrent !== false}
onclick={weeknav.onCurrent}
tabindex={weeknav.isCurrent === false ? 0 : -1}
><span>← Back to this week</span></button>
{/if}
<div class="view-tools">
{#if tabs}
<div class="tab-bar">
@@ -142,6 +157,13 @@
justify-content: space-between;
gap: 0.5rem;
}
/* Back-to-this-week CTA: lives outside .view-tools on the header's top
row, pushed right. Always rendered (visibility-toggled) so the header
keeps its height when it appears/disappears. */
.header-now {
margin-left: auto;
align-self: flex-end;
}
.nav-btn {
padding: 0.2rem 0.5rem;
border: 1px solid #d1d5db;
@@ -154,6 +176,41 @@
.nav-btn:hover {
background: #f3f4f6;
}
.nav-btn.now-btn {
font-size: 0.8rem;
font-weight: 700;
white-space: nowrap;
}
/* Same pill CTA as the member hero. Always rendered (visibility-toggled)
so the ‹ › buttons never jump when it appears/disappears. */
.now-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.now-btn span {
display: inline-block;
background: rgba(255, 255, 255, 0.92);
color: #4338ca;
border-radius: 999px;
padding: 0.35rem 0.9rem;
font-size: 0.8rem;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.view-header.hero .now-btn span {
background: rgba(255, 255, 255, 0.92);
color: #4338ca;
}
.now-btn:hover span {
background: #fff;
}
.now-btn.now-hidden {
visibility: hidden;
pointer-events: none;
}
.nav-label {
font-size: 0.85rem;
color: #374151;
+115 -20
View File
@@ -384,13 +384,12 @@
);
let weeklyTotal = $derived.by(() => {
// Must match the pocket-money % threshold definition: all assigned
// non-todo chores for the member (no season filter), weighted by the
// number of days in the displayed period — so the hero total agrees
// with the Goals progress instead of showing a different denominator.
const chores = assigned.filter(
(a) => (a.memberId === null || a.memberId === memberId) && !a.isTodo
);
// Must match the pocket-money % threshold definition (and the family
// dashboard Done denominator): only the member's own chores count —
// personal assignments + shared chores they claimed. Unclaimed shared
// chores stay doable in the kanban but out of every denominator, so
// all views agree instead of drifting (e.g. 1/37 here vs 1/30 there).
const chores = assigned.filter((a) => a.memberId === memberId && !a.isTodo);
const periodDays =
Math.round(
(new Date(weekEnd + 'T00:00:00').getTime() - new Date(weekStart + 'T00:00:00').getTime()) /
@@ -407,6 +406,29 @@
(c.date?.slice(0, 10) || c.date) <= weekEnd
)
);
// Viewed-week daily activity for the hero background effect: completions
// per day across the 7-day window, plotted as a blurred white line.
let heroWeekActivity = $derived.by(() => {
const counts: number[] = [];
for (let i = 0; i < 7; i++) {
const d = addDays(weekStart, i);
counts.push(
weekCompletions.filter((c) => (c.date?.slice(0, 10) || c.date) === d).length
);
}
return counts;
});
let heroSpark = $derived.by(() => {
const max = Math.max(1, ...heroWeekActivity);
const pts = heroWeekActivity.map((v, i) => ({
x: 20 + i * ((700 - 40) / 6),
y: 100 - (v / max) * 72
}));
return {
line: pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' '),
pts
};
});
let weeklyDone = $derived(weekCompletions.length);
let weeklyPct = $derived(weeklyTotal > 0 ? Math.round((weeklyDone / weeklyTotal) * 100) : 0);
@@ -536,10 +558,10 @@
// ─── Threshold Goals (bonus configs the member is chasing) ───
let thresholdGoals = $derived.by(() => {
// Member's assigned chores (non-todo) — used to compute period potential.
const myChores = assigned.filter(
(a) => (a.memberId === null || a.memberId === memberId) && !a.isTodo
);
// Member's own chores (non-todo) — personal + claimed shared, never
// unclaimed shared — used to compute period potential. Matches
// weeklyTotal above, the family dashboard, and the server's % definition.
const myChores = assigned.filter((a) => a.memberId === memberId && !a.isTodo);
return bonusConfigs
.filter((cfg) => {
if (cfg.status !== 'active') return false;
@@ -1227,6 +1249,24 @@
{/if}
<!-- HERO -->
<header class="hero">
<svg
class="hero-bg"
viewBox="0 0 700 120"
preserveAspectRatio="none"
aria-hidden="true"
>
<polyline
points={heroSpark.line}
fill="none"
stroke="#fff"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
{#each heroSpark.pts as p}
<circle cx={p.x} cy={p.y} r="5" fill="#fff" />
{/each}
</svg>
<div class="hero-top">
<div class="hero-id">
<span
@@ -1350,18 +1390,20 @@
</div>
<div class="hero-meta">
{#if anyWeekBonuses}
{#if weekBonusTallies.length > 0}
<div class="hero-bonus-slot">
{#if anyWeekBonuses && weekBonusTallies.length > 0}
<div class="hero-pills">
{#each weekBonusTallies as t}
<span class="hero-pill">🎉 {t.name} ×{t.count}</span>
{/each}
</div>
{/if}
{:else}
<span class="hero-null">no bonuses earned yet</span>
{/if}
</div>
{#if isCurrentWeek}
<div class="hero-days"><b>{daysLeft}</b> days until payday</div>
{:else}
<button class="hero-now" onclick={jumpToNow}><span>← Back to this week</span></button>
{/if}
</div>
<nav class="hero-nav">
@@ -2107,6 +2149,50 @@
margin-bottom: 1rem;
color: #fff;
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.35);
position: relative;
overflow: hidden;
}
/* Week-activity line effect behind the hero content — no axes, just a
blurred white trace with dots. Pure decoration, ignored by AT. */
.hero-bg {
position: absolute;
inset: auto 0 0 0;
width: 100%;
height: 120px;
opacity: 0.45;
filter: blur(5px);
pointer-events: none;
}
.hero-top,
.hero-progress,
.hero-meta,
.hero-nav {
position: relative;
z-index: 1;
}
.hero-now {
/* Same full-width centered slot as .hero-days so the hero keeps its height */
flex: 1 1 100%;
margin-top: 0.25rem;
min-height: 2.6rem;
align-content: center;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.hero-now span {
display: inline-block;
background: rgba(255, 255, 255, 0.92);
color: #4338ca;
border-radius: 999px;
padding: 0.45rem 1.1rem;
font-size: 0.85rem;
font-weight: 800;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.hero-now:hover span {
background: #fff;
}
.hero-top {
display: flex;
@@ -2230,6 +2316,16 @@
margin-bottom: 0.9rem;
flex-wrap: wrap;
}
/* Fixed slot for the pills / no-bonus line so week navigation never
moves the hero when the bonus content changes height. Fits two rows
of pills; more than that still grows gracefully. */
.hero-bonus-slot {
flex: 1 1 auto;
display: flex;
align-items: center;
min-height: 3.4rem;
align-content: center;
}
.hero-pills {
display: flex;
gap: 0.4rem;
@@ -2244,10 +2340,6 @@
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.hero-null {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
}
.hero-days {
font-size: 1rem;
color: rgba(255, 255, 255, 0.9);
@@ -2255,6 +2347,9 @@
flex: 1 1 100%;
text-align: center;
margin-top: 0.25rem;
/* Fixed slot so swapping in the back-to-this-week CTA never moves the hero */
min-height: 2.6rem;
align-content: center;
}
.hero-days b {
font-size: 1.5rem;
@@ -169,7 +169,9 @@
weeknav={{
current: isCurrentWeek ? 'This week' : formatShortDate(weekStart),
onPrev: () => navigateWeek(-1),
onNext: () => navigateWeek(1)
onNext: () => navigateWeek(1),
onCurrent: () => (weekStart = currentWeek),
isCurrent: isCurrentWeek
}}
/>