mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 22:23:34 -06:00
Implements a canvas-based timeline view with D3 scales showing class counts across subjects. Features drag-to-pan, mouse wheel zoom, subject filtering, hover tooltips, and smooth animations. Timeline auto-follows current time and supports keyboard navigation.
136 lines
4.4 KiB
Svelte
136 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import { Filter, X } from "@lucide/svelte";
|
|
import { SUBJECTS, SUBJECT_COLORS, type Subject } from "$lib/timeline/data";
|
|
import { DRAWER_WIDTH } from "$lib/timeline/constants";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
enabledSubjects: Set<Subject>;
|
|
followEnabled: boolean;
|
|
onToggleSubject: (subject: Subject) => void;
|
|
onEnableAll: () => void;
|
|
onDisableAll: () => void;
|
|
onResumeFollow: () => void;
|
|
}
|
|
|
|
let {
|
|
open = $bindable(),
|
|
enabledSubjects,
|
|
followEnabled,
|
|
onToggleSubject,
|
|
onEnableAll,
|
|
onDisableAll,
|
|
onResumeFollow,
|
|
}: Props = $props();
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape" && open) {
|
|
open = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onKeyDown} />
|
|
|
|
<!-- Filter toggle button — slides out when drawer opens -->
|
|
<button
|
|
class="absolute right-3 z-50 p-2 rounded-md
|
|
bg-black text-white dark:bg-white dark:text-black
|
|
hover:bg-neutral-800 dark:hover:bg-neutral-200
|
|
border border-black/20 dark:border-white/20
|
|
shadow-md transition-all duration-200 ease-in-out cursor-pointer
|
|
{open ? 'opacity-0 pointer-events-none' : 'opacity-100'}"
|
|
style="top: 20%; transform: translateX({open ? '60px' : '0'});"
|
|
onclick={() => (open = true)}
|
|
aria-label="Open filters"
|
|
>
|
|
<Filter size={18} strokeWidth={2} />
|
|
</button>
|
|
|
|
<!-- Drawer panel -->
|
|
<div
|
|
class="absolute right-0 z-40 rounded-l-lg shadow-xl transition-transform duration-200 ease-in-out {open ? '' : 'pointer-events-none'}"
|
|
style="top: 20%; width: {DRAWER_WIDTH}px; height: 60%; transform: translateX({open
|
|
? 0
|
|
: DRAWER_WIDTH}px);"
|
|
>
|
|
<div
|
|
class="h-full flex flex-col bg-background/90 backdrop-blur-md border border-border/40 rounded-l-lg overflow-hidden"
|
|
style="width: {DRAWER_WIDTH}px;"
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-3 py-2.5 border-b border-border/40">
|
|
<span class="text-xs font-semibold text-foreground">Filters</span>
|
|
<button
|
|
class="p-0.5 rounded text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
onclick={() => (open = false)}
|
|
aria-label="Close filters"
|
|
>
|
|
<X size={14} strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Follow status -->
|
|
<div class="px-3 py-2 border-b border-border/40">
|
|
{#if followEnabled}
|
|
<div
|
|
class="px-2 py-1 rounded-md text-[10px] font-medium text-center
|
|
bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20"
|
|
>
|
|
FOLLOWING
|
|
</div>
|
|
{:else}
|
|
<button
|
|
class="w-full px-2 py-1 rounded-md text-[10px] font-medium text-center
|
|
bg-muted/80 text-muted-foreground hover:text-foreground
|
|
border border-border/50 transition-colors cursor-pointer"
|
|
onclick={onResumeFollow}
|
|
aria-label="Resume following current time"
|
|
>
|
|
FOLLOW
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Subject toggles -->
|
|
<div class="flex-1 overflow-y-auto px-3 py-2">
|
|
<div class="flex items-center justify-between mb-2 text-[10px] text-muted-foreground">
|
|
<span class="uppercase tracking-wider font-medium">Subjects</span>
|
|
<div class="flex gap-1.5">
|
|
<button
|
|
class="hover:text-foreground transition-colors cursor-pointer"
|
|
onclick={onEnableAll}>All</button
|
|
>
|
|
<span class="opacity-40">|</span>
|
|
<button
|
|
class="hover:text-foreground transition-colors cursor-pointer"
|
|
onclick={onDisableAll}>None</button
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-0.5">
|
|
{#each SUBJECTS as subject}
|
|
{@const enabled = enabledSubjects.has(subject)}
|
|
<button
|
|
class="flex items-center gap-2 w-full px-1.5 py-1 rounded text-xs
|
|
hover:bg-muted/50 transition-colors cursor-pointer text-left"
|
|
onclick={() => onToggleSubject(subject)}
|
|
>
|
|
<span
|
|
class="inline-block w-3 h-3 rounded-sm shrink-0 transition-opacity"
|
|
style="background: {SUBJECT_COLORS[subject]}; opacity: {enabled ? 1 : 0.2};"
|
|
></span>
|
|
<span
|
|
class="transition-opacity {enabled
|
|
? 'text-foreground'
|
|
: 'text-muted-foreground/50'}"
|
|
>
|
|
{subject}
|
|
</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|