name: add-landing-template description: Add a new org landing-page template to the ClassroomIO monorepo. Use when the user asks to "create/add a new landing template", "add a new template like X to the org landing pages", "build a new theme for the org landing page", or hands over a design reference (image, URL, prototype) for a new landing visual style.
Add a new org landing-page template
Adds one new theme (e.g. editorial, vibrant) to the org landing-page system. The work spans three packages — prototypes/, packages/ui/ + packages/storybook/, and apps/dashboard/ — and must be done in three phases with approval gates between them. Do not skip ahead.
Workflow
Use AskUserQuestion to gate each phase transition. The user must confirm before you move from Phase 1 → 2 → 3. If the user types a custom answer instead of selecting an option, treat any negative response as "not yet" and iterate within the current phase.
Track the work with TaskCreate/TaskUpdate — one task per numbered step below so progress is visible.
Phase 1 — Prototype (approval required)
Goal: lock the visual design before any framework code.
- Pick a short, descriptive name for the template (
editorial,vibrant,terminal, etc.). Avoid generic adjectives. - Build a single-file HTML mockup at
prototypes/org-landing-page/<theme>.html. Match the conventions of the other prototypes there (Inter font, inline CSS in<style>, no JS frameworks). - Color discipline: define one
--primaryCSS variable for the accent. Derived shades usecolor-mix(in oklab, var(--primary) X%, ...). Other neutrals (bg, fg, borders, cards) live as CSS variables too. Never hardcode hex inline — the Phase-2 port tovar(--landing-*)becomes mechanical. - Compose the page sections an org landing page actually needs: nav, hero, course catalog grid (this is the centerpiece — orgs sell courses), resources/links, FAQ or callout, footer. Do not invent sections without a data source in
OrgLandingPageProps(no per-org testimonials, no faculty cards, no instructor bios — they have no prop backing and will be misleading). - Open it in the browser. Iterate with the user on typography, spacing, color, hierarchy, layout.
Phase 1 approval gate — call AskUserQuestion:
Question: "Phase 1 complete — the prototype at
prototypes/org-landing-page/<theme>.htmlmatches your direction. Move on to Phase 2 (build the template in packages/ui + Storybook)?" Options: "Yes, proceed to Phase 2" / "Not yet — keep iterating on the prototype" Header: "Phase 1 → 2"
If the user picks "not yet" or writes a correction, stay in Phase 1 and iterate.
Phase 2 — packages/ui + Storybook (approval required)
Goal: the template renders correctly in Storybook with the shared mockOrgLandingPageProps fixture, fully isolated from dashboard.
Add
'<theme>'toOrgLandingPageThemeinpackages/ui/src/custom/org-landing-page/types.ts.Add the theme's entry to
LANDING_THEME_VARSinpackages/ui/src/custom/org-landing-page/theme-style.ts. The 20 variables fall into two groups:- Surfaces —
--landing-bg,--landing-bg-section,--landing-card,--landing-card-soft,--landing-fg,--landing-fg-muted,--landing-fg-faint,--landing-border,--landing-border-soft,--landing-accent,--landing-accent-fg - Buttons — primary/secondary/tertiary with
-bg,-fg,-bg-hover, plus-borderon secondary
If the theme shares the app's chrome (light bg, dark fg, app primary): use
baseTokenVarsand override only what differs. If the theme has its own palette (cream like editorial, dark like terminal): declare all hex/values explicitly. Decide what each button tier means per theme — for vibrant, primary = accent; for editorial, primary = fg-inverse (dark on cream); for terminal, primary = light pill on dark.- Surfaces —
Build three subcomponents in
packages/ui/src/custom/org-landing-page/<theme>/:nav.sveltehero.svelte— defensive defaults are required:courses = [],orgName = '',labelsoptional. These heroes are mounted from course pages that won't pass the full prop set.course-card.svelte— acceptscourse, optionaldisableCourseLinks, optionallabels. Used both inside the template's own catalog grid AND on the/courseslisting page.
Color rule: read every color from
var(--landing-*). The only allowed inline hex is decorative (painterly gradients, Mac traffic-light dots, dark IDE mockup overlays) where the value is intentionally invariant of theme. Nobg-background,text-foreground,bg-primary, etc. — those are app-theme tokens, not landing-theme tokens.Text rule: hardcoded user-facing strings route through
labels?.foo ?? 'English default'. See the existingOrgLandingPageLabelsfields intypes.ts.Button rule: for any CTA that isn't the template's signature primary CTA, use
<LandingButton variant="primary|secondary|tertiary">from@cio/ui/custom/org-landing-page— it automatically adopts the theme's button palette.Build the top-level composer
packages/ui/src/custom/org-landing-page/<theme>.svelte:- Wrap the root in
style={themeStyle('<theme>')}plusclass="ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)]" - Render in order:
<ThemeNav>(or as a snippet inside<ThemeHero>) →<ThemeHero>→ catalog section (using<ThemeCourseCard>) →<OrgLandingPageLinks variant="<theme>" {labels} {links} />→<OrgLandingPageEmbed variant="<theme>" {labels} {embed} />→<OrgLandingPageCallout variant="<theme>" {labels} {callout} />→<OrgLandingPageFooter variant="<theme>" {orgName} {logoUrl} {footer} />
- Wrap the root in
Wire variant branches into shared section components:
callout.svelte— add entries to all 5 token maps (sectionClasses,headingClasses,descriptionClasses,buttonClasses,eyebrowClasses) and update thedefaultEyebrowswitchlinks.svelte— add an{:else if variant === '<theme>'}branchlanding-page-footer.tokens.ts— add anif (variant === '<theme>')block ingetFooterTokenssecondary-action-button.svelte— add an entry tothemeButtonClassesembed.svelte— only add a branch if the theme needs distinct embed styling (terminal/editorial do; most templates fall through to the default)
Export from
packages/ui/src/custom/org-landing-page/index.ts:<Theme>LandingPage,<Theme>LandingNav,<Theme>LandingHero,<Theme>LandingCourseCard
Add a Storybook story in
packages/storybook/src/templates/org-landing-page/org-landing-page.stories.svelte— import the new page component and add<Story name="<Theme>"><<Theme>LandingPage {...mockProps} /></Story>.Verify Phase 2:
pnpm --filter @cio/ui build, open Storybook (pnpm --filter @cio/storybook dev), navigate to Templates → Org Landing Page →, click around all sections. Run npx --no-install svelte-check --workspace=packages/ui --no-tsconfig > /tmp/check.log 2>&1once, thengrep org-landing-page /tmp/check.log— zero errors expected in any file underorg-landing-page/.
Phase 2 approval gate — call AskUserQuestion:
Question: "Phase 2 complete — the template renders in Storybook. Move on to Phase 3 (wire it into the dashboard so it shows up in org settings + the live org pages)?" Options: "Yes, proceed to Phase 3" / "Not yet — fix things in Storybook first" Header: "Phase 2 → 3"
Phase 3 — Dashboard integration (final approval)
Goal: the template selectable in /settings/landingpage, rendering correctly on /, /courses, /course/[slug], and the course editor preview.
Add
'<theme>'toOrgLandingPageThemeinapps/dashboard/src/lib/utils/types/org.ts.Three lazy switches + one eager map — all four must include the new theme or things will silently fall back to minimal:
apps/dashboard/src/lib/features/org/utils/landing-page.ts:landingPageThemesconst arrayimportThemeComponent(theme)switch — used by the public org root pageimportThemeNavHero(theme)switch — used by/courses,/course/[slug], course-landing editorimportThemeCourseCard(theme)switch — used by/coursesper-card renderingthemeCourseGridClass(theme)— only update if the template uses a non-default grid (segmented borders, special gap)
apps/dashboard/src/lib/features/org/utils/landing-page-components.ts:- All three eager maps:
landingPageThemeComponents,landingPageNavComponents,landingPageHeroComponents - Critical: this map is only used by the settings-page live preview. The org-site routes use the lazy switches above. Forgetting the lazy ones means /settings looks right but the live page renders minimal.
- All three eager maps:
Theme picker in settings:
apps/dashboard/src/lib/features/settings/pages/landingpage.svelte→ append tothemeCardswithpreview: '/templates/<theme>.png'and translation keys.Translations:
apps/dashboard/src/lib/utils/translations/en.json→ addsettings.landing_page.theme.cards.<theme>.titleand.description.Preview image: drop
apps/dashboard/static/templates/<theme>.png. Must be under 500KB. If the source is larger, compress withsips -Z 900 source.png --out target.png(resizes to max 900px wide while preserving aspect ratio). JPEGs are acceptable when PNG compression isn't enough — update thethemeCardspreviewpath accordingly.Verify Phase 3:
pnpm --filter @cio/ui buildnpx --no-install svelte-check --workspace=apps/dashboard --no-tsconfig > /tmp/dash-check.log 2>&1(run once)grep -E "(landing-page-components|landingpage\.svelte|types/org\.ts|org/utils/landing-page\.ts)" /tmp/dash-check.log— zero errors expected in any touched file- Run the dashboard and click through: select the new template in settings, view it on the live org root,
/courses,/course/[slug]. Verify the nav/hero, course cards, Filter aside, Clear Filters button, and footer all render with the right colors on each route.
Phase 3 final approval — call AskUserQuestion:
Question: "Phase 3 complete — the
template is live in the dashboard and renders on all org-site routes. Anything else?" Options: "All good, we're done" / "There's a visual issue to fix" Header: "Final check"
Anti-checklist (mistakes to prevent)
Read this before starting. These are real bugs that have appeared in this codebase:
- Inline hex in components — the wrapper sets
--landing-*vars; components read from them. Hardcoded hex breaks theme switching and the/coursescross-page rendering. Exception: decorative painterly fills, Mac traffic-light dots, and dark IDE mockup overlays that are intentionally invariant of theme. - Missing defensive defaults — heroes that consume
coursesororgNamemust default them (courses = [],orgName = ''). They crash when mounted from course pages that don't pass the full prop set. - Per-call-site button styling — use
<LandingButton variant="...">instead. The /courses Filter aside's Clear Filters button is the canonical example. - Hardcoded user-facing strings — route through
labels?.X ?? 'Default'. The labels surface is consumed by the dashboard's translations layer. - Only updating one of the dashboard registries —
landingPageThemeComponents(eager, settings preview) and the threeimportTheme*switches (lazy, public routes) must all include the new theme. ForgettingimportThemeComponentmakes the live org page silently render minimal. - Re-running expensive checks —
svelte-checktypechecks the whole package on each invocation. Run it once, capture to/tmp/check.log, grep the captured output as many times as you need. - Adding non-prop-backed sections — testimonials, instructor cards, faculty bios, statistics aren't in
OrgLandingPageProps. Either drop them, or extend the props type for everyone (significant API surface change — discuss before doing). - Changing the layout pattern when extracting a cell — if the original catalog uses
border-t/border-lon the grid +border-r/border-bon each child (bordered-cell pattern), preserve that pattern. Two failure modes to avoid:- Don't swap it for
gap-px bg-border— that loses the outer top/left border and breaks visually when there are fewer cards than fill the grid. - Don't move the right/bottom border from the card to the grid via
[&>*]:border-r [&>*]:border-b. If a<BlurFade>(or any wrapper) sits between the grid and the card, the[&>*]:selector lands on the wrapper, not the card — different render. Keepborder-r/border-bON the card component itself; the parent owns onlyborder-t/border-l.
- Don't swap it for
File-touch summary (~30 files for a typical theme)
| Path | Purpose |
|---|---|
prototypes/org-landing-page/<theme>.html |
Phase 1 design mockup |
packages/ui/src/custom/org-landing-page/types.ts |
Theme union |
packages/ui/src/custom/org-landing-page/theme-style.ts |
CSS variable contract |
packages/ui/src/custom/org-landing-page/<theme>/nav.svelte |
Nav component |
packages/ui/src/custom/org-landing-page/<theme>/hero.svelte |
Hero component |
packages/ui/src/custom/org-landing-page/<theme>/course-card.svelte |
Card component |
packages/ui/src/custom/org-landing-page/<theme>.svelte |
Page composer |
packages/ui/src/custom/org-landing-page/callout.svelte |
Variant branch |
packages/ui/src/custom/org-landing-page/links.svelte |
Variant branch |
packages/ui/src/custom/org-landing-page/landing-page-footer.tokens.ts |
Footer tokens |
packages/ui/src/custom/org-landing-page/secondary-action-button.svelte |
Button variant |
packages/ui/src/custom/org-landing-page/embed.svelte |
Optional variant branch |
packages/ui/src/custom/org-landing-page/index.ts |
Exports |
packages/storybook/src/templates/org-landing-page/org-landing-page.stories.svelte |
Storybook story |
apps/dashboard/src/lib/utils/types/org.ts |
Dashboard theme union |
apps/dashboard/src/lib/features/org/utils/landing-page.ts |
Lazy switches + themes array |
apps/dashboard/src/lib/features/org/utils/landing-page-components.ts |
Eager maps (settings preview) |
apps/dashboard/src/lib/features/settings/pages/landingpage.svelte |
Theme picker card |
apps/dashboard/src/lib/utils/translations/en.json |
Title + description |
apps/dashboard/static/templates/<theme>.png |
Preview image (<500KB) |