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
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
# Org + Course landing pages
|
||||
|
||||
This folder holds the entire landing-page system: the public org site (`{theme}.org`) and the per-course landing (`{theme}.course`), in 10 themes.
|
||||
|
||||
Both surfaces share the same nav, hero, footer, tokens, and edit context. A course landing inherits its theme from the org's `landingpage.theme` — courses don't pick their own.
|
||||
|
||||
## Folder map
|
||||
|
||||
```
|
||||
packages/ui/src/custom/org-landing-page/
|
||||
│
|
||||
├── types.ts Public types: OrgLandingPageProps, CourseLandingPageProps,
|
||||
│ CourseLandingPageLabels, OrgLandingPageTheme, …
|
||||
├── theme-vars-base.ts Type + baseTokenVars for --landing-* CSS variables
|
||||
├── theme-style.ts Thin registry: aggregates per-theme vars + exports themeStyle()
|
||||
├── course-tokens-base.ts Type + baseCourseTokens + tokenize() helper
|
||||
├── course-landing-page.tokens.ts Thin registry: aggregates per-theme courseTokens + exports
|
||||
│ courseLandingTokens(theme)
|
||||
├── course-landing-page.helpers.ts alignHeroCtaWithPricing, buildCourseSectionNavItems
|
||||
├── fixtures.ts mockOrgLandingPageProps, mockCourseLandingPageProps
|
||||
├── index.ts Public exports
|
||||
│
|
||||
├── edit-context.ts LandingSectionKey + setLandingPageEditContext/getLandingPageEditContext
|
||||
├── editable-section.svelte Wrapper that makes a section click-to-edit when a context is set
|
||||
├── landing-button.svelte Theme-aware button used by every CTA
|
||||
├── landing-page-footer.svelte Shared footer, per-theme class tokens
|
||||
├── landing-page-footer.tokens.ts
|
||||
│
|
||||
├── {theme}/ Everything for one theme (minimal, bold, classic, saas, tech,
|
||||
│ │ studio, corporate, terminal, editorial, vibrant)
|
||||
│ ├── index.ts re-exports { nav, hero, courseCard, org, course }
|
||||
│ ├── nav.svelte reused by both org and course
|
||||
│ ├── hero.svelte reused; course populates with course-shaped hero data
|
||||
│ ├── course-card.svelte used by the org catalog
|
||||
│ ├── org.svelte org-landing composer for this theme
|
||||
│ ├── course.svelte course-landing composer for this theme
|
||||
│ ├── vars.ts --landing-* CSS variables for this theme
|
||||
│ └── course-tokens.ts CourseLandingTokens record for this theme
|
||||
│
|
||||
└── course-*.svelte Shared body components driven by `variant` + tokens:
|
||||
course-section-nav, course-social-proof, course-info-blocks, course-curriculum,
|
||||
course-chips, course-instructor, course-reviews, course-pricing
|
||||
```
|
||||
|
||||
## A theme is one folder
|
||||
|
||||
Every artifact for a theme — composer, sub-components, CSS variables, class tokens — lives in `{theme}/`. To touch one theme you edit one directory. To delete one you `rm -rf {theme}/`. To duplicate one you copy the folder.
|
||||
|
||||
The central registries (`theme-style.ts`, `course-landing-page.tokens.ts`, `index.ts`) only import-and-aggregate from each `{theme}/`.
|
||||
|
||||
## Three-layer separation
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ {theme}/course.svelte (orchestration)│ ← composes the theme
|
||||
│ ── reuses {theme}/nav, {theme}/hero, │
|
||||
│ landing-page-footer │
|
||||
└──┬─────────────────────────────────────┘
|
||||
│ renders, passes variant="…"
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ course-*.svelte (shared body) │ ← one component per section
|
||||
│ • course-section-nav │ used by all 10 themes
|
||||
│ • course-social-proof │
|
||||
│ • course-info-blocks │
|
||||
│ • course-curriculum │
|
||||
│ • course-chips │
|
||||
│ • course-instructor │
|
||||
│ • course-reviews │
|
||||
│ • course-pricing │
|
||||
└──┬─────────────────────────────────────┘
|
||||
│ reads courseLandingTokens(variant)
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ {theme}/course-tokens.ts (tokens) │ ← per-theme class strings
|
||||
│ {theme}/vars.ts (vars) │ and CSS variables
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The composer only orchestrates. Body components only render. Theme variation lives in tokens + CSS vars.
|
||||
|
||||
## Token contract
|
||||
|
||||
Each theme owns its `course-tokens.ts` exporting `courseTokens: CourseLandingTokens` via `tokenize({...overrides})`. The base lives in `course-tokens-base.ts`. Every body component reads `t = courseLandingTokens(variant)` and applies `{t.sectionShell}`, `{t.heading}`, `{t.curriculumModule}`, … Anything not overridden falls through to the base.
|
||||
|
||||
**Tokens-first rule.** Body components must not hardcode theme-varying utilities (`font-bold`, `tracking-tight`, `rounded-2xl`, `bg-card-soft`, …). Anything that differs between themes lives in tokens. Layout-only utilities that are identical everywhere (`flex`, `grid`, `gap-6`, `py-12`) can sit inline.
|
||||
|
||||
**Off-palette rule.** A course body section must only use surfaces that exist in the theme's org template. Don't introduce `bg-section` greys to minimal, don't paint card-soft on bold, etc.
|
||||
|
||||
## Data flow
|
||||
|
||||
The dashboard's `<CourseLandingPage>` (`apps/dashboard/src/lib/features/ui/course-landing-page/course-landing-page.svelte`) is the entry point. It:
|
||||
|
||||
1. Reads the org's chosen theme from `landingpage.theme`.
|
||||
2. Maps `course + org` → `CourseLandingPageProps` via `buildCourseLandingPageProps()`. Section visibility (`metadata.sectionDisplay`) is honoured here — hidden sections are stripped from `info` / `reviews` / `instructor` before rendering.
|
||||
3. Lazy-loads the theme bundle via `importThemeBundle(theme)` — one bundle per theme; the other nine never download. The bundle exposes `{ nav, hero, courseCard, org, course }`.
|
||||
|
||||
The composer wires nav/hero/footer (theme-specific files) around the shared body components, threading `variant={theme}` and the same `labels` prop into each.
|
||||
|
||||
## Edit context (click-to-edit)
|
||||
|
||||
`EditableLandingSection sectionKey="…"` wraps every editable region. When a parent route calls `setLandingPageEditContext({ selectedKey, selectKey, labelFor, iconFor })`, clicking a section in the preview calls `selectKey(key)`. The Editor sidebar reads the same key from a bound prop and opens the matching form. Without a context, the wrapper is a transparent pass-through.
|
||||
|
||||
Course `LandingSectionKey` values: `header`, `requirement`, `description`, `goals`, `certificate`, `curriculum`, `chips`, `instructor`, `reviews`, `pricing`.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
A theme is one folder. The central registries are the only files outside that folder you touch.
|
||||
|
||||
### 1. Declare the theme key
|
||||
|
||||
`types.ts`:
|
||||
|
||||
```ts
|
||||
export type OrgLandingPageTheme =
|
||||
| 'minimal'
|
||||
| …
|
||||
| 'newtheme';
|
||||
```
|
||||
|
||||
### 2. Create the folder
|
||||
|
||||
```
|
||||
newtheme/
|
||||
├── index.ts
|
||||
├── nav.svelte
|
||||
├── hero.svelte
|
||||
├── course-card.svelte
|
||||
├── org.svelte
|
||||
├── course.svelte
|
||||
├── vars.ts
|
||||
└── course-tokens.ts
|
||||
```
|
||||
|
||||
Quickest path: `cp -R minimal/ newtheme/` and edit.
|
||||
|
||||
### 3. Fill in each file
|
||||
|
||||
- `nav.svelte`, `hero.svelte`, `course-card.svelte` — the theme's UI personality. Hero composes `{@render navigation()}` if the theme renders nav inside the hero.
|
||||
- `org.svelte` — org landing composer for this theme.
|
||||
- `course.svelte` — course landing composer (use any existing theme's `course.svelte` as a template; the body sequence is the same).
|
||||
- `vars.ts` — exports `vars: LandingThemeVars`. Spread `...baseTokenVars` and override the differences. Use raw hex only for genuinely off-palette themes (see `terminal/vars.ts`, `editorial/vars.ts`).
|
||||
- `course-tokens.ts` — exports `courseTokens: CourseLandingTokens = tokenize({ ... })`. Only override regions that differ from the base.
|
||||
- `index.ts`:
|
||||
|
||||
```ts
|
||||
export { default as nav } from './nav.svelte';
|
||||
export { default as hero } from './hero.svelte';
|
||||
export { default as courseCard } from './course-card.svelte';
|
||||
export { default as org } from './org.svelte';
|
||||
export { default as course } from './course.svelte';
|
||||
```
|
||||
|
||||
### 4. Wire into the central registries
|
||||
|
||||
`theme-style.ts`:
|
||||
|
||||
```ts
|
||||
import { vars as newthemeVars } from './newtheme/vars';
|
||||
…
|
||||
export const LANDING_THEME_VARS = {
|
||||
…,
|
||||
newtheme: newthemeVars
|
||||
};
|
||||
```
|
||||
|
||||
`course-landing-page.tokens.ts`:
|
||||
|
||||
```ts
|
||||
import { courseTokens as newthemeTokens } from './newtheme/course-tokens';
|
||||
…
|
||||
const TOKENS = {
|
||||
…,
|
||||
newtheme: newthemeTokens
|
||||
};
|
||||
```
|
||||
|
||||
`index.ts`:
|
||||
|
||||
```ts
|
||||
import * as newtheme from './newtheme';
|
||||
export const NewthemeLandingPage = newtheme.org;
|
||||
export const NewthemeCourseLanding = newtheme.course;
|
||||
export const NewthemeLandingNav = newtheme.nav;
|
||||
export const NewthemeLandingHero = newtheme.hero;
|
||||
export const NewthemeLandingCourseCard = newtheme.courseCard;
|
||||
```
|
||||
|
||||
### 5. Register in the dashboard utils
|
||||
|
||||
`apps/dashboard/src/lib/features/org/utils/landing-page.ts`:
|
||||
|
||||
- Add the key to `landingPageThemes`.
|
||||
- Add a `case 'newtheme'` to `importThemeBundle` (this is the only lazy-load switch — `importThemeComponent`, `importThemeNavHero`, `importThemeCourseCard`, and `importCourseLandingPageTheme` are thin adapters over it).
|
||||
- If your hero composes `{@render navigation()}`, add the theme key to `NAV_SNIPPET_THEMES`.
|
||||
- Add an entry to `themeCourseGridClass`.
|
||||
|
||||
### 6. Pick body layouts
|
||||
|
||||
- `course-curriculum.svelte` has a `layoutByTheme` map (`accordion` for collapsible, `chapters` for permanently expanded). Add `newtheme`.
|
||||
- If the info-blocks should use a different layout, do the same in `course-info-blocks.svelte`.
|
||||
|
||||
### 7. Storybook
|
||||
|
||||
In `packages/storybook/src/templates/org-landing-page/`:
|
||||
|
||||
- `org-landing-page.stories.svelte` — add `<NewthemeLandingPage {...mockProps} />`.
|
||||
- `course-landing-page.stories.svelte` — add `<NewthemeCourseLanding {...mockProps} />`.
|
||||
|
||||
### 8. Audit
|
||||
|
||||
- Walk the new story top-to-bottom and compare against the org story for the same theme. Surfaces should match.
|
||||
- `grep -nE 'font-(black|extrabold|bold|semibold|medium)|tracking-(tight|tighter|wider|widest)|rounded-(none|sm|md|lg|xl|2xl|3xl|full)' course-*.svelte` — body components should return no matches.
|
||||
- `grep -nE 'bg-\[var\(--landing-(bg-section|card-soft|card)\)' newtheme/course-tokens.ts` — confirm you only used surfaces present in the org theme.
|
||||
- `pnpm exec svelte-check --threshold error` from the repo root.
|
||||
|
||||
That's it — one folder, two registry hooks, two Storybook entries.
|
||||
Reference in New Issue
Block a user