diff --git a/prd/learning-paths/README.md b/prd/learning-paths/README.md new file mode 100644 index 00000000..1a9e9ed6 --- /dev/null +++ b/prd/learning-paths/README.md @@ -0,0 +1,277 @@ +# Learning Paths PRD + +## Status + +- Draft (prototyped, ready for engineering review) + +## Prototypes — the UX source of truth + +**The UX of this feature must be taken from the prototype folder.** Interactive HTML prototypes for every surface live in [`prototypes/learning-paths/`](../../prototypes/learning-paths/) and are the approved design reference for layout, states (done / in progress / locked), copy, and navigation flow. When this document and a prototype disagree on a UI detail, the prototype wins. + +**Start here:** + +``` +prototypes/learning-paths/index.html +``` + +Open it in a browser (`open prototypes/learning-paths/index.html`) — it maps all three journeys (visitor → learner → teacher) and every page cross-links so you can click through each flow end to end. Each page has a light/dark toggle. + +The prototypes are built on the real design tokens (`packages/ui/src/index.css` for app pages via `app-theme.css`, the `minimal` org-landing theme for public pages via `landing-theme.css`) and encode every decision below. + +| Surface | Files | +| --- | --- | +| Public (org site) | `org-landing.html`, `public-paths.html`, `public-path.html` | +| Learner (LMS) | `lms-home.html`, `paths.html`, `path-detail.html`, `course-in-path.html` | +| Teacher (admin) | `teacher-paths.html`, `teacher-path-setup.html`, `teacher-path-builder.html`, `teacher-path-people.html`, `teacher-path-analytics.html`, `teacher-path-landing-editor.html`, `teacher-path-certificate.html`, `teacher-path-settings.html` | + +## Purpose + +Let orgs package multiple courses into an **ordered, sequentially unlocked, bundle-priced learning path** that is sold on the org's public site, completed step-by-step in the LMS, and finished with an optional path certificate. Reference experience: Coursera Professional Certificates (e.g. IBM Data Science). + +## Problem Statement + +- Courses can only be sold and taken individually; there is no way to sell a curriculum as one product with one price. +- Programs (shipped) group courses for cohort admin purposes but are explicitly **unordered**, have no gating, no public page, no pricing, and no path-level certificate. +- Instructors who run structured curricula cannot enforce prerequisites ("finish JS before React"). +- The org landing page has no product above the individual course, so multi-course offerings are invisible to visitors. + +## Confirmed Decisions + +1. **New entity, separate from Programs.** `learning_path` is its own table set. Programs remain unchanged (cohort hub + newsfeed). No migration. +2. **Terminology**: "Learning Path" everywhere (admin, LMS, public site). +3. **Self-enrollment with bundle pricing.** A path is public on the org landing site with one price. Enrolling (buy or free) auto-enrolls the learner in every course in the path. +4. **Show savings.** The public page shows the bundle price against the summed individual course prices ("$199 ~~$280~~ · Save $81 (29%)"). Toggleable per path. +5. **Unlock rule = lessons + exercises.** Course N+1 unlocks when course N has every lesson complete AND every exercise submitted/passed. Sequential unlocking is a per-path toggle (off = take in any order). +6. **Path certificate is a teacher toggle** with its own admin tab; issued automatically on full completion; appears in the learner's LMS Certificates page. +7. **Teacher workspace tabs**: Courses (ordering), People, Analytics, Landing page, Certificate, Settings. **No path newsfeed** (Programs covers cohort communication). +8. **Visitor access is teacher-configurable**: teaser only / full syllabus / syllabus + preview lessons. +9. **Public path page follows the Coursera professional-certificate shape**: hero with stats + enroll CTA, what-you'll-learn, skills tags, sequential course cards with per-course outcomes and lesson outlines, certificate block, instructors, testimonials, FAQ accordion, pricing card. +10. **Org landing page: Learning Paths section sits ABOVE the Courses section.** Plus a `/paths` catalog page and `/path/[slug]` detail page mirroring how `/courses` and `/course/[slug]` work today. +11. **Course independence**: a course can be in multiple paths and remain individually sellable. Removing a course from a path or a learner from a path never deletes `groupmember` rows or progress. +12. **Statuses**: `ACTIVE` (public + LMS), `DRAFT` (hidden while building), `ARCHIVED` (hidden everywhere, restorable). + +## Current-State Audit + +| Capability | Current state | Notes | +| --- | --- | --- | +| Grouping above course | `program` tables (unordered, admin-enrolled, newsfeed) | Keep as-is; do not extend | +| Enrollment | `groupmember` per course group; `enrollInCourse()` in `apps/api/src/services/course/invite.ts` | Reuse for auto-enrollment | +| Course completion | lesson completion + exercise submissions tracked per course | Source data for the unlock rule | +| Public org site | `(org-site)` routes + `packages/ui/src/custom/org-landing-page/` (10 themes, default `minimal`) | Mirror for paths section/list/detail | +| Course landing page | theme `course.svelte` composer: hero → anchor nav → social proof → info blocks → curriculum → chips → instructor → reviews → pricing | Path page reuses these shared components where possible | +| Payments | course payment flow on paid enroll CTA (`course-landing-page.svelte`) | Path enroll reuses the same flow with the path as the product | +| Certificates | course certificate system + LMS Certificates page | Add path-level issuance | +| Setup checklist pattern | `routes/(app)/org/[slug]/setup/+page.svelte` + `PercentRingProgress` + `Item.*` | Reuse for "Set up your path" and learner checklist view | + +## Product Goals + +1. A visitor can discover a path on the org landing page, read a full Coursera-style path page, and self-enroll (paid or free). +2. Enrolling in a path creates path membership and `groupmember` rows for every course in it. +3. A learner sees paths in the LMS (home section + Learning Paths nav), opens a path hub with per-course progress and locked/unlocked states, and moves through courses in order. +4. Inside a course that belongs to a path, the learner sees path context (position stepper, "next course unlocks when…"). +5. Teachers create a path via a setup checklist, order courses by drag, price the bundle, customize the public page, configure the certificate, and track a per-course funnel. +6. Completing all courses issues the path certificate (when enabled). +7. Programs, standalone courses, and existing enrollments are untouched (zero regression). + +## Non-Goals (v1) + +- Branching / non-linear paths (graphs, optional courses, parallel tracks). +- Path-level newsfeed or community. +- Subscriptions or installment pricing; coupon codes beyond what courses support. +- Path templates, cloning, or cross-org sharing. +- Per-learner overrides of the unlock rule (admin "skip ahead" can be a fast-follow). +- Team/B2B seat purchasing. + +--- + +## Functional Requirements + +### 1. Public — org landing site + +**Landing page section** (above Courses; see `org-landing.html`): +- Section heading "Learning Paths" + lead line; grid of path cards. +- Path card: "Learning Path" chip, name, description, ordered course preview list (first 3 + "+N more"), meta row (course count, total hours, certificate), bundle price with struck-through sum. +- "View all learning paths" → `/paths`. +- Rendered per landing-page theme like the courses section (start with `minimal`; other themes can fall back to base styling). + +**Catalog page `/paths`** (see `public-paths.html`): compact hero with search, filter sidebar (topic/tags, pricing, length), path card list, pagination. Mirrors `(org-site)/courses`. + +**Detail page `/path/[slug]`** (see `public-path.html`), section order: +1. Hero: chip, headline, subheadline (teacher-editable), instructor avatar stack, Enroll CTA with price + savings chip, sticky enroll box (price, savings, feature list, CTA, enrolled/rating counts). +2. Stats row: courses, total hours, level, rating. +3. Sticky anchor nav: About / Courses / Certificate / Instructors / Testimonials / FAQ. +4. What you'll learn (outcome bullets) + Skills tags. +5. Course series: one card per course in order — number, title, lesson count + duration, per-course outcome bullets, expandable lesson outline (lessons marked preview are openable when the teacher allows). +6. Certificate block (when enabled). +7. Instructors (auto-derived from course tutors, hideable). +8. Testimonials (teacher-managed, hideable). +9. FAQ accordion (teacher-managed, hideable). +10. Pricing card + footer. +- Enroll: free path → enroll flow like `/course/{slug}/enroll`; paid path → payment flow with the path as product. Requires auth like course enrollment. +- Visitor access level (teaser / syllabus / syllabus+preview) gates sections 5's lesson detail. +- JSON-LD `Course`/`LearningResource` schema like the course page. + +### 2. Learner — LMS + +**Home** (`lms-home.html`): "Learning Paths" section above Courses when `enrolledPaths.length > 0`; path cards show segmented per-course progress (one segment per course: done/current/locked) + overall %, and "Next: {course}". Course cards belonging to a path carry a "{path} · i/N" chip. + +**Nav**: "Learning Paths" item in the LMS sidebar with enrolled count badge, shown only when enrolled in ≥1 path. + +**List `/lms/paths`** (`paths.html`): tabs In progress / Completed / All; rows with segment bar, next course, Continue / View certificate. + +**Path hub `/lms/paths/[id]`** (`path-detail.html`): the journey spine — a vertical rail of course nodes that fills as courses complete. Per course: state (Completed / In progress / Locked), progress bar, lesson+exercise counts, unlock note ("Unlocks after Course N"), certificate end-row. + +**Course in path** (`course-in-path.html`): when the visited course belongs to a path the learner is in, wrap the course view with a path ribbon — back pill to the path, "Course 3 of 5", overall path %, horizontal stepper (done/current/locked) — and a "Next in your path" callout under the lesson showing what completing this course unlocks. + +**Unlock enforcement**: locked courses are not openable from path surfaces; direct navigation to a locked course (URL) shows a "locked — finish {course} first" state. Learners enrolled in a course independently (outside the path) keep their normal access. + +### 3. Teacher — admin dashboard + +**Org listing `/org/[slug]/paths`** (`teacher-paths.html`): nav item "Learning Paths" after Courses; stat cards (active paths, enrolled learners, completions); table (name, courses, learners, status); Create path → creation modal → setup checklist. + +**Setup checklist `/paths/[id]/setup`** (`teacher-path-setup.html`): Get Started pattern (ring + dots + item rows): name/describe ✓ auto, add courses, set order, set price, customize landing page, activate. Each step deep-links to its tab. Shown until complete; "Finish setup · N%" chip in the workspace top bar. + +**Path workspace `/paths/[id]/*`** — course-style sidebar (back link, path identity + status, tabs): + +| Tab | File | Contents | +| --- | --- | --- | +| Courses | `teacher-path-builder.html` | "Unlock courses in order" toggle; drag-to-reorder course rows (order, thumb, lessons/exercises/price, open/remove); add-course picker (org courses not in path); certificate end-row. Remove-course confirmation notes nobody is unenrolled. | +| People | `teacher-path-people.html` | Auto-enroll info banner; search + progress filter; table: learner, path progress bar, current course, enrolled date, actions (view, remove — preserves course access). Add learners (batch email, like course invites). | +| Analytics | `teacher-path-analytics.html` | Stat cards (enrolled, active, completion rate, avg time); per-course funnel with drop-off callouts; "stuck" list (lessons/exercises blocking most learners). | +| Landing page | `teacher-path-landing-editor.html` | Hero headline/subheadline; visitor access radio (teaser / syllabus / syllabus+previews); what-you'll-learn bullets; skills tags; instructors toggle (auto from course tutors); testimonials CRUD + toggle; FAQ CRUD + toggle; "View live page". | +| Certificate | `teacher-path-certificate.html` | Award toggle; requirements summary (all lessons + all exercises); title + issuer fields; live preview; awarded count. | +| Settings | `teacher-path-settings.html` | General (name, description, cover); Pricing (bundle price, currency, show-savings toggle with computed sum); Enrollment & flow (self-enrollment toggle, unlock-in-order toggle, auto-enroll toggle); Status radio (Active / Draft / Archived); Danger zone (delete path — courses & progress preserved). | + +### 4. Access control + +- Org admins manage all paths; path-level TUTOR role can manage content but not delete (mirror Programs roles: ADMIN/TUTOR/STUDENT via `roleId`). +- Students: read path data they're members of; public endpoints serve ACTIVE paths only. +- DRAFT paths: admin/tutor only. ARCHIVED: hidden everywhere, data preserved. + +--- + +## Technical Design + +### Data model (`packages/db/src/schema.ts`) + +``` +learning_path + id uuid PK · organizationId FK(organization, cascade) · name varchar · slug varchar (unique per org) + description text · coverImage text · status LEARNING_PATH_STATUS default 'DRAFT' + cost numeric · currency varchar default 'USD' · showSavings boolean default true + sequentialUnlock boolean default true · selfEnrollment boolean default true · autoEnroll boolean default true + certificateEnabled boolean default true · certificateTitle text · certificateIssuer text + landingPage jsonb -- { headline, subheadline, visitorAccess: 'teaser'|'syllabus'|'preview', + -- outcomes: string[], skills: string[], showInstructors, testimonials: [...], + -- showTestimonials, faqs: [...], showFaqs } + visitorAccess is inside landingPage; no relational IDs inside the jsonb (per repo rule) + createdByProfileId FK(profile) · createdAt / updatedAt + +learning_path_course + id uuid PK · pathId FK(learning_path, cascade) · courseId FK(course, cascade) + order integer NOT NULL -- 1-based position; the gating sequence + unique(pathId, courseId) · unique(pathId, order) · index(pathId) + +learning_path_member + id uuid PK · pathId FK(learning_path, cascade) · profileId FK(profile) · roleId FK(role) + enrolledAt timestamptz · completedAt timestamptz nullable + certificateIssuedAt timestamptz nullable · certificateId varchar nullable + unique(pathId, profileId) · index(pathId) · index(profileId) + +enum LEARNING_PATH_STATUS: ACTIVE | DRAFT | ARCHIVED +``` + +Notes: +- No denormalized progress on `learning_path_member` beyond `completedAt`; per-course progress is computed from existing lesson-completion + exercise-submission data (already queried for course cards). +- Savings figure is computed at read time from the sum of `course.cost` over path courses — never stored. +- Schema work stops at a passing `@cio/db` build; migrations are handled outside this workflow. + +### Completion / unlock logic (service layer) + +``` +courseCompleteForPath(profileId, courseId): + lessonsComplete = completedLessons == totalLessons + exercisesComplete = submittedOrPassedExercises == totalExercises + return lessonsComplete && exercisesComplete + +unlockedCourses(path, profileId): + if !path.sequentialUnlock → all + else → courses[0..k] where k = first index whose course is not complete +``` + +Path completion: all courses complete → set `completedAt`; if `certificateEnabled`, issue certificate (id `LP-XXXX-XXXX`), set `certificateIssuedAt`. Evaluate on lesson/exercise completion events for members of paths containing that course (reuse the pattern used by course-completion side effects). + +### API routes (`apps/api/src/routes/learning-path/`) + +Follow the standard layering (validation in `packages/utils/src/validation/learning-path/`, queries in `packages/db/src/queries/learning-path/`, services in `apps/api/src/services/learning-path/`). Mount as a single root segment: `.route('/learning-path', learningPathRouter)`. + +| Method | Path | Auth | Description | +| --- | --- | --- | --- | +| GET | `/learning-path` | org admin | List org paths (admin) | +| POST | `/learning-path` | org admin | Create (name, description) → DRAFT | +| GET | `/learning-path/:pathId` | member/admin | Detail incl. ordered courses + caller progress | +| PUT | `/learning-path/:pathId` | admin/tutor | Update settings/pricing/landingPage/status | +| DELETE | `/learning-path/:pathId` | org admin | Delete (courses/progress preserved) | +| POST | `/learning-path/:pathId/courses` | admin/tutor | Add course (+ auto-enroll existing students) | +| PUT | `/learning-path/:pathId/courses/order` | admin/tutor | Reorder (array of courseIds) | +| DELETE | `/learning-path/:pathId/courses/:courseId` | admin/tutor | Remove (no unenrollment) | +| GET | `/learning-path/:pathId/members` | admin/tutor | Members + per-course progress | +| POST | `/learning-path/:pathId/members` | admin/tutor | Batch add (email+role) + auto-enroll | +| DELETE | `/learning-path/:pathId/members/:memberId` | admin/tutor | Remove (course access preserved) | +| GET | `/learning-path/:pathId/analytics` | admin/tutor | Funnel + stuck items | +| POST | `/learning-path/:pathId/enroll` | auth user | Self-enroll (free) / post-payment callback (paid) | +| GET | `/organization/learning-paths/enrolled` | auth user | LMS: caller's paths with progress + unlock states | + +Public (org-site loaders, no auth): list ACTIVE paths for landing/catalog; get path by slug with landingPage content filtered by `visitorAccess`. + +### Frontend plan (dashboard) + +Follow CLAUDE.md conventions: types in `features/learning-path/utils/types.ts` inferred from the API, API classes in `features/learning-path/api/*.svelte.ts`, thin components, all copy in `en.json` under `"learningPath"`, `ui:` prefix for theme colors. + +- **Admin**: nav item in `org-navigation.ts` after Courses; routes `org/[slug]/paths/+page.svelte` and `paths/[id]/{setup,courses,people,analytics,landing,certificate,settings}` with a `PathSidebar` mirroring the course sidebar pattern. Reuse `Item.*`, `PercentRingProgress`, `Field.*`, existing drag-reorder approach from lesson ordering if present. +- **LMS**: `lms/paths` + `lms/paths/[id]`; path ribbon injected in the course layout when course ∈ caller's path; reuse `course-progress-card` math for per-course %. +- **Public**: paths section added to org landing themes (start `minimal`), `(org-site)/paths` and `(org-site)/path/[slug]` mirroring the courses equivalents; reuse `CourseSectionNav`, `CourseSocialProof`, `CourseCurriculum`-style rows, `CoursePricing` card, `LandingButton`, footer. + +### Build verification + +```bash +pnpm --filter @cio/utils build && pnpm --filter @cio/db build && pnpm --filter @cio/api build +pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build +pnpm format:check +``` + +--- + +## Implementation Order + +1. **Schema + validation + queries** (`learning_path*` tables, Zod schemas, query layer). +2. **API**: CRUD + course ordering + members + enroll service (auto-enroll transaction, `ON CONFLICT DO NOTHING` idempotency) + unlock/completion service + enrolled-paths endpoint. +3. **Admin UI**: listing → workspace shell + Courses tab → Settings → People → setup checklist → Landing editor → Certificate → Analytics. +4. **LMS**: enrolled section on home + nav, list, path hub (pick journey or checklist), course-in-path ribbon + lock enforcement. +5. **Public**: landing section (minimal theme) → catalog → detail page → enroll flow (free first, then paid via existing payment flow). +6. **Certificate issuance** + LMS Certificates page integration. + +## Acceptance Criteria + +1. Teacher can create a path, add + reorder courses, set a bundle price, and activate it; the setup checklist reflects real completion. +2. ACTIVE paths appear on the org landing page above courses, in `/paths`, and at `/path/[slug]` with all enabled sections; DRAFT/ARCHIVED paths do not. +3. Savings displays as bundle price vs summed course prices and updates when course prices change. +4. Visitor access level correctly gates lesson outlines/previews for non-enrolled visitors. +5. Enrolling (free or paid) creates the member row and `groupmember` rows for all path courses, idempotently. +6. With sequential unlock on, course N+1 is locked until course N's lessons AND exercises are complete — enforced in UI and API; toggle off restores free order. +7. Learner LMS shows path progress (segments + %), locked states, and the in-course path ribbon; direct URL access to a locked course is blocked with an explanatory state. +8. Completing all courses sets `completedAt` and (when enabled) issues the path certificate, visible in LMS Certificates. +9. Removing a course from a path or a member from a path never deletes course enrollments or progress. +10. Adding a course to a path with existing students auto-enrolls them in that course. +11. Analytics funnel counts match member course-completion data. +12. Programs and standalone courses behave exactly as before. +13. All user-facing strings use translation keys; all builds and `pnpm format:check` pass. + +## Risks and Mitigations + +- **Unlock-rule evaluation cost** (checked on every lesson/exercise completion): scope the check to paths containing that course with the learner as member; index `learning_path_course.courseId`. +- **Auto-enroll transaction size** for large paths/cohorts: batch inserts with `ON CONFLICT DO NOTHING`; >500 members → background job (same mitigation as Programs). +- **Price drift**: savings computed at read time, so course price changes are always reflected; if a course in an active path becomes free/pricier, surface a notice in the Settings pricing section. +- **Learner already enrolled in a member course independently**: idempotent enrollment; their existing progress counts toward the unlock rule immediately (decide: acceptable and desirable). +- **Locked-course deep links** shared between learners: API must enforce the lock (not just UI), returning a typed error the frontend renders as the locked state. +- **Landing theme sprawl** (10 themes): v1 ships the paths section/pages for the base + `minimal` theme with a neutral fallback for others, mirroring how course landing tokens are layered. diff --git a/prototypes/learning-paths/app-theme.css b/prototypes/learning-paths/app-theme.css new file mode 100644 index 00000000..50449607 --- /dev/null +++ b/prototypes/learning-paths/app-theme.css @@ -0,0 +1,1178 @@ +/* + * app-theme.css — shared theme for the learning-paths prototypes. + * + * Tokens mirror packages/ui/src/index.css (shadcn-svelte) exactly: + * OKLCH palette, Geist, --radius: 0.625rem, shadow-2xs, and the component + * recipes from @cio/ui base components (Button, Badge, Item, Progress, Sidebar). + * Dark mode uses the same `.dark` class convention as the app. + */ + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.488 0.243 264.376); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + + /* Semantic states from @cio/ui Badge variants (emerald / amber) */ + --success: oklch(0.596 0.145 163.225); /* emerald-600 */ + --success-foreground: #fff; + --success-soft: oklch(0.596 0.145 163.225 / 0.12); + --warning: oklch(0.666 0.179 58.318); /* amber-600 */ + --warning-foreground: #fff; + --warning-soft: oklch(0.666 0.179 58.318 / 0.14); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.424 0.199 265.638); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + + --success: oklch(0.696 0.17 162.48); /* emerald-500 */ + --success-foreground: oklch(0.262 0.051 172.552); /* emerald-950 */ + --success-soft: oklch(0.696 0.17 162.48 / 0.16); + --warning: oklch(0.769 0.188 70.08); /* amber-500 */ + --warning-foreground: oklch(0.279 0.077 45.635); /* amber-950 */ + --warning-soft: oklch(0.769 0.188 70.08 / 0.16); +} + +/* ============ base ============ */ +* { + box-sizing: border-box; + border-color: var(--border); +} +html, +body { + margin: 0; + padding: 0; +} +body { + font-family: + 'Geist', + ui-sans-serif, + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + background: var(--background); + color: var(--foreground); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +a { + color: inherit; + text-decoration: none; +} +button { + font: inherit; +} +.tnum { + font-variant-numeric: tabular-nums; +} +.text-muted { + color: var(--muted-foreground); +} +.text-primary { + color: var(--primary); +} + +/* ============ app shell ============ */ +.app { + display: grid; + grid-template-columns: 256px 1fr; + min-height: 100vh; +} +.main { + display: flex; + flex-direction: column; + min-width: 0; +} +.content { + padding: 28px 28px 64px; +} +.wrap { + max-width: 960px; + margin: 0 auto; +} +.wrap-narrow { + max-width: 760px; + margin: 0 auto; +} + +/* sidebar (mirrors @cio/ui base/sidebar) */ +.sidebar { + background: var(--sidebar); + border-right: 1px solid var(--sidebar-border); + padding: 16px 12px; + display: flex; + flex-direction: column; + gap: 2px; + position: sticky; + top: 0; + height: 100vh; +} +.brand { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px 14px; +} +.brand-mark { + width: 28px; + height: 28px; + border-radius: var(--radius-md); + background: var(--primary); + color: var(--primary-foreground); + display: grid; + place-items: center; + font-weight: 700; + font-size: 14px; +} +.brand-name { + font-weight: 600; + font-size: 14px; + letter-spacing: -0.01em; +} +.nav-label { + font-size: 11px; + color: var(--muted-foreground); + font-weight: 500; + padding: 12px 8px 4px; +} +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: var(--radius-md); + color: var(--sidebar-foreground); + font-weight: 400; + font-size: 14px; + transition: background 0.1s; +} +.nav-item svg { + width: 16px; + height: 16px; + stroke-width: 2; + color: var(--muted-foreground); + flex-shrink: 0; +} +.nav-item:hover { + background: var(--sidebar-accent); +} +.nav-item.active { + background: var(--sidebar-accent); + color: var(--sidebar-accent-foreground); + font-weight: 500; +} +.nav-item.active svg { + color: var(--sidebar-accent-foreground); +} +.nav-tail { + margin-left: auto; + font-size: 12px; + color: var(--muted-foreground); +} +.nav-badge { + margin-left: auto; + font-size: 11px; + font-weight: 600; + background: var(--primary); + color: var(--primary-foreground); + border-radius: 999px; + padding: 1px 7px; +} +.sidebar-foot { + margin-top: auto; + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-top: 1px solid var(--sidebar-border); +} +.avatar { + width: 30px; + height: 30px; + border-radius: 999px; + background: var(--secondary); + color: var(--secondary-foreground); + display: grid; + place-items: center; + font-weight: 600; + font-size: 12px; + flex-shrink: 0; +} +.who { + min-width: 0; +} +.who .nm { + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.who .rl { + font-size: 11.5px; + color: var(--muted-foreground); +} + +/* path identity block (teacher path sidebar) */ +.back-link { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--muted-foreground); + padding: 6px 8px; + border-radius: var(--radius-md); + margin-bottom: 4px; +} +.back-link:hover { + background: var(--sidebar-accent); + color: var(--foreground); +} +.back-link svg { + width: 15px; + height: 15px; + stroke-width: 2; +} +.path-id { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 8px 14px; + border-bottom: 1px solid var(--sidebar-border); + margin-bottom: 6px; +} +.path-id .ico { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + background: var(--primary); + color: var(--primary-foreground); + display: grid; + place-items: center; + flex-shrink: 0; +} +.path-id .ico svg { + width: 16px; + height: 16px; + stroke-width: 2; +} +.path-id .nm { + font-size: 13px; + font-weight: 600; + line-height: 1.3; + letter-spacing: -0.01em; +} +.path-id .st { + font-size: 11.5px; + font-weight: 500; + display: flex; + align-items: center; + gap: 5px; + margin-top: 2px; +} +.path-id .st .dot { + width: 6px; + height: 6px; + border-radius: 999px; +} +.st-active { + color: var(--success); +} +.st-active .dot { + background: var(--success); +} +.st-draft { + color: var(--warning); +} +.st-draft .dot { + background: var(--warning); +} + +/* topbar */ +.topbar { + height: 56px; + border-bottom: 1px solid var(--border); + background: color-mix(in oklab, var(--background) 90%, transparent); + backdrop-filter: blur(8px); + position: sticky; + top: 0; + z-index: 100; + display: flex; + align-items: center; + gap: 12px; + padding: 0 24px; +} +.crumbs { + display: flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + color: var(--muted-foreground); + min-width: 0; +} +.crumbs a:hover { + color: var(--foreground); +} +.crumbs .sep { + color: var(--border); +} +.crumbs .here { + color: var(--foreground); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.top-title { + font-weight: 600; + font-size: 15px; + letter-spacing: -0.01em; +} +.top-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; +} + +/* ============ buttons (mirrors @cio/ui base/button) ============ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 500; + white-space: nowrap; + height: 36px; + padding: 0 16px; + border: 1px solid transparent; + cursor: pointer; + transition: + background 0.12s, + border-color 0.12s, + color 0.12s; + box-shadow: var(--shadow-2xs); + user-select: none; +} +.btn svg { + width: 16px; + height: 16px; + stroke-width: 2; + flex-shrink: 0; +} +.btn-primary { + background: var(--primary); + color: var(--primary-foreground); +} +.btn-primary:hover { + background: color-mix(in oklab, var(--primary) 90%, transparent); +} +.btn-outline { + background: var(--background); + border-color: var(--input); + color: var(--foreground); +} +.btn-outline:hover { + background: var(--accent); + color: var(--accent-foreground); +} +.btn-secondary { + background: var(--secondary); + color: var(--secondary-foreground); +} +.btn-secondary:hover { + background: color-mix(in oklab, var(--secondary) 80%, transparent); +} +.btn-ghost { + background: transparent; + box-shadow: none; + color: var(--foreground); +} +.btn-ghost:hover { + background: var(--accent); +} +.btn-destructive { + background: var(--destructive); + color: #fff; +} +.btn-success { + background: var(--success); + color: var(--success-foreground); +} +.btn-sm { + height: 32px; + padding: 0 12px; + font-size: 13px; + gap: 6px; +} +.btn-sm svg { + width: 15px; + height: 15px; +} +.btn-lg { + height: 40px; + padding: 0 24px; +} +.btn[disabled], +.btn.disabled { + opacity: 0.5; + pointer-events: none; + cursor: not-allowed; +} +.btn-icon { + width: 36px; + padding: 0; +} +.btn-icon.btn-sm { + width: 32px; +} + +/* ============ badges (mirrors @cio/ui base/badge) ============ */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + width: fit-content; + border-radius: var(--radius-md); + border: 1px solid transparent; + padding: 2px 8px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} +.badge svg { + width: 12px; + height: 12px; + stroke-width: 2.4; +} +.badge-default { + background: var(--primary); + color: var(--primary-foreground); +} +.badge-secondary { + background: var(--secondary); + color: var(--secondary-foreground); +} +.badge-success { + background: var(--success); + color: var(--success-foreground); +} +.badge-warning { + background: var(--warning); + color: var(--warning-foreground); +} +.badge-outline { + border-color: var(--border); + color: var(--foreground); + background: transparent; +} +.badge-success-soft { + background: var(--success-soft); + color: var(--success); +} +.badge-primary-soft { + background: color-mix(in oklab, var(--primary) 12%, transparent); + color: var(--primary); +} +.badge-muted { + background: var(--muted); + color: var(--muted-foreground); +} + +/* ============ cards ============ */ +.card { + background: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-2xs); +} +.card-pad { + padding: 20px; +} + +/* ============ item rows (mirrors @cio/ui base/item) ============ */ +.item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + border-radius: var(--radius-md); + border: 1px solid transparent; + padding: 16px; + font-size: 14px; + transition: + background 0.1s, + border-color 0.1s; +} +.item-muted { + background: color-mix(in oklab, var(--muted) 50%, transparent); +} +.item-outline { + border-color: var(--border); +} +.item-done { + opacity: 0.6; +} +.item-done .item-title, +.item-done .item-desc { + color: var(--muted-foreground); + text-decoration: line-through; +} +.item-current { + background: var(--card); + border-color: var(--primary); + box-shadow: var(--shadow-sm); +} +.item-dashed { + border: 1px dashed var(--border); + background: transparent; +} +a.item:hover { + background: color-mix(in oklab, var(--accent) 50%, transparent); +} + +.item-media-icon { + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: var(--muted); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + align-self: flex-start; + margin-top: 2px; + color: var(--muted-foreground); +} +.item-media-icon svg { + width: 16px; + height: 16px; + stroke-width: 2; +} +.item-media-icon.is-done { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.item-media-icon.is-current { + background: color-mix(in oklab, var(--primary) 10%, transparent); + border-color: color-mix(in oklab, var(--primary) 30%, transparent); + color: var(--primary); +} +.item-media-lg { + width: 40px; + height: 40px; +} +.item-media-lg svg { + width: 19px; + height: 19px; +} +.item-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.item-title { + font-size: 14px; + font-weight: 500; + line-height: 1.4; + letter-spacing: -0.01em; +} +.item-title-lg { + font-size: 15px; + font-weight: 600; +} +.item-desc { + font-size: 13px; + color: var(--muted-foreground); + line-height: 1.45; +} +.item-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.item-eyebrow { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--muted-foreground); +} + +/* ============ progress (mirrors @cio/ui base/progress) ============ */ +.progress { + height: 8px; + border-radius: var(--radius-md); + overflow: hidden; + background: color-mix(in oklab, var(--primary) 20%, transparent); + flex: 1; +} +.progress > span { + display: block; + height: 100%; + background: var(--primary); + border-radius: var(--radius-md); + transition: width 0.3s; +} +.progress-success > span { + background: var(--success); +} +.progress-thin { + height: 6px; +} + +/* progress ring (mirrors @cio/ui custom/percent-ring-progress) */ +.ring { + position: relative; + width: 56px; + height: 56px; + flex-shrink: 0; +} +.ring-sm { + width: 40px; + height: 40px; +} +.ring svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); + display: block; +} +.ring .track { + stroke: var(--border); +} +.ring .arc { + stroke: var(--success); + transition: stroke-dashoffset 0.7s ease-out; +} +.ring .lbl { + position: absolute; + inset: 0; + display: grid; + place-items: center; + font-size: 12.5px; + font-weight: 600; + color: var(--foreground); +} +.ring-sm .lbl { + font-size: 10px; +} + +/* completion dots (setup page pattern) */ +.dots { + display: flex; + gap: 4px; +} +.dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--border); +} +.dot.on { + background: var(--success); +} + +/* ============ forms ============ */ +.input, +.textarea, +.select { + width: 100%; + font: inherit; + font-size: 14px; + color: var(--foreground); + background: transparent; + border: 1px solid var(--input); + border-radius: var(--radius-md); + padding: 7px 12px; + outline: none; + box-shadow: var(--shadow-2xs); + transition: + border-color 0.12s, + box-shadow 0.12s; +} +.input:focus, +.textarea:focus { + border-color: var(--ring); + box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 50%, transparent); +} +.textarea { + resize: vertical; + min-height: 76px; + line-height: 1.55; +} +.label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 6px; +} +.hint { + font-size: 12px; + color: var(--muted-foreground); + margin-top: 5px; +} +.field { + margin-bottom: 18px; +} +.field:last-child { + margin-bottom: 0; +} + +/* switch */ +.switch { + position: relative; + width: 36px; + height: 20px; + flex-shrink: 0; + cursor: pointer; + display: inline-block; +} +.switch input { + position: absolute; + opacity: 0; + inset: 0; + margin: 0; + cursor: pointer; + z-index: 1; +} +.switch .track { + position: absolute; + inset: 0; + background: var(--input); + border-radius: 999px; + transition: background 0.15s; +} +.switch .knob { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 999px; + background: var(--background); + box-shadow: var(--shadow-sm); + transition: transform 0.15s; +} +.switch input:checked + .track { + background: var(--primary); +} +.switch input:checked ~ .knob { + transform: translateX(16px); +} + +/* ============ tables ============ */ +.thead, +.trow { + display: grid; + align-items: center; + gap: 14px; + padding: 12px 16px; +} +.thead { + font-size: 12px; + color: var(--muted-foreground); + font-weight: 500; + border-bottom: 1px solid var(--border); +} +.trow { + border-bottom: 1px solid var(--border); + font-size: 14px; +} +.trow:last-child { + border-bottom: none; +} +a.trow:hover { + background: color-mix(in oklab, var(--accent) 50%, transparent); +} + +/* ============ misc components ============ */ +.icon-btn { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + border: 1px solid var(--input); + background: var(--background); + display: grid; + place-items: center; + color: var(--muted-foreground); + cursor: pointer; + box-shadow: var(--shadow-2xs); +} +.icon-btn:hover { + color: var(--foreground); + background: var(--accent); +} +.icon-btn svg { + width: 15px; + height: 15px; + stroke-width: 2; +} + +.seg-toggle { + display: inline-flex; + padding: 3px; + background: var(--muted); + border-radius: var(--radius-lg); + gap: 2px; +} +.seg-toggle a, +.seg-toggle button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: var(--radius-md); + border: none; + background: transparent; + font-size: 13px; + font-weight: 500; + color: var(--muted-foreground); + cursor: pointer; +} +.seg-toggle .on { + background: var(--background); + color: var(--foreground); + box-shadow: var(--shadow-2xs); +} +.seg-toggle svg { + width: 14px; + height: 14px; + stroke-width: 2; +} + +.stat-card { + padding: 16px 18px; +} +.stat-card .k { + font-size: 13px; + color: var(--muted-foreground); + font-weight: 500; + display: flex; + align-items: center; + gap: 7px; +} +.stat-card .k svg { + width: 15px; + height: 15px; + stroke-width: 2; +} +.stat-card .v { + font-size: 26px; + font-weight: 600; + letter-spacing: -0.02em; + margin-top: 4px; +} +.stat-card .s { + font-size: 12px; + color: var(--muted-foreground); + margin-top: 2px; +} + +/* page headers */ +.page-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin: 8px 0 20px; +} +.page-title { + font-size: 24px; + font-weight: 400; + letter-spacing: -0.025em; + margin: 0 0 4px; +} +.page-sub { + color: var(--muted-foreground); + font-size: 14px; + margin: 0; +} +.sec-title { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.01em; + margin: 0; +} +.sec-head { + display: flex; + align-items: center; + gap: 10px; + margin: 28px 0 14px; +} +.sec-head .count { + font-size: 12px; + font-weight: 500; + color: var(--muted-foreground); + background: var(--muted); + padding: 1px 8px; + border-radius: 999px; +} +.sec-head .more { + margin-left: auto; + font-size: 13.5px; + color: var(--primary); + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; +} +.sec-head .more svg { + width: 14px; + height: 14px; + stroke-width: 2; +} + +/* segmented path progress (course-count segments) */ +.segs { + display: flex; + gap: 4px; + flex: 1; +} +.seg { + flex: 1; + height: 6px; + border-radius: 999px; + background: color-mix(in oklab, var(--primary) 15%, transparent); + position: relative; + overflow: hidden; +} +.seg.done { + background: var(--success); +} +.seg.half::after { + content: ''; + position: absolute; + inset: 0; + width: 45%; + background: var(--primary); + border-radius: 999px; +} + +/* lock veil for thumbnails */ +.lock-veil { + position: absolute; + inset: 0; + background: oklch(0.145 0 0 / 0.35); + display: grid; + place-items: center; +} +.lock-veil svg { + width: 18px; + height: 18px; + color: #fff; + stroke-width: 2.2; +} + +/* ============ journey spine (learner path detail) ============ */ +.journey { + position: relative; + margin-top: 16px; +} +.step { + position: relative; + display: grid; + grid-template-columns: 36px 1fr; + gap: 16px; + padding-bottom: 16px; +} +.step:last-child { + padding-bottom: 0; +} +.rail { + position: relative; + display: flex; + justify-content: center; +} +.rail::before { + content: ''; + position: absolute; + top: 38px; + bottom: -16px; + width: 2px; + background: var(--border); + border-radius: 2px; +} +.step:last-child .rail::before, +.step.no-rail .rail::before { + display: none; +} +.step.s-done .rail::before { + background: var(--success); +} +.step.s-current .rail::before { + background: linear-gradient(180deg, var(--success) 0%, var(--border) 70%); +} +.node { + width: 36px; + height: 36px; + border-radius: 999px; + z-index: 1; + display: grid; + place-items: center; + font-weight: 600; + font-size: 13px; + background: var(--muted); + color: var(--muted-foreground); + border: 1px solid var(--border); +} +.node svg { + width: 17px; + height: 17px; + stroke-width: 2.4; +} +.step.s-done .node { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.step.s-current .node { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); + box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary) 15%, transparent); +} + +/* horizontal stepper (in-course ribbon) */ +.stepper { + display: flex; + align-items: center; +} +.snode { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + flex-shrink: 0; + width: 110px; + text-align: center; +} +.sdot { + width: 28px; + height: 28px; + border-radius: 999px; + display: grid; + place-items: center; + font-weight: 600; + font-size: 12px; + background: var(--muted); + color: var(--muted-foreground); + border: 1px solid var(--border); +} +.sdot svg { + width: 14px; + height: 14px; + stroke-width: 2.6; +} +.snode.done .sdot { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.snode.current .sdot { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); + box-shadow: 0 0 0 3px color-mix(in oklab, var(--primary) 15%, transparent); +} +.slabel { + font-size: 11px; + font-weight: 500; + color: var(--muted-foreground); + line-height: 1.25; +} +.snode.current .slabel { + color: var(--foreground); + font-weight: 600; +} +.sbar { + flex: 1; + height: 2px; + background: var(--border); + min-width: 12px; + margin: 0 -14px 18px; +} +.sbar.filled { + background: var(--success); +} + +/* ============ responsive ============ */ +@media (max-width: 860px) { + .app { + grid-template-columns: 1fr; + } + .sidebar { + display: none; + } + .item { + flex-wrap: wrap; + } + .item-actions { + width: 100%; + } + .item-actions .btn { + flex: 1; + } +} diff --git a/prototypes/learning-paths/course-in-path.html b/prototypes/learning-paths/course-in-path.html new file mode 100644 index 00000000..442fb921 --- /dev/null +++ b/prototypes/learning-paths/course-in-path.html @@ -0,0 +1,304 @@ + + + + + + Course in Path · ClassroomIO LMS + + + + + + +
+ + + + +
+ +
+ + Content + + + Learning Path +
+ + + + + 3 + + + + + Course 3 of 5 +
+
+
+ + +
+
+ + +
+
+
+

