Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions apps/web/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png" />
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
%sveltekit.head%
</head>
<script type="application/ld+json">
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/lib/components/general/style/Palette.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
{}
) as CalColors;

if (title === "Midnight" || title === "Cobalt" || title === "Shadow")
if (
title === "Midnight" ||
title === "Cobalt" ||
title === "Shadow" ||
title === "Crimson" ||
title === "Aurora"
)
darkTheme.set(true);
else darkTheme.set(false);

Expand Down
181 changes: 144 additions & 37 deletions apps/web/src/lib/components/recal/Left.svelte
Original file line number Diff line number Diff line change
@@ -1,51 +1,158 @@
<script lang="ts">
import { onMount, tick } from "svelte";
import Saved from "./left/Saved.svelte";
import SearchResults from "./left/SearchResults.svelte";
import SearchBar from "./left/SearchBar.svelte";
import Events from "./left/Events.svelte";
import Handlebar from "./left/Handlebar.svelte";
import { searchResults, recal } from "$lib/stores/recal";
import { sectionRatio, isMobile, isEventOpen } from "$lib/stores/styles";

// Element refs
let sectionEl: HTMLElement;
let eventsWrapperEl: HTMLElement;

// Height tracking
let availableHeight = 0;

// Content heights from children (dynamic measurement)
let savedContentHeight = 0;
let searchContentHeight = 0;
let eventsHeight = 0;

// Constants
const HANDLEBAR_HEIGHT = 16;
const BASE_MIN_RATIO = 0.1;
const BASE_MAX_RATIO = 0.9;
const GAP_SIZE = 16; // Two gaps of 8px each
const INNER_GAP = 8;

// Force update on window resize
function handleWindowResize() {
if (sectionEl) {
availableHeight = sectionEl.getBoundingClientRect().height;
}
$recal = !$recal; // Force re-render like CalBox click does
}

onMount(() => {
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
availableHeight = entry.contentRect.height;
}
});

resizeObserver.observe(sectionEl);
return () => resizeObserver.disconnect();
});

// Measure events section when it changes (collapsed/expanded)
$: if (eventsWrapperEl && $isEventOpen !== undefined) {
tick().then(() => {
eventsHeight = eventsWrapperEl?.offsetHeight ?? 0;
});
}

// Calculate total content heights from dynamic measurements
$: topContentHeight = eventsHeight + INNER_GAP + savedContentHeight;
$: bottomContentHeight = searchContentHeight;

// Show handlebar when there are search results AND content would overflow
$: hasSearchResults = $searchResults.length > 0;
$: totalContent = topContentHeight + bottomContentHeight + INNER_GAP;
$: showHandlebar =
!$isMobile && hasSearchResults && totalContent > availableHeight;

// Calculate usable height (minus handlebar and gaps when shown)
$: usableHeight = showHandlebar
? Math.max(0, availableHeight - HANDLEBAR_HEIGHT - GAP_SIZE)
: availableHeight;

// Content-based ratio constraints (using measured heights)
$: maxRatio =
usableHeight > 0
? Math.min(BASE_MAX_RATIO, topContentHeight / usableHeight)
: BASE_MAX_RATIO;
$: minRatio =
usableHeight > 0
? Math.max(BASE_MIN_RATIO, 1 - bottomContentHeight / usableHeight)
: BASE_MIN_RATIO;

// Default ratio based on content proportions
function getDefaultRatio(): number {
if (topContentHeight + bottomContentHeight === 0) return 0.5;
const ratio =
topContentHeight / (topContentHeight + bottomContentHeight);
return Math.max(minRatio, Math.min(maxRatio, ratio));
}

// Get effective ratio (user-set or default), clamped to valid range
$: effectiveRatio = Math.max(
minRatio,
Math.min(maxRatio, $sectionRatio ?? getDefaultRatio())
);

// Calculate heights
$: rawTopHeight = Math.round(usableHeight * effectiveRatio);

// Cap at measured content height to prevent gaps
$: topHeight = Math.min(rawTopHeight, topContentHeight);
$: bottomHeight = usableHeight - topHeight;

// Style strings
$: topSectionStyle = showHandlebar ? `height: ${topHeight}px;` : "";
$: bottomSectionStyle = showHandlebar ? `height: ${bottomHeight}px;` : "";

function handleResize(e: CustomEvent<{ ratio: number }>) {
if (e.detail.ratio === -1) {
sectionRatio.reset();
} else {
// Constrain to content-based bounds
const clampedRatio = Math.max(
minRatio,
Math.min(maxRatio, e.detail.ratio)
);
sectionRatio.set(clampedRatio);
}
}
</script>

<svelte:window on:resize={handleWindowResize} />

