npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aime.ai/renderer-react

v1.10.0

Published

React component library for rendering AIME lesson packs with BlockSuite whiteboard support

Readme

@aime.ai/renderer-react

A composable React component library for rendering AIME lesson packs — structured educational content with slide-based presentation, a BlockSuite whiteboard, speaker notes, and full RTL / i18n support.


Table of Contents


Installation

pnpm add @aime.ai/renderer-react
# or
npm install @aime.ai/renderer-react

Import the stylesheet once at your app root:

import "@aime.ai/renderer-react/style.css";

Quick Start

import "@aime.ai/renderer-react/style.css";
import {
  LessonProvider,
  LessonWindow,
  SlideNavigation,
  PresentButton,
  ExportButton,
  CompleteLessonButton,
  SlideSidebar,
  SlideViewer,
  FloatingNavigation,
  NotesPanel,
  BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";

function LessonPage({ pack }: { pack: LessonPackOutput }) {
  return (
    <LessonProvider pack={pack}>
      <SlideNavigation>
        <PresentButton />
        <ExportButton onExport={() => downloadPdf(pack)} />
        <CompleteLessonButton onComplete={() => router.back()} />
      </SlideNavigation>

      <LessonWindow>
        <SlideSidebar onBack={() => router.back()} />
        <SlideViewer />
        <BlockSuiteCanvas />
      </LessonWindow>

      <FloatingNavigation onBack={() => router.back()} />
      <NotesPanel />
    </LessonProvider>
  );
}

Core Concepts

Composability — Every component is optional. Omit BlockSuiteCanvas if you don't need whiteboard annotations. Omit NotesPanel for a minimal viewer. Compose only what you need.

Shared state via contextLessonProvider wraps two React contexts:

  • LessonPackContext — holds the current LessonPackOutput, the active slide index, and setters.
  • PresentationContext — holds UI state: presentation mode, sidebar collapse, whiteboard mode, zoom level, text direction, and i18n labels.

All built-in components read from and write to these contexts automatically.



Editing Feature

The renderer ships a full inline-editing system. When editing is enabled, every slide component swaps its static display for live-editable primitives — teachers or content creators can update text, markdown content, lists, and images without leaving the lesson view.


Enabling Edit Mode

Pass editable and editCallbacks to LessonProvider:

import {
  LessonProvider,
  LessonWindow,
  SlideNavigation,
  EditButton,
  EditView,
  SlideViewer,
} from "@aime.ai/renderer-react";
import type { EditCallbacks } from "@aime.ai/renderer-react";

const editCallbacks: EditCallbacks = {
  onSave: (updatedPack) => {
    // Persist to your API whenever any field changes
    api.patch(`/lesson-packs/${updatedPack.id}`, updatedPack);
  },
  onSelectImage: (resolve) => {
    // Open your own image picker, then call resolve() with the URL
    openMyImagePicker().then((url) => resolve(url));
  },
  onReorderSlides: (slides) => console.log("Slides reordered", slides),
  onDeleteSlide: (index) => console.log("Slide deleted at", index),
};

function LessonEditor({ pack }) {
  return (
    <LessonProvider pack={pack} editCallbacks={editCallbacks}>
      <SlideNavigation>
        <EditButton />
      </SlideNavigation>
      <LessonWindow>
        <EditView /> {/* Full-page editor — only renders when editable */}
        <SlideViewer />{" "}
        {/* Presentation viewer — hides when EditView is active */}
      </LessonWindow>
    </LessonProvider>
  );
}

Note: editable defaults to false. It can also be toggled at runtime via useLessonPack().toggleEditable() or the built-in EditButton component.


EditView

A full-page slide management view that replaces the SlideViewer when editing is active. It renders null when editable is false, so it is safe to always include in the tree.

Features:

  • Horizontal thumbnail grid — every slide is rendered as a live mini-preview.
  • Drag-to-reorder — grab the grip handle on any card and drag it to a new position. Slide numbers are re-assigned automatically and onReorderSlides + onSave are called.
  • Delete — the trash icon on each card removes the slide; onDeleteSlide + onSave are called.
  • Inline editor — clicking a card opens a single-slide editing view where the active slide's primitive fields become editable. A breadcrumb header and Prev / Next arrows allow navigation between slides without returning to the grid.
<EditView />

No props — reads all state from LessonPackContext.


EditButton

A toggle button that switches editable on and off via useLessonPack().toggleEditable(). Place it inside SlideNavigation.

<SlideNavigation>
  <EditButton />
  <PresentButton />
</SlideNavigation>

No props.


EditCallbacks

All callbacks are optional. Pass only the ones you need.

interface EditCallbacks {
  /**
   * Called (debounced) whenever any slide field changes or slides are
   * reordered / deleted. Receives the full updated pack.
   */
  onSave?: (pack: LessonPackOutput) => void;

  /**
   * Invoked when the user clicks an image placeholder or "Change image".
   * Implement your own file picker and call `resolve(url)` with the chosen
   * URL, or `resolve(null)` to cancel.
   */
  onSelectImage?: (resolve: (url: string | null) => void) => void;

  /**
   * Called after a drag-to-reorder operation completes.
   * `slides` is the full new ordered array with updated `slide_number` values.
   */
  onReorderSlides?: (slides: AnySlide[]) => void;

  /**
   * Called after a slide is deleted.
   * `slideIndex` is the zero-based position of the deleted slide.
   */
  onDeleteSlide?: (slideIndex: number) => void;
}

useLessonPack — editing API

The following editing-specific members are available on useLessonPack():

const {
  editable, // boolean — whether edit mode is active
  toggleEditable, // () => void — toggle edit mode on/off
  editCallbacks, // EditCallbacks — the callbacks passed to LessonProvider
  updateSlideField, // <K extends keyof AnySlide>(key: K, value: AnySlide[K]) => void
} = useLessonPack();

updateSlideField patches a single field on the currently active slide, merges it into the pack, and fires editCallbacks.onSave automatically.

// Update the title of the current slide
updateSlideField("title", "New Title");

// Update a slide-type-specific field (cast required for non-BaseSlide keys)
updateSlideField("content" as any, "# Updated content");

Primitive editing components

All primitive components read editable from context automatically — they render their non-interactive version when editable is false, so you can render them unconditionally.

EditableText

Inline plain-text editor. Renders as a contentEditable element. Commits on blur or Enter; reverts on Escape.

import { EditableText } from "@aime.ai/renderer-react";

<EditableText
  value={slide.title}
  onChange={(v) => updateSlideField("title", v)}
  className="font-black text-3xl text-zinc-900"
/>;

| Prop | Type | Default | Description | | ------------- | ------------------------------------------------ | ------------------ | -------------------------------- | | value | string | — | Current text value. | | onChange | (next: string) => void | — | Called on commit. | | className | string | "" | Applied to the element. | | as | "span" \| "p" \| "h1" \| "h2" \| "h3" \| "div" | "span" | Rendered tag. | | placeholder | string | "Click to edit…" | Shown when empty. | | multiline | boolean | false | Allow Shift+Enter for new lines. |


EditableMarkdown

Displays rendered markdown. In edit mode, clicking opens a raw-markdown <textarea> that auto-resizes. Commits on blur; reverts on Escape.

import { EditableMarkdown } from "@aime.ai/renderer-react";

<EditableMarkdown
  value={slide.content}
  onChange={(v) => updateSlideField("content" as any, v)}
  className="text-zinc-800 text-base leading-relaxed"
/>;

| Prop | Type | Default | Description | | ----------- | ------------------------ | ------- | -------------------------------------------- | | value | string | — | Raw markdown string. | | onChange | (next: string) => void | — | Called on commit. | | className | string | "" | Applied to both rendered and textarea views. |


EditableList

Edits a string[] array. Renders each item with an EditableText and shows Add / Remove controls. Supports a custom renderWrapper for per-item card styling.

import { EditableList } from "@aime.ai/renderer-react";

<EditableList
  items={slide.objectives}
  onChange={(v) => updateSlideField("objectives" as any, v)}
  placeholder="Add an objective…"
  renderWrapper={(child, i) => (
    <div key={i} className="flex gap-3 p-4 bg-white border rounded-xl">
      <span className="font-black text-blue-200 text-2xl">{i + 1}</span>
      <div className="flex-1">{child}</div>
    </div>
  )}
/>;

| Prop | Type | Default | Description | | --------------- | ------------------------------------------------ | ------------------ | ------------------------------------------------------------ | | items | string[] | — | Current array of strings. | | onChange | (items: string[]) => void | — | Called with the entire updated array. | | itemClassName | string | "" | className passed to each item's EditableText. | | as | "span" \| "p" \| "div" | "span" | Tag for each item text. | | placeholder | string | "Click to edit…" | Placeholder for empty items. | | renderWrapper | (child: ReactNode, index: number) => ReactNode | — | Optional custom wrapper per item (e.g. icon + number badge). |


EditableImage

Displays an image with a hover overlay to change it in edit mode. When src is null in edit mode, renders an "Add image" placeholder button. Calls editCallbacks.onSelectImage to let the host app provide an image URL.

import { EditableImage } from "@aime.ai/renderer-react";

<EditableImage
  src={slide.image_url}
  alt="Slide image"
  className="w-full h-48 rounded-2xl object-cover"
  onChanged={(url) => updateSlideField("image_url", url)}
/>;

| Prop | Type | Default | Description | | ----------- | ------------------------------- | ------- | --------------------------------------------------------- | | src | string \| null | — | Image URL. null shows the Add placeholder in edit mode. | | alt | string | "" | Alt text. | | className | string | "" | Applied to the wrapper. | | onChanged | (url: string \| null) => void | — | Called with the new URL after the user picks an image. |

EditableImage requires editCallbacks.onSelectImage to be set on LessonProvider, otherwise the click does nothing.


Components

LessonProvider

Root provider and layout shell. Must wrap all other components from this library.

<LessonProvider
  pack={lessonPack} // LessonPackOutput — pre-loads data into context
  dir="rtl" // "ltr" | "rtl" — text direction (default: "ltr")
  labels={myLabels} // LessonLabels — override button/label strings (i18n)
>
  {/* your composed UI */}
</LessonProvider>

| Prop | Type | Default | Description | | -------- | ------------------ | ---------------- | -------------------------------------------------------------------------------------------- | | pack | LessonPackOutput | — | Lesson data to render. Can be omitted and loaded later via useLessonPack().setCurrentPack. | | dir | "ltr" \| "rtl" | "ltr" | Document text direction. Applied to the root <div>. | | labels | LessonLabels | English defaults | UI string overrides for i18n. | | style | SlideStyle | "default" | Visual style preset for all slides and chrome. "default" or "neobrutalist". |


LessonWindow

Flex row container for the main content area (sidebar + viewer). Place inside LessonProvider, after any top navigation.

<LessonWindow>
  <SlideSidebar onBack={() => router.back()} />
  <SlideViewer />
  <BlockSuiteCanvas />
</LessonWindow>

SlideNavigation

Top navigation bar. Accepts any combination of action buttons as children.

<SlideNavigation>
  <PresentButton />
  <ExportButton onExport={handleExport} />
  <CompleteLessonButton onComplete={handleComplete} />
</SlideNavigation>

PresentButton / ExitButton

Toggles presentation (full-screen) mode. Renders as Present when inactive and Exit when in presentation mode — labels are i18n-aware.

<PresentButton />

ExportButton

Calls a user-supplied handler when clicked. Label is i18n-aware.

<ExportButton onExport={() => generateAndDownloadPdf(pack)} />

| Prop | Type | Description | | ---------- | ------------ | -------------------------------------------- | | onExport | () => void | Required. Called when the button is pressed. |


CompleteLessonButton

Signals lesson completion. Label is i18n-aware.

<CompleteLessonButton onComplete={() => router.push("/dashboard")} />

| Prop | Type | Description | | ------------ | ------------ | -------------------------------------------- | | onComplete | () => void | Required. Called when the button is pressed. |


SlideSidebar

Collapsible slide-strip sidebar. Shows a SlideThumbnail for every slide in the pack. Collapses automatically in presentation mode.

<SlideSidebar onBack={() => router.back()} />

| Prop | Type | Description | | -------- | ------------ | ----------------------------------------------------- | | onBack | () => void | Required. Called when the Back button is pressed. |


SlideViewer

Renders the currently active slide, scaled to fit the available space. Handles zoom via PresentationContext. On mobile in portrait orientation, the slide auto-rotates 90deg to fill the screen.

<SlideViewer />

No props — reads active slide and zoom from context.


SlideContainer

Lower-level wrapper used by SlideViewer. Use this if you need to render an individual slide without the full viewer infrastructure.

<SlideContainer slide={mySlide} scale={1} />

| Prop | Type | Description | | ------- | ---------- | ------------------------------------------------------- | | slide | AnySlide | The slide data to render. | | scale | number | Transform scale applied to the 1280 × 720 fixed canvas. |


SlideThumbnail

Renders a single slide at thumbnail size. Used inside SlideSidebar; also useful for custom slide pickers.

<SlideThumbnail slide={slide} index={0} isActive={currentIndex === 0} />

| Prop | Type | Description | | ---------- | ---------- | ---------------------------------------------------- | | slide | AnySlide | Slide to render. | | index | number | Zero-based position in the pack. | | isActive | boolean | Whether this thumbnail is the active/selected slide. |


FloatingNavigation

Previous / Next arrow buttons that float over the viewer. On desktop, only visible in presentation mode. On mobile (< 768px), always visible as a translucent pill rotated 90deg on the right edge (matching the rotated slide orientation), with prev/next arrows and a close button.

<FloatingNavigation onBack={() => router.back()} />

| Prop | Type | Description | | -------- | ------------ | -------------------------------------------------------------------------------- | | onBack | () => void | Optional. Called when the mobile close (X) button is pressed. Falls back to window.history.back(). |


NotesPanel

Slide-out panel showing teacher_notes for the current slide. Toggled via PresentationContext.toggleNotesPanel().

<NotesPanel />

No props.


BlockSuiteCanvas

An edgeless BlockSuite whiteboard that overlays the slide content. Activates when isWhiteboardMode is true in PresentationContext.

<BlockSuiteCanvas
  initialData={savedData}
  onSave={(data) => persistCanvas(data)}
/>

| Prop | Type | Description | | ------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | initialData | Uint8Array | Optional. Restores a previously saved Yjs snapshot. When provided, user annotations are re-applied on top of the slides. | | onSave | (data: Uint8Array) => void | Optional. Called (debounced 500 ms) whenever the canvas changes. Serialize to your backend or localStorage. Convert to number[] for JSON storage. |

See Canvas Persistence for a full example.


Contexts & Hooks

useLessonPack

Access and mutate the lesson pack state from any descendant of LessonProvider.

import { useLessonPack } from "@aime.ai/renderer-react";

function MyComponent() {
  const {
    currentPack, // LessonPackOutput | null
    setCurrentPack, // (pack: LessonPackOutput | null) => void
    currentSlideIndex, // number
    setCurrentSlideIndex, // (index: number) => void
    currentSlide, // AnySlide | null
  } = useLessonPack();
}

Use useLessonPackOptional() if the component might render outside a provider — returns null instead of throwing.


usePresentation

Access and mutate presentation UI state.

import { usePresentation } from "@aime.ai/renderer-react";

function MyComponent() {
  const {
    isPresentationMode, // boolean
    isSidebarCollapsed, // boolean
    isNotesPanelVisible, // boolean
    isWhiteboardMode, // boolean
    zoom, // number (50–200, default 90)
    dir, // "ltr" | "rtl"
    labels, // Required<LessonLabels>

    // Setters
    setIsPresentationMode,
    setIsSidebarCollapsed,
    setIsNotesPanelVisible,
    setIsWhiteboardMode,
    setZoom,
    setDir,

    // Convenience toggles
    togglePresentationMode,
    toggleSidebar,
    toggleNotesPanel,
    toggleWhiteboardMode,
    zoomIn, // +10, max 200
    zoomOut, // -10, min 50
    resetZoom, // back to 100
  } = usePresentation();
}

Use usePresentationOptional() if the component might render outside a provider — returns undefined instead of throwing.


useIsMobile

Reactive hook that returns true when the viewport is below the mobile breakpoint (default: 768px). Useful for building responsive wrapper components.

import { useIsMobile } from "@aime.ai/renderer-react";

function MyComponent() {
  const isMobile = useIsMobile();
  return isMobile ? <MobileLayout /> : <DesktopLayout />;
}

Accepts an optional breakpoint parameter (default: 768).


Style Presets

The renderer supports two visual style presets that control the look of all slides, chrome (sidebar, navigation, floating nav, edit views), and UI elements. Pass the style prop to LessonProvider:

<LessonProvider pack={pack} style="neobrutalist">
  {/* all components inherit the preset */}
</LessonProvider>

SlideStyle

type SlideStyle = "default" | "neobrutalist";
  • "default" — Soft gradients, rounded corners, glass-morphism cards with backdrop blur, subtle shadows.
  • "neobrutalist" — Flat colors, sharp corners, hard black borders, solid offset shadows, warm off-white backgrounds.

useStylePreset() hook

Access the full preset object from any component inside LessonProvider:

import { useStylePreset } from "@aime.ai/renderer-react";

function MyCustomSlide() {
  const s = useStylePreset();

  return (
    <div className={s.card.base}>
      <p className={s.typography.heading}>Title</p>
      <button className={s.button.primary}>Action</button>
    </div>
  );
}

The StylePreset object provides semantic tokens for:

| Token | Description | | --- | --- | | s.backgrounds[key] | Background class per slide type (e.g. s.backgrounds.warmUp) | | s.card.base | Main card style (bg, border, shadow, radius) | | s.cardSmall.base | Secondary/smaller card style | | s.button.primary | Filled action button (add your own bg color) | | s.button.secondary | Outlined/ghost action button | | s.button.ghost | Minimal hover-only button | | s.badge.base | Badge/chip style | | s.typography.* | Heading, body, label, muted text classes | | s.accentBar | Decorative accent bar style | | s.decorations.blob | Blob decoration class (or null in neobrutalist) | | s.assessmentSlide.* | Tokens for assessment-type slides (title, body, card, chip, badge) | | s.toggle.* | Toggle switch container/button styles | | s.chrome.* | Sidebar, navigation, floating nav, slide viewer, edit view tokens | | s.accents[color].* | Per-color accent sets (bg, border, text, fill, dot) |

Neobrutalist detection

Use const isNeo = !s.decorations.blob to conditionally apply preset-specific styles (e.g. square vs rounded progress dots, icon badge borders).


i18n / Labels

Every UI string in the renderer is injectable. Pass a labels prop to LessonProvider with only the keys you want to override — all others fall back to English defaults.

<LessonProvider
  pack={pack}
  dir="rtl"
  labels={{
    present: "عرض",
    exit: "خروج",
    exportLesson: "تصدير الدرس",
    completeLesson: "إتمام الدرس",
    back: "رجوع",
    teacherNotes: "ملاحظات المعلم",
    learningObjectives: "أهداف التعلم",
    revealAnswer: "اكشف الإجابة",
  }}
>
  {/* ... */}
</LessonProvider>

useLabels() hook

When building custom components that live inside LessonProvider, use useLabels() to consume the resolved label set:

import { useLabels } from "@aime.ai/renderer-react";

function MySlideWidget() {
  const labels = useLabels(); // Returns Required<LessonLabels> — always fully populated
  return <button>{labels.revealAnswer}</button>;
}

useLabels() falls back to DEFAULT_LABELS (English) when called outside a PresentationProvider, making it safe to use in server-side or testing contexts.

LessonLabels interface

All fields are optional. Comments show the English default.

interface LessonLabels {
  // Navigation / toolbar
  present?: string;                  // "Present"
  exit?: string;                     // "Exit"
  exportLesson?: string;             // "Export Lesson"
  completeLesson?: string;           // "Complete Lesson"
  back?: string;                     // "Back"
  edit?: string;                     // "Edit"
  editDone?: string;                 // "Done"
  editCancel?: string;               // "Cancel"
  editSlides?: string;               // "Edit Slides"
  allSlides?: string;                // "All Slides"
  dragToReorder?: string;            // "Drag to reorder"
  clickToEdit?: string;              // "Click to edit…"
  slides?: string;                   // "slides"

  // Collaboration UI
  pageMode?: string;                 // "Page Mode"
  edgelessMode?: string;             // "Edgeless Mode"
  modeControlledByTeacher?: string;  // "Mode controlled by teacher"
  followingTeacher?: string;         // "Following teacher"
  followingTeacherTitle?: string;    // "Following teacher — click to navigate freely"
  freeNavigation?: string;           // "Free navigation"
  freeNavigationTitle?: string;      // "Free navigation — click to follow teacher"

  // SlideViewer
  noSlideSelected?: string;          // "No slide selected"

  // NotesPanel
  teacherNotes?: string;             // "Teacher Notes"
  noTeacherNotes?: string;           // "No teacher notes added."
  senNotes?: string;                 // "SEN Notes"
  noSenNotes?: string;               // "*No SEN notes added.*"

  // Editing primitives
  clickToEditMarkdown?: string;      // "Click to edit markdown"
  addImage?: string;                 // "Add image"
  changeImage?: string;              // "Change image"
  deleteSlide?: string;              // "Delete slide"
  removeItem?: string;               // "Remove item"
  addItem?: string;                  // "Add item"

  // Shared slide labels
  of?: string;                       // "of"
  question?: string;                 // "Question"
  questions?: string;                // "Questions"
  answer?: string;                   // "Answer"
  answers?: string;                  // "Answers"
  answersOptional?: string;          // "Answers (Optional)"
  revealAnswer?: string;             // "Reveal Answer"
  problem?: string;                  // "Problem"
  problems?: string;                 // "Problems"
  task?: string;                     // "Task"

  // Cover slide
  inThisLesson?: string;             // "In this lesson"
  changeBackground?: string;         // "Change Background"
  addBackground?: string;            // "Add Background"

  // Teach slide
  lessonContent?: string;            // "Lesson Content"
  keyTerms?: string;                 // "Key Terms"

  // Hook slide
  thinkAboutIt?: string;             // "Think About It"

  // Learning Objectives slide
  learningObjectives?: string;       // "Learning Objectives"

  // Prior Knowledge slide
  priorKnowledgeCheck?: string;      // "Prior Knowledge Check"

  // Worked Example slide
  workedExample?: string;            // "Worked Example"
  checkHint?: string;                // "Check / Hint"
  stepByStepSolution?: string;       // "Step-by-Step Solution"
  addAStep?: string;                 // "Add a step…"
  nextStep?: string;                 // "Next Step"
  revealAll?: string;                // "Reveal All"
  finalAnswer?: string;              // "Final Answer"

  // Guided Practice slide
  guidedPractice?: string;           // "Guided Practice"

  // Discussion slide
  discussion?: string;               // "Discussion"
  discussionPrompt?: string;         // "Discussion Prompt"
  showTalkingPoints?: string;        // "Show Talking Points"
  nextPoint?: string;                // "Next Point"

  // Summary slide
  lessonSummary?: string;            // "Lesson Summary"
  upNext?: string;                   // "Up Next"

  // Exit Ticket slide
  exitTicket?: string;               // "Exit Ticket"

  // Warm Up slide
  warmUp?: string;                   // "Warm Up"

  // Misconception slide
  commonMisconceptions?: string;     // "Common Misconceptions"
  misconception?: string;            // "Misconception"
  correction?: string;               // "Correction"
  why?: string;                      // "Why?"
  addMisconception?: string;         // "Add Misconception"
  removeMisconception?: string;      // "Remove misconception"
  showCorrection?: string;           // "Show Correction"
  showExplanation?: string;          // "Show Explanation"

  // Student Task slide
  studentTask?: string;              // "Student Task"
  levelFoundation?: string;          // "Foundation"
  levelCore?: string;                // "Core"
  levelExtended?: string;            // "Extended"
  levelChallenge?: string;           // "Challenge"

  // Self Assessment slide
  selfAssessment?: string;           // "Self Assessment"
  assessmentScale?: string;          // "Assessment Scale"
  objectivesToSelfAssess?: string;   // "Objectives to Self-Assess"

  // Concept Map slide
  conceptMap?: string;               // "Concept Map"

  // Synthesis Task slide
  synthesisTask?: string;            // "Synthesis Task"
  context?: string;                  // "Context"
  modelAnswer?: string;              // "Model Answer"
  revealModelAnswer?: string;        // "Reveal Model Answer"

  // Mark Scheme slide
  markScheme?: string;               // "Mark Scheme"
  mark?: string;                     // "mark"
  marks?: string;                    // "marks"
  partialCredit?: string;            // "Partial Credit"

  // Worked Solution slide
  fullSolution?: string;             // "Full Solution"

  // Assessment slides
  assessment?: string;               // "Assessment"
  minutes?: string;                  // "Minutes"
  instructions?: string;             // "Instructions"
  allowedResources?: string;         // "Allowed Resources"

  // Generic slide
  noAdditionalContent?: string;      // "(No additional content)"
}

Collaboration

CollaborationProvider wraps your lesson UI to enable real-time multi-user sessions over WebSocket (powered by y-websocket). Teachers control slide navigation and whiteboard mode for all following students; students can follow the teacher or switch to free navigation.

import {
  LessonProvider,
  LessonWindow,
  SlideNavigation,
  PresentButton,
  FollowTeacherButton,
  SlideSidebar,
  SlideViewer,
  BlockSuiteCanvas,
  FloatingNavigation,
  NotesPanel,
} from "@aime.ai/renderer-react";
import {
  CollaborationProvider,
  type CollaborationRole,
} from "@aime.ai/renderer-react";

function CollaborativeLessonPage({ pack, sessionId, role, userId, userName }) {
  return (
    <LessonProvider pack={pack}>
      <CollaborationProvider
        sessionId={sessionId}
        role={role}
        userId={userId}
        userName={userName}
      >
        <SlideNavigation>
          <FollowTeacherButton />
          <PresentButton />
        </SlideNavigation>
        <LessonWindow>
          <SlideSidebar onBack={() => router.back()} />
          <SlideViewer />
          <BlockSuiteCanvas />
        </LessonWindow>
        <FloatingNavigation />
        <NotesPanel />
      </CollaborationProvider>
    </LessonProvider>
  );
}

Order mattersCollaborationProvider must be nested inside LessonProvider so it can read LessonPackContext and PresentationContext for slide-sync and whiteboard-mode sync.


CollaborationProvider

| Prop | Type | Default | Description | | --- | --- | --- | --- | | sessionId | string | — | Shared session identifier — becomes the y-websocket room name. | | role | CollaborationRole | "student" | "teacher", "student", or "observer". | | userId | string | auto-generated | Stable user identifier. Re-use across page reloads to preserve identity. | | userName | string | "Anonymous" | Display name shown in presence indicators. | | userColor | string | auto-generated | CSS color for this user's presence dot. | | roomWriteEnabled | boolean | false | Allow students to draw on the shared canvas. | | serverUrl | string | derived from window.location | y-websocket server URL, e.g. wss://host/yjs. Defaults to ws(s)://CURRENT_HOST/yjs so it works through any tunnel automatically. | | provider | WebsocketProvider | — | Inject a pre-existing WebsocketProvider (BYO transport). |


CollaborationRole

type CollaborationRole = "teacher" | "student" | "observer";
  • teacher — controls slide navigation and whiteboard mode for all following students. Has write access to all canvas layers.
  • student — follows the teacher by default. Can switch to free navigation via FollowTeacherButton. Canvas write access is gated by roomWriteEnabled.
  • observer — receives teacher navigation like a student but cannot write to any canvas layer.

FollowTeacherButton

A toggle button for students that switches between following teacher mode (teacher controls navigation) and free navigation mode (student navigates independently). Renders null when used outside a CollaborationProvider. Place it inside SlideNavigation.

<SlideNavigation>
  <FollowTeacherButton />
  <PresentButton />
</SlideNavigation>

No props — reads and writes CollaborationContext directly. Labels are i18n-aware via LessonLabels (followingTeacher, followingTeacherTitle, freeNavigation, freeNavigationTitle, modeControlledByTeacher).


useCollaboration

import { useCollaboration } from "@aime.ai/renderer-react";

function TeacherControls() {
  const {
    sessionId,         // string
    role,              // CollaborationRole
    userId,            // string
    userName,          // string
    userColor,         // string
    awareness,         // AwarenessInstance | null
    peers,             // PeerState[]
    permissions,       // CollaborationPermissions
    setPermissions,    // (partial: Partial<CollaborationPermissions>) => void
    followTeacher,     // boolean (students only)
    setFollowTeacher,  // (v: boolean) => void
    getLayer,          // (scope: string) => Y.Doc
    canWrite,          // (scope: string) => boolean
    serverUrl,         // string
  } = useCollaboration();
}

Use useCollaborationOptional() if the component might render outside a provider — returns null instead of throwing.


Peer awareness

Each connected peer publishes a PeerState object via y-websocket awareness. Read the live peer list from peers in the context:

interface PeerState {
  userId: string;
  userName: string;
  userColor: string;
  role: CollaborationRole;
  slideIndex: number;
  followTeacher: boolean;
  isWhiteboardMode: boolean;
  permissions?: CollaborationPermissions; // teacher only
}

CollaborationPermissions

interface CollaborationPermissions {
  /** Allow students to draw on the shared room canvas. */
  roomWriteEnabled: boolean;
}

Teachers can update permissions at runtime — student clients automatically adopt the new values via awareness:

const { setPermissions } = useCollaboration();

// Grant students canvas write access mid-session
setPermissions({ roomWriteEnabled: true });

Canvas sync in collab mode

When a sessionId is present, BlockSuiteCanvas automatically creates a second y-websocket room ({sessionId}-canvas) for the edgeless Yjs doc. All strokes, shapes, and stickers replicate in real time across all peers.

The teacher's pan/zoom viewport is broadcast via awareness and applied on students who are following the teacher.

Students without roomWriteEnabled see a transparent read-only overlay on the canvas — they cannot interact with it but still see all remote changes live.


Canvas Persistence

BlockSuiteCanvas exposes a raw Yjs binary API so you can store annotations wherever you like.

import { useState, useCallback } from "react";
import {
  LessonProvider,
  LessonWindow,
  SlideViewer,
  BlockSuiteCanvas,
} from "@aime.ai/renderer-react";
import type { LessonPackOutput } from "@aime.ai/renderer-react";

function LessonPage({ pack }: { pack: LessonPackOutput }) {
  // Restore from JSON field on the lesson pack
  const initialData = pack.canvasData
    ? new Uint8Array(pack.canvasData)
    : undefined;

  const handleSave = useCallback(
    (data: Uint8Array) => {
      // Convert to number[] for JSON serialisation, then PATCH your API
      saveLessonCanvas(pack.id, Array.from(data));
    },
    [pack.id],
  );

  return (
    <LessonProvider pack={pack}>
      <LessonWindow>
        <SlideViewer />
        <BlockSuiteCanvas initialData={initialData} onSave={handleSave} />
      </LessonWindow>
    </LessonProvider>
  );
}

LessonPackOutput includes a canvasData?: number[] field as a convenience slot — populate it from your API response and the component handles the rest.

If you need to serialize a doc manually (e.g. for an export flow), use the serializeDoc utility:

import { createSlideDoc, serializeDoc } from "@aime.ai/renderer-react";

const doc = createSlideDoc(pack.slides);
const bytes = serializeDoc(doc); // Uint8Array

Types Reference

LessonPackOutput

interface LessonPackOutput {
  id: string;
  lesson_intent_id: string;
  version: number;
  is_active: boolean;
  status: LessonPackStatus; // "completed" | "generating" | "starting" | "failed"
  lesson_type: LessonType;
  title: string;
  subject: string;
  grade_level: string;
  total_duration_minutes: number;
  ai_rationale: string;
  file_url: string | null;
  created_at: string;
  updated_at: string;
  slides: AnySlide[];
  resources: string[];
  canvasData?: number[]; // Persisted Yjs state for the whiteboard
}

LessonType enum

enum LessonType {
  INTRODUCTION = "INTRODUCTION",
  PRACTICE = "PRACTICE",
  DEEP_DIVE = "DEEP_DIVE",
  REVIEW = "REVIEW",
  ASSESSMENT = "ASSESSMENT",
  CONSOLIDATION = "CONSOLIDATION",
}

Slide types

Every slide extends BaseSlide and has a discriminating slide_type field:

| slide_type | Type | | --------------------------- | ----------------------------- | | "TITLE" | TitleSlide | | "LEARNING_OBJECTIVES" | LearningObjectivesSlide | | "HOOK" | HookSlide | | "PRIOR_KNOWLEDGE" | PriorKnowledgeSlide | | "TEACH" | TeachSlide | | "WORKED_EXAMPLE" | WorkedExampleSlide | | "GUIDED_PRACTICE" | GuidedPracticeSlide | | "STUDENT_TASK" | StudentTaskSlide | | "WARM_UP" | WarmUpSlide | | "DISCUSSION" | DiscussionSlide | | "WORKED_SOLUTION" | WorkedSolutionSlide | | "MISCONCEPTION" | MisconceptionSlide | | "SELF_ASSESSMENT" | SelfAssessmentSlide | | "CONCEPT_MAP" | ConceptMapSlide | | "SYNTHESIS_TASK" | SynthesisTaskSlide | | "SUMMARY" | SummarySlide | | "EXIT_TICKET" | ExitTicketSlide | | "ASSESSMENT_INSTRUCTIONS" | AssessmentInstructionsSlide | | "ASSESSMENT_QUESTION" | AssessmentQuestionSlide | | "MARK_SCHEME" | MarkSchemeSlide |

The union type AnySlide covers all of the above. Use a switch on slide_type for exhaustive handling.

BaseSlide

interface BaseSlide {
  slide_number: number;
  title: string;
  teacher_notes: string;
  duration_minutes: number;
  teacher_only: boolean;
  micro_objective_ids: number[];
  sen_support: string | null;
  image_url: string | null;
}

Peer Dependencies

The following packages must be installed alongside this library:

{
  "@blocksuite/block-std": "0.19.5",
  "@blocksuite/blocks": "0.19.5",
  "@blocksuite/icons": "2.1.75",
  "@blocksuite/presets": "0.19.5",
  "@blocksuite/store": "0.19.5",
  "lit": "^3.0.0",
  "react": "^18.0.0 || ^19.0.0",
  "react-dom": "^18.0.0 || ^19.0.0",
  "yjs": "^13.0.0"
}

Install them all at once:

pnpm add @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] @blocksuite/[email protected] lit yjs

Currently, two official plugins are available:

React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.

Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

export default defineConfig([
  globalIgnores(["dist"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ["./tsconfig.node.json", "./tsconfig.app.json"],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
]);

You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:

// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";

export default defineConfig([
  globalIgnores(["dist"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs["recommended-typescript"],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ["./tsconfig.node.json", "./tsconfig.app.json"],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
]);