Handling events

+
+ + + + +
+
+ +
+ +
+ +
+

In React you respond to user interaction by passing event handlers as props to elements. Handlers are regular functions, and React normalizes the underlying browser event for you.

+

What you'll learn

+
    +
  • Attaching onClick, onChange, and onSubmit handlers
  • +
  • Reading values from the synthetic event object
  • +
  • Updating state in response to an event
  • +
+

By the end of this lesson you'll wire up a small form that updates the screen as the user types — the foundation for everything interactive you'll build in this path.

+
+ +
+
+ + NEXT IN YOUR PATH +
+
+
+
+ Course 4 + State Management & Data Fetching + + + Unlocks when you finish this course — 3 lessons & 2 exercises to go. + +
+ Locked +
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/index.html b/prototypes/learning-paths/index.html new file mode 100644 index 00000000..1723f1b1 --- /dev/null +++ b/prototypes/learning-paths/index.html @@ -0,0 +1,142 @@ + + + + + + Learning Paths — Prototype Map + + + + + + +
+ ClassroomIO · Prototype + PRD handoff +

Learning Paths

+

+ A learning path is an ordered bundle of courses sold and taken as one: + courses unlock sequentially (lessons + exercises complete → next course), one bundle price with savings, + and an optional path certificate at the end. Built on the real packages/ui tokens. + The engineering PRD lives at prd/learning-paths/README.md. +

