improve quality of life ux
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user