Files
classroomio/packages/ui/src/custom/org-landing-page/course-section-nav.svelte
T
Rotimi Best d5b07d356e feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
2026-06-07 18:43:05 +01:00

72 lines
2.1 KiB
Svelte

<script lang="ts">
import type { CourseLandingPageLabels, OrgLandingPageTheme } from './types';
import { courseLandingTokens } from './course-landing-page.tokens';
import * as UnderlineTabs from '../underline-tabs';
interface SectionNavItem {
/** DOM id of the section to scroll to (without leading #). */
id: string;
label: string;
}
interface Props {
variant: OrgLandingPageTheme;
items: SectionNavItem[];
labels?: CourseLandingPageLabels;
}
let { variant, items }: Props = $props();
const t = $derived(courseLandingTokens(variant));
let activeId = $state<string>('');
$effect(() => {
if (typeof window === 'undefined') return;
if (items.length === 0) return;
const elements = items.map(({ id }) => document.getElementById(id)).filter((el): el is HTMLElement => el !== null);
if (elements.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (visible[0]) activeId = visible[0].target.id;
},
{
rootMargin: '-120px 0px -55% 0px',
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
elements.forEach((el) => observer.observe(el));
return () => observer.disconnect();
});
function handleScroll(id: string) {
const el = document.getElementById(id);
if (!el) return;
const top = el.getBoundingClientRect().top + window.scrollY - 24;
window.scrollTo({ top, behavior: 'smooth' });
}
</script>
{#if items.length > 1}
<nav class={t.sectionNavShell}>
<div class={t.sectionNavInner}>
<UnderlineTabs.Root bind:value={activeId} class="ui:w-full">
<UnderlineTabs.List class={t.sectionNavList}>
{#each items as item (item.id)}
<UnderlineTabs.Trigger value={item.id} class={t.sectionNavItem} onclick={() => handleScroll(item.id)}>
{item.label}
</UnderlineTabs.Trigger>
{/each}
</UnderlineTabs.List>
</UnderlineTabs.Root>
</div>
</nav>
{/if}