+

Every page has a theme toggle and cross-links so you can click through each journey end to end.

+ + +
1 · Public — org landing site (visitor)
+
+ +
+
Entry
Org Landing Page

Learning Paths section above Courses — mirrors the "minimal" landing theme.

+
+ +
+
List
Paths Catalog

/paths — search, filters, and path cards with savings pricing (like /courses).

+
+ +
+
Detail · Coursera-style
Public Path Page

Hero + enroll box with savings, stats, outcomes, skills, course series with lesson outlines, certificate, instructors, testimonials, FAQ.

+
+
+ + +
2 · Learner — LMS (enrolled student)
+
+ +
+
Entry
LMS Home

Paths above courses, segmented cross-course progress bars.

+
+ +
+
List
My Learning Paths

In progress / Completed tabs; each row shows segment progress + next course.

+
+ +
+
Detail
Path Detail

The journey spine — Done / Continue / Locked courses with per-course progress and a certificate end-row.

+
+ +
+
Inside a course
Course in Path

Lesson view wrapped in path chrome: stepper ribbon + "next course unlocks" callout.

+
+
+ + +
3 · Teacher — admin dashboard
+
+ +
+
List
Manage Paths

Org-level listing: stats, statuses, Create path → setup checklist.

+
+ +
+
Onboarding
Set up your path

