Skip to content

Commit 68d29e9

Browse files
authored
Merge branch 'moirelog:main' into main
2 parents a7cffe0 + 057dffe commit 68d29e9

9 files changed

Lines changed: 237 additions & 2 deletions

File tree

moire.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const config = {
22
title: "Moire",
33
author: "Aeris",
44
theme: "classic",
5+
heatmap: true,
56
pageSize: 20,
67
order_by: "created",
78
description: "Sync your thoughts from Apple Notes by Shortcuts.",

src/lib/components/Heatmap.svelte

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<script lang="ts">
2+
import type {Memo} from '$lib/server/memos';
3+
import {format, startOfDay, startOfWeek, endOfWeek, eachDayOfInterval, subDays} from 'date-fns';
4+
import {onMount} from 'svelte';
5+
6+
let {memos}: {memos: Memo[]} = $props();
7+
8+
const today = startOfDay(new Date());
9+
10+
const days = $derived.by(() => {
11+
let earliestDate = today;
12+
if (memos && memos.length > 0) {
13+
const minDateRaw = new Date(Math.min(...memos.map((m) => new Date(m.date).getTime())));
14+
earliestDate = startOfDay(minDateRaw);
15+
}
16+
17+
// Enforce a minimum of half a year (approx 180 days)
18+
const sixMonthsAgo = subDays(today, 360);
19+
if (earliestDate > sixMonthsAgo) {
20+
earliestDate = sixMonthsAgo;
21+
}
22+
23+
const startDate = startOfWeek(earliestDate, {weekStartsOn: 0});
24+
const endDate = endOfWeek(today, {weekStartsOn: 0});
25+
return eachDayOfInterval({start: startDate, end: endDate});
26+
});
27+
28+
// Calculate columns needed to set the grid dynamically
29+
const numColumns = $derived(Math.ceil(days.length / 7));
30+
31+
// Count actual memos
32+
const memoCounts = $derived.by(() => {
33+
const counts = new Map<string, number>();
34+
35+
// Initialize all days
36+
days.forEach((day) => {
37+
counts.set(format(day, 'yyyy-MM-dd'), 0);
38+
});
39+
40+
// Increment counts for each memo
41+
memos?.forEach((memo) => {
42+
const dateStr = format(new Date(memo.date), 'yyyy-MM-dd');
43+
if (counts.has(dateStr)) {
44+
counts.set(dateStr, counts.get(dateStr)! + 1);
45+
}
46+
});
47+
48+
return counts;
49+
});
50+
51+
const maxMemos = $derived(Math.max(1, ...Array.from(memoCounts.values())));
52+
53+
const getLevel = (count: number) => {
54+
if (count === 0) return 0;
55+
if (count === 1) return 1;
56+
if (maxMemos <= 3) return count;
57+
58+
const percentage = count / maxMemos;
59+
if (percentage <= 0.33) return 2;
60+
if (percentage <= 0.66) return 3;
61+
return 4;
62+
};
63+
64+
let scrollContainer: HTMLDivElement | undefined = $state();
65+
66+
onMount(() => {
67+
// Scroll to the end on mobile so user sees recent days first
68+
if (scrollContainer && scrollContainer.scrollWidth > scrollContainer.clientWidth) {
69+
scrollContainer.scrollLeft = scrollContainer.scrollWidth;
70+
}
71+
});
72+
73+
// Tooltip State
74+
let hoveredCell = $state<{x: number; y: number; date: string; count: number} | null>(null);
75+
76+
const handlePointerEnter = (e: PointerEvent, day: Date, count: number) => {
77+
const rect = (e.target as HTMLElement).getBoundingClientRect();
78+
hoveredCell = {
79+
x: window.scrollX + rect.left + rect.width / 2, // Document-relative X
80+
y: window.scrollY + rect.top - 8, // Document-relative Y
81+
date: format(day, 'MMM d, yyyy'),
82+
count,
83+
};
84+
};
85+
86+
const handlePointerLeave = () => {
87+
hoveredCell = null;
88+
};
89+
90+
function portal(node: HTMLElement) {
91+
if (typeof document === 'undefined') return;
92+
document.body.appendChild(node);
93+
return {
94+
destroy() {
95+
if (node.parentNode) {
96+
node.parentNode.removeChild(node);
97+
}
98+
},
99+
};
100+
}
101+
</script>
102+
103+
<div
104+
use:portal
105+
class="absolute z-[10000] text-white pointer-events-none transform -translate-x-1/2 -translate-y-full px-2.5 py-1.5 rounded-[4px] text-[11px] font-medium tracking-wide whitespace-nowrap shadow-lg transition-opacity duration-150 {hoveredCell
106+
? 'opacity-100'
107+
: 'opacity-0'}"
108+
style="left: {hoveredCell?.x ?? -9999}px; top: {hoveredCell?.y ??
109+
-9999}px; background-color: #24292f; font-family: -apple-system, BlinkMacSystemFont, sans-serif;"
110+
>
111+
{hoveredCell?.count ?? 0} memo{(hoveredCell?.count ?? 0) !== 1 ? 's' : ''} on {hoveredCell?.date ?? ''}
112+
113+
<!-- Tooltip Arrow -->
114+
<div
115+
class="absolute left-1/2 bottom-[1px] transform -translate-x-1/2 translate-y-full border-solid border-[5px] border-transparent"
116+
style="border-top-color: #24292f;"
117+
></div>
118+
</div>
119+
120+
<div class="w-full flex justify-center px-0 relative z-[9900]">
121+
<div
122+
bind:this={scrollContainer}
123+
class="heatmap-container w-full max-w-2xl overflow-x-auto py-4 flex sm:justify-center will-change-scroll"
124+
style="justify-content: {numColumns > 40 ? 'flex-start' : 'center'};"
125+
>
126+
<div
127+
class="heatmap-grid gap-[3px] sm:gap-[4px] min-w-max pb-2 px-1"
128+
style="grid-template-columns: repeat({numColumns}, 1fr);"
129+
>
130+
{#each days as day}
131+
<div
132+
class="heatmap-cell rounded-[2px] w-[10px] h-[10px] sm:w-[12px] sm:h-[12px] transition-colors duration-200"
133+
role="presentation"
134+
data-level={getLevel(memoCounts.get(format(day, 'yyyy-MM-dd')) || 0)}
135+
onpointerenter={(e) => handlePointerEnter(e, day, memoCounts.get(format(day, 'yyyy-MM-dd')) || 0)}
136+
onpointerleave={handlePointerLeave}
137+
></div>
138+
{/each}
139+
</div>
140+
</div>
141+
</div>
142+
143+
<style>
144+
.heatmap-grid {
145+
display: grid;
146+
grid-template-rows: repeat(7, 1fr);
147+
grid-auto-flow: column;
148+
}
149+
150+
.heatmap-cell {
151+
background-color: color-mix(in srgb, var(--accent-color, var(--text-color)) 10%, transparent);
152+
}
153+
154+
.heatmap-cell[data-level='1'] {
155+
background-color: color-mix(in srgb, var(--accent-color, var(--text-color)) 30%, transparent);
156+
}
157+
158+
.heatmap-cell[data-level='2'] {
159+
background-color: color-mix(in srgb, var(--accent-color, var(--text-color)) 55%, transparent);
160+
}
161+
162+
.heatmap-cell[data-level='3'] {
163+
background-color: color-mix(in srgb, var(--accent-color, var(--text-color)) 80%, transparent);
164+
}
165+
166+
.heatmap-cell[data-level='4'] {
167+
background-color: var(--accent-color, var(--text-color));
168+
}
169+
170+
.heatmap-cell:hover {
171+
outline: 1px solid var(--text-color);
172+
outline-offset: 1px;
173+
z-index: 10;
174+
}
175+
176+
.heatmap-container::-webkit-scrollbar {
177+
height: 4px;
178+
background: transparent;
179+
}
180+
181+
.heatmap-container::-webkit-scrollbar-thumb {
182+
background: color-mix(in srgb, var(--text-color) 20%, transparent);
183+
border-radius: 4px;
184+
}
185+
186+
.heatmap-container::-webkit-scrollbar-thumb:hover {
187+
background: color-mix(in srgb, var(--text-color) 40%, transparent);
188+
}
189+
</style>

src/themes/academic/index.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import type {PageData} from '../../routes/$types';
55
import {createMemoList} from '$lib/memo.svelte';
66
import Background from './Background.svelte';
7+
import Heatmap from '$lib/components/Heatmap.svelte';
78
89
let {data, config}: {data: PageData; config: any} = $props();
910
const memoList = createMemoList(() => data, config);
@@ -71,7 +72,13 @@
7172

7273
</aside>
7374

74-
<main class="md:col-span-9 space-y-16 max-w-2xl">
75+
<main class="md:col-span-9 space-y-16 max-w-2xl w-full">
76+
{#if config.heatmap}
77+
<section class="border-b border-[var(--text-color)]/20 pb-8">
78+
<Heatmap memos={data.memos} />
79+
</section>
80+
{/if}
81+
7582
{#each Object.entries(memoList.groupedMemos) as [dateKey, memos] (dateKey)}
7683
<section in:slide>
7784
<div class="flex items-baseline gap-4 mb-10 border-b border-[var(--text-color)] pb-3">

src/themes/bento/index.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import {createMemoList} from '$lib/memo.svelte';
66
import type {PageData} from '../../routes/$types';
77
import {marked} from 'marked';
8+
import Heatmap from '$lib/components/Heatmap.svelte';
89
910
let {data}: {data: PageData} = $props();
1011
const memoList = createMemoList(() => data, config);
@@ -51,6 +52,14 @@
5152
{/if}
5253
</header>
5354

55+
{#if config.heatmap}
56+
<div class="mb-10 px-4">
57+
<div class="rounded-2xl md:rounded-[2rem] border border-white/50 bg-white/30 p-2 md:p-6 shadow-sm backdrop-blur-3xl backdrop-saturate-150 relative z-30 overflow-hidden">
58+
<Heatmap memos={data.memos} />
59+
</div>
60+
</div>
61+
{/if}
62+
5463
<div class="mx-auto grid grid-cols-1 gap-6 px-4 2xl:grid-cols-2" data-selected-tag={memoList.selectedTag}>
5564
{#each memoList.visibleMemos as memo, i (memo.slug)}
5665
<div

src/themes/classic/index.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { format } from 'date-fns';
44
import type { PageData } from '../../routes/$types';
55
import { createMemoList } from '$lib/memo.svelte';
6+
import Heatmap from '$lib/components/Heatmap.svelte';
67
78
let { data, config }: { data: PageData; config: any } = $props();
89
const memoList = createMemoList(() => data, config);
@@ -37,6 +38,12 @@
3738
</div>
3839
</header>
3940

41+
{#if config.heatmap}
42+
<div class="mb-8 pb-8 border-b border-gray-100">
43+
<Heatmap memos={data.memos} />
44+
</div>
45+
{/if}
46+
4047
<div class="divide-y divide-gray-100">
4148
{#each memoList.visibleMemos as memo (memo.slug)}
4249
<article class="py-6" id={memo.slug} in:slide>

src/themes/cyberpunk/index.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {marked} from 'marked';
77
import {format} from 'date-fns';
88
import {onMount} from 'svelte';
9+
import Heatmap from '$lib/components/Heatmap.svelte';
910
1011
let {data}: {data: PageData} = $props();
1112
const memoList = createMemoList(() => data, config);
@@ -111,6 +112,12 @@
111112
class="relative z-10 pt-32 pb-32 px-4 max-w-2xl mx-auto space-y-12"
112113
data-selected-tag={memoList.selectedTag}
113114
>
115+
{#if config.heatmap}
116+
<div class="p-2 border border-[var(--accent-color)]/30 bg-[var(--accent-color)]/5 mb-8">
117+
<Heatmap memos={data.memos} />
118+
</div>
119+
{/if}
120+
114121
{#each memoList.visibleMemos as memo (memo.slug)}
115122
<article
116123
in:slide

src/themes/global.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ body.academic {
2929
body.bento {
3030
--bg-color: #f5f5f7;
3131
--text-color: #1d1d1f;
32+
--accent-color: rgb(88, 113, 206);
3233
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3334
}
3435

src/themes/pixel/index.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import {format} from 'date-fns';
88
import pixelIdle from '$lib/assets/pixel-idle.png';
99
import pixelRun from '$lib/assets/pixel-run.png';
10+
import Heatmap from '$lib/components/Heatmap.svelte';
1011
1112
let {data}: {data: PageData} = $props();
1213
const memoList = createMemoList(() => data, config);
@@ -34,7 +35,7 @@
3435
<div
3536
class="min-h-screen max-w-2xl mx-auto p-4 sm:p-8 selection:bg-[var(--text-color)]/50 selection:text-[var(--bg-color)]"
3637
>
37-
<header class="mb-12 flex items-end justify-between border-b-2 border-[var(--text-color)] px-2 pb-4 relative">
38+
<header class="mb-8 flex items-end justify-between border-b-2 border-[var(--text-color)] px-2 pb-4 relative">
3839
<div>
3940
<h1
4041
class="text-4xl text-[var(--accent-color)] drop-shadow-[3px_3px_0_var(--border-color)]/50 uppercase tracking-wider font-black"
@@ -62,6 +63,12 @@
6263
</div>
6364
</header>
6465

66+
{#if config.heatmap}
67+
<div class="mb-8 border-b-2 border-dashed border-[var(--text-color)]/30 pb-4">
68+
<Heatmap memos={data.memos} />
69+
</div>
70+
{/if}
71+
6572
<div class="mx-auto grid grid-cols-1 gap-8 2xl:grid-cols-2" data-selected-tag={memoList.selectedTag}>
6673
{#each memoList.visibleMemos as memo (memo.slug)}
6774
<article

src/themes/receipt/index.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import type {PageData} from '../../routes/$types';
66
import {createMemoList} from '$lib/memo.svelte';
77
import Background from './Background.svelte';
8+
import Heatmap from '$lib/components/Heatmap.svelte';
89
910
let {data, config}: {data: PageData; config: any} = $props();
1011
const memoList = createMemoList(() => data, config);
@@ -43,6 +44,12 @@
4344
{/if}
4445
</header>
4546

47+
{#if config.heatmap}
48+
<div class="mb-8 border-b border-dashed border-[#ccc] pb-6">
49+
<Heatmap memos={data.memos} />
50+
</div>
51+
{/if}
52+
4653
<div
4754
onclick={(e) => {
4855
const target = (e.target as HTMLElement).closest('button[data-tag]');

0 commit comments

Comments
 (0)