diff --git a/web/src/lib/components/timeline/RecentItem.stories.svelte b/web/src/lib/components/timeline/RecentItem.stories.svelte
new file mode 100644
index 00000000..6ada673a
--- /dev/null
+++ b/web/src/lib/components/timeline/RecentItem.stories.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/lib/components/timeline/RecentItem.svelte b/web/src/lib/components/timeline/RecentItem.svelte
new file mode 100644
index 00000000..4544dea2
--- /dev/null
+++ b/web/src/lib/components/timeline/RecentItem.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+ {#if item.kind === "repo"}
+
+
+
+
+ {item.ownerHandle}/{item.name}
+
+
+ {:else}
+
+
+
+ {item.title}
+
+ {item.ownerHandle}/{item.repoName}
+
+
+ {/if}
+
diff --git a/web/src/lib/components/timeline/RecentsPreview.stories.svelte b/web/src/lib/components/timeline/RecentsPreview.stories.svelte
new file mode 100644
index 00000000..b66f5cd0
--- /dev/null
+++ b/web/src/lib/components/timeline/RecentsPreview.stories.svelte
@@ -0,0 +1,53 @@
+
+
+
+
+
diff --git a/web/src/lib/components/timeline/RecentsPreview.svelte b/web/src/lib/components/timeline/RecentsPreview.svelte
new file mode 100644
index 00000000..21b2b598
--- /dev/null
+++ b/web/src/lib/components/timeline/RecentsPreview.svelte
@@ -0,0 +1,26 @@
+
+
+
+ {#if items.length > 0}
+
+
+
+ {#each items as item (recentPath(item))}
+
+ {/each}
+
+ {:else}
+
+ No recent pages yet.
+
+ {/if}
+
diff --git a/web/src/lib/components/timeline/types.ts b/web/src/lib/components/timeline/types.ts
new file mode 100644
index 00000000..639461a6
--- /dev/null
+++ b/web/src/lib/components/timeline/types.ts
@@ -0,0 +1,33 @@
+// view models for the timeline rail, mirroring the appview's recents rows.
+
+import type { PullState } from "$lib/components/repo/pulls/PullStatePill.svelte";
+
+interface RecentBase {
+ /** when the viewer last opened the page */
+ visited: string;
+ ownerHandle: string;
+}
+
+export type RecentItemData =
+ | (RecentBase & { kind: "repo"; name: string; ownerDid?: string })
+ | (RecentBase & {
+ kind: "issue";
+ repoName: string;
+ title: string;
+ issueId: number;
+ open: boolean;
+ })
+ | (RecentBase & {
+ kind: "pull";
+ repoName: string;
+ title: string;
+ pullId: number;
+ state: PullState;
+ });
+
+export interface BlogPost {
+ slug: string;
+ title: string;
+ subtitle?: string;
+ date: string;
+}
diff --git a/web/src/routes/timeline/+page.svelte b/web/src/routes/timeline/+page.svelte
index 4600cb4a..b6c5dc36 100644
--- a/web/src/routes/timeline/+page.svelte
+++ b/web/src/routes/timeline/+page.svelte
@@ -4,6 +4,7 @@
import Button from "$lib/components/ui/Button.svelte";
import ButtonGroup from "$lib/components/ui/ButtonGroup.svelte";
import NotificationsPreview from "$lib/components/timeline/NotificationsPreview.svelte";
+ import RecentsPreview from "$lib/components/timeline/RecentsPreview.svelte";
import Timeline from "$lib/components/timeline/Timeline.svelte";
import TrendingRepos from "$lib/components/timeline/TrendingRepos.svelte";
import type { PageData } from "./$types";
@@ -23,6 +24,7 @@
class="hidden md:sticky md:top-0 md:flex md:h-screen md:w-full md:max-w-100 md:flex-col md:gap-4 md:overflow-y-auto md:p-4"
>
+