"Get Started"-style checklist: courses → order → price → landing page → activate.

+
+ +
+
Tab · Courses
Path Builder

Drag-to-reorder course list + sequential-unlock toggle. Try dragging rows.

+
+ +
+
Tab · People
Learners

Per-learner path progress, current course, auto-enroll note, add learners.

+
+ +
+
Tab · Analytics
Path Funnel

Course-by-course funnel, drop-off callouts, stuck lessons/exercises.

+
+ +
+
Tab · Landing page
Landing Editor

Hero copy, visitor access level, outcomes, skills, instructors, testimonials, FAQ.

+
+ +
+
Tab · Certificate
Path Certificate

Award toggle, completion requirements, live certificate preview.

+
+ +
+
Tab · Settings
Path Settings

General, bundle pricing + savings, enrollment & unlock rules, status, danger zone.

+
+
+ +
+ Key product decisions baked into these prototypes:
+ · Learning paths are a new entity (separate from Programs) — learning_path, learning_path_course (ordered), learning_path_member.
+ · Self-enrollment with bundle pricing from the public site; the price shows savings vs summed course prices.
+ · A course unlocks when the previous course's lessons AND exercises are complete; sequential unlocking is a per-path toggle.
+ · Path certificate is a teacher toggle with its own tab; visitor access to the syllabus is teacher-configurable.
+ · Public pages mirror the "minimal" org-landing theme; app pages mirror packages/ui (shadcn-svelte, Geist, OKLCH tokens). +
+
+ + + + + diff --git a/prototypes/learning-paths/landing-theme.css b/prototypes/learning-paths/landing-theme.css new file mode 100644 index 00000000..7526ffef --- /dev/null +++ b/prototypes/learning-paths/landing-theme.css @@ -0,0 +1,947 @@ +/* + * landing-theme.css — public org-site pages for the learning-paths prototypes. + * + * Mirrors packages/ui/src/custom/org-landing-page/ default theme ("minimal"): + * --landing-* CSS vars from theme-vars-base.ts, square-cornered cards + * (--landing-radius-card: 0), no card shadows, pill floating nav, accent + * radial-gradient hero. Accent = the org brand (default ClassroomIO indigo). + */ + +:root { + --landing-bg: oklch(1 0 0); + --landing-fg: oklch(0.185 0 0); + --landing-fg-muted: oklch(0.5 0 0); + --landing-border: oklch(0.9 0 0); + --landing-card: oklch(1 0 0); + --landing-card-soft: oklch(0.975 0 0); + --landing-accent: oklch(0.488 0.243 264.376); + --landing-accent-soft: oklch(0.488 0.243 264.376 / 0.08); + --landing-button-primary-bg: oklch(0.488 0.243 264.376); + --landing-button-primary-bg-hover: oklch(0.424 0.199 265.638); + --landing-button-primary-fg: #fff; + --landing-success: oklch(0.596 0.145 163.225); + --landing-radius-card: 0px; + --landing-radius-button: 6px; +} + +.dark { + --landing-bg: oklch(0.16 0 0); + --landing-fg: oklch(0.96 0 0); + --landing-fg-muted: oklch(0.68 0 0); + --landing-border: oklch(1 0 0 / 14%); + --landing-card: oklch(0.2 0 0); + --landing-card-soft: oklch(0.24 0 0); + --landing-accent: oklch(0.623 0.214 259.815); + --landing-accent-soft: oklch(0.623 0.214 259.815 / 0.14); + --landing-button-primary-bg: oklch(0.546 0.245 262.881); + --landing-button-primary-bg-hover: oklch(0.623 0.214 259.815); + --landing-success: oklch(0.696 0.17 162.48); +} + +* { + box-sizing: border-box; + border-color: var(--landing-border); +} +html { + scroll-behavior: smooth; +} +html, +body { + margin: 0; + padding: 0; +} +body { + font-family: + 'Geist', + ui-sans-serif, + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + background: var(--landing-bg); + color: var(--landing-fg); + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} +a { + color: inherit; + text-decoration: none; +} +button { + font: inherit; +} +.tnum { + font-variant-numeric: tabular-nums; +} +.lp-wrap { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} +.lp-wrap-narrow { + max-width: 880px; + margin: 0 auto; + padding: 0 20px; +} + +/* ===== pill nav (minimal theme) ===== */ +.lp-nav-outer { + padding: 20px 20px 0; + max-width: 1240px; + margin: 0 auto; +} +.lp-nav { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 14px 24px; + border-radius: 999px; + border: 1px solid color-mix(in oklab, var(--landing-border) 60%, transparent); + background: linear-gradient( + 135deg, + color-mix(in oklab, var(--landing-accent) 14%, transparent), + color-mix(in oklab, var(--landing-accent) 6%, transparent) + ); + backdrop-filter: blur(8px); +} +.lp-nav-brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + font-size: 19px; +} +.lp-nav-logo { + width: 32px; + height: 32px; + border-radius: 8px; + background: var(--landing-accent); + color: #fff; + display: grid; + place-items: center; + font-weight: 700; + font-size: 15px; +} +.lp-nav-links { + display: flex; + align-items: center; + gap: 28px; + font-size: 14.5px; +} +.lp-nav-links > a { + opacity: 0.72; +} +.lp-nav-links > a:hover, +.lp-nav-links > a.active { + opacity: 1; +} +.lp-nav-auth { + border: 1px solid var(--landing-border); + border-radius: 999px; + padding: 8px 18px; + font-size: 14px; + font-weight: 500; + background: var(--landing-card); +} +.lp-nav-auth:hover { + border-color: var(--landing-fg-muted); +} +@media (max-width: 760px) { + .lp-nav-links > a { + display: none; + } +} + +/* ===== hero ===== */ +.lp-hero { + position: relative; + padding: 72px 0 80px; + text-align: center; + overflow: hidden; + background: + radial-gradient(60% 90% at 50% -10%, var(--landing-accent-soft) 0%, transparent 70%), + linear-gradient(180deg, transparent, transparent); +} +.lp-hero-dots { + position: absolute; + inset: 0; + opacity: 0.08; + pointer-events: none; + background-image: radial-gradient(circle, var(--landing-fg) 1px, transparent 1.5px); + background-size: 22px 22px; +} +.lp-hero-inner { + position: relative; + max-width: 860px; + margin: 0 auto; + padding: 0 20px; +} +.lp-hero h1 { + font-size: clamp(34px, 6vw, 56px); + font-weight: 600; + letter-spacing: -0.03em; + line-height: 1.08; + margin: 0 0 20px; + text-wrap: balance; +} +.lp-hero .sub { + font-size: 19px; + font-weight: 300; + color: color-mix(in oklab, var(--landing-fg) 72%, transparent); + margin: 0 auto 32px; + max-width: 60ch; +} +.lp-cta-row { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + flex-wrap: wrap; +} + +/* ===== buttons ===== */ +.lp-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: var(--landing-radius-button); + border: 1px solid transparent; + font-weight: 500; + font-size: 15px; + padding: 10px 22px; + cursor: pointer; + transition: + background 0.12s, + border-color 0.12s; +} +.lp-btn svg { + width: 16px; + height: 16px; + stroke-width: 2; +} +.lp-btn-primary { + background: var(--landing-button-primary-bg); + color: var(--landing-button-primary-fg); +} +.lp-btn-primary:hover { + background: var(--landing-button-primary-bg-hover); +} +.lp-btn-secondary { + background: var(--landing-card); + border-color: var(--landing-border); + color: var(--landing-fg); +} +.lp-btn-secondary:hover { + border-color: var(--landing-fg-muted); +} +.lp-btn-lg { + padding: 13px 28px; + font-size: 16px; +} +.lp-btn-pill { + border-radius: 999px; +} +.lp-btn-block { + width: 100%; +} + +/* ===== sections ===== */ +.lp-section { + padding: 72px 0; +} +.lp-section + .lp-section { + border-top: 1px solid var(--landing-border); +} +.lp-section h2 { + font-size: 26px; + font-weight: 600; + letter-spacing: -0.02em; + margin: 0 0 8px; +} +.lp-section .lead { + color: var(--landing-fg-muted); + margin: 0 0 32px; + max-width: 62ch; +} +.lp-eyebrow { + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 600; + color: var(--landing-accent); + margin-bottom: 10px; + display: block; +} + +/* ===== cards (minimal: square, hairline, no shadow) ===== */ +.lp-card { + background: var(--landing-card); + border: 1px solid color-mix(in oklab, var(--landing-border) 60%, transparent); + border-radius: var(--landing-radius-card); + transition: border-color 0.15s; +} +a.lp-card:hover { + border-color: var(--landing-fg-muted); +} + +/* course card (minimal shape) */ +.lp-course-card { + display: flex; + flex-direction: column; + padding: 32px; +} +.lp-course-card h3 { + font-size: 20px; + font-weight: 400; + margin: 0 0 14px; + letter-spacing: -0.01em; +} +.lp-course-card .desc { + font-size: 15.5px; + line-height: 1.65; + color: var(--landing-fg-muted); + margin: 0 0 30px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +.lp-meta-row { + display: flex; + align-items: center; + gap: 20px; + font-size: 14px; + color: var(--landing-fg-muted); + margin-top: auto; + flex-wrap: wrap; +} +.lp-meta-row .m { + display: flex; + align-items: center; + gap: 6px; +} +.lp-meta-row svg { + width: 15px; + height: 15px; + stroke-width: 1.8; +} +.lp-meta-row .price { + margin-left: auto; + font-weight: 600; + color: var(--landing-fg); + font-size: 15px; +} +.lp-meta-row .price s { + color: var(--landing-fg-muted); + font-weight: 400; + font-size: 13px; + margin-left: 6px; +} + +/* path badge + save chip */ +.lp-chip { + display: inline-flex; + align-items: center; + gap: 6px; + width: fit-content; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--landing-accent); + background: var(--landing-accent-soft); + padding: 4px 10px; + margin-bottom: 16px; +} +.lp-chip svg { + width: 13px; + height: 13px; + stroke-width: 2.2; +} +.lp-save { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12.5px; + font-weight: 600; + color: var(--landing-success); + background: color-mix(in oklab, var(--landing-success) 10%, transparent); + padding: 3px 9px; +} + +/* ordered course list inside a path card */ +.lp-path-courses { + border-top: 1px solid var(--landing-border); + margin: 22px 0; +} +.lp-path-course { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--landing-border); + font-size: 14.5px; +} +.lp-path-course .n { + width: 22px; + height: 22px; + border-radius: 999px; + background: var(--landing-accent-soft); + color: var(--landing-accent); + display: grid; + place-items: center; + font-size: 11.5px; + font-weight: 600; + flex-shrink: 0; +} +.lp-path-course .t { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.lp-path-course .d { + font-size: 13px; + color: var(--landing-fg-muted); + flex-shrink: 0; +} + +/* ===== stats row (social proof) ===== */ +.lp-stats { + display: flex; + justify-content: center; + border-block: 1px solid var(--landing-border); + padding: 28px 0; + flex-wrap: wrap; +} +.lp-stat { + padding: 0 36px; + text-align: center; + border-right: 1px solid var(--landing-border); +} +.lp-stat:last-child { + border-right: none; +} +.lp-stat .v { + font-size: 24px; + font-weight: 600; + letter-spacing: -0.02em; +} +.lp-stat .k { + font-size: 13px; + color: var(--landing-fg-muted); + margin-top: 2px; +} +@media (max-width: 720px) { + .lp-stat { + padding: 10px 20px; + border-right: none; + } +} + +/* ===== anchor nav ===== */ +.lp-anchor-nav { + position: sticky; + top: 0; + z-index: 50; + background: color-mix(in oklab, var(--landing-bg) 92%, transparent); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--landing-border); +} +.lp-anchor-nav .in { + display: flex; + gap: 6px; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + overflow-x: auto; +} +.lp-anchor-nav a { + padding: 14px 14px; + font-size: 14.5px; + color: var(--landing-fg-muted); + border-bottom: 2px solid transparent; + white-space: nowrap; +} +.lp-anchor-nav a:hover { + color: var(--landing-fg); +} +.lp-anchor-nav a.active { + color: var(--landing-fg); + border-bottom-color: var(--landing-accent); + font-weight: 500; +} + +/* ===== outcomes / bullets ===== */ +.lp-outcomes { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px 32px; +} +.lp-outcome { + display: flex; + gap: 12px; + font-size: 15.5px; + line-height: 1.55; +} +.lp-outcome svg { + width: 18px; + height: 18px; + stroke-width: 2.2; + color: var(--landing-accent); + flex-shrink: 0; + margin-top: 3px; +} +@media (max-width: 720px) { + .lp-outcomes { + grid-template-columns: 1fr; + } +} + +/* skill tags */ +.lp-skills { + display: flex; + flex-wrap: wrap; + gap: 10px; +} +.lp-skill { + border: 1px solid var(--landing-border); + padding: 7px 16px; + font-size: 14px; + border-radius: 999px; + color: var(--landing-fg); +} + +/* ===== series (sequential course cards) ===== */ +.lp-series { + display: flex; + flex-direction: column; + gap: 16px; +} +.lp-series-card { + padding: 26px 28px; +} +.lp-series-head { + display: flex; + align-items: flex-start; + gap: 16px; +} +.lp-series-n { + width: 36px; + height: 36px; + border-radius: 999px; + background: var(--landing-accent-soft); + color: var(--landing-accent); + display: grid; + place-items: center; + font-weight: 600; + font-size: 15px; + flex-shrink: 0; +} +.lp-series-card h3 { + font-size: 18px; + font-weight: 500; + margin: 4px 0 2px; + letter-spacing: -0.01em; +} +.lp-series-sub { + font-size: 13.5px; + color: var(--landing-fg-muted); +} +.lp-series-body { + margin: 16px 0 0 52px; +} +.lp-series-body .lbl { + font-size: 12px; + letter-spacing: 0.1em; + text-transform: uppercase; + font-weight: 600; + color: var(--landing-fg-muted); + margin-bottom: 10px; +} +.lp-series-outcomes { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px 24px; + margin-bottom: 4px; +} +.lp-series-outcomes .o { + display: flex; + gap: 9px; + font-size: 14px; + color: var(--landing-fg); + line-height: 1.5; +} +.lp-series-outcomes .o svg { + width: 15px; + height: 15px; + stroke-width: 2.2; + color: var(--landing-accent); + flex-shrink: 0; + margin-top: 3px; +} +.lp-lessons-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: var(--landing-accent); + background: none; + border: none; + cursor: pointer; + padding: 10px 0 0; +} +.lp-lessons-toggle svg { + width: 15px; + height: 15px; + stroke-width: 2.2; + transition: transform 0.15s; +} +.lp-lessons-toggle.open svg { + transform: rotate(180deg); +} +.lp-lessons { + display: none; + border-top: 1px solid var(--landing-border); + margin-top: 12px; +} +.lp-lessons.open { + display: block; +} +.lp-lesson { + display: flex; + align-items: center; + gap: 12px; + padding: 9px 0; + border-bottom: 1px solid var(--landing-border); + font-size: 14px; +} +.lp-lesson .ix { + font-family: ui-monospace, monospace; + font-size: 12.5px; + color: var(--landing-fg-muted); + min-width: 3ch; +} +.lp-lesson .dur { + margin-left: auto; + font-size: 12.5px; + color: var(--landing-fg-muted); +} +.lp-lesson .prev { + font-size: 12px; + font-weight: 600; + color: var(--landing-accent); +} +@media (max-width: 720px) { + .lp-series-outcomes { + grid-template-columns: 1fr; + } + .lp-series-body { + margin-left: 0; + } +} + +/* ===== instructors ===== */ +.lp-instructors { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} +.lp-instructor { + padding: 24px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} +.lp-instructor .av { + width: 52px; + height: 52px; + border-radius: 999px; + background: var(--landing-accent-soft); + color: var(--landing-accent); + display: grid; + place-items: center; + font-weight: 600; + font-size: 17px; + margin-bottom: 10px; +} +.lp-instructor .nm { + font-size: 15.5px; + font-weight: 600; +} +.lp-instructor .rl { + font-size: 13px; + color: var(--landing-fg-muted); +} +.lp-instructor .bio { + font-size: 13.5px; + color: var(--landing-fg-muted); + margin-top: 8px; + line-height: 1.55; +} +@media (max-width: 760px) { + .lp-instructors { + grid-template-columns: 1fr; + } +} + +/* ===== testimonials ===== */ +.lp-testimonials { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} +.lp-testimonial { + padding: 26px; +} +.lp-testimonial .q { + font-size: 15.5px; + line-height: 1.65; + margin: 0 0 16px; +} +.lp-testimonial .who { + display: flex; + align-items: center; + gap: 10px; + font-size: 13.5px; +} +.lp-testimonial .who .av { + width: 34px; + height: 34px; + border-radius: 999px; + background: var(--landing-accent-soft); + color: var(--landing-accent); + display: grid; + place-items: center; + font-weight: 600; + font-size: 12px; +} +.lp-testimonial .who .nm { + font-weight: 600; +} +.lp-testimonial .who .loc { + color: var(--landing-fg-muted); +} +@media (max-width: 720px) { + .lp-testimonials { + grid-template-columns: 1fr; + } +} + +/* ===== FAQ accordion ===== */ +.lp-faq { + border-top: 1px solid var(--landing-border); +} +.lp-faq-item { + border-bottom: 1px solid var(--landing-border); +} +.lp-faq-q { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 4px; + background: none; + border: none; + cursor: pointer; + text-align: left; + font-size: 15.5px; + font-weight: 500; + color: var(--landing-fg); +} +.lp-faq-q svg { + width: 17px; + height: 17px; + stroke-width: 2; + color: var(--landing-fg-muted); + flex-shrink: 0; + transition: transform 0.15s; +} +.lp-faq-item.open .lp-faq-q svg { + transform: rotate(180deg); +} +.lp-faq-a { + display: none; + padding: 0 4px 18px; + font-size: 14.5px; + color: var(--landing-fg-muted); + line-height: 1.65; + max-width: 68ch; +} +.lp-faq-item.open .lp-faq-a { + display: block; +} + +/* ===== pricing card ===== */ +.lp-pricing-card { + max-width: 440px; + margin: 0 auto; + padding: 32px; + text-align: center; +} +.lp-pricing-card .eyebrow { + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 600; + color: var(--landing-fg-muted); + margin-bottom: 12px; +} +.lp-price { + font-size: 46px; + font-weight: 600; + letter-spacing: -0.03em; + line-height: 1; +} +.lp-price-was { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin: 10px 0 20px; + font-size: 14.5px; + color: var(--landing-fg-muted); +} +.lp-price-was s { + font-size: 16px; +} +.lp-features { + text-align: left; + margin: 0 0 24px; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} +.lp-features li { + display: flex; + gap: 10px; + font-size: 14.5px; +} +.lp-features svg { + width: 16px; + height: 16px; + stroke-width: 2.2; + color: var(--landing-accent); + flex-shrink: 0; + margin-top: 3px; +} + +/* certificate banner */ +.lp-cert { + display: flex; + align-items: center; + gap: 22px; + padding: 28px 30px; +} +.lp-cert .frame { + width: 130px; + height: 92px; + border: 1px solid var(--landing-border); + background: var(--landing-card-soft); + position: relative; + flex-shrink: 0; + display: grid; + place-items: center; +} +.lp-cert .frame::before { + content: ''; + position: absolute; + inset: 6px; + border: 1px solid color-mix(in oklab, var(--landing-accent) 35%, transparent); +} +.lp-cert .frame svg { + width: 26px; + height: 26px; + stroke-width: 1.8; + color: var(--landing-accent); +} +.lp-cert h3 { + font-size: 18px; + font-weight: 600; + margin: 0 0 4px; +} +.lp-cert p { + font-size: 14.5px; + color: var(--landing-fg-muted); + margin: 0; + max-width: 56ch; +} + +/* ===== footer ===== */ +.lp-footer { + border-top: 1px solid var(--landing-border); + padding: 48px 0 40px; + margin-top: 24px; +} +.lp-footer-grid { + display: grid; + grid-template-columns: minmax(0, 1.4fr) repeat(auto-fit, minmax(140px, 1fr)); + gap: 32px; +} +.lp-footer h4 { + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--landing-fg-muted); + font-weight: 600; + margin: 0 0 12px; +} +.lp-footer a { + display: block; + font-size: 14px; + color: var(--landing-fg-muted); + padding: 3px 0; +} +.lp-footer a:hover { + color: var(--landing-fg); +} +.lp-footer .brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + margin-bottom: 10px; +} +.lp-footer .copy { + font-size: 13px; + color: var(--landing-fg-muted); + margin-top: 12px; +} +@media (max-width: 720px) { + .lp-footer-grid { + grid-template-columns: 1fr; + } +} + +/* theme toggle button (prototype affordance) */ +.lp-theme-btn { + position: fixed; + bottom: 18px; + right: 18px; + z-index: 200; + width: 38px; + height: 38px; + border-radius: 999px; + border: 1px solid var(--landing-border); + background: var(--landing-card); + color: var(--landing-fg-muted); + cursor: pointer; + display: grid; + place-items: center; + box-shadow: 0 2px 8px rgb(0 0 0 / 0.08); +} +.lp-theme-btn svg { + width: 16px; + height: 16px; + stroke-width: 2; +} diff --git a/prototypes/learning-paths/lms-home.html b/prototypes/learning-paths/lms-home.html new file mode 100644 index 00000000..cb3e1ef8 --- /dev/null +++ b/prototypes/learning-paths/lms-home.html @@ -0,0 +1,191 @@ + + + + + + My Learning · ClassroomIO LMS + + + + + + +
+ + +
+
+ My Learning +
+ + +
+
+ +
+ +
+
+
+ + + + diff --git a/prototypes/learning-paths/org-landing.html b/prototypes/learning-paths/org-landing.html new file mode 100644 index 00000000..15809848 --- /dev/null +++ b/prototypes/learning-paths/org-landing.html @@ -0,0 +1,184 @@ + + + + + + Acme Academy — Learn frontend, data, and product + + + + + + + +
+ +
+ + +
+
+
+

Practical tech skills, taught the way work actually happens

+

Courses and guided learning paths in engineering, data, and product — built by practitioners, finished with certificates.

+ +
+
+ + +
+
+ Guided programs +

Learning Paths

+

Multi-course tracks you complete in order — one enrollment, one price, one certificate at the end.

+ + + + +
+
+ + +
+
+

Our Courses

+

Standalone courses you can take in any order.

+ + + + +
+
+ + + + + + + + + diff --git a/prototypes/learning-paths/path-detail.html b/prototypes/learning-paths/path-detail.html new file mode 100644 index 00000000..5f8f3bd6 --- /dev/null +++ b/prototypes/learning-paths/path-detail.html @@ -0,0 +1,235 @@ + + + + + + Learning Path · ClassroomIO LMS + + + + + + +
+ + +
+
+ +
+ +
+
+ +
+
+
+
+ Learning Path +
+
+

Frontend Engineering Bootcamp

+

A guided, five-course track that takes you from markup fundamentals to shipping a production React app. Each course unlocks the next — finish in order to earn your certificate.

+
+ 5 courses + ~38 hours + Sequential · unlock in order + Certificate on completion +
+
+
+
Your progress
2 of 5 courses complete
+
49%
+
+
+
+ + Up next: continue Modern Web Development with React — 3 lessons and 2 exercises left. +
+
+
+
+ +
+

Path courses

+ + + Finish lessons & exercises to unlock the next course + +
+ +
+
+
+
+
+
+
Course 1Completed
+

HTML & CSS Foundations

+

Semantic markup, the box model, flexbox, and responsive layout basics.

+
100%
+
+
+ Review + Finished May 12 +
+
+
+ +
+
+
+
+
+
Course 2Completed
+

JavaScript Essentials

+

Types, functions, the DOM, async/await, and fetching data from APIs.

+
100%
+
+
+ Review + Finished Jun 3 +
+
+
+ +
+
+ +
+ +
+
+
+
+
+
Course 4Locked
+

State Management & Data Fetching

+

Context, reducers, caching, and connecting a React app to a backend.

+
+ 10 lessons + 3 exercises +
+
+
+ Locked + Unlocks after Course 3 +
+
+
+ +
+
+
+
+
+
Course 5Locked
+

Building & Shipping a Capstone

+

Plan, build, and deploy a full project — your portfolio piece.

+
+ 7 lessons + 1 capstone project +
+
+
+ Locked + Unlocks after Course 4 +
+
+
+ +
+
+
+
+
+ Bootcamp Certificate + Complete all 5 courses to unlock and download your certificate. +
+
+
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/paths.html b/prototypes/learning-paths/paths.html new file mode 100644 index 00000000..b74cdc1e --- /dev/null +++ b/prototypes/learning-paths/paths.html @@ -0,0 +1,174 @@ + + + + + + Learning Paths · ClassroomIO LMS + + + + + + +
+ + +
+
+ Learning Paths +
+ +
+
+ +
+ +
+
+
+ + + + diff --git a/prototypes/learning-paths/public-path.html b/prototypes/learning-paths/public-path.html new file mode 100644 index 00000000..c97745d9 --- /dev/null +++ b/prototypes/learning-paths/public-path.html @@ -0,0 +1,410 @@ + + + + + + Frontend Engineering Bootcamp — Acme Academy + + + + + + +
+ +
+ + +
+
+
+
+ Learning Path · Certificate +

Become a job-ready frontend engineer

+

Five courses, one path — from your first HTML tag to a deployed React capstone. Learn in order, build real projects, earn a certificate.

+
+
JDEORM
+ Taught by Jordan Diallo, Efe Obi + 1 more +
+
+ Enroll · $199 + Save $81 vs individual courses +
+
+ + +
+ +
+
5
courses in series
+
~38h
at your own pace
+
Beginner
no experience required
+
4.7 ★
213 reviews
+
+
+ + + + + +
+
+

What you'll learn

+

Practical, project-first skills — every course ends with something you built.

+
+
Build responsive, accessible websites with semantic HTML and modern CSS
+
Write production JavaScript: async, APIs, and the DOM
+
Build and ship interactive React apps with state, hooks, and data fetching
+
Complete a portfolio-ready capstone project, deployed to production
+
+ +

Skills you'll gain

+
+ HTML & CSS + Flexbox & Grid + JavaScript + REST APIs + React + State management + Deployment +
+
+
+ + +
+
+ Series · take in order +

5 courses in this path

+

Each course unlocks the next once you finish its lessons and exercises.

+ +
+ +
+
+ 1 +
+

HTML & CSS Foundations

+ Course 1 · 12 lessons · ~8 hours +
+
+
+
What you'll learn
+
+ Semantic markup and document structure + The box model, flexbox, and grid + Responsive layout from mobile up + Accessibility fundamentals +
+ +
+
01Anatomy of a web pagePreview14m
+
02Semantic HTML in practice18m
+
03The box model16m
+
…9 more lessons
+
+
+
+ + +
+
+ 2 +
+

JavaScript Essentials

+ Course 2 · 10 lessons · ~9 hours +
+
+
+
What you'll learn
+
+ Types, functions, and control flow + Working with the DOM + async/await and promises + Fetching data from APIs +
+ +
+
01Values, types, and variablesPreview15m
+
02Functions and scope17m
+
…8 more lessons
+
+
+
+ + +
+
+ 3 +
+

Modern Web Development with React

+ Course 3 · 12 lessons · ~10 hours +
+
+
+
What you'll learn
+
+ Components, props, and JSX + State and event handling + Hooks and effects + Building interactive UIs +
+ +
+
01What is React?Preview6m
+
02JSX & rendering11m
+
…10 more lessons
+
+
+
+ + +
+
+ 4 +
+

State Management & Data Fetching

+ Course 4 · 10 lessons · ~7 hours +
+
+
+
What you'll learn
+
+ Context and reducers + Caching and loading states + Connecting React to a backend + Error handling patterns +
+ +
+
01Lifting state up13m
+
…9 more lessons
+
+
+
+ + +
+
+ 5 +
+

Building & Shipping a Capstone

+ Course 5 · 7 lessons + capstone project · ~4 hours +
+
+
+
What you'll learn
+
+ Scoping and planning a real project + Deploying to production + Code review and iteration + Presenting your portfolio piece +
+
+
+
+
+
+ + +
+
+
+
+
+

Earn a shareable certificate

+

Complete all five courses — every lesson and exercise — and receive the Frontend Engineering Bootcamp certificate, ready to add to LinkedIn and your CV.

+
+
+
+
+ + +
+
+

Your instructors

+

The tutors behind the courses in this path.

+
+
+ JD + Jordan Diallo + Lead Instructor · Frontend + 10 years building web apps; previously staff engineer at a fintech scale-up. +
+
+ EO + Efe Obi + Instructor · JavaScript + Teaches JS to thousands of learners; maintainer of two OSS tooling projects. +
+
+ RM + Rita Mensah + Instructor · React + Product engineer and design-systems specialist; capstone project mentor. +
+
+
+
+ + +
+
+

What learners say

+

From people who finished the path.

+
+
+

"I went from zero to deploying my own React app in ten weeks. The order of the courses just makes sense — nothing assumed I knew things I didn't."

+
TB
Tunde Bello
Lagos, Nigeria
+
+
+

"The locked courses kept me honest — no skipping ahead, and everything built on what came before. The certificate helped me land interviews."

+
MK
Mariam Kone
Abidjan, Côte d'Ivoire
+
+
+
+
+ + +
+
+

Frequently asked questions

+
+
+ +
No — the path starts from absolute fundamentals and each course unlocks the next, so you're never dropped into material you're not ready for.
+
+
+ +
Yes, every course is also sold individually — the path bundle saves you 29% and adds the path certificate.
+
+
+ +
Yes — this path is sequential. Each course unlocks after you complete the previous one's lessons and exercises. That's what makes the curriculum build properly.
+
+
+ +
Lifetime. Learn at your own pace — most learners finish in 8–12 weeks at 4–5 hours a week.
+
+
+ +
Complete every lesson and submit every exercise across all five courses, including the capstone project. The certificate is issued automatically.
+
+
+
+
+ + +
+
+
+
Enroll
+
$199
+
$280Save $81 (29%)
+
    +
  • All 5 courses, unlocked in order
  • +
  • 52 lessons, 13 exercises, 1 capstone project
  • +
  • Path certificate on completion
  • +
  • Lifetime access
  • +
+ Enroll in this path +
+
+
+ + + + + + + + diff --git a/prototypes/learning-paths/public-paths.html b/prototypes/learning-paths/public-paths.html new file mode 100644 index 00000000..10ee880a --- /dev/null +++ b/prototypes/learning-paths/public-paths.html @@ -0,0 +1,155 @@ + + + + + + Learning Paths — Acme Academy + + + + + + +
+ +
+ +
+
+
+

Learning Paths

+

Guided multi-course tracks with one enrollment, ordered courses, and a certificate at the end.

+ +
+
+ +
+ + + + +
+ + Learning Path +

Frontend Engineering Bootcamp

+

Five courses, one path — from your first HTML tag to a deployed React capstone. Learn in order, build real projects, earn a certificate.

+
+ 5 courses + ~38 hours + 942 enrolled + Save $81 + $199 $280 +
+
+ + + Learning Path +

Data Science with Python

+

From Python basics to applied machine learning — pandas, visualization, and a real analysis project, in a fixed order that builds week over week.

+
+ 4 courses + ~30 hours + 418 enrolled + Save $53 + $159 $212 +
+
+ + + Learning Path +

Product Management Fundamentals

+

Discovery, prioritization, delivery, and stakeholder craft — a five-course track for new and aspiring PMs.

+
+ 5 courses + ~18 hours + 207 enrolled + Save $46 + $129 $175 +
+
+
+
+ + + + + + + + diff --git a/prototypes/learning-paths/teacher-path-analytics.html b/prototypes/learning-paths/teacher-path-analytics.html new file mode 100644 index 00000000..049df0b7 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-analytics.html @@ -0,0 +1,154 @@ + + + + + + Path Analytics · ClassroomIO + + + + + + +
+ + +
+
+ +
+ +
+
+ +
+
+
+
+

Analytics

+

How learners are moving through the path.

+
+ +
+ +
+
Enrolled
48
+9 this month
+
Active learners
31
worked in the last 14 days
+
Completion rate
27%
13 of 48 finished
+
Avg. time to finish
9.4 wks
across completions
+
+ +
+

Course funnel

+

How many learners have reached and completed each course, in order.

+ +
+ 1 +
+
HTML & CSS Foundations44 completed · 3 in progress
+
44
+
+
+
+ 2 +
+
JavaScript Essentials36 completed · 6 in progress
+
36
+
8 learners dropped after Course 1
+
+
+
+ 3 +
+
Modern Web Development with React22 completed · 9 in progress
+
22
+
14 learners dropped after Course 2 — biggest drop-off
+
+
+
+ 4 +
+
State Management & Data Fetching16 completed · 4 in progress
+
16
+
+
+
+ 5 +
+
Building & Shipping a Capstone13 completed · 2 in progress
+
13
+
13 certificates awarded
+
+
+
+ +
+

Where learners are stuck

+

Lessons and exercises blocking the most learners right now.

+
+
+
+
+ Exercise: Build a to-do app · React + 7 learners have had this exercise in progress for over 2 weeks. +
+ 7 stuck +
+
+
+
+ Lesson: The useEffect hook · React + 5 learners stopped at this lesson. +
+ 5 stuck +
+
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-builder.html b/prototypes/learning-paths/teacher-path-builder.html new file mode 100644 index 00000000..531d78c5 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-builder.html @@ -0,0 +1,216 @@ + + + + + + Path Courses · ClassroomIO + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+

Courses

+

Add courses and drag to set the order students move through them.

+
+
+ +
+
+
+ Unlock courses in order +

Each course stays locked until a student completes the previous one's lessons and exercises. Turn off to allow any order.

+
+ +
+ +
+

5 courses in this path

+ Drag to reorder +
+ +
+
+ + 1 + +
+

HTML & CSS Foundations

+

12 lessons2 exercises$49

+
+
+ + +
+
+ +
+ + 2 + +
+

JavaScript Essentials

+

10 lessons3 exercises$59

+
+
+ + +
+
+ +
+ + 3 + +
+

Modern Web Development with React

+

12 lessons4 exercises$79

+
+
+ + +
+
+ +
+ + 4 + +
+

State Management & Data Fetching

+

10 lessons3 exercises$59

+
+
+ + +
+
+ +
+ + 5 + +
+

Building & Shipping a Capstone

+

7 lessons1 capstone$34

+
+
+ + +
+
+
+ + + +
+ + + +

Path certificate

Awarded automatically when a student completes all 5 courses.

+
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-certificate.html b/prototypes/learning-paths/teacher-path-certificate.html new file mode 100644 index 00000000..ba440429 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-certificate.html @@ -0,0 +1,127 @@ + + + + + + Path Certificate · ClassroomIO + + + + + + +
+ + +
+
+ +
+ +
+
+ +
+
+
+
+

Certificate

+

Award a path-level certificate when a learner completes every course.

+
+
+ +
+
+

Award a path certificate

Issued automatically on completion and shown in the learner's Certificates page.

+ +
+
+
All 5 courses completed — every lesson finished
+
All exercises submitted or passed in each course
+

Completion rules follow the path's unlock rule (lessons + exercises). 13 certificates awarded so far.

+
+
+ +
+

Certificate details

What appears on the issued certificate.

+
+
+ + +
+
+ + +
+
+ Preview +
+
+
+
Acme Academy
+

Frontend Engineering Bootcamp Certificate

+

This certifies that

+
Ada Okafor
+

has successfully completed all 5 courses in the Frontend Engineering Bootcamp learning path.

+
Issued · Jul 25, 2026ID · LP-8F42-19AC
+
+
+
+
+ + +
+
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-landing-editor.html b/prototypes/learning-paths/teacher-path-landing-editor.html new file mode 100644 index 00000000..7bbba7d9 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-landing-editor.html @@ -0,0 +1,227 @@ + + + + + + Path Landing Page · ClassroomIO + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+

Landing page

+

Control what visitors see on the public path page at /path/frontend-engineering-bootcamp.

+
+
+ + +
+

Hero

The headline visitors see first.

+
+
+ + +
+
+ + +
+
+
+ + +
+

Visitor access

How much of the path a non-enrolled visitor can explore.

+
+ + + +
+
+ + +
+

What you'll learn

Outcome bullets shown near the top of the page.

+
+
+
+
+
+ +
+
+ + +
+

Skills

Tags shown as "Skills you'll gain".

+
+
+ HTML & CSS + Flexbox & Grid + JavaScript + REST APIs + React + State management + Deployment +
+ +
+
+ + +
+
+

Instructors

Pulled from the tutors of the courses in this path.

+ +
+
+ JDJordan Diallo + EOEfe Obi + RMRita Mensah +
+
+ + +
+
+

Testimonials

Learner quotes shown on the public page.

+ +
+
+
+ TB +
+ Tunde Bello + "I went from zero to deploying my own React app in ten weeks. The order of the courses just makes sense." +
+ +
+
+ MK +
+ Mariam Kone + "The locked courses kept me honest — no skipping ahead, and everything built on what came before." +
+ +
+ +
+
+ + +
+
+

FAQ

Collapsible questions at the bottom of the page.

+ +
+
+
+
+ Do I need any prior experience? + No — the path starts from absolute fundamentals and each course unlocks the next. +
+ +
+
+
+ Can I buy a single course instead? + Yes, every course is also sold individually — the path bundle saves you 29%. +
+ +
+ +
+
+ +
+ Unsaved changes +
+ + +
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-people.html b/prototypes/learning-paths/teacher-path-people.html new file mode 100644 index 00000000..f9cf01b7 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-people.html @@ -0,0 +1,139 @@ + + + + + + Path People · ClassroomIO + + + + + + +
+ + +
+
+ +
+ +
+
+ +
+
+
+
+

People

+

48 learners enrolled in this path.

+
+ Add learners +
+ +
+ + Adding a learner to this path automatically enrolls them in all 5 courses. Removing them preserves their course access and progress. +
+ +
+ + +
+ +
+
LearnerPath progressCurrent courseEnrolled
+ +
+
AO
Ada Okafor
a•••@gmail.com
+
49%
+ 3React + May 2 + +
+ +
+
TB
Tunde Bello
t•••@yahoo.com
+
100%
+ Certificate earned + Mar 18 + +
+ +
+
MK
Mariam Kone
m•••@outlook.com
+
74%
+ 4State & Data + Apr 11 + +
+ +
+
SD
Sam Diallo
s•••@gmail.com
+
12%
+ 1HTML & CSS + Jul 8 + +
+ +
+
LN
Lara Nwosu
l•••@proton.me
+
0%
+ Not started + Jul 21 + +
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-settings.html b/prototypes/learning-paths/teacher-path-settings.html new file mode 100644 index 00000000..b3850f2d --- /dev/null +++ b/prototypes/learning-paths/teacher-path-settings.html @@ -0,0 +1,216 @@ + + + + + + Path Settings · ClassroomIO + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+

Settings

+

Manage how this path appears, is priced, and behaves for learners.

+
+
+ + +
+

General

The name, summary, and cover learners see across the LMS.

+
+
+ + +
+
+ + +
+
+ Cover image +
+
+
+ + PNG or JPG, at least 1200×480px. +
+
+
+
+
+ + +
+

Pricing

One bundle price for the whole path. Enrolling unlocks every course.

+
+
+
+ + +
+
+ + +
+
+
+
+
+
Show savings vs individual courses
+
Courses sum to $280 — the page will show "Save $81 (29%)".
+
+ +
+
+ + Learners see: $199 $280 · Save $81 vs buying each course +
+
+
+ + +
+

Enrollment & flow

How learners join and move through the path.

+
+
+
+
Self-enrollment from the landing page
+
Visitors can buy or join this path from your public site. Turn off for admin-managed enrollment only.
+
+ +
+
+
+
+
Unlock courses in order
+
Each course unlocks after the previous one's lessons and exercises are complete.
+
+ +
+
+
+
+
Auto-enroll in path courses
+
Joining the path enrolls the learner in every course it contains.
+
+ +
+
+
+ + +
+

Status & visibility

Controls whether learners can see and join this path.

+
+ + + +
+
+ + +
+

Danger zone

+
+
+
+
Delete this path
+
Permanently removes the path and its landing page. Courses, enrollments, and learner progress are not deleted.
+
+ +
+
+
+ +
+ Unsaved changes +
+ + +
+
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-path-setup.html b/prototypes/learning-paths/teacher-path-setup.html new file mode 100644 index 00000000..efbe3da1 --- /dev/null +++ b/prototypes/learning-paths/teacher-path-setup.html @@ -0,0 +1,157 @@ + + + + + + Set up your path · ClassroomIO + + + + + + +
+ + +
+
+ + +
+ +
+
+
+
+

Set up your path

+

Finish these steps to launch Frontend Engineering Bootcamp to your learners.

+
+
+ + + + + 33% +
+
+ +
+
+ A path goes live once its courses are ordered, priced, and set to Active. +
+ 2 of 6 completed + +
+
+
+ +
+
+
+
+ Name & describe your path + Give the path a clear, outcome-based name and a short summary. +
+
Done
+
+ +
+
+
+ Add courses + Pick the courses that make up this learning path. +
+
Done
+
+ +
+
+
+ Set the course order + Drag courses into the order learners should complete them. +
+ +
+ +
+
+
+ Set your price + Price the path as a bundle — learners see the savings vs individual courses. +
+ +
+ +
+
+
+ Customize your landing page + Outcomes, skills, instructors, testimonials, and FAQs on the public path page. +
+ +
+ +
+
+
+ Activate the path + Set the status to Active so it appears on your landing page and LMS. +
+ +
+
+ +
+
+ +
+
+ 4 steps left to launch +

Order the courses, set a price, polish the landing page, and activate.

+
+ Activate now +
+
+
+
+
+ + + + diff --git a/prototypes/learning-paths/teacher-paths.html b/prototypes/learning-paths/teacher-paths.html new file mode 100644 index 00000000..30567157 --- /dev/null +++ b/prototypes/learning-paths/teacher-paths.html @@ -0,0 +1,131 @@ + + + + + + Learning Paths (Admin) · ClassroomIO + + + + + + +
+ + +
+
+ Learning Paths +
+ +
+
+ +
+ +
+
+
+ + + + diff --git a/skills/write-prd/SKILL.md b/skills/write-prd/SKILL.md new file mode 100644 index 00000000..c475710c --- /dev/null +++ b/skills/write-prd/SKILL.md @@ -0,0 +1,66 @@ +--- +name: write-prd +description: Write a product PRD for ClassroomIO the house way — clarify all features with the user first, research the codebase, write the PRD under prd//, then build clickable HTML prototypes under prototypes// that become the UX source of truth. Use when the user asks to "write a PRD", "spec a feature", "plan a new product/feature", or "prototype a feature end to end". +--- + +# Write a PRD + +A PRD here is two artifacts that ship together: + +1. `prd//README.md` — the written spec. +2. `prototypes//` — clickable HTML prototypes of every surface, with an `index.html` map. **The prototypes are the UX source of truth**; when prose and prototype disagree on a UI detail, the prototype wins, and the PRD must say so. + +Work in this order. Do not write the PRD before the decisions are locked, and do not build prototypes before the PRD scope is clear. + +## Phase 1 — Get clear on all the features first + +Never start writing from a one-line request. Close every open fork with the user via `AskUserQuestion` (option-based, 3–4 questions per round, 2–3 rounds max): + +- **Relationship to existing features** — new entity vs extension of something shipped? (Check `prd/` for overlapping PRDs first; e.g. "learning paths" nearly collided with the shipped Programs feature.) +- **Who touches it** — enumerate the personas and confirm each surface: public visitor (org landing site), learner (LMS), teacher/admin (dashboard). Most features need all three; ask which are in scope. +- **The business mechanics** — pricing, enrollment/self-serve vs admin-managed, gating/completion rules, certificates, statuses (Active/Draft/Archived). +- **Scope cuts** — what's explicitly v1 vs later. Every "maybe" becomes a Non-Goal or a decision. +- **References** — if the user points at a competitor page or screenshot, fetch/read it and mirror its structure deliberately. + +Every answer becomes a numbered entry in the PRD's **Confirmed Decisions** section. If the user answers with a note instead of an option, treat it as a lead to chase (fetch the URL, read the screenshot), not a checkbox. + +## Phase 2 — Research the codebase before writing + +- Read the closest existing PRD in `prd/` and match its format. +- Audit current state: what tables, services, routes, and UI patterns already exist that this feature reuses or must not break. Capture it as a **Current-State Audit** table (capability → current state → notes). +- Find the real UI patterns to mirror (use an Explore agent for breadth): the component primitives in `packages/ui`, the sibling feature's routes/sidebar/pages, the landing-page theme system. Name the actual files in the PRD. + +## Phase 3 — Write the PRD + +Location: `prd//README.md`. House structure (match `prd/programs [DONE]/README.md`): + +1. **Status** — Draft / In review / Done. +2. **Prototypes — the UX source of truth** — state that UX comes from the prototype folder, show the start page in a code block (`prototypes//index.html`), and include a surface → files table. +3. **Purpose** — one paragraph; name the reference experience if there is one. +4. **Problem Statement** — bullets of what's impossible or painful today. +5. **Confirmed Decisions** — numbered; this is the contract from Phase 1. +6. **Current-State Audit** — the table from Phase 2. +7. **Product Goals** / **Non-Goals (v1)**. +8. **Functional Requirements** — one subsection per surface (public / learner / teacher), each referencing its prototype file and describing states, not just happy paths (locked, empty, draft…). +9. **Technical Design** — data model as schema code blocks (follow repo rules: no relational IDs in jsonb, computed values never stored), core logic in pseudocode, an API route table, and a frontend plan that follows CLAUDE.md layering (validation → queries → services → routes → feature types/API classes). +10. **Implementation Order** — numbered phases an engineer can execute top to bottom, with build verification commands. +11. **Acceptance Criteria** — testable, numbered; always include a "zero regression on existing features" criterion and "all copy uses translation keys". +12. **Risks and Mitigations** — each risk paired with a concrete mitigation. + +Keep it scannable: tables for routes and audits, code blocks for schemas, prose only where judgment is needed. + +## Phase 4 — Build the prototypes + +Location: `prototypes//` — standalone HTML files, one per screen, plus shared CSS: + +- **Use the real design tokens, not invented ones.** App screens: a shared `app-theme.css` mirroring `packages/ui/src/index.css` (OKLCH vars, Geist, radii, shadows, Button/Badge/Item/Progress recipes, `.dark` class). Public screens: a `landing-theme.css` mirroring the default org-landing theme (`--landing-*` vars). Read the actual component variants (`tv()` blocks) before styling. +- **Match shipped UI, don't redesign it.** If a screen wraps an existing view (course page, setup checklist, settings form), replicate that view's real structure — sidebar, header, states — and add only the feature-specific chrome. Screenshots from the user override everything. +- **`index.html` is the start page**: a map of every screen grouped by persona/journey, each card linking in. All pages cross-link so each journey clicks through end to end. +- Every page: light/dark toggle, realistic data (no lorem), all states shown (done / in progress / locked / empty / draft). Small vanilla-JS interactions (drag reorder, accordions, tabs) are worth the effort — they sell the flow. +- Open pages with `open ` as you finish them so the user can react early; iterate on their feedback per screen. Expect header/sidebar nitpicks — fidelity to the real app is the bar. + +## Phase 5 — Close the loop + +- Re-check the PRD against the final prototypes (files renamed? screens added/removed? decisions changed mid-build?) and fix drift. +- Tell the user the start page path and the PRD path in your summary. +- Flag the one or two product calls you made without an explicit answer so they can veto them.