feat(gamification): CompletionSparkline component
Tiny inline SVG. Seven bars, zero-days render as 1 px baseline stubs. fill=currentColor so the parent's text colour (tertiary ink on the Tasks header) drives it. Self-hides if all 7 days are zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
59
src/lib/components/CompletionSparkline.svelte
Normal file
59
src/lib/components/CompletionSparkline.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
// Phase 8. Tiny inline SVG sparkline. 7 bars, oldest to newest.
|
||||
// Zero-days render as a 1 px baseline stub (never a gap, never zero
|
||||
// height). The component self-hides when all bars are zero. Caller
|
||||
// still guards on the user's settings toggle.
|
||||
import type { DailyCompletionCount } from "$lib/types/app";
|
||||
|
||||
interface Props {
|
||||
data: DailyCompletionCount[];
|
||||
/** Total width in px. Default 80. */
|
||||
width?: number;
|
||||
/** Total height in px. Default 16. */
|
||||
height?: number;
|
||||
}
|
||||
|
||||
let { data, width = 80, height = 16 }: Props = $props();
|
||||
|
||||
const BAR_GAP = 2;
|
||||
|
||||
let maxCount = $derived(Math.max(1, ...data.map((d) => d.count)));
|
||||
let barWidth = $derived(
|
||||
(width - BAR_GAP * Math.max(0, data.length - 1)) / Math.max(1, data.length),
|
||||
);
|
||||
|
||||
let ariaLabel = $derived.by(() => {
|
||||
if (data.length === 0) return "";
|
||||
const nums = data.map((d) => d.count).join(", ");
|
||||
return `Tasks completed over the last ${data.length} days: ${nums}`;
|
||||
});
|
||||
|
||||
let hasAnyCompletion = $derived(data.some((d) => d.count > 0));
|
||||
</script>
|
||||
|
||||
{#if hasAnyCompletion}
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
class="text-text-tertiary"
|
||||
>
|
||||
{#each data as d, i}
|
||||
{@const x = i * (barWidth + BAR_GAP)}
|
||||
{@const proportion = d.count / maxCount}
|
||||
{@const barHeight = Math.max(1, Math.round(proportion * height))}
|
||||
{@const y = height - barHeight}
|
||||
<rect
|
||||
{x}
|
||||
{y}
|
||||
width={barWidth}
|
||||
height={barHeight}
|
||||
fill="currentColor"
|
||||
opacity={d.count === 0 ? 0.35 : 0.85}
|
||||
rx="1"
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user