From de0d3bbf10755330a9e24e5e4c2a99d6ff06de5b Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.net>
Date: Fri, 5 Jun 2026 13:51:09 +0000
Subject: fix(schedule): use masonry columns so day panels collapse cleanly

The grid layout kept fixed row heights, so collapsing a day left a gap. Distribute days round-robin into flex columns (preserving left-to-right order) so each column packs and reflows independently on collapse.
---
 src/lib/Schedule/Days.svelte | 76 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 65 insertions(+), 11 deletions(-)

(limited to 'src/lib/Schedule/Days.svelte')

diff --git a/src/lib/Schedule/Days.svelte b/src/lib/Schedule/Days.svelte
index 754d3f61..7fe09d4e 100644
--- a/src/lib/Schedule/Days.svelte
+++ b/src/lib/Schedule/Days.svelte
@@ -14,7 +14,7 @@ import { parseOrDefault } from "$lib/Utility/parameters";
 import settings from "$stores/settings";
 import CoverBypass from "./CoverBypass.svelte";
 import "$lib/List/covers.css";
-import { onMount } from "svelte";
+import { onDestroy, onMount } from "svelte";
 import RateLimitedError from "$lib/Error/RateLimited.svelte";
 import ParallaxImage from "$lib/Image/ParallaxImage.svelte";
 import Message from "$lib/Loading/Message.svelte";
@@ -65,7 +65,20 @@ const selectTrack = (track: AirType) => {
 	}
 };
 
+// Two masonry columns above the $bp-md (800px) breakpoint, one below.
+let columnCount = 1;
+let columnQuery: MediaQueryList | undefined;
+
+const updateColumnCount = () => {
+	columnCount = columnQuery?.matches ? 1 : 2;
+};
+
 onMount(async () => {
+	columnQuery = window.matchMedia("(max-width: 800px)");
+
+	updateColumnCount();
+	columnQuery.addEventListener("change", updateColumnCount);
+
 	if (user === undefined || $identity.id === -2)
 		mediaListPromise = Promise.resolve([]);
 	else {
@@ -84,6 +97,10 @@ onMount(async () => {
 	}
 });
 
+onDestroy(() => {
+	columnQuery?.removeEventListener("change", updateColumnCount);
+});
+
 const WEEKDAYS = [
 	"Sunday",
 	"Monday",
@@ -134,6 +151,26 @@ const scheduleByDay = (entries: AiringEntry[]) => {
 	return ordered;
 };
 
+// Round-robin the ordered days across columns so reading stays left-to-right
+// (e.g. Friday, then Saturday to its right), while each column still packs and
+// reflows independently when a day is collapsed.
+const distributeDays = (
+	daysByWeekday: { [key: string]: AiringEntry[] },
+	columns: number,
+) => {
+	const distributed: {
+		day: string;
+		scheduleEntry: AiringEntry[];
+		dayIndex: number;
+	}[][] = Array.from({ length: columns }, () => []);
+
+	Object.entries(daysByWeekday).forEach(([day, scheduleEntry], dayIndex) => {
+		distributed[dayIndex % columns].push({ day, scheduleEntry, dayIndex });
+	});
+
+	return distributed;
+};
+
 const associateMedia = (
 	media: (Media | undefined)[],
 	title: string,
@@ -186,12 +223,15 @@ const episode = (media: Media, weekday: string) => {
 
   <Skeleton grid={true} count={7} height="15em" width="49.5%" />
 {:then mediaList}
-  {#each Object.entries(scheduleByDay(schedule[source])) as [day, scheduleEntry], dayIndex}
-    <details
-      open
-      class="list"
-      class:today={day === new Date().toLocaleString('en-us', { weekday: 'long' })}
-    >
+  <div class="masonry">
+    {#each distributeDays(scheduleByDay(schedule[source]), columnCount) as column}
+      <div class="masonry-column">
+        {#each column as { day, scheduleEntry, dayIndex } (dayIndex)}
+          <details
+            open
+            class="list"
+            class:today={day === new Date().toLocaleString('en-us', { weekday: 'long' })}
+          >
       <summary>{day}</summary>
 
       {#if !$settings.displayScheduleListMode && !forceListMode}
@@ -275,7 +315,10 @@ const episode = (media: Media, weekday: string) => {
         </ol>
       {/if}
     </details>
-  {/each}
+        {/each}
+      </div>
+    {/each}
+  </div>
 {:catch}
   <RateLimitedError type="Media lists" loginSessionError={false} card list={false} />
 {/await}
@@ -284,12 +327,25 @@ const episode = (media: Media, weekday: string) => {
   @use "breakpoints" as *;
 
   .track-toggle {
-    grid-column: 1 / -1;
     display: flex;
     gap: 0.5em;
     margin-bottom: 0.5em;
   }
 
+  .masonry {
+    display: flex;
+    gap: 0.5em;
+    align-items: flex-start;
+  }
+
+  .masonry-column {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    gap: 0.5em;
+  }
+
   .countdown {
     white-space: nowrap;
     float: right;
@@ -304,8 +360,6 @@ const episode = (media: Media, weekday: string) => {
 
   .list {
     overflow-y: auto;
-    break-inside: avoid-column;
-    page-break-inside: avoid;
   }
 
   @media (max-width: $bp-md) {
-- 
cgit v1.2.3

