Files
classroomio/apps/dashboard/src/lib/features/side-panel/side-panel-rail.svelte
T
Rotimi Best 03dcf812a5 feat: fit ai chat trigger to center of page
- Updated the CSS classes for the AI assistant bar to adjust positioning based on the new side panel width variable.
- Introduced a new `onWidthPreview` prop in the SidePanelRail component to handle live resizing and update the side panel width dynamically.
- Added width state management in the SidePanelStore to ensure proper width handling during panel interactions.
- Improved layout handling in the course layout component to accommodate the new side panel width adjustments.
2026-06-07 23:21:19 +01:00

177 lines
4.7 KiB
Svelte

<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/state';
import { startResizablePanelDrag } from '$lib/utils/functions/resizable-panel';
import { t } from '$lib/utils/functions/translations';
import { sidePanel } from './utils/store.svelte';
interface Props {
/**
* Live-resize hook. The host writes `--side-panel-width` directly to the
* shared shell element so the panel and fixed siblings (e.g. the Ask AI
* bar) follow the drag without a per-frame reactivity round-trip.
*/
onWidthPreview?: (width: number) => void;
}
let { onWidthPreview }: Props = $props();
let railWidth = $state(0);
let isWidthLoaded = $state(false);
let isRailResizing = $state(false);
let stopRailResize: (() => void) | null = null;
const activeDefinition = $derived(sidePanel.activeDefinition);
const activeProps = $derived(sidePanel.panelProps);
function clampWidth(width: number) {
const def = activeDefinition;
if (!def) return width;
return Math.min(def.maxWidth, Math.max(def.minWidth, width));
}
function clearResizeListeners() {
stopRailResize?.();
stopRailResize = null;
}
function loadWidthForDefinition(def: typeof activeDefinition): number {
if (!def) return 0;
try {
const stored = Number(localStorage.getItem(def.widthStorageKey));
if (Number.isFinite(stored) && stored > 0) {
return Math.min(def.maxWidth, Math.max(def.minWidth, stored));
}
} catch {
// localStorage unavailable
}
return def.defaultWidth;
}
function persistWidthForDefinition(def: typeof activeDefinition, width: number) {
if (!def) return;
try {
localStorage.setItem(def.widthStorageKey, String(Math.round(width)));
} catch {
// localStorage unavailable
}
}
$effect(() => {
const def = activeDefinition;
if (!def) {
railWidth = 0;
isWidthLoaded = false;
return;
}
railWidth = loadWidthForDefinition(def);
isWidthLoaded = true;
});
// Publish the effective width so the shared ancestor can expose it as a CSS
// variable that fixed siblings (e.g. the Ask AI bar) can offset against.
$effect(() => {
sidePanel.width = activeDefinition ? railWidth : 0;
});
$effect(() => {
if (!isWidthLoaded) return;
persistWidthForDefinition(activeDefinition, railWidth);
});
// Lesson-scoped panels should not survive navigating to a different lesson.
// The rail owns this so host layouts don't have to wire it up themselves.
let lastLessonId: string | undefined = undefined;
$effect(() => {
const lessonId = page.params?.lessonId as string | undefined;
if (lastLessonId !== undefined && lessonId !== lastLessonId) {
sidePanel.closeIfScope('lesson');
}
lastLessonId = lessonId;
});
function handleResizePointerDown(event: PointerEvent) {
if (!activeDefinition) return;
event.preventDefault();
event.stopPropagation();
clearResizeListeners();
const startWidth = railWidth;
stopRailResize = startResizablePanelDrag({
event,
startWidth,
resolveWidth: ({ startWidth, deltaX }) => clampWidth(startWidth - deltaX),
onPreview: (width) => {
onWidthPreview?.(width);
},
onCommit: ({ width }) => {
railWidth = width;
},
onDragStart: () => {
isRailResizing = true;
},
onDragEnd: () => {
isRailResizing = false;
stopRailResize = null;
}
});
}
onMount(() => {
return () => {
clearResizeListeners();
};
});
onDestroy(() => {
clearResizeListeners();
sidePanel.reset();
});
</script>
{#if activeDefinition}
{@const def = activeDefinition}
<div data-side-panel-resizing={isRailResizing} class="contents">
<div class="hidden shrink-0 md:block md:w-(--side-panel-width)"></div>
<aside
class="ui:bg-background fixed inset-y-0 right-0 z-100 flex h-screen w-full flex-col border-l md:w-(--side-panel-width)"
aria-label={t.get(def.titleKey)}
>
<button
type="button"
aria-label={t.get('side_panel.resize_panel_aria_label')}
class="absolute inset-y-0 left-0 hidden w-4 -translate-x-1/2 cursor-col-resize border-0 bg-transparent md:flex"
onpointerdown={handleResizePointerDown}
>
<span class="ui:bg-border pointer-events-none h-full w-px"></span>
</button>
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
{#key def.id}
<def.component {...activeProps} />
{/key}
</div>
</aside>
</div>
{/if}
<style>
:global([data-side-panel-resizing='true'] *) {
transition-property: none !important;
}
</style>