<div class="w-full flex flex-col h-full overflow-y-hidden">
<div>
<SearchBar />
</div>
<section class="flex-1 overflow-y-hidden mt-2">
<div class="min-h-[24px]">
<Events />
</div>
<div>
<Saved />
</div>
<div>
<SearchResults />
<section
bind:this={sectionEl}
class="flex-1 overflow-y-hidden mt-2 flex flex-col gap-2">
<!-- Top Section: Events + Saved -->
<div
class="flex flex-col gap-2 overflow-y-hidden min-h-0 shrink-0"
style={topSectionStyle}>
<div bind:this={eventsWrapperEl} class="shrink-0">
<Events />
</div>
<div class="flex-1 overflow-y-hidden min-h-0 flex flex-col">
<Saved bind:contentHeight={savedContentHeight} />
</div>
</div>
</section>
</div>

<style lang="postcss">
/*
This took hours to figure out. It allows the children to take up
the remaining space in the container while maintaining a max-height
proportional to the parent container. This is a lesson that
StackOverflow is almost always better than AI for complex problems.

Credit:
https://stackoverflow.com/questions/70324159/how-to-set-up-an-element-with-max-height-so-that-the-elements-inside-it-will-tak
*/

section {
display: grid;
grid-template-rows: repeat(auto-fit, minmax(0, min-content));
gap: 0.5rem;
}

section > *:last-child {
grid-row-end: span 2;
}
<!-- Handlebar -->
{#if showHandlebar}
<Handlebar
on:resize={handleResize}
containerHeight={usableHeight} />
{/if}

section > div {
/* height: fit-content; */
max-height: 100%;
@apply overflow-y-hidden flex flex-col;
}
</style>
<!-- Bottom Section: SearchResults -->
{#if hasSearchResults}
<div
class="overflow-y-hidden min-h-0 flex flex-col shrink-0"
class:flex-1={showHandlebar}
style={bottomSectionStyle}>
Comment on lines +149 to +153

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Search results clipped on mobile left pane

Because showHandlebar is forced to false when $isMobile is true (lines 61‑64), the search results container here never gets the flex-1 height or any explicit sizing and inherits overflow-y-hidden from its parent section. On small screens, a long list of search results therefore expands beyond the fixed section height and the overflow is simply clipped with no scrollbar, making part of the results unreachable on mobile devices.

Useful? React with 👍 / 👎.

<SearchResults bind:contentHeight={searchContentHeight} />
</div>
{/if}
</section>
</div>
18 changes: 14 additions & 4 deletions apps/web/src/lib/components/recal/Top.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
import { SCHEDULE_CAP } from "$lib/constants";
import { modalStore } from "$lib/stores/modal";
import { savedCourses } from "$lib/stores/rpool";
import { getStyles, isMobile, showCal } from "$lib/stores/styles";
import {
calColors,
getStyles,
isMobile,
showCal
} from "$lib/stores/styles";
import { toastStore } from "$lib/stores/toast";
import confetti from "canvas-confetti";
import { getContext } from "svelte";
Expand Down Expand Up @@ -107,9 +112,14 @@
});
};

// Handle theme changes
$: cssVarStyles = getStyles("0");
$: eventStyles = getStyles("6");
// Handle theme changes (reference $calColors to establish reactive dependency)
let cssVarStyles: string;
let eventStyles: string;
$: {
$calColors;
cssVarStyles = getStyles("0");
eventStyles = getStyles("6");
}
</script>

<div
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/components/recal/calendar/CalBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
<!-- Height is on scale from 0 to 90 -->
<button
id="box"
class="absolute text-left flex p-[1px] cursor-pointer rounded-sm duration-75"
class="absolute text-left flex p-[1px] cursor-pointer rounded-sm duration-75 overflow-visible"
class:hovered={isHovered()}
style={cssVarStyles}
on:click={handleClick}
Expand All @@ -170,7 +170,7 @@
$eventHoverRev = null;
}
}}>
<div class="text-xs z-40 -space-y-1 relative overflow-x-hidden">
<div class="text-xs z-40 -space-y-1 relative overflow-x-clip overflow-y-visible">
<div class="font-light text-2xs leading-3 pb-[1px]">
{valuesToTimeLabel(
params.section.start_time,
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/lib/components/recal/left/Events.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@
<div class="overflow-y-auto">
<div class="mb-2">
{#if scheduleEvents.length === 0}
<p class="text-sm">No events added yet.</p>
<p class="text-sm dark:text-zinc-100">
No events added yet.
</p>
{:else}
<h2 class="text-sm">
<h2 class="text-sm dark:text-zinc-100">
{scheduleEvents.length} Added
{scheduleEvents.length === 1 ? "Event" : "Events"}
</h2>
Expand All @@ -133,9 +135,11 @@

<div>
{#if notInSchedule.length === 0}
<p class="text-sm mb-1">No events available.</p>
<p class="text-sm mb-1 dark:text-zinc-100">
No events available.
</p>
{:else}
<h2 class="text-sm">
<h2 class="text-sm dark:text-zinc-100">
{notInSchedule.length} Available
{notInSchedule.length === 1 ? "Event" : "Events"}
</h2>
Expand Down
Loading